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