Пример #1
0
    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
Пример #2
0
def execute(statespace):

    logging.debug("Executing module: ETHER_SEND")

    issues = []

    for call in statespace.calls:

        if ("callvalue" in str(call.value)):
            logging.debug("[ETHER_SEND] Skipping refund function")
            continue

        # We're only interested in calls that send Ether

        if call.value.type == VarType.CONCRETE:
            if call.value.val == 0:
                continue

        interesting = False

        description = "In the function '" + call.node.function_name + "' "

        if re.search(r'caller', str(call.to)):
            description += "a non-zero amount of Ether is sent to msg.sender.\n"
            interesting = True

        elif re.search(r'calldata', str(call.to)):
            description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n"
            interesting = True

        else:
            m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))

            if (m):
                idx = m.group(1)

                func = statespace.find_storage_write(idx)

                if (func):
                    description += "\nThere is a check on storage index " + str(
                        idx
                    ) + ". This storage slot can be written to by calling the function '" + func + "'.\n"
                    interesting = True
                else:
                    logging.debug("[ETHER_SEND] No storage writes to index " +
                                  str(idx))

        if interesting:

            node = call.node

            can_solve = True
            constrained = False

            index = 0

            while (can_solve and index < len(node.constraints)):

                constraint = node.constraints[index]
                index += 1
                logging.debug("[ETHER_SEND] Constraint: " + str(constraint))

                m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))

                if (m):

                    constrained = True
                    idx = m.group(1)

                    func = statespace.find_storage_write(idx)

                    if (func):
                        description += "\nThere is a check on storage index " + str(
                            index
                        ) + ". This storage slot can be written to by calling the function '" + func + "'."
                    else:
                        logging.debug(
                            "[ETHER_SEND] No storage writes to index " +
                            str(index))
                        can_solve = False
                        break

                # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer

                elif (re.search(r"caller", str(constraint))
                      and re.search(r'[0-9]{20}', str(constraint))):
                    constrained = True
                    can_solve = False
                    break

            if not constrained:
                description += "It seems that this function can be called without restrictions."

            if can_solve:

                try:
                    model = solver.get_model(node.constraints)
                    logging.debug("[ETHER_SEND] MODEL: " + str(model))

                    for d in model.decls():
                        logging.debug("[ETHER_SEND] main model: %s = 0x%x" %
                                      (d.name(), model[d].as_long()))

                    issue = Issue(call.node.module_name,
                                  call.node.function_name, call.addr,
                                  "Ether send", "Warning", description)
                    issues.append(issue)

                except UnsatError:
                    logging.debug("[ETHER_SEND] no model found")

    return issues
Пример #3
0
def _analyze_state(state: GlobalState) -> list:
    instruction = state.get_current_instruction()

    annotations = cast(
        List[UncheckedRetvalAnnotation],
        [a for a in state.get_annotations(UncheckedRetvalAnnotation)],
    )
    if len(annotations) == 0:
        state.annotate(UncheckedRetvalAnnotation())
        annotations = cast(
            List[UncheckedRetvalAnnotation],
            [a for a in state.get_annotations(UncheckedRetvalAnnotation)],
        )

    retvals = annotations[0].retvals

    if instruction["opcode"] in ("STOP", "RETURN"):
        issues = []
        for retval in retvals:
            try:
                solver.get_model(state.mstate.constraints +
                                 [retval["retval"] == 0])
            except UnsatError:
                continue

            description_tail = (
                "External calls return a boolean value. If the callee contract halts with an exception, 'false' is "
                "returned and execution continues in the caller. It is usually recommended to wrap external calls "
                "into a require statement to prevent unexpected states.")

            issue = Issue(
                contract=state.environment.active_account.contract_name,
                function_name=state.environment.active_function_name,
                address=retval["address"],
                bytecode=state.environment.code.bytecode,
                title="Unchecked Call Return Value",
                swc_id=UNCHECKED_RET_VAL,
                severity="Low",
                description_head=
                "The return value of a message call is not checked.",
                description_tail=description_tail,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
            )

            issues.append(issue)

        return issues
    else:
        log.debug("End of call, extracting retval")
        assert state.environment.code.instruction_list[state.mstate.pc -
                                                       1]["opcode"] in [
                                                           "CALL",
                                                           "DELEGATECALL",
                                                           "STATICCALL",
                                                           "CALLCODE"
                                                       ]
        retval = state.mstate.stack[-1]
        # Use Typed Dict after release of mypy 0.670 and remove type ignore
        retvals.append(
            {  # type: ignore
                "address": state.instruction["address"] - 1,
                "retval": retval,
            }
        )

    return []
Пример #4
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
    # FIXME: handle exponentiation
    if instruction["opcode"] == "ADD":
        operator = "add"
        expr = op0 + op1
        # constraint = Not(BVAddNoOverflow(op0, op1, signed=False))
    else:
        operator = "multiply"
        expr = op1 * op0
        # constraint = Not(BVMulNoOverflow(op0, op1, signed=False))

    constraint = Or(And(ULT(expr, op0), op1 != 0), And(ULT(expr, op1),
                                                       op0 != 0))
    # Check satisfiable
    model = _try_constraints(node.constraints, [constraint])

    if model is None:
        logging.debug("[INTEGER_OVERFLOW] no model found")
        return issues

    if not _verify_integer_overflow(statespace, node, expr, state, model,
                                    constraint, op0, op1):
        return issues

    # Build issue
    issue = Issue(
        contract=node.contract_name,
        function_name=node.function_name,
        address=instruction["address"],
        swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
        bytecode=state.environment.code.bytecode,
        title="Integer Overflow",
        _type="Warning",
    )

    issue.description = "This binary {} operation can result in integer overflow.\n".format(
        operator)
    try:
        issue.debug = "Transaction Sequence: " + str(
            solver.get_transaction_sequence(state, node.constraints))
    except UnsatError:
        return issues

    issues.append(issue)

    return issues
