diff --git a/docs/api.rst b/docs/api.rst index c070986..659faf7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -40,6 +40,10 @@ Models EVM --- .. automodule:: manticore.platforms.evm +.. automodule:: manticore.seth + :members: +EVM Assembler +--------- .. autoclass:: manticore.platforms.evm::EVMAsm.Instruction :members: .. autoclass:: manticore.platforms.evm.EVMAsm diff --git a/examples/evm/coverage.py b/examples/evm/coverage.py index 59b8cf1..6cb9761 100644 --- a/examples/evm/coverage.py +++ b/examples/evm/coverage.py @@ -1,4 +1,4 @@ -from seth import * +from manticore.seth import ManticoreEVM seth = ManticoreEVM() seth.verbosity(3) diff --git a/examples/evm/integer_overflow.py b/examples/evm/integer_overflow.py index c65ed44..680edae 100644 --- a/examples/evm/integer_overflow.py +++ b/examples/evm/integer_overflow.py @@ -1,10 +1,9 @@ -from seth import * +from manticore.seth import ManticoreEVM seth = ManticoreEVM() #And now make the contract account to analyze source_code = ''' pragma solidity ^0.4.15; contract Overflow { - event Log(string); uint private sellerBalance=0; function add(uint value) returns (bool){ @@ -19,7 +18,7 @@ contract Overflow { user_account = seth.create_account(balance=1000) contract_account = seth.solidity_create_contract(source_code, owner=user_account, balance=0) -#First add wont owerflow uint256 representation +#First add won't overflow uint256 representation contract_account.add(seth.SValue) #Potential overflow diff --git a/examples/evm/minimal.py b/examples/evm/minimal.py index 99e1396..c4aca60 100644 --- a/examples/evm/minimal.py +++ b/examples/evm/minimal.py @@ -1,4 +1,4 @@ -from seth import ManticoreEVM +from manticore.seth import ManticoreEVM ################ Script ####################### seth = ManticoreEVM() @@ -9,7 +9,7 @@ source_code = ''' pragma solidity ^0.4.13; contract NoDistpatcher { event Log(string); - function address(){} + function() payable { if (msg.data[0] == 'A') { Log("Got an A"); diff --git a/examples/evm/reentrancy_concrete.py b/examples/evm/reentrancy_concrete.py index 7ea6e8c..edf0c0f 100644 --- a/examples/evm/reentrancy_concrete.py +++ b/examples/evm/reentrancy_concrete.py @@ -1,4 +1,4 @@ -from seth import ManticoreEVM +from manticore.seth import ManticoreEVM, ABI ################ Script ####################### seth = ManticoreEVM() @@ -58,13 +58,13 @@ contract GenericReentranceExploit { reentry_reps = reps; } - function delegate(bytes data) payable{ + function proxycall(bytes data) payable{ // call addToBalance with msg.value ethers vulnerable_contract.call.value(msg.value)(data); } function get_money(){ - suicide(owner); + selfdestruct(owner); } function () payable{ @@ -76,12 +76,6 @@ contract GenericReentranceExploit { } } } -//Function signatures: -//0ccfac9e: delegate(bytes) -//b8029269: get_money() -//9d15fd17: set_reentry_attack_string(bytes) -//0d4b1aca: set_reentry_reps(int256) -//beac44e7: set_vulnerable_contract(address) ''' @@ -90,7 +84,7 @@ user_account = seth.create_account(balance=100000000000000000) attacker_account = seth.create_account(balance=100000000000000000) contract_account = seth.solidity_create_contract(contract_source_code, owner=user_account) #Not payable -seth.world[int(contract_account)]['balance']=1000000000000000000 #give it some ether +seth.world.set_balance(contract_account, 1000000000000000000) #give it some ether exploit_account = seth.solidity_create_contract(exploit_source_code, owner=attacker_account) @@ -102,7 +96,7 @@ exploit_account.set_reentry_reps(30) print "\t Setting attack string" #'\x9d\x15\xfd\x17'+pack_msb(32)+pack_msb(4)+'\x5f\xd8\xc7\x10', -reentry_string = seth.make_function_id('withdrawBalance()') +reentry_string = ABI.make_function_id('withdrawBalance()') exploit_account.set_reentry_attack_string(reentry_string) @@ -119,10 +113,10 @@ contract_account.addToBalance(value=100000000000000000) print "[+] Let attacker deposit some small amount using exploit" -exploit_account.delegate(seth.make_function_id('addToBalance()'), value=100000000000000000) +exploit_account.proxycall(ABI.make_function_id('addToBalance()'), value=100000000000000000) print "[+] Let attacker extract all using exploit" -exploit_account.delegate(seth.make_function_id('withdrawBalance()')) +exploit_account.proxycall(ABI.make_function_id('withdrawBalance()')) print "[+] Let attacker destroy the exploit andprofit" exploit_account.get_money() diff --git a/examples/evm/reentrancy_symbolic.py b/examples/evm/reentrancy_symbolic.py index 9d78f89..ee3b3b9 100644 --- a/examples/evm/reentrancy_symbolic.py +++ b/examples/evm/reentrancy_symbolic.py @@ -1,4 +1,4 @@ -from seth import ManticoreEVM +from manticore.seth import ManticoreEVM ################ Script ####################### seth = ManticoreEVM() @@ -27,10 +27,6 @@ contract Reentrance { userBalance[msg.sender] = 0; } } -//Function signatures: -//c0e317fb: addToBalance() -//f8b2cb4f: getBalance(address) -//5fd8c710: withdrawBalance() ''' exploit_source_code = ''' @@ -58,7 +54,7 @@ contract GenericReentranceExploit { reentry_reps = reps; } - function delegate(bytes data) payable{ + function proxycall(bytes data) payable{ // call addToBalance with msg.value ethers vulnerable_contract.call.value(msg.value)(data); } @@ -76,12 +72,6 @@ contract GenericReentranceExploit { } } } -//Function signatures: -//0ccfac9e: delegate(bytes) -//b8029269: get_money() -//9d15fd17: set_reentry_attack_string(bytes) -//0d4b1aca: set_reentry_reps(int256) -//beac44e7: set_vulnerable_contract(address) ''' @@ -115,10 +105,10 @@ exploit_account.set_reentry_attack_string(seth.SByte(4)) #Attacker is print "[+] Attacker first transaction" -exploit_account.delegate(seth.SByte(4), value=seth.SValue) +exploit_account.proxycall(seth.SByte(4), value=seth.SValue) print "[+] Attacker second transaction" -exploit_account.delegate(seth.SByte(4)) +exploit_account.proxycall(seth.SByte(4)) print "[+] The attacker destroys the exploit contract and profit" exploit_account.get_money() diff --git a/examples/evm/simple_functions.py b/examples/evm/simple_functions.py index 264254c..bd678eb 100644 --- a/examples/evm/simple_functions.py +++ b/examples/evm/simple_functions.py @@ -1,4 +1,4 @@ -from seth import * +from manticore.seth import ManticoreEVM ################ Script ####################### seth = ManticoreEVM() @@ -24,7 +24,6 @@ contract Test { user_account = seth.create_account(balance=1000) contract_account = seth.solidity_create_contract(source_code, owner=user_account) - symbolic_data = seth.SByte(4) symbolic_value = None seth.transaction( caller=user_account, diff --git a/examples/evm/simple_mapping.py b/examples/evm/simple_mapping.py index 3268360..78f14d9 100644 --- a/examples/evm/simple_mapping.py +++ b/examples/evm/simple_mapping.py @@ -1,4 +1,4 @@ -from seth import * +from manticore.seth import ManticoreEVM ################ Script ####################### seth = ManticoreEVM() diff --git a/examples/evm/simple_transaction.py b/examples/evm/simple_transaction.py index 33fc0ea..0957e2e 100644 --- a/examples/evm/simple_transaction.py +++ b/examples/evm/simple_transaction.py @@ -1,4 +1,4 @@ -from seth import * +from manticore.seth import ManticoreEVM ################ Script ####################### seth = ManticoreEVM() diff --git a/manticore/platforms/evm.py b/manticore/platforms/evm.py index df89774..bfd600a 100644 --- a/manticore/platforms/evm.py +++ b/manticore/platforms/evm.py @@ -699,7 +699,7 @@ class EVMAsm(object): mnemonic = name if name == 'PUSH': mnemonic = '%s%d'%(name, (opcode&0x1f) + 1) - elif name in ('SWAP', 'LOG'): + elif name in ('SWAP', 'LOG', 'DUP'): mnemonic = '%s%d'%(name, (opcode&0xf) + 1) reverse_table[mnemonic] = opcode, name, immediate_operand_size, pops, pushes, gas, description @@ -732,7 +732,7 @@ class EVMAsm(object): return EVMAsm.Instruction(opcode, name, operand_size, pops, pushes, gas, description, operand=operand, offset=offset) except: - raise Exception("Something wron at offset %d"%offset) + raise Exception("Something wrong at offset %d"%offset) @staticmethod def assemble_all(assembler, offset=0): @@ -1902,14 +1902,53 @@ class EVMWorld(Platform): else: return self._global_storage - @storage.setter - def storage(self, value): - if self.depth: - self.current.global_storage = value - else: - self._global_storage = value + def set_storage_data(self, address, offset, value): + self.storage[address]['storage'][offset] = value - def _push(self, vm): + def get_storage_data(self, address, offset): + return self.storage[address]['storage'].get(offset) + + def set_balance(self, address, value): + self.storage[int(address)]['balance'] = value + + def get_balance(self, address): + return self.storage[address]['balance'] + + def add_to_balance(self, address, value): + self.storage[address]['balance'] += value + + def send_ether(self, src, dst, value): + src_balance = self.get_balance(src) + dst_balance = self.get_balance(dst) + #discarding absurd amount of ether + self.constraints.add(src_balance + value >= src_balance) + + if issymbolic(src_balance) or issymbolic(value): + res = solver.get_all_values(self._constraints, src_balance < value) + if set(res) == set([True, False]): + raise Concretize('Forking on available funds', + expression = src_balance < value, + setstate=lambda a,b: None, + policy='ALL') + if set(res) == set([True]): + self._pending_transaction = None + raise TerminateState("Not Enough Funds for transaction", testcase=True) + else: + if src_balance < value: + self._pending_transaction = None + raise TerminateState("Not Enough Funds for transaction", testcase=True) + + self.storage[dst]['balance'] += value + self.storage[src]['balance'] -= value + + def get_code(self, address): + return self.storage[address]['code'] + + def set_code(self, address, data): + self.storage[address]['code'] = data + + + def _push_vm(self, vm): #Storage address -> account(value, local_storage) vm.global_storage = self.storage vm.global_storage[vm.address]['storage'] = copy.copy(self.storage[vm.address]['storage']) @@ -1926,13 +1965,17 @@ class EVMWorld(Platform): self._pop(rollback=True) raise TerminateState("Maximum call depth limit is reached", testcase=True) - def _pop(self, rollback=False): + def _pop_vm(self, rollback=False): vm = self._callstack.pop() assert self.constraints == vm.constraints if self.current: self.current.constraints = vm.constraints if not rollback: - self.storage = vm.global_storage + if self.depth: + self.current.global_storage = vm.global_storage + else: + self._global_storage = vm.global_storage + self._deleted_address = self._deleted_address.union(vm.suicide) self._logs += vm.logs if not self.depth: @@ -2073,14 +2116,14 @@ class EVMWorld(Platform): for i in range(len(data)): data_symb[i] = Operators.ORD(data[i]) data = data_symb - bytecode = self.storage[address]['code'] + bytecode = self.get_code(address) self._pending_transaction = ('Call', address, origin, price, data, caller, value, bytecode, header) if run: assert self.depth == 0 assert not issymbolic(caller) assert not issymbolic(address) - assert self.storage[caller]['balance'] >= value + assert self.get_balance(caller) >= value #run contract #Assert everything is concrete? try: @@ -2095,34 +2138,10 @@ class EVMWorld(Platform): assert self.current is None or self.current.last_exception is not None ty, address, origin, price, data, caller, value, bytecode, header = self._pending_transaction - - - if issymbolic(self.storage[caller]['balance']) or issymbolic(value): - res = solver.get_all_values(self._constraints, self.storage[caller]['balance'] < value) - if set(res) == set([True, False]): - raise Concretize('Forking on available funds', - expression = self.storage[caller]['balance'] < value, - setstate=lambda a,b: None, - policy='ALL') - if set(res) == set([True]): - self._pending_transaction = None - raise TerminateState("Not Enough Funds for transaction", testcase=True) - else: - if self.storage[caller]['balance'] < value: - self._pending_transaction = None - raise TerminateState("Not Enough Funds for transaction", testcase=True) - - self._pending_transaction = None - - #discarding absurd amount of ether - self.constraints.add(self.storage[address]['balance'] + value >= self.storage[address]['balance']) - - self.storage[caller]['balance'] -= value - self.storage[address]['balance'] += value - + self.send_ether(caller, address, value) + self._pending_transaction=None new_vm = EVM(self._constraints, address, origin, price, data, caller, value, bytecode, header, global_storage=self.storage) - - self._push(new_vm) + self._push_vm(new_vm) if self.depth == 1: #handle human transactions if ty == 'Create': @@ -2137,14 +2156,14 @@ class EVMWorld(Platform): caller = self.current.address price = self.current.price depth = self.depth + 1 - bytecode = self.storage[to]['code'] + bytecode = self.get_code(to) header = {'timestamp' :1} self.transaction(address, origin, price, data, caller, value, header) self._process_pending_transaction() def RETURN(self, data): - prev_vm = self._pop() #current VM changed! + prev_vm = self._pop_vm() #current VM changed! if self.depth == 0: self.last_return=data self.last_pc = prev_vm.pc @@ -2157,8 +2176,7 @@ class EVMWorld(Platform): if isinstance(last_ex, Create): self.current._push(prev_vm.address) - self.storage[prev_vm.address]['code'] = data - + self.set_code(prev_vm.address, data) else: size = min(last_ex.out_size, len(data)) self.current.write_buffer(last_ex.out_offset, data[:size]) @@ -2167,7 +2185,7 @@ class EVMWorld(Platform): self.current.pc += self.current.instruction.size def STOP(self): - prev_vm = self._pop(rollback=False) + prev_vm = self._pop_vm(rollback=False) if self.depth == 0: self.last_pc = prev_vm.pc raise TerminateState("STOP", testcase=True) @@ -2178,7 +2196,7 @@ class EVMWorld(Platform): self.current.pc += self.current.instruction.size def THROW(self): - prev_vm = self._pop(rollback=True) + prev_vm = self._pop_vm(rollback=True) #revert balance on CALL fail self.storage[prev_vm.caller]['balance'] += prev_vm.value self.storage[prev_vm.address]['balance'] -= prev_vm.value @@ -2193,7 +2211,7 @@ class EVMWorld(Platform): self.current.pc += self.current.instruction.size def REVERT(self, data): - prev_vm = self._pop(rollback=True) + prev_vm = self._pop_vm(rollback=True) #revert balance on CALL fail self.storage[prev_vm.caller]['balance'] += prev_vm.value self.storage[prev_vm.address]['balance'] -= prev_vm.value @@ -2216,7 +2234,7 @@ class EVMWorld(Platform): self.storage[recipient]['balance'] += self.storage[address]['balance'] self.storage[address]['balance'] = 0 self.suicide.add(address) - prev_vm = self._pop(rollback=False) + prev_vm = self._pop_vm(rollback=False) if self.depth == 0: self.last_pc = prev_vm.pc raise TerminateState("SELFDESTRUCT", testcase=True) diff --git a/examples/evm/seth.py b/manticore/seth.py similarity index 59% rename from examples/evm/seth.py rename to manticore/seth.py index 2c0c9fc..d4421e6 100644 --- a/examples/evm/seth.py +++ b/manticore/seth.py @@ -1,97 +1,52 @@ -from manticore import Manticore -from manticore.core.smtlib import ConstraintSet, Operators, solver, issymbolic, Array, Expression, Constant -from manticore.core.smtlib.visitors import arithmetic_simplifier -from manticore.platforms import evm -from manticore.core.state import State +from . import Manticore +from .core.smtlib import ConstraintSet, Operators, solver, issymbolic, Array, Expression, Constant +from .core.smtlib.visitors import arithmetic_simplifier +from .platforms import evm +from .core.state import State import tempfile from subprocess import Popen, PIPE import sha3 import json +import StringIO -class EVMContract(object): +class ABI(object): + ''' + This class contains methods to handle the ABI. + The Application Binary Interface is the standard way to interact with + contracts in the Ethereum ecosystem, both from outside the blockchain + and for contract-to-contract interaction. - def __init__(self, address, seth=None, default_caller=None): - self._default_caller = default_caller - self._seth=seth - self._address=address - self._hashes = {} - self._caller = None - self._value = 0 - - name, source_code, init_bytecode, metadata, metadata_runtime, hashes = self._seth.context['seth']['metadata'][address] - for signature in hashes.keys(): - func_name = str(signature.split('(')[0]) - self._hashes[func_name] = signature, hashes[signature] - - def __int__(self): - return self._address - - @property - def address(self): - return self._address - - def value(self, value): - self._value = value - return self - - def caller(self, caller): - self._caller = caller - return self - - def __getattribute__(self, name): - if not name.startswith('_') and name in self._hashes.keys(): - def f(*args, **kwargs): - caller = kwargs.get('caller', self._caller) - value = kwargs.get('value', self._value) - tx_data = self._seth.make_function_call(str(self._hashes[name][0]),*args) - if caller is not None: - caller = int(caller) - else: - caller = self._default_caller - self._seth.transaction(caller=caller, - address=self._address, - value=value, - data=tx_data - ) - self._caller = None - self._value = 0 - return f - else: - return object.__getattribute__(self, name) - -class ManticoreEVM(Manticore): + ''' class SByte(): + ''' Unconstrained symbolic byte, not asociated with any constraint set ''' def __init__(self, size=1): self.size=size def __mul__(self, reps): return Symbol(self.size*reps) + SCHAR = SByte(1) SUINT = SByte(32) SValue = None - - - @staticmethod - def pack_msb(value): - return ''.join(ManticoreEVM.serialize_uint(value)) @staticmethod def serialize(value): + ''' Translates a python object to its EVM ABI serialization. + It supports s ''' if isinstance(value, (str,tuple)): - return ManticoreEVM.serialize_string(value) + return ABI.serialize_string(value) if isinstance(value, (list)): - return ManticoreEVM.serialize_array(value) + return ABI.serialize_array(value) if isinstance(value, (int, long)): - return ManticoreEVM.serialize_uint(value) - if isinstance(value, ManticoreEVM.SByte): - return ManticoreEVM.serialize_uint(value.size) + (None,)*value.size + (('\x00',)*(32-(value.size%32))) + return ABI.serialize_uint(value) + if isinstance(value, ABI.SByte): + return ABI.serialize_uint(value.size) + (None,)*value.size + (('\x00',)*(32-(value.size%32))) if value is None: return (None,)*32 - @staticmethod def serialize_uint(value, size=32): - '''takes an int and packs it into a 32 byte string, msb first''' + '''Translates a python int into a 32 byte string, msb first''' assert size >=1 bytes = [] for position in range(size): @@ -101,15 +56,16 @@ class ManticoreEVM(Manticore): @staticmethod def serialize_string(value): + '''Translates a string or a tuple of chars its EVM ABI serialization''' assert isinstance(value, (str,tuple)) - return ManticoreEVM.serialize_uint(len(value)) + tuple(value) + tuple('\x00'*(32-(len(value)%32))) + return ABI.serialize_uint(len(value)) + tuple(value) + tuple('\x00'*(32-(len(value)%32))) @staticmethod def serialize_array(value): assert isinstance(value, list) - serialized = [ManticoreEVM.serialize_uint(len(value))] + serialized = [ABI.serialize_uint(len(value))] for item in value: - serialized.append(ManticoreEVM.serialize(item)) + serialized.append(ABI.serialize(item)) return reduce(lambda x,y: x+y, serialized) @staticmethod @@ -125,20 +81,20 @@ class ManticoreEVM(Manticore): return () args = list(args) for i in range(len(args)): - if isinstance(args[i], EVMContract): + if isinstance(args[i], EVMAccount): args[i] = int(args[i]) result = [] dynamic_args = [] dynamic_offset = 32*len(args) for arg in args: if isinstance(arg, (list, tuple, str, ManticoreEVM.SByte)): - result.append(ManticoreEVM.serialize(dynamic_offset)) - serialized_arg = ManticoreEVM.serialize(arg) + result.append(ABI.serialize(dynamic_offset)) + serialized_arg = ABI.serialize(arg) dynamic_args.append(serialized_arg) assert len(serialized_arg)%32 ==0 dynamic_offset += len(serialized_arg) else: - result.append(ManticoreEVM.serialize(arg)) + result.append(ABI.serialize(arg)) for arg in dynamic_args: result.append(arg) @@ -147,20 +103,114 @@ class ManticoreEVM(Manticore): @staticmethod def make_function_call(method_name, *args): - function_id = ManticoreEVM.make_function_id(method_name) + function_id = ABI.make_function_id(method_name) def check_bitsize(value, size): if isinstance(value, BitVec): return value.size==size return (value & ~((1<" - raise NotImplemented + raise NotImplemented(ty) - print "TRANSACTION ", tx_num, '-', ty + output.write('TRANSACTION %d - %s' % (tx_num, ty)) try: md_name, md_source_code, md_init_bytecode, md_metadata, md_metadata_runtime, md_hashes= self.context['seth']['metadata'][address] except: md_name, md_source_code, md_init_bytecode, md_metadata, md_metadata_runtime, md_hashes = None,None,None,None,None,None - print '\t From: 0x%x'%x(caller) - print '\t To: 0x%x'%x(address) - print '\t Value: %d wei'%x(value) + output.write('\t From: 0x%x\n'% x(caller) ) + output.write('\t To: 0x%x\n'%x(address)) + output.write('\t Value: %d wei\n'%x(value)) xdata = x(data).encode('hex') - print '\t Data:', xdata + output.write('\t Data: %s\n'% xdata) if ty == 'CALL': - print '\t Function: ', done = False - rhashes = dict((hsh, signature) for signature, hsh in md_hashes.iteritems()) try: + rhashes = dict((hsh, signature) for signature, hsh in md_hashes.iteritems()) signature = rhashes.get(xdata[:8], '{fallback}()') done = True func_name = signature.split('(')[0] - print func_name,'(', + output.write('\t Function: %s(' % func_name) types = signature.split('(')[1][:-1].split(',') off = 8 for ty in types: if off != 8: print ',', val, off = consume_type(ty, xdata, off) - print val, - print ')' + output.write('%s'%val) + output.write(')\n') except Exception,e: - print e, xdata - - - - - + output.write('%s %s\n'%(e, xdata)) + return output.getvalue() def coverage(self, account_address): + ''' Output a code coverage report for contract account_address ''' account_address = int(account_address) #This will just pick one of the running states. #This assumes the code and the accounts are the same in all versions of the world @@ -613,7 +742,7 @@ class ManticoreEVM(Manticore): offset = 0 count = 0 total = 0 - for i in evm.EVMDecoder.decode_all(runtime_bytecode[:end]) : + for i in evm.EVMAsm.disassemble_all(runtime_bytecode[:end]) : if (account_address, offset) in seen: output += bcolors.OKGREEN @@ -629,16 +758,16 @@ class ManticoreEVM(Manticore): output += "Total assembler lines: %d\n"% total output += "Total assembler lines visited: %d\n"% count output += "Coverage: %2.2f%%\n"% (count*100.0/total) - - return output + def _symbolic_sha3(self, state, data, known_hashes): + ''' INTERNAL USE ''' - def symbolic_sha3(self, state, data, known_hashes): with self.locked_context('known_sha3', set) as known_sha3: state.platform._sha3.update(known_sha3) - def concrete_sha3(self, state, buf, value): + def _concrete_sha3(self, state, buf, value): + ''' INTERNAL USE ''' with self.locked_context('known_sha3', set) as known_sha3: known_sha3.add((buf,value))