Turn mythril into a MythX client

analyze-with-mythx
Nathan 5 years ago
parent fd397ed232
commit 1080184b8a
  1. 6
      mythril/analysis/templates/report_as_markdown.jinja2
  2. 4
      mythril/analysis/templates/report_as_text.jinja2
  3. 21
      mythril/ethereum/util.py
  4. 89
      mythril/interfaces/cli.py
  5. 93
      mythril/mythx/__init__.py
  6. 58
      mythril/solidity/soliditycontract.py
  7. 1
      requirements.txt
  8. 1
      setup.py
  9. 6
      tests/disassembler_test.py

@ -6,15 +6,21 @@
- SWC ID: {{ issue['swc-id'] }} - SWC ID: {{ issue['swc-id'] }}
- Severity: {{ issue.severity }} - Severity: {{ issue.severity }}
- Contract: {{ issue.contract | default("Unknown") }} - Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
- Function name: `{{ issue.function }}` - Function name: `{{ issue.function }}`
{% endif %}
- PC address: {{ issue.address }} - PC address: {{ issue.address }}
{% if issue.min_gas_used or issue.max_gas_used %}
- Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} - Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
{% endif %}
### Description ### Description
{{ issue.description.rstrip() }} {{ issue.description.rstrip() }}
{% if issue.filename and issue.lineno %} {% if issue.filename and issue.lineno %}
In file: {{ issue.filename }}:{{ issue.lineno }} In file: {{ issue.filename }}:{{ issue.lineno }}
{% elif issue.filename %}
In file: {{ issue.filename }}
{% endif %} {% endif %}
{% if issue.code %} {% if issue.code %}

@ -4,9 +4,13 @@
SWC ID: {{ issue['swc-id'] }} SWC ID: {{ issue['swc-id'] }}
Severity: {{ issue.severity }} Severity: {{ issue.severity }}
Contract: {{ issue.contract | default("Unknown") }} Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
Function name: {{ issue.function }} Function name: {{ issue.function }}
{% endif %}
PC address: {{ issue.address }} PC address: {{ issue.address }}
{% if issue.min_gas_used or issue.max_gas_used %}
Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
{% endif %}
{{ issue.description }} {{ issue.description }}
-------------------- --------------------
{% if issue.filename and issue.lineno %} {% if issue.filename and issue.lineno %}

@ -34,6 +34,7 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
""" """
cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"]
cmd = [solc_binary, "--standard-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"]
if solc_args: if solc_args:
cmd.extend(solc_args.split()) cmd.extend(solc_args.split())
@ -46,10 +47,24 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
cmd.append(file) cmd.append(file)
try: input_json = json.dumps(
p = Popen(cmd, stdout=PIPE, stderr=PIPE) {
"language": "Solidity",
"sources": {file: {"urls": [file]}},
"settings": {
"outputSelection": {
"*": {
"": ["ast"],
"*": ["metadata", "evm.bytecode", "evm.deployedBytecode"],
}
}
},
}
)
stdout, stderr = p.communicate() try:
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate(bytes(input_json, "utf8"))
ret = p.returncode ret = p.returncode
if ret != 0: if ret != 0:

@ -16,6 +16,8 @@ import traceback
import mythril.support.signatures as sigs import mythril.support.signatures as sigs
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from mythril import mythx
from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.exceptions import AddressNotFoundError, CriticalError
from mythril.mythril import ( from mythril.mythril import (
MythrilAnalyzer, MythrilAnalyzer,
@ -27,12 +29,14 @@ from mythril.__version__ import __version__ as VERSION
ANALYZE_LIST = ("analyze", "a") ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d") DISASSEMBLE_LIST = ("disassemble", "d")
PRO_LIST = ("pro", "p")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
COMMAND_LIST = ( COMMAND_LIST = (
ANALYZE_LIST ANALYZE_LIST
+ DISASSEMBLE_LIST + DISASSEMBLE_LIST
+ PRO_LIST
+ ( + (
"read-storage", "read-storage",
"leveldb-search", "leveldb-search",
@ -41,6 +45,7 @@ COMMAND_LIST = (
"version", "version",
"truffle", "truffle",
"help", "help",
"pro",
) )
) )
@ -72,7 +77,27 @@ def exit_with_error(format_, message):
sys.exit() sys.exit()
def get_input_parser() -> ArgumentParser: def get_runtime_input_parser() -> ArgumentParser:
"""
Returns Parser which handles input
:return: Parser which handles input
"""
parser = ArgumentParser(add_help=False)
parser.add_argument(
"-a",
"--address",
help="pull contract from the blockchain",
metavar="CONTRACT_ADDRESS",
)
parser.add_argument(
"--bin-runtime",
action="store_true",
help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.",
)
return parser
def get_creation_input_parser() -> ArgumentParser:
""" """
Returns Parser which handles input Returns Parser which handles input
:return: Parser which handles input :return: Parser which handles input
@ -91,17 +116,6 @@ def get_input_parser() -> ArgumentParser:
metavar="BYTECODEFILE", metavar="BYTECODEFILE",
type=argparse.FileType("r"), type=argparse.FileType("r"),
) )
parser.add_argument(
"-a",
"--address",
help="pull contract from the blockchain",
metavar="CONTRACT_ADDRESS",
)
parser.add_argument(
"--bin-runtime",
action="store_true",
help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.",
)
return parser return parser
@ -165,7 +179,8 @@ def main() -> None:
rpc_parser = get_rpc_parser() rpc_parser = get_rpc_parser()
utilities_parser = get_utilities_parser() utilities_parser = get_utilities_parser()
input_parser = get_input_parser() runtime_input_parser = get_runtime_input_parser()
creation_input_parser = get_creation_input_parser()
output_parser = get_output_parser() output_parser = get_output_parser()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Security analysis of Ethereum smart contracts" description="Security analysis of Ethereum smart contracts"
@ -179,7 +194,13 @@ def main() -> None:
analyzer_parser = subparsers.add_parser( analyzer_parser = subparsers.add_parser(
ANALYZE_LIST[0], ANALYZE_LIST[0],
help="Triggers the analysis of the smart contract", help="Triggers the analysis of the smart contract",
parents=[rpc_parser, utilities_parser, input_parser, output_parser], parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
output_parser,
],
aliases=ANALYZE_LIST[1:], aliases=ANALYZE_LIST[1:],
) )
create_analyzer_parser(analyzer_parser) create_analyzer_parser(analyzer_parser)
@ -188,10 +209,23 @@ def main() -> None:
DISASSEMBLE_LIST[0], DISASSEMBLE_LIST[0],
help="Disassembles the smart contract", help="Disassembles the smart contract",
aliases=DISASSEMBLE_LIST[1:], aliases=DISASSEMBLE_LIST[1:],
parents=[rpc_parser, utilities_parser, input_parser], parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
],
) )
create_disassemble_parser(disassemble_parser) create_disassemble_parser(disassemble_parser)
pro_parser = subparsers.add_parser(
PRO_LIST[0],
help="Analyzes input with the MythX API (https://mythx.io)",
aliases=PRO_LIST[1],
parents=[utilities_parser, creation_input_parser, output_parser],
)
create_pro_parser(pro_parser)
read_storage_parser = subparsers.add_parser( read_storage_parser = subparsers.add_parser(
"read-storage", "read-storage",
help="Retrieves storage slots from a given address through rpc", help="Retrieves storage slots from a given address through rpc",
@ -233,6 +267,20 @@ def create_disassemble_parser(parser: ArgumentParser):
parser.add_argument("solidity_file", nargs="*") parser.add_argument("solidity_file", nargs="*")
def create_pro_parser(parser: ArgumentParser):
"""
Modify parser to handle mythx analysis
:param parser:
:return:
"""
parser.add_argument(
"--full",
help="Run a full analysis. Default: quick analysis",
action="store_true",
)
parser.add_argument("solidity_file", nargs="*")
def create_read_storage_parser(read_storage_parser: ArgumentParser): def create_read_storage_parser(read_storage_parser: ArgumentParser):
""" """
Modify parser to handle storage slots Modify parser to handle storage slots
@ -539,6 +587,17 @@ def execute_command(
) )
print(storage) print(storage)
elif args.command in PRO_LIST:
mode = "full" if args.full else "quick"
report = mythx.analyze(disassembler.contracts, mode)
outputs = {
"json": report.as_json(),
"jsonv2": report.as_swc_standard_format(),
"text": report.as_text(),
"markdown": report.as_markdown(),
}
print(outputs[args.outform])
elif args.command in DISASSEMBLE_LIST: elif args.command in DISASSEMBLE_LIST:
if disassembler.contracts[0].code: if disassembler.contracts[0].code:
print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm())