Пример #5
0
def execute(statespace):

    logging.debug("Executing module: UNCHECKED_RETVAL")

    issues = []
    visited = []

    for call in statespace.calls:

        # Only needs to be checked once per call instructions (it's essentially just static analysis)

        if call.addr in visited:
            continue
        else:
            visited.append(call.addr)

        # The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.:
        # [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}]

        start_index = helper.get_instruction_index(call.node.instruction_list,
                                                   call.addr) + 1

        retval_checked = False

        # ISZERO retval should be found within the next few instructions.

        for i in range(0, 10):

            try:
                instr = call.node.instruction_list[start_index + i]
            except IndexError:
                break

            if (instr['opcode'] == 'ISZERO' and re.search(
                    r'retval', str(
                        call.node.states[instr['address']].stack[-1]))):
                retval_checked = True
                break

        if not retval_checked:

            issue = Issue(call.node.module_name, call.node.function_name,
                          call.addr, "Unchecked CALL return value")

            if (call.to.type == VarType.CONCRETE):
                receiver = hex(call.to.val)
            elif (re.search(r"caller", str(call.to))):
                receiver = "msg.sender"
            elif (re.search(r"storage", str(call.to))):
                receiver = "an address obtained from storage"
            else:
                receiver = str(call.to)


            issue.description = \
                "The function " + call.node.function_name + " contains a call to " + receiver + ".\n" \
                "The return value of this call is not checked. Note that the function will continue to execute with a return value of '0' if the called contract throws."

            issues.append(issue)

    return issues
Пример #6
0
def execute(statespace):

    logging.debug("Executing module: ETHER_SEND")

    issues = []

    for call in statespace.calls:

        state = call.state
        address = state.get_current_instruction()['address']

        if "callvalue" in str(call.value):
            logging.debug("[ETHER_SEND] Skipping refund function")
            continue

        # We're only interested in calls that send Ether

        if call.value.type == VarType.CONCRETE and call.value.val == 0:
            continue

        interesting = False

        description = "In the function `" + call.node.function_name + "` "

        if re.search(r'caller', str(call.to)):
            description += "a non-zero amount of Ether is sent to msg.sender.\n"
            interesting = True

        elif re.search(r'calldata', str(call.to)):
            description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n"
            interesting = True

        else:
            m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))

            if m:
                idx = m.group(1)

                description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(
                    idx) + ".\n"

                func = statespace.find_storage_write(
                    state.environment.active_account.address, idx)

                if func:
                    description += "There is a check on storage index " + str(
                        idx
                    ) + ". This storage slot can be written to by calling the function `" + func + "`.\n"
                    interesting = True
                else:
                    logging.debug("[ETHER_SEND] No storage writes to index " +
                                  str(idx))

        if interesting:

            node = call.node

            can_solve = True
            constrained = False

            index = 0

            while can_solve and index < len(node.constraints):

                constraint = node.constraints[index]
                index += 1
                logging.debug("[ETHER_SEND] Constraint: " + str(constraint))

                m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))

                if m:

                    constrained = True
                    idx = m.group(1)

                    func = statespace.find_storage_write(
                        state.environment.active_account.address, idx)

                    if func:
                        description += "\nThere is a check on storage index " + str(
                            idx
                        ) + ". This storage slot can be written to by calling the function `" + func + "`."
                    else:
                        logging.debug(
                            "[ETHER_SEND] No storage writes to index " +
                            str(idx))
                        can_solve = False
                        break

                # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer

                elif re.search(r"caller", str(constraint)) and re.search(
                        r'[0-9]{20}', str(constraint)):
                    constrained = True
                    can_solve = False
                    break

            if not constrained:
                description += "It seems that this function can be called without restrictions."

            if can_solve:

                try:
                    model = solver.get_model(node.constraints)

                    for d in model.decls():
                        logging.debug("[ETHER_SEND] main model: %s = 0x%x" %
                                      (d.name(), model[d].as_long()))

                    debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(
                        model)

                    issue = Issue(contract=call.node.contract_name,
                                  function=call.node.function_name,
                                  address=address,
                                  title="Ether send",
                                  _type="Warning",
                                  swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
                                  description=description,
                                  debug=debug)
                    issues.append(issue)

                except UnsatError:
                    logging.debug("[ETHER_SEND] no model found")

    return issues
Пример #7
0
    def _analyze_state(state):
        """

        :param state:
        :return:
        """
        gas = state.mstate.stack[-1]
        to = state.mstate.stack[-2]

        address = state.get_current_instruction()["address"]

        try:
            constraints = copy(state.mstate.constraints)

            transaction_sequence = solver.get_transaction_sequence(
                state,
                constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))])

            # Check whether we can also set the callee address

            try:
                constraints += [to == ATTACKER_ADDRESS]

                for tx in state.world_state.transaction_sequence:
                    if not isinstance(tx, ContractCreationTransaction):
                        constraints.append(tx.caller == ATTACKER_ADDRESS)

                transaction_sequence = solver.get_transaction_sequence(
                    state, constraints)

                description_head = "A call to a user-supplied address is executed."
                description_tail = (
                    "The callee address of an external message call can be set by "
                    "the caller. Note that the callee can contain arbitrary code and may re-enter any function "
                    "in this contract. Review the business logic carefully to prevent averse effects on the "
                    "contract state.")

                issue = Issue(
                    contract=state.environment.active_account.contract_name,
                    function_name=state.environment.active_function_name,
                    address=address,
                    swc_id=REENTRANCY,
                    title="External Call To User-Supplied Address",
                    bytecode=state.environment.code.bytecode,
                    severity="Medium",
                    description_head=description_head,
                    description_tail=description_tail,
                    transaction_sequence=transaction_sequence,
                    gas_used=(state.mstate.min_gas_used,
                              state.mstate.max_gas_used),
                )

            except UnsatError:
                if _is_precompile_call(state):
                    return []

                log.debug(
                    "[EXTERNAL_CALLS] Callee address cannot be modified. Reporting informational issue."
                )

                description_head = "The contract executes an external message call."
                description_tail = (
                    "An external function call to a fixed contract address is executed. Make sure "
                    "that the callee contract has been reviewed carefully.")

                issue = Issue(
                    contract=state.environment.active_account.contract_name,
                    function_name=state.environment.active_function_name,
                    address=address,
                    swc_id=REENTRANCY,
                    title="External Call To Fixed Address",
                    bytecode=state.environment.code.bytecode,
                    severity="Low",
                    description_head=description_head,
                    description_tail=description_tail,
                    transaction_sequence=transaction_sequence,
                    gas_used=(state.mstate.min_gas_used,
                              state.mstate.max_gas_used),
                )

        except UnsatError:
            log.debug("[EXTERNAL_CALLS] No model found.")
            return []

        return [issue]
