Skip to content

nikeshnazareth/ethernaut-attempt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

My Ethernaut Attempt

Overview

The Ethernaut is a capture-the-flag style contest for finding bugs in Solidity contract.

It was created by OpenZeppelin and is available here.

I will use this repository to describe my thought process, hold any code I write and document any useful insights.

Implementation note

  • The contracts are compiled with solc 0.4.18.
  • We are currently using solc 0.5.x.
  • Although truffle can be configured to use an old compilers, it does not produce the legacyAST variable, which causes errors.
  • Whenever I want truffle to produce an ABI for a level, I modify the code to be compatible with the latest compiler.

Progress

This level is designed to get used to interacting with contracts through the console.

  • The level presents a series of instructions (eg. call the function info())
  • It eventually instructs us to enter the password in authenticate
  • There is a state variable named password set to ethernaut0
  • Pass that to authenticate to complete the level
  • There is a Fallback contract
  • The goal is to take ownership and reduce the balance to 0
  • There is a contributions variable that tracks how much each user has contributed
  • The constructor assigns 1000 ETH to the owner ( notionally - the contract does not own any ETH )
  • There is a payable contribute function:
    • You must send less than 0.001 ETH
    • Your contribution gets recorded against your account
    • If you become the highest contributor, you also become the owner.
  • Obviously, calling contribute a million times is not the desired solution
  • There is a withdraw function that lets the owner drain the contract
  • There is a fallback function:
    • You need to send some ETH
    • You need to already have contributed some ETH
    • You become the owner

So the strategy is:

  1. Call contribute with 1 Wei (so we can call the fallback function)
  2. Call the fallback function with 1 Wei to become the owner
  3. Call withdraw to drain the contract

This is implemented in migrations/level1.js

  • There is a Fallout contract
  • The goal is to get ownership of the contract
  • There are some irrelevant functions to adjust the allocations (which are also buggy - sending allocations does not reduce the balance)
  • The supposed constructor is misspelled (as Fal1out) which means it is callable
  • It sets the owner to the caller

So the strategy is to simply call that function and become the owner.

This is implemented in migrations/level2.js

Note that this bug is mitigated with the new syntax, where constructors are not named after the contract but are just called constructor.

  • There is a CoinFlip contract
  • There is a flip function that accepts a guess, generates a random bit and updates the winning streak:
    • If the guess matched the random bit, increment the streak
    • If the guess did not match the random bit, reset the streak to 0
    • Note: the guess is a boolean, and the random bit is cast to a boolean before comparison
  • The goal is to get a winning streak of 10
  • The random number generation mechanism within flip is:
    • Ensure this is the first time the function is called this block
    • Divide the previous block's hash by 0x8000000000000000000000000000000000000000000000000000000000000000
    • This is an integer division that rounds down
    • Note that this value is 256 bits long (a 1 followed by 255 0s)
    • A block hash is also 256 bits long
    • This means the random number equals the previous block hash's first bit
  • Since block hashes are public, anyone can predict the outcome of the flip before it occurs

So the strategy is to repeatedly (10 times):

  1. retrieve the previous block's hash
  2. use it to predict the outcome of the flip
  3. 'guess' correctly

This is implemented in migrations/level3.js

  • There is a Telephone contract
  • The goal is to get ownership of the contract
  • There is a changeOwner function that sets the owner to a passed value, provided tx.origin != msg.sender
  • Recall tx.origin is the externally owned account (EOA) that initiated the transaction
  • msg.sender is the EOA or contract that directly called the current function

So the strategy is to call changeOwner from a relay contract, which will ensure:

  • tx.origin is the account that called the relay contract
  • msg.sender is the address of the relay contract

This is implemented in migrations/level4.js

  • There is a Token contract
  • We have already been given 20 tokens
  • The goal is to get more
  • The token distribution is recorded in a state variable balances
  • There is a transfer function
    • it accepts _to and _value parameters
    • if confirms that the sender's balance exceeds _value
    • it then reduces the sender's balance increases _to's balance by _value
  • The balances and _value parameter have type uint
  • The guard against overspending is achieved by subtracting _value from the sender's balance and confirming the result is non-negative.
  • Since both parameters are uints, if _value exceeds the sender's balance, it will underflow and the check will stil pass
  • Additionally, when _value is subtracted from the sender's balance, it will underflow.

