Solidity編譯器漏洞分析:ABI重編碼的缺陷
Eocene
2023-04-25 10:25
本文约1907字,阅读全文需要约8分钟
該過程本身並沒有大的邏輯問題,但是當和Solidity的cleanup機制結合時,由於Solidity編譯器代碼本身的疏漏,就導致了漏洞的存在。

一級標題

本文從源代碼層面對Solidity 編譯器( 0.5.8<= version <0.8.16)在ABIReencoding 過程中,由於對固定長度的uint 和bytes 32 類型數組的錯誤處理所導致的漏洞問題進行詳細分析,並提出相關的解決方案及規避措施。

一級標題

一級標題一級標題漏洞詳情

ABI 編碼格式是用在用戶或合約對合約進行函數調用,傳遞參數時的標準編碼方式。具體可以參考Solidity 官方關於

ABI 編碼的詳細表述。在合約開發過程中,會從用戶或其他合約傳來的calldata 數據中,獲取需要的數據,之後可能會將獲取的數據進行轉發或emit 等操作。限於evm 虛擬機的所有opcode 操作都是基於memory、stack 和storage,所以在Solidity 中,涉及到需要對數據進行ABI 編碼的操作,都會將calldata 中的數據根據新的順序按照ABI 格式進行編碼,並存儲到memory 中。

該過程本身並沒有大的邏輯問題,但是當和Solidity 的

cleanup 機制

contract Eocene {

        event VerifyABI( bytes[], uint[ 2 ]);

        function verifyABI(bytes[] calldata a, uint[ 2 ] calldata b) public  {

                emit VerifyABI(a,結合時,由於Solidity 編譯器代碼本身的疏漏,就導致了漏洞的存在。

        }

}

根據ABI 編碼規則,在去掉函數選擇符之後,ABI 編碼的數據分為head 和tail 兩部分。當數據格式為固定長度的uint 或bytes 32 數組時,ABI 會將該類型的數據都存儲在head 部分。而Solidity 對memory 中cleanup 機制的實現是在當前索引的內存被使用後,將下一個索引的內存置空,以防止下一索引的內存使用時被臟數據影響。並且,當Solidity 對一組參數數據進行ABI 編碼時,是按照從左到右的順序進行編碼! !

為了便於後面的漏洞原理探索,考慮如下形式的合約代碼:

b); //Event 數據會按照ABI 格式編碼之後存儲到鏈上verifyABI(['0x aaaaaa','0x bbbbbb'],[0x 11111, 0x 22222 ])

合約Eocene 中verifyABI 函數的作用,僅僅是將函數參數中的不定長bytes[] a 和定長uint[2 ] b 進行emit。verifyABI(['0x aaaaaa','0x bbbbbb'],[0x 11111, 0x 22222 ])這裡需要注意,event 事件也會觸發ABI 編碼。這裡參數a, b 會編碼成ABI 格式後再存儲到鏈上。

0x 5 2c d 1 a 9 c                                                                  // bytes 4(sha 3("verify(btyes[], uint[ 2 ])"))

0000000000000000000000000000000000000000000000000000000000000060            // index of  a

0000000000000000000000000000000000000000000000000000000000011111            // b[0 ]

0000000000000000000000000000000000000000000000000000000000022222            // b[1 ]

0000000000000000000000000000000000000000000000000000000000000002            // length of a

0000000000000000000000000000000000000000000000000000000000000040            // index of a[0 ]

0000000000000000000000000000000000000000000000000000000000000080            // index of a[1 ]

0000000000000000000000000000000000000000000000000000000000000003            // length of a[0 ]

aaaaaa 0000000000000000000000000000000000000000000000000000000000            // a[0 ]

0000000000000000000000000000000000000000000000000000000000000003            // length of a[1 ]

bbbbbb 0000000000000000000000000000000000000000000000000000000000            // a[1 ]

我們使用v 0.8.14 版本的Solidity 對合約代碼進行編譯,通過remix 進行部署,並傳入a, b首先,我們看一看對TX

的正確編碼格式:

如果Solidity 編譯器正常,當參數

0000000000000000000000000000000000000000000000000000000000000060            // index of  a

0000000000000000000000000000000000000000000000000000000000011111            // b[0 ]

0000000000000000000000000000000000000000000000000000000000022222            // b[1 ]

0000000000000000000000000000000000000000000000000000000000000000            // length of a?? why become 0??

0000000000000000000000000000000000000000000000000000000000000040            // index of a[0 ]

0000000000000000000000000000000000000000000000000000000000000080            // index of a[1 ]

0000000000000000000000000000000000000000000000000000000000000003            // length of a[0 ]

aaaaaa 0000000000000000000000000000000000000000000000000000000000            // a[0 ]

0000000000000000000000000000000000000000000000000000000000000003            // length of a[1 ]

bbbbbb 0000000000000000000000000000000000000000000000000000000000            // a[1 ]

被event 事件記錄到鏈上時,數據格式應該和我們發送的一樣。讓我們實際調用合約試試看,並對鏈上的log 進行查看,如果想自己對比,可以查看該

成功調用後,合約event 事件記錄如下:

  1. ! !震驚,緊跟b[1 ]的,存儲a 參數長度的值被錯誤的刪除了! !

  2. 為什麼會這樣?

  3. 正如我們前面所說,在Solidity 遇到需要進行ABI 編碼的系列參數時,參數的生成順序是從左至,具體對a, b 的編碼邏輯如下

Solidity 先對a 進行ABI 編碼,按照編碼規則,a 的索引放在頭部,a 的元素長度以及元素具體值均存放在尾部。

處理b 數據,因為b 數據類型為uint[2 ]格式,所以數據具體值被存放在head 部分。但是,由於Solidity 自身的cleanup 機制,在內存中存放了b[1 ]之後,將b[1 ]數據所在的後一個內存地址(被用於存放a 元素長度的內存地址)的值置0 。

ABI 編碼操作結束,錯誤編碼的數據存儲到了鏈上,SOL-2022-6 漏洞出現。ABIFunctions::abiEncodingFunctionCalldataArrayWithoutCleanup()

在源代碼層面,具體的錯誤邏輯也很明顯,當需要從calldata 獲取定長bytes 32 或uint 數組數據到memory 中時,Solidity 總是會在數據複製完畢後,將後一個內存索引數據置為0 。又由於ABI 編碼存在head 和tail 兩部分,且編碼順序也是從左至右,就導致了漏洞的存在。fromArrayType.isDynamicallySized()具體漏洞的Solidity 編譯代碼如下:

當源數據存儲位置為Calldata,且源數據類型為ByteArray,String,或者源數組基礎類型為uint 或bytes 32 時進入isByteArrayOrString()進入之後,會首先通過YulUtilFunctions::copyToMemoryFunction(),對源數據是否為定長數組來對源數據進行判斷,只有定長數組才符合漏洞觸發條件。

判斷結果傳遞給

一級標題

另外,另外,

一級標題

      • event

      • error

      • abi.encode*

      • returns             //the return of function

      • struct              //the user defined struct

      • all external call

解決方案

  1. 解決方案

  2. 一級標題

  3. 一級標題

關於我們

At Eocene Research, we provide the insights of intentions and security behind everything you know or don't know of blockchain, and empower every individual and organization to answer complex questions we hadn't even dreamed of back then.

Learn more: Website | Medium | Twitter

Eocene
作者文库