Last week Zeppelin released their Ethereum CTF, Ethernaut.
This CTF is a good introduction to discover how to interact with a blockchain and learn the basics of the smart contract vulnerabilities. The CTF is hosted on the ropsten blockchain, and you can receive free ethers for it. The browser developer console is used to interact with the CTF, as well as the metamask plugin.
I was fortunate enough to be the first one to finish the challenges. The following is how I did it.
1. Fallback
This challenge is the first one, and I think it is more of an introduction, to be sure that everyone is able to play with the API. Let us see in detail what a solidity smart contract looks like.
Challenge description
The contract is composed of one constructor and four functions. The goal is to become the owner of the contract and to withdraw all the money.
The first function is the constructor of the contract, because it has the same name as the contract. The constructor is a specific function which is called only once when the contract is first deployed, and cannot be called later. This function is usually used to set up some parameters (here an initial contribution from the owner).
function Fallback() { contributions[msg.sender] = 1000 * (1 ether); }
The second function, contribute()
, stores the number of ethers (msg.value) sent by the caller in the contributions map. If this value is greater than the contributions of the contract owner, then the caller becomes the owner of the contract.
function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if(contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } }
getContribution()
is a simple getter:
function getContribution() public constant returns (uint) { return contributions[msg.sender]; }
withdraw()
allows the owner of the contract to withdraw all the money. Notice the onlyOwner
keyword after the signature of the function. This is a modifier that ensures that this function is only called by the owner.
function withdraw() onlyOwner { owner.transfer(this.balance); }
Finally, the last function is the fallback function of the contract. This function can be executed if the caller has previously made a contribution.
function() payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; }
Fallback function
To understand what a fallback is, we have to understand the function selector and arguments mechanism in ethereum. When you call a function in ethereum, you are in fact sending a transaction to the network. This transaction contains, among other things, the amount of ether sent (msg.value
) and a so-called data, which is an array of bytes. This array of bytes holds the id of the function to be called, and the function’s arguments. They choose to use the first four bytes of the keccak256 of the function signature as the function id. For example, if the function signature is transfer(address,uint256), the function id is 0xa9059cbb.
If you want to call transfer(0x41414141, 0x42), the data will be:
0xa9059cbb00000000000000000000000000000000000000000000000000000000414141410000000000000000000000000000000000000000000000000000000000000042
During its execution, the first thing that a smart contract does is to check the function id, using a dispatcher. If there is no match, the fallback function is called, if it exists.
You can visualize this dispatcher using our open source disassembler Ethersplay:
Ethersplay shows the EVM dispatcher structure(*)
(*) For simplification, the Owner inheritance was removed from the source code. You can find the solidity file and the runtime bytecode here: fallback
Solution
If we put everything together we have to:
- Call contribution to put some initial value inside
contributions
- Call the fallback function to become the owner of the contract
- Call
withdraw
to get all the money
(1) is easily done by calling contract.contribution({value:1})
in the browser’s developer tool console. A simple way to call the fallback function (2) is just to send to ether directly to the contract using the metamask plugin. Then (3) is achieved by calling contract.withdraw()
.
2. Fallout
Challenge description
The goal here is to become the owner of the contract.
At first, this contract appears to have one constructor and four functions. But if we look closer at the constructor, we realize that the name of the function is slightly different than the contract’s name:
contract Fallout is Ownable {mapping (address => uint) allocations;
/* constructor */
function Fal1out() payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
As a result, this function is not a constructor, but a classic public function. Anyone can call it once the contract is deployed!
This may look too simple to be a real vulnerability, but it is real. Using our internal static analyzer Slither, we have found several contracts where this mistake was made (for example, ZiberCrowdsale or PeerBudsToken)!
Solution
We only need to call contract.Fal1out()
to become the owner, that’s it!
3. Token
Challenge description
Here we are given 20 free tokens in the contract. Our goal is to find a way to hold a very large amount of tokens.
The function called transfer allows the transfer of tokens between users:
function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
At first, the function looks fine, as it seems to check for overflow
require(balances[msg.sender] - _value >= 0);
Usually, when I have to deal with arithmetic computations, I go for the easy way and use our open source symbolic executor manticore to check if I can abuse the contract. We recently added support for evm, a really powerful tool for auditing integer related issues. But here, with a closer look, we realize that _value and balances are unsigned integers, meaning that balances[msg.sender] - _value >= 0
is always true!
So we can produce an underflow in
balances[msg.sender] -= _value;
As a result, balances[msg.sender]
will contain a very large number!
Solution
To trigger the underflow, we can simply call contract.transfer(0x0, 21)
.
balances[msg.sender]
will then contain 2**256 – 1.
4.Delegation
Challenge description
The goal here is to become the owner of the contract Delegation
.
There is no direct way to change the owner in this contract. However, it holds another contract, Delegate
with this function:
function pwn() { owner = msg.sender; }
A particularity of Delegation
is the use of delegatecall
in the fallback function.
function() { if(delegate.delegatecall(msg.data)) { this; } }
The hint of the challenge is pretty explicit about it:
Usage of
delegatecall
is particularly risky and has been used as an attack vector on multiple historic hacks. With it, you contract is practically saying “here, -other contract- or -other library-, do whatever you want with my state”. Delegates have complete access to your contract’s state. Thedelegatecall
function is a powerful feature, but a dangerous one, and must be used with extreme care.Please refer to the The Parity Wallet Hack Explained article for an accurate explanation of how this idea was used to steal 30M USD.
So here we need to call the fallback function, and to put in the msg.data
the signature of pwn()
, so that the delegatecall will execute the function pwn()
within the state of Delegation
and change the owner of the contract.
Solution
As we saw in “Fallback”, we have to put in msg.data the function id of pwn()
; which is 0xdd365b8b. As a result, Delegate.pwn()
will be called within the state of Delegation
, and we will become the owner of the contract.
5. Force
Challenge description
Here we have to send ethers to an empty contract. As there is no payable fallback function, a direct send of ether to the contract will fail.
There are other ways to send ether to a contract without executing its code:
- Calling
selfdestruct(address)
- Specifying the address as the reward mining destination
- Sending ethers to the address before the creation of the contract
Solution
We can create a contract that will simply call selfdestruct(address) to the targeted contract.
contract Selfdestruct{ function Selfdestruct() payable{} function attack(){ selfdestruct(0x..); } }
Note that we use a payable constructor. Doing so we can directly put some value inside the contract at its construction. This value will then be sent through selfdestruct.
You can easily test and deploy this contract on ropsten using Remix browser.
6. Re-entrancy
Challenge description
The last challenge! You have to send one ether to the contract during its creation and then get your money back.
The contract has four functions. We are interested in two of them.
donate
lets you donate ethers to the contract, and the number of ethers sent is stored in balances.
function donate(address _to) public payable { balances[_to] += msg.value; }
withdraw
, the second function allows users to retrieve the ethers that were stored in balances
.
function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; } }
The ethers are sent through the call msg.sender.call.value(_amount)().
At first, everything seems fine here, as the value sent decreased in balances
[msg.sender] -= _amount;
and there is no way to increase balances without sending ethers.
Now recall the fallback function mechanism explained in “Fallback.” If you send ethers to a contract containing a fallback function, this function will be executed. What is the problem here? You can have a fallback function which calls back withdraw
, and thus msg.sender.call.value(_amount)()
can be executed twice before balances[msg.sender] -= _amount
is executed!
This vulnerability is called a re-entrancy vulnerability and was used during the unfamous DAO hack.
Solution
To exploit a re-entrancy vulnerability you have to use another contract as a proxy. This contract will need to:
- Have a fallback function which will call
withdraw
- Call
donate
to deposit ethers in the vulnerable contract - Call
withdraw
In our not-so-smart-contracts database, you will find an example of a generic skeleton to exploit this vulnerability. I’ll leave the exercise of adapting this skeleton to the reader.
Similar to the previous challenge, you can test and deploy the contract on ropsten using Remix browser.
Conclusion
This CTF was really cool. The interface makes it easy to get into smart contract security. Zeppelin did a good job, so thanks to them!
If you are interested in the tools mentioned in the article, or you need a smart contract security assessment, do not hesitate to contact us!
Article Link: Hands on the Ethernaut CTF | Trail of Bits Blog