The 29 December 2022 a challenge went public on Twitter, it was the evening and what to do after a day of full of auditing solidity contract? Eat,Sleep,Audit,Repeat?
The goal of the challenge is to steal the 0.1 ETH that are waiting on the vulnerable contract.
Let’s dive into the challenge.. Obviously, the contract is not verified so we have to dive into the bytecode time to use decompilers lads!
Here, we have to go fast because the challenge is public so first come first serve… In addition, The contract size seems to be small. Letβs use dedaub this time (even if heimdall looks really promising).
After using the debugger, we have the following code decompiled:
Inside the contract we don’t have any transfer(), or .call(). But, since we got a delegatecall() at the end of the code we probably need to call it.
So we need to somehow find a way to trigger the delegatecall() on the address β v3 with a EVM bytecode that permits to execute what we wants here will be to empty the contract!.
If we are looking closely to the address v3, we can see that a contract is created and deployed from the the memory of the contract.
Hmmm.. time to make put some malicious bytecode into this memory to create and deployed our malicious contract!
So we probably need to figure out, how could we manage to deploy a malicious evm bytecode to steal the 0.1 ETH. Have to remember the famous 10M bounty from wormhole here β Wormhole Uninitialized Proxy Bugfix Review)
2. Conditions bypass
So for now we need to bypass the first require() .
If the msg.value > 0 is true then the contract will overwrite the value STORAGE[msg.sender] with the block.timestamp + 4095
So, here we know we will have to make two calls:
Setup the value into the storage to bypass the require() using a msg.value > 0.
Then recall the contract with msg.value == 0 to execute the delegatecall().
However, if we are looking closely the :
1
STORAGE_[msg.sender]=4095+block.timestamp;
but the check later on, compares:
1
block.timestamp>STORAGE[msg.sender]
At that point, we got a problem because 4095+block.timestamp will be greater than block.timestamp.
1
STORAGE_[msg.sender]=4095+block.timestamp;
This will be really awfull… As the block.timestamp will not change during the same transaction…
How could we bypass this check? Patience… we have to wait 4095 seconds before calling again the contract.
Obviously, for debugging purposes we can use cheatcode from Foundry like vm.warp(): vm.warp(4096+block.timestamp)to jump in the future of 4096 seconds.
3. Bytecode
We are now bypassing everything! We arriving at the interesting part. henceforth we need to craft our malicious contract to steal the ETH during the delegateCall().
The “base” of the evm bytecode is already made for us here:
1
2
3
MEM[0]=(msg.data.length<<232)+0x61000080600a3d393df300000000000000000000000000000000000000000000;//The "base" evm is then : 0x61000080600a3d393df300000000000000000000000000000000000000000000
We got the 0xF3 at the end (stands for RETURN), so if somehow we succeed to inject the malicious bytecode after the 0xf3(RETURN). We will be rich as yannickcrypto.eth!π
The msg.data is used to determine the length of our contract code. So for example, if the length is 30 bytes the contract deployed will be of 30 bytes.
So just to recap, we have to use the msg.data. To use it, we will use a bytes memory using the hex keyword (e.g below).
The CALLDATACOPY is used to copy the data into the memory here (all the data is copied).
So this will copy the data to the offset 10. (e.g : screenshots & explanation below).
Before CALLDATACOPY this is what looks like the memory.
After CALLDATACOPY here the size is 0x20 because the real payload is 32 bytes (0x20 = 32).
Now, we have to dive into the weird math operationβ¦ Here, I was confused with the output of the decompiler. But lucky for me, I was already familiar with this kind of xor pattern β Reversing & crackme on x86 (PleaseSubscribe).
Now we have the payload we need to make win the challenge!
However, we have to reminder the xor operation!
So using python one last time we can xor our payload with the key.
Thepayloadusednot_clean(yet)_:```solidity// SPDX-License-Identifier: UNLICENSED
pragma solidity^0.8.13;import"forge-std/Script.sol";import"forge-std/Test.sol";contractCounterScriptisScript,Test{functionsetUp()public{}functionrun()public{vm.deal(address(this),1ether);addressvictim=0xA0Eb20483Cb60213bF944c2C3833bebc9fbc4706;stringmemoryvictim_addr="";emitlog_string(" ----------------------- Before Attack ----------------------------------");emitlog_named_uint("Balance of the contract",address(this).balance);emitlog_named_uint("Balance of the victim",address(victim).balance);bytesmemorydata=hex"c9dfadca51bfda9b646381c7546bf68d2c37e248a37bde0f1833c4a029d90519";// hexadecimal payload xored.
victim.call{value:0.1ether}("");vm.warp(block.timestamp+10000);victim.call{gas:1_000_000}(data);emitlog_string(" ----------------------- After Attack ----------------------------------");emitlog_named_uint("Balance of the contract",address(this).balance);emitlog_named_uint("Balance of the victim",address(victim).balance);}}
Challenge Code EVM codes for retrying the challenge!