def create_account( self, balance=0, address=None, concrete_storage=False, dynamic_loader=None, creator=None, ) -> Account: """Create non-contract account. :param address: The account's address :param balance: Initial balance for the account :param concrete_storage: Interpret account storage as concrete :param dynamic_loader: used for dynamically loading storage from the block chain :return: The new account """ address = (symbol_factory.BitVecVal(address, 256) if address else self._generate_new_address(creator)) new_account = Account( address=address, balances=self.balances, dynamic_loader=dynamic_loader, concrete_storage=concrete_storage, ) if balance: new_account.add_balance(symbol_factory.BitVecVal(balance, 256)) self.put_account(new_account) return new_account
def _add_external_call(global_state: GlobalState) -> None: gas = global_state.mstate.stack[-1] to = global_state.mstate.stack[-2] try: constraints = copy(global_state.mstate.constraints) solver.get_model(constraints + [ UGT(gas, symbol_factory.BitVecVal(2300, 256)), Or( to > symbol_factory.BitVecVal(16, 256), to == symbol_factory.BitVecVal(0, 256), ), ]) # Check whether we can also set the callee address try: constraints += [ to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF ] solver.get_model(constraints) global_state.annotate( StateChangeCallsAnnotation(global_state, True)) except UnsatError: global_state.annotate( StateChangeCallsAnnotation(global_state, False)) except UnsatError: pass
def get_word_at(self, index: int) -> Union[int, BitVec]: """Access a word from a specified memory index. :param index: integer representing the index to access :return: 32 byte word at the specified index """ try: return symbol_factory.BitVecVal( util.concrete_int_from_bytes( bytes([ util.get_concrete_int(b) for b in self[index:index + 32] ]), 0, ), 256, ) except TypeError: result = simplify( Concat([ b if isinstance(b, BitVec) else symbol_factory.BitVecVal( b, 8) for b in cast(List[Union[int, BitVec]], self[index:index + 32]) ])) assert result.size() == 256 return result
def write_word_at(self, index: int, value: Union[int, BitVec, bool, Bool]) -> None: """Writes a 32 byte word to memory at the specified index` :param index: index to write to :param value: the value to write to memory """ try: # Attempt to concretize value if isinstance(value, bool): _bytes = (int(1).to_bytes(32, byteorder="big") if value else int(0).to_bytes(32, byteorder="big")) else: _bytes = util.concrete_int_to_bytes(value) assert len(_bytes) == 32 self[index:index + 32] = list(bytearray(_bytes)) except (Z3Exception, AttributeError): # BitVector or BoolRef value = cast(Union[BitVec, Bool], value) if isinstance(value, Bool): value_to_write = If( value, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256), ) else: value_to_write = value assert value_to_write.size() == 256 for i in range(0, value_to_write.size(), 8): self[index + 31 - (i // 8)] = Extract(i + 7, i, value_to_write)
def __getitem__(self, item: Union[str, int]) -> Any: try: return self._storage[item] except KeyError: if (self.address and self.address.value != 0 and (self.dynld and self.dynld.storage_loading)): try: self._storage[item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( contract_address=hex(self.address.value), index=int(item), ), 16, ), 256, ) return self._storage[item] except ValueError: pass if self.concrete: return symbol_factory.BitVecVal(0, 256) self._storage[item] = symbol_factory.BitVecSym( "storage_{}_{}".format(str(item), str(self.address)), 256) return self._storage[item]
def _handle_exp(self, state): op0, op1 = self._get_args(state) if op0.symbolic and op1.symbolic: constraint = And( op1 > symbol_factory.BitVecVal(256, 256), op0 > symbol_factory.BitVecVal(1, 256), ) elif op1.symbolic: if op0.value < 2: return constraint = op1 >= symbol_factory.BitVecVal( ceil(256 / log2(op0.value)), 256) elif op0.symbolic: if op1.value == 0: return constraint = op0 >= symbol_factory.BitVecVal( 2**ceil(256 / op1.value), 256) else: constraint = op0.value**op1.value >= 2**256 model = self._try_constraints(state.mstate.constraints, [constraint]) if model is None: return annotation = OverUnderflowAnnotation(state, "exponentiation", constraint) op0.annotate(annotation)
def test_create_has_storage(): last_state, created_contract_account = execute_create() storage = created_contract_account.storage # From contract, val = 10. assert storage[symbol_factory.BitVecVal(0, 256)] == symbol_factory.BitVecVal( 10, 256)
def __getitem__(self, item: Union[int, slice, BitVec]) -> Any: """ :param item: :return: """ if isinstance(item, int) or isinstance(item, Expression): return self._load(item) if isinstance(item, slice): start = 0 if item.start is None else item.start step = 1 if item.step is None else item.step stop = self.size if item.stop is None else item.stop try: current_index = ( start if isinstance(start, Expression) else symbol_factory.BitVecVal(start, 256) ) parts = [] while simplify(current_index != stop): element = self._load(current_index) if not isinstance(element, Expression): element = symbol_factory.BitVecVal(element, 8) parts.append(element) current_index = simplify(current_index + step) except Z3Exception: raise IndexError("Invalid Calldata Slice") return parts raise ValueError
def get_call_data( global_state: GlobalState, memory_start: Union[int, BitVec], memory_size: Union[int, BitVec], ): """Gets call_data from the global_state. :param global_state: state to look in :param memory_start: Start index :param memory_size: Size :return: Tuple containing: call_data array from memory or empty array if symbolic, type found """ state = global_state.mstate transaction_id = "{}_internalcall".format(global_state.current_transaction.id) memory_start = cast( BitVec, ( symbol_factory.BitVecVal(memory_start, 256) if isinstance(memory_start, int) else memory_start ), ) memory_size = cast( BitVec, ( symbol_factory.BitVecVal(memory_size, 256) if isinstance(memory_size, int) else memory_size ), ) uses_entire_calldata = simplify( memory_size == global_state.environment.calldata.calldatasize ) if is_true(uses_entire_calldata): return global_state.environment.calldata try: calldata_from_mem = state.memory[ util.get_concrete_int(memory_start) : util.get_concrete_int( memory_start + memory_size ) ] return ConcreteCalldata(transaction_id, calldata_from_mem) except TypeError: log.debug( "Unsupported symbolic memory offset %s size %s", memory_start, memory_size ) return SymbolicCalldata(transaction_id)
def get_issue( self, global_state: GlobalState, detector: DetectionModule ) -> Optional[PotentialIssue]: if not self.state_change_states: return None constraints = Constraints() gas = self.call_state.mstate.stack[-1] to = self.call_state.mstate.stack[-2] constraints += [ UGT(gas, symbol_factory.BitVecVal(2300, 256)), Or( to > symbol_factory.BitVecVal(16, 256), to == symbol_factory.BitVecVal(0, 256), ), ] if self.user_defined_address: constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF] try: solver.get_transaction_sequence( global_state, constraints + global_state.mstate.constraints ) except UnsatError: return None severity = "Medium" if self.user_defined_address else "Low" address = global_state.get_current_instruction()["address"] logging.debug( "[EXTERNAL_CALLS] Detected state changes at addresses: {}".format(address) ) description_head = ( "The contract account state is changed after an external call. " ) description_tail = ( "Consider that the called contract could re-enter the function before this " "state change takes place. This can lead to business logic vulnerabilities." ) return PotentialIssue( contract=global_state.environment.active_account.contract_name, function_name=global_state.environment.active_function_name, address=address, title="State change after external call", severity=severity, description_head=description_head, description_tail=description_tail, swc_id=REENTRANCY, bytecode=global_state.environment.code.bytecode, constraints=constraints, detector=detector, )
def _generate_new_address(self, creator=None) -> BitVec: """Generates a new address for the global state. :return: """ if creator: # TODO: Use nounce address = "0x" + str(mk_contract_address(creator, 0).hex()) return symbol_factory.BitVecVal(int(address, 16), 256) while True: address = "0x" + "".join( [str(hex(randint(0, 16)))[-1] for _ in range(40)]) if address not in self._accounts.keys(): return symbol_factory.BitVecVal(int(address, 16), 256)
def __init__(self, tx_id: str, calldata: list) -> None: """Initializes the ConcreteCalldata object. :param tx_id: Id of the transaction that the calldata is for. :param calldata: The concrete calldata content """ self._concrete_calldata = calldata self._calldata = K(256, 8, 0) for i, element in enumerate(calldata, 0): element = (symbol_factory.BitVecVal(element, 8) if isinstance( element, int) else element) self._calldata[symbol_factory.BitVecVal(i, 256)] = element super().__init__(tx_id)
def _load(self, item: Union[int, BitVec]) -> Any: """ :param item: :return: """ item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item return simplify( If( item < self._size, simplify(self._calldata[cast(BitVec, item)]), symbol_factory.BitVecVal(0, 8), ) )
def _is_precompile_call(global_state: GlobalState): to = global_state.mstate.stack[-2] # type: BitVec constraints = copy(global_state.mstate.constraints) constraints += [ Or( to < symbol_factory.BitVecVal(1, 256), to > symbol_factory.BitVecVal(16, 256), ) ] try: solver.get_model(constraints) return False except UnsatError: return True
def __getitem__(self, item: BitVec) -> BitVec: storage, is_keccak_storage = self._get_corresponding_storage(item) if is_keccak_storage: item = self._sanitize(cast(BitVecFunc, item).input_) value = storage[item] if (value.value == 0 and self.address and item.symbolic is False and self.address.value != 0 and (self.dynld and self.dynld.storage_loading)): try: storage[item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( contract_address=hex(self.address.value), index=int(item.value), ), 16, ), 256, ) self.printable_storage[item] = storage[item] return storage[item] except ValueError: pass return simplify(storage[item])
def __init__( self, address: Union[BitVec, str], code=None, contract_name="unknown", balances: Array = None, concrete_storage=False, dynamic_loader=None, ) -> None: """Constructor for account. :param address: Address of the account :param code: The contract code of the account :param contract_name: The name associated with the account :param balance: The balance for the account :param concrete_storage: Interpret storage as concrete """ self.nonce = 0 self.code = code or Disassembly("") self.address = (address if isinstance(address, BitVec) else symbol_factory.BitVecVal(int(address, 16), 256)) self.storage = Storage(concrete_storage, address=self.address, dynamic_loader=dynamic_loader) # Metadata self.contract_name = contract_name self.deleted = False self._balances = balances self.balance = lambda: self._balances[self.address]
def execute_message_call(laser_evm, callee_address: str) -> None: """Executes a message call transaction from all open states. :param laser_evm: :param callee_address: """ # TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here open_states = laser_evm.open_states[:] del laser_evm.open_states[:] for open_world_state in open_states: if open_world_state[callee_address].deleted: log.debug("Can not execute dead contract, skipping.") continue next_transaction_id = get_next_transaction_id() transaction = MessageCallTransaction( world_state=open_world_state, identifier=next_transaction_id, gas_price=symbol_factory.BitVecSym( "gas_price{}".format(next_transaction_id), 256), gas_limit=8000000, # block gas limit origin=symbol_factory.BitVecSym( "origin{}".format(next_transaction_id), 256), caller=symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256), callee_account=open_world_state[callee_address], call_data=SymbolicCalldata(next_transaction_id), call_value=symbol_factory.BitVecSym( "call_value{}".format(next_transaction_id), 256), ) _setup_global_state_for_execution(laser_evm, transaction) laser_evm.exec()
def _set_minimisation_constraints(transaction_sequence, constraints, minimize, max_size): """ Set constraints that minimise key transaction values Constraints generated: - Upper bound on calldata size - Minimisation of call value's and calldata sizes :param transaction_sequence: Transaction for which the constraints should be applied :param constraints: The constraints array which should contain any added constraints :param minimize: The minimisation array which should contain any variables that should be minimised :param max_size: The max size of the calldata array :return: updated constraints, minimize """ for transaction in transaction_sequence: # Set upper bound on calldata size max_calldata_size = symbol_factory.BitVecVal(max_size, 256) constraints.append( UGE(max_calldata_size, transaction.call_data.calldatasize)) # Minimize minimize.append(transaction.call_data.calldatasize) minimize.append(transaction.call_value) return constraints, minimize
def _sanitize(input_: BitVec) -> BitVec: if input_.size() == 512: return input_ if input_.size() > 512: return Extract(511, 0, input_) else: return Concat(symbol_factory.BitVecVal(0, 512 - input_.size()), input_)
def __getitem__(self, item: BitVec) -> BitVec: storage, is_keccak_storage = self._get_corresponding_storage(item) if is_keccak_storage: sanitized_item = self._sanitize(cast(BitVecFunc, item).input_) else: sanitized_item = item if ( self.address and self.address.value != 0 and item.symbolic is False and int(item.value) not in self.storage_keys_loaded and (self.dynld and self.dynld.storage_loading) ): try: storage[sanitized_item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( contract_address="0x{:040X}".format(self.address.value), index=int(item.value), ), 16, ), 256, ) self.storage_keys_loaded.add(int(item.value)) self.printable_storage[item] = storage[sanitized_item] except ValueError as e: log.debug("Couldn't read storage at %s: %s", item, e) return simplify(storage[sanitized_item])
def _load(self, item: Union[int, BitVec]) -> BitVec: """ :param item: :return: """ item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item return simplify(self._calldata[item])
def add_balance(self, balance: Union[int, BitVec]) -> None: """ :param balance: """ balance = (symbol_factory.BitVecVal(balance, 256) if isinstance( balance, int) else balance) self._balances[self.address] += balance
def _load(self, item: Union[int, BitVec], clean=False) -> Any: expr_item = (symbol_factory.BitVecVal(item, 256) if isinstance( item, int) else item) # type: BitVec symbolic_base_value = If( expr_item >= self._size, symbol_factory.BitVecVal(0, 8), BitVec( symbol_factory.BitVecSym( "{}_calldata_{}".format(self.tx_id, str(item)), 8)), ) return_value = symbolic_base_value for r_index, r_value in self._reads: return_value = If(r_index == expr_item, r_value, return_value) if not clean: self._reads.append((expr_item, symbolic_base_value)) return simplify(return_value)
def _analyze_state(state) -> List[Issue]: instruction = state.get_current_instruction() address, value = state.mstate.stack[-1], state.mstate.stack[-2] target_slot = 0 target_offset = 0 # In the following array we'll describe all the conditions that need to hold for a take over of ownership vulnerable_conditions = [ # Lets check that we're writing to address 0 (where the owner variable is located address == target_slot, # There is only a vulnerability if before the writing to the owner variable: owner != attacker Extract( 20*8 + target_offset, 0 + target_offset, state.environment.active_account.storage[symbol_factory.BitVecVal(0, 256)] ) != ACTORS.attacker, # There IS a vulnerability if the value being written to owner is the attacker address Extract( 20*8 + target_offset, 0 + target_offset, value, ) == ACTORS.attacker, # Lets only look for cases where the attacker makes themselves the owner by saying that the attacker # is the sender of this transaction state.environment.sender == ACTORS.attacker, ] try: # vulnerable_conditions describes when there is a vulnerability # lets check if the conditions are actually satisfiable by running the following command: # This will raise an UnsatError if the vulnerable_conditions are not satisfiable (i.e. not possible) transaction_sequence = solver.get_transaction_sequence( state, state.world_state.constraints + vulnerable_conditions, ) # Note that get_transaction_sequence also gives us `transaction_sequence` which gives us a concrete # transaction trace that can be used to exploit/demonstrate the vulnerability. # Lets register an issue with Mythril so that the vulnerability is reported to the user! return [Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], swc_id='000', bytecode=state.environment.code.bytecode, title="Ownership Takeover", severity="High", description_head="An attacker can take over ownership of this contract.", description_tail="", transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), )] except UnsatError: # Sadly (or happily), no vulnerabilities were found here. log.debug("Vulnerable conditions were not satisfiable") return list()
def set_balance(self, balance: Union[int, BitVec]) -> None: """ :param balance: """ balance = (symbol_factory.BitVecVal(balance, 256) if isinstance( balance, int) else balance) assert self._balances is not None self._balances[self.address] = balance
def calldatasize(self) -> BitVec: """ :return: Calldata size for this calldata object """ result = self.size if isinstance(result, int): return symbol_factory.BitVecVal(result, 256) return result
def get_transaction_sequence(global_state, constraints): """Generate concrete transaction sequence. :param global_state: GlobalState to generate transaction sequence for :param constraints: list of constraints used to generate transaction sequence """ transaction_sequence = global_state.world_state.transaction_sequence # gaslimit & gasprice don't exist yet tx_template = { "calldata": None, "call_value": None, "caller": "0xCA11EDEADBEEF37E636E6CA11EDEADBEEFCA11ED", } concrete_transactions = {} creation_tx_ids = [] tx_constraints = constraints.copy() minimize = [] transactions = [] for transaction in transaction_sequence: tx_id = str(transaction.id) if not isinstance(transaction, ContractCreationTransaction): transactions.append(transaction) # Constrain calldatasize max_calldatasize = symbol_factory.BitVecVal(5000, 256) tx_constraints.append( UGE(max_calldatasize, transaction.call_data.calldatasize)) minimize.append(transaction.call_data.calldatasize) minimize.append(transaction.call_value) concrete_transactions[tx_id] = tx_template.copy() else: creation_tx_ids.append(tx_id) model = get_model(tx_constraints, minimize=minimize) for transaction in transactions: tx_id = str(transaction.id) concrete_transactions[tx_id]["calldata"] = "0x" + "".join([ hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] for b in transaction.call_data.concrete(model) ]) concrete_transactions[tx_id]["call_value"] = ("0x%x" % model.eval( transaction.call_value.raw, model_completion=True).as_long()) concrete_transactions[tx_id]["caller"] = "0x" + ("%x" % model.eval( transaction.caller.raw, model_completion=True).as_long()).zfill(40) return concrete_transactions
def _set_minimisation_constraints( transaction_sequence, constraints, minimize, max_size, world_state ) -> Tuple[Constraints, tuple]: """ Set constraints that minimise key transaction values Constraints generated: - Upper bound on calldata size - Minimisation of call value's and calldata sizes :param transaction_sequence: Transaction for which the constraints should be applied :param constraints: The constraints array which should contain any added constraints :param minimize: The minimisation array which should contain any variables that should be minimised :param max_size: The max size of the calldata array :return: updated constraints, minimize """ for transaction in transaction_sequence: # Set upper bound on calldata size max_calldata_size = symbol_factory.BitVecVal(max_size, 256) constraints.append(UGE(max_calldata_size, transaction.call_data.calldatasize)) # Minimize minimize.append(transaction.call_data.calldatasize) minimize.append(transaction.call_value) constraints.append( UGE( symbol_factory.BitVecVal(1000000000000000000000, 256), world_state.starting_balances[transaction.caller], ) ) for account in world_state.accounts.values(): # Lazy way to prevent overflows and to ensure "reasonable" balances # Each account starts with less than 100 ETH constraints.append( UGE( symbol_factory.BitVecVal(100000000000000000000, 256), world_state.starting_balances[account.address], ) ) return constraints, tuple(minimize)
def append(self, element: Union[int, Expression]) -> None: """ :param element: element to be appended to the list :function: appends the element to list if the size is less than STACK_LIMIT, else throws an error """ if isinstance(element, int): element = symbol_factory.BitVecVal(element, 256) if super(MachineStack, self).__len__() >= self.STACK_LIMIT: raise StackOverflowException( "Reached the EVM stack limit of {}, you can't append more " "elements".format(self.STACK_LIMIT)) super(MachineStack, self).append(element)
def world_state_filter_hook(global_state: GlobalState): if And(*global_state.mstate.constraints[:] + [ global_state.environment.callvalue > symbol_factory.BitVecVal(0, 256) ]).is_false: return if isinstance(global_state.current_transaction, ContractCreationTransaction): return if len(list( global_state.get_annotations(MutationAnnotation))) == 0: raise PluginSkipWorldState