So the strategy is to send 21 tokens to anyone else so our balance will underflow.

This is implemented in migrations/level5.js

  • There is a Delegation contract
  • The goal is to claim ownership of the contract.
  • It is initialised with a state variable equal to a Delegate contract
  • It's fallback function executes delegatecall(msg.data) on the Delegate contract
  • The Delegate contract has a pwn function the sets its owner to the msg sender
  • delegatecall is intended for library functions that execute in the context of the caller function
  • This means that if we can get the Delegation to delegatecall to the pwn function, it will set the owner of Delegation to the message sender

So the strategy is to invoke Delegation's fallback function with message data corresponding to the pwn function of Delegate

This is implemented in migrations/level6.js

  • There is a Force contract, which is empty
  • There is no payable function.
  • The goal is to send ETH to the contract
  • There are two ways to send ETH to a contract without a payable function:
    • we can mine directly to that address
    • we can call selfdestruct on a contract with ETH, and direct the refund to the target contract

So the strategy is:

  1. Create a contract that accepts ETH
  2. Send some ETH to the contract
  3. Call selfdestruct on the contract and direct the refund to the target

This is implemented in migrations/level7.js

  • There is a Vault contract
  • It has a private state variable locked set to true
  • The goal is to set that to false
  • It has a password state variable that is initialised on deployment
  • There is an unlock function that will set locked to false if we supply the right password
  • The fact that the state variable is private means we can't query it with the contract interface
  • It is not a secret value though - it is still stored on the blockchain
  • We can retrieve it by looking at the contract storage

So the strategy is:

  1. Get the password from the Vault storage
  2. Call unlock with the password

This is implemented in migrations/level8.js

  • There is a King contract
  • It has a state variable king
  • The goal is to become the king and then prevent anyone from reclaiming the throne.
  • The contract is initialised with a 1 ETH prize on deployment
  • It has a payable fallback function that:
    • ensures the transaction amount exceeds the current prize (or the sender is the owner)
    • sends the amount to the current king
    • makes the message sender the new king
    • updates the prize to be the transaction amount
  • We can become king by sending more than 1 ETH.
  • If we use a contract, its fallback function will be called whenever the owner attempts to reclaim the thrown (because that involves sending us the new amount)

So the strategy is:

  1. Create a contract to be king
  2. Set the fallback function to always revert, preventing the owner from reclaiming the throne.
  3. Claim the throne

Note: the contract should have a mechanism for us to withdraw the funds in it. I will ignore that for now.

This is implemented in migrations/level9.js

  • There is a Reentrance contract with 1 ETH
  • The goal is to steal the funds from the contract
  • It tracks address balances with a balances mapping
  • It has a donate function which allows a user to donate ETH to any address
  • It has a withdraw(_amount) function that:
    • confirms the message sender has a balance of at least _amount
    • sends _amount to the message sender
    • reduce the message sender's balance by _amount
  • This is vulnerable to the re-entrancy attack:
    • the message sender is a contract
    • it has a callback function that calls withdraw
    • since withdraw sends funds before reducing the balance, re-entrant calls will all be executed with the original balance and then the balance will be reduced after sending the funds.
  • This means that an attacker can withdraw their balance multiple times in a single call.
  • In this case, it also means that their balance will underflow to a massive value ( which will all them to withdraw all the funds )
  • In a typical re-entrancy attack, the fallback function should have an exit condition to prevent an out-of-gas exception reverting the transaction
  • In this case, the withdraw function sends funds with call, which will simply return false once it is out of gas.

So the strategy is:

  1. Create an attacker contract
  2. Set the fallback to call withdraw.
  3. Point the attacker contract to the target Reentrance contract
  4. Call donate with the attacker contract address
  5. Trigger the withdraw function, which will loop between the attacker fallback and withdraw
  6. Drain the rest of the funds.

