Rust 智能合约养成日记: 合约安全之整数溢出
BlockSec
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即会导致上溢。
无符号整数uin32的表示范围也有下界,即最小值0。当0减去1时将导致uint32整数的下溢:
3. 整数溢出实例
BeautyChain团队2018年4月22日宣布,BEC token在4月22日出现了异常波动。攻击者利用整数溢出造成的漏洞成功获得了10^58 个BECs。
在该合约的攻击事件中,攻击者执行了具有整数溢出漏洞的函数“batchTransfer”进行了交易
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
以下是该该函数的具体实现:
该函数用来向多个地址(receivers)转账, 每个地址的转账金额为value。
上述代码的第三行
4. 整数溢出防护技术
本小节将介绍如何使用一些常用的手段并结合Rust语言的特性来避免整数溢出。
在Rust语言中:当我们编译获得release版本的目标文件时,若不加以配置,Rust将默认不检查整数溢出。当整数溢出时,例如在8位无符号整数(uint8)的情况下,Rust的做法通常是,使值256变成0,257变成1,以此类推。此时Rust并不会触发Panic,但是变量的值可能不是我们所期望的值。因此我们需要对Rust程序的编译选项稍加配置,使得程序在Release模式下也能够检查整数溢出,并能够触发Panic,从而避免因整数溢出而导致的程序异常现象。
配置
利用该配置我们可以设置程序内整数溢出时的处理策略。
4.1 使用Rust Crate uint 支持更大整数(目前最新版本为0.9.1)
对比于Solidity所能够支持的最大整数类型为u256,Rust目前标准库所能提供的最大整数类型仅为u128。为了更好地在我们的Rust智能合约中支持更大的整数运算,我们可以使用Rust uint crate来帮助拓展。
4.1.1 Rust uint crate简介
使用Rust
4.1.2 Rust uint crate使用方法
首先在Rust项目的
随后我们可以在Rust程序中导入使用该crate
如下语句可以用于构造自己想要的无符号整数类型:
我们可以使用如下方法首先定义变量
单元测试一:用于检查uint是否能够支持表示U1024所能表示的最大值。
单元测试一结果:
可见变量
单元测试二:整数上溢测试
单元测试的结果如下:
根据uint crate所提供的类型转换函数
4.3 使用Safe Math检查整数上溢和下溢
Rust语言对于整数运算中可能发生的整数溢出也提供了不同的运算行为。如果需要更精细地控制整数溢出的行为,可以调用标准库中的
单元测试三:使用checked_sub检查整数下溢
单元测试的结果如下:
此时在上述单元测试的结果中可以发现:当执行单元测试的时候尽管发生了整数溢出,并且运算结果返回了
此时的单元测试结果输出如下:
即Rust能够利用
5. 本期总结和预告
这一期我们讲述了rust智能合约中的整数溢出问题,同时给出了建议,在书写代码时使用uint类型转换函数或者safe math来防止整数溢出问题发生,下一期我们将讲述rust智能合约中的重入问题。敬请关注。
在大多数编程语言中,一个整数的数值通常保存在一段定长的内存当中。整数可分为两种类型,即无符号数与有符号数。它们之间的区别在于最高位是否被用作符号位,用来表示整数的正负。例如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?whenNotPausedreturns?(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("p to be a good number in the example");
单元测试一:用于检查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. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; 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' panicked at 'Integer overflow when casting to u128', 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. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; 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智能合约中的重入问题。敬请关注。