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. 56
      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'] }}
- Severity: {{ issue.severity }}
- Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
- Function name: `{{ issue.function }}`
{% endif %}
- 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 }}
{% endif %}
### Description
{{ issue.description.rstrip() }}
{% if issue.filename and issue.lineno %}
In file: {{ issue.filename }}:{{ issue.lineno }}
{% elif issue.filename %}
In file: {{ issue.filename }}
{% endif %}
{% if issue.code %}

@ -4,9 +4,13 @@
SWC ID: {{ issue['swc-id'] }}
Severity: {{ issue.severity }}
Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
Function name: {{ issue.function }}
{% endif %}
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 }}
{% endif %}
{{ issue.description }}
--------------------
{% 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, "--standard-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"]
if solc_args:
cmd.extend(solc_args.split())
@ -46,10 +47,24 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
cmd.append(file)
try:
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
input_json = json.dumps(
{
"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
if ret != 0:

@ -16,6 +16,8 @@ import traceback
import mythril.support.signatures as sigs
from argparse import ArgumentParser, Namespace
from mythril import mythx
from mythril.exceptions import AddressNotFoundError, CriticalError
from mythril.mythril import (
MythrilAnalyzer,
@ -27,12 +29,14 @@ from mythril.__version__ import __version__ as VERSION
ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d")
PRO_LIST = ("pro", "p")
log = logging.getLogger(__name__)
COMMAND_LIST = (
ANALYZE_LIST
+ DISASSEMBLE_LIST
+ PRO_LIST
+ (
"read-storage",
"leveldb-search",
@ -41,6 +45,7 @@ COMMAND_LIST = (
"version",
"truffle",
"help",
"pro",
)
)
@ -72,7 +77,27 @@ def exit_with_error(format_, message):
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
:return: Parser which handles input
@ -91,17 +116,6 @@ def get_input_parser() -> ArgumentParser:
metavar="BYTECODEFILE",
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
@ -165,7 +179,8 @@ def main() -> None:
rpc_parser = get_rpc_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()
parser = argparse.ArgumentParser(
description="Security analysis of Ethereum smart contracts"
@ -179,7 +194,13 @@ def main() -> None:
analyzer_parser = subparsers.add_parser(
ANALYZE_LIST[0],
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:],
)
create_analyzer_parser(analyzer_parser)
@ -188,10 +209,23 @@ def main() -> None:
DISASSEMBLE_LIST[0],
help="Disassembles the smart contract",
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)
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",
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="*")
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):
"""
Modify parser to handle storage slots
@ -539,6 +587,17 @@ def execute_command(
)
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:
if disassembler.contracts[0].code:
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)
try:
for key, contract in data["contracts"].items():
filename, name = key.split(":")
if filename == input_file and len(contract["bin-runtime"]):
for contractName in data["contracts"][input_file].keys():
if len(
data["contracts"][input_file][contractName]["evm"]["deployedBytecode"][
"object"
]
):
yield SolidityContract(
input_file=input_file,
name=name,
name=contractName,
solc_args=solc_args,
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)
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:
code = file.read()
full_contract_src_maps = self.get_full_contract_src_maps(
data["sources"][filename]["AST"]
contract["ast"]
)
self.solidity_files.append(
SolidityFile(filename, code, full_contract_src_maps)
@ -91,32 +96,25 @@ class SolidityContract(EVMContract):
srcmap_constructor = []
srcmap = []
if name:
for key, contract in sorted(data["contracts"].items()):
filename, _name = key.split(":")
if (
filename == input_file
and name == _name
and len(contract["bin-runtime"])
):
code = contract["bin-runtime"]
creation_code = contract["bin"]
srcmap = contract["srcmap-runtime"].split(";")
srcmap_constructor = contract["srcmap"].split(";")
contract = data["contracts"][input_file][name]
if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
creation_code = contract["evm"]["bytecode"]["object"]
srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(";")
has_contract = True
break
# If no contract name is specified, get the last bytecode entry for the input file
else:
for key, contract in sorted(data["contracts"].items()):
filename, name = key.split(":")
if filename == input_file and len(contract["bin-runtime"]):
code = contract["bin-runtime"]
creation_code = contract["bin"]
srcmap = contract["srcmap-runtime"].split(";")
srcmap_constructor = contract["srcmap"].split(";")
for filename, contract in sorted(data["contracts"][input_file].items()):
if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
creation_code = contract["evm"]["bytecode"]["object"]
srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(
";"
)
has_contract = True
if not has_contract:
@ -139,8 +137,8 @@ class SolidityContract(EVMContract):
:return: The source maps
"""
source_maps = set()
for child in ast["children"]:
if "contractKind" in child["attributes"]:
for child in ast["nodes"]:
if child.get("contractKind"):
source_maps.add(child["src"])
return source_maps

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

@ -50,6 +50,7 @@ REQUIRED = [
"persistent>=4.2.0",
"ethereum-input-decoder>=0.2.2",
"matplotlib",
"pythx",
]
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