This is implemented in migrations/level10.js

  • There is an Elevator contract
  • It has a top variable defaulting to false
  • The goal is to set that variable to true
  • There is a Building interface with one function:
    • `function isLastFloor(uint) view external returns (bool);
  • The Elevator contract has a goTo(_floor) function that:
    • Casts the sender to a Building
    • If isLastFloor(_floor) return false, set top to isLastFloor(_floor)
  • Note that this implies that if the function returns the same value both times, top remains false
  • The fact that isLastFloor is a view function seems to imply it cannot modify storage.
  • However, it is possible to cast any address to any type, so casting to Building does not guarantee conformance to the interface
  • This may create a run-time error if an executed function is missing or accepts the wrong parameter types

So the strategy is:

  1. Create a phony Building contract that does not inherit from Building, but does implement isLastFloor
  2. Return false on the first call and true on the second call
  3. Trigger the Elevator contract's goTo function from the attacker contract.

This is implemented in migrations/level11.js

  • There is an Privacy contract
  • It has a locked state variable
  • The goal is to set that to false
  • It seems similar to the Level 8 challenge
  • There are a number of state variables, one of which is data
  • If we can submit a processed version of data to the unlock function, it will unlock the contract
  • We should be able to just look at the contract storage while paying attention to variable sizes.
Storage Layout Refresher
  • The first item in a (32-byte) storage slot is lower-order aligned
  • If an item cannot fit in the rest of a storage slot, it is moved to the next one
  • Structs and array data always occupy a new (whole) slot, but individually items are still packed
  • Inherited contracts can have storage slots shared between variables from different contracts
  • constant variables do not occupy storage slots

In our case, the storage is packed as follows:

27 bytes 2 bytes 1 byte 1 byte 1 byte
( unused ) awkwardness denomination flattening locked
32 bytes
data[0]
32 bytes
data[1]
32 bytes
data[2]

So the strategy is:

  1. Read the 4th storage location ( data[2] )
  2. Take the top half ( bytes16( data[2] ) )
  3. Pass that value to the unlock function

This is implemented in migrations/level12.js

  • There is a GatekeeperOne contract
  • The goal is to register as an entrant
  • There is an enter function which lets you register if you can pass three modifiers
  • The first one ensures msg.sender != tx.origin, which means we need to use a contract
  • The second one ensures gasleft() % 8191 === 0, so we have to set the gas appropriately
  • The third one ensures the passed _gateKey parameter satisfies three simultaneous conditions:
    • uint32(_gateKey) == uint16(_gateKey) => bytes 2 and 3 (counting from the right) are zero
    • uint32(_gateKey) != uint64(_gateKey) => bytes 4-7 are collectively non-zero
    • uint32(_gateKey) == uint16(tx.origin) => bytes 0 and 1 are the bottom two bytes of tx.origin
  • Experimenting with Remix suggests the gas used by the first gate is 39, but the particular value will depend on the compiler and optimizations used.
  • I don't know of a sensible way to deal with this, so I will just brute force possible gas values.
  • The third gate does not compile with Remix (even with an old compiler) because you can no longer cast bytes to uints. This means I can't directly check the gas usage of the whole function. For simplicity, I will just provide significantly more gas than required per call.

So the strategy is:

  • Create a registration contract
  • Generate a _gateKey value in line with the third gate
  • Call enter with this value and (10 * 8191 + i) gas, for a range of i. (Note: we don't need to brute force different transactions - the contract itself can brute force as long as we use the call function instead of transfer)

This is implemented in migrations/level13.js

  • There is a GatekeeperTwo contract
  • This has the same structure as the previous challenge with different modifiers
  • The first one ensures msg.sender != tx.origin, which means we need to use a contract
  • The second one uses inline assembly to ensure extcodesize(caller) == 0. This implies that the caller does not have any code (it is not a contract)
  • I think these two conditions can be met if we use delegatecall in our contract (so the caller parameter is still the externally-owned account).
  • I would have guessed that this means we can't update the state of the GatekeeperTwo, but maybe it uses its own variables if our contract does not have a matching variable (ie. if our contract does not have an entrant variable, then we can still update the GatekeeperTwo contract's entrant variable through delegatecall)
  • Or maybe we have to use assembly to jump to the function without changing the context, but if such a thing is possible, it is probably a massive security issue
  • Experimenting with Remix I have been able to confirm:
    • caller is typically the calling contract, but if delegatecall is used, it is the previous caller.
    • this means we can bypass the extcodesize(caller) == 0 check if we use delegatecall
    • however, a function called with delegatecall will not affect the called contract's state, so we can't update entrant
  • I can't think of another way to ensure the first two gates can be both passed.
  • I will leave it for now - if I really can't come up with any ideas I will look at the solutions
  • I had to look up the answer, although it seems obvious in retrospect
  • The important insight is that a contract's constructor returns the run-time contract code, which means extcodesize returns 0 before the constructor completes.
  • The caller address still points to the address that will eventually hold the code.
  • So we just need to register as an entrant in the constructor of our contract to bypass the first two gates.
  • The third gate ensures the passed _gateKey is the uint64 bit inverse of keccak256(msg.sender) (ie. our contract address), cast to bytes8

So the strategy is:

  • Create a registration contract
  • In the constructor of the contract:
    1. calculate the _gateKey (using the contract's address)
    2. pass this value to the enter function on the GatekeeperTwo contract

This is implemented in migrations/level14.js

  • There is a NaughtCoin contract, which is a StandardToken
  • We currently hold all the coins.
  • The transfer function has been overloaded to prevent us specifically from sending the tokens.
  • The goal is to bypass the timelock and be able to transfer them freely.
  • The StandardToken allows us to assign tokens to other addresses (the other addresses can spend tokens on our behalf)

So the strategy is:

  1. Assign the tokens to another address that we hold
  2. Transfer them to that address

This is implemented in migrations/level15.js

  • There is a Preservation contract
  • The goal is to take ownership
  • The contract has two library addresses timeZone1Library and timeZone2Library
  • When either setFirstTime or setSecondTime is called, setTime in the corresponding library instance is executed using delegatecall.
  • The LibraryContract is specified and its setTime function simply sets the storedTime value to the passed parameter.
  • Because delegatecall maintains scope, this sets the storedTime variable in the target contract.
  • There is no direct way to set the owner.
  • I suspect we need to overflow the storage, except storedTime is after the owner variable and all the relevant values are uints.
  • I also suspect that any casting shenanigans we could play would be prevented by the new compiler.
  • If we could set one of the library addresses, we can point it to our own contract, which can then edit the state arbitrarily.
  • But neither of the functions that we can call directly set the state.
  • Another idea: we know the address of the libraries, so we could call them directly (to set their own state) or via delegatecall
  • Note: In order to use the latest compiler, I replaced the line:
    • timeZone1Library.delegatecall(setTimeSignature, _timeStamp), with
    • timeZone1Library.delegatecall(abi.encodeWithSignature("setTime(uint256)", _timeStamp))
  • Maybe the old delegatecall accepted more parameters
  • OH, I just discovered an important fact. I thought delegatecall did lookups by variable names.
  • Actually, The delegatecall callee performs variable lookups by storage slot number
  • Each variable name is simply an index into the storage
  • So in this case, the library contract has storedTime set at slot 0. When setTime is called, it sets slot 0 to be the passed in uint.
  • When the library is delegatecalled from the Preservation contract, it will set whatever is stored at slot 0, which happens to be the first library address.

So the strategy is:

  1. Create an attack contract with a setTime(uint) function that overwrites the third storage slot
  2. Call setSecondTime with the attack contract address as a parameter ( our attack contract is now timeZone1Library )
  3. Call setFirstTime with the player address, overwriting the owner storage slot

This is implemented in migrations/level16.js

  • There is a Locked contract
  • It has a storage variable unlocked initialised to false
  • The goal is to set it to true
  • There is only one function register, which updates the other state variables
  • register declares an uninitialised
  • in the challenge version, the struct defaults to storage, and since it's uninitialised, it default to zero.
  • In the current version of solidity, you have to specify either memory or storage and you cannot compile a contract with an uninitialised storage variable
  • In this case, values written into the struct will overwrite the first storage slot, which holds the unlocked variable

So the strategy is:

  1. Call register with a bytes32 name parameter that has a non-zero last byte

This is implemented in migrations/level17.js

  • There is a Recovery contract
  • It generates new SimpleToken contracts
  • The creator generated a new token, sent ETH to the contract and has now lost the address
  • The goal is to recover the lost ETH
  • The first step should be to find the lost contract. When contracts are deployed, addresses are generated as follows:
    • RLP encode [ sender, nonce ]
    • Compute Keccak256 of the result
    • Take the bottom 20 bytes
  • Since we know the Recovery contract address and we're trying to find the first deployed contract (nonce = 1), we should be able to calculate the address.
  • The SimpleToken contract has a destroy function that will return the funds to the specified address

So the strategy is:

  1. Calculate the address of the SimpleToken contract
  2. Call destroy with the player address

This is implemented in migrations/level18.js

  • There is a MagicNumber contract
  • We need to provide it with a Solver contract that
    1. returns 42 when whatIsTheMeaningOfLife() is called
    2. has at most 10 opcodes
  • There is nothing in the supplied MagicNumber contract to check the code size, but presumably the one deployed to the Ethernaut calls extcodesize when validating the contract.
  • This seems like a good opportunity to learn Solidity assembly. I will write any notes I take in an Assembly.md file in this repository.
  • This problem requires a detailed explanation. I have written my analysis in MagicNumber.md

The solution is implemented in migrations/level19.js

  • There is an ownable AlienCodex contract
  • The goal is to take ownership
  • It has three functions to modify a codex array. All of them are protected by a modifier than ensures make_contact has already been called
  • make_contact accepts a bytes32[] parameter that must be at least (1 << 200) long.
  • Obviously, we can't send such a long parameter - we have to simply set the length field to be larger than (1 << 200)
  • The record function pushes a bytes32 onto the array
  • The retract function decrements the array length
  • The revise function lets us set the value of the array at any index we choose to whatever value we choose.
  • Recall from Assembly.md a dynamic array is stored as follows:
    • the array storage slot holds the length of the array
    • the data is stored contiguously at a location specified by keccak256(index of storage slot)
  • Experimenting with the contract confirms:
    • the retract function does not do any bounds checking - we can underflow it
    • the revise function does bounds checking - you cannot update beyond the length of the array
    • however, if the array is longer than the memory address space, it wraps back to the start

So the strategy is:

  • Send a message to make_contact with a (fake) message length greater than 1 << 200
  • Call retract to underflow the array (so the whole address space is part of the array)
  • Calculate the array contents location, and the relative offset to the owner variable ( which is just the twos complement of the array content location )
  • Call revise to overwrite the owner variable with our own address

This is implemented in migrations/level20.js

  • There is a Denial contract
  • The goal is to prevent the owner from retrieving their funds
  • We can become the withdraw partner with the setWithdrawPartner function
  • The withdraw function calculates 1% of the remaining funds (rounding down), sends the that amount to the withdraw partner and then the owner.
  • Obviously this means that if the balance dips below 100, the funds are trapped
  • More relevantly, the fact that the funds get sent to us in the same transaction as the owner means that we can prevent the transaction from occurring.
  • The contract makes a low-level call and ignores the result, so we can't simply revert
  • However, we can waste the remaining gas with an assert(false) statement
  • Note that the gas is not limited to 2300 because the fallback function is invoked with call and not send or transfer

So the strategy is:

  • Create a contract to be the withdraw partner
  • Set the fallback function to execute assert(false)
  • Call setWithdrawPartner from the contract

This is implemented in migrations/level21.js

  • There is a Shop contract
  • It contains a single item and a boolean indicating if it is bought or sold
  • It contains a price for the item
  • The goal is to get the item for less than the specified price
  • The buy function
    • Treats the message sender like a Buyer (an interface with a view function price)
    • Confirms the offered price meets or exceeds the asking price
    • Sells the item at the offered price (which is retrieved with a second call)
  • This challenge is very similar to the Level 11 Elevator challenge:
    • we can create a contract has a price function that doesn't conform to the Buyer interface
    • specifically, it doesn't have to be a view function.
    • If it returns a high price the first time it is called and a low price the second time, we will get the item for the lower price
  • The additional complication is that each call only gets 3000 gas
  • This is enough to read from storage, but not to write to storage
  • Therefore we can't toggle a storage variable in between calls like we did in Level 11
  • Moreover, both calls are identical so there is no context in the call information to distinguish them
  • The only thing that changes between calls is that isSold in the Shop contract gets toggled
  • Fortunately, the isSold variable in Shop is public (so we can query it)
  • Interestingly, this means that our price function can be a view function which conforms to the interface

So the strategy is:

  • Create a Buyer contract
  • The price function reads the Shops isSold variable and returns a high price when it is false, and a low price when it is true
  • Call the buy function from the Buyer contract

This is implemented in migrations/level22.js

About

My attempt at the OpenZeppelin ethernaut

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published