當前位置:
首頁 > 新聞 > 警惕!Solidity缺陷易使合約狀態失控

警惕!Solidity缺陷易使合約狀態失控

本文以蜜罐合約和 BancorLender 合約為例,詳細介紹 Solidity 語言中「未初始化的 storage 指針」問題,並追蹤 Solidity 編譯器關於此問題的開發進展。

安比(SECBIT)實驗室在 BancorLender (0x2d820ea3A6b9302c500feeb7F6361bA1DdfA5aBa) 合約中發現野指針問題(uninitialized-wild-pointer)。該合約中的一個狀態變數會意外地被另一個函數修改,偏離原本設計意圖。目前項目方不明確。建議項目方應立即廢棄該合約,並重新發布修復後的合約。野指針問題是 Solidity 語言的最初設計欠缺考慮,而且 Solidity 編譯器為了向前兼容,對這類安全問題僅採取警告提示,而開發者往往又很容易忽視這些提示,最終導致問題代碼部署上線。

下面我們通過一個蜜罐例子來解釋「未初始化的 storage 指針」這個缺陷。


蜜罐合約:別人看中的是你的本金

在計算機領域,蜜罐(Honeypot)通常指故意偽裝成看似有利用價值並故意留有 bug 的系統,用來吸引黑客攻擊,從而達到分析、監控、收集證據、拖延攻擊等目的。

而以太坊主網上存在這樣一類遊戲合約:以高額回報為誘餌,並故意露出破綻,讓參與者誤認為自己有很高的概率可以獲勝,誘導參與者轉入以太參與遊戲而損失本金。通常稱這類合約為「蜜罐合約」。

「蜜罐」這個詞,其實很形象:罐子里有可口的蜂蜜,吸引著熊去吃,但周邊其實有暗藏的陷阱,真正目的是為了抓住熊。

「蜜罐合約」的部署者通常利用各種技巧使代碼部分特殊用途不易被參與者發現,利用當中的信息不對稱,使參與者產生錯誤判斷,從而被騙取本金。

「未初始化的 storage 指針」正是「蜜罐合約」部署者最常用的一種技巧。這個問題源於 Solidity 語言以及編譯器設計上的失誤。

我們結合下面這個名為 Honeypot 的簡化合約說明。這是一個競猜合約,參與者調用  介面,傳入  數字進行競猜,如果猜的數字等於合約中的 ,則競猜成功,參與者可獲取兩倍回報。

聰明的你可以仔細思考一下,競猜數字  應該填多少?

終極答案是 42 嗎?由於變數  在最開始(第 2 行)被賦值 42,並且沒有其他被賦值操作,因此絕大多數人都會猜 42。

然而這個合約極具迷惑性,42 並不是正確答案。到底哪裡出了問題?變數  什麼時候被修改了?

讓我們來理一理:函數  先把參與者的地址和競猜數字放入 gameHistory 數組中保存(第 12 ~ 15 行)。而數組  由  結構體(Struct)構成。函數開始先通過  聲明了一個結構體變數 ,再分別對成員變數進行賦值(第 13 ~ 14 行),最後將變數  塞到  數組中(第 15 行)。

看著「似乎」沒毛病。然而,這裡有很嚴重的問題。

傳統編程語言中,我們在函數內部申明一個變數,通常默認是局部變數。但 Solidity 在語言設計上埋了個坑,在此處反直覺地默認讓引用類型(Reference Type)變數 (第 12 行)存儲位置為 storage,因此對變數  的修改,作用範圍是「全局」的。並且對於未初始化的 storage 指針(類似傳統語言中的空指針),Solidity 默認其指向 storage 的起始地址,即指向合約開頭定義的狀態變數(第 2 ~ 3 行)。

變數  值不是 42,那麼到底是多少呢?

Solidity 將源碼中的狀態變數(常量除外),根據一定規則,按照出現順序依次排列存儲在 storage 中。

而  變數正是這個合約中第一個被定義的狀態變數,佔據了 storage 的開始位置(slot 0×00)。

game.player = msg.sender; game.number = _number;

因此以上代碼中的賦值操作會分別更新 storage slot 0×00 ~ 0×01 上的值,即將  值設為 ,將  值設為 。

如果參與者猜 42,則會白白丟幣。

的正確答案應該是調用者自己的地址。

安比(SECBIT)實驗室發現,有很多人會利用 Solidity 語言以及編譯器的這種「特性」,再加上其他複雜的干擾條件或故意漏出的破綻,部署「蜜罐」合約欺騙其他人。在大部分案例里,參與者根本無法獲勝,而部署者有許可權將合約里的幣全部轉走,並且通常中招者還具備不少智能合約安全常識。

