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即会导致上溢。
    
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智能合约中的重入问题。敬请关注。