Пример #8
0
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
Пример #9
0
    def _analyze_state(self, state: GlobalState) -> List[Issue]:
        """
        :param state: the current state
        :return: returns the issues for that corresponding state
        """

        opcode = state.get_current_instruction()["opcode"]
        address = state.get_current_instruction()["address"]

        annotations = cast(List[VisitsAnnotation],
                           list(state.get_annotations(VisitsAnnotation)))

        if len(annotations) == 0:
            annotation = VisitsAnnotation()
            state.annotate(annotation)
        else:
            annotation = annotations[0]

        if opcode in ["JUMP", "JUMPI"]:

            if annotation.loop_start is not None:
                return []
            try:
                target = util.get_concrete_int(state.mstate.stack[-1])
            except TypeError:
                log.debug("Symbolic target encountered in dos module")
                return []
            if target in annotation.jump_targets:
                annotation.jump_targets[target] += 1
            else:
                annotation.jump_targets[target] = 1

            if annotation.jump_targets[target] > 2:
                annotation.loop_start = address

        elif annotation.loop_start is not None:

            if opcode == "CALL":
                operation = "A message call"
            else:
                operation = "A storage modification"

            description_head = (
                "Potential denial-of-service if block gas limit is reached.")
            description_tail = "{} is executed in a loop. Be aware that the transaction may fail to execute if the loop is unbounded and the necessary gas exceeds the block gas limit.".format(
                operation)

            try:
                transaction_sequence = get_transaction_sequence(
                    state, state.mstate.constraints)
            except UnsatError:
                return []

            issue = Issue(
                contract=state.environment.active_account.contract_name,
                function_name=state.environment.active_function_name,
                address=annotation.loop_start,
                swc_id=DOS_WITH_BLOCK_GAS_LIMIT,
                bytecode=state.environment.code.bytecode,
                title=
                "Potential denial-of-service if block gas limit is reached",
                severity="Low",
                description_head=description_head,
                description_tail=description_tail,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
                transaction_sequence=transaction_sequence,
            )

            return [issue]

        return []
Пример #10
0
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
Пример #11
0
    def _analyze_states(self, state: GlobalState) -> List[Issue]:
        """
        :param state: the current state
        :return: returns the issues for that corresponding state
        """

        opcode = state.get_current_instruction()["opcode"]
        address = state.get_current_instruction()["address"]

        if opcode == "JUMPI":

            target = util.get_concrete_int(state.mstate.stack[-1])

            transaction = state.current_transaction
            if state.current_transaction in self._jumpdest_count:

                try:
                    self._jumpdest_count[transaction][target] += 1
                    if self._jumpdest_count[transaction][target] == 3:

                        annotation = (
                            LoopAnnotation(address, target)
                            if target > address
                            else LoopAnnotation(target, address)
                        )

                        state.annotate(annotation)
                except KeyError:
                    self._jumpdest_count[transaction][target] = 0

            else:
                self._jumpdest_count[transaction] = {}
                self._jumpdest_count[transaction][target] = 0

        else:

            annotations = cast(
                List[LoopAnnotation], list(state.get_annotations(LoopAnnotation))
            )

            for annotation in annotations:

                if annotation.contains(address):

                    operation = (
                        "A storage modification"
                        if opcode == "SSTORE"
                        else "An external call"
                    )

                    description_head = (
                        "Potential denial-of-service if block gas limit is reached."
                    )
                    description_tail = "{} is executed in a loop.".format(operation)

                    issue = Issue(
                        contract=state.environment.active_account.contract_name,
                        function_name=state.environment.active_function_name,
                        address=annotation.loop_start,
                        swc_id=DOS_WITH_BLOCK_GAS_LIMIT,
                        bytecode=state.environment.code.bytecode,
                        title="Potential denial-of-service if block gas limit is reached",
                        severity="Low",
                        description_head=description_head,
                        description_tail=description_tail,
                        gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
                    )
                    return [issue]

        return []
Пример #12
0
def execute(statespace):

    logging.debug("Executing module: UNCHECKED_SUICIDE")

    issues = []

    for k in statespace.nodes:
        node = statespace.nodes[k]

        for state in node.states:

            instruction = state.get_current_instruction()

            if(instruction['opcode'] == "SUICIDE"):

                logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name)

                description = "The function " + node.function_name + " executes the SUICIDE instruction."

                stack = copy.deepcopy(state.mstate.stack)
                to = stack.pop()

                if ("caller" in str(to)):
                    description += "\nThe remaining Ether is sent to the caller's address.\n"
                elif ("storage" in str(to)):
                    description += "\nThe remaining Ether is sent to a stored address\n"
                elif ("calldata" in str(to)):
                    description += "\nThe remaining Ether is sent to an address provided as a function argument."
                elif (type(to) == BitVecNumRef):
                    description += "\nThe remaining Ether is sent to: " + hex(to.as_long())
                else:
                    description += "\nThe remaining Ether is sent to: " + str(to) + "\n"

                constrained = False
                can_solve = True

                index = 0

                while(can_solve and index < len(node.constraints)):

                    constraint = node.constraints[index]
                    index += 1

                    m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))

                    overwrite = False

                    if (m):
                        constrained = True
                        idx = m.group(1)

                        func = statespace.find_storage_write(idx)

                        if func:
                            description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + func + "'."
                            break
                        else:
                            logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index))
                            can_solve = False
                            break


                    # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer

                    elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))):
                       can_solve = False
                       break


                if not constrained:
                    description += "\nIt seems that this function can be called without restrictions."

                if can_solve:

                    try:
                        model = solver.get_model(node.constraints)
                        logging.debug("[UNCHECKED_SUICIDE] MODEL: " + str(model))


                        for d in model.decls():
                            logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long()))

                        issue = Issue(node.contract_name, node.function_name, instruction['address'], "Unchecked SUICIDE", "Warning", description)
                        issues.append(issue)

                    except UnsatError:
                            logging.debug("[UNCHECKED_SUICIDE] no model found")         

    return issues