@ -0,0 +1,93 @@
import os
import time
from typing import List, Dict, Any
from mythril.analysis.report import Issue, Report
from mythril.solidity.soliditycontract import SolidityContract
from pythx import Client
def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> Report:
"""
Analyze contracts via the MythX API.
:param contracts: List of solidity contracts to analyze
:param analysis_mode: The mode to submit the analysis request with. "quick" or "full" (default: "quick")
:return: Report with analyzed contracts
"""
assert analysis_mode in ("quick", "full"), "analysis_mode must be 'quick' or 'full'"
c = Client(
eth_address=os.environ.get(
"MYTHX_ETH_ADDRESS", "0x0000000000000000000000000000000000000000"
),
password=os.environ.get("MYTHX_PASSWORD", "trial"),
)
issues = [] # type: List[Issue]
# TODO: Analyze multiple contracts asynchronously.
for contract in contracts:
source_codes = {}
source_list = []
sources = {} # type: Dict[str, Any]
main_source = None
try:
main_source = contract.input_file
for solidity_file in contract.solidity_files:
source_codes[solidity_file.filename] = solidity_file.data
for filename in contract.solc_json["sources"].keys():
sources[filename] = {}
if source_codes[filename]:
sources[filename]["source"] = source_codes[filename]
sources[filename]["ast"] = contract.solc_json["sources"][filename][
"ast"
]
source_list.append(filename)
source_list.sort(
key=lambda fname: contract.solc_json["sources"][fname]["id"]
)
except AttributeError:
# No solidity file
pass
assert contract.creation_code, "Creation bytecode must exist."
resp = c.analyze(
contract_name=contract.name,
analysis_mode=analysis_mode,
bytecode=contract.creation_code or None,
deployed_bytecode=contract.code or None,
sources=sources or None,
main_source=main_source,
source_list=source_list or None,
)
while not c.analysis_ready(resp.uuid):
print(c.status(resp.uuid).analysis)
time.sleep(5)
for issue in c.report(resp.uuid):
issue = Issue(
contract=contract.name,
function_name=None,
address=int(issue.locations[0].source_map.split(":")[0]),
swc_id=issue.swc_id[4:], # remove 'SWC-' prefix
title=issue.swc_title,
bytecode=contract.creation_code,
severity=issue.severity.capitalize(),
description_head=issue.description_short,
description_tail=issue.description_long,
)
issue.add_code_info(contract)
issues.append(issue)
report = Report(contracts=contracts)
for issue in issues:
report.append_issue(issue)
return report

@ -54,12 +54,15 @@ def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"):
data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary)
try: try:
for key, contract in data["contracts"].items(): for contractName in data["contracts"][input_file].keys():
filename, name = key.split(":") if len(
if filename == input_file and len(contract["bin-runtime"]): data["contracts"][input_file][contractName]["evm"]["deployedBytecode"][
"object"
]
):
yield SolidityContract( yield SolidityContract(
input_file=input_file, input_file=input_file,
name=name, name=contractName,
solc_args=solc_args, solc_args=solc_args,
solc_binary=solc_binary, solc_binary=solc_binary,
) )
@ -74,12 +77,14 @@ class SolidityContract(EVMContract):
data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary)
self.solidity_files = [] self.solidity_files = []
self.solc_json = data
self.input_file = input_file
for filename in data["sourceList"]: for filename, contract in data["sources"].items():
with open(filename, "r", encoding="utf-8") as file: with open(filename, "r", encoding="utf-8") as file:
code = file.read() code = file.read()
full_contract_src_maps = self.get_full_contract_src_maps( full_contract_src_maps = self.get_full_contract_src_maps(
data["sources"][filename]["AST"] contract["ast"]
) )
self.solidity_files.append( self.solidity_files.append(
SolidityFile(filename, code, full_contract_src_maps) SolidityFile(filename, code, full_contract_src_maps)
@ -91,32 +96,25 @@ class SolidityContract(EVMContract):
srcmap_constructor = [] srcmap_constructor = []
srcmap = [] srcmap = []
if name: if name:
for key, contract in sorted(data["contracts"].items()): contract = data["contracts"][input_file][name]
filename, _name = key.split(":") if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
if ( creation_code = contract["evm"]["bytecode"]["object"]
filename == input_file srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
and name == _name srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(";")
and len(contract["bin-runtime"]) has_contract = True
):
code = contract["bin-runtime"]
creation_code = contract["bin"]
srcmap = contract["srcmap-runtime"].split(";")
srcmap_constructor = contract["srcmap"].split(";")
has_contract = True
break
# If no contract name is specified, get the last bytecode entry for the input file # If no contract name is specified, get the last bytecode entry for the input file
else: else:
for key, contract in sorted(data["contracts"].items()): for filename, contract in sorted(data["contracts"][input_file].items()):
filename, name = key.split(":") if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
if filename == input_file and len(contract["bin-runtime"]): creation_code = contract["evm"]["bytecode"]["object"]
code = contract["bin-runtime"] srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
creation_code = contract["bin"] srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(
srcmap = contract["srcmap-runtime"].split(";") ";"
srcmap_constructor = contract["srcmap"].split(";") )
has_contract = True has_contract = True
if not has_contract: if not has_contract:
@ -139,8 +137,8 @@ class SolidityContract(EVMContract):
:return: The source maps :return: The source maps
""" """
source_maps = set() source_maps = set()
for child in ast["children"]: for child in ast["nodes"]:
if "contractKind" in child["attributes"]: if child.get("contractKind"):
source_maps.add(child["src"]) source_maps.add(child["src"])
return source_maps return source_maps

@ -28,3 +28,4 @@ transaction>=2.2.1
z3-solver>=4.8.5.0 z3-solver>=4.8.5.0
pysha3 pysha3
matplotlib matplotlib
pythx

@ -50,6 +50,7 @@ REQUIRED = [
"persistent>=4.2.0", "persistent>=4.2.0",
"ethereum-input-decoder>=0.2.2", "ethereum-input-decoder>=0.2.2",
"matplotlib", "matplotlib",
"pythx",
] ]
TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"] TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"]

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save