diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index a3dfed4f..4ba3b942 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -4,13 +4,13 @@ import json import operator from jinja2 import PackageLoader, Environment from typing import Dict, List -import _pysha3 as sha3 import hashlib from mythril.solidity.soliditycontract import SolidityContract from mythril.analysis.swc_data import SWC_TO_TITLE from mythril.support.source_support import Source from mythril.support.start_time import StartTime +from mythril.support.support_utils import get_code_hash from time import time log = logging.getLogger(__name__) @@ -63,20 +63,7 @@ class Issue: self.lineno = None self.source_mapping = None self.discovery_time = time() - StartTime().global_start_time - - try: - keccak = sha3.keccak_256() - keccak.update( - bytes.fromhex(bytecode[2:]) - if bytecode[:2] == "0x" - else bytes.fromhex(bytecode) - ) - self.bytecode_hash = "0x" + keccak.hexdigest() - except ValueError: - log.debug( - "Unable to change the bytecode to bytes. Bytecode: {}".format(bytecode) - ) - self.bytecode_hash = "" + self.bytecode_hash = get_code_hash(bytecode) @property def as_dict(self): @@ -144,7 +131,7 @@ class Report: loader=PackageLoader("mythril.analysis"), trim_blocks=True ) - def __init__(self, verbose=False, source=None, exceptions=None): + def __init__(self, verbose=False, contracts=None, exceptions=None): """ :param verbose: @@ -153,7 +140,8 @@ class Report: self.verbose = verbose self.solc_version = "" self.meta = {} - self.source = source or Source() + self.source = Source() + self.source.get_source_from_contracts_list(contracts) self.exceptions = exceptions or [] def sorted_issues(self): @@ -210,12 +198,7 @@ class Report: for key, issue in self.issues.items(): - if issue.bytecode_hash not in source_list: - idx = len(source_list) - source_list.append(issue.bytecode_hash) - else: - idx = source_list.index(issue.bytecode_hash) - + idx = self.source.get_source_index(issue.bytecode_hash) try: title = SWC_TO_TITLE[issue.swc_id] except KeyError: @@ -238,9 +221,9 @@ class Report: result = [ { "issues": _issues, - "sourceType": "raw-bytecode", - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": source_list, + "sourceType": self.source.source_type, + "sourceFormat": self.source.source_format, + "sourceList": self.source.source_list, "meta": meta_data, } ] diff --git a/mythril/ethereum/evmcontract.py b/mythril/ethereum/evmcontract.py index 47a87f83..8502058f 100644 --- a/mythril/ethereum/evmcontract.py +++ b/mythril/ethereum/evmcontract.py @@ -1,15 +1,14 @@ """This module contains the class representing EVM contracts, aka Smart Contracts.""" import re -import _pysha3 as sha3 import logging - -log = logging.getLogger(__name__) - import persistent -from ethereum import utils +from ethereum import utils from mythril.disassembler.disassembly import Disassembly +from mythril.support.support_utils import get_code_hash + +log = logging.getLogger(__name__) class EVMContract(persistent.Persistent): @@ -47,7 +46,7 @@ class EVMContract(persistent.Persistent): :return: runtime bytecode hash """ - return self._get_hash(self.code[2:]) + return get_code_hash(self.code) @property def creation_bytecode_hash(self): @@ -55,23 +54,7 @@ class EVMContract(persistent.Persistent): :return: Creation bytecode hash """ - return self._get_hash(self.creation_code[2:]) - - @staticmethod - def _get_hash(code): - """ - :param code: bytecode - :return: Returns hash of the given bytecode - """ - try: - keccak = sha3.keccak_256() - keccak.update(bytes.fromhex(code[2:])) - return "0x" + keccak.hexdigest() - except ValueError: - log.debug( - "Unable to change the bytecode to bytes. Bytecode: {}".format(code) - ) - return "" + return get_code_hash(self.creation_code) def as_dict(self): """ diff --git a/mythril/mythril.py b/mythril/mythril.py index 872f23ae..86aa6c2f 100644 --- a/mythril/mythril.py +++ b/mythril/mythril.py @@ -608,10 +608,8 @@ class Mythril(object): all_issues += issues log.info("Solver statistics: \n{}".format(str(SolverStatistics()))) - source_data = Source() - source_data.get_source_from_contracts_list(self.contracts) # Finally, output the results - report = Report(verbose_report, source_data, exceptions=exceptions) + report = Report(verbose_report, self.contracts, exceptions=exceptions) for issue in all_issues: report.append_issue(issue) diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index e42e943c..5d0596e2 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -166,7 +166,7 @@ class MythrilAnalyzer: source_data = Source() source_data.get_source_from_contracts_list(self.contracts) # Finally, output the results - report = Report(verbose_report, source_data, exceptions=exceptions) + report = Report(verbose_report, contracts=self.contracts, exceptions=exceptions) for issue in all_issues: report.append_issue(issue) diff --git a/mythril/support/source_support.py b/mythril/support/source_support.py index c774a425..51d94ee8 100644 --- a/mythril/support/source_support.py +++ b/mythril/support/source_support.py @@ -5,9 +5,7 @@ from mythril.ethereum.evmcontract import EVMContract class Source: """Class to handle to source data""" - def __init__( - self, source_type=None, source_format=None, source_list=None, meta=None - ): + def __init__(self, source_type=None, source_format=None, source_list=None): """ :param source_type: whether it is a solidity-file or evm-bytecode :param source_format: whether it is bytecode, ethereum-address or text @@ -17,7 +15,7 @@ class Source: self.source_type = source_type self.source_format = source_format self.source_list = source_list or [] - self.meta = meta + self._source_hash = [] def get_source_from_contracts_list(self, contracts): """ @@ -32,16 +30,28 @@ class Source: self.source_format = "text" for contract in contracts: self.source_list += [file.filename for file in contract.solidity_files] + self._source_hash.append(contract.bytecode_hash) elif isinstance(contracts[0], EVMContract): self.source_format = "evm-byzantium-bytecode" self.source_type = ( - "raw-bytecode" if contracts[0].name == "MAIN" else "ethereum-address" + "ethereum-address" + if len(contracts[0].name) == 42 and contracts[0].name[0:2] == "0x" + else "raw-bytecode" ) for contract in contracts: if contract.creation_code: self.source_list.append(contract.creation_bytecode_hash) if contract.code: self.source_list.append(contract.bytecode_hash) + self._source_hash = self.source_list else: assert False # Fail hard + + def get_source_index(self, bytecode_hash: str) -> int: + """ + Find the contract index in the list + :param bytecode_hash: The contract hash + :return: The index of the contract in the _source_hash list + """ + return self._source_hash.index(bytecode_hash) diff --git a/mythril/support/support_utils.py b/mythril/support/support_utils.py index b437d795..1edbefff 100644 --- a/mythril/support/support_utils.py +++ b/mythril/support/support_utils.py @@ -1,5 +1,9 @@ """This module contains utility functions for the Mythril support package.""" from typing import Dict +import logging +import _pysha3 as sha3 + +log = logging.getLogger(__name__) class Singleton(type): @@ -20,3 +24,18 @@ class Singleton(type): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + + +def get_code_hash(code: str) -> str: + """ + :param code: bytecode + :return: Returns hash of the given bytecode + """ + code = code[2:] if code[:2] == "0x" else code + try: + keccak = sha3.keccak_256() + keccak.update(bytes.fromhex(code)) + return "0x" + keccak.hexdigest() + except ValueError: + log.debug("Unable to change the bytecode to bytes. Bytecode: {}".format(code)) + return "" diff --git a/tests/report_test.py b/tests/report_test.py index e9f21001..30e9cabd 100644 --- a/tests/report_test.py +++ b/tests/report_test.py @@ -39,7 +39,7 @@ def _generate_report(input_file): ) issues = fire_lasers(sym) - report = Report() + report = Report(contracts=[contract]) for issue in issues: issue.filename = "test-filename.sol" report.append_issue(issue) diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 b/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 index 0d1e9df5..3bce29c2 100644 --- a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 @@ -1,9 +1 @@ -[ - { - "issues": [], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file +[{"issues": [], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x3746c7c2ae7b0d4c3f8b1905df9a7ea169b9f93bec68a10a00b4c9d27a18c6fb"], "sourceType": "raw-bytecode"}] diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 index 0d1e9df5..c669f304 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 @@ -1,9 +1 @@ -[ - { - "issues": [], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file +[{"issues": [], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x0e6f727bb3301e02d3be831bf34357522fd2f1d40e90dff8e2214553b06b5f6c"], "sourceType": "raw-bytecode"}] diff --git a/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 b/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 index 0d1e9df5..be2cc307 100644 --- a/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/nonascii.sol.o.jsonv2 @@ -1,9 +1 @@ -[ - { - "issues": [], - "meta": {}, - "sourceFormat": "evm-byzantium-bytecode", - "sourceList": [], - "sourceType": "raw-bytecode" - } -] \ No newline at end of file +[{"issues": [], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": ["0x11a78eb09819f505ba4f10747e6d1f7a44480e602c67573b7abac2f733a85d93"], "sourceType": "raw-bytecode"}]