diff --git a/Dockerfile b/Dockerfile
index 3e3cb592..e204e2d1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,5 @@
FROM ubuntu:bionic
-COPY . /opt/mythril
-
RUN apt-get update \
&& apt-get install -y \
build-essential \
@@ -18,14 +16,20 @@ RUN apt-get update \
python3-dev \
pandoc \
git \
- && ln -s /usr/bin/python3 /usr/local/bin/python \
- && cd /opt/mythril \
- && pip3 install -r requirements.txt \
- && python setup.py install
+ && ln -s /usr/bin/python3 /usr/local/bin/python
+
+COPY ./requirements.txt /opt/mythril/requirements.txt
+
+RUN cd /opt/mythril \
+ && pip3 install -r requirements.txt
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.en
ENV LC_ALL en_US.UTF-8
+COPY . /opt/mythril
+RUN cd /opt/mythril \
+ && python setup.py install
+
ENTRYPOINT ["/usr/local/bin/myth"]
diff --git a/README.md b/README.md
index 41566bac..4b975def 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,21 @@
-# Mythril OSS [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Mythril%20-%20Security%20Analyzer%20for%20Ethereum%20Smart%20Contracts&url=https://www.github.com/ConsenSys/mythril)
+# Mythril Classic
+
+
+
+
+
[![Discord](https://img.shields.io/discord/481002907366588416.svg)](https://discord.gg/E3YrVtG)
[![PyPI](https://badge.fury.io/py/mythril.svg)](https://pypi.python.org/pypi/mythril)
-![Master Build Status](https://img.shields.io/circleci/project/github/ConsenSys/mythril/master.svg)
-[![Waffle.io - Columns and their card count](https://badge.waffle.io/ConsenSys/mythril.svg?columns=all)](https://waffle.io/ConsenSys/mythril)
+![Master Build Status](https://img.shields.io/circleci/project/github/ConsenSys/mythril-classic/master.svg)
+[![Waffle.io - Columns and their card count](https://badge.waffle.io/ConsenSys/mythril-classic.svg?columns=In%20Progress)](https://waffle.io/ConsenSys/mythril-classic/)
[![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril)
+[![PyPI Statistics](https://pypistats.com/badge/mythril.svg)](https://pypistats.com/package/mythril)
-
-
-Mythril OSS is the classic security analysis tool for Ethereum smart contracts. It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities.
+Mythril Classic is an open-source security analysis tool for Ethereum smart contracts. It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities.
-Whether you want to contribute, need support, or want to learn what we have cooking for the future, our [Discord server](https://discord.gg/E3YrVtG) will serve your needs!
+Whether you want to contribute, need support, or want to learn what we have cooking for the future, our [Discord server](https://discord.gg/E3YrVtG) will serve your needs.
-Oh and by the way, we're now building a whole security tools ecosystem with [Mythril Platform](https://mythril.ai). You should definitely check that out as well.
+Oh and by the way, we're also building an easy-to-use security analysis platform (a.k.a. "the INFURA for smart contract security") that anybody can use to create purpose-built security tools. It's called [Mythril Platform](https://mythril.ai) and you should definitely [check it out](https://media.consensys.net/mythril-platform-api-is-upping-the-smart-contract-security-game-eee1d2642488).
## Installation and setup
@@ -31,21 +35,10 @@ See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup)
## Usage
-Instructions for using the 'myth' tool are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki).
+Instructions for using Mythril Classic are found on the [Wiki](https://github.com/ConsenSys/mythril-classic/wiki).
For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG).
## Vulnerability Remediation
Visit the [Smart Contract Vulnerability Classification Registry](https://smartcontractsecurity.github.io/SWC-registry/) to find detailed information and remediation guidance for the vulnerabilities reported.
-
-## Presentations, papers and articles
-
-- [Analyzing Ethereum Smart Contracts for Vulnerabilities](https://hackernoon.com/scanning-ethereum-smart-contracts-for-vulnerabilities-b5caefd995df)
-- [What Caused the Parity SUICIDE Vulnerability & How to Detect Similar Bugs](https://hackernoon.com/what-caused-the-latest-100-million-ethereum-bug-and-a-detection-tool-for-similar-bugs-7b80f8ab7279)
-- [Detecting Integer Overflows in Ethereum Smart Contracts](https://media.consensys.net/detecting-batchoverflow-and-similar-flaws-in-ethereum-smart-contracts-93cf5a5aaac8)
-- [How Formal Verification Can Ensure Flawless Smart Contracts](https://media.consensys.net/how-formal-verification-can-ensure-flawless-smart-contracts-cbda8ad99bd1)
-- [Smashing Smart Contracts for Fun and Real Profit](https://hackernoon.com/hitb2018ams-smashing-smart-contracts-for-fun-and-real-profit-720f5e3ac777)
-- [HITBSecConf 2018 - Presentation video](https://www.youtube.com/watch?v=iqf6epACgds)
-- [EDCon Toronto 2018 - Mythril: Find bugs and verify security properties in your contracts](https://www.youtube.com/watch?v=NJ9StJThxZY&feature=youtu.be&t=3h3m18s)
-
diff --git a/mythril/analysis/modules/deprecated_ops.py b/mythril/analysis/modules/deprecated_ops.py
index 84f39b6e..2b187e2d 100644
--- a/mythril/analysis/modules/deprecated_ops.py
+++ b/mythril/analysis/modules/deprecated_ops.py
@@ -24,7 +24,7 @@ def execute(statespace):
instruction = state.get_current_instruction()
if instruction['opcode'] == "ORIGIN":
- description = "Function %s retrieves the transaction origin (tx.origin) using the ORIGIN opcode. " \
+ description = "The function `{}` retrieves the transaction origin (tx.origin) using the ORIGIN opcode. " \
"Use msg.sender instead.\nSee also: " \
"https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin".format(node.function_name)
diff --git a/mythril/analysis/modules/multiple_sends.py b/mythril/analysis/modules/multiple_sends.py
index 0df17575..baaea896 100644
--- a/mythril/analysis/modules/multiple_sends.py
+++ b/mythril/analysis/modules/multiple_sends.py
@@ -25,8 +25,8 @@ def execute(statespace):
swc_id=MULTIPLE_SENDS, title="Multiple Calls", _type="Informational")
issue.description = \
- "Multiple sends exist in one transaction, try to isolate each external call into its own transaction." \
- " As external calls can fail accidentally or deliberately.\nConsecutive calls: \n"
+ "Multiple sends exist in one transaction. Try to isolate each external call into its own transaction," \
+ " as external calls can fail accidentally or deliberately.\nConsecutive calls: \n"
for finding in findings:
issue.description += \
@@ -38,7 +38,7 @@ def execute(statespace):
def _explore_nodes(call, statespace):
children = _child_nodes(statespace, call.node)
- sending_children = list(filter(lambda call: call.node in children, statespace.calls))
+ sending_children = list(filter(lambda c: c.node in children, statespace.calls))
return sending_children
diff --git a/mythril/analysis/modules/transaction_order_dependence.py b/mythril/analysis/modules/transaction_order_dependence.py
index f5b45f5d..f6621293 100644
--- a/mythril/analysis/modules/transaction_order_dependence.py
+++ b/mythril/analysis/modules/transaction_order_dependence.py
@@ -112,7 +112,7 @@ def _get_influencing_sstores(statespace, interesting_storages):
index, value = sstore_state.mstate.stack[-1], sstore_state.mstate.stack[-2]
try:
index = util.get_concrete_int(index)
- except AttributeError:
+ except TypeError:
index = str(index)
if "storage_{}".format(index) not in interesting_storages:
continue
diff --git a/mythril/analysis/ops.py b/mythril/analysis/ops.py
index 999bbb12..b2329294 100644
--- a/mythril/analysis/ops.py
+++ b/mythril/analysis/ops.py
@@ -21,7 +21,7 @@ class Variable:
def get_variable(i):
try:
return Variable(util.get_concrete_int(i), VarType.CONCRETE)
- except AttributeError:
+ except TypeError:
return Variable(simplify(i), VarType.SYMBOLIC)
diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py
index 09447a67..20790eb8 100644
--- a/mythril/analysis/symbolic.py
+++ b/mythril/analysis/symbolic.py
@@ -4,23 +4,27 @@ from mythril.ether.soliditycontract import SolidityContract
import copy
import logging
from .ops import get_variable, SStore, Call, VarType
-from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy, BreadthFirstSearchStrategy
+from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy, BreadthFirstSearchStrategy, \
+ ReturnRandomNaivelyStrategy, ReturnWeightedRandomStrategy
class SymExecWrapper:
- '''
+ """
Wrapper class for the LASER Symbolic virtual machine. Symbolically executes the code and does a bit of pre-analysis for convenience.
- '''
+ """
def __init__(self, contract, address, strategy, dynloader=None, max_depth=22,
- execution_timeout=None, create_timeout=None):
+ execution_timeout=None, create_timeout=None, max_transaction_count=3):
- s_strategy = None
if strategy == 'dfs':
s_strategy = DepthFirstSearchStrategy
elif strategy == 'bfs':
s_strategy = BreadthFirstSearchStrategy
+ elif strategy == 'naive-random':
+ s_strategy = ReturnRandomNaivelyStrategy
+ elif strategy == 'weighted-random':
+ s_strategy = ReturnWeightedRandomStrategy
else:
raise ValueError("Invalid strategy argument supplied")
@@ -30,7 +34,8 @@ class SymExecWrapper:
self.laser = svm.LaserEVM(self.accounts, dynamic_loader=dynloader, max_depth=max_depth,
execution_timeout=execution_timeout, strategy=s_strategy,
- create_timeout=create_timeout)
+ create_timeout=create_timeout,
+ max_transaction_count=max_transaction_count)
if isinstance(contract, SolidityContract):
self.laser.sym_exec(creation_code=contract.creation_code, contract_name=contract.name)
@@ -67,7 +72,7 @@ class SymExecWrapper:
# ignore prebuilts
continue
- if (meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE):
+ if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE:
self.calls.append(Call(self.nodes[key], state, state_index, op, to, gas, value, state.mstate.memory[meminstart.val:meminsz.val * 4]))
else:
self.calls.append(Call(self.nodes[key], state, state_index, op, to, gas, value))
@@ -105,7 +110,7 @@ class SymExecWrapper:
taint = True
for constraint in s.node.constraints:
- if ("caller" in str(constraint)):
+ if "caller" in str(constraint):
taint = False
break
diff --git a/mythril/analysis/templates/callgraph.html b/mythril/analysis/templates/callgraph.html
index 5032b2c2..807ecfc6 100644
--- a/mythril/analysis/templates/callgraph.html
+++ b/mythril/analysis/templates/callgraph.html
@@ -1,7 +1,7 @@
- Laser - Call Graph
+ Call Graph
diff --git a/mythril/analysis/traceexplore.py b/mythril/analysis/traceexplore.py
index dc7af177..d70fdcbd 100644
--- a/mythril/analysis/traceexplore.py
+++ b/mythril/analysis/traceexplore.py
@@ -13,8 +13,8 @@ colors = [
{'border': '#4753bf', 'background': '#3b46a1', 'highlight': {'border': '#fff', 'background': '#424db3'}},
]
+
def get_serializable_statespace(statespace):
-
nodes = []
edges = []
@@ -40,10 +40,10 @@ def get_serializable_statespace(statespace):
color = color_map[node.get_cfg_dict()['contract_name']]
- def get_state_accounts(state):
+ def get_state_accounts(node_state):
state_accounts = []
- for key in state.accounts:
- account = state.accounts[key].as_dict
+ for key in node_state.accounts:
+ account = node_state.accounts[key].as_dict
account.pop('code', None)
account['balance'] = str(account['balance'])
@@ -81,7 +81,7 @@ def get_serializable_statespace(statespace):
for edge in statespace.edges:
- if (edge.condition is None):
+ if edge.condition is None:
label = ""
else:
diff --git a/mythril/disassembler/disassembly.py b/mythril/disassembler/disassembly.py
index 394f22b1..1fd98a73 100644
--- a/mythril/disassembler/disassembly.py
+++ b/mythril/disassembler/disassembly.py
@@ -4,51 +4,91 @@ import logging
class Disassembly(object):
+ """
+ Disassembly class
- def __init__(self, code, enable_online_lookup=True):
+ Stores bytecode, and its disassembly.
+ Additionally it will gather the following information on the existing functions in the disassembled code:
+ - function hashes
+ - function name to entry point mapping
+ - function entry point to function name mapping
+ """
+ def __init__(self, code: str, enable_online_lookup: bool=False):
+ self.bytecode = code
self.instruction_list = asm.disassemble(util.safe_decode(code))
+
self.func_hashes = []
- self.func_to_addr = {}
- self.addr_to_func = {}
- self.bytecode = code
+ self.function_name_to_address = {}
+ self.address_to_function_name = {}
- signatures = SignatureDb(enable_online_lookup=enable_online_lookup) # control if you want to have online sighash lookups
+ signatures = SignatureDb(
+ enable_online_lookup=enable_online_lookup
+ ) # control if you want to have online signature hash lookups
try:
signatures.open() # open from default locations
except FileNotFoundError:
- logging.info("Missing function signature file. Resolving of function names from signature file disabled.")
-
- # Parse jump table & resolve function names
-
- jmptable_indices = asm.find_opcode_sequence(["PUSH4", "EQ"], self.instruction_list)
-
- for i in jmptable_indices:
- func_hash = self.instruction_list[i]['argument']
- self.func_hashes.append(func_hash)
- try:
- # tries local cache, file and optional online lookup
- # may return more than one function signature. since we cannot probe for the correct one we'll use the first
- func_names = signatures.get(func_hash)
- if len(func_names) > 1:
- # ambigious result
- func_name = "**ambiguous** %s" % func_names[0] # return first hit but note that result was ambiguous
- else:
- # only one item
- func_name = func_names[0]
- except KeyError:
- func_name = "_function_" + func_hash
-
- try:
- offset = self.instruction_list[i + 2]['argument']
- jump_target = int(offset, 16)
-
- self.func_to_addr[func_name] = jump_target
- self.addr_to_func[jump_target] = func_name
- except:
- continue
+ logging.info(
+ "Missing function signature file. Resolving of function names from signature file disabled."
+ )
+
+ # Need to take from PUSH1 to PUSH4 because solc seems to remove excess 0s at the beginning for optimizing
+ jump_table_indices = asm.find_opcode_sequence(
+ [("PUSH1", "PUSH2", "PUSH3", "PUSH4"), ("EQ",)], self.instruction_list
+ )
+
+ for index in jump_table_indices:
+ function_hash, jump_target, function_name = get_function_info(
+ index, self.instruction_list, signatures
+ )
+ self.func_hashes.append(function_hash)
+
+ if jump_target is not None and function_name is not None:
+ self.function_name_to_address[function_name] = jump_target
+ self.address_to_function_name[jump_target] = function_name
signatures.write() # store resolved signatures (potentially resolved online)
def get_easm(self):
- # todo: tintinweb - print funcsig resolved data from self.addr_to_func?
return asm.instruction_list_to_easm(self.instruction_list)
+
+
+def get_function_info(index: int, instruction_list: list, signature_database: SignatureDb) -> (str, int, str):
+ """
+ Finds the function information for a call table entry
+ Solidity uses the first 4 bytes of the calldata to indicate which function the message call should execute
+ The generated code that directs execution to the correct function looks like this:
+ - PUSH function_hash
+ - EQ
+ - PUSH entry_point
+ - JUMPI
+
+ This function takes an index that points to the first instruction, and from that finds out the function hash,
+ function entry and the function name.
+
+ :param index: Start of the entry pattern
+ :param instruction_list: Instruction list for the contract that is being analyzed
+ :param signature_database: Database used to map function hashes to their respective function names
+ :return: function hash, function entry point, function name
+ """
+
+ # Append with missing 0s at the beginning
+ function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0")
+
+ function_names = signature_database.get(function_hash)
+ if len(function_names) > 1:
+ # In this case there was an ambiguous result
+ function_name = (
+ "**ambiguous** {}".format(function_names[0])
+ )
+ elif len(function_names) == 1:
+ function_name = function_names[0]
+ else:
+ function_name = "_function_" + function_hash
+
+ try:
+ offset = instruction_list[index + 2]["argument"]
+ entry_point = int(offset, 16)
+ except (KeyError, IndexError):
+ return function_hash, None, None
+
+ return function_hash, entry_point, function_name
diff --git a/mythril/ether/asm.py b/mythril/ether/asm.py
index 5cc2b4b5..985b2f07 100644
--- a/mythril/ether/asm.py
+++ b/mythril/ether/asm.py
@@ -70,17 +70,17 @@ def find_opcode_sequence(pattern, instruction_list):
for i in range(0, len(instruction_list) - pattern_length + 1):
- if instruction_list[i]['opcode'] == pattern[0]:
+ if instruction_list[i]['opcode'] in pattern[0]:
matched = True
for j in range(1, len(pattern)):
- if not (instruction_list[i + j]['opcode'] == pattern[j]):
+ if not (instruction_list[i + j]['opcode'] in pattern[j]):
matched = False
break
- if (matched):
+ if matched:
match_indexes.append(i)
return match_indexes
@@ -102,7 +102,7 @@ def disassemble(bytecode):
instruction = {'address': addr}
try:
- if (sys.version_info > (3, 0)):
+ if sys.version_info > (3, 0):
opcode = opcodes[bytecode[addr]]
else:
opcode = opcodes[ord(bytecode[addr])]
diff --git a/mythril/ether/ethcontract.py b/mythril/ether/ethcontract.py
index b43b1919..9e0bd247 100644
--- a/mythril/ether/ethcontract.py
+++ b/mythril/ether/ethcontract.py
@@ -6,16 +6,17 @@ import re
class ETHContract(persistent.Persistent):
- def __init__(self, code, creation_code="", name="Unknown", enable_online_lookup=True):
-
- self.creation_code = creation_code
- self.name = name
-
+ def __init__(self, code, creation_code="", name="Unknown", enable_online_lookup=False):
+
# Workaround: We currently do not support compile-time linking.
# Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address
+ # Apply this for creation_code & code
- code = re.sub(r'(_+.*_+)', 'aa' * 20, code)
+ creation_code = re.sub(r'(_{2}.{38})', 'aa' * 20, creation_code)
+ code = re.sub(r'(_{2}.{38})', 'aa' * 20, code)
+ self.creation_code = creation_code
+ self.name = name
self.code = code
self.disassembly = Disassembly(code, enable_online_lookup=enable_online_lookup)
self.creation_disassembly = Disassembly(creation_code, enable_online_lookup=enable_online_lookup)
@@ -49,7 +50,7 @@ class ETHContract(persistent.Persistent):
m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token)
- if (m):
+ if m:
if easm_code is None:
easm_code = self.get_easm()
@@ -59,7 +60,7 @@ class ETHContract(persistent.Persistent):
m = re.match(r'^func#([a-zA-Z0-9\s_,(\\)\[\]]+)#$', token)
- if (m):
+ if m:
sign_hash = "0x" + utils.sha3(m.group(1))[:4].hex()
diff --git a/mythril/ether/evm.py b/mythril/ether/evm.py
index 449fcdcf..f1cf2fa5 100644
--- a/mythril/ether/evm.py
+++ b/mythril/ether/evm.py
@@ -7,69 +7,52 @@ from io import StringIO
import re
-def trace(code, calldata = ""):
-
- log_handlers = ['eth.vm.op', 'eth.vm.op.stack', 'eth.vm.op.memory', 'eth.vm.op.storage']
-
- output = StringIO()
- stream_handler = StreamHandler(output)
-
- for handler in log_handlers:
- log_vm_op = get_logger(handler)
- log_vm_op.setLevel("TRACE")
- log_vm_op.addHandler(stream_handler)
-
- addr = bytes.fromhex('0123456789ABCDEF0123456789ABCDEF01234567')
-
- state = State()
-
- ext = messages.VMExt(state, transactions.Transaction(0, 0, 21000, addr, 0, addr))
-
- message = vm.Message(addr, addr, 0, 21000, calldata)
-
- res, gas, dat = vm.vm_execute(ext, message, util.safe_decode(code))
-
- stream_handler.flush()
-
- ret = output.getvalue()
-
- lines = ret.split("\n")
-
- trace = []
-
- for line in lines:
-
- m = re.search(r'pc=b\'(\d+)\'.*op=([A-Z0-9]+)', line)
-
- if m:
- pc = m.group(1)
- op = m.group(2)
-
- m = re.match(r'.*stack=(\[.*?\])', line)
-
- if (m):
-
- stackitems = re.findall(r'b\'(\d+)\'', m.group(1))
-
- stack = "[";
-
- if (len(stackitems)):
-
- for i in range(0, len(stackitems) - 1):
- stack += hex(int(stackitems[i])) + ", "
-
- stack += hex(int(stackitems[-1]))
-
- stack += "]"
-
- else:
- stack = "[]"
-
- if (re.match(r'^PUSH.*', op)):
- val = re.search(r'pushvalue=(\d+)', line).group(1)
- pushvalue = hex(int(val))
- trace.append({'pc': pc, 'op': op, 'stack': stack, 'pushvalue': pushvalue})
- else:
- trace.append({'pc': pc, 'op': op, 'stack': stack})
-
- return trace
+def trace(code, calldata=""):
+ log_handlers = ['eth.vm.op', 'eth.vm.op.stack', 'eth.vm.op.memory', 'eth.vm.op.storage']
+ output = StringIO()
+ stream_handler = StreamHandler(output)
+
+ for handler in log_handlers:
+ log_vm_op = get_logger(handler)
+ log_vm_op.setLevel("TRACE")
+ log_vm_op.addHandler(stream_handler)
+
+ addr = bytes.fromhex('0123456789ABCDEF0123456789ABCDEF01234567')
+ state = State()
+
+ ext = messages.VMExt(state, transactions.Transaction(0, 0, 21000, addr, 0, addr))
+ message = vm.Message(addr, addr, 0, 21000, calldata)
+ vm.vm_execute(ext, message, util.safe_decode(code))
+ stream_handler.flush()
+ ret = output.getvalue()
+ lines = ret.split("\n")
+
+ state_trace = []
+ for line in lines:
+ m = re.search(r'pc=b\'(\d+)\'.*op=([A-Z0-9]+)', line)
+ if m:
+ pc = m.group(1)
+ op = m.group(2)
+ m = re.match(r'.*stack=(\[.*?\])', line)
+
+ if m:
+ stackitems = re.findall(r'b\'(\d+)\'', m.group(1))
+ stack = "["
+
+ if len(stackitems):
+ for i in range(0, len(stackitems) - 1):
+ stack += hex(int(stackitems[i])) + ", "
+ stack += hex(int(stackitems[-1]))
+
+ stack += "]"
+ else:
+ stack = "[]"
+
+ if re.match(r'^PUSH.*', op):
+ val = re.search(r'pushvalue=(\d+)', line).group(1)
+ pushvalue = hex(int(val))
+ state_trace.append({'pc': pc, 'op': op, 'stack': stack, 'pushvalue': pushvalue})
+ else:
+ state_trace.append({'pc': pc, 'op': op, 'stack': stack})
+
+ return state_trace
diff --git a/mythril/ether/util.py b/mythril/ether/util.py
index 0e4bae40..6b351665 100644
--- a/mythril/ether/util.py
+++ b/mythril/ether/util.py
@@ -10,7 +10,7 @@ import json
def safe_decode(hex_encoded_string):
- if (hex_encoded_string.startswith("0x")):
+ if hex_encoded_string.startswith("0x"):
return bytes.fromhex(hex_encoded_string[2:])
else:
return bytes.fromhex(hex_encoded_string)
diff --git a/mythril/leveldb/__init__.py b/mythril/ethereum/__init__.py
similarity index 100%
rename from mythril/leveldb/__init__.py
rename to mythril/ethereum/__init__.py
diff --git a/mythril/rpc/__init__.py b/mythril/ethereum/interface/__init__.py
similarity index 100%
rename from mythril/rpc/__init__.py
rename to mythril/ethereum/interface/__init__.py
diff --git a/mythril/ethereum/interface/leveldb/__init__.py b/mythril/ethereum/interface/leveldb/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mythril/leveldb/accountindexing.py b/mythril/ethereum/interface/leveldb/accountindexing.py
similarity index 88%
rename from mythril/leveldb/accountindexing.py
rename to mythril/ethereum/interface/leveldb/accountindexing.py
index 26a4ab9a..9d94c31e 100644
--- a/mythril/leveldb/accountindexing.py
+++ b/mythril/ethereum/interface/leveldb/accountindexing.py
@@ -34,9 +34,9 @@ class CountableList(object):
class ReceiptForStorage(rlp.Serializable):
- '''
+ """
Receipt format stored in levelDB
- '''
+ """
fields = [
('state_root', binary),
@@ -50,9 +50,9 @@ class ReceiptForStorage(rlp.Serializable):
class AccountIndexer(object):
- '''
+ """
Updates address index
- '''
+ """
def __init__(self, ethDB):
self.db = ethDB
@@ -62,29 +62,28 @@ class AccountIndexer(object):
self.updateIfNeeded()
def get_contract_by_hash(self, contract_hash):
- '''
- get mapped address by its hash, if not found try indexing
- '''
- address = self.db.reader._get_address_by_hash(contract_hash)
- if address is not None:
- return address
+ """
+ get mapped contract_address by its hash, if not found try indexing
+ """
+ contract_address = self.db.reader._get_address_by_hash(contract_hash)
+ if contract_address is not None:
+ return contract_address
+
else:
raise AddressNotFoundError
- return self.db.reader._get_address_by_hash(contract_hash)
-
def _process(self, startblock):
- '''
+ """
Processesing method
- '''
+ """
logging.debug("Processing blocks %d to %d" % (startblock, startblock + BATCH_SIZE))
addresses = []
for blockNum in range(startblock, startblock + BATCH_SIZE):
- hash = self.db.reader._get_block_hash(blockNum)
- if hash is not None:
- receipts = self.db.reader._get_block_receipts(hash, blockNum)
+ block_hash = self.db.reader._get_block_hash(blockNum)
+ if block_hash is not None:
+ receipts = self.db.reader._get_block_receipts(block_hash, blockNum)
for receipt in receipts:
if receipt.contractAddress is not None and not all(b == 0 for b in receipt.contractAddress):
@@ -96,9 +95,9 @@ class AccountIndexer(object):
return addresses
def updateIfNeeded(self):
- '''
+ """
update address index
- '''
+ """
headBlock = self.db.reader._get_head_block()
if headBlock is not None:
# avoid restarting search if head block is same & we already initialized
@@ -128,7 +127,7 @@ class AccountIndexer(object):
count = 0
processed = 0
- while (blockNum <= self.lastBlock):
+ while blockNum <= self.lastBlock:
# leveldb cannot be accessed on multiple processes (not even readonly)
# multithread version performs significantly worse than serial
try:
@@ -154,4 +153,4 @@ class AccountIndexer(object):
self.db.writer._set_last_indexed_number(self.lastProcessedBlock)
print("Finished indexing")
- self.lastBlock = self.lastProcessedBlock
\ No newline at end of file
+ self.lastBlock = self.lastProcessedBlock
diff --git a/mythril/leveldb/client.py b/mythril/ethereum/interface/leveldb/client.py
similarity index 76%
rename from mythril/leveldb/client.py
rename to mythril/ethereum/interface/leveldb/client.py
index b192b004..bdc4b46b 100644
--- a/mythril/leveldb/client.py
+++ b/mythril/ethereum/interface/leveldb/client.py
@@ -1,12 +1,12 @@
import binascii
import rlp
-from mythril.leveldb.accountindexing import CountableList
-from mythril.leveldb.accountindexing import ReceiptForStorage, AccountIndexer
+from mythril.ethereum.interface.leveldb.accountindexing import CountableList
+from mythril.ethereum.interface.leveldb.accountindexing import ReceiptForStorage, AccountIndexer
import logging
from ethereum import utils
from ethereum.block import BlockHeader, Block
-from mythril.leveldb.state import State
-from mythril.leveldb.eth_db import ETH_DB
+from mythril.ethereum.interface.leveldb.state import State
+from mythril.ethereum.interface.leveldb.eth_db import ETH_DB
from mythril.ether.ethcontract import ETHContract
from mythril.exceptions import AddressNotFoundError
@@ -26,23 +26,23 @@ address_mapping_head_key = b'accountMapping' # head (latest) number of indexed
def _format_block_number(number):
- '''
+ """
formats block number to uint64 big endian
- '''
+ """
return utils.zpad(utils.int_to_big_endian(number), 8)
def _encode_hex(v):
- '''
+ """
encodes hash as hex
- '''
+ """
return '0x' + utils.encode_hex(v)
class LevelDBReader(object):
- '''
+ """
level db reading interface, can be used with snapshot
- '''
+ """
def __init__(self, db):
self.db = db
@@ -50,125 +50,116 @@ class LevelDBReader(object):
self.head_state = None
def _get_head_state(self):
- '''
+ """
gets head state
- '''
+ """
if not self.head_state:
root = self._get_head_block().state_root
self.head_state = State(self.db, root)
return self.head_state
def _get_account(self, address):
- '''
+ """
gets account by address
- '''
+ """
state = self._get_head_state()
account_address = binascii.a2b_hex(utils.remove_0x_head(address))
return state.get_and_cache_account(account_address)
def _get_block_hash(self, number):
- '''
+ """
gets block hash by block number
- '''
+ """
num = _format_block_number(number)
hash_key = header_prefix + num + num_suffix
return self.db.get(hash_key)
def _get_head_block(self):
- '''
+ """
gets head block header
- '''
+ """
if not self.head_block_header:
- hash = self.db.get(head_header_key)
- num = self._get_block_number(hash)
- self.head_block_header = self._get_block_header(hash, num)
+ block_hash = self.db.get(head_header_key)
+ num = self._get_block_number(block_hash)
+ self.head_block_header = self._get_block_header(block_hash, num)
# find header with valid state
while not self.db.get(self.head_block_header.state_root) and self.head_block_header.prevhash is not None:
- hash = self.head_block_header.prevhash
- num = self._get_block_number(hash)
- self.head_block_header = self._get_block_header(hash, num)
+ block_hash = self.head_block_header.prevhash
+ num = self._get_block_number(block_hash)
+ self.head_block_header = self._get_block_header(block_hash, num)
return self.head_block_header
- def _get_block_number(self, hash):
- '''
- gets block number by hash
- '''
- number_key = block_hash_prefix + hash
+ def _get_block_number(self, block_hash):
+ """Get block number by its hash"""
+ number_key = block_hash_prefix + block_hash
return self.db.get(number_key)
- def _get_block_header(self, hash, num):
- '''
- get block header by block header hash & number
- '''
- header_key = header_prefix + num + hash
+ def _get_block_header(self, block_hash, num):
+ """Get block header by block header hash & number"""
+ header_key = header_prefix + num + block_hash
+
block_header_data = self.db.get(header_key)
header = rlp.decode(block_header_data, sedes=BlockHeader)
return header
- def _get_address_by_hash(self, hash):
- '''
- get mapped address by its hash
- '''
- address_key = address_prefix + hash
+ def _get_address_by_hash(self, block_hash):
+ """Get mapped address by its hash"""
+ address_key = address_prefix + block_hash
return self.db.get(address_key)
def _get_last_indexed_number(self):
- '''
- latest indexed block number
- '''
+ """Get latest indexed block number"""
return self.db.get(address_mapping_head_key)
- def _get_block_receipts(self, hash, num):
- '''
- get block transaction receipts by block header hash & number
- '''
+ def _get_block_receipts(self, block_hash, num):
+ """Get block transaction receipts by block header hash & number"""
number = _format_block_number(num)
- receipts_key = block_receipts_prefix + number + hash
+ receipts_key = block_receipts_prefix + number + block_hash
receipts_data = self.db.get(receipts_key)
receipts = rlp.decode(receipts_data, sedes=CountableList(ReceiptForStorage))
return receipts
class LevelDBWriter(object):
- '''
+ """
level db writing interface
- '''
+ """
def __init__(self, db):
self.db = db
self.wb = None
def _set_last_indexed_number(self, number):
- '''
+ """
sets latest indexed block number
- '''
+ """
return self.db.put(address_mapping_head_key, _format_block_number(number))
def _start_writing(self):
- '''
+ """
start writing a batch
- '''
+ """
self.wb = self.db.write_batch()
def _commit_batch(self):
- '''
+ """
commit batch
- '''
+ """
self.wb.write()
def _store_account_address(self, address):
- '''
+ """
get block transaction receipts by block header hash & number
- '''
+ """
address_key = address_prefix + utils.sha3(address)
self.wb.put(address_key, address)
class EthLevelDB(object):
- '''
+ """
Go-Ethereum LevelDB client class
- '''
+ """
def __init__(self, path):
self.path = path
@@ -177,9 +168,9 @@ class EthLevelDB(object):
self.writer = LevelDBWriter(self.db)
def get_contracts(self):
- '''
+ """
iterate through all contracts
- '''
+ """
for account in self.reader._get_head_state().get_all_accounts():
if account.code is not None:
code = _encode_hex(account.code)
@@ -188,9 +179,9 @@ class EthLevelDB(object):
yield contract, account.address, account.balance
def search(self, expression, callback_func):
- '''
+ """
searches through all contract accounts
- '''
+ """
cnt = 0
indexer = AccountIndexer(self)
@@ -216,28 +207,26 @@ class EthLevelDB(object):
if not cnt % 1000:
logging.info("Searched %d contracts" % cnt)
- def contract_hash_to_address(self, hash):
- '''
- tries to find corresponding account address
- '''
+ def contract_hash_to_address(self, contract_hash):
+ """Tries to find corresponding account address"""
- address_hash = binascii.a2b_hex(utils.remove_0x_head(hash))
+ address_hash = binascii.a2b_hex(utils.remove_0x_head(contract_hash))
indexer = AccountIndexer(self)
return _encode_hex(indexer.get_contract_by_hash(address_hash))
def eth_getBlockHeaderByNumber(self, number):
- '''
+ """
gets block header by block number
- '''
- hash = self.reader._get_block_hash(number)
+ """
+ block_hash = self.reader._get_block_hash(number)
block_number = _format_block_number(number)
- return self.reader._get_block_header(hash, block_number)
+ return self.reader._get_block_header(block_hash, block_number)
def eth_getBlockByNumber(self, number):
- '''
+ """
gets block body by block number
- '''
+ """
block_hash = self.reader._get_block_hash(number)
block_number = _format_block_number(number)
body_key = body_prefix + block_number + block_hash
@@ -246,22 +235,22 @@ class EthLevelDB(object):
return body
def eth_getCode(self, address):
- '''
+ """
gets account code
- '''
+ """
account = self.reader._get_account(address)
return _encode_hex(account.code)
def eth_getBalance(self, address):
- '''
+ """
gets account balance
- '''
+ """
account = self.reader._get_account(address)
return account.balance
def eth_getStorageAt(self, address, position):
- '''
+ """
gets account storage data at position
- '''
+ """
account = self.reader._get_account(address)
return _encode_hex(utils.zpad(utils.encode_int(account.get_storage_data(position)), 32))
diff --git a/mythril/leveldb/eth_db.py b/mythril/ethereum/interface/leveldb/eth_db.py
similarity index 76%
rename from mythril/leveldb/eth_db.py
rename to mythril/ethereum/interface/leveldb/eth_db.py
index a46d9e93..ab9107fa 100644
--- a/mythril/leveldb/eth_db.py
+++ b/mythril/ethereum/interface/leveldb/eth_db.py
@@ -3,27 +3,27 @@ from ethereum.db import BaseDB
class ETH_DB(BaseDB):
- '''
+ """
adopts pythereum BaseDB using plyvel
- '''
+ """
def __init__(self, path):
self.db = plyvel.DB(path)
def get(self, key):
- '''
+ """
gets value for key
- '''
+ """
return self.db.get(key)
def put(self, key, value):
- '''
+ """
puts value for key
- '''
+ """
self.db.put(key, value)
def write_batch(self):
- '''
+ """
start writing a batch
- '''
- return self.db.write_batch()
\ No newline at end of file
+ """
+ return self.db.write_batch()
diff --git a/mythril/leveldb/state.py b/mythril/ethereum/interface/leveldb/state.py
similarity index 79%
rename from mythril/leveldb/state.py
rename to mythril/ethereum/interface/leveldb/state.py
index 96360300..83507b69 100644
--- a/mythril/leveldb/state.py
+++ b/mythril/ethereum/interface/leveldb/state.py
@@ -32,9 +32,9 @@ STATE_DEFAULTS = {
class Account(rlp.Serializable):
- '''
+ """
adjusted account from ethereum.state
- '''
+ """
fields = [
('nonce', big_endian_int),
@@ -43,9 +43,9 @@ class Account(rlp.Serializable):
('code_hash', hash32)
]
- def __init__(self, nonce, balance, storage, code_hash, db, address):
+ def __init__(self, nonce, balance, storage, code_hash, db, addr):
self.db = db
- self.address = address
+ self.address = addr
super(Account, self).__init__(nonce, balance, storage, code_hash)
self.storage_cache = {}
self.storage_trie = SecureTrie(Trie(self.db))
@@ -57,15 +57,15 @@ class Account(rlp.Serializable):
@property
def code(self):
- '''
+ """
code rlp data
- '''
+ """
return self.db.get(self.code_hash)
def get_storage_data(self, key):
- '''
+ """
get storage data
- '''
+ """
if key not in self.storage_cache:
v = self.storage_trie.get(utils.encode_int32(key))
self.storage_cache[key] = utils.big_endian_to_int(
@@ -73,25 +73,25 @@ class Account(rlp.Serializable):
return self.storage_cache[key]
@classmethod
- def blank_account(cls, db, address, initial_nonce=0):
- '''
+ def blank_account(cls, db, addr, initial_nonce=0):
+ """
creates a blank account
- '''
+ """
db.put(BLANK_HASH, b'')
- o = cls(initial_nonce, 0, trie.BLANK_ROOT, BLANK_HASH, db, address)
+ o = cls(initial_nonce, 0, trie.BLANK_ROOT, BLANK_HASH, db, addr)
o.existent_at_start = False
return o
def is_blank(self):
- '''
+ """
checks if is a blank account
- '''
+ """
return self.nonce == 0 and self.balance == 0 and self.code_hash == BLANK_HASH
-class State():
- '''
+class State:
+ """
adjusted state from ethereum.state
- '''
+ """
def __init__(self, db, root):
self.db = db
@@ -100,29 +100,29 @@ class State():
self.journal = []
self.cache = {}
- def get_and_cache_account(self, address):
- '''
- gets and caches an account for an addres, creates blank if not found
- '''
- if address in self.cache:
- return self.cache[address]
- rlpdata = self.secure_trie.get(address)
- if rlpdata == trie.BLANK_NODE and len(address) == 32: # support for hashed addresses
- rlpdata = self.trie.get(address)
+ def get_and_cache_account(self, addr):
+ """Gets and caches an account for an addres, creates blank if not found"""
+
+ if addr in self.cache:
+ return self.cache[addr]
+ rlpdata = self.secure_trie.get(addr)
+ if rlpdata == trie.BLANK_NODE and len(addr) == 32: # support for hashed addresses
+ rlpdata = self.trie.get(addr)
+
if rlpdata != trie.BLANK_NODE:
- o = rlp.decode(rlpdata, Account, db=self.db, address=address)
+ o = rlp.decode(rlpdata, Account, db=self.db, address=addr)
else:
o = Account.blank_account(
- self.db, address, 0)
- self.cache[address] = o
+ self.db, addr, 0)
+ self.cache[addr] = o
o._mutable = True
o._cached_rlp = None
return o
def get_all_accounts(self):
- '''
+ """
iterates through trie to and yields non-blank leafs as accounts
- '''
+ """
for address_hash, rlpdata in self.secure_trie.trie.iter_branch():
if rlpdata != trie.BLANK_NODE:
- yield rlp.decode(rlpdata, Account, db=self.db, address=address_hash)
\ No newline at end of file
+ yield rlp.decode(rlpdata, Account, db=self.db, address=address_hash)
diff --git a/mythril/ethereum/interface/rpc/__init__.py b/mythril/ethereum/interface/rpc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mythril/rpc/base_client.py b/mythril/ethereum/interface/rpc/base_client.py
similarity index 93%
rename from mythril/rpc/base_client.py
rename to mythril/ethereum/interface/rpc/base_client.py
index bc8b1994..9234ecf9 100644
--- a/mythril/rpc/base_client.py
+++ b/mythril/ethereum/interface/rpc/base_client.py
@@ -20,64 +20,64 @@ class BaseClient(object):
pass
def eth_coinbase(self):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase
TESTED
- '''
+ """
return self._call('eth_coinbase')
def eth_blockNumber(self):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_blocknumber
TESTED
- '''
+ """
return hex_to_dec(self._call('eth_blockNumber'))
def eth_getBalance(self, address=None, block=BLOCK_TAG_LATEST):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance
TESTED
- '''
+ """
address = address or self.eth_coinbase()
block = validate_block(block)
return hex_to_dec(self._call('eth_getBalance', [address, block]))
def eth_getStorageAt(self, address=None, position=0, block=BLOCK_TAG_LATEST):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getstorageat
TESTED
- '''
+ """
block = validate_block(block)
return self._call('eth_getStorageAt', [address, hex(position), block])
def eth_getCode(self, address, default_block=BLOCK_TAG_LATEST):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getcode
NEEDS TESTING
- '''
+ """
if isinstance(default_block, str):
if default_block not in BLOCK_TAGS:
raise ValueError
return self._call('eth_getCode', [address, default_block])
def eth_getBlockByNumber(self, block=BLOCK_TAG_LATEST, tx_objects=True):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber
TESTED
- '''
+ """
block = validate_block(block)
return self._call('eth_getBlockByNumber', [block, tx_objects])
def eth_getTransactionReceipt(self, tx_hash):
- '''
+ """
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt
TESTED
- '''
+ """
return self._call('eth_getTransactionReceipt', [tx_hash])
diff --git a/mythril/rpc/client.py b/mythril/ethereum/interface/rpc/client.py
similarity index 99%
rename from mythril/rpc/client.py
rename to mythril/ethereum/interface/rpc/client.py
index 6a7f0b96..1545092f 100644
--- a/mythril/rpc/client.py
+++ b/mythril/ethereum/interface/rpc/client.py
@@ -17,9 +17,9 @@ JSON_MEDIA_TYPE = 'application/json'
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
'''
class EthJsonRpc(BaseClient):
- '''
+ """
Ethereum JSON-RPC client class
- '''
+ """
def __init__(self, host='localhost', port=GETH_DEFAULT_RPC_PORT, tls=False):
self.host = host
diff --git a/mythril/rpc/constants.py b/mythril/ethereum/interface/rpc/constants.py
similarity index 100%
rename from mythril/rpc/constants.py
rename to mythril/ethereum/interface/rpc/constants.py
diff --git a/mythril/rpc/exceptions.py b/mythril/ethereum/interface/rpc/exceptions.py
similarity index 100%
rename from mythril/rpc/exceptions.py
rename to mythril/ethereum/interface/rpc/exceptions.py
diff --git a/mythril/rpc/utils.py b/mythril/ethereum/interface/rpc/utils.py
similarity index 90%
rename from mythril/rpc/utils.py
rename to mythril/ethereum/interface/rpc/utils.py
index e87b7dc6..5f98fcea 100644
--- a/mythril/rpc/utils.py
+++ b/mythril/ethereum/interface/rpc/utils.py
@@ -2,17 +2,17 @@ from .constants import BLOCK_TAGS
def hex_to_dec(x):
- '''
+ """
Convert hex to decimal
- '''
+ """
return int(x, 16)
def clean_hex(d):
- '''
+ """
Convert decimal to hex and remove the "L" suffix that is appended to large
numbers
- '''
+ """
return hex(d).rstrip('L')
def validate_block(block):
@@ -25,14 +25,14 @@ def validate_block(block):
def wei_to_ether(wei):
- '''
+ """
Convert wei to ether
- '''
+ """
return 1.0 * wei / 10**18
def ether_to_wei(ether):
- '''
+ """
Convert ether to wei
- '''
+ """
return ether * 10**18
diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py
index be6cec59..ea55e29c 100644
--- a/mythril/interfaces/cli.py
+++ b/mythril/interfaces/cli.py
@@ -5,7 +5,7 @@
http://www.github.com/ConsenSys/mythril
"""
-import logging
+import logging, coloredlogs
import json
import sys
import argparse
@@ -15,10 +15,11 @@ import argparse
from mythril.exceptions import CriticalError, AddressNotFoundError
from mythril.mythril import Mythril
from mythril.version import VERSION
+import mythril.support.signatures as sigs
-def exit_with_error(format, message):
- if format == 'text' or format == 'markdown':
+def exit_with_error(format_, message):
+ if format_ == 'text' or format_ == 'markdown':
print(message)
else:
result = {'success': False, 'error': str(message), 'issues': []}
@@ -69,7 +70,11 @@ def main():
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=22, help='Maximum recursion depth for symbolic execution')
- options.add_argument('--strategy', choices=['dfs', 'bfs'], default='dfs', help='Symbolic execution strategy')
+
+ options.add_argument('--strategy', choices=['dfs', 'bfs', 'naive-random', 'weighted-random'],
+ default='dfs', help='Symbolic execution strategy')
+ options.add_argument('--max-transaction-count', type=int, default=3, help='Maximum number of transactions issued by laser')
+
options.add_argument('--execution-timeout', type=int, default=600, help="The amount of seconds to spend on symbolic execution")
options.add_argument('--create-timeout', type=int, default=10, help="The amount of seconds to spend on "
"the initial contract creation")
@@ -77,6 +82,7 @@ def main():
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('-q', '--query-signature', action='store_true', help='Lookup function signatures through www.4byte.directory')
rpc = parser.add_argument_group('RPC options')
rpc.add_argument('-i', action='store_true', help='Preset: Infura Node service (Mainnet)')
@@ -101,12 +107,20 @@ def main():
parser.print_help()
sys.exit()
+
if args.v:
if 0 <= args.v < 3:
- logging.basicConfig(level=[logging.NOTSET, logging.INFO, logging.DEBUG][args.v])
+ coloredlogs.install(
+ fmt='%(name)s[%(process)d] %(levelname)s %(message)s',
+ 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")
+ if args.query_signature:
+ if sigs.ethereum_input_decoder == None:
+ exit_with_error(args.outform, "The --query-signature function requires the python package ethereum-input-decoder")
+
# -- commands --
if args.hash:
print(Mythril.hash_for_function_signature(args.hash))
@@ -118,7 +132,8 @@ def main():
# solc_args = None, dynld = None, max_recursion_depth = 12):
mythril = Mythril(solv=args.solv, dynld=args.dynld,
- solc_args=args.solc_args)
+ solc_args=args.solc_args,
+ enable_online_lookup=args.query_signature)
if args.dynld and not (args.rpc or args.i):
mythril.set_api_from_config_path()
@@ -215,7 +230,8 @@ def main():
modules=[m.strip() for m in args.modules.strip().split(",")] if args.modules else [],
verbose_report=args.verbose_report,
max_depth=args.max_depth, execution_timeout=args.execution_timeout,
- create_timeout=args.create_timeout)
+ create_timeout=args.create_timeout,
+ max_transaction_count=args.max_transaction_count)
outputs = {
'json': report.as_json(),
'text': report.as_text(),
diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py
index 0bbbf731..dd597a63 100644
--- a/mythril/laser/ethereum/call.py
+++ b/mythril/laser/ethereum/call.py
@@ -48,8 +48,8 @@ def get_callee_address(global_state:GlobalState, dynamic_loader: DynLoader, symb
try:
callee_address = hex(util.get_concrete_int(symbolic_to_address))
- except AttributeError:
- logging.info("Symbolic call encountered")
+ except TypeError:
+ logging.debug("Symbolic call encountered")
match = re.search(r'storage_(\d+)', str(simplify(symbolic_to_address)))
logging.debug("CALL to: " + str(simplify(symbolic_to_address)))
@@ -58,11 +58,12 @@ def get_callee_address(global_state:GlobalState, dynamic_loader: DynLoader, symb
raise ValueError()
index = int(match.group(1))
- logging.info("Dynamic contract address at storage index {}".format(index))
+ logging.debug("Dynamic contract address at storage index {}".format(index))
# attempt to read the contract address from instance storage
try:
callee_address = dynamic_loader.read_storage(environment.active_account.address, index)
+ # TODO: verify whether this happens or not
except:
logging.debug("Error accessing contract storage.")
raise ValueError
@@ -89,22 +90,22 @@ def get_callee_account(global_state, callee_address, dynamic_loader):
return global_state.accounts[callee_address]
except KeyError:
# We have a valid call address, but contract is not in the modules list
- logging.info("Module with address " + callee_address + " not loaded.")
+ logging.debug("Module with address " + callee_address + " not loaded.")
if dynamic_loader is None:
raise ValueError()
- logging.info("Attempting to load dependency")
+ logging.debug("Attempting to load dependency")
try:
code = dynamic_loader.dynld(environment.active_account.address, callee_address)
- except Exception as e:
- logging.info("Unable to execute dynamic loader.")
+ except Exception:
+ logging.debug("Unable to execute dynamic loader.")
raise ValueError()
if code is None:
- logging.info("No code returned, not a contract account?")
+ logging.debug("No code returned, not a contract account?")
raise ValueError()
- logging.info("Dependency loaded: " + callee_address)
+ logging.debug("Dependency loaded: " + callee_address)
callee_account = Account(callee_address, code, callee_address, dynamic_loader=dynamic_loader)
accounts[callee_address] = callee_account
@@ -112,7 +113,6 @@ def get_callee_account(global_state, callee_address, dynamic_loader):
return callee_account
-
def get_call_data(global_state, memory_start, memory_size, pad=True):
"""
Gets call_data from the global_state
@@ -145,7 +145,7 @@ def get_call_data(global_state, memory_start, memory_size, pad=True):
)
call_data_type = CalldataType.CONCRETE
logging.debug("Calldata: " + str(call_data))
- except AttributeError:
+ except TypeError:
logging.info("Unsupported symbolic calldata offset")
call_data_type = CalldataType.SYMBOLIC
call_data = Calldata('{}_internalcall'.format(transaction_id))
diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py
index b2a3a5bb..338be3dd 100644
--- a/mythril/laser/ethereum/instructions.py
+++ b/mythril/laser/ethereum/instructions.py
@@ -175,7 +175,7 @@ class Instruction:
result = simplify(Concat(BitVecVal(0, 248), Extract(offset + 7, offset, op1)))
else:
result = 0
- except AttributeError:
+ except TypeError:
logging.debug("BYTE: Unsupported symbolic byte offset")
result = global_state.new_bitvec(str(simplify(op1)) + "[" + str(simplify(op0)) + "]", 256)
@@ -265,18 +265,17 @@ class Instruction:
try:
s0 = util.get_concrete_int(s0)
s1 = util.get_concrete_int(s1)
+ except TypeError:
+ return []
- if s0 <= 31:
- testbit = s0 * 8 + 7
- if s1 & (1 << testbit):
- state.stack.append(s1 | (TT256 - (1 << testbit)))
- else:
- state.stack.append(s1 & ((1 << testbit) - 1))
+ if s0 <= 31:
+ testbit = s0 * 8 + 7
+ if s1 & (1 << testbit):
+ state.stack.append(s1 | (TT256 - (1 << testbit)))
else:
- state.stack.append(s1)
- # TODO: broad exception handler
- except:
- return []
+ state.stack.append(s1 & ((1 << testbit) - 1))
+ else:
+ state.stack.append(s1)
return [global_state]
@@ -371,24 +370,22 @@ class Instruction:
try:
mstart = util.get_concrete_int(op0)
- # FIXME: broad exception catch
- except:
+ except TypeError:
logging.debug("Unsupported symbolic memory offset in CALLDATACOPY")
return [global_state]
dstart_sym = False
try:
dstart = util.get_concrete_int(op1)
- # FIXME: broad exception catch
- except:
+ except TypeError:
+ logging.debug("Unsupported symbolic calldata offset in CALLDATACOPY")
dstart = simplify(op1)
dstart_sym = True
size_sym = False
try:
size = util.get_concrete_int(op2)
- # FIXME: broad exception catch
- except:
+ except TypeError:
logging.debug("Unsupported symbolic size in CALLDATACOPY")
size = simplify(op2)
size_sym = True
@@ -403,8 +400,7 @@ class Instruction:
if size > 0:
try:
state.mem_extend(mstart, size)
- # FIXME: broad exception catch
- except:
+ except TypeError:
logging.debug("Memory allocation error: mstart = " + str(mstart) + ", size = " + str(size))
state.mem_extend(mstart, 1)
state.memory[mstart] = global_state.new_bitvec(
@@ -422,7 +418,7 @@ class Instruction:
for i in range(0, len(new_memory), 32):
state.memory[i+mstart] = simplify(Concat(new_memory[i:i+32]))
- except:
+ except IndexError:
logging.debug("Exception copying calldata to memory")
state.memory[mstart] = global_state.new_bitvec(
@@ -472,13 +468,11 @@ class Instruction:
global keccak_function_manager
state = global_state.mstate
- environment = global_state.environment
op0, op1 = state.stack.pop(), state.stack.pop()
try:
index, length = util.get_concrete_int(op0), util.get_concrete_int(op1)
- # FIXME: broad exception catch
- except:
+ except TypeError:
# Can't access symbolic memory offsets
if is_expr(op0):
op0 = simplify(op0)
@@ -490,7 +484,7 @@ class Instruction:
data = b''.join([util.get_concrete_int(i).to_bytes(1, byteorder='big')
for i in state.memory[index: index + length]])
- except AttributeError:
+ except TypeError:
argument = str(state.memory[index]).replace(" ", "_")
result = BitVec("KECCAC[{}]".format(argument), 256)
@@ -515,14 +509,14 @@ class Instruction:
try:
concrete_memory_offset = helper.get_concrete_int(memory_offset)
- except AttributeError:
+ except TypeError:
logging.debug("Unsupported symbolic memory offset in CODECOPY")
return [global_state]
try:
concrete_size = helper.get_concrete_int(size)
global_state.mstate.mem_extend(concrete_memory_offset, concrete_size)
- except:
+ except TypeError:
# except both attribute error and Exception
global_state.mstate.mem_extend(concrete_memory_offset, 1)
global_state.mstate.memory[concrete_memory_offset] = \
@@ -531,7 +525,7 @@ class Instruction:
try:
concrete_code_offset = helper.get_concrete_int(code_offset)
- except AttributeError:
+ except TypeError:
logging.debug("Unsupported symbolic code offset in CODECOPY")
global_state.mstate.mem_extend(concrete_memory_offset, concrete_size)
for i in range(concrete_size):
@@ -565,7 +559,7 @@ class Instruction:
environment = global_state.environment
try:
addr = hex(helper.get_concrete_int(addr))
- except AttributeError:
+ except TypeError:
logging.info("unsupported symbolic address for EXTCODESIZE")
state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256))
return [global_state]
@@ -639,7 +633,7 @@ class Instruction:
try:
offset = util.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("Can't MLOAD from symbolic index")
data = global_state.new_bitvec("mem[" + str(simplify(op0)) + "]", 256)
state.stack.append(data)
@@ -664,7 +658,7 @@ class Instruction:
try:
mstart = util.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("MSTORE to symbolic index. Not supported")
return [global_state]
@@ -678,16 +672,11 @@ class Instruction:
try:
# Attempt to concretize value
_bytes = util.concrete_int_to_bytes(value)
-
- i = 0
-
- for b in _bytes:
- state.memory[mstart + i] = _bytes[i]
- i += 1
+ state.memory[mstart: mstart + len(_bytes)] = _bytes
except:
try:
state.memory[mstart] = value
- except:
+ except TypeError:
logging.debug("Invalid memory access")
return [global_state]
@@ -699,7 +688,7 @@ class Instruction:
try:
offset = util.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("MSTORE to symbolic index. Not supported")
return [global_state]
@@ -720,7 +709,7 @@ class Instruction:
index = util.get_concrete_int(index)
return self._sload_helper(global_state, index)
- except AttributeError:
+ except TypeError:
if not keccak_function_manager.is_keccak(index):
return self._sload_helper(global_state, str(index))
@@ -748,7 +737,8 @@ class Instruction:
return self._sload_helper(global_state, str(index))
- def _sload_helper(self, global_state, index, constraints=None):
+ @staticmethod
+ def _sload_helper(global_state, index, constraints=None):
try:
data = global_state.environment.active_account.storage[index]
except KeyError:
@@ -761,8 +751,8 @@ class Instruction:
global_state.mstate.stack.append(data)
return [global_state]
-
- def _get_constraints(self, keccak_keys, this_key, argument):
+ @staticmethod
+ def _get_constraints(keccak_keys, this_key, argument):
global keccak_function_manager
for keccak_key in keccak_keys:
if keccak_key == this_key:
@@ -781,7 +771,7 @@ class Instruction:
try:
index = util.get_concrete_int(index)
return self._sstore_helper(global_state, index, value)
- except AttributeError:
+ except TypeError:
is_keccak = keccak_function_manager.is_keccak(index)
if not is_keccak:
return self._sstore_helper(global_state, str(index), value)
@@ -812,7 +802,8 @@ class Instruction:
return self._sstore_helper(global_state, str(index), value)
- def _sstore_helper(self, global_state, index, value, constraint=None):
+ @staticmethod
+ def _sstore_helper(global_state, index, value, constraint=None):
try:
global_state.environment.active_account = deepcopy(global_state.environment.active_account)
global_state.accounts[
@@ -834,7 +825,7 @@ class Instruction:
disassembly = global_state.environment.code
try:
jump_addr = util.get_concrete_int(state.stack.pop())
- except AttributeError:
+ except TypeError:
raise InvalidJumpDestination("Invalid jump argument (symbolic address)")
except IndexError:
raise StackUnderflowException()
@@ -864,8 +855,7 @@ class Instruction:
try:
jump_addr = util.get_concrete_int(op0)
- # FIXME: to broad exception handler
- except:
+ except TypeError:
logging.debug("Skipping JUMPI to invalid destination.")
global_state.mstate.pc += 1
return [global_state]
@@ -925,7 +915,7 @@ class Instruction:
state = global_state.mstate
dpth = int(self.op_code[3:])
state.stack.pop(), state.stack.pop()
- [state.stack.pop() for x in range(dpth)]
+ [state.stack.pop() for _ in range(dpth)]
# Not supported
return [global_state]
@@ -945,7 +935,7 @@ class Instruction:
return_data = [global_state.new_bitvec("return_data", 256)]
try:
return_data = state.memory[util.get_concrete_int(offset):util.get_concrete_int(offset + length)]
- except AttributeError:
+ except TypeError:
logging.debug("Return with symbolic length or offset. Not supported")
global_state.current_transaction.end(global_state, return_data)
@@ -970,7 +960,14 @@ class Instruction:
@StateTransition()
def revert_(self, global_state):
- return []
+ state = global_state.mstate
+ offset, length = state.stack.pop(), state.stack.pop()
+ return_data = [global_state.new_bitvec("return_data", 256)]
+ try:
+ return_data = state.memory[util.get_concrete_int(offset):util.get_concrete_int(offset + length)]
+ except TypeError:
+ logging.debug("Return with symbolic length or offset. Not supported")
+ global_state.current_transaction.end(global_state, return_data=return_data, revert=True)
@StateTransition()
def assert_fail_(self, global_state):
@@ -995,7 +992,7 @@ class Instruction:
callee_address, callee_account, call_data, value, call_data_type, gas, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader, True)
except ValueError as e:
- logging.info(
+ logging.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(e)
)
# TODO: decide what to do in this case
@@ -1012,7 +1009,7 @@ class Instruction:
try:
mem_out_start = helper.get_concrete_int(memory_out_offset)
mem_out_sz = memory_out_size.as_long()
- except AttributeError:
+ except TypeError:
logging.debug("CALL with symbolic start or offset not supported")
return [global_state]
@@ -1069,7 +1066,7 @@ class Instruction:
try:
memory_out_offset = util.get_concrete_int(memory_out_offset) if isinstance(memory_out_offset, ExprRef) else memory_out_offset
memory_out_size = util.get_concrete_int(memory_out_size) if isinstance(memory_out_size, ExprRef) else memory_out_size
- except AttributeError:
+ except TypeError:
global_state.mstate.stack.append(global_state.new_bitvec("retval_" + str(instr['address']), 256))
return [global_state]
@@ -1137,7 +1134,7 @@ class Instruction:
try:
memory_out_offset = util.get_concrete_int(memory_out_offset) if isinstance(memory_out_offset, ExprRef) else memory_out_offset
memory_out_size = util.get_concrete_int(memory_out_size) if isinstance(memory_out_size, ExprRef) else memory_out_size
- except AttributeError:
+ except TypeError:
global_state.mstate.stack.append(global_state.new_bitvec("retval_" + str(instr['address']), 256))
return [global_state]
@@ -1209,7 +1206,7 @@ class Instruction:
ExprRef) else memory_out_offset
memory_out_size = util.get_concrete_int(memory_out_size) if isinstance(memory_out_size,
ExprRef) else memory_out_size
- except AttributeError:
+ except TypeError:
global_state.mstate.stack.append(global_state.new_bitvec("retval_" + str(instr['address']), 256))
return [global_state]
diff --git a/mythril/laser/ethereum/state.py b/mythril/laser/ethereum/state.py
index 826dd7e6..4c821c7e 100644
--- a/mythril/laser/ethereum/state.py
+++ b/mythril/laser/ethereum/state.py
@@ -349,10 +349,10 @@ class GlobalState:
def new_bitvec(self, name, size=256):
transaction_id = self.current_transaction.id
- node_id = self.node.uid
return BitVec("{}_{}".format(transaction_id, name), size)
+
class WorldState:
"""
The WorldState class represents the world state as described in the yellow paper
diff --git a/mythril/laser/ethereum/strategy/__init__.py b/mythril/laser/ethereum/strategy/__init__.py
index e69de29b..3c208147 100644
--- a/mythril/laser/ethereum/strategy/__init__.py
+++ b/mythril/laser/ethereum/strategy/__init__.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+
+
+class BasicSearchStrategy(ABC):
+ __slots__ = 'work_list', 'max_depth'
+
+ def __init__(self, work_list, max_depth):
+ self.work_list = work_list
+ self.max_depth = max_depth
+
+ def __iter__(self):
+ return self
+
+ @abstractmethod
+ def get_strategic_global_state(self):
+ raise NotImplementedError("Must be implemented by a subclass")
+
+ def __next__(self):
+ try:
+ global_state = self.get_strategic_global_state()
+ if global_state.mstate.depth >= self.max_depth:
+ return self.__next__()
+ return global_state
+ except IndexError:
+ raise StopIteration
diff --git a/mythril/laser/ethereum/strategy/basic.py b/mythril/laser/ethereum/strategy/basic.py
index 33dca443..77c30b30 100644
--- a/mythril/laser/ethereum/strategy/basic.py
+++ b/mythril/laser/ethereum/strategy/basic.py
@@ -1,54 +1,67 @@
"""
This module implements basic symbolic execution search strategies
"""
+from random import randrange
+from . import BasicSearchStrategy
+try:
+ from random import choices
+except ImportError:
-class DepthFirstSearchStrategy:
+ # This is for supporting python versions < 3.6
+ from itertools import accumulate
+ from random import random
+ from bisect import bisect
+
+ def choices(population, weights=None):
+ """
+ Returns a random element out of the population based on weight.
+ If the relative weights or cumulative weights are not specified,
+ the selections are made with equal probability.
+ """
+ if weights is None:
+ return [population[int(random() * len(population))]]
+ cum_weights = accumulate(weights)
+ return [population[bisect(cum_weights, random()*cum_weights[-1], 0, len(population)-1)]]
+
+
+class DepthFirstSearchStrategy(BasicSearchStrategy):
"""
Implements a depth first search strategy
I.E. Follow one path to a leaf, and then continue to the next one
"""
- def __init__(self, work_list, max_depth):
- self.work_list = work_list
- self.max_depth = max_depth
-
- def __iter__(self):
- return self
- def __next__(self):
- """ Picks the next state to execute """
- try:
- # This strategies assumes that new states are appended at the end of the work_list
- # By taking the last element we effectively pick the "newest" states, which amounts to dfs
- global_state = self.work_list.pop()
- if global_state.mstate.depth >= self.max_depth:
- return self.__next__()
- return global_state
- except IndexError:
- raise StopIteration()
+ def get_strategic_global_state(self):
+ return self.work_list.pop()
-class BreadthFirstSearchStrategy:
+class BreadthFirstSearchStrategy(BasicSearchStrategy):
"""
Implements a breadth first search strategy
I.E. Execute all states of a "level" before continuing
"""
- def __init__(self, work_list, max_depth):
- self.work_list = work_list
- self.max_depth = max_depth
-
- def __iter__(self):
- return self
-
- def __next__(self):
- """ Picks the next state to execute """
- try:
- # This strategies assumes that new states are appended at the end of the work_list
- # By taking the first element we effectively pick the "oldest" states, which amounts to bfs
- global_state = self.work_list.pop(0)
- if global_state.mstate.depth >= self.max_depth:
- return self.__next__()
- return global_state
- except IndexError:
- raise StopIteration()
+
+ def get_strategic_global_state(self):
+ return self.work_list.pop(0)
+
+
+class ReturnRandomNaivelyStrategy(BasicSearchStrategy):
+ """
+ chooses a random state from the worklist with equal likelihood
+ """
+ def get_strategic_global_state(self):
+ if len(self.work_list) > 0:
+ return self.work_list.pop(randrange(len(self.work_list)))
+ else:
+ raise IndexError
+
+
+class ReturnWeightedRandomStrategy(BasicSearchStrategy):
+ """
+ chooses a random state from the worklist with likelihood based on inverse proportion to depth
+ """
+
+ def get_strategic_global_state(self):
+ probability_distribution = [1/(global_state.mstate.depth+1) for global_state in self.work_list]
+ return self.work_list.pop(choices(range(len(self.work_list)), probability_distribution)[0])
diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py
index cdf85484..d7809705 100644
--- a/mythril/laser/ethereum/svm.py
+++ b/mythril/laser/ethereum/svm.py
@@ -1,4 +1,5 @@
import logging
+from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.state import WorldState
from mythril.laser.ethereum.transaction import TransactionStartSignal, TransactionEndSignal, \
ContractCreationTransaction
@@ -28,7 +29,7 @@ class LaserEVM:
"""
def __init__(self, accounts, dynamic_loader=None, max_depth=float('inf'), execution_timeout=60, create_timeout=10,
- strategy=DepthFirstSearchStrategy):
+ strategy=DepthFirstSearchStrategy, max_transaction_count=3):
world_state = WorldState()
world_state.accounts = accounts
# this sets the initial world state
@@ -45,6 +46,7 @@ class LaserEVM:
self.work_list = []
self.strategy = strategy(self.work_list, max_depth)
self.max_depth = max_depth
+ self.max_transaction_count = max_transaction_count
self.execution_timeout = execution_timeout
self.create_timeout = create_timeout
@@ -73,16 +75,20 @@ class LaserEVM:
logging.info("Finished contract creation, found {} open states".format(len(self.open_states)))
if len(self.open_states) == 0:
logging.warning("No contract was created during the execution of contract creation "
- "Increase the resources for creation execution (--max-depth or --create_timeout)")
+ "Increase the resources for creation execution (--max-depth or --create-timeout)")
# Reset code coverage
self.coverage = {}
- self.time = datetime.now()
- logging.info("Starting message call transaction")
- execute_message_call(self, created_account.address)
+ for i in range(self.max_transaction_count):
+ initial_coverage = self._get_covered_instructions()
- self.time = datetime.now()
- execute_message_call(self, created_account.address)
+ self.time = datetime.now()
+ logging.info("Starting message call transaction, iteration: {}".format(i))
+ execute_message_call(self, created_account.address)
+
+ end_coverage = self._get_covered_instructions()
+ if end_coverage == initial_coverage:
+ break
logging.info("Finished symbolic execution")
logging.info("%d nodes, %d edges, %d total states", len(self.nodes), len(self.edges), self.total_states)
@@ -90,6 +96,13 @@ class LaserEVM:
cov = reduce(lambda sum_, val: sum_ + 1 if val else sum_, coverage[1]) / float(coverage[0]) * 100
logging.info("Achieved {} coverage for code: {}".format(cov, code))
+ def _get_covered_instructions(self) -> int:
+ """ Gets the total number of covered instructions for all accounts in the svm"""
+ total_covered_instructions = 0
+ for _, cv in self.coverage.items():
+ total_covered_instructions += reduce(lambda sum_, val: sum_ + 1 if val else sum_, cv[1])
+ return total_covered_instructions
+
def exec(self, create=False):
for global_state in self.strategy:
if self.execution_timeout and not create:
@@ -138,30 +151,31 @@ class LaserEVM:
new_global_states = self._end_message_call(return_global_state, global_state,
revert_changes=True, return_data=None)
- except TransactionStartSignal as e:
+ except TransactionStartSignal as start_signal:
# Setup new global state
- new_global_state = e.transaction.initial_global_state()
+ new_global_state = start_signal.transaction.initial_global_state()
- new_global_state.transaction_stack = copy(global_state.transaction_stack) + [(e.transaction, global_state)]
+ new_global_state.transaction_stack = copy(global_state.transaction_stack) + [(start_signal.transaction, global_state)]
new_global_state.node = global_state.node
new_global_state.mstate.constraints = global_state.mstate.constraints
return [new_global_state], op_code
- except TransactionEndSignal as e:
- transaction, return_global_state = e.global_state.transaction_stack.pop()
+ except TransactionEndSignal as end_signal:
+ transaction, return_global_state = end_signal.global_state.transaction_stack.pop()
if return_global_state is None:
- if not isinstance(transaction, ContractCreationTransaction) or transaction.return_data:
- e.global_state.world_state.node = global_state.node
- self.open_states.append(e.global_state.world_state)
+ if (not isinstance(transaction, ContractCreationTransaction) or transaction.return_data) and not end_signal.revert:
+ end_signal.global_state.world_state.node = global_state.node
+ self.open_states.append(end_signal.global_state.world_state)
new_global_states = []
else:
# First execute the post hook for the transaction ending instruction
- self._execute_post_hook(op_code, [e.global_state])
+ self._execute_post_hook(op_code, [end_signal.global_state])
new_global_states = self._end_message_call(return_global_state, global_state,
- revert_changes=False, return_data=transaction.return_data)
+ revert_changes=False or end_signal.revert,
+ return_data=transaction.return_data)
self._execute_post_hook(op_code, new_global_states)
@@ -246,13 +260,12 @@ class LaserEVM:
environment = state.environment
disassembly = environment.code
- if address in state.environment.code.addr_to_func:
+ if address in disassembly.address_to_function_name:
# Enter a new function
-
- environment.active_function_name = disassembly.addr_to_func[address]
+ environment.active_function_name = disassembly.address_to_function_name[address]
new_node.flags |= NodeFlags.FUNC_ENTRY
- logging.info(
+ logging.debug(
"- Entering function " + environment.active_account.contract_name + ":" + new_node.function_name)
elif address == 0:
environment.active_function_name = "fallback"
diff --git a/mythril/laser/ethereum/taint_analysis.py b/mythril/laser/ethereum/taint_analysis.py
index 4f605008..061ab088 100644
--- a/mythril/laser/ethereum/taint_analysis.py
+++ b/mythril/laser/ethereum/taint_analysis.py
@@ -109,7 +109,8 @@ class TaintRunner:
records = TaintRunner.execute_node(node, record, index)
result.add_records(records)
-
+ if len(records) == 0: # continue if there is no record to work on
+ continue
children = TaintRunner.children(node, statespace, environment, transaction_stack_length)
for child in children:
current_nodes.append((child, records[-1], 0))
@@ -212,7 +213,7 @@ class TaintRunner:
_ = record.stack.pop()
try:
index = helper.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("Can't MLOAD taint track symbolically")
record.stack.append(False)
return
@@ -224,7 +225,7 @@ class TaintRunner:
_, value_taint = record.stack.pop(), record.stack.pop()
try:
index = helper.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("Can't mstore taint track symbolically")
return
@@ -235,7 +236,7 @@ class TaintRunner:
_ = record.stack.pop()
try:
index = helper.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("Can't MLOAD taint track symbolically")
record.stack.append(False)
return
@@ -247,7 +248,7 @@ class TaintRunner:
_, value_taint = record.stack.pop(), record.stack.pop()
try:
index = helper.get_concrete_int(op0)
- except AttributeError:
+ except TypeError:
logging.debug("Can't mstore taint track symbolically")
return
diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py
index f96f2cc7..31ea7704 100644
--- a/mythril/laser/ethereum/transaction/transaction_models.py
+++ b/mythril/laser/ethereum/transaction/transaction_models.py
@@ -12,10 +12,12 @@ def get_next_transaction_id():
_next_transaction_id += 1
return _next_transaction_id
+
class TransactionEndSignal(Exception):
""" Exception raised when a transaction is finalized"""
- def __init__(self, global_state):
+ def __init__(self, global_state, revert=False):
self.global_state = global_state
+ self.revert = revert
class TransactionStartSignal(Exception):
@@ -71,9 +73,9 @@ class MessageCallTransaction:
return global_state
- def end(self, global_state, return_data=None):
+ def end(self, global_state, return_data=None, revert=False):
self.return_data = return_data
- raise TransactionEndSignal(global_state)
+ raise TransactionEndSignal(global_state, revert)
class ContractCreationTransaction:
@@ -126,9 +128,9 @@ class ContractCreationTransaction:
return global_state
- def end(self, global_state, return_data=None):
+ def end(self, global_state, return_data=None, revert=False):
- if not all([isinstance(element, int) for element in return_data]):
+ if not all([isinstance(element, int) for element in return_data]) or len(return_data) == 0:
self.return_data = None
raise TransactionEndSignal(global_state)
@@ -136,5 +138,8 @@ class ContractCreationTransaction:
global_state.environment.active_account.code = Disassembly(contract_code)
self.return_data = global_state.environment.active_account.address
+ assert global_state.environment.active_account.code.instruction_list != []
+
+ raise TransactionEndSignal(global_state, revert=revert)
+
- raise TransactionEndSignal(global_state)
diff --git a/mythril/laser/ethereum/util.py b/mythril/laser/ethereum/util.py
index c6c8e5ce..f91bf9f6 100644
--- a/mythril/laser/ethereum/util.py
+++ b/mythril/laser/ethereum/util.py
@@ -10,13 +10,15 @@ TT256M1 = 2 ** 256 - 1
TT255 = 2 ** 255
+
+
def sha3(seed):
return _sha3.keccak_256(bytes(seed)).digest()
def safe_decode(hex_encoded_string):
- if (hex_encoded_string.startswith("0x")):
+ if hex_encoded_string.startswith("0x"):
return bytes.fromhex(hex_encoded_string[2:])
else:
return bytes.fromhex(hex_encoded_string)
@@ -80,9 +82,12 @@ def get_concrete_int(item):
elif is_true(simplified):
return 1
else:
- raise ValueError("Symbolic boolref encountered")
+ raise TypeError("Symbolic boolref encountered")
- return simplify(item).as_long()
+ try:
+ return simplify(item).as_long()
+ except AttributeError:
+ raise TypeError("Got a symbolic BitVecRef")
def concrete_int_from_bytes(_bytes, start_index):
@@ -99,7 +104,7 @@ def concrete_int_to_bytes(val):
# logging.debug("concrete_int_to_bytes " + str(val))
- if (type(val) == int):
+ if type(val) == int:
return val.to_bytes(32, byteorder='big')
return (simplify(val).as_long()).to_bytes(32, byteorder='big')
diff --git a/mythril/mythril.py b/mythril/mythril.py
index 080493fd..9492b177 100644
--- a/mythril/mythril.py
+++ b/mythril/mythril.py
@@ -20,8 +20,8 @@ import platform
from mythril.ether import util
from mythril.ether.ethcontract import ETHContract
from mythril.ether.soliditycontract import SolidityContract, get_contracts_from_file
-from mythril.rpc.client import EthJsonRpc
-from mythril.rpc.exceptions import ConnectionError
+from mythril.ethereum.interface.rpc.client import EthJsonRpc
+from mythril.ethereum.interface.rpc.exceptions import ConnectionError
from mythril.support import signatures
from mythril.support.truffle import analyze_truffle_project
from mythril.support.loader import DynLoader
@@ -31,7 +31,7 @@ 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
+from mythril.ethereum.interface.leveldb.client import EthLevelDB
# logging.basicConfig(level=logging.DEBUG)
@@ -76,18 +76,20 @@ class Mythril(object):
"""
def __init__(self, solv=None,
- solc_args=None, dynld=False):
+ solc_args=None, dynld=False,
+ enable_online_lookup=False):
self.solv = solv
self.solc_args = solc_args
self.dynld = dynld
+ self.enable_online_lookup = enable_online_lookup
self.mythril_dir = self._init_mythril_dir()
- self.sigs = signatures.SignatureDb()
+ self.sigs = signatures.SignatureDb(enable_online_lookup=self.enable_online_lookup)
try:
self.sigs.open() # tries mythril_dir/signatures.json by default (provide path= arg to make this configurable)
- except FileNotFoundError as fnfe:
+ except FileNotFoundError:
logging.info(
"No signature database found. Creating database if sigs are loaded in: " + self.sigs.signatures_file + "\n" +
"Consider replacing it with the pre-initialized database at https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json")
@@ -103,7 +105,8 @@ class Mythril(object):
self.contracts = [] # loaded contracts
- def _init_mythril_dir(self):
+ @staticmethod
+ def _init_mythril_dir():
try:
mythril_dir = os.environ['MYTHRIL_DIR']
except KeyError:
@@ -179,7 +182,8 @@ class Mythril(object):
def analyze_truffle_project(self, *args, **kwargs):
return analyze_truffle_project(self.sigs, *args, **kwargs) # just passthru by passing signatures for now
- def _init_solc_binary(self, version):
+ @staticmethod
+ def _init_solc_binary(version):
# Figure out solc binary and version
# Only proper versions are supported. No nightlies, commits etc (such as available in remix)
@@ -259,8 +263,7 @@ class Mythril(object):
def search_db(self, search):
- def search_callback(contract, address, balance):
-
+ def search_callback(_, address, balance):
print("Address: " + address + ", balance: " + str(balance))
try:
@@ -277,7 +280,7 @@ class Mythril(object):
def load_from_bytecode(self, code):
address = util.get_indexed_address(0)
- self.contracts.append(ETHContract(code, name="MAIN"))
+ self.contracts.append(ETHContract(code, name="MAIN", enable_online_lookup=self.enable_online_lookup))
return address, self.contracts[-1] # return address and contract object
def load_from_address(self, address):
@@ -288,15 +291,15 @@ class Mythril(object):
code = self.eth.eth_getCode(address)
except FileNotFoundError as e:
raise CriticalError("IPC error: " + str(e))
- except ConnectionError as e:
+ except ConnectionError:
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))
+ 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))
+ self.contracts.append(ETHContract(code, name=address, enable_online_lookup=self.enable_online_lookup))
return address, self.contracts[-1] # return address and contract object
def load_from_solidity(self, solidity_files):
@@ -358,14 +361,16 @@ class Mythril(object):
return generate_graph(sym, physics=enable_physics, phrackify=phrackify)
def fire_lasers(self, strategy, contracts=None, address=None,
- modules=None, verbose_report=False, max_depth=None, execution_timeout=None, create_timeout=None):
+ modules=None, verbose_report=False, max_depth=None, execution_timeout=None, create_timeout=None,
+ max_transaction_count=None):
all_issues = []
for contract in (contracts or self.contracts):
sym = SymExecWrapper(contract, address, strategy,
dynloader=DynLoader(self.eth) if self.dynld else None,
max_depth=max_depth, execution_timeout=execution_timeout,
- create_timeout=create_timeout)
+ create_timeout=create_timeout,
+ max_transaction_count=max_transaction_count)
issues = fire_lasers(sym, modules)
@@ -431,11 +436,12 @@ class Mythril(object):
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:
+ except ConnectionError:
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):
+ @staticmethod
+ def disassemble(contract):
return contract.get_easm()
@staticmethod
diff --git a/mythril/support/loader.py b/mythril/support/loader.py
index 7c0855ec..d219d17c 100644
--- a/mythril/support/loader.py
+++ b/mythril/support/loader.py
@@ -37,7 +37,7 @@ class DynLoader:
m = re.match(r'^(0x[0-9a-fA-F]{40})$', dependency_address)
- if (m):
+ if m:
dependency_address = m.group(1)
else:
@@ -47,7 +47,7 @@ class DynLoader:
code = self.eth.eth_getCode(dependency_address)
- if (code == "0x"):
+ if code == "0x":
return None
else:
return Disassembly(code)
diff --git a/mythril/support/signatures.py b/mythril/support/signatures.py
index 7c454089..a5290088 100644
--- a/mythril/support/signatures.py
+++ b/mythril/support/signatures.py
@@ -11,7 +11,6 @@ from subprocess import Popen, PIPE
from mythril.exceptions import CompilerError
-# todo: tintinweb - make this a normal requirement? (deps: eth-abi and requests, both already required by mythril)
try:
# load if available but do not fail
import ethereum_input_decoder
@@ -54,7 +53,7 @@ except ImportError:
class SignatureDb(object):
- def __init__(self, enable_online_lookup=True):
+ def __init__(self, enable_online_lookup=False):
"""
Constr
:param enable_online_lookup: enable onlien signature hash lookup
@@ -165,9 +164,12 @@ class SignatureDb(object):
except FourByteDirectoryOnlineLookupError as fbdole:
self.online_directory_unavailable_until = time.time() + 2 * 60 # wait at least 2 mins to try again
logging.warning("online function signature lookup not available. will not try to lookup hash for the next 2 minutes. exception: %r" % fbdole)
+
+ if sighash not in self.signatures:
+ return []
if type(self.signatures[sighash]) != list:
return [self.signatures[sighash]]
- return self.signatures[sighash] # raise keyerror
+ return self.signatures[sighash]
def __getitem__(self, item):
"""
diff --git a/mythril/support/truffle.py b/mythril/support/truffle.py
index aced1086..6dd13b2e 100644
--- a/mythril/support/truffle.py
+++ b/mythril/support/truffle.py
@@ -40,7 +40,7 @@ def analyze_truffle_project(sigs, args):
if len(bytecode) < 4:
continue
- sigs.import_from_solidity_source(contractdata['sourcePath'])
+ sigs.import_from_solidity_source(contractdata['sourcePath'], solc_args=args.solc_args)
sigs.write()
ethcontract = ETHContract(bytecode, name=name)
diff --git a/requirements.txt b/requirements.txt
index 53a4a086..b41daf56 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,10 @@
+coloredlogs>=10.0
configparser>=3.5.0
coverage
eth_abi>=1.0.0
eth-account>=0.1.0a2
ethereum>=2.3.2
+ethereum-input-decoder>=0.2.2
eth-hash>=0.1.0
eth-keyfile>=0.5.1
eth-keys>=0.2.0b3
diff --git a/setup.py b/setup.py
index cfb6cb7b..e85299b7 100755
--- a/setup.py
+++ b/setup.py
@@ -82,6 +82,7 @@ setup(
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
install_requires=[
+ 'coloredlogs>=10.0',
'ethereum>=2.3.2',
'z3-solver>=4.5',
'requests',
@@ -103,7 +104,8 @@ setup(
'py-flags',
'mock',
'configparser>=3.5.0',
- 'persistent>=4.2.0'
+ 'persistent>=4.2.0',
+ 'ethereum-input-decoder>=0.2.2'
],
tests_require=[
diff --git a/static/Ownable.html b/static/Ownable.html
index 964558e4..9dff1f1a 100644
--- a/static/Ownable.html
+++ b/static/Ownable.html
@@ -1,5 +1,7 @@
+
-
+
+ Call Graph