diff --git a/manticore/__main__.py b/manticore/__main__.py index 60c01fe0e..bcfc72656 100644 --- a/manticore/__main__.py +++ b/manticore/__main__.py @@ -7,6 +7,7 @@ import pkg_resources +from crytic_compile import is_supported, cryticparser from .core.manticore import ManticoreBase, set_verbosity from .ethereum.cli import ethereum_main from .utils import config, log, install_helper @@ -36,7 +37,7 @@ def main(): set_verbosity(args.v) - if args.argv[0].endswith(".sol"): + if args.argv[0].endswith(".sol") or is_supported(args.argv[0]): ethereum_main(args, logger) else: install_helper.ensure_native_deps() @@ -55,6 +56,11 @@ def positive(value): prog="manticore", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) + + # Add crytic compile arguments + # See https://github.com/crytic/crytic-compile/wiki/Configuration + cryticparser.init(parser) + parser.add_argument("--context", type=str, default=None, help=argparse.SUPPRESS) parser.add_argument( "--coverage", type=str, default="visited.txt", help="Where to write the coverage data" diff --git a/manticore/ethereum/cli.py b/manticore/ethereum/cli.py index 787e67898..eea4b7f45 100644 --- a/manticore/ethereum/cli.py +++ b/manticore/ethereum/cli.py @@ -105,6 +105,7 @@ def ethereum_main(args, logger): tx_send_ether=not args.txnoether, tx_account=args.txaccount, tx_preconstrain=args.txpreconstrain, + crytic_compile_args=vars(args), ) if not args.no_testcases: diff --git a/manticore/ethereum/manticore.py b/manticore/ethereum/manticore.py index 743e4e9b9..47e2a28b7 100644 --- a/manticore/ethereum/manticore.py +++ b/manticore/ethereum/manticore.py @@ -15,6 +15,8 @@ import sha3 import tempfile +from crytic_compile import CryticCompile, InvalidCompilation, is_supported + from ..core.manticore import ManticoreBase from ..core.smtlib import ( ConstraintSet, @@ -38,6 +40,7 @@ from ..utils.helpers import PickleSerializer logger = logging.getLogger(__name__) +logging.getLogger("CryticCompile").setLevel(logging.ERROR) cfg = config.get_group("evm") cfg.add("defaultgas", 3000000, "Default gas value for ethereum transactions.") @@ -194,16 +197,11 @@ def constrain(self, constraint): @staticmethod def compile( - source_code, - contract_name=None, - libraries=None, - runtime=False, - solc_bin=None, - solc_remaps=[], + source_code, contract_name=None, libraries=None, runtime=False, crytic_compile_args=dict() ): """ Get initialization bytecode from a Solidity source code """ name, source_code, init_bytecode, runtime_bytecode, srcmap, srcmap_runtime, hashes, abi, warnings = ManticoreEVM._compile( - source_code, contract_name, libraries, solc_bin, solc_remaps + source_code, contract_name, libraries, crytic_compile_args ) if runtime: return runtime_bytecode @@ -332,69 +330,95 @@ def _run_solc(source_file, solc_bin=None, solc_remaps=[], working_dir=None): raise EthereumError("Solidity compilation error:\n\n{}".format(stderr)) @staticmethod - def _compile( - source_code, contract_name, libraries=None, solc_bin=None, solc_remaps=[], working_dir=None - ): + def _compile_through_crytic_compile(filename, contract_name, libraries, crytic_compile_args): + """ + :param filename: filename to compile + :param contract_name: contract to extract + :param libraries: an itemizable of pairs (library_name, address) + :param crytic_compile_args: crytic compile options (https://github.com/crytic/crytic-compile/wiki/Configuration) + :type crytic_compile_args: dict + :return: + """ + try: + + if crytic_compile_args: + crytic_compile = CryticCompile(filename, **crytic_compile_args) + else: + crytic_compile = CryticCompile(filename) + + if not contract_name: + if len(crytic_compile.contracts_names_without_libraries) > 1: + raise EthereumError( + f"Solidity file must contain exactly one contract or you must use a `--contract` parameter to specify one. Contracts found: {', '.join(crytic_compile.contracts_names)}" + ) + contract_name = list(crytic_compile.contracts_names_without_libraries)[0] + + if contract_name not in crytic_compile.contracts_names: + raise ValueError(f"Specified contract not found: {contract_name}") + + name = contract_name + + libs = crytic_compile.libraries_names(name) + if libraries: + libs = [l for l in libs if l not in libraries] + if libs: + raise DependencyError(libs) + + bytecode = bytes.fromhex(crytic_compile.bytecode_init(name, libraries)) + runtime = bytes.fromhex(crytic_compile.bytecode_runtime(name, libraries)) + srcmap = crytic_compile.srcmap_init(name) + srcmap_runtime = crytic_compile.srcmap_runtime(name) + hashes = crytic_compile.hashes(name) + abi = crytic_compile.abi(name) + + filename = crytic_compile.filename_of_contract(name).absolute + with open(filename) as f: + source_code = f.read() + + return name, source_code, bytecode, runtime, srcmap, srcmap_runtime, hashes, abi + + except InvalidCompilation as e: + raise EthereumError( + f"Errors : {e}\n. Solidity failed to generate bytecode for your contract. Check if all the abstract functions are implemented. " + ) + + @staticmethod + def _compile(source_code, contract_name, libraries=None, crytic_compile_args=None): """ Compile a Solidity contract, used internally - :param source_code: solidity source as either a string or a file handle + :param source_code: solidity source + :type source_code: string (filename, directory, etherscan address) or a file handle :param contract_name: a string with the name of the contract to analyze :param libraries: an itemizable of pairs (library_name, address) - :param solc_bin: path to solc binary - :param solc_remaps: solc import remaps - :param working_dir: working directory for solc compilation (defaults to current) + :param crytic_compile_args: crytic compile options (https://github.com/crytic/crytic-compile/wiki/Configuration) + :type crytic_compile_args: dict :return: name, source_code, bytecode, srcmap, srcmap_runtime, hashes :return: name, source_code, bytecode, runtime, srcmap, srcmap_runtime, hashes, abi, warnings """ - if isinstance(source_code, str): - with tempfile.NamedTemporaryFile("w+") as temp: + crytic_compile_args = dict() if crytic_compile_args is None else crytic_compile_args + + if isinstance(source_code, io.IOBase): + source_code = source_code.name + + if isinstance(source_code, str) and not is_supported(source_code): + with tempfile.NamedTemporaryFile("w+", suffix=".sol") as temp: temp.write(source_code) temp.flush() - output, warnings = ManticoreEVM._run_solc( - temp, solc_bin, solc_remaps, working_dir=working_dir + compilation_result = ManticoreEVM._compile_through_crytic_compile( + temp.name, contract_name, libraries, crytic_compile_args ) - elif isinstance(source_code, io.IOBase): - output, warnings = ManticoreEVM._run_solc( - source_code, solc_bin, solc_remaps, working_dir=working_dir - ) - source_code.seek(0) - source_code = source_code.read() else: - raise TypeError(f"source code bad type: {type(source_code).__name__}") - - contracts = output.get("contracts", []) - if len(contracts) != 1 and contract_name is None: - raise EthereumError( - f'Solidity file must contain exactly one contract or you must use a `--contract` parameter to specify one. Contracts found: {", ".join(contracts)}' + compilation_result = ManticoreEVM._compile_through_crytic_compile( + source_code, contract_name, libraries, crytic_compile_args ) - name, contract = None, None - if contract_name is None: - name, contract = list(contracts.items())[0] - else: - for n, c in contracts.items(): - if n == contract_name or n.split(":")[1] == contract_name: - name, contract = n, c - break - - if name is None: - raise ValueError(f"Specified contract not found: {contract_name}") - - name = name.split(":")[1] - - if contract["bin"] == "": - raise EthereumError( - "Solidity failed to generate bytecode for your contract. Check if all the abstract functions are implemented" - ) + name, source_code, bytecode, runtime, srcmap, srcmap_runtime, hashes, abi = ( + compilation_result + ) + warnings = "" - bytecode = ManticoreEVM._link(contract["bin"], libraries) - srcmap = contract["srcmap"].split(";") - srcmap_runtime = contract["srcmap-runtime"].split(";") - hashes = {str(x): str(y) for x, y in contract["hashes"].items()} - abi = json.loads(contract["abi"]) - runtime = ManticoreEVM._link(contract["bin-runtime"], libraries) - return (name, source_code, bytecode, runtime, srcmap, srcmap_runtime, hashes, abi, warnings) + return name, source_code, bytecode, runtime, srcmap, srcmap_runtime, hashes, abi, warnings @property def accounts(self): @@ -604,7 +628,9 @@ def json_create_contract( signature = SolidityMetadata.function_signature_for_name_and_inputs( item["name"], item["inputs"] ) - hashes[signature] = sha3.keccak_256(signature.encode()).hexdigest()[:8] + hashes[signature] = int( + "0x" + sha3.keccak_256(signature.encode()).hexdigest()[:8], 16 + ) if "signature" in item: if item["signature"] != f"0x{hashes[signature]}": raise Exception( @@ -671,14 +697,13 @@ def solidity_create_contract( balance=0, address=None, args=(), - solc_bin=None, - solc_remaps=[], - working_dir=None, gas=None, + crytic_compile_args=None, ): """ Creates a solidity contract and library dependencies - :param str source_code: solidity source code + :param source_code: solidity source code + :type source_code: string (filename, directory, etherscan address) or a file handle :param owner: owner account (will be default caller in any transactions) :type owner: int or EVMAccount :param contract_name: Name of the contract to analyze (optional if there is a single one in the source code) @@ -688,16 +713,15 @@ def solidity_create_contract( :param address: the address for the new contract (optional) :type address: int or EVMAccount :param tuple args: constructor arguments - :param solc_bin: path to solc binary - :type solc_bin: str - :param solc_remaps: solc import remaps - :type solc_remaps: list of str - :param working_dir: working directory for solc compilation (defaults to current) - :type working_dir: str + :param crytic_compile_args: crytic compile options (https://github.com/crytic/crytic-compile/wiki/Configuration) + :type crytic_compile_args: dict :param gas: gas budget for each contract creation needed (may be more than one if several related contracts defined in the solidity source) :type gas: int :rtype: EVMAccount """ + + crytic_compile_args = dict() if crytic_compile_args is None else crytic_compile_args + if libraries is None: deps = {} else: @@ -711,9 +735,7 @@ def solidity_create_contract( source_code, contract_name_i, libraries=deps, - solc_bin=solc_bin, - solc_remaps=solc_remaps, - working_dir=working_dir, + crytic_compile_args=crytic_compile_args, ) md = SolidityMetadata(*compile_results) if contract_name_i == contract_name: @@ -755,12 +777,16 @@ def solidity_create_contract( raise EthereumError("Failed to build contract %s" % contract_name_i) self.metadata[int(contract_account)] = md - deps[contract_name_i] = contract_account + deps[contract_name_i] = int(contract_account) except DependencyError as e: contract_names.append(contract_name_i) for lib_name in e.lib_names: if lib_name not in deps: contract_names.append(lib_name) + except EthereumError as e: + logger.error(e) + self.kill() + raise except Exception as e: self.kill() raise @@ -1130,7 +1156,6 @@ def preconstraint_for_call_transaction( def multi_tx_analysis( self, solidity_filename, - working_dir=None, contract_name=None, tx_limit=None, tx_use_coverage=True, @@ -1138,20 +1163,20 @@ def multi_tx_analysis( tx_account="attacker", tx_preconstrain=False, args=None, + crytic_compile_args=dict(), ): owner_account = self.create_account(balance=1000, name="owner") attacker_account = self.create_account(balance=1000, name="attacker") # Pretty print logger.info("Starting symbolic create contract") - with open(solidity_filename) as f: - contract_account = self.solidity_create_contract( - f, - contract_name=contract_name, - owner=owner_account, - args=args, - working_dir=working_dir, - ) + contract_account = self.solidity_create_contract( + solidity_filename, + contract_name=contract_name, + owner=owner_account, + args=args, + crytic_compile_args=crytic_compile_args, + ) if tx_account == "attacker": tx_account = [attacker_account] diff --git a/manticore/ethereum/solidity.py b/manticore/ethereum/solidity.py index b03933be5..e272473c5 100644 --- a/manticore/ethereum/solidity.py +++ b/manticore/ethereum/solidity.py @@ -51,7 +51,7 @@ def __init__( self._runtime_bytecode = runtime_bytecode self._function_signatures_by_selector = { - bytes.fromhex(sel): sig for sig, sel in hashes.items() + bytes.fromhex("{:08x}".format(sel)): sig for sig, sel in hashes.items() } fallback_selector = b"\0\0\0\0" diff --git a/scripts/travis_test.sh b/scripts/travis_test.sh index 726e6173c..d79875aca 100755 --- a/scripts/travis_test.sh +++ b/scripts/travis_test.sh @@ -74,6 +74,33 @@ make_vmtests(){ cd $DIR } +install_truffle(){ + curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash + source ~/.nvm/nvm.sh + nvm install --lts + nvm use --lts + + npm install -g truffle +} + +run_truffle_tests(){ + mkdir truffle_tests + cd truffle_tests + truffle unbox metacoin + manticore . --contract MetaCoin --workspace output + # The correct answer should be 41 + # but Manticore fails to explore the paths due to the lack of the 0x1f opcode support + # see https://github.com/trailofbits/manticore/issues/1166 + # if [ "$(ls output/*tx -l | wc -l)" != "41" ]; then + if [ "$(ls output/*tx -l | wc -l)" != "3" ]; then + echo "Truffle test failed" + return 1 + fi + echo "Truffle test succeded" + cd .. + return 0 +} + run_tests_from_dir() { DIR=$1 coverage erase @@ -108,9 +135,12 @@ run_examples() { case $1 in ethereum_vm) make_vmtests + install_truffle + run_truffle_tests + RV=$? echo "Running only the tests from 'tests/$1' directory" run_tests_from_dir $1 - RV=$? + RV=$(($RV + $?)) ;; native) ;& # Fallthrough diff --git a/setup.py b/setup.py index b524a65b9..204658ee6 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ def rtd_dependent_deps(): "pyevmasm==0.2.0", "rlp", "ply", + "crytic-compile>=0.1.1", ] + rtd_dependent_deps(), extras_require=extra_require, diff --git a/tests/ethereum/test_detectors.py b/tests/ethereum/test_detectors.py index fb25c6cb2..e0d11f779 100644 --- a/tests/ethereum/test_detectors.py +++ b/tests/ethereum/test_detectors.py @@ -67,9 +67,13 @@ def _test(self, name, should_find, use_ctor_sym_arg=False): ctor_arg = () self.mevm.register_detector(self.DETECTOR_CLASS()) + with self.mevm.kill_timeout(240): mevm.multi_tx_analysis( - filepath, contract_name="DetectThis", args=ctor_arg, working_dir=dir + filepath, + contract_name="DetectThis", + args=ctor_arg, + crytic_compile_args={"solc_working_dir": dir}, ) expected_findings = set(((finding, at_init) for finding, at_init in should_find))