Ejemplo n.º 1
0
class RpcTest(TestCase):
    client = None

    def setUp(self):
        self.client = EthJsonRpc()

    def tearDown(self):
        self.client.close()

    def test_eth_coinbase(self):
        coinbase = self.client.eth_coinbase()
        self.assertTrue(coinbase.startswith("0x"),
                        "coinbase should be a hex string")
        self.assertEqual(len(coinbase), 42,
                         "coinbase is a string with length of 42")

    def test_eth_blockNumber(self):
        block_number = self.client.eth_blockNumber()
        self.assertGreater(
            block_number, 0,
            "we have made sure the blockNumber is > 0 for testing")

    def test_eth_getBalance(self):
        balance = self.client.eth_getBalance(
            address="0x0000000000000000000000000000000000000000")
        self.assertGreater(balance, 10000000,
                           "specified address should have a lot of balance")

    def test_eth_getStorageAt(self):
        storage = self.client.eth_getStorageAt(
            address="0x0000000000000000000000000000000000000000")
        self.assertEqual(
            storage,
            '0x0000000000000000000000000000000000000000000000000000000000000000'
        )

    def test_eth_getBlockByNumber(self):
        block = self.client.eth_getBlockByNumber(0)
        self.assertEqual(
            block["extraData"],
            "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
            "the data of the first block should be right")

    def test_eth_getCode(self):
        # TODO: can't find a proper address for getting code
        code = self.client.eth_getCode(
            address="0x0000000000000000000000000000000000000001")
        self.assertEqual(code, "0x")

    def test_eth_getTransactionReceipt(self):
        transaction = self.client.eth_getTransactionReceipt(
            tx_hash=
            "0xe363505adc6b2996111f8bd774f8653a61d244cc6567b5372c2e781c6c63b737"
        )
        self.assertEqual(transaction["from"],
                         "0x22f2dcff5ad78c3eb6850b5cb951127b659522e6")
        self.assertEqual(transaction["to"],
                         "0x0000000000000000000000000000000000000000")
Ejemplo n.º 2
0
    def initialize(self, rpchost, rpcport, sync_all):

        eth = EthJsonRpc(rpchost, rpcport)

        if self.last_block:
            blockNum = self.last_block
            print("Resuming synchronization from block " + str(blockNum))
        else:

            blockNum = eth.eth_blockNumber()
            print("Starting synchronization from latest block: " +
                  str(blockNum))

        while (blockNum > 0):

            if not blockNum % 1000:
                print("Processing block " + str(blockNum) + ", " +
                      str(len(self.contracts.keys())) +
                      " individual contracts in database")

            block = eth.eth_getBlockByNumber(blockNum)

            for tx in block['transactions']:

                if not tx['to']:

                    receipt = eth.eth_getTransactionReceipt(tx['hash'])

                    contract_address = receipt['contractAddress']

                    contract_code = eth.eth_getCode(contract_address)
                    contract_balance = eth.eth_getBalance(contract_address)

                    if not contract_balance or sync_all:
                        # skip contracts with zero balance (disable with --sync-all)
                        continue

                    code = ETHContract(contract_code, tx['input'])

                    m = hashlib.md5()
                    m.update(contract_code.encode('UTF-8'))
                    contract_hash = m.digest()

                    try:
                        self.contracts[contract_hash]
                    except KeyError:
                        self.contracts[contract_hash] = code
                        m = InstanceList()
                        self.instance_lists[contract_hash] = m

                    self.instance_lists[contract_hash].add(
                        contract_address, contract_balance)

                    transaction.commit()

            self.last_block = blockNum
            blockNum -= 1
Ejemplo n.º 3
0
class MythrilRunner:
    """
    Class containing business logic to apply mythril analysis on smart contracts
    """
    def __init__(self, rpc_settings):
        """
        Constructor for MythrilRunner
        :param rpc_settings: Settings used to connect to rpc interface
        """
        logging.debug("Initializing MythrilRunner")
        # Lets just agree to use ssl
        self.eth = EthJsonRpc(rpc_settings[0], rpc_settings[1], True)

    def analyze(self, address):
        """
        Analyse a contract using mythril
        :param address: Address of contract to analyse
        :return: Findings
        """
        # The followin code is kinda straight from mythril, thanks ;)
        # Setup
        code = self.eth.eth_getCode(address)

        contract = ETHContract(code, name=address)
        sym = SymExecWrapper(contract, address, dynloader=DynLoader(self.eth))
        # Starting analysis
        logging.debug(
            "Firing lasers on contract with address: {}".format(address))
        issues = fire_lasers(sym)

        logging.debug("Found {} issues using mythril".format(len(issues)))

        # Build findings
        findings = []
        for issue in issues:
            findings += [
                Finding("Mythril analysis", issue.title, issue.description,
                        issue.contract, issue.pc, issue.type)
            ]

        return findings
Ejemplo n.º 4
0
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()
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
        print("Referenced contracts:")

        xrefs = contract.get_xrefs()

        print ("\n".join(xrefs))

        '''
        from here on are many different options!

        In this example, we'll only check one of the refernced contracts contains the initWallet() function
        If it does, we save the disassembly and callgraph for further analysis
        
        '''
 
        for xref in xrefs:
            code = homestead.eth_getCode(xref)

            disassembly = Disassembly(code)

            if contract.matches_expression("func#initWallet(address[],uint256,uint256)#"):
                print ("initWallet() in referenced library contract: " + xref)

                # Save list of contracts that forward calls to this library contract

                cwd = os.getcwd()

                with open("contracts_calling_" + xref + ".txt", "w") as f:
                    addresses = contract_storage.instance_lists[k].addresses

                    f.write("\n".join(addresses))