Knownsec Blockchain Lab｜In-depth understanding of reentry attack vulnerabilities
The concept of smart contract was first proposed by Nick Szabo in 1995. It is a computer protocol designed to spread, verify, or execute contracts in an information-based way. Trust transactions, these transactions are traceable and irreversible.
However, smart contracts are not secure. Re-Entrance (Re-Entrance) attack vulnerability is one of the attack methods in Ethereum. As early as 2016, the DAO incident caused Ethereum's hard fork.
In Ethereum, smart contracts can call the code of other external contracts. Since smart contracts can call external contracts or send Ether, these operations require the contract to submit external calls, so these external calls can be exploited by attackers to cause attacks. Hijacking allows the attacked contract to be re-executed at any location, bypassing the restrictions in the original code, and causing a reentry attack. The reentrance attack is essentially similar to the recursive call in programming, so it may happen when the contract sends Ether to an unknown address.
To put it simply, there are 2 conditions for a reentry attack vulnerability:
(1)An external contract is called and the contract is insecure
(2)The function call of the external contract is earlier than the modification of the state variable
Here is an example of a simple code snippet:
The above code snippet is the simplest withdrawal operation. Next, I will give you a detailed analysis of the cause of the reentry attack.
Before the formal analysis of reentry attacks, let's first introduce a few key knowledge.
Since the reentry attack will be sent during the transfer operation, the transfer method commonly used in Solidity is
<address>.transfer(), <address>.send() and <address>.gas().call.vale()(), the three transfer methods are described below:
(1) <address>.transfer(): Only 2300 gas will be sent for the call. When the sending fails, the rollback operation will be performed through throw, thus preventing reentry attacks.
(2) <address>.send(): Only 2300 gas will be sent for the call, and the boolean value false will be returned when the sending fails, thus preventing reentry attacks.
(3) <address>.gas().call.vale()(): All gas will be sent when the call is made, and the boolean value false will be returned when the sending fails, which cannot effectively prevent reentry attacks.
Then we will explain the fallback function.
Fallback function: The fallback function is one and only one function without a name in each contract, and the function has no parameters and no return value, as shown below:
The rollback function is executed in the following situations:
(1) No function was matched when calling the contract;
(2) No data is transmitted;
(3) The smart contract receives Ether (in order to accept Ether, the fallback function must be marked as payable).
The following code is a reentrance attack. It implements a contract similar to a public wallet. All users can use deposit() to deposit into the Reentrance contract, or use withdraw() to withdraw from the Reentrance contract. Of course, everyone can also use balanceof() to check their own or other people's balance in the contract.
First use an account (0x5B38Da6a701c568545dCfcB03FcB875f56beddC4) to play the victim, and click the Deploy button to deploy the contract in the Remix IDE.
After successfully deploying the contract, fill in 5 in the VALUE setting box, change the unit to ether, and click deposit to deposit 5 ether.
Click wallet to check the balance of the contract and find that the balance is 5 ether, indicating that our deposit is successful.
The following code is an attack against the above vulnerable contract:
Use another account (0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2) to act as an attacker, copy the vulnerable contract address to the Deploy setting box, and click Deploy to deploy the attack contract above.
After successful deployment, first call the wallet() function to check that the balance of the attacking contract is 0.
The attacker first deposits 1 ether into the vulnerable contract, where VALUE is set to 1 ether, and then clicks deposit on the attack contract to deposit.
Call the wallet function of the contract again to check the balance of the vulnerable contract and find that it has become 6 ether.
The attacker (0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2) calls the attack function of the attacking contract to simulate the attack, and then calls the wallet function of the attacked contract to check the balance of the contract and finds that it has returned to zero. At this time, he returns to the attacking contract to check the balance and finds 6 ether in the attacked contract. All funds have been withdrawn to the attacker's contract, which caused a reentry attack.
4.Source code analysis
The above explained how to carry out reentry attacks and the reasons for the vulnerabilities. Here, the source code of the vulnerabilities and the steps of the attack are sorted out, and the key codes are listed.
On June 17, 2016, TheDAO project suffered a reentry attack, resulting in more than 3 million Ether being separated from the TheDAO asset pool, and the attacker used the splitDAO() function in the TheDAO smart contract to reuse his DAO Assets undergo re-entry attacks, and DAO assets are continuously separated from the asset pool of TheDAO project and transferred to their own accounts.
The following code is part of the splitDAO() function. The source code is in TokenCreation.sol, which will transfer tokens from the parent DAO to the child DAO. The balance array uint fundsToBeMoved = (balances[msg.sender] * p.splitData.splitBalance) / p.splitData.totalSupply determines the number of tokens to be transferred.
The following code is to perform the withdrawal reward operation. Each time the attacker calls this function, p.splitData is the same (it is an attribute of p, that is, a fixed value), and p.splitData[ The values of 0].totalSupply and balances[msg.sender] occurred after the transfer operation due to the order of functions and were not updated.
paidOut[_account] += reward The updated state variable is placed after the call to the payOut function of the problem code.
Call .call.value to _recipient, transfer _amount Wei, .call.value call will use all the remaining gas by default.
Through the above analysis of reentry attacks, we can find that the key to reentry attack vulnerabilities is to use fallback and other function callbacks to cause recursive calls to perform cyclic transfer operations. Therefore, there are several solutions to reentry attack vulnerabilities.
1.Use other transfer functions
Use Solidity's built-in transfer() function when transferring Ether to an external address, because transfer() will only send 2300 gas for the call, which will not be enough to call another contract. Use transfer() to rewrite the original The withdraw() of the contract is as follows:
2.Modify the state variable first
This method is to ensure that the modification of the state variables is earlier than the transfer operation, that is, the check-effects-interactions mode (checks-effects-interactions) officially recommended by Solidity.
Mutex is to add a state variable that locks the contract during code execution to prevent reentry attacks.
4.Use OpenZeppelin official library
There is a security contract specifically for reentry attacks in the official OpenZeppelin library:
1. Several hard forks of Ethereum:
2. Ethereum smart contract security vulnerabilities (1): re-entry attacks:
3. Those things about the blockchain — THE DAO attack source code analysis:
About Us： Knownsec Blockchain Lab has a team of top international blockchain security experts and 9 years of experience in security services for leading blockchain companies. It has served as the world’s leading digital currency exchange, wallet, underlying public chain, Smart contracts and other projects conduct security audits and defense deployments, and maintain the leading domestic core competitiveness in blockchain technology security, risk control security, and anti-hacking security.