再如另一個名為 OpenAddressLottery 的彩票合約(0x741F1923974464eFd0Aa70e77800BA5d9ed18902),根據參與者的地址「隨機」生成一個 0 至 7 間的整數。合約聲稱任何人均有八分之一的概率中獎而贏走 7 倍於投注金額的以太幣,中獎條件為生成的數等於代碼中的  [2]。

與第一個例子類似,代碼中標明了  值為 7(第 11 行),並且看上去沒有其他方法可以修改該變數。目前以太坊智能合約中很難生成無法預測的隨機數(其實這是部署者故意留的破綻)。有智能合約安全知識的人可能會躍躍欲試,利用在其他智能合約中調用的方法來預測隨機數,從而獲取獎勵(不可能的,這輩子都不可能)。

注意  函數中的 (第 16 行),這與前面的問題代碼如出一轍,並且該函數只有  才能調用。蜜罐部署者可利用該函數中第 20 行的  來修改  為想要的任意值,從而使任何人都無法中獎。蜜罐部署者最終利用  將合約自毀,並把受害者轉入的以太幣轉出至自己的地址。

類似的蜜罐合約在以太坊主網上存在不少(不完全列表如下),大家牢記這個知識點,千萬別中招。

0xd1915A2bCC4B77794d64c4e483E43444193373Fa 0x650734bfd0465b7c6cd2932ea555e721308fd0b3 0x0d83102ec81853f3334Bd2b9E9fcCE7adf96ccC7 0xe6f245bb5268b16c5d79a349ec57673e477bd015 0x787b9a8978b21476abb78876f24c49c0e513065e 0xd4342df2c7cfe5938540648582c8d222f1513c50 0xe19ca313512e0231340e778abe7110401c737c23 0x6324d9d0a23f5ddba165bf8cc61da455350895f2 0xEFba96262F277cC8073dA87e564955666D30a03b 0x6a2e025f43ca4d0d3c61bdee85a8e37e81880528


問題合約 BancorLender:從蜜罐到安全漏洞

除了「蜜罐合約」,「未初始化的 storage 指針」問題還會嚴重影響智能合約代碼質量,導致合約代碼無法正常執行,甚至留下安全漏洞。

結合 BancorLender 代碼具體分析。

BancorLender 合約  函數中聲明了一個結構體(struct)變數 。

顯然開發者原本想將  作為局部變數使用,但未初始化的 storage 指針會指向第 1035 行定義的狀態變數 。

作為由結構體  構成的動態數組, 變數佔據了 storage 的開始位置(slot 0×00),並按照動態數組的規則存放在 storage 上。

如果熟悉動態數組在 storage 上的排列方式 [1],則知道 slot 0×00 位置保存的是當前動態數組的大小,即  中的元素個數,而其他位置則依次保存的是數組中的實際值。

回到上面的問題代碼,在這裡,slot 0×00 被未初始化的  storage 指針所指向,因此,問題代碼中第 1051 行至 1054 行的賦值操作則會分別更新 storage slot 0×00 ~ 0×03 上的值。也就是說,slot 0×00 處原本存儲數組大小的值被設為 。這完全不合情理,使得代碼邏輯十分混亂,代碼的功能完全無法正常完成,在一些情況下會造成很嚴重的後果。

那麼,這裡正確的代碼究竟該如何寫?

其實很簡單,只需給第 1050 行代碼,加上  限定,即可標明  是局部變數,而不會影響到 storage 上的值。

BorrowAgreement memory agreement;

事實上,Solidity 編譯器對於這種「常見」錯誤寫法有警告,提示開發者使用關鍵字 storage 顯式標明變數,以及未初始化的 storage 指針(Uninitialized storage pointer)警告。

但是報 warning 並不會影響正常編譯,而開發者往往很容易忽略編譯器的各種警告提示(而且僅憑少量且模糊的警告信息,開發者並不知道如何正確修改代碼),繼續部署問題代碼進行使用,從而留下極大的安全風險。


Solidity 的 storage 空指針(引用)是一個設計缺陷

在傳統編程語言中(如C, C++),對空指針(Null Pointer)的訪問,通常會引起程序的報錯或崩潰。空指針的值等於零,但是語言和底層系統也同時保證內存中地址為  的位置是不能存放有意義的值。而在例如 Java 或者 C# 中有  的概念,但是它們都定義了一個的值,。是一個引用的安全保護值,保證這個引用不會指向任何數據。

