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): 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 execution continue even if the called contract throws." issues.append(issue) else: nStates = len(node.states) for idx in range(0, nStates - 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(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 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: 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 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 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 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): 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'])) 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.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 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 execute(statespace): logging.debug("Executing module: INTEGER_OVERFLOW") 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 _check_integer_overflow(self, 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)) # Check satisfiable model = self._try_constraints(node.constraints, [constraint]) if model is None: logging.debug("[INTEGER_OVERFLOW] no model found") 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", gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issue.description = "This binary {} operation can result in integer overflow.\n".format( operator) try: issue.debug = str( solver.get_transaction_sequence( state, node.constraints + [constraint])) except UnsatError: return issues issues.append(issue) return issues
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