diff --git a/README.md b/README.md index e2c1cb15..5834b17b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ Run `myth -x` with one of the input options described below to run the analysis. Mythril detects a range of [security issues](security_checks.md), including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. However, the analysis will not detect business logic issues and is not equivalent to formal verification. +### Analyzing a Truffle project + +[Truffle Suite](http://truffleframework.com) is a popular development framework for Ethereum. To analyze the smart contracts in a Truffle project, change in the project root directory and make run `truffle compile` followed by `myth --truffle`. + ### Analyzing Solidity code In order to work with Solidity source code files, the [solc command line compiler](http://solidity.readthedocs.io/en/develop/using-the-compiler.html) needs to be installed and in path. You can then provide the source file(s) as positional arguments, e.g.: @@ -54,7 +58,7 @@ Alternatively, compile the code on [Remix](http://remix.ethereum.org) and pass t $ myth -x -c "0x5060(...)" ``` -If you have multiple interdependent contracts, pass them to Mythril as separate input files. Mythril will map the first contract to address "0x0000(..)", the second one to "0x1111(...)", and so forth (make sure that contract addresses are set accordingly in the source). The contract passed in the first argument will be executed as the "main" contract. +If you have multiple interdependent contracts, pass them to Mythril as separate input files. Mythril will map the first contract to address "0x0000(..)", the second one to "0x1111(...)", and so forth (make sure that contract addresses are set accordingly in the source). The contract passed as the first argument will be used as analysis entrypoint. ```bash $ myth -x myContract.sol myLibrary.sol diff --git a/myth b/myth index cead5237..605f233e 100755 --- a/myth +++ b/myth @@ -12,6 +12,7 @@ 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 from mythril.analysis.symbolic import StateSpace @@ -41,38 +42,28 @@ def exitWithError(message): sys.exit() -parser = argparse.ArgumentParser(description='Bug hunting on the Ethereum blockchain') +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('-d', '--disassemble', action='store_true', help='disassemble') 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') -commands.add_argument('-t', '--trace', action='store_true', help='trace contract, use with --data (optional)') -commands.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION') -commands.add_argument('--xrefs', action='store_true', help='get xrefs from a contract') -commands.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE') -commands.add_argument('--storage', help='read state variables from storage index (index,length,array(in case of arrays)), use with -a', metavar='INDEX') -commands.add_argument('--init-db', action='store_true', help='initialize the contract database') +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)') 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 (experimental)') -inputs.add_argument('--data', help='message call input data for tracing') +inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies from the blockchain') -rpc = parser.add_argument_group('RPC options') -rpc.add_argument('--rpchost', default='127.0.0.1', help='RPC host') -rpc.add_argument('--rpcport', type=int, default=8545, help='RPC port') -rpc.add_argument('--rpctls', type=bool, default=False, help='RPC port') +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') -presets = parser.add_argument_group('RPC presets') -presets.add_argument('--ganache', action='store_true', help='Use local Ganache') -presets.add_argument('--infura-mainnet', action='store_true', help='Use Infura Node service (Mainnet)') -presets.add_argument('--infura-rinkeby', action='store_true', help='Use Infura Node service (Rinkeby)') -presets.add_argument('--infura-kovan', action='store_true', help='Use Infura Node service (Kovan)') -presets.add_argument('--infura-ropsten', action='store_true', help='Use Infura Node service (Ropsten)') +utils = parser.add_argument_group('utilities') +utils.add_argument('-d', '--disassemble', action='store_true', help='print disassembly') +utils.add_argument('--xrefs', action='store_true', help='get xrefs from a contract') +utils.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE') +utils.add_argument('--storage', help='read state variables from storage index (index,length,array(in case of arrays)), use with -a', metavar='INDEX') options = parser.add_argument_group('options') options.add_argument('--ipc', help='use IPC interface instead of RPC', action='store_true') @@ -81,6 +72,16 @@ options.add_argument('--max-depth', type=int, default=12, help='Maximum recursio options.add_argument('--enable-physics', type=bool, default=False, help='enable graph physics simulation') options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL') +rpc = parser.add_argument_group('RPC options') +rpc.add_argument('--rpchost', default='127.0.0.1', help='RPC host') +rpc.add_argument('--rpcport', type=int, default=8545, help='RPC port') +rpc.add_argument('--rpctls', type=bool, default=False, help='RPC port') +rpc.add_argument('--ganache', action='store_true', help='Preset: local Ganache') +rpc.add_argument('--infura-mainnet', action='store_true', help='Preset: Infura Node service (Mainnet)') +rpc.add_argument('--infura-rinkeby', action='store_true', help='Preset: Infura Node service (Rinkeby)') +rpc.add_argument('--infura-kovan', action='store_true', help='Preset: Infura Node service (Kovan)') +rpc.add_argument('--infura-ropsten', action='store_true', help='Preset: Infura Node service (Ropsten)') + # Get config values try: @@ -127,7 +128,7 @@ else: args = parser.parse_args() -if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.trace or args.storage): +if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.storage or args.truffle): parser.print_help() sys.exit() @@ -139,6 +140,17 @@ elif (args.hash): print("0x" + utils.sha3(args.hash)[:4].hex()) sys.exit() + +if args.truffle: + + try: + analyze_truffle_project() + 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() + + # Establish RPC/IPC connection if necessary if (args.address or len(args.solidity_file) or args.init_db): @@ -234,7 +246,7 @@ else: if args.storage: if not args.address: - exitWithError("To read storage, provide the address of a deployed contract with the -a option.") + exitWithError("To read storage, provide the address of a deployed contract with the -a option.") else: position = 0 length = 1 @@ -269,20 +281,6 @@ elif (args.disassemble): easm_text = contracts[0].get_easm() sys.stdout.write(easm_text) -elif (args.trace): - - if (args.data): - trace = evm.trace(contracts[0].code, args.data) - - else: - trace = evm.trace(contracts[0].code) - - for i in trace: - if (re.match(r'^PUSH.*', i['op'])): - print(str(i['pc']) + " " + i['op'] + " " + i['pushvalue'] + ";\tSTACK: " + i['stack']) - else: - print(str(i['pc']) + " " + i['op'] + ";\tSTACK: " + i['stack']) - elif (args.xrefs): print("\n".join(contracts[0].get_xrefs())) @@ -314,7 +312,14 @@ elif (args.graph) or (args.fire_lasers): else: states = StateSpace(contracts, max_depth=args.max_depth) - fire_lasers(states) + report = fire_lasers(states) + + if (len(report.issues)): + print(report.as_text()) + + else: + + print("The analysis was completed successfully. No issues were detected.") else: parser.print_help() diff --git a/mythril/analysis/modules/integer_underflow.py b/mythril/analysis/modules/integer_underflow.py index 1ae9d601..d74b930e 100644 --- a/mythril/analysis/modules/integer_underflow.py +++ b/mythril/analysis/modules/integer_underflow.py @@ -38,7 +38,8 @@ def execute(statespace): continue if (re.search(r'calldatasize_', str(op0))) \ - or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)): + or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \ + or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)): # Filter for patterns that contain possible (but apparently non-exploitable) Integer underflows. diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index eead5278..70fa96e6 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -11,6 +11,7 @@ class Issue: self.description = description self.type = _type self.debug = debug + self.code = None def as_dict(self): @@ -47,10 +48,12 @@ class Report: text += issue.description + "\n--------------------\n" + if issue.code: + text += "Affected code:\n\n" + issue.code + "\n--------------------\n" + if len(issue.debug): text += "++++ Debugging info ++++\n" + issue.debug + "\n" text+="\n" return text - diff --git a/mythril/analysis/security.py b/mythril/analysis/security.py index 164a0a4f..c4078455 100644 --- a/mythril/analysis/security.py +++ b/mythril/analysis/security.py @@ -25,8 +25,6 @@ def fire_lasers(statespace): for i in range(0, len(issues)): report.append_issue(issues[i]) - print(report.as_text()) + + return report - else: - - print("The analysis was completed successfully. No issues were detected.") diff --git a/mythril/ether/ethcontract.py b/mythril/ether/ethcontract.py index 5a5f3ffe..dd5622d6 100644 --- a/mythril/ether/ethcontract.py +++ b/mythril/ether/ethcontract.py @@ -8,11 +8,18 @@ class ETHContract(persistent.Persistent): def __init__(self, code, creation_code="", name="", address=""): - self.code = code self.creation_code = creation_code self.name = name self.address = address + # Workaround: We currently do not support compile-time linking. + # Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address + + code = re.sub(r'(_+[A-Za-z0-9]+_+)', 'aa' * 20, code) + + self.code = code + + def as_dict(self): return { diff --git a/mythril/ether/util.py b/mythril/ether/util.py index 350e08c2..98c7f7d1 100644 --- a/mythril/ether/util.py +++ b/mythril/ether/util.py @@ -9,6 +9,7 @@ import re def safe_decode(hex_encoded_string): + if (hex_encoded_string.startswith("0x")): return bytes.fromhex(hex_encoded_string[2:]) else: diff --git a/mythril/support/truffle.py b/mythril/support/truffle.py new file mode 100644 index 00000000..81b775f7 --- /dev/null +++ b/mythril/support/truffle.py @@ -0,0 +1,87 @@ +import os +import re +import sys +import json +from mythril.ether import util +from mythril.ether.ethcontract import ETHContract +from mythril.analysis.security import fire_lasers +from mythril.analysis.symbolic import StateSpace +from laser.ethereum import helper + + +def analyze_truffle_project(): + + project_root = os.getcwd() + + build_dir = os.path.join(project_root, "build", "contracts") + + files = os.listdir(build_dir) + + for filename in files: + + if re.match(r'.*\.json$', filename) and filename != "Migrations.json": + + with open(os.path.join(build_dir, filename)) as cf: + contractdata = json.load(cf) + + try: + name = contractdata['contractName'] + bytecode = contractdata['deployedBytecode'] + except: + print("Unable to parse contract data. Please use Truffle 4 to compile your project.") + sys.exit() + + + if (len(bytecode) < 4): + continue + + ethcontract= ETHContract(bytecode, name=name, address = util.get_indexed_address(0)) + + contracts = [ethcontract] + + states = StateSpace(contracts, max_depth = 10) + report = fire_lasers(states) + + # augment with source code + + disassembly = ethcontract.get_disassembly() + source = contractdata['source'] + + deployedSourceMap = contractdata['deployedSourceMap'].split(";") + + mappings = [] + i = 0 + + while(i < len(deployedSourceMap)): + + m = re.search(r"^(\d+):*(\d+)", deployedSourceMap[i]) + + if (m): + offset = m.group(1) + length = m.group(2) + else: + m = re.search(r"^:(\d+)", deployedSourceMap[i]) + + if m: + length = m.group(1) + + mappings.append((int(offset), int(length))) + + i += 1 + + for key, issue in report.issues.items(): + + index = helper.get_instruction_index(disassembly.instruction_list, issue.pc) + + if index: + issue.code_start = mappings[index][0] + issue.code_length = mappings[index][1] + issue.code = source[mappings[index][0]: mappings[index][0] + mappings[index][1]] + + + if len(report.issues): + print("Analysis result for " + name + ":\n" + report.as_text()) + else: + print("Analysis result for " + name + ": No issues found.") + + diff --git a/setup.py b/setup.py index 5f58f98f..148d9ad9 100755 --- a/setup.py +++ b/setup.py @@ -254,7 +254,7 @@ Credit setup( name='mythril', - version='0.9.1', + version='0.10.2', description='Security analysis tool for Ethereum smart contracts', long_description=long_description,