Ethernaut walkthrough [#5]
This is the fifth part of the Ethernaut challenges walkthrough. In this series, I try to share my experience of learning Solidity via the Ethernaut challenges. You can find the previous part here.
The code solutions could be found here:
however, I suggest trying to solve the challenges first as the posts are structured in a walkthrough fashion (with hints). Of course, if you feel stuck at any moment, look at my code implementations and try again yourself.
Level 14 — Gatekeeper Two
By all means, a more mathematically forgiving challenge than the previous gatekeeper. You need to pass through the different gates.
Gate One is quite straightforward. It boils down to the enter() call originating from an external contract.
Gate Two is somewhat different than previously encountered modifiers.
There are 2 essential points.
- It makes use of the assembly keyword. You can read more on that in the official docs here, but it basically allows programmers to use a syntax that is much closer to the Solidity Virtual Machine than Solidity. This is targeted towards “very fine-grained control”, mainly required when writing libraries meant to be used widely.
- The next unknown is the extcodesize() call. As always, you can find what it does in the docs.
So, the next thing is to find the caveat of how a contract’s size could be 0. It sounds illogical, since the contract’s address should hold the contract’s code and that requires memory. However, think about the point during a contract’s deployment after which the code actually starts “living” on that address. (Or google it…)
Gate Three builds on some classical knowledge from Computer Science Logic courses — the properties of the XOR(^) operator:
a ^ b = b ^ a
a ^ (b ^ c) = (a ^ b) ^ c
a ^ a = 0
a ^ 0 = a
Using this, you can show that:
a ^ b = c | (a ^ ) both sides
a ^ (a ^ b) = a ^ c
(a ^ a) ^ b = a ^ c
0 ^ b = a ^ c
b = a ^ c
Now, think of: a — hashed attack contract address, b — gateKey parameter, c — uint64(0)-1.
Combine all these approaches in the attack contract’s constructor (hint ad gateTwo) and you’ve got the solution.
Level 15 — Naught Coin
For this challenge, you should get familiar with the concept of ERC20 tokens and their basic functions. The best resource is the OpenZeppelin official documentation.
For the solution itself, you don’t really need a contract, you just have to figure out a way to transfer without using the classical transfer() function.
Maybe look for the combination of the two ERC20 functions that will allow for a similar transfer of coins as transfer().
Level 16 — Preservation
Exactly 10 levels later (ref. to Level 6 — Delegation) an exploit is made possible thanks to the delegateCall() function. If you start with the end goal of the instance, you understand that somehow you should end as the owner of the contract.
If your idea of delegateCall() is somewhat blurry, just remember the crucial fact that:
delegateCall() is executed from the context of the caller. If you don’t remember or can’t visualize what that means, just go back to the walkthrough of Level 6 and study the CALL vs. DELEGATECALL comparison picture.
In conclusion, your end goal is to create an attacker contract that mimics the memory layout of the Preservation instance up to the owner. So, you need to have 2 arbitrary public addresses and a third address would be the owner. Afterwards, you’ll need a malicious setTime(uint _time) function (make sure it has that exact same name, since you’ll need the have the same encoded keccak256 value. So, think about what value you’d like to overwrite in your malicious contract’s setTime() function. Hint: the function should be just 1 line and you’ll need to call it twice.
Another thing that you might’ve noticed is that you pass an uint value, whereas you are trying to overwrite an address value (another hint). Is that possible and how would you approach transforming an uint to an address?
P.S. If you insist on using pragma ^0.8.0, an explicit type conversion from uint256 to address results in an error/warning. To resolve that, consider the fact that an equivalent conversion would actually be from uint160 to address.
Hopefully, my hints helped you along the way. Maybe you jumped straight to the solutions repo, then came back to make sure you got everything right. Either way, the important thing is to have understood the topics and the issues that made the exploits possible. As always, feedback is highly appreciated :)
Part [#6] coming soon…