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_api_rpc_infura() # (optional) other API adapters mythril.set_api_rpc(args) mythril.set_api_ipc() mythril.set_api_rpc_localhost() mythril.set_api_leveldb(path) # (optional) other func mythril.analyze_truffle_project(args) mythril.search_db(args) # 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.sigs = signatures.SignatureDb() try: self.sigs.open( ) # tries mythril_dir/signatures.json by default (provide path= arg to make this configurable) except FileNotFoundError as fnfe: logging.info( "No signature database found. Creating database if sigs are loaded in: " + self.sigs.signatures_file + "\n" + "Consider replacing it with the pre-initialized database at https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json" ) except json.JSONDecodeError as jde: raise CriticalError("Invalid JSON in signatures file " + self.sigs.signatures_file + "\n" + str(jde)) self.solc_binary = self._init_solc_binary(solv) self.config_path = os.path.join(self.mythril_dir, 'config.ini') self.leveldb_dir = self._init_config() self.eth = None # ethereum API client self.eth_db = None # ethereum LevelDB client 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_config(self): """ If no config file exists, create it and add default options. Default LevelDB path is specified based on OS dynamic loading is set to infura by default in the file Returns: leveldb directory """ 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") leveldb_fallback_dir = os.path.join(leveldb_fallback_dir, "geth", "chaindata") if not os.path.exists(self.config_path): logging.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_fallback_dir) 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_fallback_dir) return os.path.expanduser(leveldb_dir) @staticmethod def _add_default_options(config): config.add_section('defaults') @staticmethod def _add_leveldb_option(config, leveldb_fallback_dir): 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): config.set('defaults', '#– To connect to Infura use dynamic_loading: infura') config.set('defaults', '#– To connect to Ipc use dynamic_loading: ipc') 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 analyze_truffle_project(self, *args, **kwargs): return analyze_truffle_project( self.sigs, *args, **kwargs) # just passthru by passing signatures 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_api_leveldb(self, leveldb): self.eth_db = EthLevelDB(leveldb) self.eth = self.eth_db return self.eth def set_api_rpc_infura(self): self.eth = EthJsonRpc('mainnet.infura.io', 443, True) logging.info("Using INFURA for RPC queries") def set_api_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]) logging.info("Using RPC settings: %s" % str(rpcconfig)) else: raise CriticalError( "Invalid RPC settings, check help for details.") def set_api_ipc(self): try: self.eth = EthIpc() 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_api_rpc_localhost(self): self.eth = EthJsonRpc('localhost', 8545) logging.info("Using default RPC settings: http://localhost:8545") def set_api_from_config_path(self): config = ConfigParser(allow_no_value=False) config.optionxform = str 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' if dynamic_loading == 'ipc': self.set_api_ipc() elif dynamic_loading == 'infura': self.set_api_rpc_infura() elif dynamic_loading == 'localhost': self.set_api_rpc_localhost() else: self.set_api_rpc(dynamic_loading) def search_db(self, search): def search_callback(contract, address, balance): print("Address: " + address + ", balance: " + str(balance)) try: self.eth_db.search(search, search_callback) except SyntaxError: raise CriticalError("Syntax error in search expression.") def contract_hash_to_address(self, hash): if not re.match(r'0x[a-fA-F0-9]{64}', hash): raise CriticalError( "Invalid address hash. Expected format is '0x...'.") print(self.eth_db.contract_hash_to_address(hash)) 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: # import signatures from solidity source with open(file, encoding="utf-8") as f: self.sigs.import_from_solidity_source(f.read()) # Save updated function signatures self.sigs.write( ) # dump signatures to disk (previously opened file or default location) if contract_name is not None: contract = SolidityContract(file, contract_name, solc_args=self.solc_args) self.contracts.append(contract) contracts.append(contract) else: for contract in get_contracts_from_file( file, solc_args=self.solc_args): 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: logging.info("The file " + file + " does not contain a compilable contract.") return address, contracts def dump_statespace(self, strategy, contract, address=None, max_depth=12): sym = SymExecWrapper( contract, address, strategy, dynloader=DynLoader(self.eth) if self.dynld else None, max_depth=max_depth) return get_serializable_statespace(sym) def graph_html(self, strategy, contract, address, max_depth=12, enable_physics=False, phrackify=False, execution_timeout=None): sym = SymExecWrapper( contract, address, strategy, dynloader=DynLoader(self.eth) if self.dynld else None, max_depth=max_depth, execution_timeout=execution_timeout) return generate_graph(sym, physics=enable_physics, phrackify=phrackify) def fire_lasers( self, strategy, contracts=None, address=None, modules=None, verbose_report=False, max_depth=None, execution_timeout=None, ): all_issues = [] for contract in (contracts or self.contracts): sym = SymExecWrapper( contract, address, strategy, dynloader=DynLoader(self.eth) if self.dynld else None, max_depth=max_depth, execution_timeout=execution_timeout) 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()
def set_api_leveldb(self, leveldb): self.eth_db = EthLevelDB(leveldb) self.eth = self.eth_db return self.eth
def set_db_leveldb(self, leveldb): self.ethDb = EthLevelDB(leveldb) self.eth = self.ethDb self.dbtype = "leveldb" return self.eth
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])) try: if self.dbtype == "leveldb": self.ethDb.search(search, search_callback) else: contract_storage, _ = get_persistent_storage(self.mythril_dir) contract_storage.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) self._update_signatures(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) return address, contracts def dump_statespace(self, contract, address=None, max_depth=12): sym = SymExecWrapper( contract, address, dynloader=DynLoader(self.eth) if self.dynld else None, max_depth=max_depth) return get_serializable_statespace(sym) 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()
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='<text/json>') 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()