Пример #13
0
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
Пример #14
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(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
Пример #15
0
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
Пример #16
0
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
Пример #17
0
def execute(statespace):

    issues = []

    for call in statespace.calls:

        state = call.state
        address = state.get_current_instruction()['address']

        if (call.type == "CALL"):

            logging.info("[EXTERNAL_CALLS] Call to: %s, value = %s, gas = %s" % (str(call.to), str(call.value), str(call.gas)))

            if (call.to.type == VarType.SYMBOLIC and (call.gas.type == VarType.CONCRETE and call.gas.val > 2300) or (call.gas.type == VarType.SYMBOLIC and "2300" not in str(call.gas))):

                description = "This contract executes a message call to "

                target = str(call.to)
                user_supplied = False

                if ("calldata" in target or "caller" in target):

                    if ("calldata" in target):
                        description += "an address provided as a function argument. "
                    else:
                        description += "the address of the transaction sender. "

                    user_supplied = True
                else:
                    m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))

                    if (m):
                        idx = m.group(1)

                        func = statespace.find_storage_write(state.environment.active_account.address, idx)

                        if func:

                            description += \
                                "an address found at storage slot " + str(idx) + ". " + \
                                "This storage slot can be written to by calling the function '" + func + "'. "
                            user_supplied = True

                if user_supplied:

                    description += "Generally, it is not recommended to call user-supplied adresses using Solidity's call() construct. Note that attackers might leverage reentrancy attacks to exploit race conditions or manipulate this contract's state."

                    issue = Issue(call.node.contract_name, call.node.function_name, address, "Message call to external contract", "Warning", description)

                else:

                    description += "to another contract. Make sure that the called contract is trusted and does not execute user-supplied code."

                    issue = Issue(call.node.contract_name, call.node.function_name, address, "Message call to external contract", "Informational", description)

                issues.append(issue)

                if address not in calls_visited:
                    calls_visited.append(address)

                    logging.debug("[EXTERNAL_CALLS] Checking for state changes starting from " + call.node.function_name)

                    # Check for SSTORE in remaining instructions in current node & nodes down the CFG

                    state_change_addresses = search_children(statespace, call.node, call.state_index + 1, depth=0, results=[])

                    logging.debug("[EXTERNAL_CALLS] Detected state changes at addresses: " + str(state_change_addresses))

                    if (len(state_change_addresses)):
                        for address in state_change_addresses:
                            description = "The contract account state is changed after an external call. Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities."
                            issue = Issue(call.node.contract_name, call.node.function_name, address, "State change after external call", "Warning", description)
                            issues.append(issue)

    return issues
Пример #18
0
def execute(statespace):

    logging.debug("Executing module: UNCHECKED_RETVAL")

    issues = []

    for k in statespace.nodes:

        node = statespace.nodes[k]

        if NodeFlags.CALL_RETURN in node.flags:

            retval_checked = False

            for state in node.states:

                instr = state.get_current_instruction()

                if (instr['opcode'] == 'ISZERO'
                        and re.search(r'retval', str(state.mstate.stack[-1]))):
                    retval_checked = True
                    break

            if not retval_checked:

                address = state.get_current_instruction()['address']
                issue = Issue(node.contract_name, node.function_name, address,
                              "Unchecked CALL return value")

                issue.description = \
                    "The return value of an external call is not checked. Note that the contract will continue if the call fails."

                issues.append(issue)

        else:

            nStates = len(node.states)

            for idx in range(0, nStates):

                state = node.states[idx]
                instr = state.get_current_instruction()

                if (instr['opcode'] == 'CALL'):

                    retval_checked = False

                    for _idx in range(idx, idx + 10):

                        try:
                            _state = node.states[_idx]
                            _instr = _state.get_current_instruction()

                            if (_instr['opcode'] == 'ISZERO' and re.search(
                                    r'retval', str(_state.mstate.stack[-1]))):
                                retval_checked = True
                                break

                        except IndexError:
                            break

                    if not retval_checked:

                        address = instr['address']
                        issue = Issue(node.contract_name, node.function_name,
                                      address, "Unchecked CALL return value")

                        issue.description = \
                            "The return value of an external call is not checked. Note that execution continue even if the called contract throws."

                        issues.append(issue)

    return issues
Пример #19
0
def execute(statespace):

    issues = []

    for k in statespace.nodes:
        node = statespace.nodes[k]

        for instruction in node.instruction_list:

            if(instruction['opcode'] == "SUICIDE"):

                logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name)

                issue = Issue("Unchecked SUICIDE", "Warning")
                issue.description = "The function " + node.function_name + " executes the SUICIDE instruction."

                state = node.states[instruction['address']]
                to = state.stack.pop()

                if ("caller" in str(to)):
                    issue.description += "\nThe remaining Ether is sent to the caller's address.\n"
                elif ("storage" in str(to)):
                    issue.description += "\nThe remaining Ether is sent to a stored address\n"
                elif ("calldata" in str(to)):
                    issue.description += "\nThe remaining Ether is sent to an address provided as a function argument."
                elif (type(to) == BitVecNumRef):
                    issue.description += "\nThe remaining Ether is sent to: " + hex(to.as_long())
                else:
                    issue.description += "\nThe remaining Ether is sent to: " + str(to) + "\n"

                constrained = False
                can_solve = True

                while (can_solve and len(node.constraints)):

                    constraint = node.constraints.pop()

                    m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))

                    overwrite = False

                    if (m):
                        constrained = True
                        index = m.group(1)

                        try:

                            for s in statespace.sstors[index]:

                                if s.tainted:
                                    issue.description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'."
                                    break

                            if not overwrite:
                                logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index))
                                can_solve = False
                                break

                        except KeyError:
                            logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index))
                            can_solve = False
                            break


                    # CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer

                    elif (re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint))):
                       can_solve = False
                       break


                if not constrained:
                    issue.description += "\nIt seems that this function can be called without restrictions."

                if can_solve:

                    try:
                        model = solver.get_model(node.constraints)
                        issues.append(issue)

                        for d in model.decls():
                            logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
                    except UnsatError:
                            logging.debug("[UNCHECKED_SUICIDE] no model found")         

    return issues
