Project Sekai 2024 - Play To Earn [blockchain]
Difficulty : Easy to medium
Team : Phreaks2600
You can buy coins.
Of course, you can exchange it back to cash at the original purchase price if there is any left after playing :)
TL;DR
- Start an instance
- Register my address
- Exploit bad ecdsa signature verification to allow myself some funds to transfer coins from another address to myself
- transfer coin from address(0) to my address
- withdraw the ether an get the coin
Introduction
We got three contracts ArcadeMachine.sol
, Coin.sol
and Setup.sol
The main contract is Coin.sol
which have the ether to steal.
To solve the challenge we need to have 13.37 or more ether on our balance and to be registered, in Setup
contract :
|
|
To register we have to interact with Setup
with the function Register
|
|
Setup
But before all of that we got to start instance, i will be short on this part but all we had to interact with the server and choose to start an instance with the command:
ncat --ssl play-to-earn.chals.sekai.team 1337
.
And it will give us the setup address, my address, my private key, and URL and a personnal UUID for identifiying myself and get the flag.
And we can get Coin and ArcadeMachine addresses by using cast on Setup contract as they are defined in the latter:
|
|
For Coin : cast call -r $RPC $SETUP "coin()"
For ArcadeMachine : cast call -r $RPC $SETUP "arcadeMachine()"
I set up an environment variable via a file .env
file to go faster and a way to get Coin and ArcadeMachine address automatically each time i apply the .env
file with source .env
.
|
|
check_wallet
is to rapidly check if the instance is still up because we only got 30 minutes per instance.
Recon
I can register by interacting with setup contract : cast send -r $RPC --private-key $PKEY $SETUP "register()"
which grant me 1337 coins.
Using withdraw()
from Coin contract i can convert this coin to ether.
Also the challenge name Play to earn
stand for the ability to play the arcade machine using the function play(uint256)
|
|
It calls transferFrom(src, dst) so it transfer something from sender to address(0) (which is an address with only zero, so it corresponds to no one, its just here like a void) : 0x0000000000000000000000000000000000000000
)
Let’s look at transferFrom()
:
|
|
If dst != src
and allowance[src][sender]
(interpreted as the allowance of the sender on src allowance list) != 115792089237316195423570985008687907853269984665640564039457584007913129639936 (these conditions are always True in this challenge if you transfer coin to another account)
Then it require msg.sender to have enough allowance on src account.
So it transfer coin from src account to dst account (if src != msg.sender, substract the value to transfer from msg.sender allowance on src) and then substract the transferred amount from src coin balance and increase dst balance.
Then, play(uint256 times)
function transfer 1 ether == 10**18 * times to msg.sender coin balance to address(0) coin balance.
From the challenge scenario, play(19)
is used at setup that’s why there is a comment saying people played before us.
|
|
So actually, address(0) got a balance of 19 * 10**18 coins, which will makes 19 ether if they are withdrawn.
To get these we need to find a way to transfer coins from address(0) balance to our balance using transferFrom()
, for that we need to have an allowance amount superior equal the value we want to transfer and then withdraw them.
To make short, we have two interesting functions, privilegeWithdraw
and permit
.
The first one transfer address(0) coin to msg.sender but we need to be the owner of the contract so it sucks.
|
|
The second one allow us to set allowance value for a spender
address on owner
address if we manage to confirm that spender
is the owner
(specified in the function, not the contract owner) by verifiying a signature with ecdsa.
|
|
So we need to have signer == owner
meaning : ecrecover(h,v,r,s) == 0x0000000000000000000000000000000000000000
Vulnerability
Here the vulnerability is that we can choose the v, r and s parameter in order to get a null signature.
Testing ecrecover()
locally i found that with any h
if v = r = s = 0
, then ecrecover(h,v,r,s) == 0 == address(0)
With this test contract :
|
|
Solving
So finally we need to :
- register
- call permit() with these args:
owner=address(0), spender=our_address, value=19*10**18, deadline=timestamp, v=0, r=0 and s=0
- transfer coins
- withdraw
- get the flag
calling permit : cast send -r $RPC --private-key $PKEY $COIN "permit(address,address,uint256,uint256,uint8,bytes32,bytes32)" $(cast address-zero) $ADDRESS 18000000000000000000 1732416543 0 $(cast to-bytes32 0) $(cast to-bytes32 0)
I can now check my allowance on address(0) allowance mapping : cast call $COIN -r $RPC "allowance(address, address)(uint)" $(cast address-zero) $ADDRESS
-> 1.8e+19
I now transfer my self the coin : cast send -r $RPC --private-key $PKEY $COIN "transferFrom(address,address,uint256)" $(cast address-zero) $ADDRESS 18000000000000000000
And withdraw them : cast send $COIN -r $RPC --private-key $PKEY "withdraw(uint)" 18000000000000000000
-> 18.999999999999776957
Now i can interact with instance, submit my uuid and get the flag : SEKAI{0wn3r:wh3r3_4r3_mY_c01n5_:<}