1. 整數溢出漏洞概述

在大多數編程語言中,一個整數的數值通常保存在一段定長的內存當中。整數可分為兩種類型,即無符號數與有符號數。它們之間的區別在於最高位是否被用作符號位,用來表示整數的正負。例如32bit的內存空間可以存儲0到4,294,967,295範圍之間的無符號整數(uint32),或−2,147,483,648到2,147,483,647範圍之間的有符號整數(int32)。

但是,當我們在uint32的範圍內,執行計算4,294,967,295 + 1並試圖存儲大於該整數類型最大值的結果時,會發生什麼呢?

儘管該執行的結果取決於特定編程語言和編譯器,但在大多數情況下,計算的結果將表現出“溢出”的現象並返回0。同時,大多數編程語言和編譯器不會檢查該類型的錯誤,而僅僅執行一個簡單的模運算,甚至還存在其他未定義的行為。

整數溢出的存在,往往使得程序在運行時產生意料之外的結果。在區塊鏈智能合約的編寫中,尤其是去中心化金融領域,整數數值計算的使用場景十分普遍,因此需格外注意整數溢出漏洞存在的可能性。

假設,某金融機構使用無符號的32位整數來表示股票價格。然而,當使用該整數類型表示一個大於該類型所能表示的最大值數字時,計算機將在32位的內存範圍外額外放置一個1或更多的位(即溢出),最終該數字將表示為截斷了溢出位以外的值,如可能將$429,496,7296讀為0。此時,如果有人使用該數值繼續進行交易,股票價格將為0 ,這將引起各種各樣的混亂。因此,整數溢出漏洞的問題值得我們的重視。

如何在使用Rust語言編寫智能合約時,避免整數溢出,將是本文後續討論的重點。


2. 整數溢出定義

若數值超出了變量類型所能表示的範圍,則會導致溢出。溢出主要可分為兩種情況,即整數上溢(overflow)和下溢(underflow)。

2.1 整數上溢

即類似於上文整數溢出漏洞概述中所描述的那樣,例如在Solidity中uint32所能表示的無符號整數範圍為:0 至2^32 - 1,2^32 - 1使用16進製表示為0xFFFFFFFF ,2^32 - 1再加上1即會導致上溢。

 0xFFFFFFFF + 0x00000001 ------------ =0x00000000

2.2 整數下溢

無符號整數uin32的表示範圍也有下界,即最小值0。當0減去1時將導致uint32整數的下溢:

 0x00000000 - 0x00000001 ------------ =0xFFFFFFFF


3. 整數溢出實例

BeautyChain團隊2018年4月22日宣布,BEC token在4月22日出現了異常波動。攻擊者利用整數溢出造成的漏洞成功獲得了10^58 個BECs。

在該合約的攻擊事件中,攻擊者執行了具有整數溢出漏洞的函數“batchTransfer”進行了交易

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

以下是該該函數的具體實現:

 1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { 2. uint cnt = _receivers.length; 3. uint256 amount = uint256(cnt) * _value; 4. require(cnt > 0 && cnt <= 20); 5. require(_value > 0 && balances[msg.sender] >= amount); 6. 7. balances[msg.sender] = balances[msg.sender].sub(amount); 8. for (uint i = 0; i < cnt; i++) { 9. balances[_receivers[i]] = balances[_receivers[i]].add(_value); 10. Transfer(msg.sender, _receivers[i], _value); 11. } 12. return true; 13.}

該函數用來向多個地址(_receivers)轉賬, 每個地址的轉賬金額為_value。

上述代碼的第三行uint256 amount = uint256(cnt) * _value用來計算整個需要轉賬的金額,但是該行代碼存在整數溢出的可能性。當_value =0x8000000000000000000000000000000000000000000000000000000000000000,同時_receivers的長度為2. 則在第三行代碼乘法運算的時候將發生整數溢出,使得amount = 0。由於amount = 0要比用戶的balances[msg.sender]要小,因此第5行中檢查合約調用者用戶msg.sender的餘額是否大於將要轉出的amount數額會輕鬆被通過。從而攻擊者可以執行後續的轉賬操作而獲利。

4. 整數溢出防護技術

本小節將介紹如何使用一些常用的手段並結合Rust語言的特性來避免整數溢出。

在Rust語言中:當我們編譯獲得release版本的目標文件時,若不加以配置,Rust將默認不檢查整數溢出。當整數溢出時,例如在8位無符號整數(uint8)的情況下,Rust的做法通常是,使值256變成0,257變成1,以此類推。此時Rust並不會觸發Panic,但是變量的值可能不是我們所期望的值。因此我們需要對Rust程序的編譯選項稍加配置,使得程序在Release模式下也能夠檢查整數溢出,並能夠觸發Panic,從而避免因整數溢出而導致的程序異常現象。

配置Cargo.toml ,在release模式下檢查整數溢出。

 [profile.release] overflow-checks = true panic='abort'

利用該配置我們可以設置程序內整數溢出時的處理策略。

4.1 使用Rust Crate uint 支持更大整數(目前最新版本為0.9.1)