Пример #20
0
    def _analyze_state(state):
        """

        :param state:
        :return:
        """
        instruction = state.get_current_instruction()

        if instruction["opcode"] != "CALL":
            return []

        address = instruction["address"]

        value = state.mstate.stack[-3]
        target = state.mstate.stack[-2]

        eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256)

        constraints = copy(state.mstate.constraints)

        for tx in state.world_state.transaction_sequence:
            """
            Constraint: The call value must be greater than the sum of Ether sent by the attacker over all
            transactions. This prevents false positives caused by legitimate refund functions.
            Also constrain the addition from overflowing (otherwise the solver produces solutions with 
            ridiculously high call values).
            """
            constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, False)]
            eth_sent_by_attacker = Sum(
                eth_sent_by_attacker,
                tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0),
            )

            """
            Constraint: All transactions must originate from regular users (not the creator/owner).
            This prevents false positives where the owner willingly transfers ownership to another address.
            """

            if not isinstance(tx, ContractCreationTransaction):
                constraints += [tx.caller != CREATOR_ADDRESS]

        """
        Require that the current transaction is sent by the attacker and
        that the Ether is sent to the attacker's address.
        """

        constraints += [
            UGT(value, eth_sent_by_attacker),
            target == ATTACKER_ADDRESS,
            state.current_transaction.caller == ATTACKER_ADDRESS,
        ]

        try:

            transaction_sequence = solver.get_transaction_sequence(state, constraints)

            issue = Issue(
                contract=state.environment.active_account.contract_name,
                function_name=state.environment.active_function_name,
                address=instruction["address"],
                swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
                title="Unprotected Ether Withdrawal",
                severity="High",
                bytecode=state.environment.code.bytecode,
                description_head="Anyone can withdraw ETH from the contract account.",
                description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract"
                + " account without previously having sent an equivalent amount of ETH to it. This is likely to be"
                + " a vulnerability.",
                transaction_sequence=transaction_sequence,
                gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
            )
        except UnsatError:
            log.debug("[ETHER_THIEF] no model found")
            return []

        return [issue]
Пример #21
0
def _check_integer_underflow(statespace, state, node):
    """
    Checks for integer underflow
    :param state: state from node to examine
    :param node: node to examine
    :return: found issue
    """
    issues = []
    instruction = state.get_current_instruction()
    if instruction["opcode"] == "SUB":

        stack = state.mstate.stack

        op0 = stack[-1]
        op1 = stack[-2]

        constraints = copy.deepcopy(node.constraints)

        # Filter for patterns that indicate benign underflows

        # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large.
        # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0
        if type(op0) == int and type(op1) == int:
            return []
        if re.search(r"calldatasize_", str(op0)):
            return []
        if re.search(r"256\*.*If\(1", str(op0), re.DOTALL) or re.search(
                r"256\*.*If\(1", str(op1), re.DOTALL):
            return []
        if re.search(r"32 \+.*calldata", str(op0), re.DOTALL) or re.search(
                r"32 \+.*calldata", str(op1), re.DOTALL):
            return []

        logging.debug(
            "[INTEGER_UNDERFLOW] Checking SUB {0}, {1} at address {2}".format(
                str(op0), str(op1), str(instruction["address"])))
        allowed_types = [int, BitVecRef, BitVecNumRef]

        if type(op0) in allowed_types and type(op1) in allowed_types:
            constraints.append(UGT(op1, op0))

            try:

                model = solver.get_model(constraints)

                # If we get to this point then there has been an integer overflow
                # Find out if the overflowed value is actually used
                interesting_usages = _search_children(
                    statespace,
                    node, (op0 - op1),
                    index=node.states.index(state))

                # Stop if it isn't
                if len(interesting_usages) == 0:
                    return issues

                issue = Issue(
                    contract=node.contract_name,
                    function_name=node.function_name,
                    address=instruction["address"],
                    swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
                    bytecode=state.environment.code.bytecode,
                    title="Integer Underflow",
                    _type="Warning",
                )

                issue.description = (
                    "The subtraction can result in an integer underflow.\n")

                issue.debug = "Transaction Sequence: " + str(
                    solver.get_transaction_sequence(state, node.constraints))
                issues.append(issue)

            except UnsatError:
                logging.debug("[INTEGER_UNDERFLOW] no model found")
    return issues
Пример #22
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 []

        eth_sent_total = BitVecVal(0, 256)

        for tx in state.world_state.transaction_sequence[1:]:
            eth_sent_total += tx.call_value

        try:

            model = solver.get_model(
                node.constraints + not_creator_constraints + [
                    UGT(call_value, eth_sent_total),
                    state.environment.sender == ARBITRARY_SENDER_ADDRESS,
                    target == state.environment.sender,
                ])

            transaction_sequence = solver.get_transaction_sequence(
                state,
                node.constraints + not_creator_constraints + [
                    call_value > eth_sent_total,
                    state.environment.sender == ARBITRARY_SENDER_ADDRESS,
                    target == state.environment.sender,
                ],
            )

            debug = "Transaction Sequence: " + str(transaction_sequence)

            issue = Issue(
                contract=node.contract_name,
                function_name=node.function_name,
                address=instruction["address"],
                swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
                title="Ether thief",
                _type="Warning",
                bytecode=state.environment.code.bytecode,
                description=
                "Arbitrary senders other than the contract creator can withdraw ETH from the contract"
                +
                " account without previously having sent an equivalent amount of ETH to it. This is likely to be"
                + " a vulnerability.",
                debug=debug,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
            )
            issues.append(issue)
        except UnsatError:
            logging.debug("[ETHER_THIEF] no model found")

        return issues
