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_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(global_state: GlobalState) -> List[Issue]: annotations = cast( List[StateChangeCallsAnnotation], list(global_state.get_annotations(StateChangeCallsAnnotation)), ) op_code = global_state.get_current_instruction()["opcode"] if len(annotations) == 0: if op_code in ("SSTORE", "CREATE", "CREATE2"): return [] if op_code in ("SSTORE", "CREATE", "CREATE2"): for annotation in annotations: annotation.state_change_states.append(global_state) # Record state changes following from a transfer of ether if op_code in ("CALL", "DELEGATECALL", "CALLCODE"): value = global_state.mstate.stack[-3] # type: BitVec if StateChange._balance_change(value, global_state): for annotation in annotations: annotation.state_change_states.append(global_state) # Record external calls if op_code in ("CALL", "DELEGATECALL", "CALLCODE"): StateChange._add_external_call(global_state) # Check for vulnerabilities vulnerabilities = [] for annotation in annotations: if not annotation.state_change_states: continue issue = annotation.get_issue(global_state) if issue: vulnerabilities.append(issue) return vulnerabilities
def _handle_transaction_end(self, state: GlobalState) -> None: for annotation in cast( List[OverUnderflowStateAnnotation], state.get_annotations(OverUnderflowStateAnnotation), ): ostate = annotation.overflowing_state address = _get_address_from_state(ostate) if annotation.operator == "subtraction" and self._underflow_cache.get( address, False): continue if annotation.operator != "subtraction" and self._overflow_cache.get( address, False): continue try: # This check can be disabled if the contraints are to difficult for z3 to solve # within any reasonable time. if DISABLE_EFFECT_CHECK: constraints = ostate.mstate.constraints + [ annotation.constraint ] else: constraints = state.mstate.constraints + [ annotation.constraint ] transaction_sequence = solver.get_transaction_sequence( state, constraints) except UnsatError: continue _type = "Underflow" if annotation.operator == "subtraction" else "Overflow" issue = Issue( contract=ostate.environment.active_account.contract_name, function_name=ostate.environment.active_function_name, address=ostate.get_current_instruction()["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=ostate.environment.code.bytecode, title=self._get_title(_type), severity="High", description_head=self._get_description_head(annotation, _type), description_tail=self._get_description_tail(annotation, _type), gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issue.debug = json.dumps(transaction_sequence, indent=4) if annotation.operator == "subtraction": self._underflow_cache[address] = True else: self._overflow_cache[address] = True self._issues.append(issue)
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
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 _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 _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 _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 execute_state( self, global_state: GlobalState ) -> Tuple[List[GlobalState], Optional[str]]: """Execute a single instruction in global_state. :param global_state: :return: A list of successor states. """ # Execute hooks for hook in self._execute_state_hooks: hook(global_state) instructions = global_state.environment.code.instruction_list try: op_code = instructions[global_state.mstate.pc]["opcode"] except IndexError: self._add_world_state(global_state) return [], None if len(global_state.mstate.stack) < get_required_stack_elements( op_code): error_msg = ("Stack Underflow Exception due to insufficient " "stack elements for the address {}".format( instructions[global_state.mstate.pc]["address"])) new_global_states = self.handle_vm_exception( global_state, op_code, error_msg) self._execute_post_hook(op_code, new_global_states) return new_global_states, op_code try: self._execute_pre_hook(op_code, global_state) except PluginSkipState: self._add_world_state(global_state) return [], None try: new_global_states = Instruction(op_code, self.dynamic_loader, self.iprof).evaluate(global_state) except VmException as e: new_global_states = self.handle_vm_exception( global_state, op_code, str(e)) except TransactionStartSignal as start_signal: # Setup new global state new_global_state = start_signal.transaction.initial_global_state() new_global_state.transaction_stack = copy( global_state.transaction_stack) + [ (start_signal.transaction, global_state) ] new_global_state.node = global_state.node new_global_state.mstate.constraints = ( start_signal.global_state.mstate.constraints) log.debug("Starting new transaction %s", start_signal.transaction) return [new_global_state], op_code except TransactionEndSignal as end_signal: transaction, return_global_state = end_signal.global_state.transaction_stack[ -1] log.debug("Ending transaction %s.", transaction) if return_global_state is None: if (not isinstance(transaction, ContractCreationTransaction) or transaction.return_data) and not end_signal.revert: check_potential_issues(global_state) end_signal.global_state.world_state.node = global_state.node self._add_world_state(end_signal.global_state) new_global_states = [] else: # First execute the post hook for the transaction ending instruction self._execute_post_hook(op_code, [end_signal.global_state]) # Propogate codecall based annotations if return_global_state.get_current_instruction()["opcode"] in ( "DELEGATECALL", "CALLCODE", ): new_annotations = [ annotation for annotation in global_state.get_annotations(MutationAnnotation) ] return_global_state.add_annotations(new_annotations) new_global_states = self._end_message_call( copy(return_global_state), global_state, revert_changes=False or end_signal.revert, return_data=transaction.return_data, ) self._execute_post_hook(op_code, new_global_states) return new_global_states, op_code
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 []