def execute(statespace): logging.debug("Executing module: DELEGATECALL_TO_DYNAMIC") 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.contract_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.contract_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.contract_name, call.node.function_name, call.addr, "DELEGATECALL to dynamic address", "Informational") issue.description = \ "The function " + call.node.function_name + " in contract '" + call.node.contract_name + " delegates execution to a contract with a dynamic address." \ "To address:" + str(call.to) issues.append(issue) 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): issues = [] for call in statespace.calls: if (call.type == "DELEGATECALL") and (call.node.function_name == "main"): stack = call.state.stack meminstart = get_variable(stack[-3]) if meminstart.type == VarType.CONCRETE: if (re.search(r'calldata.*_0', str(call.state.memory[meminstart.val]))): issue = Issue("CALLDATA forwarded with delegatecall()", "Informational") issue.description = \ "The contract '" + str(call.node.module_name) + "' forwards its calldata 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.\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) return issues
def check_potential_issues(state: GlobalState) -> None: """ Called at the end of a transaction, checks potential issues, and adds valid issues to the detector. :param state: The final global state of a transaction :return: """ annotation = get_potential_issues_annotation(state) for potential_issue in annotation.potential_issues: try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints + potential_issue.constraints) except UnsatError: continue annotation.potential_issues.remove(potential_issue) potential_issue.detector.cache.add(potential_issue.address) potential_issue.detector.issues.append( Issue( contract=potential_issue.contract, function_name=potential_issue.function_name, address=potential_issue.address, title=potential_issue.title, bytecode=potential_issue.bytecode, swc_id=potential_issue.swc_id, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), severity=potential_issue.severity, description_head=potential_issue.description_head, description_tail=potential_issue.description_tail, transaction_sequence=transaction_sequence, ))
def execute(statespace): """ Executes the analysis module""" logging.debug("Executing module: TOD") issues = [] for call in statespace.calls: # Do analysis interesting_storages = list(_get_influencing_storages(call)) changing_sstores = list( _get_influencing_sstores(statespace, interesting_storages) ) # Build issue if necessary if len(changing_sstores) > 0: node = call.node instruction = call.state.get_current_instruction() issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], title="Transaction order dependence", bytecode=call.state.environment.code.bytecode, swc_id=TX_ORDER_DEPENDENCE, _type="Warning", ) issue.description = ( "Possible transaction order dependence vulnerability: The value or " "direction of the call statement is determined from a tainted storage location" ) issues.append(issue) return issues
def execute(statespace): issues = [] for call in statespace.calls: findings = [] # explore state findings += _explore_states(call, statespace) # explore nodes findings += _explore_nodes(call, statespace) if len(findings) > 0: node = call.node instruction = call.state.get_current_instruction() issue = Issue( contract=node.contract_name, function=node.function_name, address=instruction["address"], swc_id=MULTIPLE_SENDS, title="Multiple Calls", _type="Informational", ) issue.description = ( "Multiple sends exist in one transaction. Try to isolate each external call into its own transaction," " as external calls can fail accidentally or deliberately.\nConsecutive calls: \n" ) for finding in findings: issue.description += "Call at address: {}\n".format( finding.state.get_current_instruction()["address"]) issues.append(issue) return issues
def get_issue(self, global_state: GlobalState) -> Optional[Issue]: if not self.state_change_states: return None severity = "Medium" if self.user_defined_address else "Low" address = global_state.get_current_instruction()["address"] logging.debug( "[EXTERNAL_CALLS] Detected state changes at addresses: {}".format( address)) description_head = ( "The contract account state is changed after an external call. ") description_tail = ( "Consider that the called contract could re-enter the function before this " "state change takes place. This can lead to business logic vulnerabilities." ) return Issue( contract=global_state.environment.active_account.contract_name, function_name=global_state.environment.active_function_name, address=address, title="State change after external call", severity=severity, description_head=description_head, description_tail=description_tail, swc_id=REENTRANCY, bytecode=global_state.environment.code.bytecode, )
def _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(ULT(expr, op0), ULT(expr, op1)) 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_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 _symbolic_call(call, state, address, statespace): 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." return [issue]
def execute(statespace): logging.debug("Executing module: DEPRECIATED OPCODES") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if (instruction['opcode'] == "ORIGIN"): issue = Issue( node.contract_name, node.function_name, instruction['address'], "Use of tx.origin", "Warning", "Function " + node.function_name + " retrieves the transaction origin (tx.origin) using the ORIGIN opcode. Use tx.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" ) issues.append(issue) return issues
def _concrete_call(call: Call, state: GlobalState, address: int, meminstart: Variable) -> List[Issue]: """ :param call: The current call's information :param state: The current state :param address: The PC address :param meminstart: memory starting position :return: issues """ if not re.search(r"calldata.*\[0", str( state.mstate.memory[meminstart.val])): return [] issue = Issue( contract=call.node.contract_name, function_name=call.node.function_name, address=address, swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, bytecode=state.environment.code.bytecode, title="Delegatecall Proxy", severity="Low", description_head="The contract implements a delegatecall proxy.", description_tail= "The smart contract forwards the received calldata via delegatecall. Note that callers" "can execute arbitrary functions in the callee contract and that the callee contract " "can access the storage of the calling contract. " "Make sure that the callee contract is audited properly.", gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) target = hex(call.to.val) if call.to.type == VarType.CONCRETE else str( call.to) issue.description += "DELEGATECALL target: {}".format(target) return [issue]
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 _concrete_call(call, state, address, meminstart): if not re.search(r"calldata.*_0", str( state.mstate.memory[meminstart.val])): return [] issue = Issue( contract=call.node.contract_name, function_name=call.node.function_name, address=address, swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, bytecode=state.environment.code.bytecode, title="Call data forwarded with delegatecall()", _type="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 ") target = hex(call.to.val) if call.to.type == VarType.CONCRETE else str( call.to) issue.description += "DELEGATECALL target: {}".format(target) return [issue]
def get_issue(self, global_state: GlobalState, transaction_sequence: Dict) -> Issue: """ Returns Issue for the annotation :param global_state: Global State :param transaction_sequence: Transaction sequence :return: Issue """ address = self.call_state.get_current_instruction()["address"] logging.debug( "[DELEGATECALL] Detected delegatecall to a user-supplied address : {}" .format(address)) description_head = "The contract delegates execution to another contract with a user-supplied address." description_tail = ( "The smart contract delegates execution to a user-supplied address. Note that callers " "can execute arbitrary contracts and that the callee contract " "can access the storage of the calling contract. ") return Issue( contract=self.call_state.environment.active_account.contract_name, function_name=self.call_state.environment.active_function_name, address=address, swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, title="Delegatecall Proxy To User-Supplied Address", bytecode=global_state.environment.code.bytecode, severity="Medium", description_head=description_head, description_tail=description_tail, transaction_sequence=transaction_sequence, gas_used=( global_state.mstate.min_gas_used, global_state.mstate.max_gas_used, ), )
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.module_name, call.node.function_name, call.addr, "CALL with gas to dynamic address", "Warning", description) issues.append(issue) return issues
def execute(statespace): logging.debug("Executing module: DEPRECATED OPCODES") issues = [] for k in statespace.nodes: node = statespace.nodes[k] for state in node.states: instruction = state.get_current_instruction() if instruction["opcode"] == "ORIGIN": description = ( "The function `{}` retrieves the transaction origin (tx.origin) using the ORIGIN opcode. " "Use msg.sender instead.\nSee also: " "https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" .format(node.function_name)) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], title="Use of tx.origin", bytecode=state.environment.code.bytecode, _type="Warning", swc_id=TX_ORIGIN_USAGE, description=description, ) issues.append(issue) 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): issues = [] for call in statespace.calls: findings = [] # explore state findings += _explore_states(call, statespace) # explore nodes findings += _explore_nodes(call, statespace) if len(findings) > 0: node = call.node instruction = call.state.get_current_instruction() issue = Issue(node.contract_name, node.function_name, instruction['address'], "Multiple Calls", "Informational") issue.description = \ "Multiple sends exist in one transaction, try to isolate each external call into its own transaction." \ " As external calls can fail accidentally or deliberately.\nConsecutive calls: \n" for finding in findings: issue.description += \ "Call at address: {}\n".format(finding.state.get_current_instruction()['address']) issues.append(issue) return issues
def execute(statespace): """ Executes the analysis module""" logging.debug("Executing module: TOD") issues = [] for call in statespace.calls: # Do analysis interesting_storages = list(_get_influencing_storages(call)) changing_sstores = list( _get_influencing_sstores(statespace, interesting_storages)) # Build issue if necessary if len(changing_sstores) > 0: node = call.node instruction = call.state.get_current_instruction() issue = Issue(node.contract_name, node.function_name, instruction['address'], "Transaction order dependence", "Warning") issue.description = \ "A possible transaction order independence vulnerability exists in function {}. The value or " \ "direction of the call statement is determined from a tainted storage location"\ .format(node.function_name) issues.append(issue) return issues
def execute(statespace): issues = [] for call in statespace.calls: # The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.: # [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}] next_i = helper.get_instruction_index(call.node.instruction_list, call.addr) + 1 instr = call.node.instruction_list[next_i] # The stack contents at a particular point of execution are found in node.states[address].stack if (instr['opcode'] != 'ISZERO' or not re.search( r'retval', str(call.node.states[call.addr + 1].stack[-1]))): issue = Issue("Unchecked CALL return value") issue.description = \ "The function " + call.node.function_name + " in contract '" + call.node.module_name + "' contains a call to " + str(call.to) + ".\n" \ "The return value of this call is not checked. Note that the function will continue to execute even if the called contract throws." issues.append(issue) return issues
def execute(statespace): 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 _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 execute(statespace): logging.debug("Executing module: UNCHECKED_RETVAL") issues = [] visited = [] for call in statespace.calls: state = call.state address = state.get_current_instruction()['address'] # Only needs to be checked once per call instructions (it's essentially just static analysis) if call.state.mstate.pc in visited: continue else: visited.append(call.state.mstate.pc) retval_checked = False # ISZERO retval is expected to be found within the next few instructions. for i in range(0, 10): _state = call.node.states[call.state_index + i] try: instr = _state.get_current_instruction() except IndexError: break if (instr['opcode'] == 'ISZERO' and re.search(r'retval', str(_state.mstate.stack[-1]))): retval_checked = True break if not retval_checked: issue = Issue(call.node.contract_name, call.node.function_name, address, "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 _analyze_state(state) -> List[Issue]: instruction = state.get_current_instruction() address, value = state.mstate.stack[-1], state.mstate.stack[-2] target_slot = 0 target_offset = 0 # In the following array we'll describe all the conditions that need to hold for a take over of ownership vulnerable_conditions = [ # Lets check that we're writing to address 0 (where the owner variable is located address == target_slot, # There is only a vulnerability if before the writing to the owner variable: owner != attacker Extract( 20*8 + target_offset, 0 + target_offset, state.environment.active_account.storage[symbol_factory.BitVecVal(0, 256)] ) != ACTORS.attacker, # There IS a vulnerability if the value being written to owner is the attacker address Extract( 20*8 + target_offset, 0 + target_offset, value, ) == ACTORS.attacker, # Lets only look for cases where the attacker makes themselves the owner by saying that the attacker # is the sender of this transaction state.environment.sender == ACTORS.attacker, ] try: # vulnerable_conditions describes when there is a vulnerability # lets check if the conditions are actually satisfiable by running the following command: # This will raise an UnsatError if the vulnerable_conditions are not satisfiable (i.e. not possible) transaction_sequence = solver.get_transaction_sequence( state, state.world_state.constraints + vulnerable_conditions, ) # Note that get_transaction_sequence also gives us `transaction_sequence` which gives us a concrete # transaction trace that can be used to exploit/demonstrate the vulnerability. # Lets register an issue with Mythril so that the vulnerability is reported to the user! return [Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=instruction["address"], swc_id='000', bytecode=state.environment.code.bytecode, title="Ownership Takeover", severity="High", description_head="An attacker can take over ownership of this contract.", description_tail="", transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), )] except UnsatError: # Sadly (or happily), no vulnerabilities were found here. log.debug("Vulnerable conditions were not satisfiable") return list()
def _analyze_state(state: GlobalState): """ :param state: the current state :return: returns the issues for that corresponding state """ instruction = state.get_current_instruction() annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) if len(annotations) == 0: state.annotate(MultipleSendsAnnotation()) annotations = cast( List[MultipleSendsAnnotation], list(state.get_annotations(MultipleSendsAnnotation)), ) call_offsets = annotations[0].call_offsets if instruction["opcode"] in [ "CALL", "DELEGATECALL", "STATICCALL", "CALLCODE" ]: call_offsets.append(state.get_current_instruction()["address"]) else: # RETURN or STOP for offset in call_offsets[1:]: try: transaction_sequence = get_transaction_sequence( state, state.mstate.constraints) except UnsatError: continue description_tail = ( "This call is executed after a previous call in the same transaction. " "Try to isolate each call, transfer or send into its own transaction." ) issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, address=offset, swc_id=MULTIPLE_SENDS, bytecode=state.environment.code.bytecode, title="Multiple Calls in a Single Transaction", severity="Low", description_head= "Multiple calls are executed in the same transaction.", description_tail=description_tail, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), transaction_sequence=transaction_sequence, ) return [issue] return []
def _analyze_state(state): instruction = state.get_current_instruction() node = state.node if instruction["opcode"] != "CALL": return [] call_value = state.mstate.stack[-3] target = state.mstate.stack[-2] eth_sent_total = BitVecVal(0, 256) constraints = copy(node.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 = 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), ) except UnsatError: logging.debug("[ETHER_THIEF] no model found") return [] return [issue]
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 _handle_transaction_end(self, state: GlobalState) -> None: for annotation in cast( List[OverUnderflowStateAnnotation], state.get_annotations(OverUnderflowStateAnnotation), ): ostate = annotation.overflowing_state address = _get_address_from_state(ostate) if annotation.operator == "subtraction" and self._underflow_cache.get( address, False): continue if annotation.operator != "subtraction" and self._overflow_cache.get( address, False): continue try: # This check can be disabled if the contraints are to difficult for z3 to solve # within any reasonable time. if DISABLE_EFFECT_CHECK: constraints = ostate.mstate.constraints + [ annotation.constraint ] else: constraints = state.mstate.constraints + [ annotation.constraint ] transaction_sequence = solver.get_transaction_sequence( state, constraints) except UnsatError: continue _type = "Underflow" if annotation.operator == "subtraction" else "Overflow" issue = Issue( contract=ostate.environment.active_account.contract_name, function_name=ostate.environment.active_function_name, address=ostate.get_current_instruction()["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=ostate.environment.code.bytecode, title=self._get_title(_type), severity="High", description_head=self._get_description_head(annotation, _type), description_tail=self._get_description_tail(annotation, _type), gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) issue.debug = json.dumps(transaction_sequence, indent=4) if annotation.operator == "subtraction": self._underflow_cache[address] = True else: self._overflow_cache[address] = True self._issues.append(issue)
def _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