Пример #23
0
def execute(statespace):

    logging.debug("Executing module: CALL_TO_DYNAMIC_WITH_GAS")

    issues = []

    for call in statespace.calls:

        if (call.type == "CALL"):

            logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] Call to: " +
                          str(call.to) + ", value " + str(call.value) +
                          ", gas = " + str(call.gas))

            if (call.to.type == VarType.SYMBOLIC and
                (call.gas.type == VarType.CONCRETE and call.gas.val > 2300)
                    or (call.gas.type == VarType.SYMBOLIC
                        and "2300" not in str(call.gas))):

                description = "The function " + call.node.function_name + " contains a function call to "

                target = str(call.to)
                is_valid = False

                if ("calldata" in target or "caller" in target):

                    if ("calldata" in target):
                        description += "an address provided as a function argument. "
                    else:
                        description += "the address of the transaction sender. "

                    is_valid = True
                else:
                    m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))

                    if (m):
                        index = m.group(1)

                        try:

                            for s in statespace.sstors[index]:

                                if s.tainted:

                                    description += \
                                        "an address found at storage position " + str(index) + ".\n" + \
                                        "This storage position can be written to by calling the function '" + s.node.function_name + "'.\n" \
                                        "Verify that the contract address cannot be set by untrusted users.\n"

                                    is_valid = True
                                    break

                        except KeyError:
                            logging.debug(
                                "[CALL_TO_DYNAMIC_WITH_GAS] No storage writes to index "
                                + str(index))
                            continue

                if is_valid:

                    description += "The available gas is forwarded to the called contract. Make sure that the logic of the calling contract is not adversely affected if the called contract misbehaves (e.g. reentrancy)."

                    issue = Issue(call.node.contract_name,
                                  call.node.function_name, call.addr,
                                  "CALL with gas to dynamic address",
                                  "Warning", description)

                    issues.append(issue)

    return issues
    def _analyze_state(state: GlobalState) -> list:
        """

        :param state:
        :return:
        """

        issues = []

        if is_prehook():

            opcode = state.get_current_instruction()["opcode"]

            if opcode in final_ops:

                for annotation in state.annotations:

                    if isinstance(annotation, PredictablePathAnnotation):
                        if annotation.add_constraints:
                            constraints = (state.mstate.constraints +
                                           annotation.add_constraints)
                        else:
                            constraints = copy(state.mstate.constraints)
                        try:
                            transaction_sequence = solver.get_transaction_sequence(
                                state, constraints)
                        except UnsatError:
                            continue
                        description = (
                            "The " + annotation.operation +
                            " is used in to determine a control flow decision. "
                        )
                        description += (
                            "Note that the values of variables like coinbase, gaslimit, block number and timestamp "
                            "are predictable and can be manipulated by a malicious miner. Also keep in mind that attackers "
                            "know hashes of earlier blocks. Don't use any of those environment variables for random number "
                            "generation or to make critical control flow decisions."
                        )
                        """
                        Usually report low severity except in cases where the hash of a previous block is used to
                        determine control flow. 
                        """

                        severity = "Medium" if "hash" in annotation.operation else "Low"
                        """
                        Note: We report the location of the JUMPI that lead to this path. Usually this maps to an if or
                        require statement.
                        """

                        swc_id = (TIMESTAMP_DEPENDENCE if "timestamp"
                                  in annotation.operation else WEAK_RANDOMNESS)

                        issue = Issue(
                            contract=state.environment.active_account.
                            contract_name,
                            function_name=state.environment.
                            active_function_name,
                            address=annotation.location,
                            swc_id=swc_id,
                            bytecode=state.environment.code.bytecode,
                            title=
                            "Dependence on predictable environment variable",
                            severity=severity,
                            description_head=
                            "A control flow decision is made based on a predictable variable.",
                            description_tail=description,
                            gas_used=(
                                state.mstate.min_gas_used,
                                state.mstate.max_gas_used,
                            ),
                            transaction_sequence=transaction_sequence,
                        )

                        issues.append(issue)

            elif opcode == "JUMPI":

                # Look for predictable state variables in jump condition

                for annotation in state.mstate.stack[-2].annotations:

                    if isinstance(annotation, PredictableValueAnnotation):
                        state.annotate(
                            PredictablePathAnnotation(
                                annotation.operation,
                                state.get_current_instruction()["address"],
                                add_constraints=annotation.add_constraints,
                            ))
                        break

            elif opcode == "BLOCKHASH":

                param = state.mstate.stack[-1]

                try:
                    constraint = [
                        ULT(param, state.environment.block_number),
                        ULT(
                            state.environment.block_number,
                            symbol_factory.BitVecVal(2**255, 256),
                        ),
                    ]

                    # Why the second constraint? Because without it Z3 returns a solution where param overflows.

                    solver.get_model(state.mstate.constraints + constraint)
                    state.annotate(OldBlockNumberUsedAnnotation(constraint))

                except UnsatError:
                    pass

        else:
            # we're in post hook

            opcode = state.environment.code.instruction_list[state.mstate.pc -
                                                             1]["opcode"]

            if opcode == "BLOCKHASH":
                # if we're in the post hook of a BLOCKHASH op, check if an old block number was used to create it.

                annotations = cast(
                    List[OldBlockNumberUsedAnnotation],
                    list(state.get_annotations(OldBlockNumberUsedAnnotation)),
                )

                if len(annotations):
                    # We can append any block constraint here
                    state.mstate.stack[-1].annotate(
                        PredictableValueAnnotation(
                            "block hash of a previous block",
                            add_constraints=annotations[0].block_constraints,
                        ))
            else:
                # Always create an annotation when COINBASE, GASLIMIT, TIMESTAMP or NUMBER is executed.

                state.mstate.stack[-1].annotate(
                    PredictableValueAnnotation(
                        "block.{} environment variable".format(
                            opcode.lower())))

        return issues
def execute(statespace):

    logging.debug("Executing module: DEPENDENCE_ON_PREDICTABLE_VARS")

    issues = []

    for call in statespace.calls:

        if "callvalue" in str(call.value):
            logging.debug(
                "[DEPENDENCE_ON_PREDICTABLE_VARS] Skipping refund function")
            continue

        # We're only interested in calls that send Ether

        if call.value.type == VarType.CONCRETE and call.value.val == 0:
            continue

        address = call.state.get_current_instruction()['address']

        description = "In the function `" + call.node.function_name + "` "
        description += "the following predictable state variables are used to determine Ether recipient:\n"

        # First check: look for predictable state variables in node & call recipient constraints

        vars = ["coinbase", "gaslimit", "timestamp", "number"]

        found = []
        for var in vars:
            for constraint in call.node.constraints + [call.to]:
                if var in str(constraint):
                    found.append(var)

        if len(found):
            for item in found:
                description += "- block.{}\n".format(item)
            if solve(call):
                swc_type = TIMESTAMP_DEPENDENCE if item == 'timestamp' else PREDICTABLE_VARS_DEPENDENCE
                issue = Issue(
                    contract=call.node.contract_name,
                    function=call.node.function_name,
                    address=address,
                    swc_id=swc_type,
                    title="Dependence on predictable environment variable",
                    _type="Warning",
                    description=description)
                issues.append(issue)

        # Second check: blockhash

        for constraint in call.node.constraints + [call.to]:
            if "blockhash" in str(constraint):
                description = "In the function `" + call.node.function_name + "` "
                if "number" in str(constraint):
                    m = re.search(r'blockhash\w+(\s-\s(\d+))*',
                                  str(constraint))
                    if m and solve(call):

                        found = m.group(1)

                        if found:  # block.blockhash(block.number - N)
                            description += "predictable expression 'block.blockhash(block.number - " + m.group(2) + \
                                ")' is used to determine Ether recipient"
                            if int(m.group(2)) > 255:
                                description += ", this expression will always be equal to zero."
                        elif "storage" in str(
                                constraint
                        ):  # block.blockhash(block.number - storage_0)
                            description += "predictable expression 'block.blockhash(block.number - " + \
                                           "some_storage_var)' is used to determine Ether recipient"
                        else:  # block.blockhash(block.number)
                            description += "predictable expression 'block.blockhash(block.number)'" + \
                                           " is used to determine Ether recipient"
                            description += ", this expression will always be equal to zero."

                        issue = Issue(
                            contract=call.node.contract_name,
                            function=call.node.function_name,
                            address=address,
                            title="Dependence on predictable variable",
                            _type="Warning",
                            description=description,
                            swc_id=PREDICTABLE_VARS_DEPENDENCE)
                        issues.append(issue)
                        break
                else:
                    r = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))
                    if r:  # block.blockhash(storage_0)
                        '''
                        We actually can do better here by adding a constraint blockhash_block_storage_0 == 0
                        and checking model satisfiability. When this is done, severity can be raised
                        from 'Informational' to 'Warning'.
                        Checking that storage at given index can be tainted is not necessary, since it usually contains
                        block.number of the 'commit' transaction in commit-reveal workflow.
                        '''

                        index = r.group(1)
                        if index and solve(call):
                            description += 'block.blockhash() is calculated using a value from storage ' \
                                           'at index {}'.format(index)
                            issue = Issue(
                                contract=call.node.contract_name,
                                function=call.node.function_name,
                                address=address,
                                title="Dependence on predictable variable",
                                _type="Informational",
                                description=description,
                                swc_id=PREDICTABLE_VARS_DEPENDENCE)
                            issues.append(issue)
                            break
    return issues
