Risk Warning: Beware of illegal fundraising in the name of 'virtual currency' and 'blockchain'. — Five departments including the Banking and Insurance Regulatory Commission
Information
Discover
Search
Login
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
View Market
Rust smart contract development diary (4)
BlockSec
特邀专栏作者
2022-03-29 10:30
This article is about 6467 words, reading the full article takes about 10 minutes
Contract-safe integer overflow.

related articles:

Rust smart contract development diary (1) Contract state data definition and method implementation

picture

Rust smart contract development diary (1) Contract state data definition and method implementation

Rust Smart Contract Development Diary (2) Writing Rust Smart Contract Unit Tests

Rust Smart Contract Development Diary (2) Writing Rust Smart Contract Unit Tests

Rust smart contract development diary (3) Rust smart contract deployment, function call and use of Explorer

Rust smart contract development diary (3) Rust smart contract deployment, function call and use of Explorer

first level title

1. Overview of Integer Overflow Vulnerabilities

picture

In most programming languages, the value of an integer is usually stored in a fixed-length memory. Integers can be divided into two types, unsigned and signed. The difference between them is whether the highest bit is used as a sign bit, which is used to indicate the sign of an integer. For example, the 32bit memory space can store unsigned integers (uint32) between 0 and 4,294,967,295, or signed integers (int32) between −2,147,483,648 and 2,147,483,647.

But what happens when we perform the calculation 4,294,967,295 + 1 in the range of uint32 and try to store the result larger than the maximum value of that integer type?

   0xFFFFFFFF
+ 0x00000001
------------
= 0x00000000

Although the result of this execution depends on the particular programming language and compiler, in most cases the result of the calculation will exhibit "overflow" and return 0. At the same time, most programming languages ​​and compilers do not check for this type of error, but only perform a simple modulo operation, and there are even other undefined behaviors.

The existence of integer overflow often makes the program produce unexpected results at runtime. In the writing of blockchain smart contracts, especially in the field of decentralized finance, the usage scenarios of integer numerical calculations are very common, so special attention should be paid to the possibility of integer overflow vulnerabilities.

   0x00000000
- 0x00000001
------------
= 0xFFFFFFFF

Suppose a financial institution uses unsigned 32-bit integers to represent stock prices. However, when using this integer type to represent a number larger than the maximum value that the type can represent, the computer will place an extra 1 or more bits outside the 32-bit memory range (that is, overflow), and finally the number will be represented as Values ​​other than overflow bits are truncated, $429,496,7296 are read as 0 if possible. At this point, if someone continues to trade using that value, the stock price will be 0, which will cause all kinds of confusion. Therefore, the problem of integer overflow vulnerabilities deserves our attention.

How to avoid integer overflow when writing smart contracts in Rust language will be the focus of the subsequent discussion in this article.

2. Integer overflow definition

https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

If the value exceeds the range that the variable type can represent, overflow will result. Overflow can be mainly divided into two cases, namely integer overflow (overflow) and underflow (underflow).

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. }

2.1 Integer overflow

That is, similar to what is described in the overview of integer overflow vulnerabilities above, for example, the range of unsigned integers that can be represented by uint32 in Solidity is: 0 to 2^32 - 1, 2^32 - 1 is represented as 0xFFFFFFFF in hexadecimal , 2^32 - 1 plus 1 will cause overflow.

2.2 Integer underflow

The representation range of the unsigned integer uin32 also has a lower bound, that is, the minimum value is 0. Subtracting 1 from 0 will result in an underflow of the uint32 integer:

3. Integer overflow instance

The BeautyChain team announced on April 22, 2018 that the BEC token fluctuated abnormally on April 22. The attacker successfully obtained 10^58 BECs by exploiting the vulnerability caused by integer overflow.

[profile.release]
overflow-checks = true
panic = "abort"

In the attack event of this contract, the attacker executed the function "batchTransfer" with an integer overflow vulnerability to make a transaction

The following is the specific implementation of this function:

This function is used to transfer money to multiple addresses (receivers), and the transfer amount of each address is value.

The third line of the above code uint256 amount = uint256(cnt) * _value is used to calculate the entire amount that needs to be transferred, but there is a possibility of integer overflow in this line of code. When value = 0x800000000000000000000000000000000000000000000000000000000000000, and the length of receivers is 2, an integer overflow will occur during the multiplication operation of the third line of code, making amount = 0. Since amount = 0 is smaller than the user's balances[msg.sender], checking whether the balance of the contract caller user msg.sender in line 5 is greater than the amount to be transferred will be easily passed. In this way, the attacker can perform subsequent transfer operations to make a profit.

