0x01 overflow attack event
On April 22, 2018, hackers launched an attack on BEC smart contract and took out:
5789604461865810000000000000000000000000000000000000000000000000000000000000000000000000000.792003956564819968 BEC tokens were sold in the market. BEC depreciated sharply and its value was almost zero. The market collapsed instantly.
On April 25, 2018, the SMT project side found that there were abnormalities in its transaction, and hackers used its function vulnerabilities to create:
65133050195990400000000000000000000000000000000000000000000000000000000000 + 50659039041325800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.
On December 27, 2018, an integer overflow vulnerability occurred in Ethereum smart contract Fountain(FNT). Hackers took advantage of its function vulnerability to create:
2 + 115792089273161954235709850086879078532699846656405640394575840079132963935 SMT coins.
The bloody and tearful lessons of history should not appear again today. Let's 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
- Integer overflow principle
Since the bottom of the computer is binary, any decimal number will be encoded into binary. Overflow discards the highest bit, resulting in incorrect values.
For example, the maximum value of an eight bit unsigned integer type is 255, which is 1111 when translated into binary; When you add one more time, all current 1s will become 0 and carry up. However, since the positions that can be accommodated by this integer type are all 1, and then carry up, the highest bit will be discarded, so the binary becomes 0000
Note: for signed integer type, its binary highest bit represents positive and negative. Therefore, positive overflow of this type will become negative instead of zero.
- Integer overflow example (general programming language)
Integer overflow vulnerabilities caused by arithmetic are common in programming languages. There are three types:
• additive overflow
• subtraction overflow
• multiplication overflow
Let's take the addition operation of Kotlin programming language running on the JVM to test integer overflow as an example:
fun main() { println(Long.MAX_VALUE + 1) // Long is a signed 128 bit Integer type }
The program will print - 9223372036854775808, which is actually that integer overflow is not prevented during compilation, because the compiler allows the overflow code to be compiled.
Of course, there are programming languages that strictly check integer overflow at compile time. For example, the blockchain world's hottest Rust programming language:
fn main() { dbg!(u128::MAX + 1); // u128 is an unsigned 128 bit Integer type }
If you compile this code, you will get a compilation error:
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
Well, this effectively prevents the problem of compile time overflow. 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 is an unsigned 8-bit Integer type }
Run cargo r and enter 255 to get panic:
thread 'main' panicked at 'attempt to add with overflow'
It can be seen that in the debug mode, the overflow will directly panic, that is, the program crashes and stops working. So, is this also true 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 come to a conclusion: even if the overflow program language is strictly checked during compilation, there will still be integer overflow. Integer overflow is like a magic spell. It always appears every once in a while and cannot be eliminated once and for all.
- Integer overflow in smart contract (solid language)
In the world of blockchain, the solid language of smart contract also has integer overflow problem for versions below 0.8.0.
Like general-purpose programming languages, let's first look at whether overflow occurs during compilation:
Actually, the test function will directly generate compilation errors. Let's look at the Runtime:
Actually, the program will overflow at runtime. We recommend using the SafeMath library to resolve the vulnerability overflow:
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 the version above Solidity 0.8.0, the official has fixed this problem. So how is it repaired? What happens to prevent overflow when it is about to overflow?
According to the actual measurement, the run-time overflow of the version above 0.8 will be directly reversed.
Originally, the way to repair is not to allow overflow. int256 is big enough. As long as we ensure that it cannot be used by hackers to create revenue out of thin air, we will succeed.
0x03 vulnerability contract and attack tactics
Take BEC contract as an example. The contract address is:
0xC5d105E63711398aF9bbff092d4B6769C82F793D
The address on etherscan is:
https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
The contract code with overflow vulnerability is as follows:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) { uint cnt = _receivers.length; uint256 amount = uint256(cnt) * _value; //Overflow point, where integer overflow exists 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; }
At that time, the contract version 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 passes in a maximum value (2 * * 255 here) and overflows upward through multiplication, so that the amount (total coins to be transferred) overflows and becomes a very small number or 0 (0 here), thus bypassing the check code of balances [MSG. Sender] > = amount and making it huge_ The malicious transfer of value was successful.
Malicious transfer record of actual attack:
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
0x04 summary
Under the condition of solidity version 0.8 and without using SafeMath Library: hackers often use overflow to construct a minimum / maximum value, so as to bypass some checks and make huge malicious transfers successful.
Of course, the contract vulnerability is not just integer overflow. In addition to the developers themselves to improve their awareness of security development, it is also very necessary to find a professional security team to conduct a comprehensive audit of the contract.