Пример #26
0
def _analyze_state(state):

    node = state.node
    gas = state.mstate.stack[-1]
    to = state.mstate.stack[-2]

    address = state.get_current_instruction()["address"]

    try:
        constraints = node.constraints
        transaction_sequence = solver.get_transaction_sequence(
            state, constraints + [UGT(gas, 2300)])

        # Check whether we can also set the callee address

        try:
            constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF]
            transaction_sequence = solver.get_transaction_sequence(
                state, constraints)

            debug = str(transaction_sequence)
            description = (
                "The contract executes a function call with high gas to a user-supplied address. "
                "Note that the callee can contain arbitrary code and may re-enter any function in this contract. "
                "Review the business logic carefully to prevent unanticipated effects on the contract state."
            )

            issue = Issue(
                contract=node.contract_name,
                function_name=node.function_name,
                address=address,
                swc_id=REENTRANCY,
                title="External call to user-supplied address",
                _type="Warning",
                bytecode=state.environment.code.bytecode,
                description=description,
                debug=debug,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
            )

        except UnsatError:

            logging.debug(
                "[EXTERNAL_CALLS] Callee address cannot be modified. Reporting informational issue."
            )

            debug = str(transaction_sequence)
            description = (
                "The contract executes a function call to an external address. "
                "Verify that the code at this address is trusted and immutable."
            )

            issue = Issue(
                contract=node.contract_name,
                function_name=state.node.function_name,
                address=address,
                swc_id=REENTRANCY,
                title="External call",
                _type="Informational",
                bytecode=state.environment.code.bytecode,
                description=description,
                debug=debug,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
            )

    except UnsatError:
        logging.debug("[EXTERNAL_CALLS] No model found.")
        return []

    return [issue]
Пример #27
0
def execute(statespace):

    logging.debug("Executing module: INTEGER")

    issues = []

    for k in statespace.nodes:
        node = statespace.nodes[k]

        for state in node.states:

            instruction = state.get_current_instruction()

            if (instruction['opcode'] == "SUB"):

                stack = state.mstate.stack

                op0 = stack[-1]
                op1 = stack[-2]

                constraints = copy.deepcopy(node.constraints)

                if type(op0) == int and type(op1) == int:
                    continue

                if (re.search(r'calldatasize_', str(op0))) \
                    or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \
                    or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)):

                    # Filter for patterns that contain possible (but apparently non-exploitable) Integer underflows.

                    # Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large.
                    # Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0

                    # Both seem to be standard compiler outputs that exist in many contracts.

                    continue

                logging.debug("[INTEGER_UNDERFLOW] Checking SUB " + str(op0) +
                              ", " + str(op1) + " at address " +
                              str(instruction['address']))

                allowed_types = [int, BitVecRef, BitVecNumRef]

                if type(op0) in allowed_types and type(op1) in allowed_types:
                    constraints.append(UGT(op1, op0))

                    try:

                        model = solver.get_model(constraints)

                        issue = Issue(node.contract_name, node.function_name,
                                      instruction['address'],
                                      "Integer Underflow", "Warning")

                        issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \
                            "The SUB instruction at address " + str(instruction['address']) + " may result in a value < 0."

                        issue.debug = "(" + str(op0) + ") - (" + str(
                            op1) + ").]"

                        issues.append(issue)

                        for d in model.decls():
                            logging.debug(
                                "[INTEGER_UNDERFLOW] model: %s = 0x%x" %
                                (d.name(), model[d].as_long()))

                    except UnsatError:
                        logging.debug("[INTEGER_UNDERFLOW] no model found")

    return issues
