def test_delegate_call(sym_mock, concrete_mock, curr_instruction): # arrange # sym_mock = mocker.patch.object(delegatecall, "_symbolic_call") # concrete_mock = mocker.patch.object(delegatecall, "_concrete_call") sym_mock.return_value = [] concrete_mock.return_value = [] curr_instruction.return_value = {"address": "0x10"} active_account = Account("0x10") active_account.code = Disassembly("00") environment = Environment(active_account, None, None, None, None, None) state = GlobalState(None, environment, Node) state.mstate.memory = ["placeholder", "calldata_bling_0"] state.mstate.stack = [1, 2, 3] assert state.get_current_instruction() == {"address": "0x10"} node = Node("example") node.contract_name = "the contract name" node.function_name = "fallback" to = Variable("storage_1", VarType.SYMBOLIC) call = Call(node, state, None, "DELEGATECALL", to, None) statespace = MagicMock() statespace.calls = [call] # act execute(statespace) # assert assert concrete_mock.call_count == 1 assert sym_mock.call_count == 1
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 _end_message_call( self, return_global_state: GlobalState, global_state: GlobalState, revert_changes=False, return_data=None, ) -> List[GlobalState]: # Resume execution of the transaction initializing instruction op_code = return_global_state.environment.code.instruction_list[ return_global_state.mstate.pc]["opcode"] # Set execution result in the return_state return_global_state.last_return_data = return_data if not revert_changes: return_global_state.world_state = copy(global_state.world_state) return_global_state.environment.active_account = global_state.accounts[ return_global_state.environment.active_account.address] # Execute the post instruction handler new_global_states = Instruction(op_code, self.dynamic_loader).evaluate( return_global_state, True) # In order to get a nice call graph we need to set the nodes here for state in new_global_states: state.node = global_state.node return new_global_states
def test_execute_node(mocker): record = TaintRecord() record.stack = [True, True, False, False] state_1 = GlobalState(None, None, None) state_1.mstate.stack = [1, 2, 3, 1] state_1.mstate.pc = 1 mocker.patch.object(state_1, "get_current_instruction") state_1.get_current_instruction.return_value = {"opcode": "SWAP1"} state_2 = GlobalState(None, 1, None) state_2.mstate.stack = [1, 2, 4, 1] mocker.patch.object(state_2, "get_current_instruction") state_2.get_current_instruction.return_value = {"opcode": "ADD"} node = Node("Test contract") node.states = [state_1, state_2] # Act records = TaintRunner.execute_node(node, record) # Assert assert len(records) == 2 assert records[0].stack == [True, True, False, False] assert records[1].stack == [True, True, False] assert state_2 in records[0].states assert state_1 in record.states
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: state_annotation = _get_overflowunderflow_state_annotation(state) for annotation in state_annotation.overflowing_state_annotations: ostate = annotation.overflowing_state if ostate in self._ostates_unsatisfiable: continue if ostate not in self._ostates_satisfiable: try: constraints = ostate.mstate.constraints + [ annotation.constraint ] solver.get_model(constraints) self._ostates_satisfiable.add(ostate) except: self._ostates_unsatisfiable.add(ostate) continue log.debug( "Checking overflow in {} at transaction end address {}, ostate address {}" .format( state.get_current_instruction()["opcode"], state.get_current_instruction()["address"], ostate.get_current_instruction()["address"], )) try: 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), transaction_sequence=transaction_sequence, ) address = _get_address_from_state(ostate) self.cache.add(address) self.issues.append(issue)
def __init__(self, call_state: GlobalState, constraints: List) -> None: """ Initialize DelegateCall Annotation :param call_state: Call state """ self.call_state = call_state self.constraints = constraints self.return_value = call_state.new_bitvec( "retval_{}".format(call_state.get_current_instruction()["address"]), 256 )
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 native_call( global_state: GlobalState, callee_address: Union[str, BitVec], call_data: BaseCalldata, memory_out_offset: Union[int, Expression], memory_out_size: Union[int, Expression], ) -> Optional[List[GlobalState]]: if ( isinstance(callee_address, BitVec) or not 0 < int(callee_address, 16) <= PRECOMPILE_COUNT ): return None log.debug("Native contract called: " + callee_address) try: mem_out_start = util.get_concrete_int(memory_out_offset) mem_out_sz = util.get_concrete_int(memory_out_size) except TypeError: log.debug("CALL with symbolic start or offset not supported") return [global_state] contract_list = ["ecrecover", "sha256", "ripemd160", "identity"] call_address_int = int(callee_address, 16) native_gas_min, native_gas_max = calculate_native_gas( global_state.mstate.calculate_extension_size(mem_out_start, mem_out_sz), contract_list[call_address_int - 1], ) global_state.mstate.min_gas_used += native_gas_min global_state.mstate.max_gas_used += native_gas_max global_state.mstate.mem_extend(mem_out_start, mem_out_sz) try: data = natives.native_contracts(call_address_int, call_data) except natives.NativeContractException: for i in range(mem_out_sz): global_state.mstate.memory[mem_out_start + i] = global_state.new_bitvec( contract_list[call_address_int - 1] + "(" + str(call_data) + ")", 8 ) 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] retval = global_state.new_bitvec( "retval_" + str(global_state.get_current_instruction()["address"]), 256 ) global_state.mstate.stack.append(retval) global_state.node.constraints.append(retval == 1) return [global_state]
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 _execute(self, state: GlobalState) -> None: """Executes analysis module for integer underflow and integer overflow. :param state: Statespace to analyse :return: Found issues """ address = _get_address_from_state(state) if address in self.cache: return opcode = state.get_current_instruction()["opcode"] funcs = { "ADD": [self._handle_add], "SUB": [self._handle_sub], "MUL": [self._handle_mul], "SSTORE": [self._handle_sstore], "JUMPI": [self._handle_jumpi], "CALL": [self._handle_call], "RETURN": [self._handle_return, self._handle_transaction_end], "STOP": [self._handle_transaction_end], "EXP": [self._handle_exp], } for func in funcs[opcode]: func(state)
def _execute(self, state: GlobalState) -> None: if state.get_current_instruction()["address"] in self._cache: return issues = self._analyze_state(state) for issue in issues: self._cache.add(issue.address) self._issues.extend(issues)
def _execute(self, state: GlobalState) -> None: if state.get_current_instruction()["address"] in self.cache: return issues = self._analyze_state(state) annotation = get_potential_issues_annotation(state) annotation.potential_issues.extend(issues)
def execute_state(record: TaintRecord, state: GlobalState) -> TaintRecord: assert len(state.mstate.stack) == len(record.stack) """ Runs taint analysis on a state """ record.add_state(state) new_record = record.clone() # Apply Change op = state.get_current_instruction()["opcode"] if op in TaintRunner.stack_taint_table.keys(): mutator = TaintRunner.stack_taint_table[op] TaintRunner.mutate_stack(new_record, mutator) elif op.startswith("PUSH"): TaintRunner.mutate_push(op, new_record) elif op.startswith("DUP"): TaintRunner.mutate_dup(op, new_record) elif op.startswith("SWAP"): TaintRunner.mutate_swap(op, new_record) elif op is "MLOAD": TaintRunner.mutate_mload(new_record, state.mstate.stack[-1]) elif op.startswith("MSTORE"): TaintRunner.mutate_mstore(new_record, state.mstate.stack[-1]) elif op is "SLOAD": TaintRunner.mutate_sload(new_record, state.mstate.stack[-1]) elif op is "SSTORE": TaintRunner.mutate_sstore(new_record, state.mstate.stack[-1]) elif op.startswith("LOG"): TaintRunner.mutate_log(new_record, op) elif op in ("CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"): TaintRunner.mutate_call(new_record, op) else: logging.debug("Unknown operation encountered: {}".format(op)) return new_record
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 get_issue(self, global_state: GlobalState) -> Optional[Issue]: if not self.state_change_states: 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 Issue( 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, )
def execute_create(): global last_state global created_contract_account if not last_state and not created_contract_account: code_raw = [] for i in range(len(contract_init_code) // 2): code_raw.append(int(contract_init_code[2 * i:2 * (i + 1)], 16)) calldata = ConcreteCalldata(0, code_raw) world_state = WorldState() account = world_state.create_account(balance=1000000, address=101) account.code = Disassembly("60a760006000f000") environment = Environment(account, None, calldata, None, None, None) og_state = GlobalState(world_state, environment, None, MachineState(gas_limit=8000000)) og_state.transaction_stack.append( (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None)) laser = LaserEVM() states = [og_state] last_state = og_state for state in states: new_states, op_code = laser.execute_state(state) last_state = state if op_code == "STOP": break states.extend(new_states) created_contract_address = last_state.mstate.stack[-1].value created_contract_account = last_state.world_state.accounts[ created_contract_address] return last_state, created_contract_account
def world_state_filter_hook(state: GlobalState): if isinstance(state.current_transaction, ContractCreationTransaction): # Reset iteration variable self.iteration = 0 return world_state_annotation = get_ws_dependency_annotation(state) annotation = get_dependency_annotation(state) # Reset the state annotation except for storage written which is carried on to # the next transaction annotation.path = [0] annotation.storage_loaded = [] annotation.has_call = False world_state_annotation.annotations_stack.append(annotation) log.debug( "Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}" .format( self.iteration, state.get_current_instruction()["address"], state.node.function_name, self.dependency_map, annotation.storage_written[self.iteration], ))
def jumpi_hook(state: GlobalState): address = state.get_current_instruction()["address"] annotation = get_dependency_annotation(state) annotation.path.append(address) _check_basic_block(address, annotation)
def test_concrete_call_symbolic_to(): # arrange address = "0x10" active_account = Account(address) active_account.code = Disassembly("00") environment = Environment(active_account, None, None, None, None, None) state = GlobalState(None, environment, None) state.mstate.memory = ["placeholder", "calldata_bling_0"] node = Node("example") node.contract_name = "the contract name" node.function_name = "the function name" to = Variable("calldata_3", VarType.SYMBOLIC) meminstart = Variable(1, VarType.CONCRETE) call = Call(node, state, None, None, to, None) # act issues = _concrete_call(call, state, address, meminstart) # assert issue = issues[0] assert issue.address == address assert issue.contract == node.contract_name assert issue.function == node.function_name assert issue.title == "Call data forwarded with delegatecall()" assert issue.type == "Informational" assert ( issue.description == "This contract forwards its call data via DELEGATECALL in its fallback function." " This means that any function in the called contract can be executed." " Note that the callee contract will have access to the storage of the " "calling contract.\n DELEGATECALL target: calldata_3" )
def get_state(): active_account = Account("0x0", code=Disassembly("60606040")) environment = Environment(active_account, None, None, None, None, None) state = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) state.transaction_stack.append( (MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None) ) return state
def _new_node_state( self, state: GlobalState, edge_type=JumpType.UNCONDITIONAL, condition=None ) -> None: """ :param state: :param edge_type: :param condition: """ new_node = Node(state.environment.active_account.contract_name) old_node = state.node state.node = new_node new_node.constraints = state.mstate.constraints if self.requires_statespace: self.nodes[new_node.uid] = new_node self.edges.append( Edge( old_node.uid, new_node.uid, edge_type=edge_type, condition=condition ) ) if edge_type == JumpType.RETURN: new_node.flags |= NodeFlags.CALL_RETURN elif edge_type == JumpType.CALL: try: if "retval" in str(state.mstate.stack[-1]): new_node.flags |= NodeFlags.CALL_RETURN else: new_node.flags |= NodeFlags.FUNC_ENTRY except StackUnderflowException: new_node.flags |= NodeFlags.FUNC_ENTRY address = state.environment.code.instruction_list[state.mstate.pc]["address"] environment = state.environment disassembly = environment.code if isinstance( state.world_state.transaction_sequence[-1], ContractCreationTransaction ): environment.active_function_name = "constructor" elif address in disassembly.address_to_function_name: # Enter a new function environment.active_function_name = disassembly.address_to_function_name[ address ] new_node.flags |= NodeFlags.FUNC_ENTRY log.debug( "- Entering function " + environment.active_account.contract_name + ":" + new_node.function_name ) elif address == 0: environment.active_function_name = "fallback" new_node.function_name = environment.active_function_name
def _end_message_call( self, return_global_state: GlobalState, global_state: GlobalState, revert_changes=False, return_data=None, ) -> List[GlobalState]: """ :param return_global_state: :param global_state: :param revert_changes: :param return_data: :return: """ return_global_state.mstate.constraints += global_state.mstate.constraints # Resume execution of the transaction initializing instruction op_code = return_global_state.environment.code.instruction_list[ return_global_state.mstate.pc]["opcode"] # Set execution result in the return_state return_global_state.last_return_data = return_data if not revert_changes: return_global_state.world_state = copy(global_state.world_state) return_global_state.environment.active_account = global_state.accounts[ return_global_state.environment.active_account.address.value] if isinstance(global_state.current_transaction, ContractCreationTransaction): return_global_state.mstate.min_gas_used += ( global_state.mstate.min_gas_used) return_global_state.mstate.max_gas_used += ( global_state.mstate.max_gas_used) # Execute the post instruction handler new_global_states = Instruction(op_code, self.dynamic_loader, self.iprof).evaluate( return_global_state, True) # In order to get a nice call graph we need to set the nodes here for state in new_global_states: state.node = global_state.node return new_global_states
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 _get_global_state(): active_account = Account("0x0", code=Disassembly("60606040")) passive_account = Account("0x325345346564645654645", code=Disassembly("6060604061626364")) environment = Environment(active_account, None, None, None, None, None) world_state = WorldState() world_state.put_account(active_account) world_state.put_account(passive_account) return GlobalState(world_state, environment, None, MachineState(gas_limit=8000000))
def test_execute(mocker): active_account = Account("0x00") environment = Environment(active_account, None, None, None, None, None) state_1 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) state_1.mstate.stack = [1, 2] mocker.patch.object(state_1, "get_current_instruction") state_1.get_current_instruction.return_value = {"opcode": "PUSH"} state_2 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) state_2.mstate.stack = [1, 2, 3] mocker.patch.object(state_2, "get_current_instruction") state_2.get_current_instruction.return_value = {"opcode": "ADD"} node_1 = Node("Test contract") node_1.states = [state_1, state_2] state_3 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) state_3.mstate.stack = [1, 2] mocker.patch.object(state_3, "get_current_instruction") state_3.get_current_instruction.return_value = {"opcode": "ADD"} node_2 = Node("Test contract") node_2.states = [state_3] edge = Edge(node_1.uid, node_2.uid) statespace = LaserEVM(None) statespace.edges = [edge] statespace.nodes[node_1.uid] = node_1 statespace.nodes[node_2.uid] = node_2 # Act result = TaintRunner.execute(statespace, node_1, state_1, [True, True]) # Assert print(result) assert len(result.records) == 3 assert result.records[2].states == [] assert state_3 in result.records[1].states
def initial_global_state_from_environment(self, environment, active_function, last_function_called=None): # Initialize the execution environment global_state = GlobalState(self.world_state, environment, None, last_function_called=last_function_called) global_state.environment.active_function_name = active_function return global_state
def test_concrete_call_not_calldata(): # arrange state = GlobalState(None, None, None) state.mstate.memory = ["placeholder", "not_calldata"] meminstart = Variable(1, VarType.CONCRETE) # act issues = _concrete_call(None, state, None, meminstart) # assert assert issues == []