智能合約安全審計入門系列之拒絕服務

作者:小白

封面: Photo by GuerrillaBuzz Crypto PR on Unsplash

背景概述

在上次的文章中我們學習了智能合約中獲取隨機數常用的幾種方式,以及他們的優缺點,還介紹了偽隨機數在智能合約中會造成哪些危害,這次我們來了解一個既存在於傳統網絡安全又存在於智能合約安全中的問題——拒絕服務。

前置知識

傳統網絡安全拒絕服務攻擊(DoS):DoS 是 Denial of service 的簡稱,即拒絕服務,任何對服務的干涉,使得其可用性降低或者失去可用性均稱為拒絕服務。常見的針對網絡協議造成拒絕服務的攻擊手段大致有以下幾種:SYN Flood,IP 欺騙性攻擊,UDP 洪水攻擊,Ping 洪流攻擊,Teardrop 攻擊,Land 攻擊,Smurf 攻擊,Fraggle 攻擊等。

智能合約拒絕服務攻擊:可以導致智能合約無法正常使用的代碼邏輯錯誤,兼容性錯誤或調用深度過大(區塊鏈虛擬機的特性)的安全問題。智能合約中的拒絕服務攻擊手法就相對比較簡單,包括但不限於以下三種:

1、基於代碼邏輯的拒絕服務攻擊:這種類型的拒絕服務攻擊一般情況下是因為合約代碼邏輯的不嚴謹造成的,最典型的就是當合約中存在對傳入的映射或數組循環遍歷的邏輯且沒有限制傳入的映射或數組的長度時攻擊者可以通過傳入超長的映射或者數組進行循環遍歷而大量消耗 Gas 從而該筆交易的 Gas 溢出,最後使得智能合約暫時或永久不可操作。

2、基於外部調用的拒絕服務攻擊:這種拒絕服務攻擊是建立在合約中對外部調用處理不當導致的。例如智能合約中存在基於外部函數執行的結來改變合約狀態且沒有對交易一直失敗的情況做出處理,攻擊者會利用這個特點故意使交易失敗,智能合約則會一直重複這筆失敗的交易從而造成智能合約邏輯卡在這裡不能繼續執行,最後使得智能合約暫時或永久不可操作。

3、基於運營管理的拒絕服務攻擊:這種拒絕服務攻擊就是建立在後期運營情況下,例如在智能合約中通常會存在以 Owner 賬戶作為管理員角色,該角色通常會持有很高的權限,例如開啟或暫停轉賬功能,當 Owner 角色操作失誤或私鑰丟失可能會受到非主觀意義上的拒絕服務攻擊。

漏洞示例

通過前置知識相信大家已經對拒絕服務這類攻擊有一定的了解了,在觸發拒絕服務攻擊的三種手法中最典型的就是基於外部調用的拒絕服務攻擊。下面我們就通過一段典型的代碼示例來帶大家深入了解:

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;contract KingOfEther {    address public king;uintpublic balance;function claimThrone() external payable {        require(msg.value > balance, "Need to pay more to become the king");        (bool sent, ) = king.call{value: balance}("");        require(sent, "Failed to send Ether");        balance = msg.value;        king = msg.sender;    }}

漏洞分析

我們可以看到上述合約的目的是選取 “以太之王”,玩家可以通過 claimThrone() 合約中打入大於之前用戶的任意數量的以太幣來競爭 “以太之王” 的稱號,當打入的以太幣高於之前玩家時打入的以太幣留在合約中並獲得 “以太之王” 稱號,之前玩家的以太幣會原路退回。

我們可以看到,生成新王和退回舊王的邏輯是在同一函數內完成的,並且 claimThrone() 中還檢查了退款的返回值 sent,下面我們來結合這個特點來完成攻擊。

攻擊合約

注:以下合約代碼邏輯以及攻擊場景僅作演示示例,請勿胡亂聯想。

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;
contract Attack { KingOfEther kingOfEther;
constructor(KingOfEther _kingOfEther) { kingOfEther = KingOfEther(_kingOfEther); }
function attack() public payable { kingOfEther.claimThrone{value: msg.value}(); }}

首先我們先來分析攻擊流程:

1. Alice 部署 KingOfEther 合約。

2. Alice 調用 KingOfEther.claimThrone() 發送 1 個以太到 KingOfEther 合約中成為 “以太之王”。

