diff --git a/.gitignore b/.gitignore index 775ecfe0..761f1c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -175,7 +175,7 @@ lol* coverage_html_report/ tests/testdata/outputs_current/ tests/testdata/outputs_current_laser_result/ -tests/mythril_dir/signatures.db +tests/testdata/mythril_config_inputs/config.ini # VSCode .vscode diff --git a/README.md b/README.md index a017d155..ad0e66dc 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ [![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril) [![Downloads](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril) -Mythril Classic is an open-source security analysis tool for Ethereum smart contracts. It uses symbolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities. It's also an experimental tool designed for security pros. If you a smart contract developer you might prefer smoother tools such as: +Mythril Classic is an open-source security analysis tool for Ethereum smart contracts. It uses symbolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities. -- [Mythos](https://github.com/cleanunicorn/mythos) -- [Truffle Security](https://github.com/ConsenSys/truffle-security) +Note that Mythril Classic is designed for security auditors. If you are a smart contract developer, we recommend using [MythX tools](https://github.com/b-mueller/awesome-mythx-smart-contract-security) which are optimized for usability and cover a wider range of security issues: + +- [Sabre](https://github.com/b-mueller/sabre) +- [MythX Plugin for Truffle](https://github.com/ConsenSys/truffle-security) Whether you want to contribute, need support, or want to learn what we have cooking for the future, our [Discord server](https://discord.gg/E3YrVtG) will serve your needs. diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index fb72c93e..3ef4a338 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -50,7 +50,7 @@ def _analyze_states(state: GlobalState) -> List[Issue]: if call.type is not "DELEGATECALL": return [] - if call.node.function_name is not "fallback": + if state.environment.active_function_name is not "fallback": return [] state = call.state @@ -77,8 +77,8 @@ def _concrete_call( return [] issue = Issue( - contract=call.node.contract_name, - function_name=call.node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=address, swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, bytecode=state.environment.code.bytecode, diff --git a/mythril/analysis/modules/dependence_on_predictable_vars.py b/mythril/analysis/modules/dependence_on_predictable_vars.py index 1450f93a..7a931997 100644 --- a/mythril/analysis/modules/dependence_on_predictable_vars.py +++ b/mythril/analysis/modules/dependence_on_predictable_vars.py @@ -71,13 +71,13 @@ def _analyze_states(state: GlobalState) -> list: "The contract sends Ether depending on the values of the following variables:\n" ) - # First check: look for predictable state variables in node & call recipient constraints + # First check: look for predictable state variables in state & call recipient constraints vars = ["coinbase", "gaslimit", "timestamp", "number"] found = [] for var in vars: - for constraint in call.node.constraints[:] + [call.to]: + for constraint in call.state.mstate.constraints[:] + [call.to]: if var in str(constraint): found.append(var) @@ -94,8 +94,8 @@ def _analyze_states(state: GlobalState) -> list: ) issue = Issue( - contract=call.node.contract_name, - function_name=call.node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=address, swc_id=swc_id, bytecode=call.state.environment.code.bytecode, @@ -112,7 +112,7 @@ def _analyze_states(state: GlobalState) -> list: # Second check: blockhash - for constraint in call.node.constraints[:] + [call.to]: + for constraint in call.state.mstate.constraints[:] + [call.to]: if "blockhash" in str(constraint): if "number" in str(constraint): m = re.search(r"blockhash\w+(\s-\s(\d+))*", str(constraint)) @@ -145,8 +145,8 @@ def _analyze_states(state: GlobalState) -> list: description += ", this expression will always be equal to zero." issue = Issue( - contract=call.node.contract_name, - function_name=call.node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=address, bytecode=call.state.environment.code.bytecode, title="Dependence on Predictable Variable", @@ -186,8 +186,8 @@ def _analyze_states(state: GlobalState) -> list: ) ) issue = Issue( - contract=call.node.contract_name, - function_name=call.node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=address, bytecode=call.state.environment.code.bytecode, title="Dependence on Predictable Variable", @@ -212,7 +212,7 @@ def solve(call: Call) -> bool: :return: """ try: - model = solver.get_model(call.node.constraints) + model = solver.get_model(call.state.mstate.constraints) log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model)) pretty_model = solver.pretty_print_model(model) diff --git a/mythril/analysis/modules/deprecated_ops.py b/mythril/analysis/modules/deprecated_ops.py index a23b55bd..6e8adacc 100644 --- a/mythril/analysis/modules/deprecated_ops.py +++ b/mythril/analysis/modules/deprecated_ops.py @@ -30,13 +30,13 @@ def _analyze_state(state): "Use of msg.origin is deprecated and the instruction may be removed in the future. " "Use msg.sender instead.\nSee also: " "https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin".format( - node.function_name + state.environment.active_function_name ) ) swc_id = DEPRECATED_FUNCTIONS_USAGE elif instruction["opcode"] == "CALLCODE": - log.debug("CALLCODE in function " + node.function_name) + log.debug("CALLCODE in function " + state.environment.active_function_name) title = "Use of callcode" description_head = "Use of callcode is deprecated." description_tail = ( @@ -45,10 +45,12 @@ def _analyze_state(state): "therefore deprecated and may be removed in the future. Use the delegatecall method instead." ) swc_id = DEPRECATED_FUNCTIONS_USAGE + else: + return issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=instruction["address"], title=title, bytecode=state.environment.code.bytecode, diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index ccdb5167..d382ca07 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -67,7 +67,6 @@ class EtherThief(DetectionModule): :return: """ instruction = state.get_current_instruction() - node = state.node if instruction["opcode"] != "CALL": return [] @@ -80,7 +79,7 @@ class EtherThief(DetectionModule): eth_sent_total = symbol_factory.BitVecVal(0, 256) - constraints = copy(node.constraints) + constraints = copy(state.mstate.constraints) for tx in state.world_state.transaction_sequence: if tx.caller == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF: @@ -101,8 +100,8 @@ class EtherThief(DetectionModule): debug = json.dumps(transaction_sequence, indent=4) issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + 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", diff --git a/mythril/analysis/modules/exceptions.py b/mythril/analysis/modules/exceptions.py index d819d0ea..8eaf6400 100644 --- a/mythril/analysis/modules/exceptions.py +++ b/mythril/analysis/modules/exceptions.py @@ -19,9 +19,8 @@ def _analyze_state(state) -> list: :return: """ log.info("Exceptions module: found ASSERT_FAIL instruction") - node = state.node - log.debug("ASSERT_FAIL in function " + node.function_name) + log.debug("ASSERT_FAIL in function " + state.environment.active_function_name) try: address = state.get_current_instruction()["address"] @@ -34,12 +33,14 @@ def _analyze_state(state) -> list: "Use `require()` for regular input checking." ) - transaction_sequence = solver.get_transaction_sequence(state, node.constraints) + transaction_sequence = solver.get_transaction_sequence( + state, state.mstate.constraints + ) debug = json.dumps(transaction_sequence, indent=4) issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=address, swc_id=ASSERT_VIOLATION, title="Exception State", diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index 00545bc1..95eb428e 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -8,6 +8,7 @@ from mythril.analysis.report import Issue from mythril.laser.smt import UGT, symbol_factory from mythril.laser.ethereum.state.global_state import GlobalState from mythril.exceptions import UnsatError +from copy import copy import logging import json @@ -28,14 +29,13 @@ def _analyze_state(state): :param state: :return: """ - node = state.node gas = state.mstate.stack[-1] to = state.mstate.stack[-2] address = state.get_current_instruction()["address"] try: - constraints = node.constraints + constraints = copy(state.mstate.constraints) transaction_sequence = solver.get_transaction_sequence( state, constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))] ) @@ -56,8 +56,8 @@ def _analyze_state(state): ) issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + 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", @@ -83,8 +83,8 @@ def _analyze_state(state): ) issue = Issue( - contract=node.contract_name, - function_name=state.node.function_name, + 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", diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 94b3588b..0c6582a0 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -120,7 +120,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): c = Not(BVAddNoOverflow(op0, op1, False)) # Check satisfiable - model = self._try_constraints(state.node.constraints, [c]) + model = self._try_constraints(state.mstate.constraints, [c]) if model is None: return @@ -132,7 +132,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): c = Not(BVMulNoOverflow(op0, op1, False)) # Check satisfiable - model = self._try_constraints(state.node.constraints, [c]) + model = self._try_constraints(state.mstate.constraints, [c]) if model is None: return @@ -144,7 +144,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): c = Not(BVSubNoUnderflow(op0, op1, False)) # Check satisfiable - model = self._try_constraints(state.node.constraints, [c]) + model = self._try_constraints(state.mstate.constraints, [c]) if model is None: return @@ -172,7 +172,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): ) else: constraint = op0.value ** op1.value >= 2 ** 256 - model = self._try_constraints(state.node.constraints, [constraint]) + model = self._try_constraints(state.mstate.constraints, [constraint]) if model is None: return annotation = OverUnderflowAnnotation(state, "exponentiation", constraint) @@ -286,19 +286,18 @@ class IntegerOverflowUnderflowModule(DetectionModule): ): continue - node = ostate.node try: transaction_sequence = solver.get_transaction_sequence( - state, node.constraints + [annotation.constraint] + state, state.mstate.constraints + [annotation.constraint] ) except UnsatError: continue _type = "Underflow" if annotation.operator == "subtraction" else "Overflow" issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + contract=ostate.environment.active_account.contract_name, + function_name=ostate.environment.active_function_name, address=ostate.get_current_instruction()["address"], swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, bytecode=ostate.environment.code.bytecode, @@ -319,8 +318,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): @staticmethod def _try_constraints(constraints, new_constraints): - """ - Tries new constraints + """ Tries new constraints :return Model if satisfiable otherwise None """ try: diff --git a/mythril/analysis/modules/multiple_sends.py b/mythril/analysis/modules/multiple_sends.py index a4c650da..1afaaed2 100644 --- a/mythril/analysis/modules/multiple_sends.py +++ b/mythril/analysis/modules/multiple_sends.py @@ -55,7 +55,6 @@ def _analyze_state(state: GlobalState): :param state: the current state :return: returns the issues for that corresponding state """ - node = state.node instruction = state.get_current_instruction() annotations = cast( @@ -95,8 +94,8 @@ def _analyze_state(state: GlobalState): ) issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=instruction["address"], swc_id=MULTIPLE_SENDS, bytecode=state.environment.code.bytecode, diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index b9cfce4e..62bc3e00 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -48,13 +48,14 @@ class SuicideModule(DetectionModule): def _analyze_state(self, state): log.info("Suicide module: Analyzing suicide instruction") - node = state.node instruction = state.get_current_instruction() if self._cache_address.get(instruction["address"], False): return [] to = state.mstate.stack[-1] - log.debug("[SUICIDE] SUICIDE in function " + node.function_name) + log.debug( + "[SUICIDE] SUICIDE in function " + state.environment.active_function_name + ) description_head = "The contract can be killed by anyone." @@ -62,7 +63,7 @@ class SuicideModule(DetectionModule): try: transaction_sequence = solver.get_transaction_sequence( state, - node.constraints + state.mstate.constraints + [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF], ) description_tail = ( @@ -71,7 +72,7 @@ class SuicideModule(DetectionModule): ) except UnsatError: transaction_sequence = solver.get_transaction_sequence( - state, node.constraints + state, state.mstate.constraints ) description_tail = "Arbitrary senders can kill this contract." @@ -79,8 +80,8 @@ class SuicideModule(DetectionModule): self._cache_address[instruction["address"]] = True issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + contract=state.environment.active_account.contract_name, + function_name=state.environment.active_function_name, address=instruction["address"], swc_id=UNPROTECTED_SELFDESTRUCT, bytecode=state.environment.code.bytecode, diff --git a/mythril/analysis/modules/transaction_order_dependence.py b/mythril/analysis/modules/transaction_order_dependence.py deleted file mode 100644 index 4759d751..00000000 --- a/mythril/analysis/modules/transaction_order_dependence.py +++ /dev/null @@ -1,198 +0,0 @@ -"""This module contains the detection code to find the existence of transaction -order dependence.""" -import copy -import logging -import re - -from mythril.analysis import solver -from mythril.analysis.modules.base import DetectionModule -from mythril.analysis.ops import * -from mythril.analysis.report import Issue -from mythril.analysis.swc_data import TX_ORDER_DEPENDENCE -from mythril.exceptions import UnsatError - -log = logging.getLogger(__name__) - - -class TxOrderDependenceModule(DetectionModule): - """This module finds the existence of transaction order dependence.""" - - def __init__(self): - super().__init__( - name="Transaction Order Dependence", - swc_id=TX_ORDER_DEPENDENCE, - description=( - "This module finds the existance of transaction order dependence " - "vulnerabilities. The following webpage contains an extensive description " - "of the vulnerability: " - "https://consensys.github.io/smart-contract-best-practices/known_attacks/#transaction-ordering-dependence-tod-front-running" - ), - ) - - def execute(self, statespace): - """Executes the analysis module. - - :param statespace: - :return: - """ - log.debug("Executing module: TOD") - - issues = [] - - for call in statespace.calls: - # Do analysis - interesting_storages = list(self._get_influencing_storages(call)) - changing_sstores = list( - self._get_influencing_sstores(statespace, interesting_storages) - ) - - description_tail = ( - "A transaction order dependence vulnerability may exist in this contract. The value or " - "target of the call statement is loaded from a writable storage location." - ) - - # Build issue if necessary - if len(changing_sstores) > 0: - node = call.node - instruction = call.state.get_current_instruction() - issue = Issue( - contract=node.contract_name, - function_name=node.function_name, - address=instruction["address"], - title="Transaction Order Dependence", - bytecode=call.state.environment.code.bytecode, - swc_id=TX_ORDER_DEPENDENCE, - severity="Medium", - description_head="The call outcome may depend on transaction order.", - description_tail=description_tail, - gas_used=( - call.state.mstate.min_gas_used, - call.state.mstate.max_gas_used, - ), - ) - - issues.append(issue) - - return issues - - # TODO: move to __init__ or util module - @staticmethod - def _get_states_with_opcode(statespace, opcode): - """Gets all (state, node) tuples in statespace with opcode. - - :param statespace: - :param opcode: - """ - for k in statespace.nodes: - node = statespace.nodes[k] - for state in node.states: - if state.get_current_instruction()["opcode"] == opcode: - yield state, node - - @staticmethod - def _dependent_on_storage(expression): - """Checks if expression is dependent on a storage symbol and returns - the influencing storages. - - :param expression: - :return: - """ - pattern = re.compile(r"storage_[a-z0-9_&^]*[0-9]+") - return pattern.findall(str(simplify(expression))) - - @staticmethod - def _get_storage_variable(storage, state): - """Get storage z3 object given storage name and the state. - - :param storage: storage name example: storage_0 - :param state: state to retrieve the variable from - :return: z3 object representing storage - """ - index = int(re.search("[0-9]+", storage).group()) - try: - return state.environment.active_account.storage[index] - except KeyError: - return None - - def _can_change(self, constraints, variable): - """Checks if the variable can change given some constraints. - - :param constraints: - :param variable: - :return: - """ - _constraints = copy.deepcopy(constraints) - try: - model = solver.get_model(_constraints) - except UnsatError: - return False - try: - initial_value = int(str(model.eval(variable, model_completion=True))) - return ( - self._try_constraints(constraints, [variable != initial_value]) - is not None - ) - except AttributeError: - return False - - def _get_influencing_storages(self, call): - """Examines a Call object and returns an iterator of all storages that - influence the call value or direction. - - :param call: - """ - state = call.state - node = call.node - - # Get relevant storages - to, value = call.to, call.value - storages = [] - if to.type == VarType.SYMBOLIC: - storages += self._dependent_on_storage(to.val) - if value.type == VarType.SYMBOLIC: - storages += self._dependent_on_storage(value.val) - - # See if they can change within the constraints of the node - for storage in storages: - variable = self._get_storage_variable(storage, state) - can_change = self._can_change(node.constraints, variable) - if can_change: - yield storage - - def _get_influencing_sstores(self, statespace, interesting_storages): - """Gets sstore (state, node) tuples that write to interesting_storages. - - :param statespace: - :param interesting_storages: - """ - for sstore_state, node in self._get_states_with_opcode(statespace, "SSTORE"): - index, value = sstore_state.mstate.stack[-1], sstore_state.mstate.stack[-2] - try: - index = util.get_concrete_int(index) - except TypeError: - index = str(index) - if "storage_{}".format(index) not in interesting_storages: - continue - - yield sstore_state, node - - # TODO: remove - @staticmethod - def _try_constraints(constraints, new_constraints): - """Tries new constraints. - - :param constraints: - :param new_constraints: - :return Model if satisfiable otherwise None - """ - _constraints = copy.deepcopy(constraints) - for constraint in new_constraints: - _constraints.append(copy.deepcopy(constraint)) - try: - model = solver.get_model(_constraints) - return model - except UnsatError: - return None - - -detector = TxOrderDependenceModule() diff --git a/mythril/analysis/modules/unchecked_retval.py b/mythril/analysis/modules/unchecked_retval.py index 5beb0fda..45fdceec 100644 --- a/mythril/analysis/modules/unchecked_retval.py +++ b/mythril/analysis/modules/unchecked_retval.py @@ -61,7 +61,6 @@ class UncheckedRetvalModule(DetectionModule): def _analyze_state(state: GlobalState) -> list: instruction = state.get_current_instruction() - node = state.node annotations = cast( List[UncheckedRetvalAnnotation], @@ -80,7 +79,7 @@ def _analyze_state(state: GlobalState) -> list: issues = [] for retval in retvals: try: - solver.get_model(node.constraints + [retval["retval"] == 0]) + solver.get_model(state.mstate.constraints + [retval["retval"] == 0]) except UnsatError: continue @@ -91,8 +90,8 @@ def _analyze_state(state: GlobalState) -> list: ) issue = Issue( - contract=node.contract_name, - function_name=node.function_name, + 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", diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index 44fede86..4ba3b942 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -214,7 +214,7 @@ class Report: }, "severity": issue.severity, "locations": [{"sourceMap": "%d:1:%d" % (issue.address, idx)}], - "extra": {}, + "extra": {"discoveryTime": int(issue.discovery_time * 10 ** 9)}, } ) meta_data = self._get_exception_data() diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index f4477fad..79f70efb 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -12,7 +12,9 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnWeightedRandomStrategy, ) -from mythril.laser.ethereum.plugins.mutation_pruner import MutationPruner +from mythril.laser.ethereum.plugins.plugin_factory import PluginFactory +from mythril.laser.ethereum.plugins.plugin_loader import LaserPluginLoader + from mythril.solidity.soliditycontract import EVMContract, SolidityContract from .ops import Call, SStore, VarType, get_variable @@ -84,9 +86,10 @@ class SymExecWrapper: requires_statespace=requires_statespace, enable_iprof=enable_iprof, ) - mutation_plugin = MutationPruner() - mutation_plugin.initialize(self.laser) + plugin_loader = LaserPluginLoader(self.laser) + plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) + plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) self.laser.register_hooks( hook_type="pre", diff --git a/mythril/ethereum/interface/leveldb/state.py b/mythril/ethereum/interface/leveldb/state.py index 4cd5c133..4306897e 100644 --- a/mythril/ethereum/interface/leveldb/state.py +++ b/mythril/ethereum/interface/leveldb/state.py @@ -150,7 +150,7 @@ class State: rlpdata = self.trie.get(addr) if rlpdata != trie.BLANK_NODE: - o = rlp.decode(rlpdata, Account, db=self.db, address=addr) + o = rlp.decode(rlpdata, Account, db=self.db, addr=addr) else: o = Account.blank_account(self.db, addr, 0) self.cache[addr] = o @@ -162,4 +162,4 @@ class State: """iterates through trie to and yields non-blank leafs as accounts.""" for address_hash, rlpdata in self.secure_trie.trie.iter_branch(): if rlpdata != trie.BLANK_NODE: - yield rlp.decode(rlpdata, Account, db=self.db, address=address_hash) + yield rlp.decode(rlpdata, Account, db=self.db, addr=address_hash) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index c51e8b2a..514f039d 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -10,13 +10,18 @@ import json import logging import os import sys -import traceback import coloredlogs +import traceback import mythril.support.signatures as sigs from mythril.exceptions import AddressNotFoundError, CriticalError -from mythril.mythril import Mythril +from mythril.mythril import ( + MythrilAnalyzer, + MythrilDisassembler, + MythrilConfig, + MythrilLevelDB, +) from mythril.version import VERSION # logging.basicConfig(level=logging.DEBUG) @@ -26,7 +31,6 @@ log = logging.getLogger(__name__) def exit_with_error(format_, message): """ - :param format_: :param message: """ @@ -64,10 +68,6 @@ def main() -> None: parse_args(parser=parser, args=args) -if __name__ == "__main__": - main() - - def create_parser(parser: argparse.ArgumentParser) -> None: """ Creates the parser by setting all the possible arguments @@ -315,61 +315,56 @@ def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): def quick_commands(args: argparse.Namespace): if args.hash: - print(Mythril.hash_for_function_signature(args.hash)) + print(MythrilDisassembler.hash_for_function_signature(args.hash)) sys.exit() def set_config(args: argparse.Namespace): - mythril = Mythril( - solv=args.solv, - dynld=args.dynld, - onchain_storage_access=(not args.no_onchain_storage_access), - solc_args=args.solc_args, - enable_online_lookup=args.query_signature, - ) + config = MythrilConfig() if args.dynld or not args.no_onchain_storage_access and not (args.rpc or args.i): - mythril.set_api_from_config_path() + config.set_api_from_config_path() if args.address: # Establish RPC connection if necessary - mythril.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) + config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) elif args.search or args.contract_hash_to_address: # Open LevelDB if necessary - mythril.set_api_leveldb( - mythril.leveldb_dir if not args.leveldb_dir else args.leveldb_dir + config.set_api_leveldb( + config.leveldb_dir if not args.leveldb_dir else args.leveldb_dir ) - return mythril + return config -def leveldb_search(mythril: Mythril, args: argparse.Namespace): - if args.search: - # Database search ops - mythril.search_db(args.search) - sys.exit() +def leveldb_search(config: MythrilConfig, args: argparse.Namespace): + if args.search or args.contract_hash_to_address: + leveldb_searcher = MythrilLevelDB(config.eth_db) + if args.search: + # Database search ops + leveldb_searcher.search_db(args.search) - if args.contract_hash_to_address: - # search corresponding address - try: - mythril.contract_hash_to_address(args.contract_hash_to_address) - except AddressNotFoundError: - print("Address not found.") + else: + # search corresponding address + try: + leveldb_searcher.contract_hash_to_address(args.contract_hash_to_address) + except AddressNotFoundError: + print("Address not found.") sys.exit() -def get_code(mythril: Mythril, args: argparse.Namespace): +def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): address = None if args.code: # Load from bytecode code = args.code[2:] if args.code.startswith("0x") else args.code - address, _ = mythril.load_from_bytecode(code, args.bin_runtime) + address, _ = disassembler.load_from_bytecode(code, args.bin_runtime) elif args.codefile: bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0]) bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode - address, _ = mythril.load_from_bytecode(bytecode, args.bin_runtime) + address, _ = disassembler.load_from_bytecode(bytecode, args.bin_runtime) elif args.address: # Get bytecode from a contract address - address, _ = mythril.load_from_address(args.address) + address, _ = disassembler.load_from_address(args.address) elif args.solidity_file: # Compile Solidity source file(s) if args.graph and len(args.solidity_file) > 1: @@ -377,7 +372,9 @@ def get_code(mythril: Mythril, args: argparse.Namespace): args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.", ) - address, _ = mythril.load_from_solidity(args.solidity_file) # list of files + address, _ = disassembler.load_from_solidity( + args.solidity_file + ) # list of files else: exit_with_error( args.outform, @@ -387,11 +384,12 @@ def get_code(mythril: Mythril, args: argparse.Namespace): def execute_command( - mythril: Mythril, + disassembler: MythrilDisassembler, address: str, parser: argparse.ArgumentParser, args: argparse.Namespace, ): + if args.storage: if not args.address: exit_with_error( @@ -399,36 +397,42 @@ def execute_command( "To read storage, provide the address of a deployed contract with the -a option.", ) - storage = mythril.get_state_variable_from_storage( + storage = disassembler.get_state_variable_from_storage( address=address, params=[a.strip() for a in args.storage.strip().split(",")] ) print(storage) + return + + analyzer = MythrilAnalyzer( + strategy=args.strategy, + disassembler=disassembler, + address=address, + max_depth=args.max_depth, + execution_timeout=args.execution_timeout, + create_timeout=args.create_timeout, + enable_iprof=args.enable_iprof, + onchain_storage_access=not args.no_onchain_storage_access, + ) - elif args.disassemble: + if args.disassemble: # or mythril.disassemble(mythril.contracts[0]) - if mythril.contracts[0].code: - print("Runtime Disassembly: \n" + mythril.contracts[0].get_easm()) - if mythril.contracts[0].creation_code: - print("Disassembly: \n" + mythril.contracts[0].get_creation_easm()) + if disassembler.contracts[0].code: + print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) + if disassembler.contracts[0].creation_code: + print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm()) elif args.graph or args.fire_lasers: - if not mythril.contracts: + if not disassembler.contracts: exit_with_error( args.outform, "input files do not contain any valid contracts" ) if args.graph: - html = mythril.graph_html( - strategy=args.strategy, - contract=mythril.contracts[0], - address=address, + html = analyzer.graph_html( + contract=analyzer.contracts[0], enable_physics=args.enable_physics, phrackify=args.phrack, - max_depth=args.max_depth, - execution_timeout=args.execution_timeout, - create_timeout=args.create_timeout, - enable_iprof=args.enable_iprof, ) try: @@ -439,18 +443,12 @@ def execute_command( else: try: - report = mythril.fire_lasers( - strategy=args.strategy, - address=address, + report = analyzer.fire_lasers( modules=[m.strip() for m in args.modules.strip().split(",")] if args.modules else [], verbose_report=args.verbose_report, - max_depth=args.max_depth, - execution_timeout=args.execution_timeout, - create_timeout=args.create_timeout, transaction_count=args.transaction_count, - enable_iprof=args.enable_iprof, ) outputs = { "json": report.as_json(), @@ -466,20 +464,12 @@ def execute_command( elif args.statespace_json: - if not mythril.contracts: + if not analyzer.contracts: exit_with_error( args.outform, "input files do not contain any valid contracts" ) - statespace = mythril.dump_statespace( - strategy=args.strategy, - contract=mythril.contracts[0], - address=address, - max_depth=args.max_depth, - execution_timeout=args.execution_timeout, - create_timeout=args.create_timeout, - enable_iprof=args.enable_iprof, - ) + statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) try: with open(args.statespace_json, "w") as f: @@ -515,20 +505,27 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non validate_args(parser, args) try: quick_commands(args) - mythril = set_config(args) - leveldb_search(mythril, args) - + config = set_config(args) + leveldb_search(config, args) + dissasembler = MythrilDisassembler( + eth=config.eth, + solc_version=args.solv, + solc_args=args.solc_args, + enable_online_lookup=args.query_signature, + ) if args.truffle: try: - mythril.analyze_truffle_project(args) + dissasembler.analyze_truffle_project(args) except FileNotFoundError: print( "Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully." ) sys.exit() - address = get_code(mythril, args) - execute_command(mythril=mythril, address=address, parser=parser, args=args) + address = get_code(dissasembler, args) + execute_command( + disassembler=dissasembler, address=address, parser=parser, args=args + ) except CriticalError as ce: exit_with_error(args.outform, str(ce)) except Exception: diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 27df993e..bb86b7a8 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -136,7 +136,7 @@ def get_callee_account( log.debug("Attempting to load dependency") try: - code = dynamic_loader.dynld(environment.active_account.address, callee_address) + code = dynamic_loader.dynld(callee_address) except ValueError as error: log.debug("Unable to execute dynamic loader because: {}".format(str(error))) raise error diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 09152f80..b6bb61cd 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1097,7 +1097,7 @@ class Instruction: return [global_state] try: - code = self.dynamic_loader.dynld(environment.active_account.address, addr) + code = self.dynamic_loader.dynld(addr) except (ValueError, AttributeError) as e: log.debug("error accessing contract storage due to: " + str(e)) state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256)) diff --git a/mythril/laser/ethereum/plugins/__init__.py b/mythril/laser/ethereum/plugins/__init__.py index e69de29b..8265e162 100644 --- a/mythril/laser/ethereum/plugins/__init__.py +++ b/mythril/laser/ethereum/plugins/__init__.py @@ -0,0 +1,21 @@ +""" Laser plugins + +This module contains everything to do with laser plugins + +Laser plugins are a way of extending laser's functionality without complicating the core business logic. +Different features that have been implemented in the form of plugins are: +- benchmarking +- path pruning + +Plugins also provide a way to implement optimisations outside of the mythril code base and to inject them. +The api that laser currently provides is still unstable and will probably change to suit our needs +as more plugins get developed. + +For the implementation of plugins the following modules are of interest: +- laser.plugins.plugin +- laser.plugins.signals +- laser.svm + +Which show the basic interfaces with which plugins are able to interact +""" +from mythril.laser.ethereum.plugins.signals import PluginSignal diff --git a/mythril/laser/ethereum/plugins/implementations/__init__.py b/mythril/laser/ethereum/plugins/implementations/__init__.py new file mode 100644 index 00000000..e00b7421 --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/__init__.py @@ -0,0 +1,7 @@ +""" Plugin implementations + +This module contains the implementation of some features + +- benchmarking +- pruning +""" diff --git a/mythril/laser/ethereum/plugins/benchmark.py b/mythril/laser/ethereum/plugins/implementations/benchmark.py similarity index 94% rename from mythril/laser/ethereum/plugins/benchmark.py rename to mythril/laser/ethereum/plugins/implementations/benchmark.py index 530c312e..3aef4c25 100644 --- a/mythril/laser/ethereum/plugins/benchmark.py +++ b/mythril/laser/ethereum/plugins/implementations/benchmark.py @@ -1,4 +1,5 @@ from mythril.laser.ethereum.svm import LaserEVM +from mythril.laser.ethereum.plugins.plugin import LaserPlugin from time import time import matplotlib.pyplot as plt import logging @@ -6,7 +7,8 @@ import logging log = logging.getLogger(__name__) -class BenchmarkPlugin: +# TODO: introduce dependency on coverage plugin +class BenchmarkPlugin(LaserPlugin): """Benchmark Plugin This plugin aggregates the following information: diff --git a/mythril/laser/ethereum/plugins/implementations/coverage/__init__.py b/mythril/laser/ethereum/plugins/implementations/coverage/__init__.py new file mode 100644 index 00000000..b600877d --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/coverage/__init__.py @@ -0,0 +1,3 @@ +from mythril.laser.ethereum.plugins.implementations.coverage.coverage_plugin import ( + InstructionCoveragePlugin, +) diff --git a/mythril/laser/ethereum/plugins/implementations/coverage/coverage_plugin.py b/mythril/laser/ethereum/plugins/implementations/coverage/coverage_plugin.py new file mode 100644 index 00000000..75c6bfa4 --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/coverage/coverage_plugin.py @@ -0,0 +1,98 @@ +from mythril.laser.ethereum.svm import LaserEVM +from mythril.laser.ethereum.plugins.plugin import LaserPlugin +from mythril.laser.ethereum.state.global_state import GlobalState + +from typing import Dict, Tuple, List + +import logging + +log = logging.getLogger(__name__) + + +class InstructionCoveragePlugin(LaserPlugin): + """InstructionCoveragePlugin + + This plugin measures the instruction coverage of mythril. + The instruction coverage is the ratio between the instructions that have been executed + and the total amount of instructions. + + Note that with lazy constraint solving enabled that this metric will be "unsound" as + reachability will not be considered for the calculation of instruction coverage. + + """ + + def __init__(self): + self.coverage = {} # type: Dict[str, Tuple[int, List[bool]]] + self.initial_coverage = 0 + self.tx_id = 0 + + def initialize(self, symbolic_vm: LaserEVM): + """Initializes the instruction coverage plugin + + Introduces hooks for each instruction + :param symbolic_vm: + :return: + """ + self.coverage = {} + self.initial_coverage = 0 + self.tx_id = 0 + + @symbolic_vm.laser_hook("stop_sym_exec") + def stop_sym_exec_hook(): + # Print results + for code, code_cov in self.coverage.items(): + cov_percentage = sum(code_cov[1]) / float(code_cov[0]) * 100 + + log.info( + "Achieved {:.2f}% coverage for code: {}".format( + cov_percentage, code + ) + ) + + @symbolic_vm.laser_hook("execute_state") + def execute_state_hook(global_state: GlobalState): + # Record coverage + code = global_state.environment.code.bytecode + + if code not in self.coverage.keys(): + number_of_instructions = len( + global_state.environment.code.instruction_list + ) + self.coverage[code] = ( + number_of_instructions, + [False] * number_of_instructions, + ) + + self.coverage[code][1][global_state.mstate.pc] = True + + @symbolic_vm.laser_hook("start_sym_trans") + def execute_start_sym_trans_hook(): + self.initial_coverage = self._get_covered_instructions() + + @symbolic_vm.laser_hook("stop_sym_trans") + def execute_stop_sym_trans_hook(): + end_coverage = self._get_covered_instructions() + log.info( + "Number of new instructions covered in tx %d: %d" + % (self.tx_id, end_coverage - self.initial_coverage) + ) + self.tx_id += 1 + + def _get_covered_instructions(self) -> int: + """Gets the total number of covered instructions for all accounts in + the svm. + :return: + """ + total_covered_instructions = 0 + for _, cv in self.coverage.items(): + total_covered_instructions += sum(cv[1]) + return total_covered_instructions + + def is_instruction_covered(self, bytecode, index): + if bytecode not in self.coverage.keys(): + return False + + try: + return self.coverage[bytecode][index] + except IndexError: + return False diff --git a/mythril/laser/ethereum/plugins/implementations/coverage/coverage_strategy.py b/mythril/laser/ethereum/plugins/implementations/coverage/coverage_strategy.py new file mode 100644 index 00000000..2ffa29d6 --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/coverage/coverage_strategy.py @@ -0,0 +1,43 @@ +from mythril.laser.ethereum.strategy import BasicSearchStrategy +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.plugins.implementations.coverage import ( + InstructionCoveragePlugin, +) + + +class CoverageStrategy(BasicSearchStrategy): + """Implements a instruction coverage based search strategy + + This strategy is quite simple and effective, it prioritizes the execution of instructions that have previously been + uncovered. Once there is no such global state left in the work list, it will resort to using the super_strategy. + + This strategy is intended to be used "on top of" another one + """ + + def __init__( + self, + super_strategy: BasicSearchStrategy, + instruction_coverage_plugin: InstructionCoveragePlugin, + ): + self.super_strategy = super_strategy + self.instruction_coverage_plugin = instruction_coverage_plugin + BasicSearchStrategy.__init__( + self, super_strategy.work_list, super_strategy.max_depth + ) + + def get_strategic_global_state(self) -> GlobalState: + """ + Returns the first uncovered global state in the work list if it exists, + otherwise super_strategy.get_strategic_global_state() is returned. + """ + for global_state in self.work_list: + if not self._is_covered(global_state): + self.work_list.remove(global_state) + return global_state + return self.super_strategy.get_strategic_global_state() + + def _is_covered(self, global_state: GlobalState) -> bool: + """ Checks if the instruction for the given global state is already covered""" + bytecode = global_state.environment.code.bytecode + index = global_state.mstate.pc + return self.instruction_coverage_plugin.is_instruction_covered(bytecode, index) diff --git a/mythril/laser/ethereum/plugins/mutation_pruner.py b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py similarity index 95% rename from mythril/laser/ethereum/plugins/mutation_pruner.py rename to mythril/laser/ethereum/plugins/implementations/mutation_pruner.py index 38ad1410..8a6fc9d6 100644 --- a/mythril/laser/ethereum/plugins/mutation_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/mutation_pruner.py @@ -1,6 +1,7 @@ from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.svm import LaserEVM from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState +from mythril.laser.ethereum.plugins.plugin import LaserPlugin from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, @@ -17,7 +18,7 @@ class MutationAnnotation(StateAnnotation): pass -class MutationPruner: +class MutationPruner(LaserPlugin): """Mutation pruner plugin Let S be a world state from which T is a symbolic transaction, and S' is the resulting world state. diff --git a/mythril/laser/ethereum/plugins/plugin.py b/mythril/laser/ethereum/plugins/plugin.py new file mode 100644 index 00000000..55fbdb72 --- /dev/null +++ b/mythril/laser/ethereum/plugins/plugin.py @@ -0,0 +1,23 @@ +from mythril.laser.ethereum.svm import LaserEVM + + +class LaserPlugin: + """ Base class for laser plugins + + Functionality in laser that the symbolic execution process does not need to depend on + can be implemented in the form of a laser plugin. + + Laser plugins implement the function initialize(symbolic_vm) which is called with the laser virtual machine + when they are loaded. + Regularly a plugin will introduce several hooks into laser in this function + + Plugins can direct actions by raising Signals defined in mythril.laser.ethereum.plugins.signals + For example, a pruning plugin might raise the PluginSkipWorldState signal. + """ + + def initialize(self, symbolic_vm: LaserEVM) -> None: + """ Initializes this plugin on the symbolic virtual machine + + :param symbolic_vm: symbolic virtual machine to initialize the laser plugin on + """ + raise NotImplementedError diff --git a/mythril/laser/ethereum/plugins/plugin_factory.py b/mythril/laser/ethereum/plugins/plugin_factory.py new file mode 100644 index 00000000..1ce61b34 --- /dev/null +++ b/mythril/laser/ethereum/plugins/plugin_factory.py @@ -0,0 +1,32 @@ +from mythril.laser.ethereum.plugins.plugin import LaserPlugin + + +class PluginFactory: + """ The plugin factory constructs the plugins provided with laser """ + + @staticmethod + def build_benchmark_plugin(name: str) -> LaserPlugin: + """ Creates an instance of the benchmark plugin with the given name """ + from mythril.laser.ethereum.plugins.implementations.benchmark import ( + BenchmarkPlugin, + ) + + return BenchmarkPlugin(name) + + @staticmethod + def build_mutation_pruner_plugin() -> LaserPlugin: + """ Creates an instance of the mutation pruner plugin""" + from mythril.laser.ethereum.plugins.implementations.mutation_pruner import ( + MutationPruner, + ) + + return MutationPruner() + + @staticmethod + def build_instruction_coverage_plugin() -> LaserPlugin: + """ Creates an instance of the instruction coverage plugin""" + from mythril.laser.ethereum.plugins.implementations.coverage import ( + InstructionCoveragePlugin, + ) + + return InstructionCoveragePlugin() diff --git a/mythril/laser/ethereum/plugins/plugin_loader.py b/mythril/laser/ethereum/plugins/plugin_loader.py new file mode 100644 index 00000000..ef8b9ced --- /dev/null +++ b/mythril/laser/ethereum/plugins/plugin_loader.py @@ -0,0 +1,38 @@ +from mythril.laser.ethereum.svm import LaserEVM +from mythril.laser.ethereum.plugins.plugin import LaserPlugin + +from typing import List +import logging + +log = logging.getLogger(__name__) + + +class LaserPluginLoader: + """ + The LaserPluginLoader is used to abstract the logic relating to plugins. + Components outside of laser thus don't have to be aware of the interface that plugins provide + """ + + def __init__(self, symbolic_vm: LaserEVM) -> None: + """ Initializes the plugin loader + + :param symbolic_vm: symbolic virtual machine to load plugins for + """ + self.symbolic_vm = symbolic_vm + self.laser_plugins = [] # type: List[LaserPlugin] + + def load(self, laser_plugin: LaserPlugin) -> None: + """ Loads the plugin + + :param laser_plugin: plugin that will be loaded in the symbolic virtual machine + """ + log.info("Loading plugin: {}".format(str(laser_plugin))) + laser_plugin.initialize(self.symbolic_vm) + self.laser_plugins.append(laser_plugin) + + def is_enabled(self, laser_plugin: LaserPlugin) -> bool: + """ Returns whether the plugin is loaded in the symbolic_vm + + :param laser_plugin: plugin that will be checked + """ + return laser_plugin in self.laser_plugins diff --git a/mythril/laser/ethereum/strategy/__init__.py b/mythril/laser/ethereum/strategy/__init__.py index 58672339..140880b0 100644 --- a/mythril/laser/ethereum/strategy/__init__.py +++ b/mythril/laser/ethereum/strategy/__init__.py @@ -1,4 +1,6 @@ from abc import ABC, abstractmethod +from typing import List +from mythril.laser.ethereum.state.global_state import GlobalState class BasicSearchStrategy(ABC): @@ -7,7 +9,7 @@ class BasicSearchStrategy(ABC): __slots__ = "work_list", "max_depth" def __init__(self, work_list, max_depth): - self.work_list = work_list + self.work_list = work_list # type: List[GlobalState] self.max_depth = max_depth def __iter__(self): diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 56e4b948..0fcb8d77 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -72,8 +72,6 @@ class LaserEVM: self.world_state = world_state self.open_states = [world_state] - self.coverage = {} # type: Dict[str, Tuple[int, List[bool]]] - self.total_states = 0 self.dynamic_loader = dynamic_loader @@ -97,6 +95,10 @@ class LaserEVM: self._add_world_state_hooks = [] # type: List[Callable] self._execute_state_hooks = [] # type: List[Callable] + + self._start_sym_trans_hooks = [] # type: List[Callable] + self._stop_sym_trans_hooks = [] # type: List[Callable] + self._start_sym_exec_hooks = [] # type: List[Callable] self._stop_sym_exec_hooks = [] # type: List[Callable] @@ -158,10 +160,6 @@ class LaserEVM: len(self.edges), self.total_states, ) - for code, coverage in self.coverage.items(): - cov = sum(coverage[1]) / float(coverage[0]) * 100 - - log.info("Achieved {:.2f}% coverage for code: {}".format(cov, code)) if self.iprof is not None: log.info("Instruction Statistics:\n{}".format(self.iprof)) @@ -170,42 +168,25 @@ class LaserEVM: hook() def _execute_transactions(self, address): - """This function executes multiple transactions on the address based on - the coverage. + """This function executes multiple transactions on the address :param address: Address of the contract :return: """ - self.coverage = {} for i in range(self.transaction_count): - initial_coverage = self._get_covered_instructions() - self.time = datetime.now() log.info( "Starting message call transaction, iteration: {}, {} initial states".format( i, len(self.open_states) ) ) + for hook in self._start_sym_trans_hooks: + hook() execute_message_call(self, address) - end_coverage = self._get_covered_instructions() - - log.info( - "Number of new instructions covered in tx %d: %d" - % (i, end_coverage - initial_coverage) - ) - - def _get_covered_instructions(self) -> int: - """Gets the total number of covered instructions for all accounts in - the svm. - - :return: - """ - total_covered_instructions = 0 - for _, cv in self.coverage.items(): - total_covered_instructions += sum(cv[1]) - return total_covered_instructions + for hook in self._stop_sym_trans_hooks: + hook() def exec(self, create=False, track_gas=False) -> Union[List[GlobalState], None]: """ @@ -284,7 +265,6 @@ class LaserEVM: self._execute_pre_hook(op_code, global_state) try: - self._measure_coverage(global_state) new_global_states = Instruction( op_code, self.dynamic_loader, self.iprof ).evaluate(global_state) @@ -389,23 +369,6 @@ class LaserEVM: return new_global_states - def _measure_coverage(self, global_state: GlobalState) -> None: - """ - - :param global_state: - """ - code = global_state.environment.code.bytecode - number_of_instructions = len(global_state.environment.code.instruction_list) - instruction_index = global_state.mstate.pc - - if code not in self.coverage.keys(): - self.coverage[code] = ( - number_of_instructions, - [False] * number_of_instructions, - ) - - self.coverage[code][1][instruction_index] = True - def manage_cfg(self, opcode: str, new_states: List[GlobalState]) -> None: """ @@ -527,6 +490,10 @@ class LaserEVM: self._start_sym_exec_hooks.append(hook) elif hook_type == "stop_sym_exec": self._stop_sym_exec_hooks.append(hook) + elif hook_type == "start_sym_trans": + self._start_sym_trans_hooks.append(hook) + elif hook_type == "stop_sym_trans": + self._stop_sym_trans_hooks.append(hook) else: raise ValueError( "Invalid hook type %s. Must be one of {add_world_state}", hook_type diff --git a/mythril/laser/ethereum/taint_analysis.py b/mythril/laser/ethereum/taint_analysis.py deleted file mode 100644 index daf82c31..00000000 --- a/mythril/laser/ethereum/taint_analysis.py +++ /dev/null @@ -1,454 +0,0 @@ -"""This module implements classes needed to perform taint analysis.""" -import copy -import logging -from typing import List, Tuple, Union - -import mythril.laser.ethereum.util as helper -from mythril.analysis.symbolic import SymExecWrapper -from mythril.laser.ethereum.cfg import JumpType, Node -from mythril.laser.ethereum.state.environment import Environment -from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.smt import Expression - -log = logging.getLogger(__name__) - - -class TaintRecord: - """TaintRecord contains tainting information for a specific (state, node) - the information specifies the taint status before executing the operation - belonging to the state.""" - - def __init__(self): - """Builds a taint record.""" - self.stack = [] - self.memory = {} - self.storage = {} - self.states = [] - - def stack_tainted(self, index: int) -> Union[bool, None]: - """Returns taint value of stack element at index. - - :param index: - :return: - """ - if index < len(self.stack): - return self.stack[index] - return None - - def memory_tainted(self, index: int) -> bool: - """Returns taint value of memory element at index. - - :param index: - :return: - """ - if index in self.memory.keys(): - return self.memory[index] - return False - - def storage_tainted(self, index: int) -> bool: - """Returns taint value of storage element at index. - - :param index: - :return: - """ - if index in self.storage.keys(): - return self.storage[index] - return False - - def add_state(self, state: GlobalState) -> None: - """Adds state with this taint record. - - :param state: - """ - self.states.append(state) - - def clone(self) -> "TaintRecord": - """Clones this record. - - :return: - """ - clone = TaintRecord() - clone.stack = copy.deepcopy(self.stack) - clone.memory = copy.deepcopy(self.memory) - clone.storage = copy.deepcopy(self.storage) - return clone - - -class TaintResult: - """Taint analysis result obtained after having ran the taint runner.""" - - def __init__(self): - """Create a new tains result.""" - self.records = [] - - def check(self, state: GlobalState, stack_index: int) -> Union[bool, None]: - """Checks if stack variable is tainted, before executing the - instruction. - - :param state: state to check variable in - :param stack_index: index of stack variable - :return: tainted - """ - record = self._try_get_record(state) - if record is None: - return None - return record.stack_tainted(stack_index) - - def add_records(self, records: List[TaintRecord]) -> None: - """Adds records to this taint result. - - :param records: - """ - self.records += records - - def _try_get_record(self, state: GlobalState) -> Union[TaintRecord, None]: - """Finds record belonging to the state. - - :param state: - :return: - """ - for record in self.records: - if state in record.states: - return record - return None - - -class TaintRunner: - """Taint runner, is able to run taint analysis on symbolic execution - result.""" - - @staticmethod - def execute( - statespace: SymExecWrapper, node: Node, state: GlobalState, initial_stack=None - ) -> TaintResult: - """Runs taint analysis on the statespace. - - :param initial_stack: - :param statespace: symbolic statespace to run taint analysis on - :param node: taint introduction node - :param state: taint introduction state - :return: TaintResult object containing analysis results - """ - if initial_stack is None: - initial_stack = [] - result = TaintResult() - transaction_stack_length = len(node.states[0].transaction_stack) - # Build initial current_node - init_record = TaintRecord() - init_record.stack = initial_stack - - state_index = node.states.index(state) - - # List of (Node, TaintRecord, index) - current_nodes = [(node, init_record, state_index)] - environment = node.states[0].environment - - for node, record, index in current_nodes: - records = TaintRunner.execute_node(node, record, index) - - result.add_records(records) - if len(records) == 0: # continue if there is no record to work on - continue - children = TaintRunner.children( - node, statespace, environment, transaction_stack_length - ) - for child in children: - current_nodes.append((child, records[-1], 0)) - return result - - @staticmethod - def children( - node: Node, - statespace: SymExecWrapper, - environment: Environment, - transaction_stack_length: int, - ) -> List[Node]: - """ - - :param node: - :param statespace: - :param environment: - :param transaction_stack_length: - :return: - """ - direct_children = [ - statespace.nodes[edge.node_to] - for edge in statespace.edges - if edge.node_from == node.uid and edge.type != JumpType.Transaction - ] - children = [] - for child in direct_children: - if all( - len(state.transaction_stack) == transaction_stack_length - for state in child.states - ): - children.append(child) - elif all( - len(state.transaction_stack) > transaction_stack_length - for state in child.states - ): - children += TaintRunner.children( - child, statespace, environment, transaction_stack_length - ) - return children - - @staticmethod - def execute_node( - node: Node, last_record: TaintRecord, state_index=0 - ) -> List[TaintRecord]: - """Runs taint analysis on a given node. - - :param node: node to analyse - :param last_record: last taint record to work from - :param state_index: state index to start from - :return: List of taint records linked to the states in this node - """ - records = [last_record] - for index in range(state_index, len(node.states)): - current_state = node.states[index] - records.append(TaintRunner.execute_state(records[-1], current_state)) - return records[1:] - - @staticmethod - def execute_state(record: TaintRecord, state: GlobalState) -> TaintRecord: - """ - - :param record: - :param state: - :return: - """ - assert len(state.mstate.stack) == len(record.stack) - """ Runs taint analysis on a state """ - record.add_state(state) - new_record = record.clone() - - # Apply Change - op = state.get_current_instruction()["opcode"] - - if op in TaintRunner.stack_taint_table.keys(): - mutator = TaintRunner.stack_taint_table[op] - TaintRunner.mutate_stack(new_record, mutator) - elif op.startswith("PUSH"): - TaintRunner.mutate_push(op, new_record) - elif op.startswith("DUP"): - TaintRunner.mutate_dup(op, new_record) - elif op.startswith("SWAP"): - TaintRunner.mutate_swap(op, new_record) - elif op is "MLOAD": - TaintRunner.mutate_mload(new_record, state.mstate.stack[-1]) - elif op.startswith("MSTORE"): - TaintRunner.mutate_mstore(new_record, state.mstate.stack[-1]) - elif op is "SLOAD": - TaintRunner.mutate_sload(new_record, state.mstate.stack[-1]) - elif op is "SSTORE": - TaintRunner.mutate_sstore(new_record, state.mstate.stack[-1]) - elif op.startswith("LOG"): - TaintRunner.mutate_log(new_record, op) - elif op in ("CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"): - TaintRunner.mutate_call(new_record, op) - else: - log.debug("Unknown operation encountered: {}".format(op)) - - return new_record - - @staticmethod - def mutate_stack(record: TaintRecord, mutator: Tuple[int, int]) -> None: - """ - - :param record: - :param mutator: - """ - pop, push = mutator - - values = [] - for i in range(pop): - values.append(record.stack.pop()) - - taint = any(values) - - for i in range(push): - record.stack.append(taint) - - @staticmethod - def mutate_push(op: str, record: TaintRecord) -> None: - """ - - :param op: - :param record: - """ - TaintRunner.mutate_stack(record, (0, 1)) - - @staticmethod - def mutate_dup(op: str, record: TaintRecord) -> None: - """ - - :param op: - :param record: - """ - depth = int(op[3:]) - index = len(record.stack) - depth - record.stack.append(record.stack[index]) - - @staticmethod - def mutate_swap(op: str, record: TaintRecord) -> None: - """ - - :param op: - :param record: - """ - depth = int(op[4:]) - l = len(record.stack) - 1 - i = l - depth - record.stack[l], record.stack[i] = record.stack[i], record.stack[l] - - @staticmethod - def mutate_mload(record: TaintRecord, op0: Expression) -> None: - """ - - :param record: - :param op0: - :return: - """ - _ = record.stack.pop() - try: - index = helper.get_concrete_int(op0) - except TypeError: - log.debug("Can't MLOAD taint track symbolically") - record.stack.append(False) - return - - record.stack.append(record.memory_tainted(index)) - - @staticmethod - def mutate_mstore(record: TaintRecord, op0: Expression) -> None: - """ - - :param record: - :param op0: - :return: - """ - _, value_taint = record.stack.pop(), record.stack.pop() - try: - index = helper.get_concrete_int(op0) - except TypeError: - log.debug("Can't mstore taint track symbolically") - return - - record.memory[index] = value_taint - - @staticmethod - def mutate_sload(record: TaintRecord, op0: Expression) -> None: - """ - - :param record: - :param op0: - :return: - """ - _ = record.stack.pop() - try: - index = helper.get_concrete_int(op0) - except TypeError: - log.debug("Can't MLOAD taint track symbolically") - record.stack.append(False) - return - - record.stack.append(record.storage_tainted(index)) - - @staticmethod - def mutate_sstore(record: TaintRecord, op0: Expression) -> None: - """ - - :param record: - :param op0: - :return: - """ - _, value_taint = record.stack.pop(), record.stack.pop() - try: - index = helper.get_concrete_int(op0) - except TypeError: - log.debug("Can't mstore taint track symbolically") - return - - record.storage[index] = value_taint - - @staticmethod - def mutate_log(record: TaintRecord, op: str) -> None: - """ - - :param record: - :param op: - """ - depth = int(op[3:]) - for _ in range(depth + 2): - record.stack.pop() - - @staticmethod - def mutate_call(record: TaintRecord, op: str) -> None: - """ - - :param record: - :param op: - """ - pops = 6 - if op in ("CALL", "CALLCODE"): - pops += 1 - for _ in range(pops): - record.stack.pop() - - record.stack.append(False) - - stack_taint_table = { - # instruction: (taint source, taint target) - "POP": (1, 0), - "ADD": (2, 1), - "MUL": (2, 1), - "SUB": (2, 1), - "AND": (2, 1), - "OR": (2, 1), - "XOR": (2, 1), - "NOT": (1, 1), - "BYTE": (2, 1), - "DIV": (2, 1), - "MOD": (2, 1), - "SDIV": (2, 1), - "SMOD": (2, 1), - "ADDMOD": (3, 1), - "MULMOD": (3, 1), - "EXP": (2, 1), - "SIGNEXTEND": (2, 1), - "LT": (2, 1), - "GT": (2, 1), - "SLT": (2, 1), - "SGT": (2, 1), - "EQ": (2, 1), - "ISZERO": (1, 1), - "CALLVALUE": (0, 1), - "CALLDATALOAD": (1, 1), - "CALLDATACOPY": (3, 0), # todo - "CALLDATASIZE": (0, 1), - "ADDRESS": (0, 1), - "BALANCE": (1, 1), - "ORIGIN": (0, 1), - "CALLER": (0, 1), - "CODESIZE": (0, 1), - "SHA3": (2, 1), - "GASPRICE": (0, 1), - "CODECOPY": (3, 0), - "EXTCODESIZE": (1, 1), - "EXTCODECOPY": (4, 0), - "RETURNDATASIZE": (0, 1), - "BLOCKHASH": (1, 1), - "COINBASE": (0, 1), - "TIMESTAMP": (0, 1), - "NUMBER": (0, 1), - "DIFFICULTY": (0, 1), - "GASLIMIT": (0, 1), - "JUMP": (1, 0), - "JUMPI": (2, 0), - "PC": (0, 1), - "MSIZE": (0, 1), - "GAS": (0, 1), - "CREATE": (3, 1), - "CREATE2": (4, 1), - "RETURN": (2, 0), - } diff --git a/mythril/mythril/__init__.py b/mythril/mythril/__init__.py new file mode 100644 index 00000000..189a7812 --- /dev/null +++ b/mythril/mythril/__init__.py @@ -0,0 +1,4 @@ +from .mythril_disassembler import MythrilDisassembler +from .mythril_analyzer import MythrilAnalyzer +from .mythril_config import MythrilConfig +from .mythril_leveldb import MythrilLevelDB diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py new file mode 100644 index 00000000..e42e943c --- /dev/null +++ b/mythril/mythril/mythril_analyzer.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +import traceback +from typing import Optional, List + +from . import MythrilDisassembler +from mythril.support.source_support import Source +from mythril.support.loader import DynLoader +from mythril.analysis.symbolic import SymExecWrapper +from mythril.analysis.callgraph import generate_graph +from mythril.analysis.traceexplore import get_serializable_statespace +from mythril.analysis.security import fire_lasers, retrieve_callback_issues +from mythril.analysis.report import Report, Issue +from mythril.ethereum.evmcontract import EVMContract +from mythril.laser.smt import SolverStatistics +from mythril.support.start_time import StartTime + +log = logging.getLogger(__name__) + + +class MythrilAnalyzer: + """ + The Mythril Analyzer class + Responsible for the analysis of the smart contracts + """ + + def __init__( + self, + disassembler: MythrilDisassembler, + requires_dynld: bool = False, + onchain_storage_access: bool = True, + strategy: str = "dfs", + address: Optional[str] = None, + max_depth: Optional[int] = None, + execution_timeout: Optional[int] = None, + create_timeout: Optional[int] = None, + enable_iprof: bool = False, + ): + """ + + :param disassembler: The MythrilDisassembler class + :param requires_dynld: whether dynamic loading should be done or not + :param onchain_storage_access: Whether onchain access should be done or not + """ + self.eth = disassembler.eth + self.contracts = disassembler.contracts or [] # type: List[EVMContract] + self.enable_online_lookup = disassembler.enable_online_lookup + self.dynld = requires_dynld + self.onchain_storage_access = onchain_storage_access + self.strategy = strategy + self.address = address + self.max_depth = max_depth + self.execution_timeout = execution_timeout + self.create_timeout = create_timeout + self.enable_iprof = enable_iprof + + def dump_statespace(self, contract: EVMContract = None) -> str: + """ + Returns serializable statespace of the contract + :param contract: The Contract on which the analysis should be done + :return: The serialized state space + """ + sym = SymExecWrapper( + contract or self.contracts[0], + self.address, + self.strategy, + dynloader=DynLoader( + self.eth, + storage_loading=self.onchain_storage_access, + contract_loading=self.dynld, + ), + max_depth=self.max_depth, + execution_timeout=self.execution_timeout, + create_timeout=self.create_timeout, + enable_iprof=self.enable_iprof, + ) + + return get_serializable_statespace(sym) + + def graph_html( + self, + contract: EVMContract = None, + enable_physics: bool = False, + phrackify: bool = False, + transaction_count: Optional[int] = None, + ) -> str: + """ + + :param contract: The Contract on which the analysis should be done + :param enable_physics: If true then enables the graph physics simulation + :param phrackify: If true generates Phrack-style call graph + :param transaction_count: The amount of transactions to be executed + :return: The generated graph in html format + """ + sym = SymExecWrapper( + contract or self.contracts[0], + self.address, + self.strategy, + dynloader=DynLoader( + self.eth, + storage_loading=self.onchain_storage_access, + contract_loading=self.dynld, + ), + max_depth=self.max_depth, + execution_timeout=self.execution_timeout, + transaction_count=transaction_count, + create_timeout=self.create_timeout, + enable_iprof=self.enable_iprof, + ) + return generate_graph(sym, physics=enable_physics, phrackify=phrackify) + + def fire_lasers( + self, + modules: Optional[List[str]] = None, + verbose_report: bool = False, + transaction_count: Optional[int] = None, + ) -> Report: + """ + :param modules: The analysis modules which should be executed + :param verbose_report: Gives out the transaction sequence of the vulnerability + :param transaction_count: The amount of transactions to be executed + :return: The Report class which contains the all the issues/vulnerabilities + """ + all_issues = [] # type: List[Issue] + SolverStatistics().enabled = True + exceptions = [] + for contract in self.contracts: + StartTime() # Reinitialize start time for new contracts + try: + sym = SymExecWrapper( + contract, + self.address, + self.strategy, + dynloader=DynLoader( + self.eth, + storage_loading=self.onchain_storage_access, + contract_loading=self.dynld, + ), + max_depth=self.max_depth, + execution_timeout=self.execution_timeout, + create_timeout=self.create_timeout, + transaction_count=transaction_count, + modules=modules, + compulsory_statespace=False, + enable_iprof=self.enable_iprof, + ) + issues = fire_lasers(sym, modules) + except KeyboardInterrupt: + log.critical("Keyboard Interrupt") + issues = retrieve_callback_issues(modules) + except Exception: + log.critical( + "Exception occurred, aborting analysis. Please report this issue to the Mythril GitHub page.\n" + + traceback.format_exc() + ) + issues = retrieve_callback_issues(modules) + exceptions.append(traceback.format_exc()) + for issue in issues: + issue.add_code_info(contract) + + all_issues += issues + log.info("Solver statistics: \n{}".format(str(SolverStatistics()))) + + source_data = Source() + source_data.get_source_from_contracts_list(self.contracts) + # Finally, output the results + report = Report(verbose_report, source_data, exceptions=exceptions) + for issue in all_issues: + report.append_issue(issue) + + return report diff --git a/mythril/mythril/mythril_config.py b/mythril/mythril/mythril_config.py new file mode 100644 index 00000000..f2ad1217 --- /dev/null +++ b/mythril/mythril/mythril_config.py @@ -0,0 +1,226 @@ +import codecs +import logging +import os +import platform +import re + +from pathlib import Path +from shutil import copyfile +from configparser import ConfigParser +from typing import Optional + +from mythril.exceptions import CriticalError +from mythril.ethereum.interface.rpc.client import EthJsonRpc +from mythril.ethereum.interface.leveldb.client import EthLevelDB + +log = logging.getLogger(__name__) + + +class MythrilConfig: + """ + The Mythril Analyzer class + Responsible for setup of the mythril environment + """ + + def __init__(self): + self.mythril_dir = self._init_mythril_dir() + self.config_path = os.path.join(self.mythril_dir, "config.ini") + self.leveldb_dir = None + self._init_config() + self.eth = None # type: Optional[EthJsonRpc] + self.eth_db = None # type: Optional[EthLevelDB] + + @staticmethod + def _init_mythril_dir() -> str: + """ + Initializes the mythril dir and config.ini file + :return: The mythril dir's path + """ + + try: + mythril_dir = os.environ["MYTHRIL_DIR"] + except KeyError: + mythril_dir = os.path.join(os.path.expanduser("~"), ".mythril") + + if not os.path.exists(mythril_dir): + # Initialize data directory + log.info("Creating mythril data directory") + os.mkdir(mythril_dir) + + db_path = str(Path(mythril_dir) / "signatures.db") + if not os.path.exists(db_path): + # if the default mythril dir doesn't contain a signature DB + # initialize it with the default one from the project root + asset_dir = Path(__file__).parent.parent / "support" / "assets" + copyfile(str(asset_dir / "signatures.db"), db_path) + + return mythril_dir + + def _init_config(self): + """If no config file exists, create it and add default options. + Defaults:- + - Default LevelDB path is specified based on OS + - dynamic loading is set to infura by default in the file + This function also sets self.leveldb_dir path + """ + + leveldb_default_path = self._get_default_leveldb_path() + + if not os.path.exists(self.config_path): + log.info("No config file found. Creating default: " + self.config_path) + open(self.config_path, "a").close() + + config = ConfigParser(allow_no_value=True) + + config.optionxform = str + config.read(self.config_path, "utf-8") + if "defaults" not in config.sections(): + self._add_default_options(config) + + if not config.has_option("defaults", "leveldb_dir"): + self._add_leveldb_option(config, leveldb_default_path) + + if not config.has_option("defaults", "dynamic_loading"): + self._add_dynamic_loading_option(config) + + with codecs.open(self.config_path, "w", "utf-8") as fp: + config.write(fp) + + leveldb_dir = config.get( + "defaults", "leveldb_dir", fallback=leveldb_default_path + ) + self.leveldb_dir = os.path.expanduser(leveldb_dir) + + @staticmethod + def _get_default_leveldb_path() -> str: + """ + Returns the LevelDB path + :return: The LevelDB path + """ + system = platform.system().lower() + leveldb_fallback_dir = os.path.expanduser("~") + if system.startswith("darwin"): + leveldb_fallback_dir = os.path.join( + leveldb_fallback_dir, "Library", "Ethereum" + ) + elif system.startswith("windows"): + leveldb_fallback_dir = os.path.join( + leveldb_fallback_dir, "AppData", "Roaming", "Ethereum" + ) + else: + leveldb_fallback_dir = os.path.join(leveldb_fallback_dir, ".ethereum") + return os.path.join(leveldb_fallback_dir, "geth", "chaindata") + + @staticmethod + def _add_default_options(config: ConfigParser) -> None: + """ + Adds defaults option to config.ini + :param config: The config file object + :return: None + """ + config.add_section("defaults") + + @staticmethod + def _add_leveldb_option(config: ConfigParser, leveldb_fallback_dir: str) -> None: + """ + Sets a default leveldb path in .mythril/config.ini file + :param config: The config file object + :param leveldb_fallback_dir: The leveldb dir to use by default for searches + :return: None + """ + config.set("defaults", "#Default chaindata locations:", "") + config.set("defaults", "#– Mac: ~/Library/Ethereum/geth/chaindata", "") + config.set("defaults", "#– Linux: ~/.ethereum/geth/chaindata", "") + config.set( + "defaults", + "#– Windows: %USERPROFILE%\\AppData\\Roaming\\Ethereum\\geth\\chaindata", + "", + ) + config.set("defaults", "leveldb_dir", leveldb_fallback_dir) + + @staticmethod + def _add_dynamic_loading_option(config: ConfigParser) -> None: + """ + Sets the dynamic loading config option in .mythril/config.ini file + :param config: The config file object + :return: None + """ + config.set( + "defaults", "#– To connect to Infura use dynamic_loading: infura", "" + ) + config.set( + "defaults", + "#– To connect to Rpc use " + "dynamic_loading: HOST:PORT / ganache / infura-[network_name]", + "", + ) + config.set( + "defaults", "#– To connect to local host use dynamic_loading: localhost", "" + ) + config.set("defaults", "dynamic_loading", "infura") + + def set_api_leveldb(self, leveldb_path: str) -> None: + """ + """ + self.eth_db = EthLevelDB(leveldb_path) + + def set_api_rpc_infura(self) -> None: + """Set the RPC mode to INFURA on Mainnet.""" + log.info("Using INFURA Main Net for RPC queries") + self.eth = EthJsonRpc("mainnet.infura.io", 443, True) + + def set_api_rpc(self, rpc: str = None, rpctls: bool = False) -> None: + """ + Sets the RPC mode to either of ganache or infura + :param rpc: either of the strings - ganache, infura-mainnet, infura-rinkeby, infura-kovan, infura-ropsten + """ + if rpc == "ganache": + rpcconfig = ("localhost", 8545, False) + else: + m = re.match(r"infura-(.*)", rpc) + if m and m.group(1) in ["mainnet", "rinkeby", "kovan", "ropsten"]: + rpcconfig = (m.group(1) + ".infura.io", 443, True) + else: + try: + host, port = rpc.split(":") + rpcconfig = (host, int(port), rpctls) + except ValueError: + raise CriticalError( + "Invalid RPC argument, use 'ganache', 'infura-[network]' or 'HOST:PORT'" + ) + + if rpcconfig: + log.info("Using RPC settings: %s" % str(rpcconfig)) + self.eth = EthJsonRpc(rpcconfig[0], int(rpcconfig[1]), rpcconfig[2]) + else: + raise CriticalError("Invalid RPC settings, check help for details.") + + def set_api_rpc_localhost(self) -> None: + """Set the RPC mode to a local instance.""" + log.info("Using default RPC settings: http://localhost:8545") + self.eth = EthJsonRpc("localhost", 8545) + + def set_api_from_config_path(self) -> None: + """Set the RPC mode based on a given config file.""" + config = ConfigParser(allow_no_value=False) + # TODO: Remove this after this issue https://github.com/python/mypy/issues/2427 is closed + config.optionxform = str # type:ignore + config.read(self.config_path, "utf-8") + if config.has_option("defaults", "dynamic_loading"): + dynamic_loading = config.get("defaults", "dynamic_loading") + else: + dynamic_loading = "infura" + self._set_rpc(dynamic_loading) + + def _set_rpc(self, rpc_type: str) -> None: + """ + Sets rpc based on the type + :param rpc_type: The type of connection: like infura, ganache, localhost + :return: + """ + if rpc_type == "infura": + self.set_api_rpc_infura() + elif rpc_type == "localhost": + self.set_api_rpc_localhost() + else: + self.set_api_rpc(rpc_type) diff --git a/mythril/mythril/mythril_disassembler.py b/mythril/mythril/mythril_disassembler.py new file mode 100644 index 00000000..5e1e72c3 --- /dev/null +++ b/mythril/mythril/mythril_disassembler.py @@ -0,0 +1,303 @@ +import logging +import re +import solc +import os + +from ethereum import utils +from solc.exceptions import SolcError +from typing import List, Tuple, Optional +from mythril.ethereum import util +from mythril.ethereum.interface.rpc.client import EthJsonRpc +from mythril.exceptions import CriticalError, CompilerError, NoContractFoundError +from mythril.support import signatures +from mythril.support.truffle import analyze_truffle_project +from mythril.ethereum.evmcontract import EVMContract +from mythril.ethereum.interface.rpc.exceptions import ConnectionError +from mythril.solidity.soliditycontract import SolidityContract, get_contracts_from_file + +log = logging.getLogger(__name__) + + +class MythrilDisassembler: + """ + The Mythril Disassembler class + Responsible for generating disassembly of smart contracts + - Compiles solc code from file/onchain + - Can also be used to access onchain storage data + """ + + def __init__( + self, + eth: Optional[EthJsonRpc] = None, + solc_version: str = None, + solc_args: str = None, + enable_online_lookup: bool = False, + ) -> None: + self.solc_binary = self._init_solc_binary(solc_version) + self.solc_args = solc_args + self.eth = eth + self.enable_online_lookup = enable_online_lookup + self.sigs = signatures.SignatureDB(enable_online_lookup=enable_online_lookup) + self.contracts = [] # type: List[EVMContract] + + @staticmethod + def _init_solc_binary(version: str) -> str: + """ + Only proper versions are supported. No nightlies, commits etc (such as available in remix). + :param version: Version of the solc binary required + :return: The solc binary of the corresponding version + """ + + if not version: + return os.environ.get("SOLC") or "solc" + + # tried converting input to semver, seemed not necessary so just slicing for now + main_version = solc.main.get_solc_version_string() + main_version_number = re.match(r"\d+.\d+.\d+", main_version) + if main_version is None: + raise CriticalError( + "Could not extract solc version from string {}".format(main_version) + ) + if version == main_version_number: + log.info("Given version matches installed version") + solc_binary = os.environ.get("SOLC") or "solc" + else: + solc_binary = util.solc_exists(version) + if solc_binary: + log.info("Given version is already installed") + else: + try: + solc.install_solc("v" + version) + solc_binary = util.solc_exists(version) + if not solc_binary: + raise SolcError() + except SolcError: + raise CriticalError( + "There was an error when trying to install the specified solc version" + ) + + log.info("Setting the compiler to %s", solc_binary) + + return solc_binary + + def load_from_bytecode( + self, code: str, bin_runtime: bool = False, address: Optional[str] = None + ) -> Tuple[str, EVMContract]: + """ + Returns the address and the contract class for the given bytecode + :param code: Bytecode + :param bin_runtime: Whether the code is runtime code or creation code + :param address: address of contract + :return: tuple(address, Contract class) + """ + if address is None: + address = util.get_indexed_address(0) + if bin_runtime: + self.contracts.append( + EVMContract( + code=code, + name="MAIN", + enable_online_lookup=self.enable_online_lookup, + ) + ) + else: + self.contracts.append( + EVMContract( + creation_code=code, + name="MAIN", + enable_online_lookup=self.enable_online_lookup, + ) + ) + return address, self.contracts[-1] # return address and contract object + + def load_from_address(self, address: str) -> Tuple[str, EVMContract]: + """ + Returns the contract given it's on chain address + :param address: The on chain address of a contract + :return: tuple(address, contract) + """ + if not re.match(r"0x[a-fA-F0-9]{40}", address): + raise CriticalError("Invalid contract address. Expected format is '0x...'.") + + try: + code = self.eth.eth_getCode(address) + except FileNotFoundError as e: + raise CriticalError("IPC error: " + str(e)) + except ConnectionError: + raise CriticalError( + "Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly." + ) + except Exception as e: + raise CriticalError("IPC / RPC error: " + str(e)) + + if code == "0x" or code == "0x0": + raise CriticalError( + "Received an empty response from eth_getCode. Check the contract address and verify that you are on the correct chain." + ) + else: + self.contracts.append( + EVMContract( + code, name=address, enable_online_lookup=self.enable_online_lookup + ) + ) + return address, self.contracts[-1] # return address and contract object + + def load_from_solidity( + self, solidity_files: List[str] + ) -> Tuple[str, List[SolidityContract]]: + """ + + :param solidity_files: List of solidity_files + :return: tuple of address, contract class list + """ + address = util.get_indexed_address(0) + contracts = [] + for file in solidity_files: + if ":" in file: + file, contract_name = file.split(":") + else: + contract_name = None + + file = os.path.expanduser(file) + + try: + # import signatures from solidity source + self.sigs.import_solidity_file( + file, solc_binary=self.solc_binary, solc_args=self.solc_args + ) + if contract_name is not None: + contract = SolidityContract( + input_file=file, + name=contract_name, + solc_args=self.solc_args, + solc_binary=self.solc_binary, + ) + self.contracts.append(contract) + contracts.append(contract) + else: + for contract in get_contracts_from_file( + input_file=file, + solc_args=self.solc_args, + solc_binary=self.solc_binary, + ): + self.contracts.append(contract) + contracts.append(contract) + + except FileNotFoundError: + raise CriticalError("Input file not found: " + file) + except CompilerError as e: + raise CriticalError(e) + except NoContractFoundError: + log.error( + "The file " + file + " does not contain a compilable contract." + ) + + return address, contracts + + def analyze_truffle_project(self, *args, **kwargs) -> None: + """ + :param args: + :param kwargs: + :return: + """ + analyze_truffle_project( + self.sigs, *args, **kwargs + ) # just passthru by passing signatures for now + + @staticmethod + def hash_for_function_signature(func: str) -> str: + """ + Return function names corresponding signature hash + :param func: function name + :return: Its hash signature + """ + return "0x%s" % utils.sha3(func)[:4].hex() + + def get_state_variable_from_storage( + self, address: str, params: Optional[List[str]] = None + ) -> str: + """ + Get variables from the storage + :param address: The contract address + :param params: The list of parameters + param types: [position, length] or ["mapping", position, key1, key2, ... ] + or [position, length, array] + :return: The corresponding storage slot and its value + """ + params = params or [] + (position, length, mappings) = (0, 1, []) + try: + if params[0] == "mapping": + if len(params) < 3: + raise CriticalError("Invalid number of parameters.") + position = int(params[1]) + position_formatted = utils.zpad(utils.int_to_big_endian(position), 32) + for i in range(2, len(params)): + key = bytes(params[i], "utf8") + key_formatted = utils.rzpad(key, 32) + mappings.append( + int.from_bytes( + utils.sha3(key_formatted + position_formatted), + byteorder="big", + ) + ) + + length = len(mappings) + if length == 1: + position = mappings[0] + + else: + if len(params) >= 4: + raise CriticalError("Invalid number of parameters.") + + if len(params) >= 1: + position = int(params[0]) + if len(params) >= 2: + length = int(params[1]) + if len(params) == 3 and params[2] == "array": + position_formatted = utils.zpad( + utils.int_to_big_endian(position), 32 + ) + position = int.from_bytes( + utils.sha3(position_formatted), byteorder="big" + ) + + except ValueError: + raise CriticalError( + "Invalid storage index. Please provide a numeric value." + ) + + outtxt = [] + + try: + if length == 1: + outtxt.append( + "{}: {}".format( + position, self.eth.eth_getStorageAt(address, position) + ) + ) + else: + if len(mappings) > 0: + for i in range(0, len(mappings)): + position = mappings[i] + outtxt.append( + "{}: {}".format( + hex(position), + self.eth.eth_getStorageAt(address, position), + ) + ) + else: + for i in range(position, position + length): + outtxt.append( + "{}: {}".format( + hex(i), self.eth.eth_getStorageAt(address, i) + ) + ) + except FileNotFoundError as e: + raise CriticalError("IPC error: " + str(e)) + except ConnectionError: + raise CriticalError( + "Could not connect to RPC server. " + "Make sure that your node is running and that RPC parameters are set correctly." + ) + return "\n".join(outtxt) diff --git a/mythril/mythril/mythril_leveldb.py b/mythril/mythril/mythril_leveldb.py new file mode 100644 index 00000000..7ab78f56 --- /dev/null +++ b/mythril/mythril/mythril_leveldb.py @@ -0,0 +1,49 @@ +import re +from mythril.exceptions import CriticalError + + +class MythrilLevelDB: + """ + Class which does search operations on leveldb + There are two DBs + 1) Key value pairs of hashes and it's corresponding address + 2) The LevelDB Trie + """ + + def __init__(self, leveldb): + """ + + :param leveldb: Leveldb path + """ + self.leveldb = leveldb + + def search_db(self, search): + """ + Searches the corresponding code + :param search: The code part to be searched + """ + + def search_callback(_, address, balance): + """ + + :param _: + :param address: The address of the contract with the code in search + :param balance: The balance of the corresponding contract + """ + print("Address: " + address + ", balance: " + str(balance)) + + try: + self.leveldb.search(search, search_callback) + + except SyntaxError: + raise CriticalError("Syntax error in search expression.") + + def contract_hash_to_address(self, contract_hash): + """ + Returns address of the corresponding hash by searching the leveldb + :param contract_hash: Hash to be searched + """ + if not re.match(r"0x[a-fA-F0-9]{64}", contract_hash): + raise CriticalError("Invalid address hash. Expected format is '0x...'.") + + print(self.leveldb.contract_hash_to_address(contract_hash)) diff --git a/mythril/support/loader.py b/mythril/support/loader.py index 6e434439..f46feff5 100644 --- a/mythril/support/loader.py +++ b/mythril/support/loader.py @@ -58,17 +58,15 @@ class DynLoader: return data - def dynld(self, contract_address, dependency_address): + def dynld(self, dependency_address): """ - - :param contract_address: :param dependency_address: :return: """ if not self.contract_loading: raise ValueError("Cannot load contract when contract_loading flag is false") - log.debug("Dynld at contract " + contract_address + ": " + dependency_address) + log.debug("Dynld at contract " + dependency_address) # Ensure that dependency_address is the correct length, with 0s prepended as needed. dependency_address = ( diff --git a/mythril/version.py b/mythril/version.py index 2754f21b..07cb42e1 100644 --- a/mythril/version.py +++ b/mythril/version.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -VERSION = "v0.20.2" # NOQA +VERSION = "v0.20.3" # NOQA diff --git a/requirements.txt b/requirements.txt index 2189f7cc..c22d72d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,4 @@ rlp>=1.0.1 transaction>=2.2.1 z3-solver-mythril>=4.8.4.1 pysha3 +matplotlib diff --git a/setup.py b/setup.py index 1f0a0a02..67d7f39a 100755 --- a/setup.py +++ b/setup.py @@ -97,6 +97,7 @@ setup( "configparser>=3.5.0", "persistent>=4.2.0", "ethereum-input-decoder>=0.2.2", + "matplotlib", ], tests_require=["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"], python_requires=">=3.5", diff --git a/tests/graph_test.py b/tests/graph_test.py index ba84f7da..1e1e3350 100644 --- a/tests/graph_test.py +++ b/tests/graph_test.py @@ -1,5 +1,5 @@ from mythril.analysis.callgraph import generate_graph -from mythril.analysis.symbolic import SymExecWrapper +from mythril.mythril import MythrilAnalyzer, MythrilDisassembler from mythril.ethereum import util from mythril.solidity.soliditycontract import EVMContract from tests import ( @@ -22,16 +22,17 @@ class GraphTest(BaseTestCase): ) contract = EVMContract(input_file.read_text()) - - sym = SymExecWrapper( - contract, - address=(util.get_indexed_address(0)), + disassembler = MythrilDisassembler() + disassembler.contracts.append(contract) + analyzer = MythrilAnalyzer( + disassembler=disassembler, strategy="dfs", - transaction_count=1, execution_timeout=5, + max_depth=30, + address=(util.get_indexed_address(0)), ) - html = generate_graph(sym) + html = analyzer.graph_html(transaction_count=1) output_current.write_text(html) lines_expected = re.findall( diff --git a/tests/laser/transaction/create_transaction_test.py b/tests/laser/transaction/create_transaction_test.py index 10a1f837..5c4e93ad 100644 --- a/tests/laser/transaction/create_transaction_test.py +++ b/tests/laser/transaction/create_transaction_test.py @@ -1,4 +1,4 @@ -from mythril.mythril import Mythril +from mythril.mythril import MythrilDisassembler from mythril.laser.ethereum.transaction import execute_contract_creation from mythril.ethereum import util import mythril.laser.ethereum.svm as svm @@ -13,7 +13,7 @@ from mythril.analysis.symbolic import SymExecWrapper def test_create(): contract = SolidityContract( str(tests.TESTDATA_INPUTS_CONTRACTS / "calls.sol"), - solc_binary=Mythril._init_solc_binary("0.5.0"), + solc_binary=MythrilDisassembler._init_solc_binary("0.5.0"), ) laser_evm = svm.LaserEVM({}) @@ -37,7 +37,7 @@ def test_create(): def test_sym_exec(): contract = SolidityContract( str(tests.TESTDATA_INPUTS_CONTRACTS / "calls.sol"), - solc_binary=Mythril._init_solc_binary("0.5.0"), + solc_binary=MythrilDisassembler._init_solc_binary("0.5.0"), ) sym = SymExecWrapper( diff --git a/tests/mythril/mythril_analyzer_test.py b/tests/mythril/mythril_analyzer_test.py new file mode 100644 index 00000000..0e826cc8 --- /dev/null +++ b/tests/mythril/mythril_analyzer_test.py @@ -0,0 +1,31 @@ +from pathlib import Path +from mythril.mythril import MythrilDisassembler, MythrilAnalyzer +from mythril.analysis.report import Issue +from mock import patch + + +@patch("mythril.analysis.report.Issue.add_code_info", return_value=None) +@patch( + "mythril.mythril.mythril_analyzer.fire_lasers", + return_value=[Issue("", "", "234", "101", "title", "0x02445")], +) +@patch("mythril.mythril.mythril_analyzer.SymExecWrapper", return_value=None) +def test_fire_lasers(mock_sym, mock_fire_lasers, mock_code_info): + disassembler = MythrilDisassembler(eth=None) + disassembler.load_from_solidity( + [ + str( + ( + Path(__file__).parent.parent / "testdata/input_contracts/origin.sol" + ).absolute() + ) + ] + ) + analyzer = MythrilAnalyzer(disassembler, strategy="dfs") + + issues = analyzer.fire_lasers(modules=[]).sorted_issues() + mock_sym.assert_called() + mock_fire_lasers.assert_called() + mock_code_info.assert_called() + assert len(issues) == 1 + assert issues[0]["swc-id"] == "101" diff --git a/tests/mythril/mythril_config_test.py b/tests/mythril/mythril_config_test.py new file mode 100644 index 00000000..19c23c39 --- /dev/null +++ b/tests/mythril/mythril_config_test.py @@ -0,0 +1,58 @@ +import pytest + +from configparser import ConfigParser +from pathlib import Path + +from mythril.mythril import MythrilConfig +from mythril.exceptions import CriticalError + + +def test_config_path_dynloading(): + config = MythrilConfig() + config.config_path = str( + Path(__file__).parent.parent / "testdata/mythril_config_inputs/config.ini" + ) + config.set_api_from_config_path() + assert config.eth.host == "mainnet.infura.io" + assert config.eth.port == 443 + + +rpc_types_tests = [ + ("infura", "mainnet.infura.io", 443, True), + ("ganache", "localhost", 8545, True), + ("infura-rinkeby", "rinkeby.infura.io", 443, True), + ("infura-ropsten", "ropsten.infura.io", 443, True), + ("infura-kovan", "kovan.infura.io", 443, True), + ("localhost", "localhost", 8545, True), + ("localhost:9022", "localhost", 9022, True), + ("pinfura", None, None, False), + ("infura-finkeby", None, None, False), +] + + +@pytest.mark.parametrize("rpc_type,host,port,success", rpc_types_tests) +def test_set_rpc(rpc_type, host, port, success): + config = MythrilConfig() + if success: + config._set_rpc(rpc_type) + assert config.eth.host == host + assert config.eth.port == port + else: + with pytest.raises(CriticalError): + config._set_rpc(rpc_type) + + +def test_leveldb_config_addition(): + config = ConfigParser() + config.add_section("defaults") + MythrilConfig._add_leveldb_option(config, "test") + assert config.has_section("defaults") + assert config.get("defaults", "leveldb_dir") == "test" + + +def test_dynld_config_addition(): + config = ConfigParser() + config.add_section("defaults") + MythrilConfig._add_dynamic_loading_option(config) + assert config.has_section("defaults") + assert config.get("defaults", "dynamic_loading") == "infura" diff --git a/tests/mythril/mythril_disassembler_test.py b/tests/mythril/mythril_disassembler_test.py new file mode 100644 index 00000000..8433128e --- /dev/null +++ b/tests/mythril/mythril_disassembler_test.py @@ -0,0 +1,70 @@ +import pytest +from mythril.mythril import MythrilConfig, MythrilDisassembler +from mythril.exceptions import CriticalError + +storage_test = [ + ( + ["438767356", "3"], + [ + "0x1a270efc: 0x0000000000000000000000000000000000000000000000000000000000000000", + "0x1a270efd: 0x0000000000000000000000000000000000000000000000000000000000000000", + "0x1a270efe: 0x0000000000000000000000000000000000000000000000000000000000000000", + ], + ), + ( + ["mapping", "4588934759847", "1", "2"], + [ + "0x7e523d5aeb10cdb378b0b1f76138c28063a2cb9ec8ff710f42a0972f4d53cf44: " + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xba36da34ceec88853a2ebdde88e023c6919b90348f41e8905b422dc9ce22301c: " + "0x0000000000000000000000000000000000000000000000000000000000000000", + ], + ), + ( + ["mapping", "4588934759847", "10"], + [ + "45998575720532480608987132552042185415362901038635143236141343153058112000553: " + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + ), + ( + ["4588934759847", "1", "array"], + [ + "30699902832541380821728647136767910246735388184559883985790189062258823875816: " + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + ), +] + + +@pytest.mark.parametrize("params,ans", storage_test) +def test_get_data_from_storage(params, ans): + config = MythrilConfig() + config.set_api_rpc_infura() + disassembler = MythrilDisassembler(eth=config.eth, solc_version="0.4.23") + outtext = disassembler.get_state_variable_from_storage( + "0x76799f77587738bfeef09452df215b63d2cfb08a", params + ).split("\n") + assert outtext == ans + + +storage_test_incorrect_params = [ + (["1", "2", "3", "4"]), + (["mapping", "1"]), + (["a", "b", "c"]), +] + + +@pytest.mark.parametrize("params", storage_test_incorrect_params) +def test_get_data_from_storage_incorrect_params(params): + config = MythrilConfig() + config.set_api_rpc_infura() + disassembler = MythrilDisassembler(eth=config.eth, solc_version="0.4.23") + with pytest.raises(CriticalError): + disassembler.get_state_variable_from_storage( + "0x76799f77587738bfeef09452df215b63d2cfb08a", params + ) + + +def test_solc_install(): + MythrilDisassembler(eth=None, solc_version="0.4.19") diff --git a/tests/mythril/mythril_leveldb_test.py b/tests/mythril/mythril_leveldb_test.py new file mode 100644 index 00000000..73e46827 --- /dev/null +++ b/tests/mythril/mythril_leveldb_test.py @@ -0,0 +1,51 @@ +import io +import pytest +from contextlib import redirect_stdout +from mock import patch + +from mythril.mythril import MythrilLevelDB, MythrilConfig +from mythril.exceptions import CriticalError + + +@patch("mythril.ethereum.interface.leveldb.client.EthLevelDB.search") +@patch("mythril.ethereum.interface.leveldb.client.ETH_DB", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBReader", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBWriter", return_value=None) +def test_leveldb_code_search(mock_leveldb, f1, f2, f3): + config = MythrilConfig() + config.set_api_leveldb("some path") + leveldb_search = MythrilLevelDB(leveldb=config.eth_db) + leveldb_search.search_db("code#PUSH#") + mock_leveldb.assert_called() + + +@patch("mythril.ethereum.interface.leveldb.client.ETH_DB", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBReader", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBWriter", return_value=None) +def test_leveldb_hash_search_incorrect_input(f1, f2, f3): + config = MythrilConfig() + config.set_api_leveldb("some path") + leveldb_search = MythrilLevelDB(leveldb=config.eth_db) + with pytest.raises(CriticalError): + leveldb_search.contract_hash_to_address("0x23") + + +@patch( + "mythril.ethereum.interface.leveldb.client.EthLevelDB.contract_hash_to_address", + return_value="0xddbb615cb2ffaff7233d8a6f3601621de94795e1", +) +@patch("mythril.ethereum.interface.leveldb.client.ETH_DB", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBReader", return_value=None) +@patch("mythril.ethereum.interface.leveldb.client.LevelDBWriter", return_value=None) +def test_leveldb_hash_search_correct_input(mock_hash_to_address, f1, f2, f3): + config = MythrilConfig() + config.set_api_leveldb("some path") + leveldb_search = MythrilLevelDB(leveldb=config.eth_db) + f = io.StringIO() + with redirect_stdout(f): + leveldb_search.contract_hash_to_address( + "0x0464e651bcc40de28fc7fcde269218d16850bac9689da5f4a6bd640fd3cdf6aa" + ) + out = f.getvalue() + mock_hash_to_address.assert_called() + assert out == "0xddbb615cb2ffaff7233d8a6f3601621de94795e1\n" diff --git a/tests/native_test.py b/tests/native_test.py index 3d0438ff..c58a25e3 100644 --- a/tests/native_test.py +++ b/tests/native_test.py @@ -1,5 +1,5 @@ from mythril.solidity.soliditycontract import SolidityContract -from mythril.mythril import Mythril +from mythril.mythril import MythrilDisassembler from mythril.laser.ethereum.state.account import Account from mythril.laser.ethereum.state.machine_state import MachineState from mythril.laser.ethereum.state.global_state import GlobalState @@ -84,7 +84,8 @@ class NativeTests(BaseTestCase): def runTest(): """""" disassembly = SolidityContract( - "./tests/native_tests.sol", solc_binary=Mythril._init_solc_binary("0.5.0") + "./tests/native_tests.sol", + solc_binary=MythrilDisassembler._init_solc_binary("0.5.0"), ).disassembly account = Account("0x0000000000000000000000000000000000000000", disassembly) accounts = {account.address: account} diff --git a/tests/report_test.py b/tests/report_test.py index 5a1642b9..30e9cabd 100644 --- a/tests/report_test.py +++ b/tests/report_test.py @@ -21,6 +21,13 @@ def _fix_debug_data(json_str): return json.dumps(read_json, sort_keys=True, indent=4) +def _add_jsonv2_stubs(json_str): + read_json = json.loads(json_str) + for issue in read_json[0]["issues"]: + issue["extra"]["discoveryTime"] = "" + return json.dumps(read_json, sort_keys=True, indent=4) + + def _generate_report(input_file): contract = EVMContract(input_file.read_text(), enable_online_lookup=False) sym = SymExecWrapper( @@ -181,7 +188,9 @@ def test_text_report(reports): def test_jsonv2_report(reports): _assert_empty_json( _get_changed_files_json( - lambda report: _fix_path(report.as_swc_standard_format()).strip(), + lambda report: _fix_path( + _add_jsonv2_stubs(report.as_swc_standard_format()) + ).strip(), reports, ".jsonv2", ), diff --git a/tests/solidity_contract_test.py b/tests/solidity_contract_test.py index 6b55aad1..4edb8e6e 100644 --- a/tests/solidity_contract_test.py +++ b/tests/solidity_contract_test.py @@ -1,6 +1,6 @@ from pathlib import Path -from mythril.mythril import Mythril +from mythril.mythril import MythrilDisassembler from mythril.solidity.soliditycontract import SolidityContract from tests import BaseTestCase @@ -11,7 +11,7 @@ class SolidityContractTest(BaseTestCase): def test_get_source_info_without_name_gets_latest_contract_info(self): input_file = TEST_FILES / "multi_contracts.sol" contract = SolidityContract( - str(input_file), solc_binary=Mythril._init_solc_binary("0.5.0") + str(input_file), solc_binary=MythrilDisassembler._init_solc_binary("0.5.0") ) code_info = contract.get_source_info(142) @@ -25,7 +25,7 @@ class SolidityContractTest(BaseTestCase): contract = SolidityContract( str(input_file), name="Transfer1", - solc_binary=Mythril._init_solc_binary("0.5.0"), + solc_binary=MythrilDisassembler._init_solc_binary("0.5.0"), ) code_info = contract.get_source_info(142) @@ -39,7 +39,7 @@ class SolidityContractTest(BaseTestCase): contract = SolidityContract( str(input_file), name="AssertFail", - solc_binary=Mythril._init_solc_binary("0.5.0"), + solc_binary=MythrilDisassembler._init_solc_binary("0.5.0"), ) code_info = contract.get_source_info(70, constructor=True) diff --git a/tests/taint_mutate_stack_test.py b/tests/taint_mutate_stack_test.py deleted file mode 100644 index 6414340b..00000000 --- a/tests/taint_mutate_stack_test.py +++ /dev/null @@ -1,30 +0,0 @@ -from mythril.laser.ethereum.taint_analysis import * - - -def test_mutate_not_tainted(): - # Arrange - record = TaintRecord() - - record.stack = [True, False, False] - # Act - TaintRunner.mutate_stack(record, (2, 1)) - - # Assert - assert record.stack_tainted(0) - assert record.stack_tainted(1) is False - assert record.stack == [True, False] - - -def test_mutate_tainted(): - # Arrange - record = TaintRecord() - - record.stack = [True, False, True] - - # Act - TaintRunner.mutate_stack(record, (2, 1)) - - # Assert - assert record.stack_tainted(0) - assert record.stack_tainted(1) - assert record.stack == [True, True] diff --git a/tests/taint_record_test.py b/tests/taint_record_test.py deleted file mode 100644 index ba45f295..00000000 --- a/tests/taint_record_test.py +++ /dev/null @@ -1,36 +0,0 @@ -from mythril.laser.ethereum.taint_analysis import * - - -def test_record_tainted_check(): - # arrange - record = TaintRecord() - record.stack = [True, False, True] - - # act - tainted = record.stack_tainted(2) - - # assert - assert tainted is True - - -def test_record_untainted_check(): - # arrange - record = TaintRecord() - record.stack = [True, False, False] - - # act - tainted = record.stack_tainted(2) - - # assert - assert tainted is False - - -def test_record_untouched_check(): - # arrange - record = TaintRecord() - - # act - tainted = record.stack_tainted(3) - - # assert - assert tainted is None diff --git a/tests/taint_result_test.py b/tests/taint_result_test.py deleted file mode 100644 index 6670f228..00000000 --- a/tests/taint_result_test.py +++ /dev/null @@ -1,35 +0,0 @@ -from mythril.laser.ethereum.taint_analysis import * -from mythril.laser.ethereum.state.global_state import GlobalState - - -def test_result_state(): - # arrange - taint_result = TaintResult() - record = TaintRecord() - state = GlobalState(2, None, None) - state.mstate.stack = [1, 2, 3] - record.add_state(state) - record.stack = [False, False, False] - # act - taint_result.add_records([record]) - tainted = taint_result.check(state, 2) - - # assert - assert tainted is False - assert record in taint_result.records - - -def test_result_no_state(): - # arrange - taint_result = TaintResult() - record = TaintRecord() - state = GlobalState(2, None, None) - state.mstate.stack = [1, 2, 3] - - # act - taint_result.add_records([record]) - tainted = taint_result.check(state, 2) - - # assert - assert tainted is None - assert record in taint_result.records diff --git a/tests/taint_runner_test.py b/tests/taint_runner_test.py deleted file mode 100644 index a4f40bbc..00000000 --- a/tests/taint_runner_test.py +++ /dev/null @@ -1,99 +0,0 @@ -import mock -import pytest -from pytest_mock import mocker -from mythril.laser.ethereum.taint_analysis import * -from mythril.laser.ethereum.cfg import Node, Edge -from mythril.laser.ethereum.state.account import Account -from mythril.laser.ethereum.state.environment import Environment -from mythril.laser.ethereum.state.machine_state import MachineState -from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.ethereum.svm import LaserEVM - - -def test_execute_state(mocker): - record = TaintRecord() - record.stack = [True, False, True] - - state = GlobalState(None, None, None) - state.mstate.stack = [1, 2, 3] - mocker.patch.object(state, "get_current_instruction") - state.get_current_instruction.return_value = {"opcode": "ADD"} - - # Act - new_record = TaintRunner.execute_state(record, state) - - # Assert - assert new_record.stack == [True, True] - assert record.stack == [True, False, True] - - -def test_execute_node(mocker): - record = TaintRecord() - record.stack = [True, True, False, False] - - state_1 = GlobalState(None, None, None) - state_1.mstate.stack = [1, 2, 3, 1] - state_1.mstate.pc = 1 - mocker.patch.object(state_1, "get_current_instruction") - state_1.get_current_instruction.return_value = {"opcode": "SWAP1"} - - state_2 = GlobalState(None, 1, None) - state_2.mstate.stack = [1, 2, 4, 1] - mocker.patch.object(state_2, "get_current_instruction") - state_2.get_current_instruction.return_value = {"opcode": "ADD"} - - node = Node("Test contract") - node.states = [state_1, state_2] - - # Act - records = TaintRunner.execute_node(node, record) - - # Assert - assert len(records) == 2 - - assert records[0].stack == [True, True, False, False] - assert records[1].stack == [True, True, False] - - assert state_2 in records[0].states - assert state_1 in record.states - - -def test_execute(mocker): - active_account = Account("0x00") - environment = Environment(active_account, None, None, None, None, None) - state_1 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) - state_1.mstate.stack = [1, 2] - mocker.patch.object(state_1, "get_current_instruction") - state_1.get_current_instruction.return_value = {"opcode": "PUSH"} - - state_2 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) - state_2.mstate.stack = [1, 2, 3] - mocker.patch.object(state_2, "get_current_instruction") - state_2.get_current_instruction.return_value = {"opcode": "ADD"} - - node_1 = Node("Test contract") - node_1.states = [state_1, state_2] - - state_3 = GlobalState(None, environment, None, MachineState(gas_limit=8000000)) - state_3.mstate.stack = [1, 2] - mocker.patch.object(state_3, "get_current_instruction") - state_3.get_current_instruction.return_value = {"opcode": "ADD"} - - node_2 = Node("Test contract") - node_2.states = [state_3] - - edge = Edge(node_1.uid, node_2.uid) - - statespace = LaserEVM(None) - statespace.edges = [edge] - statespace.nodes[node_1.uid] = node_1 - statespace.nodes[node_2.uid] = node_2 - - # Act - result = TaintRunner.execute(statespace, node_1, state_1, [True, True]) - - # Assert - print(result) - assert len(result.records) == 3 - assert result.records[2].states == [] - assert state_3 in result.records[1].states diff --git a/tests/testdata/mythril_config_inputs/config.ini b/tests/testdata/mythril_config_inputs/config.ini new file mode 100644 index 00000000..bb6e7061 --- /dev/null +++ b/tests/testdata/mythril_config_inputs/config.ini @@ -0,0 +1,2 @@ +[defaults] +dynamic_loading = infura diff --git a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/calls.sol.o.jsonv2 index d42f0a1c..da380624 100644 --- a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/calls.sol.o.jsonv2 @@ -1 +1,148 @@ -[{"issues": [{"description": {"head": "The contract executes an external message call.", "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully."}, "extra": {}, "locations": [{"sourceMap": "661:1:0"}], "severity": "Low", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The contract executes an external message call.", "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully."}, "extra": {}, "locations": [{"sourceMap": "779:1:0"}], "severity": "Low", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The contract executes an external message call.", "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully."}, "extra": {}, "locations": [{"sourceMap": "858:1:0"}], "severity": "Low", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "A call to a user-supplied address is executed.", "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."}, "extra": {}, "locations": [{"sourceMap": "912:1:0"}], "severity": "Medium", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "661:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "779:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "858:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "912:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x7cbb77986c6b1bf6e945cd3fba06d3ea3d28cfc49cdfdc9571ec30703ac5862f"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "The contract executes an external message call.", + "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "661:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The contract executes an external message call.", + "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "779:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The contract executes an external message call.", + "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "858:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "A call to a user-supplied address is executed.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "912:1:0" + } + ], + "severity": "Medium", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "661:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "779:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "858:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "912:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0x7cbb77986c6b1bf6e945cd3fba06d3ea3d28cfc49cdfdc9571ec30703ac5862f" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 b/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 index 6f71bb6a..032cfc01 100644 --- a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 @@ -1 +1,80 @@ -[{"issues": [{"description": {"head": "A reachable exception has been detected.", "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking."}, "extra": {}, "locations": [{"sourceMap": "446:1:0"}], "severity": "Low", "swcID": "SWC-110", "swcTitle": "Assert Violation"}, {"description": {"head": "A reachable exception has been detected.", "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking."}, "extra": {}, "locations": [{"sourceMap": "484:1:0"}], "severity": "Low", "swcID": "SWC-110", "swcTitle": "Assert Violation"}, {"description": {"head": "A reachable exception has been detected.", "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking."}, "extra": {}, "locations": [{"sourceMap": "506:1:0"}], "severity": "Low", "swcID": "SWC-110", "swcTitle": "Assert Violation"}, {"description": {"head": "A reachable exception has been detected.", "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking."}, "extra": {}, "locations": [{"sourceMap": "531:1:0"}], "severity": "Low", "swcID": "SWC-110", "swcTitle": "Assert Violation"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x4a773a86bc6fb269f88bf09bb3094de29b6073cf13b1760e9d01d957f50a9dfd"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "A reachable exception has been detected.", + "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "446:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-110", + "swcTitle": "Assert Violation" + }, + { + "description": { + "head": "A reachable exception has been detected.", + "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "484:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-110", + "swcTitle": "Assert Violation" + }, + { + "description": { + "head": "A reachable exception has been detected.", + "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "506:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-110", + "swcTitle": "Assert Violation" + }, + { + "description": { + "head": "A reachable exception has been detected.", + "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "531:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-110", + "swcTitle": "Assert Violation" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0x4a773a86bc6fb269f88bf09bb3094de29b6073cf13b1760e9d01d957f50a9dfd" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 index 04cfe7f6..4f0d13e0 100644 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 @@ -1 +1,97 @@ -[{"issues": [{"description": {"head": "Use of callcode is deprecated.", "tail": "The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead."}, "extra": {}, "locations": [{"sourceMap": "618:1:0"}], "severity": "Medium", "swcID": "SWC-111", "swcTitle": "Use of Deprecated Solidity Functions"}, {"description": {"head": "A call to a user-supplied address is executed.", "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."}, "extra": {}, "locations": [{"sourceMap": "1038:1:0"}], "severity": "Medium", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "618:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "849:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "1038:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x6daec61d05d8f1210661e7e7d1ed6d72bd6ade639398fac1e867aff50abfc1c1"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "Use of callcode is deprecated.", + "tail": "The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "618:1:0" + } + ], + "severity": "Medium", + "swcID": "SWC-111", + "swcTitle": "Use of Deprecated Solidity Functions" + }, + { + "description": { + "head": "A call to a user-supplied address is executed.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "1038:1:0" + } + ], + "severity": "Medium", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "618:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "849:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "1038:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0x6daec61d05d8f1210661e7e7d1ed6d72bd6ade639398fac1e867aff50abfc1c1" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 b/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 index dcd4c195..21672449 100644 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 @@ -1 +1,29 @@ -[{"issues": [{"description": {"head": "Anyone can withdraw ETH from the contract account.", "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."}, "extra": {}, "locations": [{"sourceMap": "142:1:0"}], "severity": "High", "swcID": "SWC-105", "swcTitle": "Unprotected Ether Withdrawal"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0xbc9c3d9db56d20cf4ca3b6fd88ff9215cf728a092cca1ed8edb83272b933ff5b"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "Anyone can withdraw ETH from the contract account.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "142:1:0" + } + ], + "severity": "High", + "swcID": "SWC-105", + "swcTitle": "Unprotected Ether Withdrawal" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0xbc9c3d9db56d20cf4ca3b6fd88ff9215cf728a092cca1ed8edb83272b933ff5b" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 b/tests/testdata/outputs_expected/origin.sol.o.jsonv2 index 2d9efb87..27322fde 100644 --- a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/origin.sol.o.jsonv2 @@ -1 +1,29 @@ -[{"issues": [{"description": {"head": "Use of tx.origin is deprecated.", "tail": "The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin"}, "extra": {}, "locations": [{"sourceMap": "317:1:0"}], "severity": "Medium", "swcID": "SWC-111", "swcTitle": "Use of Deprecated Solidity Functions"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x25b20ef097dfc0aa56a932c4e09f06ee02a69c005767df86877f48c6c2412f03"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "Use of tx.origin is deprecated.", + "tail": "The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "317:1:0" + } + ], + "severity": "Medium", + "swcID": "SWC-111", + "swcTitle": "Use of Deprecated Solidity Functions" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0x25b20ef097dfc0aa56a932c4e09f06ee02a69c005767df86877f48c6c2412f03" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 index 9071dc3b..dfcc29d5 100644 --- a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 @@ -1 +1,46 @@ -[{"issues": [{"description": {"head": "The binary subtraction can underflow.", "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion."}, "extra": {}, "locations": [{"sourceMap": "567:1:0"}], "severity": "High", "swcID": "SWC-101", "swcTitle": "Integer Overflow and Underflow"}, {"description": {"head": "The binary subtraction can underflow.", "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion."}, "extra": {}, "locations": [{"sourceMap": "649:1:0"}], "severity": "High", "swcID": "SWC-101", "swcTitle": "Integer Overflow and Underflow"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0xf230bec502569e8b7e7737616d0ad0f200c436624e3c223e5398c0615cd2d6b9"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "The binary subtraction can underflow.", + "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "567:1:0" + } + ], + "severity": "High", + "swcID": "SWC-101", + "swcTitle": "Integer Overflow and Underflow" + }, + { + "description": { + "head": "The binary subtraction can underflow.", + "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "649:1:0" + } + ], + "severity": "High", + "swcID": "SWC-101", + "swcTitle": "Integer Overflow and Underflow" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0xf230bec502569e8b7e7737616d0ad0f200c436624e3c223e5398c0615cd2d6b9" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 b/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 index 00402e72..03fb9c0d 100644 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 @@ -1 +1,63 @@ -[{"issues": [{"description": {"head": "The contract executes an external message call.", "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully."}, "extra": {}, "locations": [{"sourceMap": "196:1:0"}], "severity": "Low", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The contract executes an external message call.", "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully."}, "extra": {}, "locations": [{"sourceMap": "285:1:0"}], "severity": "Low", "swcID": "SWC-107", "swcTitle": "Reentrancy"}, {"description": {"head": "The return value of a message call is not checked.", "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."}, "extra": {}, "locations": [{"sourceMap": "285:1:0"}], "severity": "Low", "swcID": "SWC-104", "swcTitle": "Unchecked Call Return Value"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0xb191cf6cc0d8cc37a91c9d88019cc011b932169fb5776df616e2bb9cd93b4039"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "The contract executes an external message call.", + "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "196:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The contract executes an external message call.", + "tail": "An external function call to a fixed contract address is executed. Make sure that the callee contract has been reviewed carefully." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "285:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-107", + "swcTitle": "Reentrancy" + }, + { + "description": { + "head": "The return value of a message call is not checked.", + "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." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "285:1:0" + } + ], + "severity": "Low", + "swcID": "SWC-104", + "swcTitle": "Unchecked Call Return Value" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0xb191cf6cc0d8cc37a91c9d88019cc011b932169fb5776df616e2bb9cd93b4039" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 b/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 index 6516a9a6..c492c24c 100644 --- a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 @@ -1 +1,29 @@ -[{"issues": [{"description": {"head": "The contract can be killed by anyone.", "tail": "Anyone can kill this contract and withdraw its balance to an arbitrary address."}, "extra": {}, "locations": [{"sourceMap": "146:1:0"}], "severity": "High", "swcID": "SWC-106", "swcTitle": "Unprotected SELFDESTRUCT Instruction"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x2fb801366b61a05b30550481a1c8f7d5f20de0b93d9f2f2ce2b28c4e322033c9"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "The contract can be killed by anyone.", + "tail": "Anyone can kill this contract and withdraw its balance to an arbitrary address." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "146:1:0" + } + ], + "severity": "High", + "swcID": "SWC-106", + "swcTitle": "Unprotected SELFDESTRUCT Instruction" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0x2fb801366b61a05b30550481a1c8f7d5f20de0b93d9f2f2ce2b28c4e322033c9" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 index 548c0ec6..94854e04 100644 --- a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 @@ -1 +1,46 @@ -[{"issues": [{"description": {"head": "The binary subtraction can underflow.", "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion."}, "extra": {}, "locations": [{"sourceMap": "567:1:0"}], "severity": "High", "swcID": "SWC-101", "swcTitle": "Integer Overflow and Underflow"}, {"description": {"head": "The binary subtraction can underflow.", "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion."}, "extra": {}, "locations": [{"sourceMap": "649:1:0"}], "severity": "High", "swcID": "SWC-101", "swcTitle": "Integer Overflow and Underflow"}], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0xabef56740bf7795a9f8732e4781ebd27f2977f8a4997e3ff11cee79a4ba6c0ce"], "sourceType": "raw-bytecode"}] \ No newline at end of file +[ + { + "issues": [ + { + "description": { + "head": "The binary subtraction can underflow.", + "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "567:1:0" + } + ], + "severity": "High", + "swcID": "SWC-101", + "swcTitle": "Integer Overflow and Underflow" + }, + { + "description": { + "head": "The binary subtraction can underflow.", + "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "649:1:0" + } + ], + "severity": "High", + "swcID": "SWC-101", + "swcTitle": "Integer Overflow and Underflow" + } + ], + "meta": {}, + "sourceFormat": "evm-byzantium-bytecode", + "sourceList": [ + "0xabef56740bf7795a9f8732e4781ebd27f2977f8a4997e3ff11cee79a4ba6c0ce" + ], + "sourceType": "raw-bytecode" + } +] \ No newline at end of file diff --git a/tox.ini b/tox.ini index 93ee48a2..e50f07fe 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ commands = mkdir -p {toxinidir}/tests/testdata/outputs_current_laser_result/ py.test -v \ --junitxml={toxworkdir}/output/{envname}/junit.xml \ + --disable-pytest-warnings \ {posargs} [testenv:py36] @@ -35,6 +36,7 @@ commands = --cov-report=xml:{toxworkdir}/output/{envname}/coverage.xml \ --cov-report=html:{toxworkdir}/output/{envname}/covhtml \ --junitxml={toxworkdir}/output/{envname}/junit.xml \ + --disable-pytest-warnings \ {posargs}