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
Example #2
0
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
Example #3
0
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
Example #4
0
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
Example #6
0
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,
        )
Example #8
0
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
Example #9
0
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
Example #10
0
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]
Example #11
0
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
Example #12
0
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]
Example #13
0
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
Example #14
0
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]
Example #15
0
    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
Example #17
0
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
Example #18
0
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
Example #19
0
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
Example #21
0
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
Example #22
0
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
Example #23
0
    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)
Example #24
0
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
Example #25
0
    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()
Example #26
0
    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 []
Example #27
0
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]
Example #28
0
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
Example #29
0
    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)
Example #30
0
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