Skip to content
All tools
Binary exploitationRuns locallyNo account

EVM bytecode analyzer: disassemble, decode selectors, read storage

Disassemble Ethereum contract bytecode, resolve function selectors, decode ABI calldata, and read a storage slot the way Solidity packs it.

Open in ctfpal

A blockchain challenge usually hands you an address and no source. What is on chain is the runtime bytecode, and everything you need is derivable from it - but only if you can read the two conventions the compiler follows.

The dispatcher tells you the ABI

Every Solidity contract begins with the same shape: load the first four bytes of calldata, and compare them against a list of constants. Those constants are function selectors - the first four bytes of keccak256("transfer(address,uint256)") - so the dispatcher is the ABI, listed in the order the compiler emitted it.

Selectors are looked up against a local table of common signatures. A selector that resolves tells you the name and the argument types; one that does not is either a custom function or a deliberately obscured one, and either way its position in the dispatcher tells you it exists.

Storage is packed, and the packing is the puzzle

  • Value types are laid out in declaration order from slot 0, and several small ones share a slot: three uint64s and a bool fit in one 32-byte word, right-aligned, in the order declared. Reading a slot as a single number when it holds four fields is the most common way to misread contract state.
  • A dynamic array at slot `p` stores its length in p and its elements from keccak256(p) onwards.
  • A mapping at slot `p` stores the value for key k at keccak256(k . p) - which is why "private" mapping values are readable by anyone who can compute a hash.

private in Solidity is a compile-time visibility rule and not a secret. Every slot is public on chain, and a challenge whose flag is in a private variable is asking whether you know that.

Reading calldata

Calldata is the selector followed by 32-byte-aligned arguments, with dynamic types stored as an offset to a length-prefixed region later in the buffer. Decoding it against a signature turns a wall of hex into the arguments somebody actually passed - which, for a transaction that drained a contract, is the whole answer.