Rust智能合约养成日记(4)

avatar
BlockSec
2年前
本文约2630字,阅读全文需要约4分钟
合约安全之整数溢出。

相关文章:

1. 整数溢出漏洞概述

Rust智能合约养成日记(4)

在大多数编程语言中,一个整数的数值通常保存在一段定长的内存当中。整数可分为两种类型,即无符号数与有符号数。它们之间的区别在于最高位是否被用作符号位,用来表示整数的正负。例如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

以下是该该函数的具体实现:

1function batchTransfer(address[] _receiversuint256 _valuepublic 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 = 0i < cnti++) {
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智能合约中的重入问题。敬请关注。

原创文章,作者:BlockSec。转载/内容合作/寻求报道请联系 report@odaily.email;违规转载法律必究。

ODAILY提醒,请广大读者树立正确的货币观念和投资理念,理性看待区块链,切实提高风险意识;对发现的违法犯罪线索,可积极向有关部门举报反映。

推荐阅读
星球精选