對比於Solidity所能夠支持的最大整數類型為u256,Rust目前標準庫所能提供的最大整數類型僅為u128。為了更好地在我們的Rust智能合約中支持更大的整數運算,我們可以使用Rust uint crate來幫助拓展。

4.1.1 Rust uint crate簡介

使用Rust uint crate可提供大無符號整數類型,並內置支持了與Rust原始整數類型非常相似的API,同時兼顧了性能與跨平台可用性。

4.1.2 Rust uint crate使用方法

首先在Rust項目的Cargo.toml中添加對uint crate的依賴,並指定版本號為最新的'0.9.1'版本。

 [dependencies] # 其他依赖,例如near-sdk,near-contract-standards等uint={version='0.9.1',default-features=false}

隨後我們可以在Rust程序中導入使用該crate

 use uint::construct_uint;

如下語句可以用於構造自己想要的無符號整數類型:

construct_uint! { pub struct U1024(16); } construct_uint! { pub struct U512(8); } construct_uint! { pub struct U256(4); }

4.2 使用uint類型轉化函數檢測整數上溢

我們可以使用如下方法首先定義變量p ,並使用uint crate為U1024定義的方法from_dec_str為變量p賦值。

 //(2^1024)-1=179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215 let p=U1024::from_dec_str( '179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215' ).expect( 'ptobeagoodnumberintheexample' );

單元測試一:用於檢查uint是否能夠支持表示U1024所能表示的最大值。

 #[test] fn test_uint(){ let p = U1024::from_dec_str( '179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215' ).expect( 'p to be a good number in the example' ); assert_eq!(p,U1024::max_value()); }

單元測試一結果:

 running 1 test test tests::test_uint ... ok test result:ok.1passed;0failed;0ignored;0measured;3filteredout;finished in 0.00s

可見變量p: U1024準確保存了U1024所能表示的最大值。

單元測試二:整數上溢測試

 #[test] fn test_overflow(){ // u128所能表示的最大值,即2^128 -1 let amounts: u128 = 340282366920938463463374607431768211455 ;          // U256能够正常表示(2^128 -1)*(2^128 -1)的运算结果,并不会发生溢出。 let amount_u256 = U256:: from (amounts) * U256:: from (amounts); println!( '{:?}' ,amount_u256);        // 此处(2^128 -1) + 1 = 2^128 let amount_u256 = U256:: from (amounts) + 1 ; println!( '{:?}' ,amount_u256);        // 将溢出u128无符号整数所能表示的范围0至2^128 -1,因此会触发Panic. let amount_u128 = amount_u256.as_u128(); println!( '{:?}' ,amount_u128); }

單元測試的結果如下:

 running 1 test 115792089237316195423570985008687907852589419931798687112530834793049593217025 340282366920938463463374607431768211456 thread 'tests::test_overflow' panickedat 'Integeroverflowwhencastingtou128' ,src/lib.rs:16:1

根據uint crate所提供的類型轉換函數.as_u128()特性可知,當將amount_u256 通過類型轉化為u128的時候,由於溢出了u128無符號整數所能表示的範圍,因此將觸發Painc。可見此時Rust能夠檢測整數上溢。

4.3 使用Safe Math檢查整數上溢和下溢

Rust語言對於整數運算中可能發生的整數溢出也提供了不同的運算行為。如果需要更精細地控制整數溢出的行為,可以調用標準庫中的wrapping_*saturating_*checked_*overflowing_*系列函數,本節將重點講述checked_*函數,讀者可以檢索上述關鍵字了解更多的控制整數溢出的方式。

checked_*返回的類型是Option<_> ,當出現溢出的時候,返回值是None;

如checked_sub就會進行減法運算,並且檢查溢出是否會發生。

單元測試三:使用checked_sub檢查整數下溢

 #[test] fn test_underflow(){ let amounts= U256:: from ( 0 ); let amount_u256 = amounts.checked_sub(U256:: from ( 1 )); println!( '{:?}' ,amount_u256); }

單元測試的結果如下:

 running 1 test None test tests::test_underflow ... ok test result:ok.1passed;0failed;0ignored;0measured;4filteredout;finished in 0.00s

此時在上述單元測試的結果中可以發現:當執行單元測試的時候儘管發生了整數溢出,並且運算結果返回了None 。但是並沒有觸發Panic。為此我們需要基於運算結果的返回值來判斷是否需要觸發Panic.

 #[test] fn test_underflow(){ let amounts= U256:: from ( 0 ); - let amount_u256 = amounts.checked_sub(U256:: from ( 1 )); + let amount_u256 = amounts.checked_sub(U256:: from ( 1 )).expect( 'ERR_SUB_INSUFFICIENT' ); println!( '{:?}' ,amount_u256); }

此時的單元測試結果輸出如下:

 running 1 test thread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT' , src/lib.rs:126:62

即Rust能夠利用checked_*系列函數檢測整數下溢。同理我們也可以用上述方式來檢測整數的上溢情況,並在適當的時候觸發Panic終止程序的運行。


5. 本期總結和預告

這一期我們講述了rust智能合約中的整數溢出問題,同時給出了建議,在書寫代碼時使用uint類型轉換函數或者safe math來防止整數溢出問題發生,下一期我們將講述rust智能合約中的重入問題。敬請關注。