Reversing Ethereum Smart Contracts: Part 2

In my previous tutorial, we began reversing engineering the Greeter.sol contract. Specifically, we looked at Greeter.sol’s dispatcher, the portion of the contract that takes your transaction data and determines to what function it should send you.

Here’s the Greeter.sol contract again.

contract mortal {
    /* Define variable owner of the type address */
    address owner;

    /* This function is executed at initialization and sets the owner of the contract */
    function mortal() { owner = msg.sender; }

    /* Function to recover the funds on the contract */
    function kill() { if (msg.sender == owner) selfdestruct(owner); }
}

contract greeter is mortal {
    /* Define variable greeting of the type string */
    string greeting;
    
    /* This runs when the contract is executed */
    function greeter(string _greeting) public {
        greeting = _greeting;
    }

    /* Main function */
    function greet() constant returns (string) {
        return greeting;
    }
}

Let’s examine the kill() method this time.

The dispatcher exists in every smart contract in existance. The function identifier for “kill()” is 0x41c0e1b5, because these are the first 4 bytes of its keccak256 hash:

keccak256("kill()") = 41c0e1b5...

Here is the part of the dispatcher that examines the incoming transaction to our smart contract and determines whether it wants to communicate with the kill() function. Again, for a more thorough breakdown of these instructions, see part 1.

Dispatch to kill()

Let’s examine what happens when the dispatcher sends us here.

kill()

The kill() function in the Greeter.sol contract is actually inherited from the mortal contract above it:

contract mortal {
    /* Define variable owner of the type address */
    address owner;

    ...

    /* Function to recover the funds on the contract */
    function kill() { if (msg.sender == owner) selfdestruct(owner); }
}

contract greeter is mortal {
    ...
}

Because greeter is mortal, all of mortal’s functions and members are accessible to greeter. Even though we only placed the bytecode for greeter into Binary Ninja, because of this inheritence, that bytecode contains all of mortal’s functions as well.

The kill() function does the following:

1) Checks to see if the address that sent the transaction matches the address owner member of the contract.

2) If so, kill() calls the built-in selfdestruct function and passes the owner address as an argument.

selfdestruct is actually an opcode, so it is already built in to the EVM. It’s the only way, in theory, you can remove your smart contract from the Ethereum blockchain. If your contract accepts ether, the address you pass as an argument to selfdestruct receives all the ether stored in your contract before the contract code is deleted.

The motiviation for selfdestruct (initially called suicide until EIP6) was to allow people to de-clutter the blockchain by deleting their old or unused contracts. If anyone sends ether to a contract that has been self-destructed, it will be lost forever, since the contract address no longer has any code to transfer ether to another address. You can read more about selfdestruct here.

Disassembling kill()

Let’s disassemble kill() and examine the opcodes.

kill()

Being “Payable”

The first instructions are:

Payable portion

CALLVALUE
ISZERO
PUSH2 0x5c
JUMPI

CALLVALUE is the number of wei sent in a transaction and corresponds to the msg.value parameter of a transaction. Wei is the smallest denomination of ether, like a cent is to a dollar, except 1 ether = 1018 wei. For simplicity, I’ll denote the value sent in ether.

CALLVALUE pushes however many ether were sent to the kill() function onto the stack. ISZERO pops this value off and pushes 1 onto the stack if it was 0 (no ether was sent to kill()).

Remember, msg.data corresponds to calldataload, whereas msg.value corresponds to callvalue. An Ethereum transaction to a contract contains both fields. The msg.data field tells the smart contract with what function the transaction wants to interact, and also contains any arguments for that function. The msg.value field can include some ether for that function as well — a totally separate field.

In our case, let’s assume someone did send some ether in their transaction to kill(). This means 0 gets pushed onto the stack by ISZERO. After PUSH2 0x5c, the stack looks like so:

0: 0
1: 0x5c

As explained in part 1, JUMPI is jumpi(label, cond), which means to jump to label if cond is nonzero. In this case, cond is 0, so we don’t jump. This leads us to this branch on the left, which gets us immediately to a REVERT.

kill() is not payable

Why do we get to a REVERT if someone sent ether to the kill() function? Because the kill() function is not marked as payable in the source code.

