def sstore_(self, global_state): global keccak_function_manager state = global_state.mstate index, value = state.stack.pop(), state.stack.pop() logging.debug("Write to storage[" + str(index) + "]") try: index = util.get_concrete_int(index) return self._sstore_helper(global_state, index, value) except AttributeError: is_keccak = keccak_function_manager.is_keccak(index) if not is_keccak: return self._sstore_helper(global_state, str(index), value) storage_keys = global_state.environment.active_account.storage.keys() keccak_keys = filter(keccak_function_manager.is_keccak, storage_keys) solver = Solver() solver.set(timeout=1000) results = [] new = False for keccak_key in keccak_keys: key_argument = keccak_function_manager.get_argument(keccak_key) index_argument = keccak_function_manager.get_argument(index) if is_true(key_argument == index_argument): return self._sstore_helper(copy(global_state), keccak_key, value, key_argument == index_argument) results += self._sstore_helper(copy(global_state), keccak_key, value, key_argument == index_argument) new = Or(new, key_argument != index_argument) if len(results) > 0: results += self._sstore_helper(copy(global_state), str(index), value, new) return results return self._sstore_helper(global_state, str(index), value)
def get_callee_address(global_state: GlobalState, dynamic_loader: DynLoader, symbolic_to_address): """ Gets the address of the callee :param global_state: state to look in :param dynamic_loader: dynamic loader to use :param symbolic_to_address: The (symbolic) callee address :return: Address of the callee """ environment = global_state.environment try: callee_address = hex(util.get_concrete_int(symbolic_to_address)) except TypeError: logging.debug("Symbolic call encountered") match = re.search(r'storage_(\d+)', str(simplify(symbolic_to_address))) logging.debug("CALL to: " + str(simplify(symbolic_to_address))) if match is None or dynamic_loader is None: raise ValueError() index = int(match.group(1)) logging.debug( "Dynamic contract address at storage index {}".format(index)) # attempt to read the contract address from instance storage try: callee_address = dynamic_loader.read_storage( environment.active_account.address, index) # TODO: verify whether this happens or not except: logging.debug("Error accessing contract storage.") raise ValueError # testrpc simply returns the address, geth response is more elaborate. if not re.match(r"^0x[0-9a-f]{40}$", callee_address): callee_address = "0x" + callee_address[26:] return callee_address
def get_strategic_global_state(self) -> GlobalState: """ :return: """ while True: state = self.super_strategy.get_strategic_global_state() opcode = state.get_current_instruction()["opcode"] if opcode != "JUMPI": return state annotations = cast( List[JumpdestCountAnnotation], list(state.get_annotations(JumpdestCountAnnotation)), ) if len(annotations) == 0: annotation = JumpdestCountAnnotation() state.annotate(annotation) else: annotation = annotations[0] try: target = util.get_concrete_int(state.mstate.stack[-1]) except TypeError: return state try: annotation._jumpdest_count[target] += 1 except KeyError: annotation._jumpdest_count[target] = 1 if annotation._jumpdest_count[target] > self.jumpdest_limit: log.debug("JUMPDEST limit reached, skipping JUMPI") continue return state
def sstore_(self, global_state): state = global_state.mstate index, value = state.stack.pop(), state.stack.pop() logging.debug("Write to storage[" + str(index) + "]") try: index = util.get_concrete_int(index) except AttributeError: index = str(index) try: global_state.environment.active_account = deepcopy( global_state.environment.active_account) global_state.accounts[ global_state.environment.active_account. address] = global_state.environment.active_account global_state.environment.active_account.storage[index] = value except KeyError: logging.debug("Error writing to storage: Invalid index") return [global_state]
def return_(self, global_state): # TODO: memory state = global_state.mstate offset, length = state.stack.pop(), state.stack.pop() try: _ = state.memory[util.get_concrete_int(offset):util. get_concrete_int(offset + length)] except AttributeError: logging.debug( "Return with symbolic length or offset. Not supported") return_value = BitVec( "retval_" + global_state.environment.active_function_name, 256) if not global_state.call_stack: return [] new_global_state = deepcopy(global_state.call_stack.pop()) new_global_state.node = global_state.node # TODO: copy memory return [new_global_state]
def calldataload_(self, global_state): state = global_state.mstate environment = global_state.environment op0 = state.stack.pop() try: offset = util.get_concrete_int(simplify(op0)) b = environment.calldata[offset] except AttributeError: logging.debug("CALLDATALOAD: Unsupported symbolic index") state.stack.append(BitVec( "calldata_" + str(environment.active_account.contract_name) + "[" + str(simplify(op0)) + "]", 256)) return [global_state] except IndexError: logging.debug("Calldata not set, using symbolic variable instead") state.stack.append(BitVec( "calldata_" + str(environment.active_account.contract_name) + "[" + str(simplify(op0)) + "]", 256)) return [global_state] if type(b) == int: val = b'' try: for i in range(offset, offset + 32): val += environment.calldata[i].to_bytes(1, byteorder='big') logging.debug("Final value: " + str(int.from_bytes(val, byteorder='big'))) state.stack.append(BitVecVal(int.from_bytes(val, byteorder='big'), 256)) # FIXME: broad exception catch except: state.stack.append(BitVec( "calldata_" + str(environment.active_account.contract_name) + "[" + str(simplify(op0)) + "]", 256)) else: # symbolic variable state.stack.append(BitVec( "calldata_" + str(environment.active_account.contract_name) + "[" + str(simplify(op0)) + "]", 256)) return [global_state]
def get_callee_address(global_state, dynamic_loader, to): """ Gets the address of the callee :param global_state: state to look in :param dynamic_loader: dynamic loader to use :return: Address of the callee """ environment = global_state.environment try: callee_address = hex(util.get_concrete_int(to)) except AttributeError: logging.info("Symbolic call encountered") match = re.search(r'storage_(\d+)', str(simplify(to))) logging.debug("CALL to: " + str(simplify(to))) if match is None or dynamic_loader is None: raise ValueError() index = int(match.group(1)) logging.info( "Dynamic contract address at storage index {}".format(index)) # attempt to read the contract address from instance storage # TODO: we need to do this correctly using multi transactional analysis try: callee_address = dynamic_loader.read_storage( environment.active_account.address, index) except: logging.debug("Error accessing contract storage.") raise ValueError # testrpc simply returns the address, geth response is more elaborate. if not re.match(r"^0x[0-9a-f]{40}$", callee_address): callee_address = "0x" + callee_address[26:] return callee_address
def sload_(self, global_state): global keccak_function_manager state = global_state.mstate index = state.stack.pop() logging.debug("Storage access at index " + str(index)) try: index = util.get_concrete_int(index) return self._sload_helper(global_state, index) except AttributeError: if not keccak_function_manager.is_keccak(index): return self._sload_helper(global_state, str(index)) storage_keys = global_state.environment.active_account.storage.keys() keccak_keys = list(filter(keccak_function_manager.is_keccak, storage_keys)) results = [] constraints = [] for keccak_key in keccak_keys: key_argument = keccak_function_manager.get_argument(keccak_key) index_argument = keccak_function_manager.get_argument(index) constraints.append((keccak_key, key_argument == index_argument)) for (keccak_key, constraint) in constraints: if constraint in state.constraints: results += self._sload_helper(global_state, keccak_key, [constraint]) if len(results) > 0: return results for (keccak_key, constraint) in constraints: results += self._sload_helper(copy(global_state), keccak_key, [constraint]) if len(results) > 0: return results return self._sload_helper(global_state, str(index))
def __getitem__(self, item: Union[int, slice]) -> Any: if isinstance(item, slice): start, step, stop = item.start, item.step, item.stop try: if start is None: start = 0 if step is None: step = 1 if stop is None: stop = self.calldatasize current_index = (start if isinstance(start, BitVecRef) else BitVecVal(start, 256)) dataparts = [] while simplify(current_index != stop): dataparts.append(self[current_index]) current_index = simplify(current_index + step) except Z3Exception: raise IndexError("Invalid Calldata Slice") values, constraints = zip(*dataparts) result_constraints = [] for c in constraints: result_constraints.extend(c) return simplify(Concat(values)), result_constraints if self.concrete: try: return self._calldata[get_concrete_int(item)], () except IndexError: return BitVecVal(0, 8), () else: constraints = [ Implies(self._calldata[item] != 0, UGT(self.calldatasize, item)) ] return self._calldata[item], constraints
def mstore_(self, global_state): state = global_state.mstate try: op0, value = state.stack.pop(), state.stack.pop() except IndexError: raise StackUnderflowException() try: mstart = util.get_concrete_int(op0) except AttributeError: logging.debug("MSTORE to symbolic index. Not supported") return [global_state] try: state.mem_extend(mstart, 32) except Exception: logging.debug("Error extending memory, mstart = " + str(mstart) + ", size = 32") logging.debug("MSTORE to mem[" + str(mstart) + "]: " + str(value)) try: # Attempt to concretize value _bytes = util.concrete_int_to_bytes(value) i = 0 for b in _bytes: state.memory[mstart + i] = _bytes[i] i += 1 except: try: state.memory[mstart] = value except: logging.debug("Invalid memory access") return [global_state]
def extcodesize_(self, global_state): state = global_state.mstate addr = state.stack.pop() environment = global_state.environment try: addr = hex(helper.get_concrete_int(addr)) except AttributeError: logging.info("unsupported symbolic address for EXTCODESIZE") state.stack.append(BitVec("extcodesize_" + str(addr), 256)) return [global_state] try: code = self.dynamic_loader.dynld(environment.active_account.address, addr) except Exception as e: logging.info("error accessing contract storage due to: " + str(e)) state.stack.append(BitVec("extcodesize_" + str(addr), 256)) return [global_state] if code is None: state.stack.append(0) else: state.stack.append(len(code.bytecode) // 2) return [global_state]
def get_call_data(global_state, memory_start, memory_size, pad=True): """ 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 try: # TODO: This only allows for either fully concrete or fully symbolic calldata. # Improve management of memory and callata to support a mix between both types. call_data = state.memory[util.get_concrete_int(memory_start):util. get_concrete_int(memory_start + memory_size)] if len(call_data) < 32 and pad: call_data += [0] * (32 - len(call_data)) call_data_type = CalldataType.CONCRETE logging.debug("Calldata: " + str(call_data)) except AttributeError: logging.info("Unsupported symbolic calldata offset") call_data_type = CalldataType.SYMBOLIC call_data = [] return call_data, call_data_type
def jump_(self, global_state): state = global_state.mstate disassembly = global_state.environment.code try: jump_addr = util.get_concrete_int(state.stack.pop()) except AttributeError: raise InvalidJumpDestination("Invalid jump argument (symbolic address)") except IndexError: raise StackUnderflowException() index = util.get_instruction_index(disassembly.instruction_list, jump_addr) if index is None: raise InvalidJumpDestination("JUMP to invalid address") op_code = disassembly.instruction_list[index]['opcode'] if op_code != "JUMPDEST": raise InvalidJumpDestination("Skipping JUMP to invalid destination (not JUMPDEST): " + str(jump_addr)) new_state = copy(global_state) new_state.mstate.pc = index new_state.mstate.depth += 1 return [new_state]
def sstore_(self, global_state): state = global_state.mstate index, value = state.stack.pop(), state.stack.pop() logging.debug("Write to storage[" + str(index) + "]") try: index = util.get_concrete_int(index) except AttributeError: index = str(index) try: # Create a fresh copy of the account object before modifying storage for k in global_state.accounts: if global_state.accounts[k] == global_state.environment.active_account: global_state.accounts[k] = deepcopy(global_state.accounts[k]) global_state.environment.active_account = global_state.accounts[k] break global_state.environment.active_account.storage[index] = value except KeyError: logging.debug("Error writing to storage: Invalid index") return [global_state]
def get_variable(i): try: return Variable(util.get_concrete_int(i), VarType.CONCRETE) except AttributeError: return Variable(simplify(i), VarType.SYMBOLIC)
def call_(self, global_state): instr = global_state.get_current_instruction() environment = global_state.environment try: callee_address, callee_account, call_data, value, call_data_type, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader, True) except ValueError as e: logging.info( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}" .format(e)) # TODO: decide what to do in this case global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) return [global_state] if 0 < int(callee_address, 16) < 5: logging.info("Native contract called: " + callee_address) if call_data == [] and call_data_type == CalldataType.SYMBOLIC: logging.debug("CALL with symbolic data not supported") global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) return [global_state] data = natives.native_contracts(int(callee_address, 16), call_data) try: mem_out_start = helper.get_concrete_int(memory_out_offset) mem_out_sz = memory_out_size.as_long() except AttributeError: logging.debug( "CALL with symbolic start or offset not supported") global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) return [global_state] global_state.mstate.mem_extend(mem_out_start, mem_out_sz) try: for i in range(min(len(data), mem_out_sz) ): # If more data is used then it's chopped off global_state.mstate.memory[mem_out_start + i] = data[i] except: global_state.mstate.memory[mem_out_start] = BitVec(data, 256) # TODO: maybe use BitVec here constrained to 1 global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) return [global_state] global_state.call_stack.append(instr['address']) callee_environment = Environment( callee_account, BitVecVal(int(environment.active_account.address, 16), 256), call_data, environment.gasprice, value, environment.origin, calldata_type=call_data_type) new_global_state = GlobalState(global_state.accounts, callee_environment, MachineState(gas)) new_global_state.mstate.depth = global_state.mstate.depth + 1 new_global_state.mstate.constraints = copy( global_state.mstate.constraints) return [global_state]
def calldatacopy_(self, global_state): state = global_state.mstate environment = global_state.environment op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop() try: mstart = util.get_concrete_int(op0) # FIXME: broad exception catch except: logging.debug("Unsupported symbolic memory offset in CALLDATACOPY") return [global_state] try: dstart = util.get_concrete_int(op1) # FIXME: broad exception catch except: logging.debug( "Unsupported symbolic calldata offset in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(environment.active_account.contract_name) + "_cpy", 256) return [global_state] try: size = util.get_concrete_int(op2) # FIXME: broad exception catch except: logging.debug("Unsupported symbolic size in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) return [global_state] if size > 0: try: state.mem_extend(mstart, size) # FIXME: broad exception catch except: logging.debug("Memory allocation error: mstart = " + str(mstart) + ", size = " + str(size)) state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) return [global_state] try: i_data = environment.calldata[dstart] for i in range(mstart, mstart + size): state.memory[i] = environment.calldata[i_data] i_data += 1 except: logging.debug("Exception copying calldata to memory") state.memory[mstart] = BitVec( "calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) return [global_state]
def call_(self, global_state): instr = global_state.get_current_instruction() environment = global_state.environment try: callee_address, callee_account, call_data, value, call_data_type, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader, True) except ValueError as e: logging.info( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}" .format(e)) # TODO: decide what to do in this case global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) return [global_state] global_state.mstate.stack.append( BitVec("retval_" + str(instr['address']), 256)) if 0 < int(callee_address, 16) < 5: logging.info("Native contract called: " + callee_address) if call_data == [] and call_data_type == CalldataType.SYMBOLIC: logging.debug("CALL with symbolic data not supported") return [global_state] try: mem_out_start = helper.get_concrete_int(memory_out_offset) mem_out_sz = memory_out_size.as_long() except AttributeError: logging.debug( "CALL with symbolic start or offset not supported") return [global_state] global_state.mstate.mem_extend(mem_out_start, mem_out_sz) call_address_int = int(callee_address, 16) try: data = natives.native_contracts(call_address_int, call_data) except natives.NativeContractException: contract_list = [ 'ecerecover', 'sha256', 'ripemd160', 'identity' ] for i in range(mem_out_sz): global_state.mstate.memory[mem_out_start + i] = BitVec( contract_list[call_address_int - 1] + "(" + str(call_data) + ")", 256) return [global_state] for i in range(min( len(data), mem_out_sz)): # If more data is used then it's chopped off global_state.mstate.memory[mem_out_start + i] = data[i] # TODO: maybe use BitVec here constrained to 1 return [global_state] transaction = MessageCallTransaction( global_state.world_state, callee_account, BitVecVal(int(environment.active_account.address, 16), 256), call_data, environment.gasprice, value, environment.origin, call_data_type) raise TransactionStartSignal(transaction, self.op_code)
def _analyze_state(self, state: GlobalState) -> List[Issue]: """ :param state: the current state :return: returns the issues for that corresponding state """ opcode = state.get_current_instruction()["opcode"] address = state.get_current_instruction()["address"] annotations = cast(List[VisitsAnnotation], list(state.get_annotations(VisitsAnnotation))) if len(annotations) == 0: annotation = VisitsAnnotation() state.annotate(annotation) else: annotation = annotations[0] if opcode in ["JUMP", "JUMPI"]: if annotation.loop_start is not None: return [] try: target = util.get_concrete_int(state.mstate.stack[-1]) except TypeError: log.debug("Symbolic target encountered in dos module") return [] if target in annotation.jump_targets: annotation.jump_targets[target] += 1 else: annotation.jump_targets[target] = 1 if annotation.jump_targets[target] > min( 2, analysis_args.loop_bound - 1): annotation.loop_start = address elif annotation.loop_start is not None: if opcode == "CALL": operation = "A message call" else: operation = "A storage modification" description_head = ( "Potential denial-of-service if block gas limit is reached.") description_tail = "{} is executed in a loop. Be aware that the transaction may fail to execute if the loop is unbounded and the necessary gas exceeds the block gas limit.".format( operation) try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints) except UnsatError: return [] issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=annotation.loop_start, swc_id=DOS_WITH_BLOCK_GAS_LIMIT, bytecode=state.environment.code.bytecode, title= "Potential denial-of-service if block gas limit is reached", severity="Low", description_head=description_head, description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), transaction_sequence=transaction_sequence, ) return [issue] return []
def _analyze_states(self, state: GlobalState) -> List[Issue]: """ :param state: the current state :return: returns the issues for that corresponding state """ opcode = state.get_current_instruction()["opcode"] address = state.get_current_instruction()["address"] if opcode == "JUMPI": target = util.get_concrete_int(state.mstate.stack[-1]) transaction = state.current_transaction if state.current_transaction in self._jumpdest_count: try: self._jumpdest_count[transaction][target] += 1 if self._jumpdest_count[transaction][target] == 3: annotation = ( LoopAnnotation(address, target) if target > address else LoopAnnotation(target, address) ) state.annotate(annotation) except KeyError: self._jumpdest_count[transaction][target] = 0 else: self._jumpdest_count[transaction] = {} self._jumpdest_count[transaction][target] = 0 else: annotations = cast( List[LoopAnnotation], list(state.get_annotations(LoopAnnotation)) ) for annotation in annotations: if annotation.contains(address): operation = ( "A storage modification" if opcode == "SSTORE" else "An external call" ) description_head = ( "Potential denial-of-service if block gas limit is reached." ) description_tail = "{} is executed in a loop.".format(operation) issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=annotation.loop_start, swc_id=DOS_WITH_BLOCK_GAS_LIMIT, bytecode=state.environment.code.bytecode, title="Potential denial-of-service if block gas limit is reached", severity="Low", description_head=description_head, description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) return [issue] return []