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 _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 _balance_change(value: BitVec, global_state: GlobalState) -> bool: if not value.symbolic: assert value.value is not None return value.value > 0 else: constraints = copy(global_state.mstate.constraints) try: solver.get_model(constraints + [value > symbol_factory.BitVecVal(0, 256)]) return True except UnsatError: return False
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 _analyze_state(state, node): issues = [] instruction = state.get_current_instruction() if instruction["opcode"] != "SUICIDE": return [] to = state.mstate.stack[-1] logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) description = "A reachable SUICIDE instruction was detected. " if "caller" in str(to): description += "The remaining Ether is sent to the caller's address.\n" elif "storage" in str(to): description += "The remaining Ether is sent to a stored address.\n" elif "calldata" in str(to): description += "The remaining Ether is sent to an address provided as a function argument.\n" elif type(to) == BitVecNumRef: description += "The remaining Ether is sent to: " + hex(to.as_long()) + "\n" else: description += "The remaining Ether is sent to: " + str(to) + "\n" not_creator_constraints = [] if len(state.world_state.transaction_sequence) > 1: creator = state.world_state.transaction_sequence[0].caller for transaction in state.world_state.transaction_sequence[1:]: not_creator_constraints.append( Not(Extract(159, 0, transaction.caller) == Extract(159, 0, creator)) ) not_creator_constraints.append( Not(Extract(159, 0, transaction.caller) == 0) ) try: model = solver.get_model(node.constraints + not_creator_constraints) debug = "Transaction Sequence: " + str( solver.get_transaction_sequence( state, node.constraints + not_creator_constraints ) ) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], swc_id=UNPROTECTED_SELFDESTRUCT, bytecode=state.environment.code.bytecode, title="Unchecked SUICIDE", _type="Warning", description=description, debug=debug, ) issues.append(issue) except UnsatError: logging.debug("[UNCHECKED_SUICIDE] no model found") return issues
def execute(statespace): logging.debug("Executing module: EXCEPTIONS") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if (instruction['opcode'] == "ASSERT_FAIL"): try: model = solver.get_model(node.constraints) address = state.get_current_instruction()['address'] description = "A reachable exception (opcode 0xfe) has been detected. This can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. " description += "This is acceptable in most situations. Note however that `assert()` should only be used to check invariants. Use `require()` for regular input checking. " debug = "The exception is triggered under the following conditions:\n\n" debug += solver.pretty_print_model(model) issues.append( Issue(node.contract_name, node.function_name, address, "Exception state", "Informational", description, debug)) except UnsatError: logging.debug("[EXCEPTIONS] no model found") return issues
def execute(statespace): logging.debug("Executing module: ASSERT FAILS") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for instruction in node.instruction_list: if (instruction['opcode'] == "INVALID"): try: model = solver.get_model(node.constraints) issue = Issue(node.module_name, node.function_name, instruction['address'], "Assert Fail", "Warning") issue.description = "A possible assert failure exists in the function " + node.function_name issues.append(issue) for d in model.decls(): print("[ASSERT_FAIL] model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug( "Couldn't find constraints to reach this invalid") return issues
def execute(statespace): logging.debug("Executing module: INTEGER_UNDERFLOW") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for instruction in node.instruction_list: if(instruction['opcode'] == "SUB"): stack = node.states[instruction['address']].stack op0 = stack[-1] op1 = stack[-2] constraints = copy.deepcopy(node.constraints) if type(op0) == int and type(op1) == int: continue if (re.search(r'calldatasize_', str(op0))) \ or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \ or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)): # Filter for patterns that contain possible (but apparently non-exploitable) Integer underflows. # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large. # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0 # Both seem to be standard compiler outputs that exist in many contracts. continue logging.debug("[INTEGER_UNDERFLOW] Checking SUB " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address'])) constraints.append(UGT(op1,op0)) try: model = solver.get_model(constraints) issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Underflow", "Warning") issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \ "The SUB instruction at address " + str(instruction['address']) + " may result in a value < 0." issue.debug = "(" + str(op0) + ") - (" + str(op1) + ").]" issues.append(issue) for d in model.decls(): logging.debug("[INTEGER_UNDERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] no model found") return issues
def _try_constraints(constraints, new_constraints): """ Tries new constraints :return Model if satisfiable otherwise None """ try: return solver.get_model(constraints + new_constraints) except UnsatError: return None
def test_vmtest( test_name: str, pre_condition: dict, action: dict, post_condition: dict ) -> None: # Arrange accounts = {} for address, details in pre_condition.items(): account = Account(address) account.code = Disassembly(details["code"][2:]) account.balance = int(details["balance"], 16) account.nonce = int(details["nonce"], 16) accounts[address] = account laser_evm = LaserEVM(accounts) # Act laser_evm.time = datetime.now() # TODO: move this line below and check for VmExceptions when gas has been implemented if post_condition == {}: return execute_message_call( laser_evm, callee_address=action["address"], caller_address=action["caller"], origin_address=action["origin"], code=action["code"][2:], gas=action["gas"], data=binascii.a2b_hex(action["data"][2:]), gas_price=int(action["gasPrice"], 16), value=int(action["value"], 16), ) # Assert assert len(laser_evm.open_states) == 1 world_state = laser_evm.open_states[0] model = get_model(next(iter(laser_evm.nodes.values())).states[0].mstate.constraints) for address, details in post_condition.items(): account = world_state[address] assert account.nonce == int(details["nonce"], 16) assert account.code.bytecode == details["code"][2:] for index, value in details["storage"].items(): expected = int(value, 16) if type(account.storage[int(index, 16)]) != int: actual = model.eval(account.storage[int(index, 16)]) actual = 1 if actual == True else 0 if actual == False else actual else: actual = account.storage[int(index, 16)] assert actual == expected
def _check_integer_underflow(state, node): """ Checks for integer underflow :param state: state from node to examine :param node: node to examine :return: found issue """ issues = [] instruction = state.get_current_instruction() if instruction['opcode'] == "SUB": stack = state.mstate.stack op0 = stack[-1] op1 = stack[-2] constraints = copy.deepcopy(node.constraints) # Filter for patterns that contain bening nteger underflows. # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large. # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0 if type(op0) == int and type(op1) == int: return [] if re.search(r'calldatasize_', str(op0)): return [] if re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL): return [] if re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL): return [] logging.debug("[INTEGER_UNDERFLOW] Checking SUB {0}, {1} at address {2}".format(str(op0), str(op1), str(instruction['address']))) allowed_types = [int, BitVecRef, BitVecNumRef] if type(op0) in allowed_types and type(op1) in allowed_types: constraints.append(UGT(op1, op0)) try: model = solver.get_model(constraints) issue = Issue(node.contract_name, node.function_name, instruction['address'], "Integer Underflow", "Warning") issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \ "The subtraction may result in a value < 0." issue.debug = solver.pretty_print_model(model) issues.append(issue) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] no model found") return issues
def _can_change(constraints, variable): """ Checks if the variable can change given some constraints """ _constraints = copy.deepcopy(constraints) try: model = solver.get_model(_constraints) except UnsatError: return False initial_value = int(str(model.eval(variable, model_completion=True))) return _try_constraints(constraints, [variable != initial_value]) is not None
def _analyze_state(state, node): issues = [] instruction = state.get_current_instruction() if instruction["opcode"] != "CALL": return [] call_value = state.mstate.stack[-3] target = state.mstate.stack[-2] not_creator_constraints, constrained = get_non_creator_constraints(state) if constrained: return [] try: """ FIXME: Instead of solving for call_value > 0, check whether call value can be greater than the total value of all transactions received by the caller """ model = solver.get_model(node.constraints + not_creator_constraints + [call_value > 0]) transaction_sequence = solver.get_transaction_sequence( state, node.constraints + not_creator_constraints + [call_value > 0]) # For now we only report an issue if zero ETH has been sent to the contract account. for key, value in transaction_sequence.items(): if int(value["call_value"], 16) > 0: return [] debug = "Transaction Sequence: " + str(transaction_sequence) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], swc_id=UNPROTECTED_ETHER_WITHDRAWAL, title="Ether thief", _type="Warning", bytecode=state.environment.code.bytecode, description= "Users other than the contract creator can withdraw ETH from the contract account" + " without previously having sent any ETH to it. This is likely to be vulnerability.", debug=debug, ) issues.append(issue) except UnsatError: logging.debug("[ETHER_THIEF] no model found") return issues
def solve(call): try: model = solver.get_model(call.node.constraints) logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model)) pretty_model = solver.pretty_print_model(model) logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] main model: \n%s" % pretty_model) return True except UnsatError: logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] no model found") return False
def solve(call): try: model = solver.get_model(call.node.constraints) logging.debug("[WEAK_RANDOM] MODEL: " + str(model)) for d in model.decls(): logging.debug("[WEAK_RANDOM] main model: %s = 0x%x" % (d.name(), model[d].as_long())) return True except UnsatError: logging.debug("[WEAK_RANDOM] no model found") return False
def _try_constraints(constraints, new_constraints): """ Tries new constraints :return Model if satisfiable otherwise None """ _constraints = copy.deepcopy(constraints) for constraint in new_constraints: _constraints.append(copy.deepcopy(constraint)) try: model = solver.get_model(_constraints) return model except UnsatError: return None
def sstor_analysis(self): logging.info("Analyzing storage operations...") for index in self.sstors: for s in self.sstors[index]: # For now we simply 'taint' every storage location that is reachable without any constraint on msg.sender taint = True for constraint in s.node.constraints: if ("caller" in str(constraint)): taint = False break if taint: try: solver.get_model(s.node.constraints) s.tainted = True except UnsatError: s.tainted = False
def execute(self, statespace): logging.debug("Executing module: EXCEPTIONS") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if instruction["opcode"] == "ASSERT_FAIL": try: model = solver.get_model(node.constraints) address = state.get_current_instruction()["address"] description = ( "A reachable exception (opcode 0xfe) has been detected. " "This can be caused by type errors, division by zero, " "out-of-bounds array access, or assert violations. " ) description += ( "Note that explicit `assert()` should only be used to check invariants. " "Use `require()` for regular input checking.") debug = "Transaction Sequence: " + str( solver.get_transaction_sequence( state, node.constraints)) issues.append( Issue( contract=node.contract_name, function_name=node.function_name, address=address, swc_id=ASSERT_VIOLATION, title="Exception state", _type="Informational", description=description, bytecode=state.environment.code.bytecode, debug=debug, gas_used=( state.mstate.min_gas_used, state.mstate.max_gas_used, ), )) except UnsatError: logging.debug("[EXCEPTIONS] no model found") return issues
def solve(call): try: model = solver.get_model(call.node.constraints) logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model)) for d in model.decls(): logging.debug( "[DEPENDENCE_ON_PREDICTABLE_VARS] main model: %s = 0x%x" % (d.name(), model[d].as_long())) return True except UnsatError: logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] no model found") return False
def _analyze_state(state, node): issues = [] instruction = state.get_current_instruction() if instruction['opcode'] != "SUICIDE": return [] to = state.mstate.stack[-1] logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) description = "The function `" + node.function_name + "` executes the SUICIDE instruction. " if "caller" in str(to): description += "The remaining Ether is sent to the caller's address.\n" elif "storage" in str(to): description += "The remaining Ether is sent to a stored address.\n" elif "calldata" in str(to): description += "The remaining Ether is sent to an address provided as a function argument.\n" elif type(to) == BitVecNumRef: description += "The remaining Ether is sent to: " + hex( to.as_long()) + "\n" else: description += "The remaining Ether is sent to: " + str(to) + "\n" not_creator_constraints = [] if len(state.world_state.transaction_sequence) > 1: creator = state.world_state.transaction_sequence[0].caller for transaction in state.world_state.transaction_sequence[1:]: not_creator_constraints.append( Not( Extract(159, 0, transaction.caller) == Extract( 159, 0, creator))) not_creator_constraints.append( Not(Extract(159, 0, transaction.caller) == 0)) try: model = solver.get_model(node.constraints + not_creator_constraints) debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(model) issue = Issue(node.contract_name, node.function_name, instruction['address'], "Unchecked SUICIDE", "Warning", description, debug) issues.append(issue) except UnsatError: logging.debug("[UNCHECKED_SUICIDE] no model found") return issues
def _analyze_state(state, node): issues = [] instruction = state.get_current_instruction() if instruction["opcode"] != "CALL": return [] call_value = state.mstate.stack[-3] target = state.mstate.stack[-2] not_creator_constraints = [] if len(state.world_state.transaction_sequence) > 1: creator = state.world_state.transaction_sequence[0].caller for transaction in state.world_state.transaction_sequence[1:]: not_creator_constraints.append( Not( Extract(159, 0, transaction.caller) == Extract( 159, 0, creator))) not_creator_constraints.append( Not(Extract(159, 0, transaction.caller) == 0)) try: model = solver.get_model(node.constraints + not_creator_constraints + [call_value > 0]) debug = "Transaction Sequence: " + str( solver.get_transaction_sequence( state, node.constraints + not_creator_constraints + [call_value > 0])) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], swc_id=UNPROTECTED_ETHER_WITHDRAWAL, title="Ether send", _type="Warning", bytecode=state.environment.code.bytecode, description= "It seems that an attacker is able to execute an call instruction," " this can mean that the attacker is able to extract funds " "out of the contract.".format(target), debug=debug, ) issues.append(issue) except UnsatError: logging.debug("[UNCHECKED_SUICIDE] no model found") return issues
def wanna_execute(self, address: int, annotation: DependencyAnnotation) -> bool: """Decide whether the basic block starting at 'address' should be executed. :param address :param storage_write_cache """ storage_write_cache = annotation.get_storage_write_cache( self.iteration - 1) if address in self.calls_on_path: return True # Skip "pure" paths that don't have any dependencies. if address not in self.sloads_on_path: return False # Execute the path if there are state modifications along it that *could* be relevant if address in self.storage_accessed_global: for location in self.sstores_on_path: try: solver.get_model((location == address, )) return True except UnsatError: continue dependencies = self.sloads_on_path[address] # Execute the path if there's any dependency on state modified in the previous transaction for location in storage_write_cache: for dependency in dependencies: # Is there a known read operation along this path that matches a write in the previous transaction? try: solver.get_model((location == dependency, )) return True except UnsatError: continue # Has the *currently executed* path been influenced by a write operation in the previous transaction? for dependency in annotation.storage_loaded: try: solver.get_model((location == dependency, )) return True except UnsatError: continue return False
def wanna_execute(self, address: int, storage_write_cache) -> bool: """Decide whether the basic block starting at 'address' should be executed. :param address :param storage_write_cache """ if address in self.protected_addresses or address not in self.dependency_map: return True dependencies = self.dependency_map[address] # Return if *any* dependency is found for location in storage_write_cache: for dependency in dependencies: try: solver.get_model([location == dependency]) return True except UnsatError: continue return False
def _check_integer_overflow(statespace, state, node): """ Checks for integer overflow :param statespace: statespace that is being examined :param state: state from node to examine :param node: node to examine :return: found issue """ issues = [] # Check the instruction instruction = state.get_current_instruction() if instruction['opcode'] != "ADD": return [] constraints = copy.deepcopy(node.constraints) # Formulate overflow constraints stack = state.mstate.stack op0, op1 = stack[-1], stack[-2] # An integer overflow is possible if op0 + op1, constraints.append(UGT(op0 + op1, (2 ** 32) - 1)) try: model = solver.get_model(constraints) # If we get to this point then there has been an integer overflow # Find out if the overflowed value is actually used interesting_usages = _search_children(statespace, node, (op0 + op1), index=node.states.index(state)) # Stop if it isn't if len(interesting_usages) == 0: return issues issue = Issue(node.contract_name, node.function_name, instruction['address'], "Integer Overflow ", "Warning") issue.description = "A possible integer overflow exists in the function {}.\n" \ "The addition may result in a value higher than the maximum representable integer.".format(node.function_name) issue.debug = solver.pretty_print_model(model) issues.append(issue) except UnsatError: logging.debug("[INTEGER_OVERFLOW] no model found") return issues
def execute(statespace): logging.debug("Executing module: EXCEPTIONS") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if (instruction['opcode'] == "ASSERT_FAIL"): logging.debug("Opcode 0xfe detected.") try: model = solver.get_model(node.constraints) logging.debug("[EXCEPTIONS] MODEL: " + str(model)) address = state.get_current_instruction()['address'] description = "A reachable exception (opcode 0xfe) has been detected. This can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. " description += "This is acceptable in most situations. Note however that assert() should only be used to check invariants. Use require() for regular input checking. " description += "The exception is triggered under the following conditions:\n\n" for d in model.decls(): try: condition = hex(model[d].as_long()) except: condition = str(simplify(model[d])) description += ("%s: %s\n" % (d.name(), condition)) description += "\n" issues.append( Issue(node.contract_name, node.function_name, address, "Exception state", "Informational", description)) except UnsatError: logging.debug("[EXCEPTIONS] no model found") return issues
def solve(call: Call) -> bool: """ :param call: :return: """ try: model = solver.get_model(call.state.mstate.constraints) log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model)) pretty_model = solver.pretty_print_model(model) log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] main model: \n%s" % pretty_model) return True except UnsatError: log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] no model found") return False
def _can_change(self, constraints, variable): """Checks if the variable can change given some constraints. :param constraints: :param variable: :return: """ _constraints = copy.deepcopy(constraints) try: model = solver.get_model(_constraints) except UnsatError: return False try: initial_value = int(str(model.eval(variable, model_completion=True))) return ( self._try_constraints(constraints, [variable != initial_value]) is not None ) except AttributeError: return False
def execute(statespace): logging.debug("Executing module: INTEGER") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if (instruction['opcode'] == "SUB"): stack = state.mstate.stack op0 = stack[-1] op1 = stack[-2] constraints = copy.deepcopy(node.constraints) if type(op0) == int and type(op1) == int: continue if (re.search(r'calldatasize_', str(op0))) \ or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \ or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)): # Filter for patterns that contain bening nteger underflows. # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large. # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0 continue logging.debug("[INTEGER_UNDERFLOW] Checking SUB " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address'])) allowed_types = [int, BitVecRef, BitVecNumRef] if type(op0) in allowed_types and type(op1) in allowed_types: constraints.append(UGT(op1, op0)) try: model = solver.get_model(constraints) issue = Issue(node.contract_name, node.function_name, instruction['address'], "Integer Underflow", "Warning") issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \ "The substraction may result in a value < 0." issue.debug = solver.pretty_print_model(model) issues.append(issue) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] no model found") return issues
def _check_integer_underflow(statespace, state, node): """ Checks for integer underflow :param state: state from node to examine :param node: node to examine :return: found issue """ issues = [] instruction = state.get_current_instruction() if instruction['opcode'] == "SUB": stack = state.mstate.stack op0 = stack[-1] op1 = stack[-2] constraints = copy.deepcopy(node.constraints) # Filter for patterns that indicate benign underflows # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large. # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0 if type(op0) == int and type(op1) == int: return [] if re.search(r'calldatasize_', str(op0)): return [] if re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search( r'256\*.*If\(1', str(op1), re.DOTALL): return [] if re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search( r'32 \+.*calldata', str(op1), re.DOTALL): return [] logging.debug( "[INTEGER_UNDERFLOW] Checking SUB {0}, {1} at address {2}".format( str(op0), str(op1), str(instruction['address']))) allowed_types = [int, BitVecRef, BitVecNumRef] if type(op0) in allowed_types and type(op1) in allowed_types: constraints.append(UGT(op1, op0)) try: model = solver.get_model(constraints) # If we get to this point then there has been an integer overflow # Find out if the overflowed value is actually used interesting_usages = _search_children( statespace, node, (op0 - op1), index=node.states.index(state)) # Stop if it isn't if len(interesting_usages) == 0: return issues issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, title="Integer Underflow", _type="Warning") issue.description = "A possible integer underflow exists in the function `" + node.function_name + "`.\n" \ "The subtraction may result in a value < 0." issue.debug = solver.pretty_print_model(model) issues.append(issue) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] no model found") return issues
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