function kill() {

When a function prototype does not have the payable modifier at the end of it, it rejects any transaction intended for it that contains ether. If a smart contract author doesn’t explicitly include a function to forward the ether stored in their contract elsewhere, it is lost forever. Requiring this “payable” modifier ensures this happens less frequently.

Optimizer

Solidity has done a great job at serving as an accessible language for such a daunting task as writing smart contracts. However, as it is still relatively new (the same applies to Ethereum in general), the Solidity compiler solc can produce redundant instructions in the compiled bytecode.

Take, for example, this set of instructions in our kill() function:

Need optimizer

These three instructions are PUSH1 0x0, DUP1, and SWAP1. What this does is pushes 0x0 onto the stack:

0: 0x0

… duplicates it:

0: 0x0
1: 0x0

… then swaps it, so the two 0x0s on the stack have flipped.

0: 0x0
1: 0x0

These kinks are still being worked out, but luckily the solc compiler has an optimizer flag which does a good job getting rid of some of these redundancies. You can read more about it here.

In our case, we could generate optimized bytecode with the following command:

solc --bin-runtime --optimize --optimize-rounds 200 Greeter.sol

Placing that new bytecode into Binary Ninja, we get the following output:

Need optimizer

You’ll notice the payable logic we examined is still the same, but the number of operations have dramatically decreased!

We’ll continue our analysis with this optimized bytecode.

Breaking down kill()

As we’ve already covered the payable logic, we will proceed with the instructions immediately following it in kill():

Breaking down kill

The first instruction we see is PUSH2 0x65. This will actually stay on the stack until the very end of kill(). You can tell this is the case ahead of time, because if you look at the very bottom, there is a JUMP instruction at address 0x131.

We know that JUMP requires an argument to tell the EVM where to jump, so there must be something still on the stack. We also see that this JUMP instruction immediately leads us to address 0x65. Thus, we can conclude the 0x65 we just pushed onto the stack will be used as an argument for this JUMP instruction at the very end of this function.

The next instruction, PUSH2 0xf1, is just an argument for the JUMP immediately after it. After the JUMP takes place, the stack once again only contains 0x65

Next, we have the first major part of kill():

First major part of kill()

After JUMPDEST, which serves as a placeholder saying where a JUMP landed, the first instructions are PUSH1 0x0 and then SLOAD. We know that SLOAD is short for storage load, which loads from an index in storage and then pushes it onto the stack.

Instruction Result
sload(p) storage[p]

In this case, 0 is the argument passed to it (since it is directly above it on the stack), so SLOAD pushes storage[0] on the stack. In our contract, this is “address owner” member of our contract!

0: 0x65
1: contract owner's address

The next instruction is CALLER, which pushes the address of the call sender (or the person/contract who sent the transaction).

0: 0x65
1: contract owner's address
2: caller address

After PUSH20 0xffffff..., SWAP1, DUP2, we get:

0: 0x65
1: contract owner's address
2: 0xffffff... (20 bytes long)
3: caller address
4: 0xffffff... (20 bytes long)

The next instruction is AND. When ANDing 0xffffff… (20 bytes long) with the caller address, nothing changes. This instruction just makes sure the proper bits of the stack are set. AND pops these two values off the stack and then pushes this address onto the stack.

0: 0x65
1: contract owner's address
2: 0xffffff... (20 bytes long)
3: caller address

The next instructions are SWAP2 and then AND, which uses the AND operation on the contract owner’s address. Again, the result of this AND is pushed to the top of the stack, which is the unchanged contratct owner’s address. After these instructions, the stack looks as follows:

0: 0x65
1: caller address
2: contract owner's address

The next instruction is EQ, which checks if the top two stack items are equal, in which case it pushes 1, and otherwise pushes 0. In this case, EQ checks if the caller address is equal to the contract owner’s address.

Does this sound familiar? It should. This was the if (msg.sender == owner) line of the kill() function!

    /* Function to recover the funds on the contract */
    function kill() { if (msg.sender == owner) selfdestruct(owner); }

The next instruction is ISZERO, which will check if the EQ evaluated to 0 or 1. If it evaluated to 0, it means the message sender was not the contract’s owner, and ISZERO evaluates to true. If ISZERO evaluates to true, it pushes 1 onto the stack, and ultimately tells the JUMP instruction to skip the next block and go to 0x130, which will soon kick you out of the contract.

Jump if sender not owner

Let’s assume the address that sent this transaction did match the contract’s “owner” address. Execution would continue at the PUSH1 0x0. After this instruction, the stack looks as follows:

0: 0x65
1: 0

Again, we have SLOAD, which again took 0 as its argument and therefor pushes the contract owner’s address onto the stack. After the familiar PUSH20 0xffffff... and AND instructions, our stack contains:

0: 0x65
1: contract owner's address

The last instruction in this block is SELFDESTRUCT, which treats the topmost item on the stack as the destination address for all the contract’s stored ether, and then deletes all the contract’s code. After SELFDESTRUCT pops off the contract owner’s address, all that’s left on our stack is 0x65, which again is used as the argument to that final JUMP instruction that leads to a STOP.

Our contract’s code has now been deleted, and all the ether stored in the contract has been sent to owner. Well done!

Twitter

For more content on Ethereum and smart contracts, follow my Twitter.