diff --git a/myth b/myth index c528229d..8a27925e 100755 --- a/myth +++ b/myth @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- """mythril.py: Bug hunting on the Ethereum blockchain http://www.github.com/b-mueller/mythril """ -from mythril import __main__ +import mythril.interfaces.cli if __name__ == "__main__": - __main__.main() + mythril.interfaces.cli.main() diff --git a/mythril/__main__.py b/mythril/__main__.py index cc89f98b..a1ed78df 100644 --- a/mythril/__main__.py +++ b/mythril/__main__.py @@ -1,458 +1,6 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -"""mythril.py: Bug hunting on the Ethereum blockchain - - http://www.github.com/b-mueller/mythril -""" - - -import logging -import json -import sys -import argparse -import os -import re - -from ethereum import utils -from solc.exceptions import SolcError -import solc - -from mythril.ether import util -from mythril.ether.contractstorage import get_persistent_storage -from mythril.ether.ethcontract import ETHContract -from mythril.ether.soliditycontract import SolidityContract -from mythril.rpc.client import EthJsonRpc -from mythril.ipc.client import EthIpc -from mythril.rpc.exceptions import ConnectionError -from mythril.support import signatures -from mythril.support.truffle import analyze_truffle_project -from mythril.support.loader import DynLoader -from mythril.exceptions import CompilerError, NoContractFoundError -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 -from mythril.analysis.report import Report -from mythril.leveldb.client import EthLevelDB - -# logging.basicConfig(level=logging.DEBUG) - - -def searchCallback(code_hash, code, addresses, balances): - print("Matched contract with code hash " + code_hash) - - for i in range(0, len(addresses)): - print("Address: " + addresses[i] + ", balance: " + str(balances[i])) - - -def exitWithError(format, message): - if format == 'text' or format == 'markdown': - print(message) - else: - result = {'success': False, 'error': str(message), 'issues': []} - print(json.dumps(result)) - sys.exit() - -def main(): - parser = argparse.ArgumentParser(description='Security analysis of Ethereum smart contracts') - parser.add_argument("solidity_file", nargs='*') - - commands = parser.add_argument_group('commands') - commands.add_argument('-g', '--graph', help='generate a control flow graph', metavar='OUTPUT_FILE') - commands.add_argument('-x', '--fire-lasers', action='store_true', help='detect vulnerabilities, use with -c, -a or solidity file(s)') - commands.add_argument('-t', '--truffle', action='store_true', help='analyze a truffle project (run from project dir)') - commands.add_argument('-d', '--disassemble', action='store_true', help='print disassembly') - commands.add_argument('-j', '--statespace-json', help='dumps the statespace json', metavar='OUTPUT_FILE') - - inputs = parser.add_argument_group('input arguments') - inputs.add_argument('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE') - inputs.add_argument('-a', '--address', help='pull contract from the blockchain', metavar='CONTRACT_ADDRESS') - inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies from the blockchain') - - outputs = parser.add_argument_group('output formats') - outputs.add_argument('-o', '--outform', choices=['text', 'markdown', 'json'], default='text', help='report output format', metavar='') - outputs.add_argument('--verbose-report', action='store_true', help='Include debugging information in report') - - database = parser.add_argument_group('local contracts database') - database.add_argument('--init-db', action='store_true', help='initialize the contract database') - database.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION') - - utilities = parser.add_argument_group('utilities') - utilities.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE') - utilities.add_argument('--storage', help='read state variables from storage index, use with -a', metavar='INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]') - utilities.add_argument('--solv', help='specify solidity compiler version. If not present, will try to install it (Experimental)', metavar='SOLV') - - options = parser.add_argument_group('options') - options.add_argument('-m', '--modules', help='Comma-separated list of security analysis modules', metavar='MODULES') - options.add_argument('--max-depth', type=int, default=12, help='Maximum recursion depth for symbolic execution') - options.add_argument('--solc-args', help='Extra arguments for solc') - options.add_argument('--phrack', action='store_true', help='Phrack-style call graph') - options.add_argument('--enable-physics', action='store_true', help='enable graph physics simulation') - options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL') - options.add_argument('--leveldb', help='enable direct leveldb access operations', metavar='LEVELDB_PATH') - - rpc = parser.add_argument_group('RPC options') - rpc.add_argument('-i', action='store_true', help='Preset: Infura Node service (Mainnet)') - rpc.add_argument('--rpc', help='custom RPC settings', metavar='HOST:PORT / ganache / infura-[network_name]') - rpc.add_argument('--rpctls', type=bool, default=False, help='RPC connection over TLS') - rpc.add_argument('--ipc', action='store_true', help='Connect via local IPC') - - # Get config values - - args = parser.parse_args() - - try: - mythril_dir = os.environ['MYTHRIL_DIR'] - except KeyError: - mythril_dir = os.path.join(os.path.expanduser('~'), ".mythril") - - # Detect unsupported combinations of command line args - - if args.dynld and not args.address: - exitWithError(args.outform, "Dynamic loader can be used in on-chain analysis mode only (-a).") - - # Initialize data directory and signature database - - if not os.path.exists(mythril_dir): - logging.info("Creating mythril data directory") - os.mkdir(mythril_dir) - - # If no function signature file exists, create it. Function signatures from Solidity source code are added automatically. - - signatures_file = os.path.join(mythril_dir, 'signatures.json') - - sigs = {} - if not os.path.exists(signatures_file): - logging.info("No signature database found. Creating empty database: " + signatures_file + "\n" + - "Consider replacing it with the pre-initialized database at https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json") - with open(signatures_file, 'a') as f: - json.dump({}, f) - - with open(signatures_file) as f: - try: - sigs = json.load(f) - except JSONDecodeError as e: - exitWithError(args.outform, "Invalid JSON in signatures file " + signatures_file + "\n" + str(e)) - - # Parse cmdline args - - if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.fire_lasers or args.storage or args.truffle or args.statespace_json): - parser.print_help() - sys.exit() - - if args.v: - if 0 <= args.v < 3: - logging.basicConfig(level=[logging.NOTSET, logging.INFO, logging.DEBUG][args.v]) - else: - exitWithError(args.outform, "Invalid -v value, you can find valid values in usage") - - if args.hash: - print("0x" + utils.sha3(args.hash)[:4].hex()) - sys.exit() - - if args.truffle: - try: - 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() - - # Figure out solc binary and version - # Only proper versions are supported. No nightlies, commits etc (such as available in remix) - - if args.solv: - version = args.solv - # tried converting input to semver, seemed not necessary so just slicing for now - if version == str(solc.main.get_solc_version())[:6]: - logging.info('Given version matches installed version') - try: - solc_binary = os.environ['SOLC'] - except KeyError: - solc_binary = 'solc' - else: - if util.solc_exists(version): - logging.info('Given version is already installed') - else: - try: - solc.install_solc('v' + version) - except SolcError: - exitWithError(args.outform, "There was an error when trying to install the specified solc version") - - solc_binary = os.path.join(os.environ['HOME'], ".py-solc/solc-v" + version, "bin/solc") - logging.info("Setting the compiler to " + str(solc_binary)) - else: - try: - solc_binary = os.environ['SOLC'] - except KeyError: - solc_binary = 'solc' - - # Open LevelDB if specified - - if args.leveldb: - ethDB = EthLevelDB(args.leveldb) - eth = ethDB - - # Establish RPC/IPC connection if necessary - - if (args.address or args.init_db) and not args.leveldb: - - if args.i: - eth = EthJsonRpc('mainnet.infura.io', 443, True) - logging.info("Using INFURA for RPC queries") - elif args.rpc: - - if args.rpc == 'ganache': - rpcconfig = ('localhost', 7545, False) - - else: - - m = re.match(r'infura-(.*)', args.rpc) - - if m and m.group(1) in ['mainnet', 'rinkeby', 'kovan', 'ropsten']: - rpcconfig = (m.group(1) + '.infura.io', 443, True) - - else: - try: - host, port = args.rpc.split(":") - rpcconfig = (host, int(port), args.rpctls) - - except ValueError: - exitWithError(args.outform, "Invalid RPC argument, use 'ganache', 'infura-[network]' or 'HOST:PORT'") - - if (rpcconfig): - - eth = EthJsonRpc(rpcconfig[0], int(rpcconfig[1]), rpcconfig[2]) - logging.info("Using RPC settings: %s" % str(rpcconfig)) - - else: - exitWithError(args.outform, "Invalid RPC settings, check help for details.") - - elif args.ipc: - try: - eth = EthIpc() - except Exception as e: - exitWithError(args.outform, "IPC initialization failed. Please verify that your local Ethereum node is running, or use the -i flag to connect to INFURA. \n" + str(e)) - - else: # Default configuration if neither RPC or IPC are set - - eth = EthJsonRpc('localhost', 8545) - logging.info("Using default RPC settings: http://localhost:8545") - - - # Database search ops - - if args.search or args.init_db: - contract_storage, _ = get_persistent_storage(mythril_dir) - if args.search: - try: - if not args.leveldb: - contract_storage.search(args.search, searchCallback) - else: - ethDB.search(args.search, searchCallback) - except SyntaxError: - exitWithError(args.outform, "Syntax error in search expression.") - elif args.init_db: - try: - contract_storage.initialize(eth) - except FileNotFoundError as e: - exitWithError(args.outform, "Error syncing database over IPC: " + str(e)) - except ConnectionError as e: - exitWithError(args.outform, "Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.") - - sys.exit() - - # Load / compile input contracts - - contracts = [] - address = None - - if args.code: - address = util.get_indexed_address(0) - contracts.append(ETHContract(args.code, name="MAIN")) - - # Get bytecode from a contract address - - elif args.address: - address = args.address - if not re.match(r'0x[a-fA-F0-9]{40}', args.address): - exitWithError(args.outform, "Invalid contract address. Expected format is '0x...'.") - - try: - code = eth.eth_getCode(args.address) - except FileNotFoundError as e: - exitWithError(args.outform, "IPC error: " + str(e)) - except ConnectionError as e: - exitWithError(args.outform, "Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.") - except Exception as e: - exitWithError(args.outform, "IPC / RPC error: " + str(e)) - else: - if code == "0x" or code == "0x0": - exitWithError(args.outform, "Received an empty response from eth_getCode. Check the contract address and verify that you are on the correct chain.") - else: - contracts.append(ETHContract(code, name=args.address)) - - # Compile Solidity source file(s) - - elif args.solidity_file: - address = util.get_indexed_address(0) - if args.graph and len(args.solidity_file) > 1: - exitWithError(args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.") - - for file in args.solidity_file: - if ":" in file: - file, contract_name = file.split(":") - else: - contract_name = None - - file = os.path.expanduser(file) - - try: - signatures.add_signatures_from_file(file, sigs) - contract = SolidityContract(file, contract_name, solc_args=args.solc_args) - logging.info("Analyzing contract %s:%s" % (file, contract.name)) - except FileNotFoundError: - exitWithError(args.outform, "Input file not found: " + file) - except CompilerError as e: - exitWithError(args.outform, e) - except NoContractFoundError: - logging.info("The file " + file + " does not contain a compilable contract.") - else: - contracts.append(contract) - - # Save updated function signatures - with open(signatures_file, 'w') as f: - json.dump(sigs, f) - - else: - exitWithError(args.outform, "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES") - - # Commands - - if args.storage: - if not args.address: - exitWithError(args.outform, "To read storage, provide the address of a deployed contract with the -a option.") - else: - (position, length, mappings) = (0, 1, []) - try: - params = args.storage.split(",") - if params[0] == "mapping": - if len(params) < 3: - exitWithError(args.outform, "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: - exitWithError(args.outform, "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: - exitWithError(args.outform, "Invalid storage index. Please provide a numeric value.") - - try: - if length == 1: - print("{}: {}".format(position, eth.eth_getStorageAt(args.address, position))) - else: - if len(mappings) > 0: - for i in range(0, len(mappings)): - position = mappings[i] - print("{}: {}".format(hex(position), eth.eth_getStorageAt(args.address, position))) - else: - for i in range(position, position + length): - print("{}: {}".format(hex(i), eth.eth_getStorageAt(args.address, i))) - except FileNotFoundError as e: - exitWithError(args.outform, "IPC error: " + str(e)) - except ConnectionError as e: - exitWithError(args.outform, "Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.") - - - elif args.disassemble: - easm_text = contracts[0].get_easm() - sys.stdout.write(easm_text) - - - elif args.graph or args.fire_lasers: - if not contracts: - exitWithError(args.outform, "input files do not contain any valid contracts") - - if args.graph: - if args.dynld: - sym = SymExecWrapper(contracts[0], address, dynloader=DynLoader(eth), max_depth=args.max_depth) - else: - sym = SymExecWrapper(contracts[0], address, max_depth=args.max_depth) - - html = generate_graph(sym, physics=args.enable_physics, phrackify=args.phrack) - - try: - with open(args.graph, "w") as f: - f.write(html) - except Exception as e: - exitWithError(args.outform, "Error saving graph: " + str(e)) - - else: - all_issues = [] - for contract in contracts: - if args.dynld: - sym = SymExecWrapper(contract, address, dynloader=DynLoader(eth), max_depth=args.max_depth) - else: - sym = SymExecWrapper(contract, address, max_depth=args.max_depth) - - if args.modules: - issues = fire_lasers(sym, args.modules.split(",")) - else: - issues = fire_lasers(sym) - - if type(contract) == SolidityContract: - for issue in issues: - issue.add_code_info(contract) - - all_issues += issues - - # Finally, output the results - report = Report(args.verbose_report) - for issue in all_issues: - report.append_issue(issue) - - outputs = { - 'json': report.as_json(), - 'text': report.as_text() or "The analysis was completed successfully. No issues were detected.", - 'markdown': report.as_markdown() or "The analysis was completed successfully. No issues were detected." - } - print(outputs[args.outform]) - - elif args.statespace_json: - if not contracts: - exitWithError(args.outform, "input files do not contain any valid contracts") - - if args.dynld: - sym = SymExecWrapper(contracts[0], address, dynloader=DynLoader(eth), max_depth=args.max_depth) - else: - sym = SymExecWrapper(contracts[0], address, max_depth=args.max_depth) - - try: - with open(args.statespace_json, "w") as f: - json.dump(get_serializable_statespace(sym), f) - except Exception as e: - exitWithError(args.outform, "Error saving json: " + str(e)) - - else: - parser.print_help() +import mythril.interfaces.cli if __name__ == "__main__": - main() - + mythril.interfaces.cli.main() diff --git a/mythril/exceptions.py b/mythril/exceptions.py index f7bf51c9..2c7dc510 100644 --- a/mythril/exceptions.py +++ b/mythril/exceptions.py @@ -1,8 +1,19 @@ -class CompilerError(Exception): +class MythrilBaseException(Exception): pass -class UnsatError(Exception): + +class CompilerError(MythrilBaseException): + pass + + +class UnsatError(MythrilBaseException): + pass + + +class NoContractFoundError(MythrilBaseException): + pass + + +class CriticalError(MythrilBaseException): pass -class NoContractFoundError(Exception): - pass \ No newline at end of file diff --git a/mythril/interfaces/__init__.py b/mythril/interfaces/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py new file mode 100644 index 00000000..94724219 --- /dev/null +++ b/mythril/interfaces/cli.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +"""mythril.py: Bug hunting on the Ethereum blockchain + + http://www.github.com/b-mueller/mythril +""" + +import logging +import json +import sys +import argparse + +# logging.basicConfig(level=logging.DEBUG) + +from mythril.exceptions import CriticalError +from mythril.mythril import Mythril + + +def exit_with_error(format, message): + if format == 'text' or format == 'markdown': + print(message) + else: + result = {'success': False, 'error': str(message), 'issues': []} + print(json.dumps(result)) + sys.exit() + + +def main(): + parser = argparse.ArgumentParser(description='Security analysis of Ethereum smart contracts') + parser.add_argument("solidity_file", nargs='*') + + commands = parser.add_argument_group('commands') + commands.add_argument('-g', '--graph', help='generate a control flow graph', metavar='OUTPUT_FILE') + commands.add_argument('-x', '--fire-lasers', action='store_true', + help='detect vulnerabilities, use with -c, -a or solidity file(s)') + commands.add_argument('-t', '--truffle', action='store_true', + help='analyze a truffle project (run from project dir)') + commands.add_argument('-d', '--disassemble', action='store_true', help='print disassembly') + commands.add_argument('-j', '--statespace-json', help='dumps the statespace json', metavar='OUTPUT_FILE') + + inputs = parser.add_argument_group('input arguments') + inputs.add_argument('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE') + inputs.add_argument('-a', '--address', help='pull contract from the blockchain', metavar='CONTRACT_ADDRESS') + inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies from the blockchain') + + outputs = parser.add_argument_group('output formats') + outputs.add_argument('-o', '--outform', choices=['text', 'markdown', 'json'], default='text', + help='report output format', metavar='') + outputs.add_argument('--verbose-report', action='store_true', help='Include debugging information in report') + + database = parser.add_argument_group('local contracts database') + database.add_argument('--init-db', action='store_true', help='initialize the contract database') + database.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION') + + utilities = parser.add_argument_group('utilities') + utilities.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE') + utilities.add_argument('--storage', help='read state variables from storage index, use with -a', + metavar='INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]') + utilities.add_argument('--solv', + help='specify solidity compiler version. If not present, will try to install it (Experimental)', + metavar='SOLV') + + options = parser.add_argument_group('options') + options.add_argument('-m', '--modules', help='Comma-separated list of security analysis modules', metavar='MODULES') + options.add_argument('--max-depth', type=int, default=12, help='Maximum recursion depth for symbolic execution') + options.add_argument('--solc-args', help='Extra arguments for solc') + options.add_argument('--phrack', action='store_true', help='Phrack-style call graph') + options.add_argument('--enable-physics', action='store_true', help='enable graph physics simulation') + options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL') + options.add_argument('--leveldb', help='enable direct leveldb access operations', metavar='LEVELDB_PATH') + + rpc = parser.add_argument_group('RPC options') + rpc.add_argument('-i', action='store_true', help='Preset: Infura Node service (Mainnet)') + rpc.add_argument('--rpc', help='custom RPC settings', metavar='HOST:PORT / ganache / infura-[network_name]') + rpc.add_argument('--rpctls', type=bool, default=False, help='RPC connection over TLS') + rpc.add_argument('--ipc', action='store_true', help='Connect via local IPC') + + # Get config values + + args = parser.parse_args() + + # -- args sanity checks -- + # Detect unsupported combinations of command line args + + if args.dynld and not args.address: + exit_with_error(args.outform, "Dynamic loader can be used in on-chain analysis mode only (-a).") + + # Parse cmdline args + + if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.fire_lasers + or args.storage or args.truffle or args.statespace_json): + parser.print_help() + sys.exit() + + if args.v: + if 0 <= args.v < 3: + logging.basicConfig(level=[logging.NOTSET, logging.INFO, logging.DEBUG][args.v]) + else: + exit_with_error(args.outform, "Invalid -v value, you can find valid values in usage") + + # -- commands -- + if args.hash: + print(Mythril.hash_for_function_signature(args.hash)) + sys.exit() + + + try: + # the mythril object should be our main interface + #init_db = None, infura = None, rpc = None, rpctls = None, ipc = None, + #solc_args = None, dynld = None, max_recursion_depth = 12): + + + mythril = Mythril(solv=args.solv, dynld=args.dynld, + solc_args=args.solc_args) + + if args.leveldb: + # Open LevelDB if specified + mythril.set_db_leveldb(args.leveldb) + + elif (args.address or args.init_db) and not args.leveldb: + # Establish RPC/IPC connection if necessary + if args.i: + mythril.set_db_rpc_infura() + elif args.rpc: + mythril.set_db_rpc(rpc=args.rpc, rpctls=args.rpctls) + elif args.ipc: + mythril.set_db_ipc() + else: + mythril.set_db_rpc_localhost() + + if args.truffle: + try: + # not really pythonic atm. needs refactoring + mythril.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() + + elif args.search: + # Database search ops + mythril.search_db(args.search) + sys.exit() + + elif args.init_db: + mythril.init_db() + sys.exit() + + # Load / compile input contracts + address = None + + if args.code: + # Load from bytecode + address, _ = mythril.load_from_bytecode(args.code) + elif args.address: + # Get bytecode from a contract address + address, _ = mythril.load_from_address(args.address) + elif args.solidity_file: + # Compile Solidity source file(s) + if args.graph and len(args.solidity_file) > 1: + exit_with_error(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 + else: + exit_with_error(args.outform, + "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES") + + # Commands + + if args.storage: + if not args.address: + exit_with_error(args.outform, + "To read storage, provide the address of a deployed contract with the -a option.") + + storage = mythril.get_state_variable_from_storage(address=address, + params=[a.strip() for a in args.storage.strip().split(",")]) + print(storage) + + elif args.disassemble: + easm_text = mythril.contracts[0].get_easm() # or mythril.disassemble(mythril.contracts[0]) + sys.stdout.write(easm_text) + + elif args.graph or args.fire_lasers: + if not mythril.contracts: + exit_with_error(args.outform, "input files do not contain any valid contracts") + + if args.graph: + # dot this for all contracts or just the first? + for nr, contract in enumerate(mythril.contracts): + html = mythril.graph_html(contract, address=address, + enable_physics=args.enable_physics, phrackify=args.phrack, + max_depth=args.max_depth) + + try: + with open("graph_%s_%d_%s" % (args.graph, nr, contract.name), "w") as f: + f.write(html) + except Exception as e: + exit_with_error(args.outform, "Error saving graph: " + str(e)) + + else: + report = mythril.fire_lasers(address=address, + modules=[m.strip() for m in args.modules.strip().split(",")] if args.modules else [], + verbose_report=args.verbose_report, + max_depth=args.max_depth) + outputs = { + 'json': report.as_json(), + 'text': report.as_text() or "The analysis was completed successfully. No issues were detected.", + 'markdown': report.as_markdown() or "The analysis was completed successfully. No issues were detected." + } + print(outputs[args.outform]) + + elif args.statespace_json: + if not mythril.contracts: + exit_with_error(args.outform, "input files do not contain any valid contracts") + + for nr, contract_statespace in enumerate(mythril.dump_statespaces(address=address, max_depth=args.max_depth)): + + contract, statespace = contract_statespace + try: + with open("%s_%d_%s.json" % (args.statespace_json, nr, contract.name), "w") as f: + json.dump(statespace, f) + except Exception as e: + exit_with_error(args.outform, "Error saving json: " + str(e)) + + else: + parser.print_help() + + except CriticalError as ce: + exit_with_error(args.outform, str(ce)) + +if __name__ == "__main__": + main() diff --git a/mythril/mythril.py b/mythril/mythril.py new file mode 100644 index 00000000..444f34c1 --- /dev/null +++ b/mythril/mythril.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +"""mythril.py: Bug hunting on the Ethereum blockchain + + http://www.github.com/b-mueller/mythril +""" + +import logging +import json +import os +import re + +from ethereum import utils +from solc.exceptions import SolcError +import solc + +from mythril.ether import util +from mythril.ether.contractstorage import get_persistent_storage +from mythril.ether.ethcontract import ETHContract +from mythril.ether.soliditycontract import SolidityContract +from mythril.rpc.client import EthJsonRpc +from mythril.ipc.client import EthIpc +from mythril.rpc.exceptions import ConnectionError +from mythril.support import signatures +from mythril.support.truffle import analyze_truffle_project +from mythril.support.loader import DynLoader +from mythril.exceptions import CompilerError, NoContractFoundError, CriticalError +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 +from mythril.analysis.report import Report +from mythril.leveldb.client import EthLevelDB + + +# logging.basicConfig(level=logging.DEBUG) + +class Mythril(object): + """ + Mythril main interface class. + + 1. create mythril object + 2. set rpc or leveldb interface if needed + 3. load contracts (from solidity, bytecode, address) + 4. fire_lasers + + Example: + mythril = Mythril() + mythril.set_db_rpc_infura() + + # (optional) other db adapters + mythril.set_db_rpc(args) + mythril.set_db_ipc() + mythril.set_db_rpc_localhost() + + # (optional) other func + mythril.analyze_truffle_project(args) + mythril.search_db(args) + mythril.init_db() + + # load contract + mythril.load_from_bytecode(bytecode) + mythril.load_from_address(address) + mythril.load_from_solidity(solidity_file) + + # analyze + print(mythril.fire_lasers(args).as_text()) + + # (optional) graph + for contract in mythril.contracts: + print(mythril.graph_html(args)) # prints html or save it to file + + # (optional) other funcs + mythril.dump_statespaces(args) + mythril.disassemble(contract) + mythril.get_state_variable_from_storage(args) + + """ + + + def __init__(self, solv=None, + solc_args=None, dynld=False): + + self.solv = solv + self.solc_args = solc_args + self.dynld = dynld + + self.mythril_dir = self._init_mythril_dir() + self.signatures_file, self.sigs = self._init_signatures() + self.solc_binary = self._init_solc_binary(solv) + + self.eth = None + self.ethDb = None + self.dbtype = None # track type of db (rpc,ipc,leveldb) used + + self.contracts = [] # loaded contracts + + def _init_mythril_dir(self): + try: + mythril_dir = os.environ['MYTHRIL_DIR'] + except KeyError: + mythril_dir = os.path.join(os.path.expanduser('~'), ".mythril") + + # Initialize data directory and signature database + + if not os.path.exists(mythril_dir): + logging.info("Creating mythril data directory") + os.mkdir(mythril_dir) + return mythril_dir + + def _init_signatures(self): + + # If no function signature file exists, create it. Function signatures from Solidity source code are added automatically. + + signatures_file = os.path.join(self.mythril_dir, 'signatures.json') + + sigs = {} + if not os.path.exists(signatures_file): + logging.info("No signature database found. Creating empty database: " + signatures_file + "\n" + + "Consider replacing it with the pre-initialized database at https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json") + with open(signatures_file, 'a') as f: + json.dump({}, f) + + with open(signatures_file) as f: + try: + sigs = json.load(f) + except json.JSONDecodeError as e: + raise CriticalError("Invalid JSON in signatures file " + signatures_file + "\n" + str(e)) + return signatures_file, sigs + + def _update_signatures(self, jsonsigs): + # Save updated function signatures + with open(self.signatures_file, 'w') as f: + json.dump(jsonsigs, f) + + self.sigs = jsonsigs + + def analyze_truffle_project(self, *args, **kwargs): + return analyze_truffle_project(*args, **kwargs) # just passthru for now + + def _init_solc_binary(self, version): + # Figure out solc binary and version + # Only proper versions are supported. No nightlies, commits etc (such as available in remix) + + if version: + # tried converting input to semver, seemed not necessary so just slicing for now + if version == str(solc.main.get_solc_version())[:6]: + logging.info('Given version matches installed version') + try: + solc_binary = os.environ['SOLC'] + except KeyError: + solc_binary = 'solc' + else: + if util.solc_exists(version): + logging.info('Given version is already installed') + else: + try: + solc.install_solc('v' + version) + except SolcError: + raise CriticalError("There was an error when trying to install the specified solc version") + + solc_binary = os.path.join(os.environ['HOME'], ".py-solc/solc-v" + version, "bin/solc") + logging.info("Setting the compiler to " + str(solc_binary)) + else: + try: + solc_binary = os.environ['SOLC'] + except KeyError: + solc_binary = 'solc' + return solc_binary + + def set_db_leveldb(self, leveldb): + self.ethDb = EthLevelDB(leveldb) + self.eth = self.ethDb + self.dbtype = "leveldb" + return self.eth + + def set_db_rpc_infura(self): + self.eth = EthJsonRpc('mainnet.infura.io', 443, True) + logging.info("Using INFURA for RPC queries") + self.dbtype = "rpc" + + def set_db_rpc(self, rpc=None, rpctls=False): + if rpc == 'ganache': + rpcconfig = ('localhost', 7545, 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: + self.eth = EthJsonRpc(rpcconfig[0], int(rpcconfig[1]), rpcconfig[2]) + self.dbtype = "rpc" + logging.info("Using RPC settings: %s" % str(rpcconfig)) + else: + raise CriticalError("Invalid RPC settings, check help for details.") + + def set_db_ipc(self): + try: + self.eth = EthIpc() + self.dbtype = "ipc" + except Exception as e: + raise CriticalError( + "IPC initialization failed. Please verify that your local Ethereum node is running, or use the -i flag to connect to INFURA. \n" + str( + e)) + + def set_db_rpc_localhost(self): + self.eth = EthJsonRpc('localhost', 8545) + self.dbtype = "rpc" + logging.info("Using default RPC settings: http://localhost:8545") + + def search_db(self, search): + + def search_callback(code_hash, code, addresses, balances): + print("Matched contract with code hash " + code_hash) + for i in range(0, len(addresses)): + print("Address: " + addresses[i] + ", balance: " + str(balances[i])) + + contract_storage, _ = get_persistent_storage(self.mythril_dir) + try: + if self.dbtype=="leveldb": + contract_storage.search(search, search_callback) + else: + self.ethDB.search(search, search_callback) + except SyntaxError: + raise CriticalError("Syntax error in search expression.") + + def init_db(self): + contract_storage, _ = get_persistent_storage(self.mythril_dir) + try: + contract_storage.initialize(self.eth) + except FileNotFoundError as e: + raise CriticalError("Error syncing database over IPC: " + str(e)) + except ConnectionError as e: + raise CriticalError("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.") + + def load_from_bytecode(self, code): + address = util.get_indexed_address(0) + self.contracts.append(ETHContract(code, name="MAIN")) + return address, self.contracts[-1] # return address and contract object + + def load_from_address(self, address): + 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 as e: + 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)) + else: + 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(ETHContract(code, name=address)) + return address, self.contracts[-1] # return address and contract object + + def load_from_solidity(self, solidity_files): + """ + UPDATES self.sigs! + :param solidity_files: + :return: + """ + 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: + signatures.add_signatures_from_file(file, self.sigs) + contract = SolidityContract(file, contract_name, solc_args=self.solc_args) + logging.info("Analyzing contract %s:%s" % (file, contract.name)) + except FileNotFoundError: + raise CriticalError("Input file not found: " + file) + except CompilerError as e: + raise CriticalError(e) + except NoContractFoundError: + logging.info("The file " + file + " does not contain a compilable contract.") + else: + self.contracts.append(contract) + contracts.append(contract) + + self._update_signatures(self.sigs) + return address, contracts + + def dump_statespaces(self, contracts=None, address=None, max_depth=12): + statespaces = [] + + for contract in (contracts or self.contracts): + sym = SymExecWrapper(contract, address, + dynloader=DynLoader(self.eth) if self.dynld else None, + max_depth=max_depth) + statespaces.append((contract, get_serializable_statespace(sym))) + + return statespaces + + def graph_html(self, contract, address, max_depth=12, enable_physics=False, phrackify=False): + sym = SymExecWrapper(contract, address, + dynloader=DynLoader(self.eth) if self.dynld else None, + max_depth=max_depth) + return generate_graph(sym, physics=enable_physics, phrackify=phrackify) + + def fire_lasers(self, contracts=None, address=None, + modules=None, + verbose_report=False, max_depth=12): + + all_issues = [] + for contract in (contracts or self.contracts): + + sym = SymExecWrapper(contract, address, + dynloader=DynLoader(self.eth) if self.dynld else None, + max_depth=max_depth) + + issues = fire_lasers(sym, modules) + + if type(contract) == SolidityContract: + for issue in issues: + issue.add_code_info(contract) + + all_issues += issues + + # Finally, output the results + report = Report(verbose_report) + for issue in all_issues: + report.append_issue(issue) + + return report + + def get_state_variable_from_storage(self, address, params=[]): + (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 as e: + 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) + + def disassemble(self, contract): + return contract.get_easm() + + @staticmethod + def hash_for_function_signature(sig): + return "0x%s" % utils.sha3(sig)[:4].hex()