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 - -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