知道創宇區塊鏈安全實驗室|深入理解重入攻擊漏洞

買賣虛擬貨幣

前言


智慧合約(英文:smart contract )的概念於 1995 年由 nick szabo 首次提出,它是一種旨在以資訊化方式傳播、驗證或執行合同的計算機協議,它允許在沒有第三方的情況下進行可信交易,這些交易可追蹤且不可逆轉。然而智慧合約也並非是安全的,其中 重入 (re-entrance) 攻擊漏洞是以太坊中的攻擊方式之一,早在 2016 年就因為 the dao 事件而造成了以太坊的硬分叉。


漏洞概述


在以太坊中,智慧合約能夠呼叫其他外部合約的程式碼,由於智慧合約可以呼叫外部合約或者傳送以太幣,這些操作需要合約提交外部的呼叫,所以這些合約外部的呼叫就可以被攻擊者利用造成攻擊劫持,使得被攻擊合約在任意位置重新執行,繞過原始碼中的限制條件,從而發生重入攻擊。重入攻擊本質上與程式設計裡的遞迴呼叫類似,所以當合約將以太幣傳送到未知地址時就可能會發生。


簡單的來說,發生重入攻擊漏洞的條件有 2 個:

  • 呼叫了外部的合約且該合約是不安全的
  • 外部合約的函式呼叫早於狀態變數的修改


下面給出一個簡單的程式碼片段示例:

上述程式碼片段就是最簡單的提款操作,接下來會給大家詳細分析重入攻擊造成的原因。


漏洞分析


在正式的分析重入攻擊之前,我們先來介紹幾個重點知識。


01 轉賬方法

由於重入攻擊會傳送在轉賬操作時,而 solidity 中常用的轉賬方法為

<address>.transfer(),<address>.send() 和 <address>.gas().call.vale()(),下面對這 3 種轉賬方法進行說明:

  • <address>.transfer():只會傳送 2300 gas 進行呼叫,當傳送失敗時會透過 throw 來進行回滾操作,從而防止了重入攻擊。
  • <address>.send():只會傳送 2300 gas 進行呼叫,當傳送失敗時會返回布林值 false,從而防止了重入攻擊。
  • <address>.gas().call.vale()():在呼叫時會傳送所有的 gas,當傳送失敗時會返回布林值 false,不能有效的防止重入攻擊。


02 fallback 函式

接著我們來講解下 fallback 回退函式。
回退函式 (fallback function):回退函式是每個合約中有且僅有一個沒有名字的函式,並且該函式無引數,無返回值,如下所示:

回退函式在以下幾種情況中被執行: * 呼叫合約時沒有匹配到任何一個函式; * 沒有傳資料; * 智慧合約收到以太幣(為了接受以太幣,fallback 函式必被標記為 payable)。

03 漏洞程式碼

下面的程式碼就是存在重入攻擊的,實現的是一個類似於公共錢包的合約,所有的使用者都可以使用 deposit() 存款到 reentrance 合約中,也可以從 reentrance 合約中使用 withdraw() 進行提款,當然了所有人也可以使用 balanceof() 查詢自己或者其他人在該合約中的餘額。

首先使用一個賬戶 (0x5b38da6a701c568545dcfcb03fcb875f56beddc4) 扮演受害者,將該合約在 remix ide 點選 deploy 按鈕進行部署。

在部署合約成功後在 value 設定框中填寫 5,將單位改成 ether,點選 deposit 存入 5 個以太幣。

點選 wallet 檢視該合約的餘額,發現餘額為 5 ether,說明我們的存款成功。

而下面的程式碼則是針對上面存在漏洞的合約進行的攻擊:

使用另外一個賬戶 (0xab8483f64d9c6d1ecf9b849ae677dd3315835cb2) 扮演攻擊者,複製存在漏洞的合約地址到 deploy 的設定框內,點選 deploy 部署上面的攻擊合約。

部署成功後先呼叫 wallet() 函式檢視攻擊合約的餘額為 0。

攻擊者先存款 1 ether 到漏洞合約中,這裡設定 value 為 1 ether,之後點選攻擊合約的 deposit 進行存款。

再次呼叫合約的 wallet 函式檢視漏洞合約的餘額,發現已經變成了 6 ether。

攻擊者 (0xab8483f64d9c6d1ecf9b849ae677dd3315835cb2) 呼叫攻擊合約的 attack 函式模擬攻擊,之後呼叫被攻擊合約的 wallet 函式去檢視合約的餘額,發現已經歸零,此時回到攻擊合約檢視餘額,發現被攻擊合約中的 6 ether 已經全部提款到了攻擊者合約中,這就造成了重入攻擊。


04原始碼分析

