“Reentrancy Attack” on a Smart Contract
Bugs in Solidity are costly, putting yourself and many others at risk, so its important to take precautions when writing and deploying smart contracts. We’re going to explore one of those bugs, a recursive send exploit. We’re going be going through a simplified reentrancy attack scenario using 2 smart contracts, a Victim and an Attacker contract.
Prerequisites:
Its expected that you have a basic understanding of the Ethereum Blockchain technologies and the programming language Solidity, a smart contract concept, which compiles down to EVM bytecode.
If you don’t know what Smart Contract’s are or Solidity is, explore these links below:
- Im using a mac, apologies and forewarning’s.
2. NodeJS — is a JavaScript runtime built on Chrome’s V8 JavaScript engine you’ll need at least version 6.9.1.
3. NPM packages:
$ npm i -g ethereumjs-testrpc
The first package we install is ethereumjs-testrpc, which is a “NodeJS based Ethereum client for testing and development”. TestRPC is a private blockchain with its own genesis block, that has all the functionality of the live Ethereum blockchain.
$ npm i -g truffle
The second package truffle, is a deployment and testing framework built to make contract deployment and management easier for the developer. We’re going to use this framework for our deployment flow and the console that it provides, which is a NodeJS console with a couple of extra packages injected into it.
Getting Started:
We’re going to run a few commands to scaffold out our project.
$ mkdir reentrancy_attack
$ cd reentrancy_attack
The commands above create our project folder and then changes to that project folder.
$ truffle init
$ rm -rf test/ contracts/ConvertLib.sol contracts/MetaCoin.sol
$ touch contracts/Attacker.sol contracts/Victim.sol
These commands are going to delete and create a couple files, the ones we’re going to delete we will not need, they were created when we ran truffle init
. The files we’re creating are going to hold our contracts code that we will eventually be deploying.
Victim’s Contract:
The contents below go inside of our Victim.sol
file.
This is the Victim contract with the reentrancy vulnerability. The withdraw
function is where the vulnerability actually lies and inside the if
statement the victim writes !msg.sender.call.value(transferAmt)()
which is an external call. The external call problem:
“Avoid external calls when possible. Calls to untrusted contracts can introduce several unexpected risks or errors. External calls may execute malicious code in that contract or any other contract that it depends upon. As such, every external call should be treated as a potential security risk, and removed if possible.” — Ethereum Wiki
The withdraw
function sends 1 ether to the msg.sender
, which in this case is the attacker. The attacker should only be able to receive that 1 ether per call, but we’ll see how an attacker is able to call the function more than once before it finishes, the recursive send exploit.
We're only using the deposit()
function to transfer some ether to the Victim
contract. The contract starts with 0
”ether on deployment, so we’ll need to send the contract some ether so the attacker can swipe it (evil laugh).
Attacker’s Contract:
The contents below go inside of our Attacker.sol
file.
This is the attackers contract code, they simply declare a contract called Attacker
. The attacker declares a variable v
(on line 6) and sets that variable to type Victim
(you can use contracts as explicit types in Solidity). Then in our constructor function (on lines 11-13) we cast the deployed Victim
contract’s address to our Victim
type and set that to be the v
variable.
Then we declare two functions:
attack()
function () payable {}
(our fallback function)
The attack()
function calls the v.withdraw()
method which calls the deployed Victim
contracts withdraw
method (see lines 5–8 on the Victim
contract above).
Once this happens, 1 ether is sent to the attacker using the withdraw
method. In our case the 1 ether is sent to our Attacker
contract, its all fine and dandy until the sent ether actually arrives at the malicious contract code. Thats when the second Attacker
contract function is called, the fallback function.
The fallback function is invoked every time ether is sent to the Attacker
contracts address. The payable
modifier is actually what allows the contract to receive ether. When ether is sent to the Attacker
contract, the fallback function is ran (almost like a “fishing net” to catch all the ether that is sent to this contract) which in turn causes several steps to take place:
- The
LogFallback
event fires every time the fallback function is called. (more on events in this post) - Then we have an
if
statement that just stops this function from running more than 10 times, this keeps thewithdraw()
call from running out of gas and having the stolen ether reverted. - Then
v.withdraw()
is called again!!!
Before the original call to v.withdraw()
ever finishes it calls the v.withdraw()
method again which continues to recursively call itself until the count
meets the condition.
Deployment Code:
The contents below go inside of our 2_deploy_contracts.js
file.
Go ahead an delete the code that is in this file and replace it with the code above. The code above allows us to control the deployment flow of our 2 contracts, in two simple steps:
- So we deploy our
Victim
contract to the TestRPC blockchain. - We wait for the results of that deployment.
- Once the deployment was successful we take that address of the freshly deployed
Victim
contract and create an instance of ourAttacker
contract with that address passed to theAttacker
contract’s constructor function.
Now to the Console:
You’re going to want to have TestRPC running in the background so we can deploy our contracts to it.
$ testrpc -u 0
This will start up TestRPC with the first account in the web3.eth.accounts
array unlocked to send ether to our Victim
contract.
Deploying our contracts:
$ truffle compile
This command is going to create a build
folder, compile the 2 contracts down to EVM bytecode and create the ABI for both contracts.
$ truffle migrate
This command is what actually deploys our contracts to TestRPC. You should see your TestRPC log some block hashes and their transactions.
$ truffle console
Running this command puts us in a NodeJS repl, where we have access to a couple of global variables that we can use (more on that here, needs link).
Inside of the Truffle console:
> acct1 = web3.eth.accounts[0]
First we going to set a variable acct1
, thats going to be equal to the first account in the web3.eth.accounts
array. When starting TestRPC you get 10 accounts that are uploaded with 100 ether each. We going to be using this account to send money to our Victims
contract.
> Victim.deployed().then(contract => victim = contract)
Here we are going to get our deployed Victim
contract instance and set it to a variable victim. This allows us to interact with our deployed contract.
> getBalance = web3.eth.getBalance> balanceInEth = address => web3.fromWei(getBalance(address).toString())
Here we set up a helper method that will check the account balances in their ether denomination. When checking account balances with the normal web3.eth.getBalance(address)
it gives you the results in Wei. Now we can check the balances of our Victim
contract and our acct1
address (external account #1 from TestRPC) in ether.
> balanceInEth(victim.address)
"0"> balanceInEth(acct1)
"99.789101234"
After checking the balances, acct1
has a bunch of ether and the victim doesn’t have any. Lets share some of that with our Victim
.
> options = { from: acct1, to: victim.address, value: web3.toWei(11, 'ether') }> victim.deposit.sendTransaction(options)
"very-long-transaction-hash-that-keeps-going"
The fact that we made a function deposit
that was payable
allows us to send money to our Victim
contract. Now when we check the balances of our two accounts the changes are reflected.
> balanceInEth(acct1)
"88.678910"> balanceInEth(victim.address)
"11"
Now we can see that our acct1
balance has 11 less ether plus some gas expenses (senders pay transaction fees) and now the victim has 11 ether.
Now for the Attack!!:
> Attacker.deployed().then(contract => attacker = contract)
Like before, we’re getting our Attacker
contract instance which allows us to access the methods that we defined when we originally deployed our Attacker
contract. Now lets check the balance of this contract.
> balanceInEth(attacker.address)
"0"
The attacker doesn’t have any ether in their account. Lets change that.
> attacker.attack()
If the command runs correctly, you’ll see similar logs like the ones below.
The logs above represent the LogFallback
events that we’ve emitted and its strictly for feedback to show us the recursive calls. You should be able to see an array logs: […]. This shows us with a single attack()
call, that we where able to call v.withdraw()
9 additional times along with our original call, BAD.
Now lets check the balances of the parties involved.
> balanceInEth(attacker.address)
"10"
Now the Attacker
contracts balance has 10 ether.
> balanceInEth(victim.address)
"1"
The Victim
contracts balance is less than it was before, with an entire 10 ether gone, poof.
What Happened?:
Since we didn’t implement code to block concurrent calls from happening in the withdraw
function, the withdraw
function can be called multiple times before the original invocation finishes. When using msg.sender.call.value(ethAmt)()
an external call it puts you and your contract code at risk. External calls may execute malicious code resulting in you losing the control flow of the function call and potentially losing ether that the contract holds.
Solutions:
There are 3 solutions we could use to protect contract code from a “Reentrancy Attack”, but only 2 apply to our situation because we are not keeping track of any balances (more on this in a sec):
- The first and simplest thing you could do is replace
msg.sender.call.value(ethAmt)()
withmsg.sender.send(ethAmt)
which also allows you to execute external code, but limits the gas stipend to 2,300 gas, which is only enough to log an event, but not launch an attack. - We can also create a
Mutex
modifier which will lock the function, effectively blocking any additional calls while thewithdraw
function is already in use:
3. The final solution which does not apply to our contracts, but its common enough and deserves a mention. Its called zeroing out which you set the attackers balance to 0
before the ether is even sent. This limits what they can withdraw from the contract to only what they have. It relies on the attacker having some sort of stake in the contract, which is good because you can set the limit in the withdraw function and check if the attacker is a stake holder.
and if sending the ether fails for some reason just give it back.
Conclusion:
As you can see Solidity is like any other programming language but the same mistakes can be much more costly (literally). All writes
in Ethereum cost some sort of gas and untracked bugs can cost you ether and some of that ether might not even be yours. So do your due diligence when writing these contracts. Be sure to take advantage of the tools available and the many Testnets in the world today. Feedback welcome and Thank you for your time.
—
ETH: devonwesley.eth
BTC: 1A4ygq9YrUsXabo5iF8PfxocGTGZrPzoXt