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

  • BlockSec
  • 更新于 2022-03-28 11:00
  • 阅读 2664

往期回顾:Rust智能合约养成日记(1)合约状态数据定义与方法实现 , Rust智能合约养成日记(2)编写Rust智能合约单元测试,Rust智能合约养成日记(3)Rust智能合约部署,函数调用及Explorer的使用

往期回顾:


1. 整数溢出漏洞概述

41.jpg

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

42.png

2.2 整数下溢

无符号整数uin32的表示范围也有下界,即最小值0。当0减去1时将导致uint32整数的下溢:

43.png

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模式下检查整数溢出。

44.png

利用该配置我们可以设置程序内整数溢出时的处理策略。

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项目的<span>Cargo.toml</span>中添加对uint crate的依赖,并指定版本号为最新的"0.9.1"版本。

45.png

随后我们可以在Rust程序中导入使用该crate

use uint::construct_uint;

如下语句可以用于构造自己想要的无符号整数类型:

46.png

4.2 使用uint类型转化函数检测整数上溢

我们可以使用如下方法首先定义变量<span>p</span>,并使用uint crate为U1024定义的方法<span>from_dec_str</span>为变量<span>p</span>赋值。

// (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 testtest 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所能表示的最大值。

单元测试二:整数上溢测试

47.png

单元测试的结果如下:

running 1 test115792089237316195423570985008687907852589419931798687112530834793049593217025340282366920938463463374607431768211456thread '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检查整数下溢

48.png

单元测试的结果如下:

running 1 testNonetest 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 testthread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62

即Rust能够利用checked_* 系列函数检测整数下溢。同理我们也可以用上述方式来检测整数的上溢情况,并在适当的时候触发Panic终止程序的运行。


5. 本期总结和预告

这一期我们讲述了rust智能合约中的整数溢出问题,同时给出了建议,在书写代码时使用uint类型转换函数或者safe math来防止整数溢出问题发生,下一期我们将讲述rust智能合约中的重入问题。敬请关注。

640.jpg

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
BlockSec
BlockSec
江湖只有他的大名,没有他的介绍。