3. 高富帥 Bob 調用 KingOfEther.claimThrone() 發送 2 個以太到 KingOfEther 合約中成為新王。

4. Alice 收到 1 個以太幣的退款。

5. Eve 使用 KingOfEther 的地址部署攻擊合約 Attack。

6. Eve 調用 Attack.attack() 向 KingOfEther 合約中發送 3 個以太。

7. Attack 合約成為新王。

8. 高富帥 Bob 覺得不服,再次調用 KingOfEther.claimThrone() 向 KingOfEther 合約中發送了 20 個以太展現自己的 “鈔能力”。

9. Bob 發現自己的交易一直被 revert,無法成為新王。至此,Eve 的攻擊使 KingOfEther 合約永久失效,Attack 合約成為了永遠的 “以太之王”。

高富帥 Bob 覺得不可思議,為啥自己這麼有錢還不能稱王呢?我們來看看到底是為什麼。

當 Bob 調用 KingOfEther.claimThrone()  發送 20 個以太到 KingOfEther 合約時會觸發 KingOfEther.claimThrone()  的退款邏輯,將之前 Eve 通過 Attack.attack()  向 KingOfEther 合約中發送的 3 個以太原路退回到 Attack 合約。我們再來看 Attack 合約,該合約中沒有實現 payable  的 fallback()  所以不能接收以太幣,這將導致 KingOfEther.claimThrone()  的退款邏輯一直失敗,退款返回值 sent  將一直為 false  無法通過 require(sent, "Failed to send Ether")  檢查一直被 revert。因為只要觸發退款就會被 revert 導致 KingOfEther 合約中繼 Attack 合約後無人能成為新王,Eve 成功完成了拒絕服務攻擊。

修復建議

作為開發者

1. 在智能合約開發中應當注意處理連續失敗的情況,例如將可能出現失敗的外部調用邏輯異步處理。

2. 在使用 call 進行外部調用以及使用循環和遍歷時應當注意 Gas 消耗。

3. 避免對單個角色過度授權的情況,處理合約權限時應做到合理的權限劃分,對擁有權限的角色使用多簽錢包管理,防止由於私鑰洩漏導致權限丟失。

下面是針對上面漏洞合約的修復示例:

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;
contract KingOfEther { address public king; uint public KingValue; mapping(address => uint) public balances;
function claimThrone() external payable { balances[msg.sender] += msg.value;
require(balances[msg.sender] > balance, "Need to pay more to become the king"); KingValue = balances[msg.sender]; king = msg.sender; }
function withdraw() public { require(msg.sender != king, "Current king cannot withdraw");
uint amount = balances[msg.sender]; balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}(""); require(sent, "Failed to send Ether"); }}

可以看到修復合約中添加了 balances 映射,它記錄了每個人向合約中打入以太的總數量相較於之前合約的優勢是玩家失去王位後可以追加以太重新獲得王位。修復版本的關鍵點是將退款邏輯作異步處理,需要玩家手動調用 withdraw()  來自助退款,就算遇到惡意玩家拒收以太也只能影響到自己,不會再造成之前的拒絕服務了。

作為審計者

  • 內部合約進行分析:

1. 注意合約中是否存在邏輯上的錯誤導致影響了可用性。

2. 注意是否存在由於虛擬機調用深度過大導致的 DoS(深度最大 1024)。

3. 重點關注在代碼邏輯中是否存在大量消耗 Gas 的邏輯。

  • 外部合約進行分析:

1. 關注與外部合約進行交互的時候沒有考慮好兼容性的問題,如:未處理 TRC20-USDT 的返回值的兼容性,導致代幣被鎖定。

2. 重點檢查有沒有判斷外部合約調用的返回值是否符合預期的效果。

  • 對權限管理進行分析:

在審計中需要檢查和確認所有函數方法的可見性及調用權限,需要結合項目方提供設計文檔在審計中根據設計文檔中的描述一一確認權限。如果發現過度授權或權限劃分不清晰的需要與項目方交流改進方法,並且與項目方溝通運營操作的流程,確保流程上能夠避免合約在運營的時候管理員操作失誤或配置錯誤的情況。

注:本文參考於《Solidity by Example》參考鏈接:

https://solidity-by-example.org/hacks/randomness

免責聲明:作為區塊鏈信息平台,本站所發布文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。本文內容僅用於信息分享,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。