def _check_integer_underflow(self, 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) if type(op0) == int and type(op1) == int: 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( Not(BVSubNoUnderflow(op0, op1, signed=False))) 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 = self._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_name=node.function_name, address=instruction["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=state.environment.code.bytecode, title="Integer Underflow", _type="Warning", gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issue.description = ( "The subtraction can result in an integer underflow.\n" ) issue.debug = str( solver.get_transaction_sequence( state, node.constraints)) issues.append(issue) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] no model found") return issues
def execute(statespace): logging.debug("Executing module: ETHER_SEND") issues = [] for call in statespace.calls: if ("callvalue" in str(call.value)): logging.debug("[ETHER_SEND] Skipping refund function") continue # We're only interested in calls that send Ether if call.value.type == VarType.CONCRETE: if call.value.val == 0: continue interesting = False description = "In the function '" + call.node.function_name + "' " if re.search(r'caller', str(call.to)): description += "a non-zero amount of Ether is sent to msg.sender.\n" interesting = True elif re.search(r'calldata', str(call.to)): description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n" interesting = True else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if (m): idx = m.group(1) func = statespace.find_storage_write(idx) if (func): description += "\nThere is a check on storage index " + str( idx ) + ". This storage slot can be written to by calling the function '" + func + "'.\n" interesting = True else: logging.debug("[ETHER_SEND] No storage writes to index " + str(idx)) if interesting: node = call.node can_solve = True constrained = False index = 0 while (can_solve and index < len(node.constraints)): constraint = node.constraints[index] index += 1 logging.debug("[ETHER_SEND] Constraint: " + str(constraint)) m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) if (m): constrained = True idx = m.group(1) func = statespace.find_storage_write(idx) if (func): description += "\nThere is a check on storage index " + str( index ) + ". This storage slot can be written to by calling the function '" + func + "'." else: logging.debug( "[ETHER_SEND] No storage writes to index " + str(index)) can_solve = False break # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): constrained = True can_solve = False break if not constrained: description += "It seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.constraints) logging.debug("[ETHER_SEND] MODEL: " + str(model)) for d in model.decls(): logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long())) issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Ether send", "Warning", description) issues.append(issue) except UnsatError: logging.debug("[ETHER_SEND] no model found") return issues
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 _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"] not in ("ADD", "MUL"): return issues # Formulate overflow constraints stack = state.mstate.stack op0, op1 = stack[-1], stack[-2] # An integer overflow is possible if op0 + op1 or op0 * op1 > MAX_UINT # Do a type check allowed_types = [int, BitVecRef, BitVecNumRef] if not (type(op0) in allowed_types and type(op1) in allowed_types): return issues # Change ints to BitVec if type(op0) is int: op0 = BitVecVal(op0, 256) if type(op1) is int: op1 = BitVecVal(op1, 256) # Formulate expression # FIXME: handle exponentiation if instruction["opcode"] == "ADD": operator = "add" expr = op0 + op1 # constraint = Not(BVAddNoOverflow(op0, op1, signed=False)) else: operator = "multiply" expr = op1 * op0 # constraint = Not(BVMulNoOverflow(op0, op1, signed=False)) constraint = Or(And(ULT(expr, op0), op1 != 0), And(ULT(expr, op1), op0 != 0)) # Check satisfiable model = _try_constraints(node.constraints, [constraint]) if model is None: logging.debug("[INTEGER_OVERFLOW] no model found") return issues if not _verify_integer_overflow(statespace, node, expr, state, model, constraint, op0, op1): return issues # Build issue issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=state.environment.code.bytecode, title="Integer Overflow", _type="Warning", ) issue.description = "This binary {} operation can result in integer overflow.\n".format( operator) try: issue.debug = "Transaction Sequence: " + str( solver.get_transaction_sequence(state, node.constraints)) except UnsatError: return issues issues.append(issue) return issues
def execute(statespace): logging.debug("Executing module: UNCHECKED_RETVAL") issues = [] visited = [] for call in statespace.calls: # Only needs to be checked once per call instructions (it's essentially just static analysis) if call.addr in visited: continue else: visited.append(call.addr) # The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.: # [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}] start_index = helper.get_instruction_index(call.node.instruction_list, call.addr) + 1 retval_checked = False # ISZERO retval should be found within the next few instructions. for i in range(0, 10): try: instr = call.node.instruction_list[start_index + i] except IndexError: break if (instr['opcode'] == 'ISZERO' and re.search( r'retval', str( call.node.states[instr['address']].stack[-1]))): retval_checked = True break if not retval_checked: issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Unchecked CALL return value") if (call.to.type == VarType.CONCRETE): receiver = hex(call.to.val) elif (re.search(r"caller", str(call.to))): receiver = "msg.sender" elif (re.search(r"storage", str(call.to))): receiver = "an address obtained from storage" else: receiver = str(call.to) issue.description = \ "The function " + call.node.function_name + " contains a call to " + receiver + ".\n" \ "The return value of this call is not checked. Note that the function will continue to execute with a return value of '0' if the called contract throws." issues.append(issue) return issues
def execute(statespace): logging.debug("Executing module: ETHER_SEND") issues = [] for call in statespace.calls: state = call.state address = state.get_current_instruction()['address'] if "callvalue" in str(call.value): logging.debug("[ETHER_SEND] Skipping refund function") continue # We're only interested in calls that send Ether if call.value.type == VarType.CONCRETE and call.value.val == 0: continue interesting = False description = "In the function `" + call.node.function_name + "` " if re.search(r'caller', str(call.to)): description += "a non-zero amount of Ether is sent to msg.sender.\n" interesting = True elif re.search(r'calldata', str(call.to)): description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n" interesting = True else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if m: idx = m.group(1) description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str( idx) + ".\n" func = statespace.find_storage_write( state.environment.active_account.address, idx) if func: description += "There is a check on storage index " + str( idx ) + ". This storage slot can be written to by calling the function `" + func + "`.\n" interesting = True else: logging.debug("[ETHER_SEND] No storage writes to index " + str(idx)) if interesting: node = call.node can_solve = True constrained = False index = 0 while can_solve and index < len(node.constraints): constraint = node.constraints[index] index += 1 logging.debug("[ETHER_SEND] Constraint: " + str(constraint)) m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) if m: constrained = True idx = m.group(1) func = statespace.find_storage_write( state.environment.active_account.address, idx) if func: description += "\nThere is a check on storage index " + str( idx ) + ". This storage slot can be written to by calling the function `" + func + "`." else: logging.debug( "[ETHER_SEND] No storage writes to index " + str(idx)) can_solve = False break # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer elif re.search(r"caller", str(constraint)) and re.search( r'[0-9]{20}', str(constraint)): constrained = True can_solve = False break if not constrained: description += "It seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.constraints) for d in model.decls(): logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long())) debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model( model) issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address, title="Ether send", _type="Warning", swc_id=UNPROTECTED_ETHER_WITHDRAWAL, description=description, debug=debug) issues.append(issue) except UnsatError: logging.debug("[ETHER_SEND] no model found") return issues
def _analyze_state(state): """ :param state: :return: """ gas = state.mstate.stack[-1] to = state.mstate.stack[-2] address = state.get_current_instruction()["address"] try: constraints = copy(state.mstate.constraints) transaction_sequence = solver.get_transaction_sequence( state, constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))]) # Check whether we can also set the callee address try: constraints += [to == ATTACKER_ADDRESS] for tx in state.world_state.transaction_sequence: if not isinstance(tx, ContractCreationTransaction): constraints.append(tx.caller == ATTACKER_ADDRESS) transaction_sequence = solver.get_transaction_sequence( state, constraints) description_head = "A call to a user-supplied address is executed." description_tail = ( "The callee address of an external message call can be set by " "the caller. Note that the callee can contain arbitrary code and may re-enter any function " "in this contract. Review the business logic carefully to prevent averse effects on the " "contract state.") issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=address, swc_id=REENTRANCY, title="External Call To User-Supplied Address", bytecode=state.environment.code.bytecode, severity="Medium", description_head=description_head, description_tail=description_tail, transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: if _is_precompile_call(state): return [] log.debug( "[EXTERNAL_CALLS] Callee address cannot be modified. Reporting informational issue." ) description_head = "The contract executes an external message call." description_tail = ( "An external function call to a fixed contract address is executed. Make sure " "that the callee contract has been reviewed carefully.") issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=address, swc_id=REENTRANCY, title="External Call To Fixed Address", bytecode=state.environment.code.bytecode, severity="Low", description_head=description_head, description_tail=description_tail, transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: log.debug("[EXTERNAL_CALLS] No model found.") return [] return [issue]
def execute(statespace): logging.debug("Executing module: UNCHECKED_RETVAL") issues = [] for k in statespace.nodes: node = statespace.nodes[k] if NodeFlags.CALL_RETURN in node.flags: retval_checked = False for state in node.states: instr = state.get_current_instruction() if instr["opcode"] == "ISZERO" and re.search( r"retval", str(state.mstate.stack[-1]) ): retval_checked = True break if not retval_checked: address = state.get_current_instruction()["address"] issue = Issue( contract=node.contract_name, function_name=node.function_name, address=address, bytecode=state.environment.code.bytecode, title="Unchecked CALL return value", swc_id=UNCHECKED_RET_VAL, ) issue.description = ( "The return value of an external call is not checked. " "Note that execution continue even if the called contract throws." ) issues.append(issue) else: n_states = len(node.states) for idx in range( 0, n_states - 1 ): # Ignore CALLs at last position in a node state = node.states[idx] instr = state.get_current_instruction() if instr["opcode"] == "CALL": retval_checked = False for _idx in range(idx, idx + 10): try: _state = node.states[_idx] _instr = _state.get_current_instruction() if _instr["opcode"] == "ISZERO" and re.search( r"retval", str(_state.mstate.stack[-1]) ): retval_checked = True break except IndexError: break if not retval_checked: address = instr["address"] issue = Issue( contract=node.contract_name, function_name=node.function_name, bytecode=state.environment.code.bytecode, address=address, title="Unchecked CALL return value", swc_id=UNCHECKED_RET_VAL, ) issue.description = ( "The return value of an external call is not checked. " "Note that execution continue even if the called contract throws." ) issues.append(issue) 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] > 2: 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 execute(statespace): issues = [] for call in statespace.calls: if (call.type == "DELEGATECALL"): state = call.state address = state.get_current_instruction()['address'] if (call.node.function_name == "fallback"): stack = state.mstate.stack meminstart = get_variable(stack[-3]) if meminstart.type == VarType.CONCRETE: if (re.search(r'calldata.*_0', str(state.mstate.memory[meminstart.val]))): issue = Issue( call.node.contract_name, call.node.function_name, address, "Call data forwarded with delegatecall()", "Informational") 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" if (call.to.type == VarType.CONCRETE): issue.description += ("DELEGATECALL target: " + hex(call.to.val)) else: issue.description += "DELEGATECALL target: " + str( call.to) issues.append(issue) if (call.to.type == VarType.SYMBOLIC): issue = Issue(call.node.contract_name, call.node.function_name, address, call.type + " to a user-supplied address") if ("calldata" in str(call.to)): issue.description = \ "This contract delegates execution to a contract address obtained from calldata. " else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if (m): idx = m.group(1) func = statespace.find_storage_write( state.environment.active_account.address, idx) if (func): issue.description = "This contract delegates execution to a contract address in storage slot " + str( idx ) + ". This storage slot can be written to by calling the function '" + func + "'. " else: logging.debug( "[DELEGATECALL] No storage writes to index " + str(idx)) issue.description += "Be aware that the called contract gets unrestricted access to this contract's state." issues.append(issue) return issues
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 []
def execute(statespace): logging.debug("Executing module: UNCHECKED_SUICIDE") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if(instruction['opcode'] == "SUICIDE"): logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) description = "The function " + node.function_name + " executes the SUICIDE instruction." stack = copy.deepcopy(state.mstate.stack) to = stack.pop() if ("caller" in str(to)): description += "\nThe remaining Ether is sent to the caller's address.\n" elif ("storage" in str(to)): description += "\nThe remaining Ether is sent to a stored address\n" elif ("calldata" in str(to)): description += "\nThe remaining Ether is sent to an address provided as a function argument." elif (type(to) == BitVecNumRef): description += "\nThe remaining Ether is sent to: " + hex(to.as_long()) else: description += "\nThe remaining Ether is sent to: " + str(to) + "\n" constrained = False can_solve = True index = 0 while(can_solve and index < len(node.constraints)): constraint = node.constraints[index] index += 1 m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) overwrite = False if (m): constrained = True idx = m.group(1) func = statespace.find_storage_write(idx) if func: description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + func + "'." break else: logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index)) can_solve = False break # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): can_solve = False break if not constrained: description += "\nIt seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.constraints) logging.debug("[UNCHECKED_SUICIDE] MODEL: " + str(model)) for d in model.decls(): logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long())) issue = Issue(node.contract_name, node.function_name, instruction['address'], "Unchecked SUICIDE", "Warning", description) issues.append(issue) except UnsatError: logging.debug("[UNCHECKED_SUICIDE] no model found") return issues
def execute(statespace): issues = [] for call in statespace.calls: if (call.type == "DELEGATECALL" or call.type == "CALLCODE"): if (call.to.type == VarType.SYMBOLIC): if ("calldata" in str(call.to)): issue = Issue(call.node.module_name, call.node.function_name, call.addr, call.type + " to dynamic address") issue.description = \ "The function " + call.node.function_name + " delegates execution to a contract address obtained from calldata.\n" \ "Recipient address: " + str(call.to) issues.append(issue) else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if (m): index = m.group(1) logging.debug( "DELEGATECALL to contract address in storage") try: for s in statespace.sstors[index]: if s.tainted: issue = Issue( call.type + " to dynamic address in storage", "Warning") issue.description = \ "The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract address stored in a state variable. " \ "There is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'.\n" \ "Make sure that the contract address cannot be set by untrusted users." issues.append(issue) break except KeyError: logging.debug( "[ETHER_SEND] No storage writes to index " + str(index)) else: issue = Issue(call.node.module_name, call.node.function_name, call.addr, "DELEGATECALL to dynamic address", "Informational") issue.description = \ "The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract with a dynamic address." \ "To address:" + str(call.to) issues.append(issue) return issues
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'] not in ("ADD", "MUL"): return issues # Formulate overflow constraints stack = state.mstate.stack op0, op1 = stack[-1], stack[-2] # An integer overflow is possible if op0 + op1 or op0 * op1 > MAX_UINT # Do a type check allowed_types = [int, BitVecRef, BitVecNumRef] if not (type(op0) in allowed_types and type(op1) in allowed_types): return issues # Change ints to BitVec if type(op0) is int: op0 = BitVecVal(op0, 256) if type(op1) is int: op1 = BitVecVal(op1, 256) # Formulate expression if instruction['opcode'] == "ADD": expr = op0 + op1 else: expr = op1 * op0 # Check satisfiable constraint = Or(And(ULT(expr, op0), op1 != 0), And(ULT(expr, op1), op0 != 0)) model = _try_constraints(node.constraints, [constraint]) if model is None: logging.debug("[INTEGER_OVERFLOW] no model found") return issues if not _verify_integer_overflow(statespace, node, expr, state, model, constraint, op0, op1): return issues # Build issue 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 or multiplication 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) 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)) logging.info(interesting_usages) # Stop if it isn't if len(interesting_usages) == 0: return issues 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 execute(statespace): issues = [] for call in statespace.calls: if ("callvalue" in str(call.value)): logging.debug("[ETHER_SEND] Skipping refund function") continue logging.debug("[ETHER_SEND] CALL with value " + str(call.value.val)) issue = Issue("Ether send", "Warning") # We're only interested in calls that send Ether if call.value.type == VarType.CONCRETE: if call.value.val == 0: continue interesting = False issue.description = "In the function '" + call.node.function_name + "' " # Check the CALL target if re.search(r'caller', str(call.to)): issue.description += "a non-zero amount of Ether is sent to msg.sender.\n" interesting = True if re.search(r'calldata', str(call.to)): issue.description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n" interesting = True issue.description += "Call value is " + str(call.value) + "\n" if interesting: node = call.node constrained = False can_solve = True while (can_solve and len(node.constraints)): constraint = node.constraints.pop() m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) overwrite = False if (m): constrained = True index = m.group(1) try: for s in statespace.sstors[index]: if s.tainted: issue.description += "\nThere is a check on storage index " + str( index ) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'." break if not overwrite: logging.debug( "[ETHER_SEND] No storage writes to index " + str(index)) can_solve = False break except KeyError: logging.debug( "[ETHER_SEND] No storage writes to index " + str(index)) can_solve = False break # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): can_solve = False break if not constrained: issue.description += "\nIt seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.constraints) issues.append(issue) for d in model.decls(): logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug("[ETHER_SEND] no model found") return issues
def execute(statespace): issues = [] for call in statespace.calls: state = call.state address = state.get_current_instruction()['address'] if (call.type == "CALL"): logging.info("[EXTERNAL_CALLS] Call to: %s, value = %s, gas = %s" % (str(call.to), str(call.value), str(call.gas))) if (call.to.type == VarType.SYMBOLIC and (call.gas.type == VarType.CONCRETE and call.gas.val > 2300) or (call.gas.type == VarType.SYMBOLIC and "2300" not in str(call.gas))): description = "This contract executes a message call to " target = str(call.to) user_supplied = False if ("calldata" in target or "caller" in target): if ("calldata" in target): description += "an address provided as a function argument. " else: description += "the address of the transaction sender. " user_supplied = True else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if (m): idx = m.group(1) func = statespace.find_storage_write(state.environment.active_account.address, idx) if func: description += \ "an address found at storage slot " + str(idx) + ". " + \ "This storage slot can be written to by calling the function '" + func + "'. " user_supplied = True if user_supplied: description += "Generally, it is not recommended to call user-supplied adresses using Solidity's call() construct. Note that attackers might leverage reentrancy attacks to exploit race conditions or manipulate this contract's state." issue = Issue(call.node.contract_name, call.node.function_name, address, "Message call to external contract", "Warning", description) else: description += "to another contract. Make sure that the called contract is trusted and does not execute user-supplied code." issue = Issue(call.node.contract_name, call.node.function_name, address, "Message call to external contract", "Informational", description) issues.append(issue) if address not in calls_visited: calls_visited.append(address) logging.debug("[EXTERNAL_CALLS] Checking for state changes starting from " + call.node.function_name) # Check for SSTORE in remaining instructions in current node & nodes down the CFG state_change_addresses = search_children(statespace, call.node, call.state_index + 1, depth=0, results=[]) logging.debug("[EXTERNAL_CALLS] Detected state changes at addresses: " + str(state_change_addresses)) if (len(state_change_addresses)): for address in state_change_addresses: description = "The contract account state is changed after an external call. Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities." issue = Issue(call.node.contract_name, call.node.function_name, address, "State change after external call", "Warning", description) issues.append(issue) return issues
def execute(statespace): logging.debug("Executing module: UNCHECKED_RETVAL") issues = [] for k in statespace.nodes: node = statespace.nodes[k] if NodeFlags.CALL_RETURN in node.flags: retval_checked = False for state in node.states: instr = state.get_current_instruction() if (instr['opcode'] == 'ISZERO' and re.search(r'retval', str(state.mstate.stack[-1]))): retval_checked = True break if not retval_checked: address = state.get_current_instruction()['address'] issue = Issue(node.contract_name, node.function_name, address, "Unchecked CALL return value") issue.description = \ "The return value of an external call is not checked. Note that the contract will continue if the call fails." issues.append(issue) else: nStates = len(node.states) for idx in range(0, nStates): state = node.states[idx] instr = state.get_current_instruction() if (instr['opcode'] == 'CALL'): retval_checked = False for _idx in range(idx, idx + 10): try: _state = node.states[_idx] _instr = _state.get_current_instruction() if (_instr['opcode'] == 'ISZERO' and re.search( r'retval', str(_state.mstate.stack[-1]))): retval_checked = True break except IndexError: break if not retval_checked: address = instr['address'] issue = Issue(node.contract_name, node.function_name, address, "Unchecked CALL return value") issue.description = \ "The return value of an external call is not checked. Note that execution continue even if the called contract throws." issues.append(issue) return issues
def execute(statespace): issues = [] for k in statespace.nodes: node = statespace.nodes[k] for instruction in node.instruction_list: if(instruction['opcode'] == "SUICIDE"): logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) issue = Issue("Unchecked SUICIDE", "Warning") issue.description = "The function " + node.function_name + " executes the SUICIDE instruction." state = node.states[instruction['address']] to = state.stack.pop() if ("caller" in str(to)): issue.description += "\nThe remaining Ether is sent to the caller's address.\n" elif ("storage" in str(to)): issue.description += "\nThe remaining Ether is sent to a stored address\n" elif ("calldata" in str(to)): issue.description += "\nThe remaining Ether is sent to an address provided as a function argument." elif (type(to) == BitVecNumRef): issue.description += "\nThe remaining Ether is sent to: " + hex(to.as_long()) else: issue.description += "\nThe remaining Ether is sent to: " + str(to) + "\n" constrained = False can_solve = True while (can_solve and len(node.constraints)): constraint = node.constraints.pop() m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) overwrite = False if (m): constrained = True index = m.group(1) try: for s in statespace.sstors[index]: if s.tainted: issue.description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'." break if not overwrite: logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index)) can_solve = False break except KeyError: logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index)) can_solve = False break # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): can_solve = False break if not constrained: issue.description += "\nIt seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.constraints) issues.append(issue) for d in model.decls(): logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug("[UNCHECKED_SUICIDE] no model found") return issues
def _analyze_state(state): """ :param state: :return: """ instruction = state.get_current_instruction() if instruction["opcode"] != "CALL": return [] address = instruction["address"] value = state.mstate.stack[-3] target = state.mstate.stack[-2] eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256) constraints = copy(state.mstate.constraints) for tx in state.world_state.transaction_sequence: """ Constraint: The call value must be greater than the sum of Ether sent by the attacker over all transactions. This prevents false positives caused by legitimate refund functions. Also constrain the addition from overflowing (otherwise the solver produces solutions with ridiculously high call values). """ constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, False)] eth_sent_by_attacker = Sum( eth_sent_by_attacker, tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0), ) """ Constraint: All transactions must originate from regular users (not the creator/owner). This prevents false positives where the owner willingly transfers ownership to another address. """ if not isinstance(tx, ContractCreationTransaction): constraints += [tx.caller != CREATOR_ADDRESS] """ Require that the current transaction is sent by the attacker and that the Ether is sent to the attacker's address. """ constraints += [ UGT(value, eth_sent_by_attacker), target == ATTACKER_ADDRESS, state.current_transaction.caller == ATTACKER_ADDRESS, ] try: transaction_sequence = solver.get_transaction_sequence(state, constraints) issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], swc_id=UNPROTECTED_ETHER_WITHDRAWAL, title="Unprotected Ether Withdrawal", severity="High", bytecode=state.environment.code.bytecode, description_head="Anyone can withdraw ETH from the contract account.", description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract" + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + " a vulnerability.", transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: log.debug("[ETHER_THIEF] no model found") return [] return [issue]
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_name=node.function_name, address=instruction["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=state.environment.code.bytecode, title="Integer Underflow", _type="Warning", ) issue.description = ( "The subtraction can result in an integer underflow.\n") issue.debug = "Transaction Sequence: " + str( solver.get_transaction_sequence(state, node.constraints)) issues.append(issue) except UnsatError: logging.debug("[INTEGER_UNDERFLOW] 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, constrained = get_non_creator_constraints( state) if constrained: return [] eth_sent_total = BitVecVal(0, 256) for tx in state.world_state.transaction_sequence[1:]: eth_sent_total += tx.call_value try: model = solver.get_model( node.constraints + not_creator_constraints + [ UGT(call_value, eth_sent_total), state.environment.sender == ARBITRARY_SENDER_ADDRESS, target == state.environment.sender, ]) transaction_sequence = solver.get_transaction_sequence( state, node.constraints + not_creator_constraints + [ call_value > eth_sent_total, state.environment.sender == ARBITRARY_SENDER_ADDRESS, target == state.environment.sender, ], ) 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= "Arbitrary senders other than the contract creator can withdraw ETH from the contract" + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + " a vulnerability.", debug=debug, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issues.append(issue) except UnsatError: logging.debug("[ETHER_THIEF] no model found") return issues
def execute(statespace): logging.debug("Executing module: CALL_TO_DYNAMIC_WITH_GAS") issues = [] for call in statespace.calls: if (call.type == "CALL"): logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] Call to: " + str(call.to) + ", value " + str(call.value) + ", gas = " + str(call.gas)) if (call.to.type == VarType.SYMBOLIC and (call.gas.type == VarType.CONCRETE and call.gas.val > 2300) or (call.gas.type == VarType.SYMBOLIC and "2300" not in str(call.gas))): description = "The function " + call.node.function_name + " contains a function call to " target = str(call.to) is_valid = False if ("calldata" in target or "caller" in target): if ("calldata" in target): description += "an address provided as a function argument. " else: description += "the address of the transaction sender. " is_valid = True else: m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) if (m): index = m.group(1) try: for s in statespace.sstors[index]: if s.tainted: description += \ "an address found at storage position " + str(index) + ".\n" + \ "This storage position can be written to by calling the function '" + s.node.function_name + "'.\n" \ "Verify that the contract address cannot be set by untrusted users.\n" is_valid = True break except KeyError: logging.debug( "[CALL_TO_DYNAMIC_WITH_GAS] No storage writes to index " + str(index)) continue if is_valid: description += "The available gas is forwarded to the called contract. Make sure that the logic of the calling contract is not adversely affected if the called contract misbehaves (e.g. reentrancy)." issue = Issue(call.node.contract_name, call.node.function_name, call.addr, "CALL with gas to dynamic address", "Warning", description) issues.append(issue) return issues
def _analyze_state(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): if annotation.add_constraints: constraints = (state.mstate.constraints + annotation.add_constraints) else: constraints = copy(state.mstate.constraints) try: transaction_sequence = solver.get_transaction_sequence( state, constraints) except UnsatError: continue 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, ), transaction_sequence=transaction_sequence, ) 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"], add_constraints=annotation.add_constraints, )) 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(state.mstate.constraints + constraint) state.annotate(OldBlockNumberUsedAnnotation(constraint)) 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): # We can append any block constraint here state.mstate.stack[-1].annotate( PredictableValueAnnotation( "block hash of a previous block", add_constraints=annotations[0].block_constraints, )) 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 execute(statespace): logging.debug("Executing module: DEPENDENCE_ON_PREDICTABLE_VARS") issues = [] for call in statespace.calls: if "callvalue" in str(call.value): logging.debug( "[DEPENDENCE_ON_PREDICTABLE_VARS] Skipping refund function") continue # We're only interested in calls that send Ether if call.value.type == VarType.CONCRETE and call.value.val == 0: continue address = call.state.get_current_instruction()['address'] description = "In the function `" + call.node.function_name + "` " description += "the following predictable state variables are used to determine Ether recipient:\n" # First check: look for predictable state variables in node & call recipient constraints vars = ["coinbase", "gaslimit", "timestamp", "number"] found = [] for var in vars: for constraint in call.node.constraints + [call.to]: if var in str(constraint): found.append(var) if len(found): for item in found: description += "- block.{}\n".format(item) if solve(call): swc_type = TIMESTAMP_DEPENDENCE if item == 'timestamp' else PREDICTABLE_VARS_DEPENDENCE issue = Issue( contract=call.node.contract_name, function=call.node.function_name, address=address, swc_id=swc_type, title="Dependence on predictable environment variable", _type="Warning", description=description) issues.append(issue) # Second check: blockhash for constraint in call.node.constraints + [call.to]: if "blockhash" in str(constraint): description = "In the function `" + call.node.function_name + "` " if "number" in str(constraint): m = re.search(r'blockhash\w+(\s-\s(\d+))*', str(constraint)) if m and solve(call): found = m.group(1) if found: # block.blockhash(block.number - N) description += "predictable expression 'block.blockhash(block.number - " + m.group(2) + \ ")' is used to determine Ether recipient" if int(m.group(2)) > 255: description += ", this expression will always be equal to zero." elif "storage" in str( constraint ): # block.blockhash(block.number - storage_0) description += "predictable expression 'block.blockhash(block.number - " + \ "some_storage_var)' is used to determine Ether recipient" else: # block.blockhash(block.number) description += "predictable expression 'block.blockhash(block.number)'" + \ " is used to determine Ether recipient" description += ", this expression will always be equal to zero." issue = Issue( contract=call.node.contract_name, function=call.node.function_name, address=address, title="Dependence on predictable variable", _type="Warning", description=description, swc_id=PREDICTABLE_VARS_DEPENDENCE) issues.append(issue) break else: r = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) if r: # block.blockhash(storage_0) ''' We actually can do better here by adding a constraint blockhash_block_storage_0 == 0 and checking model satisfiability. When this is done, severity can be raised from 'Informational' to 'Warning'. Checking that storage at given index can be tainted is not necessary, since it usually contains block.number of the 'commit' transaction in commit-reveal workflow. ''' index = r.group(1) if index and solve(call): description += 'block.blockhash() is calculated using a value from storage ' \ 'at index {}'.format(index) issue = Issue( contract=call.node.contract_name, function=call.node.function_name, address=address, title="Dependence on predictable variable", _type="Informational", description=description, swc_id=PREDICTABLE_VARS_DEPENDENCE) issues.append(issue) break return issues
def _analyze_state(state): node = state.node gas = state.mstate.stack[-1] to = state.mstate.stack[-2] address = state.get_current_instruction()["address"] try: constraints = node.constraints transaction_sequence = solver.get_transaction_sequence( state, constraints + [UGT(gas, 2300)]) # Check whether we can also set the callee address try: constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF] transaction_sequence = solver.get_transaction_sequence( state, constraints) debug = str(transaction_sequence) description = ( "The contract executes a function call with high gas to a user-supplied address. " "Note that the callee can contain arbitrary code and may re-enter any function in this contract. " "Review the business logic carefully to prevent unanticipated effects on the contract state." ) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=address, swc_id=REENTRANCY, title="External call to user-supplied address", _type="Warning", bytecode=state.environment.code.bytecode, description=description, debug=debug, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: logging.debug( "[EXTERNAL_CALLS] Callee address cannot be modified. Reporting informational issue." ) debug = str(transaction_sequence) description = ( "The contract executes a function call to an external address. " "Verify that the code at this address is trusted and immutable." ) issue = Issue( contract=node.contract_name, function_name=state.node.function_name, address=address, swc_id=REENTRANCY, title="External call", _type="Informational", bytecode=state.environment.code.bytecode, description=description, debug=debug, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: logging.debug("[EXTERNAL_CALLS] No model found.") return [] return [issue]
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 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'])) 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 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 execute(statespace): issues = [] for k in statespace.nodes: node = statespace.nodes[k] for instruction in node.instruction_list: ''' This generates a lot of noise. if(instruction['opcode'] == "ADD"): stack = node.states[instruction['address']].stack op0 = stack[-1] op1 = stack[-2] if type(op0) == int and type(op1) == int: continue logging.debug("[INTEGER_OVERFLOW] Checking ADD " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address'])) constraints = copy.deepcopy(node.constraints) constraints.append(UGT(op0, UINT_MAX - op1)) try: model = solver.get_model(constraints) issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Overflow", "Warning") issue.description = "A possible integer overflow exists in the function " + node.function_name + ".\n" \ "The addition at address " + str(instruction['address']) + " may result in a value greater than UINT_MAX." issue.debug = "(" + str(op0) + ") + (" + str(op1) + ") > (" + hex(UINT_MAX.as_long()) + ")" issues.append(issue) for d in model.decls(): logging.debug("[INTEGER_OVERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug("[INTEGER_OVERFLOW] no model found") ''' if (instruction['opcode'] == "MUL"): stack = node.states[instruction['address']].stack op0 = stack[-1] op1 = stack[-2] if (type(op0) == int and type(op1) == int ) or type(op0) == BoolRef or type(op1) == BoolRef: continue logging.debug("[INTEGER_OVERFLOW] Checking MUL " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address'])) if re.search(r'146150163733', str(op0), re.DOTALL) or re.search( r'146150163733', str(op1), re.DOTALL) or "(2 << 160 - 1)" in str( op0) or "(2 << 160 - 1)" in str(op1): continue constraints = copy.deepcopy(node.constraints) constraints.append(UGT(op0, UDiv(UINT_MAX, op1))) try: model = solver.get_model(constraints) issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Overflow", "Warning") issue.description = "A possible integer overflow exists in the function " + node.function_name + ".\n" \ "The multiplication at address " + str(instruction['address']) + " may result in a value greater than UINT_MAX." issue.debug = "(" + str(op0) + ") * (" + str( op1) + ") > (" + hex(UINT_MAX.as_long()) + ")" issues.append(issue) logging.debug("Constraints: " + str(constraints)) for d in model.decls(): logging.debug("[INTEGER_OVERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long())) except UnsatError: logging.debug("[INTEGER_OVERFLOW] no model found") return issues
def execute(statespace): logging.debug("Executing module: UNCHECKED_SUICIDE") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if (instruction['opcode'] == "SUICIDE"): logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name) description = "The function `" + node.function_name + "` executes the SUICIDE instruction. " stack = copy.deepcopy(state.mstate.stack) to = stack.pop() 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" constrained = False can_solve = True index = 0 while (can_solve and index < len(node.constraints)): constraint = node.constraints[index] index += 1 m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) if (m): constrained = True idx = m.group(1) logging.debug("STORAGE CONSTRAINT FOUND: " + idx) func = statespace.find_storage_write( state.environment.active_account.address, idx) if func: description += "\nThere is a check on storage index " + str( idx ) + ". This storage index can be written to by calling the function `" + func + "`." break else: logging.debug( "[UNCHECKED_SUICIDE] No storage writes to index " + str(idx)) can_solve = False break elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))): can_solve = False break if not constrained: description += "\nIt seems that this function can be called without restrictions." if can_solve: try: model = solver.get_model(node.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(self, state): """ :param state: :return: """ instruction = state.get_current_instruction() if instruction["opcode"] != "CALL": return [] address = instruction["address"] if self._cache_addresses.get(address, False): return [] call_value = state.mstate.stack[-3] target = state.mstate.stack[-2] eth_sent_total = symbol_factory.BitVecVal(0, 256) constraints = copy(state.mstate.constraints) for tx in state.world_state.transaction_sequence: if tx.caller == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF: # There's sometimes no overflow check on balances added. # But we don't care about attacks that require more 2^^256 ETH to be sent. constraints += [ BVAddNoOverflow(eth_sent_total, tx.call_value, False) ] eth_sent_total = Sum(eth_sent_total, tx.call_value) constraints += [ UGT(call_value, eth_sent_total), target == state.environment.sender, ] try: transaction_sequence = solver.get_transaction_sequence( state, constraints) debug = json.dumps(transaction_sequence, indent=4) issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], swc_id=UNPROTECTED_ETHER_WITHDRAWAL, title="Unprotected Ether Withdrawal", severity="High", bytecode=state.environment.code.bytecode, description_head= "Anyone can withdraw ETH from the contract account.", description_tail= "Arbitrary senders other than the contract creator can withdraw ETH from the contract" + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + " a vulnerability.", debug=debug, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: log.debug("[ETHER_THIEF] no model found") return [] self._cache_addresses[address] = True return [issue]