上面講解了如何進行重入攻擊已經漏洞原因,這裡梳理了漏洞原始碼和攻擊的步驟,列出了關鍵程式碼。


相關案例


2016 年 6 月 17 日,thedao 專案遭到了重入攻擊,導致了 300 多萬個以太幣被從 thedao 資產池中分離出來,而攻擊者利用 thedao 智慧合約中的 splitdao() 函式重複利用自己的 dao 資產進行重入攻擊,不斷的從 thedao 專案的資產池中將 dao 資產分離出來並轉移到自己的賬戶中。


下列程式碼為 splitdao() 函式中的部分程式碼,原始碼在 tokencreation.sol 中,它會將代幣從 the parent dao 轉移到 the child dao 中。平衡陣列 uint fundstobemoved = (balances[msg.sender] * p.splitdata[0].splitbalance) / p.splitdata[0].totalsupply 決定了要轉移的代幣數量。

下面的程式碼則是進行提款獎勵操作,每次攻擊者呼叫這項功能時 p.splitdata[0] 都是一樣的(它是 p 的一個屬性,即一個固定的值),並且 p.splitdata[0].totalsupply 與 balances[msg.sender] 的值由於函式順序問題,發生在了轉賬操作之後,並沒有被更新。

paidout[_account] += reward 更新狀態變數放在了問題程式碼 payout 函式呼叫之後。

對_recipient 發出 .call.value 呼叫,轉賬_amount 個 wei,.call.value 呼叫預設會使用當前剩餘的所有 gas。


解決辦法


透過上面對重入攻擊的分析,我們可以發現重入攻擊漏洞的重點在於使用了 fallback 等函式回撥自己造成遞迴呼叫進行迴圈轉賬操作,所以針對重入攻擊漏洞的解決辦法有以下幾種。


01使用其他轉賬函式

在進行以太幣轉賬傳送給外部地址時使用 solidity 內建的 transfer() 函式,因為 transfer() 轉賬時只會傳送 2300 gas 進行呼叫,這將不足以呼叫另一份合約,使用 transfer() 重寫原合約的 withdraw() 如下:


02先修改狀態變數

這種方式就是確保狀態變數的修改要早於轉賬操作,即 solidity 官方推薦的檢查-生效-互動模式 (checks-effects-interactions)。


03使用互斥鎖

互斥鎖就是新增一個在程式碼執行過程中鎖定合約的狀態變數以防止重入攻擊。


04使用 openzeppelin 官方庫

openzeppelin 官方庫中有一個專門針對重入攻擊的安全合約:

https://github.com/openzeppelin/openzeppelin-contracts/blob/master/contracts/security/reentrancyguard.sol


// spdx-license-identifier: mit

pragma solidity ^0.8.0;

/** 
* @dev contract module that helps prevent reentrant calls to a function. 

* inheriting from `reentrancyguard` will make the {nonreentrant} modifier 
* available, which can be applied to functions to make sure there are no nested 
* (reentrant) calls to them. 
* note that because there is a single `nonreentrant` guard, functions marked as 
* `nonreentrant` may not call one another. this can be worked around by making 
* those functions `private`, and then adding `external` `nonreentrant` entry * points to them. 
* tip: if you would like to learn more about reentrancy and alternative ways 
* to protect against it, check out our blog post 
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[reentrancy after istanbul]. 
*/
abstract contract reentrancyguard {
    // booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra sload to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. this is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // the values being non-zero value makes deployment a bit more expensive, 
   // but in exchange the refund on every call to nonreentrant will be lower in
    // amount. since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _not_entered = 1;
    uint256 private constant _entered = 2;

    uint256 private _status;

    constructor () {
        _status = _not_entered;
    }

    /**
     * @dev prevents a contract from calling itself, directly or indirectly. 
     * calling a `nonreentrant` function from another `nonreentrant`
     * function is not supported. it is possible to prevent this from happening
     * by making the `nonreentrant` function external, and make it call a 
     * `private` function that does the actual work.
     */
    modifier nonreentrant() {
        // on the first call to nonreentrant, _notentered will be true
        require(_status != _entered, "reentrancyguard: reentrant call");

        // any calls to nonreentrant after this point will fail
        _status = _entered;

        _;

        // by storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/eips/eip-2200)
        _status = _not_entered;
    }
}

參考文獻


1. 以太坊的幾次硬分叉:

https://zhuanlan.zhihu.com/p/111446792

2. 以太坊智慧合約安全漏洞 (1):重入攻擊:

https://blog.csdn.net/henrynote/article/details/82119116

3. 區塊鏈的那些事 — the dao 攻擊事件原始碼分析:

https://blog.csdn.net/fly_hps/article/details/83095036



知道創宇區塊鏈安全實驗室官網:www.knownseclab.com


免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读