智能合約安全審計入門系列之delegatecall。
By:小白@慢霧安全團隊
背景概述
上篇文章中我們了解了什麼是delegatecall 函數以及一個基礎的漏洞,這篇文章的目的是加深一下大家對delegatecall 的印象並帶大家一起去玩點刺激的,拿下一個進階版的漏洞合約。
前置知識
這裡就不再重複之前的基礎知識了,不了解或者遺忘的可以再看看上一篇文章:《智能合約安全審計入門篇—— delegatecall (1)》。
漏洞示例
contract Lib { uint public someNumber; function doSomething(uint _num) public { someNumber = _num; }}contract HackMe { address public lib; address public owner; uint public someNumber; constructor(address _lib) { lib = _lib; owner = msg.sender; } function doSomething(uint _num) public { lib.delegatecall(abi.encodeWithSignature(“doSomething(uint256)”, _num)); }}
漏洞分析
這次的攻擊目標依然是獲得HackMe 合約中的owner 權限,我們可以看到兩個合約中除了HackMe 合約中的構造函數可以修改合約的owner 其他地方並沒有修改owner 的函數。我們要如何完成攻擊呢?這裡需要一點小技巧,大家可以思考一下,剛好也可以驗證一下自己對於之前知識的掌握程度以及自己的思維是否活躍。
是否有想法呢?沒有想法也沒關係,我們一起來看攻擊是如何完成的:
攻擊合約
// SPDX-License-Identifier: MITpragma solidity ^0.8.13;contract Attack { // Make sure the storage layout is the same as HackMe// This will allow us to correctly update the state variablesaddress public lib; address public owner; uint public someNumber; HackMe public hackMe; constructor(HackMe _hackMe) { hackMe = HackMe(_hackMe); } function attack() public { // override address of libhackMe.doSomething(uint(uint160(address(this)))); // pass any number as input, the function doSomething() below will// be calledhackMe.doSomething(1); } // function signature must match HackMe.doSomething()function doSomething(uint _num) public { owner = msg.sender; }}
我們先看攻擊流程:
1. Alice 部署Lib 合約;
2. Alice 部署HackMe 合約並在構造函數中傳入Lib 合約的地址;
3. 攻擊者Eve 部署Attack 合約並在構造函數中傳入HackMe 合約的地址;
4. 攻擊者調用Attack.attack() 函數將HackMe 合約中的owner 變為自己。
咋回事兒呢?其實這個攻擊方式就是很巧妙的運用了delegatecall 這個函數修改storage 類型變量時的特徵:delegatecall 函數的執行環境是調用者的環境並且對於storage 類型變量的修改是根據被調用合約變量存儲的插槽位置來修改的。
1. Attack.attack() 函數先將自己的地址轉換為uint256 類型(這一步是為了兼容目標合約中的數據類型)第一次調用HackMe.doSomething() 函數;
2. HackMe.doSomething() 函數使用delegatecall 函數帶著傳入的Attack 合約的地址調用了Lib.doSomething() 函數;
3. 可以看到Lib.doSomething() 函數將合約中存儲位置為slot0 的參數改為傳入的值,這樣當HackMe 合約使用delegatecall 調用Lib.doSomething() 函數時也將改變自己在slot0 位置存儲的變量的值,也就是將lib 參數(這裡存儲的是Lib 合約的地址)改為我們傳入的Attack 合約的地址。此時之前在HackMe.lib 參數中存儲的Lib 合約的地址就被修改成我們傳入的Attack 合約的地址了;
4. Attack.attack() 函數再次調用HackMe.doSomething() 函數,由於在上一步我們已經將HackMe.lib 變量修改為Attack 合約的地址了,這時HackMe.doSomething() 函數將不再調用之前的Lib 合約而是用delegatecall 去調用Attack.doSomething() 函數。此時我們再來觀察Attack 合約的寫法,發現其變量的存儲位置故意和HackMe 合約保持一致,並且不難發現Attack.doSomething() 函數的內容也被攻擊者寫為owner = msg.sender,這個操作修改了合約中存儲位置為slot1 的變量。所以HackMe 合約使用delegatecall 調用Attack.doSomething() 函數就會將合約中存儲位置為slot1 的變量owner 修改為msg.sender 也就是Eve 的地址,至此攻擊者完成了他的攻擊。
修復建議
作為開發者
1. 在使用delegatecall 時應注意被調用合約的地址不能是可控的;
2. 在較為複雜的合約環境下需要注意變量的聲明順序以及存儲位置。因為使用delegatecall 進行外部調用時會根據被調用合約的數據結構來修改本合約相應slot 中存儲的數據,當數據結構發生變化時這可能會造成非預期的變量覆蓋。
作為審計者
1. 在審計過程中遇到合約中有使用delegatecall 時需要注意被調用的合約地址是否可控;
2. 當被調用合約中的函數存在修改storage 變量的情況時需要注意變量存儲插槽的位置,避免由於數據結構不一致而導致本合約中存儲的storage 變量被錯誤的覆蓋。
注:本文參考於《Solidity by Example》
參考鏈接:https://solidity-by-example.org/hacks/delegatecall
展開全文打開碳鏈價值APP 查看更多精彩資訊