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智能合約中的重入問題。敬請關注。