但與傳統編程語言不同,以太坊智能合約語言 Solidity 中存在 memory 與 storage 的兩個數據存儲的概念,其中 storage 是一個外部的持久化存儲空間,位於區塊鏈上。然而,Solidity 語言卻允許定義一個指向外部存儲 storage 的指針(引用),這個引用在未初始化的情況下等於 ,而在 storage 地址為  的位置存放著有意義的數據。大家這時候可能已經感覺到哪裡不對了,在 Solidity 語言中,竟然允許存在一個沒有定義  狀態的數據引用,即一個未初始化的指針會默認指向有意義的數據,如果此時直接對「未初始化的 storage 引用」進行賦值,那麼就會錯誤覆蓋合約存儲在 storage 上面的狀態變數。如果 Solidity在設計初期考慮了  的值,或者像 C++ 那樣禁止定義 ,那麼這類問題就能徹底避免。

註:在 Solidity 術語中,引用與指針兩個概念並不做區分。


新曙光:Solidity 編譯器即將改進升級

編譯器把源碼編譯成位元組碼的過程中,會對源碼進行語法以及安全性檢查,並給出各類提示。其中代表有問題的提示級別有 warning 和 error。通常 warning 級別不會影響編譯結果,而 error 級別的問題會導致編譯器罷工。

追溯 Solidity 編譯器開發歷程我們發現,早期版本的 Solidity,一直把上文提到的「未初始化的 storage 指針」問題作為 error 處理。2016 年 10 月 15 日,開發者為了修復其他一些問題,將此處的提示級別降為 warning [3]。

此後一直不斷有人提 issue,警告不應允許「未初始化的 storage 指針」。而開發團隊則一直回應稱編譯器已提示 warning 信息 [4]。

並解釋之所以不提升為 error 是為了兼容部分特殊場景下代碼可編譯通過。

由於 Solidity 編譯器開發團隊認為修復該問題可能會帶來兼容性問題,於是在今年 3 月份將該問題的修復放到了下一個大版本(0.5.0)。開發者需使用  標記來觸發。

普通開發者很少會利用這個實驗特性,再加上普遍忽視警告信息,因此以太坊主網上一直部署著不少帶有此問題的代碼。

好消息是,安比(SECBIT)實驗室發現 Solidity 編譯器開發團隊於 20 多天前往 develop 分支合併了該問題的修復代碼,不區分是否是 0.5.0 以上的版本 [4]。也就是說上文中的所有問題代碼,不出意外在下一個版本(Solidity 0.4.25)都無法正常通過編譯。

安比(SECBIT)實驗室同步了最新編譯器代碼進行驗證。

// test1.sol contract C { function f() public { uint[] storage x; uint[10] storage y; uint[10] z; } }

對於以上問題代碼,新版編譯器報錯如下:

test1.sol:3:3: Error: Uninitialized storage pointer. uint[] storage x; ^--------------^ test1.sol:4:3: Error: Uninitialized storage pointer. uint[10] storage y; ^----------------^

明確提示 ,無法通過編譯。

// test2.sol contract C {    function f() public {        uint[] x;   } }

而對於沒有顯示聲明變數存儲位置(storage 或 memory)的代碼,報錯如下:

test2.sol:4:3: Error: Data location must be specified as either "memory" or "storage". uint[] x; ^------^

同樣也無法通過編譯。

Solidity 0.4.25 應該很快進入正式發布階段。

很明顯,0.4.24 以來,Solidity 語法上新增了很多更嚴格的要求,強制要求開發者寫出更嚴謹的合約代碼。


案例帶來的提示

回顧本文中 Solidity「未初始化的 storage 指針」問題。Solidity 中函數內部聲明的引用類型變數默認存儲位置為 storage,而未初始化的 storage 指針會指向 storage 的起始地址,從而合約開頭定義的若干個狀態變數會被覆蓋修改。

以此案例為教訓,安比(SECBIT)實驗室有下列提示:

智能合約開發者需要搞清楚  和  等關鍵詞的意義和用法,盡量顯示標明

智能合約開發者必須重視合約編譯過程中的每一個 warning 信息

編譯器作為基礎工具,設計得當則可在一定程度上杜絕特定安全問題

編譯器開發和程序語言設計一定要嚴謹,從底層設計層面規避因使用者應理解偏差或使用不當帶來的風險

我們欣慰地看到,Solidity 語言正變得越來越嚴謹。有理由相信以太坊 Solidity 開發生態將迎來更大的發展。


參考文獻

作者:安比(SECBIT)實驗室 & 輕信科技(LedgerGo)

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 巴比特資訊 的精彩文章:

數字貨幣錢包Bitfi疑似被黑,John McAfee回應稱是無稽之談
IMEOS:「EOS Node」是一個不存在的EOS節點

TAG:巴比特資訊 |