【精通以太坊】——第九章 智慧合約安全
- 2022 年 5 月 2 日
- 筆記
智慧合約安全
安全最佳實踐
最小化/簡單化
程式碼重用
程式碼品質
因為你處在航天工程那樣或其他類似的零容錯的工程領域之中
可讀性和可審計性
智慧合約是公開的,任何人都可以獲得其位元組碼並進行反向工程
因此智慧合約很契合在開源社區協作開發
測試覆蓋率
儘可能測試所有情況
安全風險
重入
漏洞
外部的惡意合約通過函數調用來重新進入合約程式碼執行過程。通過使用回退函數來重入合約函數。
防範技術
- 儘可能使用內置的transfer函數向外部合約發送以太幣。transfer函數只會提供多餘的2300gas,這些ETH只夠用來用於執行外部合約發送ETH,沒有多餘的gas可以用來重入合約
- 所有對狀態變數的修改都在向其他合約發送以太幣之前來執行
- 引入互斥鎖,增加一個狀態變數來在程式碼執行中鎖定合約,避免重入的調用
整數溢出
漏洞
比如unit8範圍是[0,255],用來保存256時就溢出變為了0,257變為1,以此類推。類似於pwn中的整數溢出漏洞。在solidity中只有整數,因此要注意加減乘等等的溢出,包括上溢下溢。除法不會導致溢出,但除以0時會拋出異常
防範技術
使用或構建安全的算數運算的庫合約來代替標準的算數操作,如使用OpenZeppelin的SafeMath.sol
意外的以太幣
漏洞
並不是所有的向一個合約傳ETH的動作都會調用合約函數來處理。兩種方法:1.合約的析構函數執行時向外部轉ETH不會引發任何函數的執行,包括回退函數。2.在合約註冊生效之前,向合約地址發送ETH。這種手法的基礎是,合約的地址是可以被確定的,可以被預知。攻擊者向合約發送以太幣,導致合約的初始數值非零,產生一些負面影響。
防範技術
避免依賴合約餘額的具體數值(this.balance),想要儲存一個合約的充值數額應該自定義變數在payable函數記錄數額變動。
DELEGATECALL
DELEGATECALL是運行在調用合約的上下文,CALL則是運行在目標合約的上下文
漏洞
可能導致非預期的程式碼執行結果
防範技術
使用DELEGATECALL要非常仔細地注意庫合約和主調用合約的可能的調用上下文
並儘可能構建無狀態的庫合約
默認的可見性
漏洞
solidity中,函數默認可見性是public,因此如果函數不指定可見性,函數就是可以被外部用戶調用的
防範技術
對合約中的所有函數明確指定可見性
無序錯覺
漏洞
區塊鏈是一個確定性的系統,任何來自於區塊鏈系統內部的隨機數都是偽隨機的。這意味著可以被人為控制。包括哈希值、時間戳、區塊號或者gas上限。一個例子是,如果使用未來的區塊哈希值判定賭局,礦工可以不發布不利於自己的區塊,而是重新打包區塊直到得到有利的新的區塊哈希值再發布。如果使用過去的變數更加災難,因為這些對所有人都是透明的,也就更容易被攻擊者知曉
防範技術
無序性(隨機性)必須來自區塊鏈外部,如使用RandDAO
外部合約引用
漏洞
在部署時更改外部合約引用,從而運行攻擊者自己的程式碼
防範技術
- 使用new創建引用的合約,這樣以來部署的用戶也無法在不更改程式碼的情況下替換改引用合約。一旦程式碼發生變化,合約位元組碼就發生變化,合作用戶就能發現程式碼被篡改過
- 對外部合約地址進行硬編碼。將外部合約地址設定為public,用戶就可以輕鬆檢查合約所引用的外部合約,否則則認為可能存在問題。
短地址/參數攻擊
漏洞
智慧合約中,如果傳遞的參數不符合ABI規範,EVM會自動使用0來補齊缺失的位。攻擊者故意傳一個位數少了的地址參數,讓沒有檢查正確性的合約使用0自動補齊,進而產生不良影響。
防範技術
所有外部應用在把輸入參數發送到區塊鏈之前都應該對它們進行校驗,包括參數的順序等都同樣扮演著重要角色
未檢查的調用返回值
漏洞
Call和send函數會返回一個布爾值來指明調用是否成功。即使外部調用失敗,執行了這些函數的那個交易也不會revert回滾
防範技術
調用call和send函數後檢查布爾值,以分別處理成功以及失敗的場景。另外盡量使用transfer函數來執行傳輸
競爭條件/預先交易
漏洞
攻擊者監視交易池中的交易,如果交易中包含了對某個問題的答案,那麼攻擊者可以去修改或者撤銷解決者的許可權,或者把合約修改為對解決者不利的狀態。然後,攻擊者從解決者的交易中獲取數據,並用更高的gasPrice創建他們自己的交易,這樣他們的交易就會優先於原始交易被打包到區塊中。
直白點:攻擊者作弊偷了答案,並用更高的gasPrice讓自己優先交卷。產生這種攻擊的原因是交易打包按gasPrice排序,gas高者會被先打包。
防範技術
有兩種角色可以發動攻擊,用戶(通過修改交易的gasPrice)和礦工(礦工可以按任意順序隨意打包交易,但只有在挖到礦的時候才能攻擊成功)。
- 為了防範用戶的攻擊,可以將gasPrice設置為上限,這樣可以防止其他用戶提高gasPrice來競爭打包。但對礦工無效,因為礦工可以以任意順序打包交易。
- 提示-揭示策略:發送帶有隱秘資訊的交易(通常是一個哈希值),當交易被包含到區塊後再發送一個交易揭示先前發送的數據。如ENS智慧合約允許用戶先提交它們希望話費的以太幣數量,同時附帶任意數量的以太幣,然後在揭示階段,用戶可以獲得以太幣返還
- 水底發送策略:NULL
拒絕服務
漏洞
- 基於可被外部操縱的映射或數組的循環:Distribute向多個賬戶分發代幣時,攻擊者創建過多的賬戶,使該合約的gas消耗超過gasLimit
- 主人的操作:必須經過擁有特權的主任來進行某些操作才能進入下一步狀態,如果主人丟失了私鑰,這個合約就進行不了下一步了
- 基於外部調用來修改狀態:比如創建一個不接受轉賬的合約,而新的狀態需要先將以太幣被全部取走才能進入下一步
防範技術
- 第一個例子,合約不應該基於一個可以被外部用戶人為操作的數據結構來執行循環
這裡推薦使用取回模式,只能讓取款人單獨地調用withdraw函數來取回他們各自的代幣 - 第二、三個例子,可以使用時間解鎖機制。保證意外發生,也可在到期時自動進行下一步
區塊時間戳操縱
漏洞
礦工是可以操縱時間戳的,但是時間戳必須單項正增長,同時礦工也不能指定過遠的將來時間戳。
防範技術
時間戳不應該被用來作為無序數據或生成隨機數,或者說不能用來作為重要的狀態變動。如果確實有需要時間相關邏輯,則應該使用block.number和平均區塊時間來估算(一個區塊大約是10s)。或是簡單指定一個區塊號作為條件。
小心使用構造函數
漏洞
如果合約名稱被改動後,構造函數忘記修改,就會變成普通的可被調用的public函數,從而引發攻擊
防範技術
0.4.22版本的solidity編譯器引入了constructor關鍵字來制定構造函數,因此此漏洞已經不存在了
未初始化的存儲指針
漏洞
EVM是用存儲或記憶體來保存數據的,不適當的變數初始化可能會產生有漏洞的合約
防範技術
注意為初始化的存儲變數警告
在處理複雜數據類型時嚴格使用memory或storage標識符
努力讓行為符合預期
浮點數和精度
漏洞
舊版solidity沒有浮點型,需要特殊處理
防範技術
有時候先乘後除精度會更高
Tx.Origin驗證
Solidity中的一個全局變數,它會回溯整個調用棧並返回最初發起這個調用或交易的賬戶地址
漏洞
如果受害者的合約A是通過tx.origin==owner來授權提取餘額的,那攻擊者可以編寫攻擊合約B,誘導受害者調用合約B(如在將合約B偽造成外部賬戶在社交活動中釣魚),而實際上合約B調用了合約A(前提是轉入了足夠的gas),從而將合約A中的餘額轉入攻擊者賬戶上
防範技術
智慧合約不應該使用tx.origin來進行驗證授權。Tx.origin的合理用法如:可以用來拒絕外部合約調用當前合約