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_dependency_annotation(state: GlobalState) -> DependencyAnnotation: """ Returns a dependency annotation :param state: A global state object """ annotations = cast(List[DependencyAnnotation], list(state.get_annotations(DependencyAnnotation))) if len(annotations) == 0: """FIXME: Hack for carrying over state annotations from the STOP and RETURN states of the previous states. The states are pushed on a stack in the world state annotation and popped off the stack in the subsequent iteration. This might break if any other strategy than bfs is used (?). """ try: world_state_annotation = get_ws_dependency_annotation(state) annotation = world_state_annotation.annotations_stack.pop() except IndexError: annotation = DependencyAnnotation() state.annotate(annotation) else: annotation = annotations[0] return annotation
def _analyze_state(state: GlobalState): """ :param state: the current state :return: returns the issues for that corresponding state """ instruction = state.get_current_instruction() annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) if len(annotations) == 0: state.annotate(MultipleSendsAnnotation()) annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) call_offsets = annotations[0].call_offsets if instruction["opcode"] in [ "CALL", "DELEGATECALL", "STATICCALL", "CALLCODE" ]: call_offsets.append(state.get_current_instruction()["address"]) else: # RETURN or STOP for offset in call_offsets[1:]: try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints) except UnsatError: continue description_tail = ( "This call is executed after a previous call in the same transaction. " "Try to isolate each call, transfer or send into its own transaction." ) issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=offset, swc_id=MULTIPLE_SENDS, bytecode=state.environment.code.bytecode, title="Multiple Calls in a Single Transaction", severity="Low", description_head= "Multiple calls are executed in the same transaction.", description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), transaction_sequence=transaction_sequence, ) return [issue] return []
def _get_overflowunderflow_state_annotation( state: GlobalState) -> OverUnderflowStateAnnotation: state_annotations = cast( List[OverUnderflowStateAnnotation], list(state.get_annotations(OverUnderflowStateAnnotation)), ) if len(state_annotations) == 0: state_annotation = OverUnderflowStateAnnotation() state.annotate(state_annotation) return state_annotation else: return state_annotations[0]
def _handle_sstore(state: GlobalState) -> None: stack = state.mstate.stack value = stack[-2] if not isinstance(value, Expression): return for annotation in value.annotations: if not isinstance(annotation, OverUnderflowAnnotation): continue state.annotate( OverUnderflowStateAnnotation( annotation.overflowing_state, annotation.operator, annotation.constraint, ))
def _analyze_states(state: GlobalState) -> List[Issue]: """ :param state: the current state :return: returns the issues for that corresponding state """ issues = [] op_code = state.get_current_instruction()["opcode"] annotations = cast( List[DelegateCallAnnotation], list(state.get_annotations(DelegateCallAnnotation)), ) if len(annotations) == 0 and op_code in ("RETURN", "STOP"): return [] if op_code == "DELEGATECALL": gas = state.mstate.stack[-1] to = state.mstate.stack[-2] constraints = [ to == ATTACKER_ADDRESS, UGT(gas, symbol_factory.BitVecVal(2300, 256)), ] for tx in state.world_state.transaction_sequence: if not isinstance(tx, ContractCreationTransaction): constraints.append(tx.caller == ATTACKER_ADDRESS) state.annotate(DelegateCallAnnotation(state, constraints)) return [] else: for annotation in annotations: try: transaction_sequence = solver.get_transaction_sequence( state, state.mstate.constraints + annotation.constraints + [annotation.return_value == 1], ) issues.append( annotation.get_issue( state, transaction_sequence=transaction_sequence ) ) except UnsatError: continue return issues
def get_potential_issues_annotation( state: GlobalState) -> PotentialIssuesAnnotation: """ Returns the potential issues annotation of the given global state, and creates one if one does not already exist. :param state: The global state :return: """ for annotation in state.annotations: if isinstance(annotation, PotentialIssuesAnnotation): return annotation annotation = PotentialIssuesAnnotation() state.annotate(annotation) return annotation
def _handle_return(state: GlobalState) -> None: """ Adds all the annotations into the state which correspond to the locations in the memory returned by RETURN opcode. :param state: The Global State """ stack = state.mstate.stack try: offset, length = get_concrete_int(stack[-1]), get_concrete_int( stack[-2]) except TypeError: return for element in state.mstate.memory[offset:offset + length]: if not isinstance(element, Expression): continue for annotation in element.annotations: if isinstance(annotation, OverUnderflowAnnotation): state.annotate( OverUnderflowStateAnnotation( annotation.overflowing_state, annotation.operator, annotation.constraint, ))
def _analyze_state(state: GlobalState): """ :param state: the current state :return: returns the issues for that corresponding state """ instruction = state.get_current_instruction() annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) if len(annotations) == 0: log.debug("Creating annotation for state") state.annotate(MultipleSendsAnnotation()) annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) calls = annotations[0].calls if instruction["opcode"] in [ "CALL", "DELEGATECALL", "STATICCALL", "CALLCODE" ]: call = get_call_from_state(state) if call: calls += [call] else: # RETURN or STOP if len(calls) > 1: description_tail = ( "Consecutive calls are executed at the following bytecode offsets:\n" ) for call in calls: description_tail += "Offset: {}\n".format( call.state.get_current_instruction()["address"]) description_tail += ( "Try to isolate each external call into its own transaction," " as external calls can fail accidentally or deliberately.\n") issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], swc_id=MULTIPLE_SENDS, bytecode=state.environment.code.bytecode, title="Multiple Calls in a Single Transaction", severity="Medium", description_head= "Multiple sends are executed in one transaction.", description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) return [issue] return []
def _analyze_states(state: GlobalState) -> list: """ :param state: :return: """ issues = [] if is_prehook(): opcode = state.get_current_instruction()["opcode"] if opcode in final_ops: for annotation in state.annotations: if isinstance(annotation, PredictablePathAnnotation): description = ( "The " + annotation.operation + " is used in to determine a control flow decision. ") description += ( "Note that the values of variables like coinbase, gaslimit, block number and timestamp " "are predictable and can be manipulated by a malicious miner. Also keep in mind that attackers " "know hashes of earlier blocks. Don't use any of those environment variables for random number " "generation or to make critical control flow decisions." ) """ Usually report low severity except in cases where the hash of a previous block is used to determine control flow. """ severity = "Medium" if "hash" in annotation.operation else "Low" """ Note: We report the location of the JUMPI that lead to this path. Usually this maps to an if or require statement. """ swc_id = (TIMESTAMP_DEPENDENCE if "timestamp" in annotation.operation else WEAK_RANDOMNESS) issue = Issue( contract=state.environment.active_account. contract_name, function_name=state.environment.active_function_name, address=annotation.location, swc_id=swc_id, bytecode=state.environment.code.bytecode, title="Dependence on predictable environment variable", severity=severity, description_head= "A control flow decision is made based on a predictable variable.", description_tail=description, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issues.append(issue) elif opcode == "JUMPI": # Look for predictable state variables in jump condition for annotation in state.mstate.stack[-2].annotations: if isinstance(annotation, PredictableValueAnnotation): state.annotate( PredictablePathAnnotation( annotation.operation, state.get_current_instruction()["address"], )) break elif opcode == "BLOCKHASH": param = state.mstate.stack[-1] try: constraint = [ ULT(param, state.environment.block_number), ULT( state.environment.block_number, symbol_factory.BitVecVal(2**255, 256), ), ] # Why the second constraint? Because without it Z3 returns a solution where param overflows. solver.get_model(constraint) state.annotate(OldBlockNumberUsedAnnotation()) except UnsatError: pass else: # we're in post hook opcode = state.environment.code.instruction_list[state.mstate.pc - 1]["opcode"] if opcode == "BLOCKHASH": # if we're in the post hook of a BLOCKHASH op, check if an old block number was used to create it. annotations = cast( List[OldBlockNumberUsedAnnotation], list(state.get_annotations(OldBlockNumberUsedAnnotation)), ) if len(annotations): state.mstate.stack[-1].annotate( PredictableValueAnnotation( "block hash of a previous block")) else: # Always create an annotation when COINBASE, GASLIMIT, TIMESTAMP or NUMBER is executed. state.mstate.stack[-1].annotate( PredictableValueAnnotation( "block.{} environment variable".format(opcode.lower()))) return issues
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 mutator_hook(global_state: GlobalState): global_state.annotate(MutationAnnotation())
def _analyze_state(state: GlobalState) -> list: instruction = state.get_current_instruction() annotations = cast( List[UncheckedRetvalAnnotation], [a for a in state.get_annotations(UncheckedRetvalAnnotation)], ) if len(annotations) == 0: state.annotate(UncheckedRetvalAnnotation()) annotations = cast( List[UncheckedRetvalAnnotation], [a for a in state.get_annotations(UncheckedRetvalAnnotation)], ) retvals = annotations[0].retvals if instruction["opcode"] in ("STOP", "RETURN"): issues = [] for retval in retvals: try: solver.get_model(state.mstate.constraints + [retval["retval"] == 0]) except UnsatError: continue description_tail = ( "External calls return a boolean value. If the callee contract halts with an exception, 'false' is " "returned and execution continues in the caller. It is usually recommended to wrap external calls " "into a require statement to prevent unexpected states.") issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=retval["address"], bytecode=state.environment.code.bytecode, title="Unchecked Call Return Value", swc_id=UNCHECKED_RET_VAL, severity="Low", description_head= "The return value of a message call is not checked.", description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issues.append(issue) return issues else: log.debug("End of call, extracting retval") assert state.environment.code.instruction_list[state.mstate.pc - 1]["opcode"] in [ "CALL", "DELEGATECALL", "STATICCALL", "CALLCODE" ] retval = state.mstate.stack[-1] # Use Typed Dict after release of mypy 0.670 and remove type ignore retvals.append( { # type: ignore "address": state.instruction["address"] - 1, "retval": retval, } ) 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 []