[ad_1]
- Token Sale
- Token Whale
- Retirement Fund
Capture The Ether is a CTF (capture the flag) game based on Solidity and the EVM, where the goal is to hack multiple smart contracts and steal the funds.
Don’t know when to buy and sell cryp, Try Copy trading.
I’ve done (and written/kept in Notion) these challenge for many month ago, and sadly, these challenges are not playable anymore (at least from its website) as it was deployed on Ropsten, a deprecated Ethereum testnet.
But I thought it was still interesting to share this with you and show I beat some of these challenges. It’s possible you will learn some things by reading this article.
Last thing : I didn’t do much rewriting of the articles since they were written. I did these challenges rigth after the CryptoZombies tutorial.
If you’re looking for this kind of challenges you also have Ethernaut or DamnVunerableDeFi.
disclaimer : these challenges are based on solidity 0.4.21 which is a pretty old version now. There can be some differences from the latest (0.8.x) but most of the things still apply. The biggest change that affect these contract are overflow and underflow that could happen during addition, subtraction and multiplications. Since solidity 0.8.0 these operators have safeguards directly integrated. Before 0.8.0, people were using @openzeppelin/SafeMath library to protect the contracts from overflow/underflow
code to hack :
This contract simulate a very simple DEX where a user can buy tokens with ETH, or sell tokens with what he got in his balance (balanceOf mapping)
This challenge is part of the “Math” section, meaning solution have something to do with numbers and calculations. This gives us some hints.
First thing I thought was “overflow/underflow”
Overflow and underflow
Underflow and overflow happens when we try to fit a number which is too big or too small into a data type.
For example, trying to put the value 260 into a uint8 (unsigned integer 8 bits) will generate an overflow, because uint are bounded between [0;255] (the biggest value of a uintN is 2^N -1)
Same for the value -5 which will generate an underflow.
In solidity, when an overflow/underflow happens, the number just wrap to the first or last number of the datatype
260 into a uint8 will then give 255+5 = 4 [255 (+1)>> 0 (+1)>> 1 (+1)>> 2 (+1)>> 3 (+1)>> 4]
While -5 will give 0–5 = [0 (- 1)>> 255 (- 1)>> 254 (- 1)>> 253 (- 1)>> 252 (- 1)>> 251]
I showed you this with addition and subtraction, but obviously it also works with multiplication as the resulting number can fall outside of the bounds.
Going back to the contract
So, now that we know that, can you find where this kind of behavior can happen in the code ?
What I like to do for this challenge section is to write down as a comment locations where this can happen, let’s see what we got :
As you can see, there’s 4 lines marked with a comment : 3 potential overflows, and 1 underflow.
Now we have to think which one can be responsible of such an awful situation
First one :
require(msg.value == numTokens * PRICE_PER_TOKEN); //overflow
Here, we check ifnumTokens
[uint256] multiplied by PRICE_PER_TOKEN
[uint256] is equal to the value of ETH the user sends (msg.value here)
No suspens here, this this where the overflow happens. But how ?
PRICE_PER_TOKEN
is equal to 1 ether as shown at the beginning of the contract. So, whatever value we put in numTokens
, this should gives us a uint256 * 1, so no overflow, right ?..
Wrong. On the EVM, 1 ether is equal to 10¹⁸ or 1e18 wei
Wei is the smallest unit of ether (see explanations here)
So, in fact PRICE_PER_TOKEN
isn’t equal to 1, but to 10^18. You see know what could happens ?..
The biggest value of a uint256 is (2²⁵⁶)-1, which is equal to :
115792089237316195423570985008687907853269984665640564039457584007913129639934
So, if we multiply 10¹⁸ by 115792089237316195423570985008687907853269984665640564039457
(I removed the last 18 numbers), we have :
115792089237316195423570985008687907853269984665640564039457000000000000000000
(simply added 18 ‘zeros’)
This is still less than 2²⁵⁶, so no overflow, in fact, we’re 584007913129639934
from the max value.
But if I multiply 11579208923731619542357098500868790785326998466564056403945**8**
(i added 1, last digit changed) by 10^18, now we’re bigger than the uint256 by 0.416 ETH (see the quick math below)
11579208923731619542357098500868790785326998466564056403945**8000000000000000000**
(-)11579208923731619542357098500868790785326998466564056403945**7584007913129639934**
= ___________________________________________________________ **415992086870360064**
= ___________________________________________________________ 0.416 ETH
So, giving as numTokens
this value, we just have to send 415992086870360064
wei to the contract, which as said above is around 0.416 ETH.
Okay cool, but “what now” you think, right ?
Give a look at the next line :
balanceOf[msg.sender] += numTokens;
The contract will think that we have 1157920892373.....564039458000000000000000000
in our balance !
That means we can sell as much token as the contract hold, as far as it is less or equal to our balance.
As the winning condition of this challenge is :
function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}
By sending 0.416 ETH we are able to retrieve 1 ETH, hackerz right ?
It worked as expected ! We have a balance of 115792089237316195423570985008687907853269
ETH.
Now we just have to call the sell function with the value 1 to take 1 ETH.
Btw, in real situation there could be way more than 1.41 ETH in the contract balance, in this case, I would have been able so withdraw everything…
Scary right ?
code to hack :
This contract simulate an ERC20 contract, with its total supply, the balances for each addresses and function to transfer tokens from one to another.
I’ve already put the overflow and underflow comments on vulnerable lines. The goal of this challenge is to have a balance >1M tokens, when the total supply is 1000 and with no way to mint new tokens.
There is 4 functions, 3 being publicly accessible (can be called by external contract or by an EOA)
The other one is internal, that means that only the contract itself can call it, or any contract that inherit from this one.
Let’s take a look at these functions one by one.
_transfer
takes as input the target account and the quantity of token we want to transfer, and update the balances adequately.
The sender sees his balances lowered by the amount of token he is transferring, and the recipient sees it increased by the same value.
As we can see, no protection are present here. But as this function is internal, we cannot call it directly.
We will still keep it in mind.
Next function :
This is the public function of _transfer
. We see here why there is two functions.
Transfer do some security check with require statements before calling _transfer
We see here that an overflow is possible in the require statements. That doesn’t mean the code is vulnerable here though.
Next function :
This function allow the sender to approve another user (the spender) to spend tokens on his behalf.
This update a mapping called allowance
, which is checked whenever a spender want to spend tokens from another user.
Next function :
This is where allowance
is used. This function allow someone to transfer token from any address to another one, but this can only be done if he is allowed to, and only for the allowed quantity of token.
We can see that again the _transfer
function is called.
So, what can we do here, there’s a bunch of overflow/underflow vulnerabilities, right ?
We need a way to use them !
The issue is in the logic of the functions itself. The developer made a mistake, can you see it ?
Solving the challenge
The vulnerability is in the transferFrom
function.
Let’s check each line :function transferFrom(address from, address to, uint256 value) public
we transfer value
from from
to to
Let say from = msg.sender = A
, to = B
and value=1000
This gives us :
So, to send tokens from A to B, we first check that balance[B]+value is still superior to balance[B]. This is an overflow protection 😉
Then we check that A allowed A to spend these tokens (value)
Then we subtract the value to the value A is allowed to spend from himself
And then we transfer tokens from A to B.
Okay, no issue here.
Let’s do it again, but this time we want to send these tokens from B to C.
And why not be the owner of the B too ? Anyone on the blockchain can be the owner of as many address as he wants.
- So, with B, we allow A to spend 99999999 tokens using the
approve
function, using B wallet. - Now, we connect again with wallet A, transfer from B to C, and C will also be our wallet.
Did you see that ?.. We’re sending tokens from B to C, but in the logic this is the allowance from B to A that is checked !
This isn’t normal at all. Here is our vulnerability.
Let’s see what happens when we call _transfer
now :
Now A have way more than 1M token in his balance, and the challenge is solved.
Here a visualization I made on Excel to help my weak brain to find the solution, this will probably help you to understand the mecanism if it wasn’t clear :
contract to hack :
This challenge is based on a vault contract, where a user can lock his ETH for 10 years. If he tries to withdraw them before the 10 years period, he will get only 9/10 of the locked value.
If he do so, then he could call this contract a HODL vault.
The challenge here is to steal the totality of the balance the poor person locked in.
Here the owner is the challenge, and the beneficiary is myself.
Unless the owner withdraw his fund, I will not be able to collect penalty because withdrawn will be equal to 0.
So, this is what prevent us to collect the fund, the withdrawn value as we have no way modify the balance, the only one who can do that is the owner with the withdraw()
function.
If the contract had a fallback()
or receive()
function, I would have been able to send Ether and change its balance, and then change the value of the withdrawn
variable, but there is no such function in his contract.
Really ?..
I was able to solve this challenge pretty quickly because I was able to remember a vulnerability I discovered in an article.
It is possible to send Ether to a contract, even if he do not have these functions implemented.
For that we will use the selfdestruct(address target)
function which simply removes all the bytecode from the contract where the function is called, and send the remaining Ether to the target contract.
So, I created a contract to put this in practice :
I then deploy this contract with Remix, using the RetirementFundChallenge address as input to the constructor.
Then :
- I send 0.5 ETH to my Attack contract using
sendEtherToContract()
- I call the
attack()
function which destroy the Attack contract and sends the 0.5 ETH to the RetirementFundChallenge contract - Now if I call the
collectPenalty()
function, the result ofstartBalance - address(this).balance
will be equal to 1, which means the require statement is >0 and is passed - I withdraw all the funds from the contract !
Thanks for reading ! See you next time on another security related post 😉
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing
[ad_2]
Source link