Пример #28
0
def execute(statespace):

    issues = []

    for k in statespace.nodes:
        node = statespace.nodes[k]

        for instruction in node.instruction_list:
            ''' This generates a lot of noise.

            if(instruction['opcode'] == "ADD"):

                stack = node.states[instruction['address']].stack

                op0 = stack[-1]
                op1 = stack[-2]

                if type(op0) == int and type(op1) == int:
                    continue

                logging.debug("[INTEGER_OVERFLOW] Checking ADD " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address']))

                constraints = copy.deepcopy(node.constraints)

                constraints.append(UGT(op0, UINT_MAX - op1))

                try:
                    
                    model = solver.get_model(constraints)

                    issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Overflow", "Warning")

                    issue.description = "A possible integer overflow exists in the function " + node.function_name + ".\n" \
                        "The addition at address " + str(instruction['address']) + " may result in a value greater than UINT_MAX." 

                    issue.debug = "(" + str(op0) + ") + (" + str(op1) + ") > (" + hex(UINT_MAX.as_long()) + ")"

                    issues.append(issue)

                    for d in model.decls():
                        logging.debug("[INTEGER_OVERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long()))

                except UnsatError:
                    logging.debug("[INTEGER_OVERFLOW] no model found")   

            '''

            if (instruction['opcode'] == "MUL"):

                stack = node.states[instruction['address']].stack

                op0 = stack[-1]
                op1 = stack[-2]

                if (type(op0) == int and type(op1) == int
                    ) or type(op0) == BoolRef or type(op1) == BoolRef:
                    continue

                logging.debug("[INTEGER_OVERFLOW] Checking MUL " + str(op0) +
                              ", " + str(op1) + " at address " +
                              str(instruction['address']))

                if re.search(r'146150163733',
                             str(op0), re.DOTALL) or re.search(
                                 r'146150163733', str(op1),
                                 re.DOTALL) or "(2 << 160 - 1)" in str(
                                     op0) or "(2 << 160 - 1)" in str(op1):
                    continue

                constraints = copy.deepcopy(node.constraints)

                constraints.append(UGT(op0, UDiv(UINT_MAX, op1)))

                try:

                    model = solver.get_model(constraints)

                    issue = Issue(node.module_name, node.function_name,
                                  instruction['address'], "Integer Overflow",
                                  "Warning")

                    issue.description = "A possible integer overflow exists in the function " + node.function_name + ".\n" \
                        "The multiplication at address " + str(instruction['address']) + " may result in a value greater than UINT_MAX."

                    issue.debug = "(" + str(op0) + ") * (" + str(
                        op1) + ") > (" + hex(UINT_MAX.as_long()) + ")"

                    issues.append(issue)

                    logging.debug("Constraints: " + str(constraints))

                    for d in model.decls():
                        logging.debug("[INTEGER_OVERFLOW] model: %s = 0x%x" %
                                      (d.name(), model[d].as_long()))

                except UnsatError:
                    logging.debug("[INTEGER_OVERFLOW] no model found")

    return issues
Пример #29
0
def execute(statespace):

    logging.debug("Executing module: UNCHECKED_SUICIDE")

    issues = []

    for k in statespace.nodes:
        node = statespace.nodes[k]

        for state in node.states:

            instruction = state.get_current_instruction()

            if (instruction['opcode'] == "SUICIDE"):

                logging.debug("[UNCHECKED_SUICIDE] suicide in function " +
                              node.function_name)

                description = "The function `" + node.function_name + "` executes the SUICIDE instruction. "

                stack = copy.deepcopy(state.mstate.stack)
                to = stack.pop()

                if ("caller" in str(to)):
                    description += "The remaining Ether is sent to the caller's address.\n"
                elif ("storage" in str(to)):
                    description += "The remaining Ether is sent to a stored address.\n"
                elif ("calldata" in str(to)):
                    description += "The remaining Ether is sent to an address provided as a function argument.\n"
                elif (type(to) == BitVecNumRef):
                    description += "The remaining Ether is sent to: " + hex(
                        to.as_long()) + "\n"
                else:
                    description += "The remaining Ether is sent to: " + str(
                        to) + "\n"

                constrained = False
                can_solve = True

                index = 0

                while (can_solve and index < len(node.constraints)):

                    constraint = node.constraints[index]
                    index += 1

                    m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))

                    if (m):
                        constrained = True
                        idx = m.group(1)

                        logging.debug("STORAGE CONSTRAINT FOUND: " + idx)

                        func = statespace.find_storage_write(
                            state.environment.active_account.address, idx)

                        if func:
                            description += "\nThere is a check on storage index " + str(
                                idx
                            ) + ". This storage index can be written to by calling the function `" + func + "`."
                            break
                        else:
                            logging.debug(
                                "[UNCHECKED_SUICIDE] No storage writes to index "
                                + str(idx))
                            can_solve = False
                            break

                    elif (re.search(r"caller", str(constraint))
                          and re.search(r'[0-9]{20}', str(constraint))):
                        can_solve = False
                        break

                if not constrained:
                    description += "\nIt seems that this function can be called without restrictions."

                if can_solve:

                    try:

                        model = solver.get_model(node.constraints)

                        debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(
                            model)

                        issue = Issue(node.contract_name, node.function_name,
                                      instruction['address'],
                                      "Unchecked SUICIDE", "Warning",
                                      description, debug)
                        issues.append(issue)

                    except UnsatError:
                        logging.debug("[UNCHECKED_SUICIDE] no model found")

    return issues
Пример #30
0
    def _analyze_state(self, state):
        """

        :param state:
        :return:
        """
        instruction = state.get_current_instruction()

        if instruction["opcode"] != "CALL":
            return []

        address = instruction["address"]
        if self._cache_addresses.get(address, False):
            return []
        call_value = state.mstate.stack[-3]
        target = state.mstate.stack[-2]

        eth_sent_total = symbol_factory.BitVecVal(0, 256)

        constraints = copy(state.mstate.constraints)

        for tx in state.world_state.transaction_sequence:
            if tx.caller == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF:
                # There's sometimes no overflow check on balances added.
                # But we don't care about attacks that require more 2^^256 ETH to be sent.

                constraints += [
                    BVAddNoOverflow(eth_sent_total, tx.call_value, False)
                ]
                eth_sent_total = Sum(eth_sent_total, tx.call_value)
        constraints += [
            UGT(call_value, eth_sent_total),
            target == state.environment.sender,
        ]

        try:

            transaction_sequence = solver.get_transaction_sequence(
                state, constraints)

            debug = json.dumps(transaction_sequence, indent=4)

            issue = Issue(
                contract=state.environment.active_account.contract_name,
                function_name=state.environment.active_function_name,
                address=instruction["address"],
                swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
                title="Unprotected Ether Withdrawal",
                severity="High",
                bytecode=state.environment.code.bytecode,
                description_head=
                "Anyone can withdraw ETH from the contract account.",
                description_tail=
                "Arbitrary senders other than the contract creator can withdraw ETH from the contract"
                +
                " account without previously having sent an equivalent amount of ETH to it. This is likely to be"
                + " a vulnerability.",
                debug=debug,
                gas_used=(state.mstate.min_gas_used,
                          state.mstate.max_gas_used),
            )
        except UnsatError:
            log.debug("[ETHER_THIEF] no model found")
            return []

        self._cache_addresses[address] = True

        return [issue]