4. Integer overflow protection technology

This section will introduce how to use some common methods combined with the features of Rust language to avoid integer overflow.

In the Rust language: When we compile the target file of the release version, if it is not configured, Rust will not check integer overflow by default. When an integer overflows, such as in the case of an 8-bit unsigned integer (uint8), Rust's usual approach is to make the value 256 become 0, 257 become 1, and so on. At this time, Rust will not trigger Panic, but the value of the variable may not be the value we expected. Therefore, we need to configure the compilation options of the Rust program a little, so that the program can also check for integer overflow in Release mode, and can trigger Panic, so as to avoid program abnormalities caused by integer overflow."0.9.1"Configure Cargo.toml to check for integer overflow in release mode.

[dependencies]
Using this configuration, we can set the processing strategy for integer overflow in the program.
uint = { version = "0.9.1", default-features = false }

4.1 Use Rust Crate uint to support larger integers (currently the latest version is 0.9.1)

use uint::construct_uint;

Compared with the largest integer type that Solidity can support is u256, the largest integer type that Rust's current standard library can provide is only u128. To better support larger integer operations in our Rust smart contracts, we can use the Rust uint crate to help with scaling.

construct_uint! {
    pub struct U1024(16);
}
construct_uint! {
    pub struct U512(8);
}
construct_uint! {
    pub struct U256(4);
}

4.1.1 Introduction to Rust uint crates

Use the Rust uint crate to provide large unsigned integer types, and built-in support for APIs that are very similar to Rust's original integer types, while taking into account performance and cross-platform usability.

// (2^1024)-1 = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215 
let p =U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");

4.1.2 How to use Rust uint crate

   #[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());
   }

First add the dependency on uint crate in the Cargo.toml of the Rust project, and specify the version number as the latest

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

Version.

# Other dependencies, such as near-sdk, near-contract-standards, etc.

   #[test]
   fn test_overflow(){
Then we can import and use the crate in the Rust program
       let amounts: u128 = 340282366920938463463374607431768211455;
     
The following statement can be used to construct the unsigned integer type you want:
       let amount_u256 = U256::from(amounts) * U256::from(amounts);
       println!("{:?}",amount_u256);

4.2 Use the uint type conversion function to detect integer overflow
       let amount_u256 = U256::from(amounts) + 1;
       println!("{:?}",amount_u256);
       
We can use the following method to first define the variable p, and use the method from_dec_str defined by uint crate for U1024 to assign a value to the variable p.
      let amount_u128 = amount_u256.as_u128();
       println!("{:?}",amount_u128);
   }

Unit test 1: It is used to check whether uint can support the maximum value that U1024 can represent.

running 1 test
115792089237316195423570985008687907852589419931798687112530834793049593217025
340282366920938463463374607431768211456
thread 'tests::test_overflow' panicked at 'Integer overflow when casting to u128', src/lib.rs:16:1

Unit test one result:

It can be seen that the variable p: U1024 accurately saves the maximum value that U1024 can represent.

Unit Test 2: Integer Overflow Test

// The maximum value that u128 can represent, that is, 2^128 -1<_>// U256 can normally express the operation result of (2^128 -1)*(2^128 -1) without overflow.

// here (2^128 -1) + 1 = 2^128

#[test]
fn test_underflow(){
   let amounts= U256::from(0);
   let amount_u256 = amounts.checked_sub(U256::from(1));
   println!("{:?}",amount_u256);
}

// Will overflow the range 0 to 2^128 -1 that can be represented by u128 unsigned integer, so Panic will be triggered.

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

The results of the unit tests are as follows:

#[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);
}

According to the type conversion function .as_u128() feature provided by uint crate, when amount_u256 is converted to u128 by type, Painc will be triggered because it overflows the range that u128 unsigned integer can represent. It can be seen that Rust can detect integer overflow at this time.

running 1 test
thread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62

4.3 Checking for Integer Overflow and Underflow Using Safe Math

The Rust language also provides different operation behaviors for integer overflows that may occur in integer operations. If you need to control the behavior of integer overflow more finely, you can call wrapping_*, saturating_*, checked_* and overflowing_* series functions in the standard library. This section will focus on the checked_* functions. Readers can search the above keywords to learn more Ways to control integer overflow.

The type returned by checked_* is Option

smart contract
Welcome to Join Odaily Official Community