Zero Time Technology|Solidity Smart Contract Basic Vulnerability - Integer Overflow Vulnerability

 

0x01 Overflow attack event

On April 22, 2018, hackers launched an attack on the BEC smart contract and took out out of thin air:

57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 BEC tokens were sold in the market.

On April 25, 2018, the SMT project found that its transactions were abnormal, and hackers used its function loopholes to create:

65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000+50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000的SMT币,火币Pro随即暂停了所有币种的充值提取业务。

On December 27, 2018, an integer overflow vulnerability occurred in the Ethereum smart contract Fountain (FNT). Hackers used its function vulnerability to create:

2+115792089237316195423570985008687907853269984665640564039457584007913129639935 SMT coins.

The lessons of blood and tears of history should not reappear now. Let us cherish the memory of these tokens that returned to zero overnight, and learn from the experience and lessons of our predecessors.

0x02 Introduction to Integer Overflow

  • Principle of Integer Overflow

Since the underlying computer is binary, any decimal number will be encoded to binary. Overflow discards the highest bit, resulting in an incorrect value.

For example: the maximum value of the eight-bit unsigned integer type is 255, which translates to 1111 1111 in binary; when one is added, all current 1s will become 0, and carry up. However, since the positions that the integer type can hold are all 1, and then carry up, the highest bit will be discarded, so the binary becomes 0000 0000

Note: For signed integer types, the highest binary bit represents positive and negative. So an overflow of a positive number of that type becomes a negative number, not zero.

  • Integer Overflow Example (General Programming Languages)

Integer overflow vulnerabilities caused by arithmetic in programming languages ​​are common, and their types include the following three types:

• Addition overflow

• Subtract overflow

• multiplication overflow

Let's take the Kotlin programming language running on the JVM as an example to test integer overflow by adding operations:

fun main() {
   println(Long.MAX_VALUE + 1) // Long 是有符号的 128 位 Integer 类型
}

The program will print out -9223372036854775808, which is actually because the integer overflow was not prevented during compilation, because the compiler allowed the overflowed code to be compiled.

Of course, there are also programming languages ​​that strictly check integer overflow at compile time. Such as the hottest Rust programming language in the blockchain world:

fn main() {
    dbg!(u128::MAX + 1); // u128 是无符号的 128 位 Integer 类型
}

Compile this code, and you'll get compilation errors:

error: this arithmetic operation will overflow
 --> src/main.rs:2:10
  |
2 |     dbg!(u128::MAX + 1);
  |          ^^^^^^^^^^^^^ attempt to compute `u128::MAX + 1_u128`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

Great, this effectively prevents compile-time overflow problems. So what if it's runtime? Let's try reading user input:

fn main() {
    let mut s = String::new();
    std::io::stdin().read_line(&mut s).unwrap();
    dbg!(s.trim_end().parse::<u8>().unwrap() + 1); // u8 是无符号的 8 位 Integer 类型
}

Run cargo r, input: 255, get panic:

thread 'main' panicked at 'attempt to add with overflow'

It can be seen that in debug mode, the overflow will panic directly, that is: the program crashes and stops working. So, is it the same in release mode?

Run cargo r --release, input: 255, print:

[src/main.rs:4] s.trim_end().parse::<u8>().unwrap() + 1 = 0

To sum up, we have come to a conclusion: Even if the programming language that strictly checks overflow at compile time, there will still be integer overflow problems. Integer overflow is like a curse, it will always appear every now and then, and it cannot be eliminated once and for all.

  • Integer overflow in smart contracts (Solidity language)

In the world of blockchain, in the Solidity language of smart contracts, for versions below 0.8.0, there is also an integer overflow problem.

Like general-purpose programming languages, let's first see if overflow occurs at compile time:

 

In actual measurement, the test function will directly cause a compilation error. Let's look at runtime again:

 

It is measured that the program will overflow at runtime. We recommend using the SafeMath library to address vulnerability overflows:

library SafeMath {
  function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

  function div(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a / b;
    return c;
  }

  function sub(uint256 a, uint256 b) internal constant returns (uint256) {
    assert(b <= a);
    return a - b;
  }

  function add(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

For versions above Solidity 0.8.0, the official has fixed this problem. So how exactly is it fixed? What happens to prevent overflow when it is about to overflow?

According to the actual measurement, Solidity versions above 0.8 will directly revert if runtime overflow occurs.

Turns out, the fix was to not allow overflow. int256 is large enough, as long as it cannot be exploited by hackers to create revenue out of thin air, we will be successful.

0x03 Vulnerable contracts and attack methods

Taking the BEC contract as an example, the contract address is:

0xC5d105E63711398aF9bbff092d4B6769C82F793D

The address on etherscan is:

https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

The code of the contract with the overflow vulnerability is as follows:

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    uint256 amount = uint256(cnt) * _value; //溢出点,这里存在整数溢出
    require(cnt > 0 && cnt <= 20);
    require(_value > 0 && balances[msg.sender] >= amount);

    balances[msg.sender] = balances[msg.sender].sub(amount);
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }

The contract version at that time was ^0.4.16, which was less than version 0.8, and the SafeMath library was not used, so there was an integer overflow problem.

The hacker passed in a very large value (here it is 2**255), and overflowed through multiplication, so that the amount (the total number of coins to be transferred) overflowed and became a small number or 0 (here became 0) , so as to bypass the check code of balances[msg.sender] >= amount, so that the malicious transfer of a huge amount of _value can be successful.

Malicious transfer records of actual attacks:

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

0x04 summary

If the Solidity version is below 0.8 and the SafeMath library is not used: Hackers often use overflow to construct a minimum value/maximum value, thereby bypassing some checks and making huge malicious transfers successful.

Of course, contract vulnerabilities are not limited to integer overflows. In addition to developers themselves raising awareness of security development, it is also necessary to find a professional security team to conduct a comprehensive audit of the contract.

Guess you like

Origin blog.csdn.net/m0_37598434/article/details/123113836