From 0f260ddb6c0fd7ea33873fe1110e2f2b9b294ae7 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 29 May 2019 21:04:45 +0200 Subject: [PATCH 001/108] Add a basic custom strategy --- mythril/analysis/symbolic.py | 1 + mythril/laser/ethereum/strategy/custom.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 mythril/laser/ethereum/strategy/custom.py diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index cdcb9070..f0b48892 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -15,6 +15,7 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnRandomNaivelyStrategy, ReturnWeightedRandomStrategy, BasicSearchStrategy, + ) from mythril.laser.smt import symbol_factory, BitVec from typing import Union, List, Dict, Type diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py new file mode 100644 index 00000000..eac81d04 --- /dev/null +++ b/mythril/laser/ethereum/strategy/custom.py @@ -0,0 +1,19 @@ +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.strategy.basic import BreadthFirstSearchStrategy + + +class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): + """Implements a breadth first search strategy that prunes loops. + """ + + def __init__(self): + pass + + def get_strategic_global_state(self) -> GlobalState: + """ + :return: + """ + + state = self.work_list.pop(0) + + return state From c49500d662bc5987ca99eb0324528dc0dd748c56 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 29 May 2019 21:10:48 +0200 Subject: [PATCH 002/108] Bump version --- mythril/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/version.py b/mythril/version.py index 0b19f08b..0e8364bd 100644 --- a/mythril/version.py +++ b/mythril/version.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -VERSION = "v0.20.7" # NOQA +VERSION = "v0.20.8" # NOQA From 3a1f3a77c30e1da7f0da69527019b30ed10779d8 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 30 May 2019 12:22:30 +0530 Subject: [PATCH 003/108] Fix the source index problem by using proper function name --- mythril/laser/ethereum/svm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 8f74410a..7c09cbc7 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -436,7 +436,11 @@ class LaserEVM: environment = state.environment disassembly = environment.code - if address in disassembly.address_to_function_name: + if isinstance( + state.world_state.transaction_sequence[-1], ContractCreationTransaction + ): + environment.active_function_name = "constructor" + elif address in disassembly.address_to_function_name: # Enter a new function environment.active_function_name = disassembly.address_to_function_name[ address From 37847948f3d398509351dcea146655cf79fbe294 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 30 May 2019 12:41:28 +0530 Subject: [PATCH 004/108] Append transaction in concolic.py --- mythril/laser/ethereum/transaction/concolic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/transaction/concolic.py b/mythril/laser/ethereum/transaction/concolic.py index d8c251dd..164df8db 100644 --- a/mythril/laser/ethereum/transaction/concolic.py +++ b/mythril/laser/ethereum/transaction/concolic.py @@ -88,7 +88,7 @@ def _setup_global_state_for_execution(laser_evm, transaction) -> None: condition=None, ) ) - + global_state.world_state.transaction_sequence.append(transaction) global_state.node = new_node new_node.states.append(global_state) laser_evm.work_list.append(global_state) From bdf3fa944b1d78675f8a494c2bee561d4e4a6b77 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 10:56:00 +0200 Subject: [PATCH 005/108] Add bfs-bounded strategy --- mythril/analysis/modules/dos.py | 2 +- mythril/analysis/symbolic.py | 4 ++- mythril/interfaces/cli.py | 4 +-- mythril/laser/ethereum/strategy/custom.py | 39 +++++++++++++++++++++-- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 9614ba95..3427cd15 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -70,7 +70,7 @@ class DOS(DetectionModule): try: self._jumpdest_count[transaction][target] += 1 - if self._jumpdest_count[transaction][target] == 4: + if self._jumpdest_count[transaction][target] == 3: annotation = ( LoopAnnotation(address, target) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index f0b48892..acb9510e 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -15,8 +15,8 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnRandomNaivelyStrategy, ReturnWeightedRandomStrategy, BasicSearchStrategy, - ) +from mythril.laser.ethereum.strategy.custom import BFSBoundedLoopsStrategy from mythril.laser.smt import symbol_factory, BitVec from typing import Union, List, Dict, Type from mythril.solidity.soliditycontract import EVMContract, SolidityContract @@ -69,6 +69,8 @@ class SymExecWrapper: s_strategy = ReturnRandomNaivelyStrategy elif strategy == "weighted-random": s_strategy = ReturnWeightedRandomStrategy + elif strategy == "bfs-bounded": + s_strategy = BFSBoundedLoopsStrategy else: raise ValueError("Invalid strategy argument supplied") diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index ad2d79c7..b442461b 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -200,8 +200,8 @@ def create_parser(parser: argparse.ArgumentParser) -> None: options.add_argument( "--strategy", - choices=["dfs", "bfs", "naive-random", "weighted-random"], - default="bfs", + choices=["dfs", "bfs", "naive-random", "weighted-random", "bfs-bounded"], + default="bfs-bounded", help="Symbolic execution strategy", ) options.add_argument( diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index eac81d04..edc001bc 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -1,13 +1,22 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.strategy.basic import BreadthFirstSearchStrategy +from mythril.laser.ethereum import util +import logging + + +JUMPDEST_LIMIT = 4 +log = logging.getLogger(__name__) class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): """Implements a breadth first search strategy that prunes loops. + JUMPI instructions are skipped after a jump destination has been + targeted JUMPDEST_LIMIT times. """ - def __init__(self): - pass + def __init__(self, work_list, max_depth) -> None: + super().__init__(work_list, max_depth) + self._jumpdest_count = {} # type: Dict[object, dict] def get_strategic_global_state(self) -> GlobalState: """ @@ -16,4 +25,30 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): state = self.work_list.pop(0) + opcode = state.get_current_instruction()["opcode"] + + if opcode == "JUMPI": + + transaction = state.current_transaction + target = util.get_concrete_int(state.mstate.stack[-1]) + + if transaction in self._jumpdest_count: + + try: + if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: + log.info("Skipping JUMPI") + return self.work_list.pop(0) + except KeyError: + self._jumpdest_count[transaction][target] = 0 + + self._jumpdest_count[transaction][target] += 1 + + else: + self._jumpdest_count[transaction] = {} + self._jumpdest_count[transaction][target] = 0 + + log.info( + "JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target]) + ) + return state From ff0609b7c998a811ca2c23c7745e978e45ce308e Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 11:10:23 +0200 Subject: [PATCH 006/108] Add missing import --- mythril/laser/ethereum/strategy/custom.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index edc001bc..238343c0 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -1,6 +1,7 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.strategy.basic import BreadthFirstSearchStrategy from mythril.laser.ethereum import util +from typing import Dict import logging From 8a006ecff95e7699a4ca65f2af5ff566648c3a0d Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 11:14:07 +0200 Subject: [PATCH 007/108] Add norhh suggestion #1 --- mythril/analysis/modules/dos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 3427cd15..47e8db00 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -4,6 +4,7 @@ import logging from typing import Dict, cast, List from mythril.analysis.swc_data import DOS_WITH_BLOCK_GAS_LIMIT +from mythril.laser.ethereum.strategy.custom import JUMPDEST_LIMIT from mythril.analysis.report import Issue from mythril.analysis.modules.base import DetectionModule from mythril.laser.ethereum.state.global_state import GlobalState @@ -70,7 +71,7 @@ class DOS(DetectionModule): try: self._jumpdest_count[transaction][target] += 1 - if self._jumpdest_count[transaction][target] == 3: + if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT - 1: annotation = ( LoopAnnotation(address, target) From 0293ef842701adf6a30e0cab45b39941697eb351 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 11:20:02 +0200 Subject: [PATCH 008/108] Add norhh suggestion #2, remove logging --- mythril/laser/ethereum/strategy/custom.py | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 238343c0..1e4d1922 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -28,28 +28,28 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): opcode = state.get_current_instruction()["opcode"] - if opcode == "JUMPI": + if opcode != "JUMPI": + return state - transaction = state.current_transaction - target = util.get_concrete_int(state.mstate.stack[-1]) + transaction = state.current_transaction + target = util.get_concrete_int(state.mstate.stack[-1]) - if transaction in self._jumpdest_count: + if transaction in self._jumpdest_count: - try: - if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: - log.info("Skipping JUMPI") - return self.work_list.pop(0) - except KeyError: - self._jumpdest_count[transaction][target] = 0 + try: + if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: + return self.work_list.pop(0) + except KeyError: + self._jumpdest_count[transaction][target] = 0 - self._jumpdest_count[transaction][target] += 1 + self._jumpdest_count[transaction][target] += 1 - else: - self._jumpdest_count[transaction] = {} - self._jumpdest_count[transaction][target] = 0 + else: + self._jumpdest_count[transaction] = {} + self._jumpdest_count[transaction][target] = 0 - log.info( - "JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target]) - ) + log.info( + "JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target]) + ) return state From beff100b486d5a39c8739d1c7722ace15f724d47 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 11:23:36 +0200 Subject: [PATCH 009/108] Black &$^@#Q*&ETQ#&^ --- mythril/laser/ethereum/strategy/custom.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 1e4d1922..258ae728 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -48,8 +48,6 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): self._jumpdest_count[transaction] = {} self._jumpdest_count[transaction][target] = 0 - log.info( - "JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target]) - ) + log.info("JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target])) return state From ccf98a311e92fc0f0afa96053d01a1e572087789 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 30 May 2019 11:53:23 +0200 Subject: [PATCH 010/108] Remove another logging statement --- mythril/laser/ethereum/strategy/custom.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 258ae728..b84d0536 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -48,6 +48,4 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): self._jumpdest_count[transaction] = {} self._jumpdest_count[transaction][target] = 0 - log.info("JUMPDEST COUNT: {}".format(self._jumpdest_count[transaction][target])) - return state From 1ad0e8b844de7e2607a8e18c2869001db305e4e5 Mon Sep 17 00:00:00 2001 From: Joran Honig Date: Fri, 31 May 2019 23:44:53 +0200 Subject: [PATCH 011/108] refactor and update setup.py - update project name in setup.py & documentation - update copyright - update url - update setup.py according to best practices --- docs/source/conf.py | 8 +- mythril/__init__.py | 2 +- mythril/{version.py => __version__.py} | 0 mythril/interfaces/cli.py | 2 +- setup.py | 134 +++++++++++++++---------- 5 files changed, 85 insertions(+), 61 deletions(-) rename mythril/{version.py => __version__.py} (100%) diff --git a/docs/source/conf.py b/docs/source/conf.py index 36f41a8c..7169c4fa 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,14 +20,14 @@ sys.path.insert(0, os.path.abspath("../../")) # -- Project information ----------------------------------------------------- -project = "Mythril Classic" -copyright = "2018, Bernhard Mueller" -author = "Bernhard Mueller" +project = "Mythril" +copyright = "2019, ConsenSys Diligence" +author = "ConsenSys Dilligence" # The short X.Y version version = "" # The full version, including alpha/beta/rc tags -from mythril.version import VERSION +from mythril.__version__ import __version__ as VERSION release = VERSION diff --git a/mythril/__init__.py b/mythril/__init__.py index bfd74099..5f8e6963 100644 --- a/mythril/__init__.py +++ b/mythril/__init__.py @@ -3,4 +3,4 @@ __docformat__ = "restructuredtext" # Accept mythril.VERSION to get mythril's current version number -from .version import VERSION # NOQA +from .__version__ import __version__ as VERSION # NOQA diff --git a/mythril/version.py b/mythril/__version__.py similarity index 100% rename from mythril/version.py rename to mythril/__version__.py diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index ad2d79c7..e3725d38 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -22,7 +22,7 @@ from mythril.mythril import ( MythrilConfig, MythrilLevelDB, ) -from mythril.version import VERSION +from mythril.__version__ import __version__ as VERSION log = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index 21474308..b46a378f 100755 --- a/setup.py +++ b/setup.py @@ -9,21 +9,82 @@ publish to pypi w/o having to convert Readme.md to RST: """ from setuptools import setup, find_packages from setuptools.command.install import install -from pathlib import Path import sys import os +import io -# To make lint checkers happy we set VERSION here, but -# it is redefined by the exec below +# Package meta-data. +NAME = "mythril" +DESCRIPTION = "Security analysis tool for Ethereum smart contracts" +URL = "https://github.com/ConsenSys/mythril" +AUTHOR = "ConsenSys Dilligence" +AUTHOR_MAIL = None +REQUIRES_PYTHON = ">=3.5.0" + + +# What packages are required for this module to be executed? +REQUIRED = [ + "coloredlogs>=10.0", + "py_ecc==1.4.2", + "ethereum>=2.3.2", + "z3-solver>=4.8.5.0", + "requests", + "py-solc", + "plyvel", + "eth_abi==1.3.0", + "eth-utils>=1.0.1", + "eth-account>=0.1.0a2,<=0.3.0", + "eth-hash>=0.1.0", + "eth-keyfile>=0.5.1", + "eth-keys>=0.2.0b3", + "eth-rlp>=0.1.0", + "eth-tester==0.1.0b32", + "eth-typing>=2.0.0", + "coverage", + "jinja2>=2.9", + "rlp>=1.0.1", + "transaction>=2.2.1", + "py-flags", + "mock", + "configparser>=3.5.0", + "persistent>=4.2.0", + "ethereum-input-decoder>=0.2.2", + "matplotlib", +] + +TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"] + +# What packages are optional? +EXTRAS = { + # 'fancy feature': ['django'], +} + +# If version is set to None then it will be fetched from __version__.py VERSION = None -# Package version (vX.Y.Z). It must match git tag being used for CircleCI -# deployment; otherwise the build will failed. +here = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if 'README.md' is present in your MANIFEST.in file! +try: + with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f: + long_description = "\n" + f.read() +except FileNotFoundError: + long_description = DESCRIPTION -version_path = (Path(__file__).parent / "mythril" / "version.py").absolute() -exec(open(str(version_path), "r").read()) +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + project_slug = NAME.lower().replace("-", "_").replace(" ", "_") + with open(os.path.join(here, project_slug, "__version__.py")) as f: + exec(f.read(), about) +else: + about["__version__"] = VERSION + +# Package version (vX.Y.Z). It must match git tag being used for CircleCI +# deployment; otherwise the build will failed. class VerifyVersionCommand(install): """Custom command to verify that the git tag matches our version.""" @@ -40,25 +101,15 @@ class VerifyVersionCommand(install): sys.exit(info) -def read_file(fname): - """return file contents. - - :param fname: path relative to setup.py - :return: file contents - """ - with open(os.path.join(os.path.dirname(__file__), fname), "r") as fd: - return fd.read() - - setup( - name="mythril", - version=VERSION[1:], - description="Security analysis tool for Ethereum smart contracts", - long_description=read_file("README.md") if os.path.isfile("README.md") else "", + name=NAME, + version=about["__version__"][1:], + description=DESCRIPTION, + long_description=long_description, long_description_content_type="text/markdown", # requires twine and recent setuptools - url="https://github.com/b-mueller/mythril", - author="Bernhard Mueller", - author_email="bernhard.mueller11@gmail.com", + url=URL, + author=AUTHOR, + author_mail=AUTHOR_MAIL, license="MIT", classifiers=[ "Development Status :: 3 - Alpha", @@ -71,37 +122,10 @@ setup( ], keywords="hacking disassembler security ethereum", packages=find_packages(exclude=["contrib", "docs", "tests"]), - install_requires=[ - "coloredlogs>=10.0", - "py_ecc==1.4.2", - "ethereum>=2.3.2", - "z3-solver>=4.8.5.0", - "requests", - "py-solc", - "plyvel", - "eth_abi==1.3.0", - "eth-utils>=1.0.1", - "eth-account>=0.1.0a2,<=0.3.0", - "eth-hash>=0.1.0", - "eth-keyfile>=0.5.1", - "eth-keys>=0.2.0b3", - "eth-rlp>=0.1.0", - "eth-tester==0.1.0b32", - "eth-typing>=2.0.0", - "coverage", - "jinja2>=2.9", - "rlp>=1.0.1", - "transaction>=2.2.1", - "py-flags", - "mock", - "configparser>=3.5.0", - "persistent>=4.2.0", - "ethereum-input-decoder>=0.2.2", - "matplotlib", - ], - tests_require=["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"], - python_requires=">=3.5", - extras_require={}, + install_requires=REQUIRED, + tests_require=TESTS_REQUIRE, + python_requires=REQUIRES_PYTHON, + extras_require=EXTRAS, package_data={"mythril.analysis.templates": ["*"], "mythril.support.assets": ["*"]}, include_package_data=True, entry_points={"console_scripts": ["myth=mythril.interfaces.cli:main"]}, From 678ff395ec34e992afd07d223ca4f4ab1bf02116 Mon Sep 17 00:00:00 2001 From: Joran Honig Date: Fri, 31 May 2019 23:47:07 +0200 Subject: [PATCH 012/108] remove unused pipfile --- Pipfile | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 753481aa..00000000 --- a/Pipfile +++ /dev/null @@ -1,19 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -"e1839a8" = {path = ".", editable = true} - -[dev-packages] -pylint = "*" -yapf = "*" -pytest = "*" -pytest-mock = "*" -pytest-cov = "*" - -[requires] - -[pipenv] -allow_prereleases = true From 3ad9919527adf69f7203bdfc56698e48f106f74a Mon Sep 17 00:00:00 2001 From: Joran Honig Date: Sat, 1 Jun 2019 00:10:54 +0200 Subject: [PATCH 013/108] apply style rules --- mythril/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__init__.py b/mythril/__init__.py index 5f8e6963..063d3b9b 100644 --- a/mythril/__init__.py +++ b/mythril/__init__.py @@ -3,4 +3,4 @@ __docformat__ = "restructuredtext" # Accept mythril.VERSION to get mythril's current version number -from .__version__ import __version__ as VERSION # NOQA +from .__version__ import __version__ as VERSION # NOQA From 0d4023d629b14d4396d370c395a68fa87b59ddb2 Mon Sep 17 00:00:00 2001 From: Joran Honig Date: Sat, 1 Jun 2019 00:19:30 +0200 Subject: [PATCH 014/108] rename to __version__ --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 0e8364bd..47e8bec6 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -VERSION = "v0.20.8" # NOQA +__version__ = "v0.20.8" From 415b611c3cf906fad8fa918facc6fdf59645f37b Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 5 Jun 2019 22:12:36 +0530 Subject: [PATCH 015/108] Fix Dockerfile --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9843f72a..eb1c32b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,8 +39,6 @@ COPY . /opt/mythril RUN cd /opt/mythril \ && python setup.py install -RUN useradd -m mythril -USER mythril WORKDIR /home/mythril RUN ( [ ! -z "${SOLC}" ] && set -e && for ver in $SOLC; do python -m solc.install v${ver}; done ) || true From 8f3da6a07048b2d0a607fee575a813602978a0dd Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 00:19:48 +0200 Subject: [PATCH 016/108] Refactor to use StateAnnotation --- mythril/laser/ethereum/strategy/custom.py | 38 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index b84d0536..202897a2 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -1,7 +1,9 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.strategy.basic import BreadthFirstSearchStrategy +from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum import util -from typing import Dict +from typing import Dict, cast, List +from copy import copy import logging @@ -9,6 +11,18 @@ JUMPDEST_LIMIT = 4 log = logging.getLogger(__name__) +class JumpdestCountAnnotation(StateAnnotation): + """State annotation used when a path is chosen based on a predictable variable.""" + + def __init__(self) -> None: + self._jumpdest_count = {} # type: Dict[object, dict] + + def __copy__(self): + result = JumpdestCountAnnotation() + result.call_offsets = copy(self._jumpdest_count) + return result + + class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): """Implements a breadth first search strategy that prunes loops. JUMPI instructions are skipped after a jump destination has been @@ -31,21 +45,33 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): if opcode != "JUMPI": return state + annotations = cast( + List[JumpdestCountAnnotation], + list(state.get_annotations(JumpdestCountAnnotation)), + ) + + if len(annotations) == 0: + annotation = JumpdestCountAnnotation() + state.annotate(annotation) + else: + annotation = annotations[0] + transaction = state.current_transaction target = util.get_concrete_int(state.mstate.stack[-1]) if transaction in self._jumpdest_count: try: - if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: + if annotation._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: + log.info("JUMPDEST limit reached, skipping JUMPI") return self.work_list.pop(0) except KeyError: - self._jumpdest_count[transaction][target] = 0 + annotation._jumpdest_count[transaction][target] = 0 - self._jumpdest_count[transaction][target] += 1 + annotation._jumpdest_count[transaction][target] += 1 else: - self._jumpdest_count[transaction] = {} - self._jumpdest_count[transaction][target] = 0 + annotation._jumpdest_count[transaction] = {} + annotation._jumpdest_count[transaction][target] = 0 return state From ff767f7fd5ed8c7abf96c50f4c9c9c65d560fd9e Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 6 Jun 2019 13:35:08 +0530 Subject: [PATCH 017/108] Fix badge by passing token --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d057cfb..8e874045 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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) [![Read the Docs](https://readthedocs.org/projects/mythril/badge/?version=master)](https://mythril.readthedocs.io/en/master/) -![Master Build Status](https://img.shields.io/circleci/project/github/ConsenSys/mythril/master.svg) +![Master Build Status](https://img.shields.io/circleci/build/github/ConsenSys/mythril/master.svg?token=97124ecfaee54366859cae98b5dafc0714325f8b) [![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril) [![Pypi Installs](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril) [![DockerHub Pulls](https://img.shields.io/docker/pulls/mythril/myth.svg?label=DockerHub Pulls)](https://cloud.docker.com/u/mythril/repository/docker/mythril/myth) From d51d8fd8717c90a6216d81e6b271f27a47bec07c Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 6 Jun 2019 13:42:22 +0530 Subject: [PATCH 018/108] Change the build branch for flag --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e874045..8618cb5a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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) [![Read the Docs](https://readthedocs.org/projects/mythril/badge/?version=master)](https://mythril.readthedocs.io/en/master/) -![Master Build Status](https://img.shields.io/circleci/build/github/ConsenSys/mythril/master.svg?token=97124ecfaee54366859cae98b5dafc0714325f8b) +![Master Build Status](https://img.shields.io/circleci/build/github/ConsenSys/mythril.svg?token=97124ecfaee54366859cae98b5dafc0714325f8b) [![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril) [![Pypi Installs](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril) [![DockerHub Pulls](https://img.shields.io/docker/pulls/mythril/myth.svg?label=DockerHub Pulls)](https://cloud.docker.com/u/mythril/repository/docker/mythril/myth) From 4bea0111ebb9859ae2fda144e1997a3695082dcb Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 14:19:59 +0600 Subject: [PATCH 019/108] Run tool-based integration tests --- .circleci/config.yml | 57 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 46a67785..3b2bb35d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -78,44 +78,39 @@ jobs: # command: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./run-integration-tests.sh; fi # working_directory: /home - - run: - name: Call webhook - command: | - curl -I -X POST -H -d "https://circleci.com/api/v1/project/${ORGANIZATION}/${WEBHOOK_PROJECT}/tree/master?circle-token=${CIRCLE_TOKEN}" | head -n 1 | cut -d$' ' -f2 - integration_tests: docker: - - image: mythril/mythx-ci - working_directory: /home + - image: circleci/python:3.6.4 + working_directory: ~/project steps: - - checkout: - path: /home/mythril-classic + - checkout + - setup_remote_docker - run: - name: Builds `mythril-classic` - command: cd mythril-classic && python3 setup.py install + name: Clone Mythril + command: git clone https://github.com/Consensys/mythril - run: - name: Installs other MythX components - command: | - ./install-mythx-components.sh pythx edelweiss harvey-cli \ - harvey-tyro maestro maru maru-tyro mythril-api \ - mythril-tyro tyro + name: Clone Edelweiss + command: git clone --recurse-submodules https://$GIT_TOKEN@github.com/Consensys/Edelweiss.git - run: - background: true - name: Launches MythX platform - command: ./launch-mythx.sh + name: Update SWC-registry + working_directory: ~/project/Edelweiss + command: git submodule update --recursive --remote - run: - name: Waits for MythX to spin-up - command: sleep 15s + name: Build Edelweiss + command: | + docker build \ + --build-arg AWS_ACCESS_KEY_ID=$S3_AWS_ACCESS_KEY_ID \ + --build-arg AWS_SECRET_ACCESS_KEY=$S3_AWS_SECRET_ACCESS_KEY \ + --build-arg AWS_DEFAULT_REGION=us-east-1 --rm -t "edelweiss-mythril:latest" . -f Edelweiss/dockerfiles/mythril/Dockerfile - run: - name: Quick Edelweiss test - command: /home/run-edelweiss-test.sh CircleCI/latest.quick.csv 5 - - # TODO: Temporary disabled - # - run: - # name: Full Edelweiss test - # environment: - # MYTHX_API_FULL_MODE: true - # command: /home/run-edelweiss-test.sh CircleCI/latest.full.csv + name: Run Edelweiss + command: | + docker run --rm edelweiss-mythril:latest \ + --timeout 10 \ + --output-dir /opt/edelweiss \ + --plugin-dir /opt/mythril \ + --s3 \ + --circle-ci CircleCI/mythril.csv pypi_release: <<: *defaults @@ -172,6 +167,8 @@ workflows: only: - develop - master + # TODO: remove after merge. For checking PR only. + - improvement/tool-based-integration-tests tags: only: /v[0-9]+(\.[0-9]+)*/ requires: From 85c599d83265a0c8556b71b5a13ad54a0514d29c Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 6 Jun 2019 14:32:40 +0530 Subject: [PATCH 020/108] Add a check for analysis modules run (#1064) --- mythril/analysis/symbolic.py | 18 ++++++++++-------- mythril/mythril/mythril_analyzer.py | 2 ++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index cdcb9070..e84939bf 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -42,6 +42,7 @@ class SymExecWrapper: modules=(), compulsory_statespace=True, enable_iprof=False, + run_analysis_modules=True, ): """ @@ -90,14 +91,15 @@ class SymExecWrapper: plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) - self.laser.register_hooks( - hook_type="pre", - hook_dict=get_detection_module_hooks(modules, hook_type="pre"), - ) - self.laser.register_hooks( - hook_type="post", - hook_dict=get_detection_module_hooks(modules, hook_type="post"), - ) + if run_analysis_modules: + self.laser.register_hooks( + hook_type="pre", + hook_dict=get_detection_module_hooks(modules, hook_type="pre"), + ) + self.laser.register_hooks( + hook_type="post", + hook_dict=get_detection_module_hooks(modules, hook_type="post"), + ) if isinstance(contract, SolidityContract): self.laser.sym_exec( diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index 5d0596e2..d4d4cd5b 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -75,6 +75,7 @@ class MythrilAnalyzer: execution_timeout=self.execution_timeout, create_timeout=self.create_timeout, enable_iprof=self.enable_iprof, + run_analysis_modules=False, ) return get_serializable_statespace(sym) @@ -108,6 +109,7 @@ class MythrilAnalyzer: transaction_count=transaction_count, create_timeout=self.create_timeout, enable_iprof=self.enable_iprof, + run_analysis_modules=False, ) return generate_graph(sym, physics=enable_physics, phrackify=phrackify) From b4e684f244b726693b9a817271826a2f9a70ef23 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 15:28:29 +0600 Subject: [PATCH 021/108] Remove mythril clone step --- .circleci/config.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3b2bb35d..f645af41 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,9 +85,6 @@ jobs: steps: - checkout - setup_remote_docker - - run: - name: Clone Mythril - command: git clone https://github.com/Consensys/mythril - run: name: Clone Edelweiss command: git clone --recurse-submodules https://$GIT_TOKEN@github.com/Consensys/Edelweiss.git @@ -171,8 +168,8 @@ workflows: - improvement/tool-based-integration-tests tags: only: /v[0-9]+(\.[0-9]+)*/ - requires: - - test + # requires: + # - test - pypi_release: filters: branches: From 7a5a3cf38afd4b5b16b01d6e027b1bb363d35943 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 15:41:45 +0600 Subject: [PATCH 022/108] Change env name --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f645af41..08e9607c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,7 +87,7 @@ jobs: - setup_remote_docker - run: name: Clone Edelweiss - command: git clone --recurse-submodules https://$GIT_TOKEN@github.com/Consensys/Edelweiss.git + command: git clone --recurse-submodules https://$GITHUB_TOKEN@github.com/Consensys/Edelweiss.git - run: name: Update SWC-registry working_directory: ~/project/Edelweiss From b490009d28eb5302a805d9824482dd1a2e5903bb Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 16:56:56 +0600 Subject: [PATCH 023/108] Prevent installing unsupported version of eth-account --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3cabe805..148b9b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ configparser>=3.5.0 coverage py_ecc==1.4.2 eth_abi==1.3.0 -eth-account>=0.1.0a2,<=0.3.0 +eth-account>=0.1.0a2,<0.3.0 ethereum>=2.3.2 ethereum-input-decoder>=0.2.2 eth-hash>=0.1.0 From d2d380f420c33878f467184854f7fc1802363778 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 17:02:03 +0600 Subject: [PATCH 024/108] Prevent installing unsupported version of eth-account --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 148b9b7f..6812f634 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,12 +3,12 @@ configparser>=3.5.0 coverage py_ecc==1.4.2 eth_abi==1.3.0 -eth-account>=0.1.0a2,<0.3.0 +eth-account>=0.1.0a2,<=0.3.0 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 +eth-keys>=0.2.0b3<0.3.0 eth-rlp>=0.1.0 eth-tester==0.1.0b32 eth-typing>=2.0.0 From 2ed8084d8ce179e2225b6fa4283cda7555fa150d Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Thu, 6 Jun 2019 17:07:24 +0600 Subject: [PATCH 025/108] Update eth-keys version conditions --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6812f634..1b49350c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ 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<0.3.0 +eth-keys>=0.2.0b3,<0.3.0 eth-rlp>=0.1.0 eth-tester==0.1.0b32 eth-typing>=2.0.0 From 81fed3d61663e34df7d51f67d48bc8a2fa9b1af6 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 13:22:15 +0200 Subject: [PATCH 026/108] Clean up --- mythril/laser/ethereum/strategy/custom.py | 30 +++++++++-------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 202897a2..0b8bc7d8 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -7,7 +7,7 @@ from copy import copy import logging -JUMPDEST_LIMIT = 4 +JUMPDEST_LIMIT = 2 log = logging.getLogger(__name__) @@ -15,11 +15,11 @@ class JumpdestCountAnnotation(StateAnnotation): """State annotation used when a path is chosen based on a predictable variable.""" def __init__(self) -> None: - self._jumpdest_count = {} # type: Dict[object, dict] + self._jumpdest_count = {} # type: Dict[int, int] def __copy__(self): result = JumpdestCountAnnotation() - result.call_offsets = copy(self._jumpdest_count) + result._jumpdest_count = copy(self._jumpdest_count) return result @@ -31,7 +31,6 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): def __init__(self, work_list, max_depth) -> None: super().__init__(work_list, max_depth) - self._jumpdest_count = {} # type: Dict[object, dict] def get_strategic_global_state(self) -> GlobalState: """ @@ -56,22 +55,15 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): else: annotation = annotations[0] - transaction = state.current_transaction - target = util.get_concrete_int(state.mstate.stack[-1]) + target = int(util.get_concrete_int(state.mstate.stack[-1])) - if transaction in self._jumpdest_count: + try: + annotation._jumpdest_count[target] += 1 + except KeyError: + annotation._jumpdest_count[target] = 1 - try: - if annotation._jumpdest_count[transaction][target] == JUMPDEST_LIMIT: - log.info("JUMPDEST limit reached, skipping JUMPI") - return self.work_list.pop(0) - except KeyError: - annotation._jumpdest_count[transaction][target] = 0 - - annotation._jumpdest_count[transaction][target] += 1 - - else: - annotation._jumpdest_count[transaction] = {} - annotation._jumpdest_count[transaction][target] = 0 + if annotation._jumpdest_count[target] > JUMPDEST_LIMIT: + print("JUMPDEST limit reached, skipping JUMPI") + return self.work_list.pop(0) return state From 6372eecee384ed0e0a730a07296fb507cdf5e42f Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 13:24:21 +0200 Subject: [PATCH 027/108] Remove print statement --- mythril/laser/ethereum/strategy/custom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 0b8bc7d8..a2284fa9 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -7,7 +7,7 @@ from copy import copy import logging -JUMPDEST_LIMIT = 2 +JUMPDEST_LIMIT = 1 log = logging.getLogger(__name__) @@ -63,7 +63,7 @@ class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): annotation._jumpdest_count[target] = 1 if annotation._jumpdest_count[target] > JUMPDEST_LIMIT: - print("JUMPDEST limit reached, skipping JUMPI") + log.debug("JUMPDEST limit reached, skipping JUMPI") return self.work_list.pop(0) return state From 45e77166ccb9de7918026bcd09e144825b55b7bd Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 13:30:07 +0200 Subject: [PATCH 028/108] Set jumpest limit to 2 (aborts at 3rd iteration) --- mythril/laser/ethereum/strategy/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index a2284fa9..71c20613 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -7,7 +7,7 @@ from copy import copy import logging -JUMPDEST_LIMIT = 1 +JUMPDEST_LIMIT = 2 log = logging.getLogger(__name__) From c121372792f1427623f696b547f7f6342a45a8e3 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 16:56:42 +0200 Subject: [PATCH 029/108] Refactor integer module --- mythril/analysis/modules/integer.py | 115 +++++++++++++++++----------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 48556389..e2e5c64d 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -13,6 +13,7 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.util import get_concrete_int from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.analysis.modules.base import DetectionModule +from copy import copy from mythril.laser.smt import ( BVAddNoOverflow, @@ -27,8 +28,6 @@ from mythril.laser.smt import ( ) import logging - - log = logging.getLogger(__name__) DISABLE_EFFECT_CHECK = True @@ -49,11 +48,18 @@ class OverUnderflowStateAnnotation(StateAnnotation): """ State Annotation used if an overflow is both possible and used in the annotated path""" def __init__( - self, overflowing_state: GlobalState, operator: str, constraint: Bool + self ) -> None: - self.overflowing_state = overflowing_state - self.operator = operator - self.constraint = constraint + self.overflowing_state_annotations = [] # type: List[OverUnderflowAnnotation] + self.ostates_seen = [] # type: List[GlobalState] + + def __copy__(self): + new_annotation = OverUnderflowStateAnnotation() + + new_annotation.overflowing_state_annotations = copy(self.overflowing_state_annotations) + new_annotation.ostates_seen = copy(self.ostates_seen) + + return new_annotation class IntegerOverflowUnderflowModule(DetectionModule): @@ -90,12 +96,16 @@ class IntegerOverflowUnderflowModule(DetectionModule): :param state: Statespace to analyse :return: Found issues """ + address = _get_address_from_state(state) has_overflow = self._overflow_cache.get(address, False) has_underflow = self._underflow_cache.get(address, False) if has_overflow or has_underflow: return opcode = state.get_current_instruction()["opcode"] + + logging.info("Integer overflow module instruction: " + opcode) + funcs = { "ADD": [self._handle_add], "SUB": [self._handle_sub], @@ -121,11 +131,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): op0, op1 = self._get_args(state) c = Not(BVAddNoOverflow(op0, op1, False)) - # Check satisfiable - model = self._try_constraints(state.mstate.constraints, [c]) - if model is None: - return - annotation = OverUnderflowAnnotation(state, "addition", c) op0.annotate(annotation) @@ -133,11 +138,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): op0, op1 = self._get_args(state) c = Not(BVMulNoOverflow(op0, op1, False)) - # Check satisfiable - model = self._try_constraints(state.mstate.constraints, [c]) - if model is None: - return - annotation = OverUnderflowAnnotation(state, "multiplication", c) op0.annotate(annotation) @@ -145,11 +145,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): op0, op1 = self._get_args(state) c = Not(BVSubNoUnderflow(op0, op1, False)) - # Check satisfiable - model = self._try_constraints(state.mstate.constraints, [c]) - if model is None: - return - annotation = OverUnderflowAnnotation(state, "subtraction", c) op0.annotate(annotation) @@ -174,9 +169,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): ) else: constraint = op0.value ** op1.value >= 2 ** 256 - model = self._try_constraints(state.mstate.constraints, [constraint]) - if model is None: - return + annotation = OverUnderflowAnnotation(state, "exponentiation", constraint) op0.annotate(annotation) @@ -213,36 +206,54 @@ class IntegerOverflowUnderflowModule(DetectionModule): @staticmethod def _handle_sstore(state: GlobalState) -> None: + stack = state.mstate.stack value = stack[-2] + if not isinstance(value, Expression): return + + state_annotations = cast( + List[OverUnderflowStateAnnotation], + list(state.get_annotations(OverUnderflowStateAnnotation)), + ) + + if len(state_annotations) == 0: + state_annotation = OverUnderflowStateAnnotation() + state.annotate(state_annotation) + else: + state_annotation = state_annotations[0] + for annotation in value.annotations: - if not isinstance(annotation, OverUnderflowAnnotation): + if not isinstance(annotation, OverUnderflowAnnotation) or annotation.overflowing_state in state_annotation.ostates_seen: continue - state.annotate( - OverUnderflowStateAnnotation( - annotation.overflowing_state, - annotation.operator, - annotation.constraint, - ) - ) + + state_annotation.overflowing_state_annotations.append(annotation) + state_annotation.ostates_seen.append(annotation.overflowing_state) @staticmethod def _handle_jumpi(state): + stack = state.mstate.stack value = stack[-2] + state_annotations = cast( + List[OverUnderflowStateAnnotation], + list(state.get_annotations(OverUnderflowStateAnnotation)), + ) + + if len(state_annotations) == 0: + state_annotation = OverUnderflowStateAnnotation() + state.annotate(state_annotation) + else: + state_annotation = state_annotations[0] + for annotation in value.annotations: - if not isinstance(annotation, OverUnderflowAnnotation): + if not isinstance(annotation, OverUnderflowAnnotation) or annotation.overflowing_state in state_annotation.ostates_seen: continue - state.annotate( - OverUnderflowStateAnnotation( - annotation.overflowing_state, - annotation.operator, - annotation.constraint, - ) - ) + + state_annotation.overflowing_state_annotations.append(annotation) + state_annotation.ostates_seen.append(annotation.overflowing_state) @staticmethod def _handle_return(state: GlobalState) -> None: @@ -256,11 +267,14 @@ class IntegerOverflowUnderflowModule(DetectionModule): offset, length = get_concrete_int(stack[-1]), get_concrete_int(stack[-2]) except TypeError: return + + ''' TODO: Rewrite this for element in state.mstate.memory[offset : offset + length]: if not isinstance(element, Expression): continue for annotation in element.annotations: if isinstance(annotation, OverUnderflowAnnotation): + state.annotate( OverUnderflowStateAnnotation( annotation.overflowing_state, @@ -268,12 +282,23 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotation.constraint, ) ) + ''' def _handle_transaction_end(self, state: GlobalState) -> None: - for annotation in cast( + + state_annotations = cast( List[OverUnderflowStateAnnotation], - state.get_annotations(OverUnderflowStateAnnotation), - ): + list(state.get_annotations(OverUnderflowStateAnnotation)), + ) + + if len(state_annotations) == 0: + return + + annotations = state_annotations[0].overflowing_state_annotations + + logging.info("Number of potentially overflowing states: {}".format(len(annotations))) + + for annotation in annotations: ostate = annotation.overflowing_state address = _get_address_from_state(ostate) @@ -289,7 +314,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): continue try: - # This check can be disabled if the contraints are to difficult for z3 to solve + # This check can be disabled if the constraints are to difficult for z3 to solve # within any reasonable time. if DISABLE_EFFECT_CHECK: constraints = ostate.mstate.constraints + [annotation.constraint] @@ -326,7 +351,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): @staticmethod def _try_constraints(constraints, new_constraints): - """ Tries new constraints + """ Tries new constraints :return Model if satisfiable otherwise None """ try: From 9f17033ccdfc9aa98feec0b2e92ccb046a1d1441 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 17:05:54 +0200 Subject: [PATCH 030/108] Never forget black --- mythril/analysis/modules/integer.py | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index e2e5c64d..61ce2ff1 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -28,6 +28,7 @@ from mythril.laser.smt import ( ) import logging + log = logging.getLogger(__name__) DISABLE_EFFECT_CHECK = True @@ -47,16 +48,16 @@ class OverUnderflowAnnotation: class OverUnderflowStateAnnotation(StateAnnotation): """ State Annotation used if an overflow is both possible and used in the annotated path""" - def __init__( - self - ) -> None: - self.overflowing_state_annotations = [] # type: List[OverUnderflowAnnotation] - self.ostates_seen = [] # type: List[GlobalState] + def __init__(self) -> None: + self.overflowing_state_annotations = [] # type: List[OverUnderflowAnnotation] + self.ostates_seen = [] # type: List[GlobalState] def __copy__(self): new_annotation = OverUnderflowStateAnnotation() - new_annotation.overflowing_state_annotations = copy(self.overflowing_state_annotations) + new_annotation.overflowing_state_annotations = copy( + self.overflowing_state_annotations + ) new_annotation.ostates_seen = copy(self.ostates_seen) return new_annotation @@ -225,7 +226,10 @@ class IntegerOverflowUnderflowModule(DetectionModule): state_annotation = state_annotations[0] for annotation in value.annotations: - if not isinstance(annotation, OverUnderflowAnnotation) or annotation.overflowing_state in state_annotation.ostates_seen: + if ( + not isinstance(annotation, OverUnderflowAnnotation) + or annotation.overflowing_state in state_annotation.ostates_seen + ): continue state_annotation.overflowing_state_annotations.append(annotation) @@ -249,7 +253,10 @@ class IntegerOverflowUnderflowModule(DetectionModule): state_annotation = state_annotations[0] for annotation in value.annotations: - if not isinstance(annotation, OverUnderflowAnnotation) or annotation.overflowing_state in state_annotation.ostates_seen: + if ( + not isinstance(annotation, OverUnderflowAnnotation) + or annotation.overflowing_state in state_annotation.ostates_seen + ): continue state_annotation.overflowing_state_annotations.append(annotation) @@ -268,7 +275,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): except TypeError: return - ''' TODO: Rewrite this + """ TODO: Rewrite this for element in state.mstate.memory[offset : offset + length]: if not isinstance(element, Expression): continue @@ -282,7 +289,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotation.constraint, ) ) - ''' + """ def _handle_transaction_end(self, state: GlobalState) -> None: @@ -296,7 +303,9 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotations = state_annotations[0].overflowing_state_annotations - logging.info("Number of potentially overflowing states: {}".format(len(annotations))) + logging.info( + "Number of potentially overflowing states: {}".format(len(annotations)) + ) for annotation in annotations: From fadeaea98cffcac900662c70dd6b11a43584c31b Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 6 Jun 2019 20:52:37 +0530 Subject: [PATCH 031/108] Fix problem with requirements (#1066) --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3cabe805..1b49350c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ 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 +eth-keys>=0.2.0b3,<0.3.0 eth-rlp>=0.1.0 eth-tester==0.1.0b32 eth-typing>=2.0.0 diff --git a/setup.py b/setup.py index b46a378f..58931527 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRED = [ "eth-account>=0.1.0a2,<=0.3.0", "eth-hash>=0.1.0", "eth-keyfile>=0.5.1", - "eth-keys>=0.2.0b3", + "eth-keys>=0.2.0b3,<0.3.0", "eth-rlp>=0.1.0", "eth-tester==0.1.0b32", "eth-typing>=2.0.0", From 37d2fee4a8d5c372b448d5c58ac3bf15e7af0a64 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 18:40:57 +0200 Subject: [PATCH 032/108] Ad even more debugging --- mythril/analysis/modules/integer.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 61ce2ff1..4301f970 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -31,7 +31,7 @@ import logging log = logging.getLogger(__name__) -DISABLE_EFFECT_CHECK = True +DISABLE_EFFECT_CHECK = False class OverUnderflowAnnotation: @@ -105,7 +105,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): return opcode = state.get_current_instruction()["opcode"] - logging.info("Integer overflow module instruction: " + opcode) + # logging.info("Integer overflow module instruction: " + opcode) funcs = { "ADD": [self._handle_add], @@ -303,6 +303,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotations = state_annotations[0].overflowing_state_annotations + log.info("Transaction end with {}".format(state.get_current_instruction()['opcode'])) + logging.info( "Number of potentially overflowing states: {}".format(len(annotations)) ) @@ -330,10 +332,18 @@ class IntegerOverflowUnderflowModule(DetectionModule): else: constraints = state.mstate.constraints + [annotation.constraint] + if ostate.get_current_instruction()['address'] == 1587: + logging.info(constraints) + + log.info("Potential overflow: {} at {}".format(annotation.operator, ostate.get_current_instruction()['address'])) + transaction_sequence = solver.get_transaction_sequence( state, constraints ) + + log.info("SAT") except UnsatError: + log.info("UNSAT") continue _type = "Underflow" if annotation.operator == "subtraction" else "Overflow" From 01990f3298bc0173746c05ca5b1731e8dca95b09 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 18:54:21 +0200 Subject: [PATCH 033/108] Remove constraints dump --- mythril/analysis/modules/integer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 4301f970..92f40dff 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -332,9 +332,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): else: constraints = state.mstate.constraints + [annotation.constraint] - if ostate.get_current_instruction()['address'] == 1587: - logging.info(constraints) - log.info("Potential overflow: {} at {}".format(annotation.operator, ostate.get_current_instruction()['address'])) transaction_sequence = solver.get_transaction_sequence( From bb2487e8bfaa092fca9ec6a2813abe337ae32825 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 20:13:46 +0200 Subject: [PATCH 034/108] Remove overflow cache as it no longer speeds up the analysis --- mythril/analysis/modules/integer.py | 38 +++++++---------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 92f40dff..34c901b0 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -4,7 +4,7 @@ underflows.""" import json from math import log2, ceil -from typing import Dict, cast, List +from typing import cast, List from mythril.analysis import solver from mythril.analysis.report import Issue from mythril.analysis.swc_data import INTEGER_OVERFLOW_AND_UNDERFLOW @@ -79,8 +79,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): entrypoint="callback", pre_hooks=["ADD", "MUL", "EXP", "SUB", "SSTORE", "JUMPI", "STOP", "RETURN"], ) - self._overflow_cache = {} # type: Dict[int, bool] - self._underflow_cache = {} # type: Dict[int, bool] def reset_module(self): """ @@ -88,8 +86,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): :return: """ super().reset_module() - self._overflow_cache = {} - self._underflow_cache = {} def _execute(self, state: GlobalState) -> None: """Executes analysis module for integer underflow and integer overflow. @@ -98,15 +94,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): :return: Found issues """ - address = _get_address_from_state(state) - has_overflow = self._overflow_cache.get(address, False) - has_underflow = self._underflow_cache.get(address, False) - if has_overflow or has_underflow: - return opcode = state.get_current_instruction()["opcode"] - # logging.info("Integer overflow module instruction: " + opcode) - funcs = { "ADD": [self._handle_add], "SUB": [self._handle_sub], @@ -303,7 +292,9 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotations = state_annotations[0].overflowing_state_annotations - log.info("Transaction end with {}".format(state.get_current_instruction()['opcode'])) + log.info( + "Transaction end with {}".format(state.get_current_instruction()["opcode"]) + ) logging.info( "Number of potentially overflowing states: {}".format(len(annotations)) @@ -312,17 +303,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): for annotation in annotations: ostate = annotation.overflowing_state - address = _get_address_from_state(ostate) - - if annotation.operator == "subtraction" and self._underflow_cache.get( - address, False - ): - continue - - if annotation.operator != "subtraction" and self._overflow_cache.get( - address, False - ): - continue try: # This check can be disabled if the constraints are to difficult for z3 to solve @@ -332,7 +312,11 @@ class IntegerOverflowUnderflowModule(DetectionModule): else: constraints = state.mstate.constraints + [annotation.constraint] - log.info("Potential overflow: {} at {}".format(annotation.operator, ostate.get_current_instruction()['address'])) + log.info( + "Potential overflow: {} at {}".format( + annotation.operator, ostate.get_current_instruction()["address"] + ) + ) transaction_sequence = solver.get_transaction_sequence( state, constraints @@ -359,10 +343,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): issue.debug = json.dumps(transaction_sequence, indent=4) - if annotation.operator == "subtraction": - self._underflow_cache[address] = True - else: - self._overflow_cache[address] = True self._issues.append(issue) @staticmethod From 19ba9b895af563b153af13af37eb0439ab7a043a Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Thu, 6 Jun 2019 20:15:19 +0200 Subject: [PATCH 035/108] Remove unused helper function --- mythril/analysis/modules/integer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 34c901b0..95edfd6c 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -357,7 +357,3 @@ class IntegerOverflowUnderflowModule(DetectionModule): detector = IntegerOverflowUnderflowModule() - - -def _get_address_from_state(state): - return state.get_current_instruction()["address"] From 72066c8f82aa62ff6ad10335c2e3e628651760e4 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Fri, 7 Jun 2019 09:34:06 +0600 Subject: [PATCH 036/108] Increase timeout param --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 08e9607c..21551fb7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -103,7 +103,7 @@ jobs: name: Run Edelweiss command: | docker run --rm edelweiss-mythril:latest \ - --timeout 10 \ + --timeout 90 \ --output-dir /opt/edelweiss \ --plugin-dir /opt/mythril \ --s3 \ From bae7eac1cd9da57f28a01b135c661a75106316c7 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Fri, 7 Jun 2019 14:22:45 +0600 Subject: [PATCH 037/108] Remove branch from circle ci workflow --- .circleci/config.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 21551fb7..89cfd337 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -164,12 +164,10 @@ workflows: only: - develop - master - # TODO: remove after merge. For checking PR only. - - improvement/tool-based-integration-tests tags: only: /v[0-9]+(\.[0-9]+)*/ - # requires: - # - test + requires: + - test - pypi_release: filters: branches: From 68c85215eeb6751506532d1518d64bed5d8f55f2 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 13:41:08 +0200 Subject: [PATCH 038/108] Remove debugging, disable effects check --- mythril/analysis/modules/integer.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 95edfd6c..0bc58ecb 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -31,7 +31,7 @@ import logging log = logging.getLogger(__name__) -DISABLE_EFFECT_CHECK = False +DISABLE_EFFECT_CHECK = True class OverUnderflowAnnotation: @@ -292,14 +292,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): annotations = state_annotations[0].overflowing_state_annotations - log.info( - "Transaction end with {}".format(state.get_current_instruction()["opcode"]) - ) - - logging.info( - "Number of potentially overflowing states: {}".format(len(annotations)) - ) - for annotation in annotations: ostate = annotation.overflowing_state @@ -312,19 +304,11 @@ class IntegerOverflowUnderflowModule(DetectionModule): else: constraints = state.mstate.constraints + [annotation.constraint] - log.info( - "Potential overflow: {} at {}".format( - annotation.operator, ostate.get_current_instruction()["address"] - ) - ) - transaction_sequence = solver.get_transaction_sequence( state, constraints ) - log.info("SAT") except UnsatError: - log.info("UNSAT") continue _type = "Underflow" if annotation.operator == "subtraction" else "Overflow" From 770b964340378fb27531108f32262983a4bd19df Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 13:42:50 +0200 Subject: [PATCH 039/108] Remove annoying log.info in exceptions module --- mythril/analysis/modules/exceptions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mythril/analysis/modules/exceptions.py b/mythril/analysis/modules/exceptions.py index 387a1975..a6ad6f27 100644 --- a/mythril/analysis/modules/exceptions.py +++ b/mythril/analysis/modules/exceptions.py @@ -18,8 +18,6 @@ def _analyze_state(state) -> list: :param state: :return: """ - log.info("Exceptions module: found ASSERT_FAIL instruction") - log.debug("ASSERT_FAIL in function " + state.environment.active_function_name) try: From eaec4906bc3bdbbfb0cb484eed06f5854614a208 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 14:35:25 +0200 Subject: [PATCH 040/108] Re-add the cache --- mythril/analysis/modules/integer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 0bc58ecb..3bac3efb 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -80,12 +80,15 @@ class IntegerOverflowUnderflowModule(DetectionModule): pre_hooks=["ADD", "MUL", "EXP", "SUB", "SSTORE", "JUMPI", "STOP", "RETURN"], ) + self._overflow_cache = {} # type: Dict[int, bool] + def reset_module(self): """ Resets the module :return: """ super().reset_module() + self._overflow_cache = {} def _execute(self, state: GlobalState) -> None: """Executes analysis module for integer underflow and integer overflow. @@ -94,6 +97,10 @@ class IntegerOverflowUnderflowModule(DetectionModule): :return: Found issues """ + address = _get_address_from_state(state) + if self._overflow_cache.get(address, False): + return + opcode = state.get_current_instruction()["opcode"] funcs = { @@ -327,6 +334,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): issue.debug = json.dumps(transaction_sequence, indent=4) + address = _get_address_from_state(ostate) + self._overflow_cache[address] = True self._issues.append(issue) @staticmethod @@ -341,3 +350,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): detector = IntegerOverflowUnderflowModule() + + +def _get_address_from_state(state): + return state.get_current_instruction()["address"] From fada2c5b3808f3e5e6c4a42e8e39c5657c79e61b Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 15:59:53 +0200 Subject: [PATCH 041/108] Enable effects check --- mythril/analysis/modules/integer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 3bac3efb..4aa9b567 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -31,7 +31,7 @@ import logging log = logging.getLogger(__name__) -DISABLE_EFFECT_CHECK = True +DISABLE_EFFECT_CHECK = False class OverUnderflowAnnotation: From 41ce26631850f6fecf1558d62e1b875f0b17d813 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 16:07:46 +0200 Subject: [PATCH 042/108] Decouple analysis module & re-set limit to 4 --- mythril/analysis/modules/dos.py | 3 +-- mythril/laser/ethereum/strategy/custom.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 47e8db00..3427cd15 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -4,7 +4,6 @@ import logging from typing import Dict, cast, List from mythril.analysis.swc_data import DOS_WITH_BLOCK_GAS_LIMIT -from mythril.laser.ethereum.strategy.custom import JUMPDEST_LIMIT from mythril.analysis.report import Issue from mythril.analysis.modules.base import DetectionModule from mythril.laser.ethereum.state.global_state import GlobalState @@ -71,7 +70,7 @@ class DOS(DetectionModule): try: self._jumpdest_count[transaction][target] += 1 - if self._jumpdest_count[transaction][target] == JUMPDEST_LIMIT - 1: + if self._jumpdest_count[transaction][target] == 3: annotation = ( LoopAnnotation(address, target) diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py index 71c20613..9e89a552 100644 --- a/mythril/laser/ethereum/strategy/custom.py +++ b/mythril/laser/ethereum/strategy/custom.py @@ -7,7 +7,7 @@ from copy import copy import logging -JUMPDEST_LIMIT = 2 +JUMPDEST_LIMIT = 4 log = logging.getLogger(__name__) From ba2ed3c1be73321340ad8e0eec39d25a461ec0e2 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 7 Jun 2019 16:12:10 +0200 Subject: [PATCH 043/108] Add missing import (again?) --- mythril/analysis/modules/integer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 4aa9b567..81732b02 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -4,7 +4,7 @@ underflows.""" import json from math import log2, ceil -from typing import cast, List +from typing import cast, List, Dict from mythril.analysis import solver from mythril.analysis.report import Issue from mythril.analysis.swc_data import INTEGER_OVERFLOW_AND_UNDERFLOW From 780ea9ab9bbef13b534508e26ad899f97f93f7b5 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 9 Jun 2019 12:39:58 +0200 Subject: [PATCH 044/108] Black --- mythril/analysis/symbolic.py | 1 - mythril/interfaces/cli.py | 4 +- mythril/laser/ethereum/strategy/custom.py | 69 ------------------ .../strategy/extensions/bounded_loops.py | 72 +++++++++++++++++++ mythril/laser/ethereum/svm.py | 8 ++- 5 files changed, 81 insertions(+), 73 deletions(-) delete mode 100644 mythril/laser/ethereum/strategy/custom.py create mode 100644 mythril/laser/ethereum/strategy/extensions/bounded_loops.py diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index e5990a77..dac47f46 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -16,7 +16,6 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnWeightedRandomStrategy, BasicSearchStrategy, ) -from mythril.laser.ethereum.strategy.custom import BFSBoundedLoopsStrategy from mythril.laser.smt import symbol_factory, BitVec from typing import Union, List, Dict, Type from mythril.solidity.soliditycontract import EVMContract, SolidityContract diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 4b0ed347..e3725d38 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -200,8 +200,8 @@ def create_parser(parser: argparse.ArgumentParser) -> None: options.add_argument( "--strategy", - choices=["dfs", "bfs", "naive-random", "weighted-random", "bfs-bounded"], - default="bfs-bounded", + choices=["dfs", "bfs", "naive-random", "weighted-random"], + default="bfs", help="Symbolic execution strategy", ) options.add_argument( diff --git a/mythril/laser/ethereum/strategy/custom.py b/mythril/laser/ethereum/strategy/custom.py deleted file mode 100644 index 9e89a552..00000000 --- a/mythril/laser/ethereum/strategy/custom.py +++ /dev/null @@ -1,69 +0,0 @@ -from mythril.laser.ethereum.state.global_state import GlobalState -from mythril.laser.ethereum.strategy.basic import BreadthFirstSearchStrategy -from mythril.laser.ethereum.state.annotation import StateAnnotation -from mythril.laser.ethereum import util -from typing import Dict, cast, List -from copy import copy -import logging - - -JUMPDEST_LIMIT = 4 -log = logging.getLogger(__name__) - - -class JumpdestCountAnnotation(StateAnnotation): - """State annotation used when a path is chosen based on a predictable variable.""" - - def __init__(self) -> None: - self._jumpdest_count = {} # type: Dict[int, int] - - def __copy__(self): - result = JumpdestCountAnnotation() - result._jumpdest_count = copy(self._jumpdest_count) - return result - - -class BFSBoundedLoopsStrategy(BreadthFirstSearchStrategy): - """Implements a breadth first search strategy that prunes loops. - JUMPI instructions are skipped after a jump destination has been - targeted JUMPDEST_LIMIT times. - """ - - def __init__(self, work_list, max_depth) -> None: - super().__init__(work_list, max_depth) - - def get_strategic_global_state(self) -> GlobalState: - """ - :return: - """ - - state = self.work_list.pop(0) - - opcode = state.get_current_instruction()["opcode"] - - if opcode != "JUMPI": - return state - - annotations = cast( - List[JumpdestCountAnnotation], - list(state.get_annotations(JumpdestCountAnnotation)), - ) - - if len(annotations) == 0: - annotation = JumpdestCountAnnotation() - state.annotate(annotation) - else: - annotation = annotations[0] - - target = int(util.get_concrete_int(state.mstate.stack[-1])) - - try: - annotation._jumpdest_count[target] += 1 - except KeyError: - annotation._jumpdest_count[target] = 1 - - if annotation._jumpdest_count[target] > JUMPDEST_LIMIT: - log.debug("JUMPDEST limit reached, skipping JUMPI") - return self.work_list.pop(0) - - return state diff --git a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py new file mode 100644 index 00000000..81c59b13 --- /dev/null +++ b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py @@ -0,0 +1,72 @@ +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.strategy.basic import BasicSearchStrategy +from mythril.laser.ethereum.state.annotation import StateAnnotation +from mythril.laser.ethereum import util +from typing import Dict, cast, List +from copy import copy +import logging + + +JUMPDEST_LIMIT = 4 +log = logging.getLogger(__name__) + + +class JumpdestCountAnnotation(StateAnnotation): + """State annotation that counts the number of jumps per destination.""" + + def __init__(self) -> None: + self._jumpdest_count = {} # type: Dict[int, int] + + def __copy__(self): + result = JumpdestCountAnnotation() + result._jumpdest_count = copy(self._jumpdest_count) + return result + + +class BFSBoundedLoopsStrategy(BasicSearchStrategy): + """Adds loop pruning to the search strategy. + Ignores JUMPI instruction if the destination was targeted >JUMPDEST_LIMIT times. + """ + + def __init__(self, super_strategy: BasicSearchStrategy): + self.super_strategy = super_strategy + BasicSearchStrategy.__init__( + self, super_strategy.work_list, super_strategy.max_depth + ) + + def get_strategic_global_state(self) -> GlobalState: + """ + :return: + """ + + while 1: + + state = self.super_strategy.get_strategic_global_state() + opcode = state.get_current_instruction()["opcode"] + + if opcode != "JUMPI": + return state + + annotations = cast( + List[JumpdestCountAnnotation], + list(state.get_annotations(JumpdestCountAnnotation)), + ) + + if len(annotations) == 0: + annotation = JumpdestCountAnnotation() + state.annotate(annotation) + else: + annotation = annotations[0] + + target = int(util.get_concrete_int(state.mstate.stack[-1])) + + try: + annotation._jumpdest_count[target] += 1 + except KeyError: + annotation._jumpdest_count[target] = 1 + + if annotation._jumpdest_count[target] > JUMPDEST_LIMIT: + log.debug("JUMPDEST limit reached, skipping JUMPI") + continue + + return state diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 8f74410a..51536a3f 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -13,7 +13,10 @@ from mythril.laser.ethereum.iprof import InstructionProfiler from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState -from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy +from mythril.laser.ethereum.strategy.basic import ( + BasicSearchStrategy, + DepthFirstSearchStrategy, +) from mythril.laser.ethereum.time_handler import time_handler from mythril.laser.ethereum.transaction import ( ContractCreationTransaction, @@ -102,6 +105,9 @@ class LaserEVM: log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader)) + def extend_strategy(self, strategy: BasicSearchStrategy) -> None: + self.strategy = strategy(self.strategy) + def sym_exec( self, world_state: WorldState = None, From 6292ccd2163d8a8fdd29b352cf7464b4295f5b7f Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 9 Jun 2019 12:53:34 +0200 Subject: [PATCH 045/108] Add 'loop-bound' CLI argument' --- mythril/interfaces/cli.py | 8 +++++++- .../laser/ethereum/strategy/extensions/bounded_loops.py | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index e3725d38..5bdf0ea9 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -197,13 +197,19 @@ def create_parser(parser: argparse.ArgumentParser) -> None: default=50, help="Maximum recursion depth for symbolic execution", ) - options.add_argument( "--strategy", choices=["dfs", "bfs", "naive-random", "weighted-random"], default="bfs", help="Symbolic execution strategy", ) + options.add_argument( + "-b", + "--loop-bound", + type=int, + default=4, + help="Bound loops at n iterations", + ) options.add_argument( "-t", "--transaction-count", diff --git a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py index 81c59b13..d92e0e92 100644 --- a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py +++ b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py @@ -7,7 +7,6 @@ from copy import copy import logging -JUMPDEST_LIMIT = 4 log = logging.getLogger(__name__) @@ -28,8 +27,10 @@ class BFSBoundedLoopsStrategy(BasicSearchStrategy): Ignores JUMPI instruction if the destination was targeted >JUMPDEST_LIMIT times. """ - def __init__(self, super_strategy: BasicSearchStrategy): + def __init__(self, super_strategy: BasicSearchStrategy, loop_bound: int): self.super_strategy = super_strategy + self.jumpdest_limit = loop_bound + BasicSearchStrategy.__init__( self, super_strategy.work_list, super_strategy.max_depth ) @@ -65,7 +66,7 @@ class BFSBoundedLoopsStrategy(BasicSearchStrategy): except KeyError: annotation._jumpdest_count[target] = 1 - if annotation._jumpdest_count[target] > JUMPDEST_LIMIT: + if annotation._jumpdest_count[target] > self.jumpdest_limit: log.debug("JUMPDEST limit reached, skipping JUMPI") continue From b0828e17fa7eb2992999965a6cc832a23a770486 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 9 Jun 2019 13:25:02 +0200 Subject: [PATCH 046/108] Pass though loop_count arg --- mythril/analysis/symbolic.py | 9 +++++++-- mythril/interfaces/cli.py | 2 ++ .../ethereum/strategy/extensions/bounded_loops.py | 14 +++++++++++--- mythril/laser/ethereum/svm.py | 4 ++-- mythril/mythril/mythril_analyzer.py | 4 ++++ 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index dac47f46..55ebf39d 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -16,6 +16,9 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnWeightedRandomStrategy, BasicSearchStrategy, ) +from mythril.laser.ethereum.strategy.extensions.bounded_loops import ( + BoundedLoopsStrategy, +) from mythril.laser.smt import symbol_factory, BitVec from typing import Union, List, Dict, Type from mythril.solidity.soliditycontract import EVMContract, SolidityContract @@ -37,6 +40,7 @@ class SymExecWrapper: dynloader=None, max_depth=22, execution_timeout=None, + loop_bound=4, create_timeout=None, transaction_count=2, modules=(), @@ -69,8 +73,6 @@ class SymExecWrapper: s_strategy = ReturnRandomNaivelyStrategy elif strategy == "weighted-random": s_strategy = ReturnWeightedRandomStrategy - elif strategy == "bfs-bounded": - s_strategy = BFSBoundedLoopsStrategy else: raise ValueError("Invalid strategy argument supplied") @@ -89,6 +91,9 @@ class SymExecWrapper: enable_iprof=enable_iprof, ) + if loop_bound is not None: + self.laser.extend_strategy(BoundedLoopsStrategy, loop_bound) + plugin_loader = LaserPluginLoader(self.laser) plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 5bdf0ea9..2d3a63e1 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -209,6 +209,7 @@ def create_parser(parser: argparse.ArgumentParser) -> None: type=int, default=4, help="Bound loops at n iterations", + metavar="N", ) options.add_argument( "-t", @@ -413,6 +414,7 @@ def execute_command( address=address, max_depth=args.max_depth, execution_timeout=args.execution_timeout, + loop_bound=args.loop_bound, create_timeout=args.create_timeout, enable_iprof=args.enable_iprof, onchain_storage_access=not args.no_onchain_storage_access, diff --git a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py index d92e0e92..10525ffa 100644 --- a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py +++ b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py @@ -22,14 +22,22 @@ class JumpdestCountAnnotation(StateAnnotation): return result -class BFSBoundedLoopsStrategy(BasicSearchStrategy): +class BoundedLoopsStrategy(BasicSearchStrategy): """Adds loop pruning to the search strategy. Ignores JUMPI instruction if the destination was targeted >JUMPDEST_LIMIT times. """ - def __init__(self, super_strategy: BasicSearchStrategy, loop_bound: int): + def __init__(self, super_strategy: BasicSearchStrategy, *args) -> None: + """""" + self.super_strategy = super_strategy - self.jumpdest_limit = loop_bound + self.jumpdest_limit = args[0][0] + + log.info( + "Loaded search strategy extension: Loop bounds (limit = {})".format( + self.jumpdest_limit + ) + ) BasicSearchStrategy.__init__( self, super_strategy.work_list, super_strategy.max_depth diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index 51536a3f..b7086497 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -105,8 +105,8 @@ class LaserEVM: log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader)) - def extend_strategy(self, strategy: BasicSearchStrategy) -> None: - self.strategy = strategy(self.strategy) + def extend_strategy(self, extension: BasicSearchStrategy, *args) -> None: + self.strategy = extension(self.strategy, args) def sym_exec( self, diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index d4d4cd5b..9433278e 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -35,6 +35,7 @@ class MythrilAnalyzer: address: Optional[str] = None, max_depth: Optional[int] = None, execution_timeout: Optional[int] = None, + loop_bound: Optional[int] = None, create_timeout: Optional[int] = None, enable_iprof: bool = False, ): @@ -53,6 +54,7 @@ class MythrilAnalyzer: self.address = address self.max_depth = max_depth self.execution_timeout = execution_timeout + self.loop_bound = loop_bound self.create_timeout = create_timeout self.enable_iprof = enable_iprof @@ -142,12 +144,14 @@ class MythrilAnalyzer: ), max_depth=self.max_depth, execution_timeout=self.execution_timeout, + loop_bound=self.loop_bound, create_timeout=self.create_timeout, transaction_count=transaction_count, modules=modules, compulsory_statespace=False, enable_iprof=self.enable_iprof, ) + issues = fire_lasers(sym, modules) except KeyboardInterrupt: log.critical("Keyboard Interrupt") From f27f1acb9e14da39cf46b2aceae793adf7dd2723 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sun, 9 Jun 2019 14:01:43 +0200 Subject: [PATCH 047/108] Try fix for CircleCI failure --- mythril/laser/ethereum/svm.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index b7086497..a13cf5b7 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -13,10 +13,8 @@ from mythril.laser.ethereum.iprof import InstructionProfiler from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState -from mythril.laser.ethereum.strategy.basic import ( - BasicSearchStrategy, - DepthFirstSearchStrategy, -) +from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy +from abc import ABCMeta from mythril.laser.ethereum.time_handler import time_handler from mythril.laser.ethereum.transaction import ( ContractCreationTransaction, @@ -105,7 +103,7 @@ class LaserEVM: log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader)) - def extend_strategy(self, extension: BasicSearchStrategy, *args) -> None: + def extend_strategy(self, extension: ABCMeta, *args) -> None: self.strategy = extension(self.strategy, args) def sym_exec( From d554a5ae07f9cfaf2e70bb5cfb2ccd3ccdff4852 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 10 Jun 2019 12:01:31 +0200 Subject: [PATCH 048/108] Optimization: Cache satisifability of overflowing states --- mythril/analysis/modules/integer.py | 53 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 81732b02..2c977752 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -31,8 +31,6 @@ import logging log = logging.getLogger(__name__) -DISABLE_EFFECT_CHECK = False - class OverUnderflowAnnotation: """ Symbol Annotation used if a BitVector can overflow""" @@ -80,8 +78,19 @@ class IntegerOverflowUnderflowModule(DetectionModule): pre_hooks=["ADD", "MUL", "EXP", "SUB", "SSTORE", "JUMPI", "STOP", "RETURN"], ) + """ + Cache addresses for which overflows already have been detected. + """ + self._overflow_cache = {} # type: Dict[int, bool] + """ + Cache satisfiability of overflow constraints + """ + + self._ostates_satisfiable = [] # type: List[GlobalState] + self._ostates_unsatisfiable = [] # type: List[GlobalState] + def reset_module(self): """ Resets the module @@ -98,6 +107,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): """ address = _get_address_from_state(state) + if self._overflow_cache.get(address, False): return @@ -303,18 +313,33 @@ class IntegerOverflowUnderflowModule(DetectionModule): ostate = annotation.overflowing_state - try: - # This check can be disabled if the constraints are to difficult for z3 to solve - # within any reasonable time. - if DISABLE_EFFECT_CHECK: + if ostate in self._ostates_unsatisfiable: + continue + + if ostate not in self._ostates_satisfiable: + try: constraints = ostate.mstate.constraints + [annotation.constraint] - else: - constraints = state.mstate.constraints + [annotation.constraint] + solver.get_model(constraints) + self._ostates_satisfiable.append(ostate) + except: + self._ostates_unsatisfiable.append(ostate) + continue + + log.debug( + "Checking overflow in {} at transaction end address {}, ostate address {}".format( + state.get_current_instruction()["opcode"], + state.get_current_instruction()["address"], + ostate.get_current_instruction()["address"], + ) + ) + + try: + + constraints = state.mstate.constraints + [annotation.constraint] transaction_sequence = solver.get_transaction_sequence( state, constraints ) - except UnsatError: continue @@ -338,16 +363,6 @@ class IntegerOverflowUnderflowModule(DetectionModule): self._overflow_cache[address] = True self._issues.append(issue) - @staticmethod - def _try_constraints(constraints, new_constraints): - """ Tries new constraints - :return Model if satisfiable otherwise None - """ - try: - return solver.get_model(constraints + new_constraints) - except UnsatError: - return None - detector = IntegerOverflowUnderflowModule() From 5e9498b39dc2c17cf3780e7bcc417a0636ac3b48 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 10 Jun 2019 12:02:22 +0200 Subject: [PATCH 049/108] Reset all caches in reset_module --- mythril/analysis/modules/integer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 2c977752..ac825904 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -98,6 +98,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): """ super().reset_module() self._overflow_cache = {} + self._ostates_satisfiable = [] + self._ostates_unsatisfiable = [] def _execute(self, state: GlobalState) -> None: """Executes analysis module for integer underflow and integer overflow. From c4c932e1aa49815bf8d5a5d4e97ac66ba9cb6726 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 10 Jun 2019 20:28:17 +0530 Subject: [PATCH 050/108] Mythril v0.20.9 --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 47e8bec6..0917f2ac 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.20.8" +__version__ = "v0.20.9" From 4d16e8f7fb986e5ec75850fb50878edca7ced3af Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Tue, 11 Jun 2019 15:20:27 +0530 Subject: [PATCH 051/108] Fix suicide module by constraining to attacker (#1071) --- mythril/analysis/modules/suicide.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index abe38ac9..57c593e2 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -4,6 +4,7 @@ from mythril.analysis.swc_data import UNPROTECTED_SELFDESTRUCT from mythril.exceptions import UnsatError from mythril.analysis.modules.base import DetectionModule from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS import logging import json @@ -57,13 +58,16 @@ class SuicideModule(DetectionModule): ) description_head = "The contract can be killed by anyone." - + caller = state.current_transaction.caller try: try: transaction_sequence = solver.get_transaction_sequence( state, state.mstate.constraints - + [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF], + + [ + to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, + caller == ATTACKER_ADDRESS, + ], ) description_tail = ( "Anyone can kill this contract and withdraw its balance to an arbitrary " @@ -71,7 +75,7 @@ class SuicideModule(DetectionModule): ) except UnsatError: transaction_sequence = solver.get_transaction_sequence( - state, state.mstate.constraints + state, state.mstate.constraints + [caller == ATTACKER_ADDRESS] ) description_tail = "Arbitrary senders can kill this contract." From 1204e80863e67ac1eebf41dbebc41f7fc0c5cb00 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 12 Jun 2019 11:32:30 +0530 Subject: [PATCH 052/108] Change mythril-classic to Mythril --- docs/source/about.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/about.rst b/docs/source/about.rst index edfdc308..169fe4f7 100644 --- a/docs/source/about.rst +++ b/docs/source/about.rst @@ -1,6 +1,6 @@ -What is Mythril Classic? +What is Mythril? ======================== -Mythril Classic is a security analysis tool for Ethereum smart contracts. It was `introduced at HITBSecConf 2018 `_. +Mythril is a security analysis tool for Ethereum smart contracts. It was `introduced at HITBSecConf 2018 `_. -Mythril Classic detects a range of security issues, including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. Note that Mythril is targeted at finding common vulnerabilities, and is not able to discover issues in the business logic of an application. Furthermore, Mythril and symbolic executors are generally unsound, as they are often unable to explore all possible states of a program. +Mythril detects a range of security issues, including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. Note that Mythril is targeted at finding common vulnerabilities, and is not able to discover issues in the business logic of an application. Furthermore, Mythril and symbolic executors are generally unsound, as they are often unable to explore all possible states of a program. From 5737b8db5239373fadf6235c92f90f8f4c228b3d Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 12 Jun 2019 11:33:22 +0530 Subject: [PATCH 053/108] Change mythril-classic to Mythril --- docs/source/analysis-modules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/analysis-modules.rst b/docs/source/analysis-modules.rst index e6e33790..d1751547 100644 --- a/docs/source/analysis-modules.rst +++ b/docs/source/analysis-modules.rst @@ -1,7 +1,7 @@ Analysis Modules ================ -Mythril Classic's detection capabilities are written in modules in the `/analysis/modules `_ directory. +Mythril's detection capabilities are written in modules in the `/analysis/modules `_ directory. .. toctree:: From 93f882123f923dfac186ca15f7cc23b80f0701b8 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Wed, 12 Jun 2019 12:06:04 +0530 Subject: [PATCH 054/108] fix docs (#1072) * Remove classic in mythril-classic * Reformat with black --- .circleci/config.yml | 6 +++--- docs/source/conf.py | 20 ++++++-------------- docs/source/create-module.rst | 2 +- docs/source/index.rst | 2 +- docs/source/module-list.rst | 22 +++++++++++----------- docs/source/security-analysis.rst | 2 +- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 46a67785..42d12a89 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,10 +89,10 @@ jobs: working_directory: /home steps: - checkout: - path: /home/mythril-classic + path: /home/mythril - run: - name: Builds `mythril-classic` - command: cd mythril-classic && python3 setup.py install + name: Builds `mythril` + command: cd mythril && python3 setup.py install - run: name: Installs other MythX components command: | diff --git a/docs/source/conf.py b/docs/source/conf.py index 7169c4fa..1c06fbc4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -108,7 +108,7 @@ html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = "MythrilClassicdoc" +htmlhelp_basename = "Mythrildoc" # -- Options for LaTeX output ------------------------------------------------ @@ -132,13 +132,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ( - master_doc, - "MythrilClassic.tex", - "Mythril Classic Documentation", - "Bernhard Mueller", - "manual", - ) + (master_doc, "Mythril.tex", "Mythril Documentation", "Bernhard Mueller", "manual") ] @@ -146,9 +140,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "mythrilclassic", "Mythril Classic Documentation", [author], 1) -] +man_pages = [(master_doc, "mythril", "Mythril Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -159,10 +151,10 @@ man_pages = [ texinfo_documents = [ ( master_doc, - "MythrilClassic", - "Mythril Classic Documentation", + "Mythril", + "Mythril Documentation", author, - "MythrilClassic", + "Mythril", "One line description of project.", "Miscellaneous", ) diff --git a/docs/source/create-module.rst b/docs/source/create-module.rst index 84aa2cc4..7f6ef505 100644 --- a/docs/source/create-module.rst +++ b/docs/source/create-module.rst @@ -1,4 +1,4 @@ Creating a Module ================= -Create a module in the :code:`analysis/modules` directory, and create an instance of a class that inherits :code:`DetectionModule` named :code:`detector`. Take a look at the `suicide module `_ as an example. +Create a module in the :code:`analysis/modules` directory, and create an instance of a class that inherits :code:`DetectionModule` named :code:`detector`. Take a look at the `suicide module `_ as an example. diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bbabc49..e3f9df42 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,4 +1,4 @@ -Welcome to Mythril Classic's documentation! +Welcome to Mythril's documentation! =========================================== .. toctree:: diff --git a/docs/source/module-list.rst b/docs/source/module-list.rst index 89a5cb05..11c40d02 100644 --- a/docs/source/module-list.rst +++ b/docs/source/module-list.rst @@ -5,65 +5,65 @@ Modules Delegate Call To Untrusted Contract *********************************** -The `delegatecall module `_ detects `SWC-112 (DELEGATECALL to Untrusted Callee) `_. +The `delegatecall module `_ detects `SWC-112 (DELEGATECALL to Untrusted Callee) `_. *********************************** Dependence on Predictable Variables *********************************** -The `predictable variables module `_ detects `SWC-120 (Weak Randomness) `_ and `SWC-116 (Timestamp Dependence) `_. +The `predictable variables module `_ detects `SWC-120 (Weak Randomness) `_ and `SWC-116 (Timestamp Dependence) `_. ****************** Deprecated Opcodes ****************** -The `deprecated opcodes module `_ detects `SWC-111 (Use of Deprecated Functions) `_. +The `deprecated opcodes module `_ detects `SWC-111 (Use of Deprecated Functions) `_. *********** Ether Thief *********** -The `Ether Thief module `_ detects `SWC-105 (Unprotected Ether Withdrawal) `_. +The `Ether Thief module `_ detects `SWC-105 (Unprotected Ether Withdrawal) `_. ********** Exceptions ********** -The `exceptions module `_ detects `SWC-110 (Assert Violation) `_. +The `exceptions module `_ detects `SWC-110 (Assert Violation) `_. ************** External Calls ************** -The `external calls module `_ warns about `SWC-117 (Reentrancy) `_ by detecting calls to external contracts. +The `external calls module `_ warns about `SWC-117 (Reentrancy) `_ by detecting calls to external contracts. ******* Integer ******* -The `integer module `_ detects `SWC-101 (Integer Overflow and Underflow) `_. +The `integer module `_ detects `SWC-101 (Integer Overflow and Underflow) `_. ************** Multiple Sends ************** -The `multiple sends module `_ detects `SWC-113 (Denial of Service with Failed Call) `_ by checking for multiple calls or sends in a single transaction. +The `multiple sends module `_ detects `SWC-113 (Denial of Service with Failed Call) `_ by checking for multiple calls or sends in a single transaction. ******* Suicide ******* -The `suicide module `_ detects `SWC-106 (Unprotected SELFDESTRUCT) `_. +The `suicide module `_ detects `SWC-106 (Unprotected SELFDESTRUCT) `_. **************************** State Change External Calls **************************** -The `state change external calls module `_ detects `SWC-107 (Reentrancy) `_ by detecting state change after calls to an external contract. +The `state change external calls module `_ detects `SWC-107 (Reentrancy) `_ by detecting state change after calls to an external contract. **************** Unchecked Retval **************** -The `unchecked retval module `_ detects `SWC-104 (Unchecked Call Return Value) `_. +The `unchecked retval module `_ detects `SWC-104 (Unchecked Call Return Value) `_. diff --git a/docs/source/security-analysis.rst b/docs/source/security-analysis.rst index afe348a4..4cd1010e 100644 --- a/docs/source/security-analysis.rst +++ b/docs/source/security-analysis.rst @@ -1,7 +1,7 @@ Security Analysis ================= -Run :code:`myth -x` with one of the input options described below will run the analysis modules in the `/analysis/modules `_ directory. +Run :code:`myth -x` with one of the input options described below will run the analysis modules in the `/analysis/modules `_ directory. *********************** Analyzing Solidity Code From 294105f924849cfcdfe7fd3f71e0ff618196739f Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 09:15:12 +0200 Subject: [PATCH 055/108] Catch possible TypeError --- mythril/analysis/modules/dos.py | 2 +- mythril/laser/ethereum/strategy/extensions/bounded_loops.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mythril/analysis/modules/dos.py b/mythril/analysis/modules/dos.py index 3427cd15..3134363c 100644 --- a/mythril/analysis/modules/dos.py +++ b/mythril/analysis/modules/dos.py @@ -44,8 +44,8 @@ class DOS(DetectionModule): self._jumpdest_count = {} # type: Dict[object, dict] def _execute(self, state: GlobalState) -> None: - """ + """ :param state: :return: """ diff --git a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py index 10525ffa..064f8827 100644 --- a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py +++ b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py @@ -67,7 +67,10 @@ class BoundedLoopsStrategy(BasicSearchStrategy): else: annotation = annotations[0] - target = int(util.get_concrete_int(state.mstate.stack[-1])) + try: + target = util.get_concrete_int(state.mstate.stack[-1]) + except TypeError: + return state try: annotation._jumpdest_count[target] += 1 From f9788dcaf7c9a75eefa8f0ca71671f375d8f38ef Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 09:26:09 +0200 Subject: [PATCH 056/108] Norhh suggestions --- mythril/laser/ethereum/strategy/extensions/bounded_loops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py index 064f8827..48f77383 100644 --- a/mythril/laser/ethereum/strategy/extensions/bounded_loops.py +++ b/mythril/laser/ethereum/strategy/extensions/bounded_loops.py @@ -48,7 +48,7 @@ class BoundedLoopsStrategy(BasicSearchStrategy): :return: """ - while 1: + while True: state = self.super_strategy.get_strategic_global_state() opcode = state.get_current_instruction()["opcode"] From 1b73ddec1293f1d1e2f22396bf189b187f61b3a9 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 09:53:59 +0200 Subject: [PATCH 057/108] Norhh suggestion + some refactoring --- mythril/analysis/modules/integer.py | 64 +++++++++++------------------ 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index ac825904..13d2eaa3 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -222,16 +222,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): if not isinstance(value, Expression): return - state_annotations = cast( - List[OverUnderflowStateAnnotation], - list(state.get_annotations(OverUnderflowStateAnnotation)), - ) - - if len(state_annotations) == 0: - state_annotation = OverUnderflowStateAnnotation() - state.annotate(state_annotation) - else: - state_annotation = state_annotations[0] + state_annotation = _get_overflowunderflow_state_annotation(state) for annotation in value.annotations: if ( @@ -249,16 +240,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): stack = state.mstate.stack value = stack[-2] - state_annotations = cast( - List[OverUnderflowStateAnnotation], - list(state.get_annotations(OverUnderflowStateAnnotation)), - ) - - if len(state_annotations) == 0: - state_annotation = OverUnderflowStateAnnotation() - state.annotate(state_annotation) - else: - state_annotation = state_annotations[0] + state_annotation = _get_overflowunderflow_state_annotation(state) for annotation in value.annotations: if ( @@ -283,35 +265,21 @@ class IntegerOverflowUnderflowModule(DetectionModule): except TypeError: return - """ TODO: Rewrite this for element in state.mstate.memory[offset : offset + length]: if not isinstance(element, Expression): continue + + state_annotation = _get_overflowunderflow_state_annotation(state) + for annotation in element.annotations: if isinstance(annotation, OverUnderflowAnnotation): - - state.annotate( - OverUnderflowStateAnnotation( - annotation.overflowing_state, - annotation.operator, - annotation.constraint, - ) - ) - """ + state_annotation.overflowing_state_annotations.append(annotation) def _handle_transaction_end(self, state: GlobalState) -> None: - state_annotations = cast( - List[OverUnderflowStateAnnotation], - list(state.get_annotations(OverUnderflowStateAnnotation)), - ) - - if len(state_annotations) == 0: - return + state_annotation = _get_overflowunderflow_state_annotation(state) - annotations = state_annotations[0].overflowing_state_annotations - - for annotation in annotations: + for annotation in state_annotation.overflowing_state_annotations: ostate = annotation.overflowing_state @@ -371,3 +339,19 @@ detector = IntegerOverflowUnderflowModule() def _get_address_from_state(state): return state.get_current_instruction()["address"] + + +def _get_overflowunderflow_state_annotation( + state: GlobalState +) -> OverUnderflowStateAnnotation: + state_annotations = cast( + List[OverUnderflowStateAnnotation], + list(state.get_annotations(OverUnderflowStateAnnotation)), + ) + + if len(state_annotations) == 0: + state_annotation = OverUnderflowStateAnnotation() + state.annotate(state_annotation) + return state_annotation + else: + return state_annotations[0] From 58cf0c6efc5d72f2061e705b571bfa1c89dad18e Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 10:00:51 +0200 Subject: [PATCH 058/108] Comment out the handle_return code again --- mythril/analysis/modules/integer.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 13d2eaa3..46b5b70a 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -258,23 +258,27 @@ class IntegerOverflowUnderflowModule(DetectionModule): Adds all the annotations into the state which correspond to the locations in the memory returned by RETURN opcode. :param state: The Global State - """ + + stack = state.mstate.stack try: offset, length = get_concrete_int(stack[-1]), get_concrete_int(stack[-2]) except TypeError: return + state_annotation = _get_overflowunderflow_state_annotation(state) + for element in state.mstate.memory[offset : offset + length]: + if not isinstance(element, Expression): continue - state_annotation = _get_overflowunderflow_state_annotation(state) - for annotation in element.annotations: if isinstance(annotation, OverUnderflowAnnotation): state_annotation.overflowing_state_annotations.append(annotation) + """ + def _handle_transaction_end(self, state: GlobalState) -> None: state_annotation = _get_overflowunderflow_state_annotation(state) From a15d38c8b2790ef682f3df76a72bda4de892c6ea Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 10:14:04 +0200 Subject: [PATCH 059/108] Fix processing of return, change lists to sets --- mythril/analysis/modules/integer.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 46b5b70a..24db48e2 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -48,7 +48,7 @@ class OverUnderflowStateAnnotation(StateAnnotation): def __init__(self) -> None: self.overflowing_state_annotations = [] # type: List[OverUnderflowAnnotation] - self.ostates_seen = [] # type: List[GlobalState] + self.ostates_seen = set() # type: List[GlobalState] def __copy__(self): new_annotation = OverUnderflowStateAnnotation() @@ -88,8 +88,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): Cache satisfiability of overflow constraints """ - self._ostates_satisfiable = [] # type: List[GlobalState] - self._ostates_unsatisfiable = [] # type: List[GlobalState] + self._ostates_satisfiable = set() + self._ostates_unsatisfiable = set() def reset_module(self): """ @@ -98,8 +98,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): """ super().reset_module() self._overflow_cache = {} - self._ostates_satisfiable = [] - self._ostates_unsatisfiable = [] + self._ostates_satisfiable = set() + self._ostates_unsatisfiable = set() def _execute(self, state: GlobalState) -> None: """Executes analysis module for integer underflow and integer overflow. @@ -232,7 +232,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): continue state_annotation.overflowing_state_annotations.append(annotation) - state_annotation.ostates_seen.append(annotation.overflowing_state) + state_annotation.ostates_seen.add(annotation.overflowing_state) @staticmethod def _handle_jumpi(state): @@ -250,7 +250,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): continue state_annotation.overflowing_state_annotations.append(annotation) - state_annotation.ostates_seen.append(annotation.overflowing_state) + state_annotation.ostates_seen.add(annotation.overflowing_state) @staticmethod def _handle_return(state: GlobalState) -> None: @@ -258,7 +258,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): Adds all the annotations into the state which correspond to the locations in the memory returned by RETURN opcode. :param state: The Global State - + """ stack = state.mstate.stack try: @@ -274,11 +274,12 @@ class IntegerOverflowUnderflowModule(DetectionModule): continue for annotation in element.annotations: - if isinstance(annotation, OverUnderflowAnnotation): + if ( + isinstance(annotation, OverUnderflowAnnotation) + and annotation not in state_annotation.overflowing_state_annotations + ): state_annotation.overflowing_state_annotations.append(annotation) - """ - def _handle_transaction_end(self, state: GlobalState) -> None: state_annotation = _get_overflowunderflow_state_annotation(state) @@ -294,9 +295,9 @@ class IntegerOverflowUnderflowModule(DetectionModule): try: constraints = ostate.mstate.constraints + [annotation.constraint] solver.get_model(constraints) - self._ostates_satisfiable.append(ostate) + self._ostates_satisfiable.add(ostate) except: - self._ostates_unsatisfiable.append(ostate) + self._ostates_unsatisfiable.add(ostate) continue log.debug( From 1faaf2a21dc7c02e2b4205cbfdd992b591998413 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 10:15:36 +0200 Subject: [PATCH 060/108] Update comment --- mythril/analysis/modules/integer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 24db48e2..2c849a79 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -79,7 +79,7 @@ class IntegerOverflowUnderflowModule(DetectionModule): ) """ - Cache addresses for which overflows already have been detected. + Cache addresses for which overflows or underflows already have been detected. """ self._overflow_cache = {} # type: Dict[int, bool] From 2f4648341ced317b88b4144ace6b2f0f9c0ac1bc Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 12 Jun 2019 12:05:15 +0200 Subject: [PATCH 061/108] Fix type hints --- mythril/analysis/modules/integer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 2c849a79..8b8a5d87 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -4,7 +4,7 @@ underflows.""" import json from math import log2, ceil -from typing import cast, List, Dict +from typing import cast, List, Dict, Set from mythril.analysis import solver from mythril.analysis.report import Issue from mythril.analysis.swc_data import INTEGER_OVERFLOW_AND_UNDERFLOW @@ -48,7 +48,7 @@ class OverUnderflowStateAnnotation(StateAnnotation): def __init__(self) -> None: self.overflowing_state_annotations = [] # type: List[OverUnderflowAnnotation] - self.ostates_seen = set() # type: List[GlobalState] + self.ostates_seen = set() # type: Set[GlobalState] def __copy__(self): new_annotation = OverUnderflowStateAnnotation() @@ -88,8 +88,8 @@ class IntegerOverflowUnderflowModule(DetectionModule): Cache satisfiability of overflow constraints """ - self._ostates_satisfiable = set() - self._ostates_unsatisfiable = set() + self._ostates_satisfiable = set() # type: Set[GlobalState] + self._ostates_unsatisfiable = set() # type: Set[GlobalState] def reset_module(self): """ From 6bcc392347249fe75816141c3e5041f2ad69aecb Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 13 Jun 2019 16:34:53 +0530 Subject: [PATCH 062/108] Update mythril to mythril classic as docs link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8618cb5a..14fed21b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![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) -[![Read the Docs](https://readthedocs.org/projects/mythril/badge/?version=master)](https://mythril.readthedocs.io/en/master/) +[![Read the Docs](https://readthedocs.org/projects/mythril-classic/badge/?version=master)](https://mythril.readthedocs.io/en/master/) ![Master Build Status](https://img.shields.io/circleci/build/github/ConsenSys/mythril.svg?token=97124ecfaee54366859cae98b5dafc0714325f8b) [![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril) [![Pypi Installs](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril) @@ -41,7 +41,7 @@ Instructions for using Mythril are found on the [Wiki](https://github.com/Consen For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG). ## Bulding the Documentation -Mythril's documentation is contained in the `docs` folder and is published to [Read the Docs](https://mythril.readthedocs.io/en/master/). It is based on Sphinx and can be built using the Makefile contained in the subdirectory: +Mythril's documentation is contained in the `docs` folder and is published to [Read the Docs](https://mythril-classic.readthedocs.io/en/master/). It is based on Sphinx and can be built using the Makefile contained in the subdirectory: ``` cd docs From 3a4cfb96e04aeab3661babf87da8fd81d4e5fe2a Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Thu, 13 Jun 2019 16:35:54 +0530 Subject: [PATCH 063/108] Change badge for docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14fed21b..586d6fd8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![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) -[![Read the Docs](https://readthedocs.org/projects/mythril-classic/badge/?version=master)](https://mythril.readthedocs.io/en/master/) +[![Read the Docs](https://readthedocs.org/projects/mythril-classic/badge/?version=master)](https://mythril-classic.readthedocs.io/en/master/) ![Master Build Status](https://img.shields.io/circleci/build/github/ConsenSys/mythril.svg?token=97124ecfaee54366859cae98b5dafc0714325f8b) [![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril) [![Pypi Installs](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril) From 3e5df1c05c8baf4e771b870e504c86fbef5c2ba0 Mon Sep 17 00:00:00 2001 From: Joran Honig Date: Thu, 13 Jun 2019 15:07:53 +0200 Subject: [PATCH 064/108] make dir python module --- mythril/laser/ethereum/strategy/extensions/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mythril/laser/ethereum/strategy/extensions/__init__.py diff --git a/mythril/laser/ethereum/strategy/extensions/__init__.py b/mythril/laser/ethereum/strategy/extensions/__init__.py new file mode 100644 index 00000000..e69de29b From b3bf6fbcde2b5749f946c16d0a113263bdfbd733 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 14 Jun 2019 10:55:18 +0530 Subject: [PATCH 065/108] Fix Storage and calldata --- mythril/laser/ethereum/instructions.py | 70 +++++++++++-------- mythril/laser/ethereum/state/account.py | 16 ++++- tests/laser/evm_testsuite/evm_test.py | 4 +- tests/laser/state/storage_test.py | 12 ++-- .../metacoin.sol.o.graph.html | 6 +- .../outputs_expected/metacoin.sol.o.json | 16 ++++- .../outputs_expected/metacoin.sol.o.jsonv2 | 20 +++++- .../outputs_expected/metacoin.sol.o.markdown | 15 +++- .../outputs_expected/metacoin.sol.o.text | 12 +++- 9 files changed, 124 insertions(+), 47 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index e1903c42..8ab3b5a3 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -768,18 +768,10 @@ class Instruction: size_sym = True if size_sym: - state.mem_extend(mstart, 1) - state.memory[mstart] = global_state.new_bitvec( - "calldata_" - + str(environment.active_account.contract_name) - + "[" - + str(dstart) - + ": + " - + str(size) - + "]", - 8, - ) - return [global_state] + size = ( + 320 + ) # This excess stuff will get overwritten as memory is dynamically sized + size = cast(int, size) if size > 0: try: @@ -1389,7 +1381,15 @@ class Instruction: return self._sload_helper(global_state, str(index)) storage_keys = global_state.environment.active_account.storage.keys() - keccak_keys = list(filter(keccak_function_manager.is_keccak, storage_keys)) + keys = filter(keccak_function_manager.is_keccak, storage_keys) + addr = global_state.get_current_instruction()["address"] + keccak_keys = [ + key + for key in keys + if global_state.environment.active_account.storage.potential_func( + key, addr + ) + ] results = [] # type: List[GlobalState] constraints = [] @@ -1427,11 +1427,16 @@ class Instruction: :param constraints: :return: """ + address = global_state.get_current_instruction()["address"] try: - data = global_state.environment.active_account.storage[index] + data = global_state.environment.active_account.storage.get( + index, addr=address + ) except KeyError: data = global_state.new_bitvec("storage_" + str(index), 256) - global_state.environment.active_account.storage[index] = data + global_state.environment.active_account.storage.put( + key=index, value=data, addr=address + ) if constraints is not None: global_state.mstate.constraints += constraints @@ -1475,7 +1480,15 @@ class Instruction: return self._sstore_helper(global_state, str(index), value) storage_keys = global_state.environment.active_account.storage.keys() - keccak_keys = filter(keccak_function_manager.is_keccak, storage_keys) + keccak_keys = list(filter(keccak_function_manager.is_keccak, storage_keys)) + addr = global_state.get_current_instruction()["address"] + keccak_keys = [ + key + for key in keccak_keys + if global_state.environment.active_account.storage.potential_func( + key, addr + ) + ] results = [] # type: List[GlobalState] new = symbol_factory.Bool(False) @@ -1528,19 +1541,18 @@ class Instruction: :param constraint: :return: """ - try: - global_state.environment.active_account = deepcopy( - global_state.environment.active_account - ) - global_state.accounts[ - global_state.environment.active_account.address.value - ] = global_state.environment.active_account - - global_state.environment.active_account.storage[index] = ( - value if not isinstance(value, Expression) else simplify(value) - ) - except KeyError: - log.debug("Error writing to storage: Invalid index") + global_state.environment.active_account = deepcopy( + global_state.environment.active_account + ) + global_state.accounts[ + global_state.environment.active_account.address.value + ] = global_state.environment.active_account + address = global_state.get_current_instruction()["address"] + global_state.environment.active_account.storage.put( + key=index, + value=value if not isinstance(value, Expression) else simplify(value), + addr=address, + ) if constraint is not None: global_state.mstate.constraints.append(constraint) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index e806a71e..93b03831 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -24,8 +24,12 @@ class Storage: self.concrete = concrete self.dynld = dynamic_loader self.address = address + self._storage_opcodes = {} # type: Dict - def __getitem__(self, item: Union[str, int]) -> Any: + def get(self, item: Union[str, int], addr: int) -> Any: + if item not in self._storage_opcodes: + self._storage_opcodes[item] = set() + self._storage_opcodes[item].add(addr) try: return self._storage[item] except KeyError: @@ -56,9 +60,17 @@ class Storage: ) return self._storage[item] - def __setitem__(self, key: Union[int, str], value: Any) -> None: + def put(self, key: Union[int, str], value: Any, addr) -> None: + if key not in self._storage_opcodes: + self._storage_opcodes[key] = set() + self._storage_opcodes[key].add(addr) self._storage[key] = value + def potential_func(self, key, opcode) -> bool: + if key not in self._storage_opcodes: + return False + return opcode in self._storage_opcodes[key] + def keys(self) -> KeysView: """ diff --git a/tests/laser/evm_testsuite/evm_test.py b/tests/laser/evm_testsuite/evm_test.py index 381438cb..93e927ed 100644 --- a/tests/laser/evm_testsuite/evm_test.py +++ b/tests/laser/evm_testsuite/evm_test.py @@ -125,7 +125,7 @@ def test_vmtest( account.code = Disassembly(details["code"][2:]) account.nonce = int(details["nonce"], 16) for key, value in details["storage"].items(): - account.storage[int(key, 16)] = int(value, 16) + account.storage.put(int(key, 16), int(value, 16), 10) world_state.put_account(account) account.set_balance(int(details["balance"], 16)) @@ -175,7 +175,7 @@ def test_vmtest( for index, value in details["storage"].items(): expected = int(value, 16) - actual = account.storage[int(index, 16)] + actual = account.storage.get(int(index, 16), 0) if isinstance(actual, Expression): actual = actual.value diff --git a/tests/laser/state/storage_test.py b/tests/laser/state/storage_test.py index 35e31df3..5fa6078f 100644 --- a/tests/laser/state/storage_test.py +++ b/tests/laser/state/storage_test.py @@ -12,7 +12,7 @@ def test_concrete_storage_uninitialized_index(initial_storage, key): storage._storage = initial_storage # Act - value = storage[key] + value = storage.get(key, 0) # Assert assert value == 0 @@ -25,7 +25,7 @@ def test_symbolic_storage_uninitialized_index(initial_storage, key): storage._storage = initial_storage # Act - value = storage[key] + value = storage.get(key, 0) # Assert assert isinstance(value, Expression) @@ -36,10 +36,10 @@ def test_storage_set_item(): storage = Storage() # Act - storage[1] = 13 + storage.put(key=1, value=13, addr=10) # Assert - assert storage[1] == 13 + assert storage.put(key=1, value=13, addr=10) def test_storage_change_item(): @@ -47,7 +47,7 @@ def test_storage_change_item(): storage = Storage() storage._storage = {1: 12} # Act - storage[1] = 14 + storage.put(key=1, value=14, addr=10) # Assert - assert storage[1] == 14 + assert storage.get(item=1, addr=10) == 14 diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.graph.html b/tests/testdata/outputs_expected/metacoin.sol.o.graph.html index 6ee81b9c..64169495 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.graph.html +++ b/tests/testdata/outputs_expected/metacoin.sol.o.graph.html @@ -24,8 +24,8 @@ @@ -59,4 +59,4 @@ }); - + \ No newline at end of file diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.json b/tests/testdata/outputs_expected/metacoin.sol.o.json index 712f50c1..2dd384d1 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.json +++ b/tests/testdata/outputs_expected/metacoin.sol.o.json @@ -1,5 +1,19 @@ { "error": null, - "issues": [], + "issues": [ + { + "address": 498, + "contract": "Unknown", + "debug": "", + "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", + "function": "sendToken(address,uint256)", + "max_gas_used": 52806, + "min_gas_used": 11860, + "severity": "High", + "sourceMap": null, + "swc-id": "101", + "title": "Integer Overflow" + } + ], "success": true } \ No newline at end of file diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 index 40de69b4..9c96d9d3 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 @@ -1,6 +1,24 @@ [ { - "issues": [], + "issues": [ + { + "description": { + "head": "The binary addition can overflow.", + "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." + }, + "extra": { + "discoveryTime": "" + }, + "locations": [ + { + "sourceMap": "498:1:0" + } + ], + "severity": "High", + "swcID": "SWC-101", + "swcTitle": "Integer Overflow and Underflow" + } + ], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": [ diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.markdown b/tests/testdata/outputs_expected/metacoin.sol.o.markdown index 321484fd..1eb30c6a 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.markdown +++ b/tests/testdata/outputs_expected/metacoin.sol.o.markdown @@ -1,3 +1,14 @@ -# Analysis results for None +# Analysis results for test-filename.sol -The analysis was completed successfully. No issues were detected. +## Integer Overflow +- SWC ID: 101 +- Severity: High +- Contract: Unknown +- Function name: `sendToken(address,uint256)` +- PC address: 498 +- Estimated Gas Usage: 11860 - 52806 + +### Description + +The binary addition can overflow. +The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.text b/tests/testdata/outputs_expected/metacoin.sol.o.text index 729320d8..412039a2 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.text +++ b/tests/testdata/outputs_expected/metacoin.sol.o.text @@ -1 +1,11 @@ -The analysis was completed successfully. No issues were detected. +==== Integer Overflow ==== +SWC ID: 101 +Severity: High +Contract: Unknown +Function name: sendToken(address,uint256) +PC address: 498 +Estimated Gas Usage: 11860 - 52806 +The binary addition can overflow. +The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. +-------------------- + From bf90a122419b7bf6d14d93c63a0915d6228e13b3 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Fri, 14 Jun 2019 12:11:51 +0600 Subject: [PATCH 066/108] Add required env variables to Edelweiss container --- .circleci/config.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 89cfd337..2ca56887 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,7 +102,11 @@ jobs: - run: name: Run Edelweiss command: | - docker run --rm edelweiss-mythril:latest \ + docker run \ + -e CIRCLE_BUILD_NUM=$CIRCLE_BUILD_NUM \ + -e CIRCLE_BUILD_URL=$CIRCLE_BUILD_URL \ + -e CIRCLE_WEBHOOK_URL=$CIRCLE_WEBHOOK_URL \ + --rm edelweiss-mythril:latest \ --timeout 90 \ --output-dir /opt/edelweiss \ --plugin-dir /opt/mythril \ @@ -164,6 +168,8 @@ workflows: only: - develop - master + # Temprorary add for checking envs injection + - improvement/circleci-envs tags: only: /v[0-9]+(\.[0-9]+)*/ requires: From bedbc7d287c1b2dd686d1884935f28569ca79764 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Fri, 14 Jun 2019 11:57:41 +0530 Subject: [PATCH 067/108] Fix storage tests --- tests/laser/state/storage_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/laser/state/storage_test.py b/tests/laser/state/storage_test.py index 5fa6078f..a0da2c94 100644 --- a/tests/laser/state/storage_test.py +++ b/tests/laser/state/storage_test.py @@ -39,7 +39,7 @@ def test_storage_set_item(): storage.put(key=1, value=13, addr=10) # Assert - assert storage.put(key=1, value=13, addr=10) + assert storage.get(item=1, addr=10) == 13 def test_storage_change_item(): From 07b99c5166f5d205001bc61d288aab402e0125d4 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Fri, 14 Jun 2019 14:12:04 +0600 Subject: [PATCH 068/108] Remove temporary branch from workflow --- .circleci/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2ca56887..6174655f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -168,8 +168,6 @@ workflows: only: - develop - master - # Temprorary add for checking envs injection - - improvement/circleci-envs tags: only: /v[0-9]+(\.[0-9]+)*/ requires: From e3fe79f7773c1faf15147369806133d57ce442de Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Fri, 14 Jun 2019 15:26:52 +0600 Subject: [PATCH 069/108] Define ENV variable to suspend regressions reporting for some testcases --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6174655f..8be4f6b6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,7 +111,8 @@ jobs: --output-dir /opt/edelweiss \ --plugin-dir /opt/mythril \ --s3 \ - --circle-ci CircleCI/mythril.csv + --circle-ci CircleCI/mythril.csv\ + --ignore-regressions $IGNORE_REGRESSIONS pypi_release: <<: *defaults From 22d286e13637434845090b003298f54e7011269f Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 14 Jun 2019 15:19:30 +0200 Subject: [PATCH 070/108] Check constraints on caller in delegatecall module --- mythril/analysis/modules/delegatecall.py | 38 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 71bc0fb4..eee0b20f 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -6,6 +6,10 @@ from typing import List, cast from mythril.analysis import solver from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT +from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS +from mythril.laser.ethereum.transaction.transaction_models import ( + ContractCreationTransaction, +) from mythril.analysis.report import Issue from mythril.analysis.modules.base import DetectionModule from mythril.exceptions import UnsatError @@ -17,16 +21,26 @@ log = logging.getLogger(__name__) class DelegateCallAnnotation(StateAnnotation): - def __init__(self, call_state: GlobalState) -> None: + def __init__(self, call_state: GlobalState, constraints: List) -> None: """ Initialize DelegateCall Annotation :param call_state: Call state """ self.call_state = call_state + self.constraints = constraints self.return_value = call_state.new_bitvec( "retval_{}".format(call_state.get_current_instruction()["address"]), 256 ) + """ + def __copy__(self): + result = DelegateCallAnnotation() + result.call_state = self.call_state + result.return_value = self.return_value + result.constraints = copy(self.constraints) + return result + """ + def get_issue(self, global_state: GlobalState, transaction_sequence: str) -> Issue: """ Returns Issue for the annotation @@ -107,26 +121,26 @@ def _analyze_states(state: GlobalState) -> List[Issue]: gas = state.mstate.stack[-1] to = state.mstate.stack[-2] - constraints = copy(state.mstate.constraints) - # Check whether we can also set the callee address - - constraints += [ - to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, + constraints = [ + to == ATTACKER_ADDRESS, UGT(gas, symbol_factory.BitVecVal(2300, 256)), ] - try: - solver.get_model(constraints) - state.annotate(DelegateCallAnnotation(call_state=state)) - except UnsatError: - log.debug("[DELEGATECALL] Annotation skipped.") + for tx in state.world_state.transaction_sequence: + if not isinstance(tx, ContractCreationTransaction): + constraints.append(tx.caller == ATTACKER_ADDRESS) + + state.annotate(DelegateCallAnnotation(state, constraints)) return [] else: for annotation in annotations: try: transaction_sequence = solver.get_transaction_sequence( - state, state.mstate.constraints + [annotation.return_value == 1] + state, + state.mstate.constraints + + annotation.constraints + + [annotation.return_value == 1], ) debug = json.dumps(transaction_sequence, indent=4) issues.append(annotation.get_issue(state, transaction_sequence=debug)) From 34d627086489c8be621e281f2403abbaa7713926 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 14 Jun 2019 15:50:27 +0200 Subject: [PATCH 071/108] Add caller constraints to all relevant modules --- mythril/analysis/modules/delegatecall.py | 3 --- mythril/analysis/modules/ether_thief.py | 14 ++++++++------ mythril/analysis/modules/external_calls.py | 11 ++++++++++- mythril/analysis/modules/suicide.py | 19 ++++++++++++------- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index eee0b20f..4eeec52b 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -1,7 +1,6 @@ """This module contains the detection code for insecure delegate call usage.""" import json import logging -from copy import copy from typing import List, cast from mythril.analysis import solver @@ -32,14 +31,12 @@ class DelegateCallAnnotation(StateAnnotation): "retval_{}".format(call_state.get_current_instruction()["address"]), 256 ) - """ def __copy__(self): result = DelegateCallAnnotation() result.call_state = self.call_state result.return_value = self.return_value result.constraints = copy(self.constraints) return result - """ def get_issue(self, global_state: GlobalState, transaction_sequence: str) -> Issue: """ diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 3fa65aa7..064ef0b7 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -8,6 +8,9 @@ from mythril.analysis import solver from mythril.analysis.modules.base import DetectionModule from mythril.analysis.report import Issue from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS +from mythril.laser.ethereum.transaction.transaction_models import ( + ContractCreationTransaction, +) from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.exceptions import UnsatError from mythril.laser.ethereum.state.global_state import GlobalState @@ -85,11 +88,10 @@ class EtherThief(DetectionModule): constraints += [BVAddNoOverflow(eth_sent_total, tx.call_value, False)] eth_sent_total = Sum(eth_sent_total, tx.call_value) - constraints += [ - tx.caller == ATTACKER_ADDRESS, - UGT(call_value, eth_sent_total), - target == ATTACKER_ADDRESS, - ] + if not isinstance(tx, ContractCreationTransaction): + constraints.append(tx.caller == ATTACKER_ADDRESS) + + constraints += [UGT(call_value, eth_sent_total), target == ATTACKER_ADDRESS] try: @@ -116,7 +118,7 @@ class EtherThief(DetectionModule): log.debug("[ETHER_THIEF] no model found") return [] - self._cache_addresses[address] = True + # self._cache_addresses[address] = True return [issue] diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index 510dd82c..21605503 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -3,6 +3,10 @@ calls.""" from mythril.analysis import solver from mythril.analysis.swc_data import REENTRANCY +from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS +from mythril.laser.ethereum.transaction.transaction_models import ( + ContractCreationTransaction, +) from mythril.analysis.modules.base import DetectionModule from mythril.analysis.report import Issue from mythril.laser.smt import UGT, symbol_factory, Or, BitVec @@ -44,7 +48,12 @@ def _analyze_state(state): # Check whether we can also set the callee address try: - constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF] + constraints += [to == ATTACKER_ADDRESS] + + for tx in state.world_state.transaction_sequence: + if not isinstance(tx, ContractCreationTransaction): + constraints.append(tx.caller == ATTACKER_ADDRESS) + transaction_sequence = solver.get_transaction_sequence(state, constraints) debug = json.dumps(transaction_sequence, indent=4) diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index 57c593e2..a35636b1 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -5,6 +5,9 @@ from mythril.exceptions import UnsatError from mythril.analysis.modules.base import DetectionModule from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS +from mythril.laser.ethereum.transaction.transaction_models import ( + ContractCreationTransaction, +) import logging import json @@ -58,16 +61,18 @@ class SuicideModule(DetectionModule): ) description_head = "The contract can be killed by anyone." - caller = state.current_transaction.caller + + constraints = [] + + for tx in state.world_state.transaction_sequence: + if not isinstance(tx, ContractCreationTransaction): + constraints.append(tx.caller == ATTACKER_ADDRESS) + try: try: transaction_sequence = solver.get_transaction_sequence( state, - state.mstate.constraints - + [ - to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, - caller == ATTACKER_ADDRESS, - ], + state.mstate.constraints + constraints + [to == ATTACKER_ADDRESS], ) description_tail = ( "Anyone can kill this contract and withdraw its balance to an arbitrary " @@ -75,7 +80,7 @@ class SuicideModule(DetectionModule): ) except UnsatError: transaction_sequence = solver.get_transaction_sequence( - state, state.mstate.constraints + [caller == ATTACKER_ADDRESS] + state, state.mstate.constraints + constraints ) description_tail = "Arbitrary senders can kill this contract." From a7bad0e96b63039b120c938af12b7224ecff218c Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 14 Jun 2019 16:35:29 +0200 Subject: [PATCH 072/108] Fix annotation copy --- mythril/analysis/modules/delegatecall.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 4eeec52b..55d61532 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -2,6 +2,7 @@ import json import logging from typing import List, cast +from copy import copy from mythril.analysis import solver from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT @@ -32,11 +33,7 @@ class DelegateCallAnnotation(StateAnnotation): ) def __copy__(self): - result = DelegateCallAnnotation() - result.call_state = self.call_state - result.return_value = self.return_value - result.constraints = copy(self.constraints) - return result + return DelegateCallAnnotation(self.call_state, copy(self.constraints)) def get_issue(self, global_state: GlobalState, transaction_sequence: str) -> Issue: """ From cdbda2a81c50615cc91d7a14e9b48a9ab875de31 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 01:20:57 +0200 Subject: [PATCH 073/108] Add mutation pruner & debut usage of get_concrete_int --- mythril/analysis/symbolic.py | 1 + .../implementations/dependency_pruner.py | 274 ++++++++++++++++++ .../laser/ethereum/plugins/plugin_factory.py | 9 + mythril/laser/ethereum/plugins/signals.py | 10 + mythril/laser/ethereum/svm.py | 14 +- 5 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 mythril/laser/ethereum/plugins/implementations/dependency_pruner.py diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 55ebf39d..e4099469 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -96,6 +96,7 @@ class SymExecWrapper: plugin_loader = LaserPluginLoader(self.laser) plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) + plugin_loader.load(PluginFactory.build_dependency_pruner_plugin()) plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) if run_analysis_modules: diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py new file mode 100644 index 00000000..6a255155 --- /dev/null +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -0,0 +1,274 @@ +from mythril.laser.ethereum.svm import LaserEVM +from mythril.laser.ethereum.plugins.plugin import LaserPlugin +from mythril.laser.ethereum.plugins.signals import PluginSkipState +from mythril.laser.ethereum.state.annotation import StateAnnotation +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.transaction.transaction_models import ( + ContractCreationTransaction, +) +from mythril.laser.ethereum.util import get_concrete_int +from typing import cast, List +from copy import copy +import logging + + +log = logging.getLogger(__name__) + + +class DependencyAnnotation(StateAnnotation): + """Dependency Annotation + + This annotation tracks read and write access to the state during each transaction. + """ + + def __init__(self): + self.storage_loaded = [] # type: List + self.storage_written = {} # type: Dict[int, List] + self.path = [0] # type: List + + def __copy__(self): + result = DependencyAnnotation() + result.storage_loaded = copy(self.storage_loaded) + result.storage_written = copy(self.storage_written) + result.path = copy(self.path) + return result + + def get_storage_write_cache(self, iteration: int): + if iteration not in self.storage_written: + self.storage_written[iteration] = [] + + return self.storage_written[iteration] + + def extend_storage_write_cache(self, iteration: int, value: object): + if iteration not in self.storage_written: + self.storage_written[iteration] = [value] + else: + self.storage_written[iteration] = list( + set(self.storage_written[iteration] + [value]) + ) + + +class WSDependencyAnnotation(StateAnnotation): + """Dependency Annotation for World state + + This world state annotation maintains a stack of state annotations. + It is used to transfer individual state annotations from one transaction to the next. + """ + + def __init__(self): + self.annotations_stack = [] + + def __copy__(self): + result = WSDependencyAnnotation() + result.annotations_stack = copy(self.annotations_stack) + return result + + +def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation: + """ Returns a dependency annotation + + :param state: A global state object + """ + + annotations = cast( + List[DependencyAnnotation], list(state.get_annotations(DependencyAnnotation)) + ) + + if len(annotations) == 0: + + try: + world_state_annotation = get_ws_dependency_annotation(state) + annotation = world_state_annotation.annotations_stack.pop() + except IndexError: + annotation = DependencyAnnotation() + + state.annotate(annotation) + else: + annotation = annotations[0] + + return annotation + + +def get_ws_dependency_annotation(state: GlobalState) -> WSDependencyAnnotation: + """ Returns the world state annotation + + :param state: A global state object + """ + + annotations = cast( + List[WSDependencyAnnotation], + list(state.world_state.get_annotations(WSDependencyAnnotation)), + ) + + if len(annotations) == 0: + annotation = WSDependencyAnnotation() + state.world_state.annotate(annotation) + else: + annotation = annotations[0] + + return annotation + + +class DependencyPruner(LaserPlugin): + """Dependency Pruner Plugin + + For every basic block, this plugin keeps a list of storage locations that + are accessed (read) in the execution path containing that block's. This map + is built up over the whole symbolic execution run. + + After the initial build up of the map in the first transaction, blocks are + executed only if any of the storage locations written to in the previous + transaction can have an effect on that block or any of its successors. + """ + + def __init__(self): + """Creates DependencyPruner""" + self._reset() + + def _reset(self): + self.iteration = 0 + self.dependency_map = {} # type: Dict[int, List] + + def initialize(self, symbolic_vm: LaserEVM): + """Initializes the DependencyPruner + + :param symbolic_vm + """ + self._reset() + + @symbolic_vm.laser_hook("start_sym_trans") + def start_sym_trans_hook(): + self.iteration += 1 + + @symbolic_vm.post_hook("JUMP") + def mutator_hook(state: GlobalState): + address = state.get_current_instruction()["address"] + annotation = get_dependency_annotation(state) + + _check_basic_block(address, annotation) + + @symbolic_vm.pre_hook("JUMPDEST") + def mutator_hook(state: GlobalState): + address = state.get_current_instruction()["address"] + annotation = get_dependency_annotation(state) + + _check_basic_block(address, annotation) + + @symbolic_vm.post_hook("JUMPI") + def mutator_hook(state: GlobalState): + address = state.get_current_instruction()["address"] + annotation = get_dependency_annotation(state) + + _check_basic_block(address, annotation) + + def _check_basic_block(address, annotation): + """This method is where the actual pruning happens. + + :param state: + :return: + """ + + if self.iteration < 1: + return + + annotation.path.append(address) + + if address not in self.dependency_map: + return + + if not set( + annotation.get_storage_write_cache(self.iteration - 1) + ).intersection(set(self.dependency_map[address])): + log.info( + "Skipping state: Storage slots {} not read in block at address {}".format( + annotation.get_storage_write_cache(self.iteration - 1), address + ) + ) + raise PluginSkipState + + @symbolic_vm.pre_hook("SSTORE") + def sstore_hook(state: GlobalState): + annotation = get_dependency_annotation(state) + + index = get_concrete_int(state.mstate.stack[-1]) + + annotation.extend_storage_write_cache( + self.iteration, index + ) + + @symbolic_vm.pre_hook("SLOAD") + def sload_hook(state: GlobalState): + + index = get_concrete_int(state.mstate.stack[-1]) + + annotation = get_dependency_annotation(state) + annotation.storage_loaded = list(set(annotation.storage_loaded + [index])) + + # We need to backwards-annotate the path here in case execution never reaches a stop or return + # (which may change in a future transaction). + + for address in annotation.path: + + if address in self.dependency_map: + self.dependency_map[address] = list( + set(self.dependency_map[address] + [index]) + ) + else: + self.dependency_map[address] = [index] + + @symbolic_vm.pre_hook("STOP") + def stop_hook(state: GlobalState): + _transaction_end(state) + + @symbolic_vm.pre_hook("RETURN") + def return_hook(state: GlobalState): + _transaction_end(state) + + def _transaction_end(state: GlobalState): + """When a stop or return is reached, the storage locations read along the path are entered into + the dependency map for all nodes encountered in this path. + + :param state: + :return: + """ + + annotation = get_dependency_annotation(state) + + for index in annotation.storage_loaded: + + for address in annotation.path: + + if address in self.dependency_map: + self.dependency_map[address] = list( + set(self.dependency_map[address] + [index]) + ) + else: + self.dependency_map[address] = [index] + + @symbolic_vm.laser_hook("add_world_state") + def world_state_filter_hook(state: GlobalState): + + if isinstance(state.current_transaction, ContractCreationTransaction): + return + + world_state_annotation = get_ws_dependency_annotation(state) + annotation = get_dependency_annotation(state) + + # Reset the state annotation except for storage written which is carried on to + # the next transaction + + annotation.path = [0] + annotation.storage_loaded = [] + + world_state_annotation.annotations_stack.append(annotation) + + log.info( + "Iteration {}: Adding world state at address {}, end of function {}.\n" + + "Dependency map: {}\nStorage written: {}".format( + self.iteration, + state.get_current_instruction()["address"], + state.node.function_name, + self.dependency_map, + annotation.storage_written[self.iteration], + ) + ) diff --git a/mythril/laser/ethereum/plugins/plugin_factory.py b/mythril/laser/ethereum/plugins/plugin_factory.py index 1ce61b34..a81f80c4 100644 --- a/mythril/laser/ethereum/plugins/plugin_factory.py +++ b/mythril/laser/ethereum/plugins/plugin_factory.py @@ -30,3 +30,12 @@ class PluginFactory: ) return InstructionCoveragePlugin() + + @staticmethod + def build_dependency_pruner_plugin() -> LaserPlugin: + """ Creates an instance of the mutation pruner plugin""" + from mythril.laser.ethereum.plugins.implementations.dependency_pruner import ( + DependencyPruner, + ) + + return DependencyPruner() diff --git a/mythril/laser/ethereum/plugins/signals.py b/mythril/laser/ethereum/plugins/signals.py index 325f2d6a..b59614ca 100644 --- a/mythril/laser/ethereum/plugins/signals.py +++ b/mythril/laser/ethereum/plugins/signals.py @@ -15,3 +15,13 @@ class PluginSkipWorldState(PluginSignal): """ pass + + +class PluginSkipState(PluginSignal): + """ Plugin to skip world state + + Plugins that raise this signal while the add_world_state hook is being executed + will force laser to abandon that world state. + """ + + pass diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index a13cf5b7..8edbae3f 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -10,7 +10,7 @@ from mythril.laser.ethereum.evm_exceptions import StackUnderflowException from mythril.laser.ethereum.evm_exceptions import VmException from mythril.laser.ethereum.instructions import Instruction from mythril.laser.ethereum.iprof import InstructionProfiler -from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState +from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState, PluginSkipState from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy @@ -271,7 +271,12 @@ class LaserEVM: self._add_world_state(global_state) return [], None - self._execute_pre_hook(op_code, global_state) + try: + self._execute_pre_hook(op_code, global_state) + except PluginSkipState: + self._add_world_state(global_state) + return [], None + try: new_global_states = Instruction( op_code, self.dynamic_loader, self.iprof @@ -538,7 +543,10 @@ class LaserEVM: for hook in self.post_hooks[op_code]: for global_state in global_states: - hook(global_state) + try: + hook(global_state) + except PluginSkipState: + global_states.remove(global_state) def pre_hook(self, op_code: str) -> Callable: """ From 337d3e7ea91526c4ff5d3c79937f8b30535bf9a1 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 01:39:02 +0200 Subject: [PATCH 074/108] Add hack --- .../implementations/dependency_pruner.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 6a255155..9ee960cb 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -10,7 +10,7 @@ from mythril.laser.ethereum.util import get_concrete_int from typing import cast, List from copy import copy import logging - +import hashlib log = logging.getLogger(__name__) @@ -168,7 +168,7 @@ class DependencyPruner(LaserPlugin): :return: """ - if self.iteration < 1: + if self.iteration < 2: return annotation.path.append(address) @@ -190,7 +190,12 @@ class DependencyPruner(LaserPlugin): def sstore_hook(state: GlobalState): annotation = get_dependency_annotation(state) - index = get_concrete_int(state.mstate.stack[-1]) + try: + index = get_concrete_int(state.mstate.stack[-1]) + except TypeError: + m = hashlib.md5() + m.update(str(state.mstate.stack[-1]).encode('utf-8')) + index = m.digest().hex() annotation.extend_storage_write_cache( self.iteration, index @@ -199,7 +204,12 @@ class DependencyPruner(LaserPlugin): @symbolic_vm.pre_hook("SLOAD") def sload_hook(state: GlobalState): - index = get_concrete_int(state.mstate.stack[-1]) + try: + index = get_concrete_int(state.mstate.stack[-1]) + except TypeError: + m = hashlib.md5() + m.update(str(state.mstate.stack[-1]).encode('utf-8')) + index = m.digest().hex() annotation = get_dependency_annotation(state) annotation.storage_loaded = list(set(annotation.storage_loaded + [index])) @@ -263,8 +273,7 @@ class DependencyPruner(LaserPlugin): world_state_annotation.annotations_stack.append(annotation) log.info( - "Iteration {}: Adding world state at address {}, end of function {}.\n" - + "Dependency map: {}\nStorage written: {}".format( + "Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}".format( self.iteration, state.get_current_instruction()["address"], state.node.function_name, From 57883f2d6364adf45f0b85b9222ac3fb21ff7d07 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 01:54:09 +0200 Subject: [PATCH 075/108] Add 'support' for symbolic storage addresses --- .../plugins/implementations/dependency_pruner.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 9ee960cb..cfe815e1 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -168,7 +168,7 @@ class DependencyPruner(LaserPlugin): :return: """ - if self.iteration < 2: + if self.iteration < 1: return annotation.path.append(address) @@ -194,12 +194,10 @@ class DependencyPruner(LaserPlugin): index = get_concrete_int(state.mstate.stack[-1]) except TypeError: m = hashlib.md5() - m.update(str(state.mstate.stack[-1]).encode('utf-8')) + m.update(str(state.mstate.stack[-1]).encode("utf-8")) index = m.digest().hex() - annotation.extend_storage_write_cache( - self.iteration, index - ) + annotation.extend_storage_write_cache(self.iteration, index) @symbolic_vm.pre_hook("SLOAD") def sload_hook(state: GlobalState): @@ -208,7 +206,7 @@ class DependencyPruner(LaserPlugin): index = get_concrete_int(state.mstate.stack[-1]) except TypeError: m = hashlib.md5() - m.update(str(state.mstate.stack[-1]).encode('utf-8')) + m.update(str(state.mstate.stack[-1]).encode("utf-8")) index = m.digest().hex() annotation = get_dependency_annotation(state) From 82559dd0a58821b13bd3bcfbbf8a905bb0e96fd2 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 01:58:09 +0200 Subject: [PATCH 076/108] Now without log.info --- .../ethereum/plugins/implementations/dependency_pruner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index cfe815e1..a17acbeb 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -179,7 +179,7 @@ class DependencyPruner(LaserPlugin): if not set( annotation.get_storage_write_cache(self.iteration - 1) ).intersection(set(self.dependency_map[address])): - log.info( + log.debug( "Skipping state: Storage slots {} not read in block at address {}".format( annotation.get_storage_write_cache(self.iteration - 1), address ) @@ -270,7 +270,7 @@ class DependencyPruner(LaserPlugin): world_state_annotation.annotations_stack.append(annotation) - log.info( + log.debug( "Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}".format( self.iteration, state.get_current_instruction()["address"], From 43e0991acd7090b9adb957c74fb14b515f224bc1 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 13:08:19 +0200 Subject: [PATCH 077/108] Refactor to only consider concrete cases (for now) --- .../implementations/dependency_pruner.py | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index a17acbeb..e168d249 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -24,6 +24,7 @@ class DependencyAnnotation(StateAnnotation): def __init__(self): self.storage_loaded = [] # type: List self.storage_written = {} # type: Dict[int, List] + self.has_call = False self.path = [0] # type: List def __copy__(self): @@ -127,7 +128,54 @@ class DependencyPruner(LaserPlugin): def _reset(self): self.iteration = 0 - self.dependency_map = {} # type: Dict[int, List] + self.dependency_map = {} # type: Dict[int, List[object]] + self.protected_addresses = [] # type: List[int] + + def update_dependency_map(self, path: [int], target_location: object): + """Update the dependency map for the block offsets on the given path. + + :param path + :param target_location + """ + + for address in path: + + if address in self.dependency_map: + self.dependency_map[address] = list( + set(self.dependency_map[address] + [target_location]) + ) + else: + self.dependency_map[address] = [target_location] + + def wanna_execute(self, address: int, storage_write_cache) -> bool: + """TODO: Description + + :param address + :param storage_write_cache + """ + + if address in self.protected_addresses: + return False + + if address not in self.dependency_map: + return True + + dependencies = self.dependency_map[address] + + # Return true if there's any match + + for location in storage_write_cache: + for dependency in dependencies: + + if isinstance(location, int) and isinstance(dependency, int): + if location == dependency: + return True + + else: + # FIXME: Handle symbolic locations + return True + + return False def initialize(self, symbolic_vm: LaserEVM): """Initializes the DependencyPruner @@ -140,6 +188,12 @@ class DependencyPruner(LaserPlugin): def start_sym_trans_hook(): self.iteration += 1 + @symbolic_vm.post_hook("CALL") + def mutator_hook(state: GlobalState): + annotation = get_dependency_annotation(state) + + annotation.has_call = True + @symbolic_vm.post_hook("JUMP") def mutator_hook(state: GlobalState): address = state.get_current_instruction()["address"] @@ -165,6 +219,7 @@ class DependencyPruner(LaserPlugin): """This method is where the actual pruning happens. :param state: + :param annotation :return: """ @@ -173,12 +228,9 @@ class DependencyPruner(LaserPlugin): annotation.path.append(address) - if address not in self.dependency_map: + if self.wanna_execute(address, annotation.get_storage_write_cache(self.iteration - 1)): return - - if not set( - annotation.get_storage_write_cache(self.iteration - 1) - ).intersection(set(self.dependency_map[address])): + else: log.debug( "Skipping state: Storage slots {} not read in block at address {}".format( annotation.get_storage_write_cache(self.iteration - 1), address @@ -215,14 +267,7 @@ class DependencyPruner(LaserPlugin): # We need to backwards-annotate the path here in case execution never reaches a stop or return # (which may change in a future transaction). - for address in annotation.path: - - if address in self.dependency_map: - self.dependency_map[address] = list( - set(self.dependency_map[address] + [index]) - ) - else: - self.dependency_map[address] = [index] + self.update_dependency_map(annotation.path, index) @symbolic_vm.pre_hook("STOP") def stop_hook(state: GlobalState): @@ -243,15 +288,7 @@ class DependencyPruner(LaserPlugin): annotation = get_dependency_annotation(state) for index in annotation.storage_loaded: - - for address in annotation.path: - - if address in self.dependency_map: - self.dependency_map[address] = list( - set(self.dependency_map[address] + [index]) - ) - else: - self.dependency_map[address] = [index] + self.update_dependency_map(annotation.path, index) @symbolic_vm.laser_hook("add_world_state") def world_state_filter_hook(state: GlobalState): @@ -267,6 +304,7 @@ class DependencyPruner(LaserPlugin): annotation.path = [0] annotation.storage_loaded = [] + annotation.has_call = False world_state_annotation.annotations_stack.append(annotation) From a3a979bb5579e0a624f54c57e7ed6699e7850ef1 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 13:08:41 +0200 Subject: [PATCH 078/108] A little black can't hurt --- .../ethereum/plugins/implementations/dependency_pruner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index e168d249..602d2ea0 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -129,7 +129,7 @@ class DependencyPruner(LaserPlugin): def _reset(self): self.iteration = 0 self.dependency_map = {} # type: Dict[int, List[object]] - self.protected_addresses = [] # type: List[int] + self.protected_addresses = [] # type: List[int] def update_dependency_map(self, path: [int], target_location: object): """Update the dependency map for the block offsets on the given path. @@ -228,7 +228,9 @@ class DependencyPruner(LaserPlugin): annotation.path.append(address) - if self.wanna_execute(address, annotation.get_storage_write_cache(self.iteration - 1)): + if self.wanna_execute( + address, annotation.get_storage_write_cache(self.iteration - 1) + ): return else: log.debug( From 86c6c2041f6dfb5ddef6cd8ce6bb66bf149b3ff9 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 18:11:33 +0200 Subject: [PATCH 079/108] Protect paths that contain a message call --- .../implementations/dependency_pruner.py | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 602d2ea0..614e9883 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -6,11 +6,11 @@ from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, ) -from mythril.laser.ethereum.util import get_concrete_int +from mythril.exceptions import UnsatError +from mythril.analysis import solver from typing import cast, List from copy import copy import logging -import hashlib log = logging.getLogger(__name__) @@ -44,9 +44,8 @@ class DependencyAnnotation(StateAnnotation): if iteration not in self.storage_written: self.storage_written[iteration] = [value] else: - self.storage_written[iteration] = list( - set(self.storage_written[iteration] + [value]) - ) + if value not in self.storage_written[iteration]: + self.storage_written[iteration].append(value) class WSDependencyAnnotation(StateAnnotation): @@ -129,9 +128,9 @@ class DependencyPruner(LaserPlugin): def _reset(self): self.iteration = 0 self.dependency_map = {} # type: Dict[int, List[object]] - self.protected_addresses = [] # type: List[int] + self.protected_addresses = {} # type: Set[int] - def update_dependency_map(self, path: [int], target_location: object): + def update_dependency_map(self, path: [int], target_location: object) -> None: """Update the dependency map for the block offsets on the given path. :param path @@ -141,39 +140,42 @@ class DependencyPruner(LaserPlugin): for address in path: if address in self.dependency_map: - self.dependency_map[address] = list( - set(self.dependency_map[address] + [target_location]) - ) + if target_location not in self.dependency_map[address]: + self.dependency_map[address].append(target_location) else: self.dependency_map[address] = [target_location] + def protect_path(self, path: [int]) -> None: + """Prevent an execution path of being pruned. + + :param path + """ + + for address in path: + self.protected_addresses.add(address) + def wanna_execute(self, address: int, storage_write_cache) -> bool: - """TODO: Description + """Decide whether the basic block starting at 'address' should be executed. :param address :param storage_write_cache """ - if address in self.protected_addresses: - return False - - if address not in self.dependency_map: + if address in self.protected_addresses or address not in self.dependency_map: return True dependencies = self.dependency_map[address] - # Return true if there's any match + # Return if *any* dependency is found for location in storage_write_cache: for dependency in dependencies: - if isinstance(location, int) and isinstance(dependency, int): - if location == dependency: - return True - - else: - # FIXME: Handle symbolic locations + try: + solver.get_model([location == dependency]) return True + except UnsatError: + continue return False @@ -193,6 +195,7 @@ class DependencyPruner(LaserPlugin): annotation = get_dependency_annotation(state) annotation.has_call = True + self.protect_path(annotation.path) @symbolic_vm.post_hook("JUMP") def mutator_hook(state: GlobalState): @@ -238,38 +241,28 @@ class DependencyPruner(LaserPlugin): annotation.get_storage_write_cache(self.iteration - 1), address ) ) + raise PluginSkipState @symbolic_vm.pre_hook("SSTORE") def sstore_hook(state: GlobalState): annotation = get_dependency_annotation(state) - - try: - index = get_concrete_int(state.mstate.stack[-1]) - except TypeError: - m = hashlib.md5() - m.update(str(state.mstate.stack[-1]).encode("utf-8")) - index = m.digest().hex() - - annotation.extend_storage_write_cache(self.iteration, index) + annotation.extend_storage_write_cache( + self.iteration, state.mstate.stack[-1] + ) @symbolic_vm.pre_hook("SLOAD") def sload_hook(state: GlobalState): - - try: - index = get_concrete_int(state.mstate.stack[-1]) - except TypeError: - m = hashlib.md5() - m.update(str(state.mstate.stack[-1]).encode("utf-8")) - index = m.digest().hex() - annotation = get_dependency_annotation(state) - annotation.storage_loaded = list(set(annotation.storage_loaded + [index])) + location = state.mstate.stack[-1] - # We need to backwards-annotate the path here in case execution never reaches a stop or return - # (which may change in a future transaction). + if location not in annotation.storage_loaded: + annotation.storage_loaded.append(location) - self.update_dependency_map(annotation.path, index) + # We backwards-annotate the path here as sometimes execution never reaches a stop or return + # (and this may change in a future transaction). + + self.update_dependency_map(annotation.path, location) @symbolic_vm.pre_hook("STOP") def stop_hook(state: GlobalState): @@ -289,6 +282,9 @@ class DependencyPruner(LaserPlugin): annotation = get_dependency_annotation(state) + if annotation.has_call: + self.protect_path(annotation.path) + for index in annotation.storage_loaded: self.update_dependency_map(annotation.path, index) From 4915a6be74effb58f9ffdcbbb9d6686f39decd41 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 18:27:38 +0200 Subject: [PATCH 080/108] Add flag to disable pruning --- mythril/analysis/symbolic.py | 5 ++++- mythril/interfaces/cli.py | 6 ++++++ mythril/mythril/mythril_analyzer.py | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index e4099469..c41fb28e 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -46,6 +46,7 @@ class SymExecWrapper: modules=(), compulsory_statespace=True, enable_iprof=False, + disable_dependency_pruning=False, run_analysis_modules=True, ): """ @@ -96,9 +97,11 @@ class SymExecWrapper: plugin_loader = LaserPluginLoader(self.laser) plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) - plugin_loader.load(PluginFactory.build_dependency_pruner_plugin()) plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) + if not disable_dependency_pruning: + plugin_loader.load(PluginFactory.build_dependency_pruner_plugin()) + if run_analysis_modules: self.laser.register_hooks( hook_type="pre", diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 2d3a63e1..4c9d4222 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -249,6 +249,11 @@ def create_parser(parser: argparse.ArgumentParser) -> None: options.add_argument( "--enable-iprof", action="store_true", help="enable the instruction profiler" ) + options.add_argument( + "--disable-dependency-pruning", + action="store_true", + help="Deactivate dependency-based pruning", + ) rpc = parser.add_argument_group("RPC options") @@ -417,6 +422,7 @@ def execute_command( loop_bound=args.loop_bound, create_timeout=args.create_timeout, enable_iprof=args.enable_iprof, + disable_dependency_pruning=args.disable_dependency_pruning, onchain_storage_access=not args.no_onchain_storage_access, ) diff --git a/mythril/mythril/mythril_analyzer.py b/mythril/mythril/mythril_analyzer.py index 9433278e..d6267a94 100644 --- a/mythril/mythril/mythril_analyzer.py +++ b/mythril/mythril/mythril_analyzer.py @@ -38,6 +38,7 @@ class MythrilAnalyzer: loop_bound: Optional[int] = None, create_timeout: Optional[int] = None, enable_iprof: bool = False, + disable_dependency_pruning: bool = False, ): """ @@ -57,6 +58,7 @@ class MythrilAnalyzer: self.loop_bound = loop_bound self.create_timeout = create_timeout self.enable_iprof = enable_iprof + self.disable_dependency_pruning = disable_dependency_pruning def dump_statespace(self, contract: EVMContract = None) -> str: """ @@ -77,6 +79,7 @@ class MythrilAnalyzer: execution_timeout=self.execution_timeout, create_timeout=self.create_timeout, enable_iprof=self.enable_iprof, + disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, ) @@ -111,6 +114,7 @@ class MythrilAnalyzer: transaction_count=transaction_count, create_timeout=self.create_timeout, enable_iprof=self.enable_iprof, + disable_dependency_pruning=self.disable_dependency_pruning, run_analysis_modules=False, ) return generate_graph(sym, physics=enable_physics, phrackify=phrackify) @@ -150,6 +154,7 @@ class MythrilAnalyzer: modules=modules, compulsory_statespace=False, enable_iprof=self.enable_iprof, + disable_dependency_pruning=self.disable_dependency_pruning, ) issues = fire_lasers(sym, modules) From 0e3c04c835d7b60bbc9f72a9541aad5794687948 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 19:42:52 +0200 Subject: [PATCH 081/108] Improve type hints and annotations --- .../plugins/implementations/dependency_pruner.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 614e9883..e57d79c3 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -8,10 +8,11 @@ from mythril.laser.ethereum.transaction.transaction_models import ( ) from mythril.exceptions import UnsatError from mythril.analysis import solver -from typing import cast, List +from typing import cast, List, Dict from copy import copy import logging + log = logging.getLogger(__name__) @@ -179,7 +180,7 @@ class DependencyPruner(LaserPlugin): return False - def initialize(self, symbolic_vm: LaserEVM): + def initialize(self, symbolic_vm: LaserEVM) -> None: """Initializes the DependencyPruner :param symbolic_vm @@ -218,12 +219,11 @@ class DependencyPruner(LaserPlugin): _check_basic_block(address, annotation) - def _check_basic_block(address, annotation): + def _check_basic_block(address: int, annotation: DependencyAnnotation): """This method is where the actual pruning happens. - :param state: + :param address: Start address (bytecode offset) of the block :param annotation - :return: """ if self.iteration < 1: @@ -272,12 +272,11 @@ class DependencyPruner(LaserPlugin): def return_hook(state: GlobalState): _transaction_end(state) - def _transaction_end(state: GlobalState): + def _transaction_end(state: GlobalState) -> None: """When a stop or return is reached, the storage locations read along the path are entered into the dependency map for all nodes encountered in this path. :param state: - :return: """ annotation = get_dependency_annotation(state) From 1c207b0591426d973ea727c7cd1f29c201eedec3 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 19:55:26 +0200 Subject: [PATCH 082/108] Don't run in contract creation and 1st runtime tx --- .../ethereum/plugins/implementations/dependency_pruner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index e57d79c3..be5abc4a 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -226,7 +226,8 @@ class DependencyPruner(LaserPlugin): :param annotation """ - if self.iteration < 1: + # Don't skip any blocks in the contract creation transaction + if self.iteration < 2: return annotation.path.append(address) @@ -291,6 +292,8 @@ class DependencyPruner(LaserPlugin): def world_state_filter_hook(state: GlobalState): if isinstance(state.current_transaction, ContractCreationTransaction): + # Reset iteration variable + self.iteration = 0 return world_state_annotation = get_ws_dependency_annotation(state) From 44d5eff9b63f7f1c60d9fef2b354dcc3300d38a1 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 20:07:04 +0200 Subject: [PATCH 083/108] Formatting --- .../laser/ethereum/plugins/implementations/dependency_pruner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index be5abc4a..2a77a745 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -114,7 +114,7 @@ class DependencyPruner(LaserPlugin): """Dependency Pruner Plugin For every basic block, this plugin keeps a list of storage locations that - are accessed (read) in the execution path containing that block's. This map + are accessed (read) in the execution path containing that block. This map is built up over the whole symbolic execution run. After the initial build up of the map in the first transaction, blocks are From f9b7d6829124d86f49e097c60fb57fab9f061668 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 20:09:44 +0200 Subject: [PATCH 084/108] Fix a couple of errors --- .../ethereum/plugins/implementations/dependency_pruner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 2a77a745..6d757dda 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -8,7 +8,7 @@ from mythril.laser.ethereum.transaction.transaction_models import ( ) from mythril.exceptions import UnsatError from mythril.analysis import solver -from typing import cast, List, Dict +from typing import cast, List, Dict, Set from copy import copy import logging @@ -33,6 +33,7 @@ class DependencyAnnotation(StateAnnotation): result.storage_loaded = copy(self.storage_loaded) result.storage_written = copy(self.storage_written) result.path = copy(self.path) + result.has_call = self.has_call return result def get_storage_write_cache(self, iteration: int): @@ -129,7 +130,7 @@ class DependencyPruner(LaserPlugin): def _reset(self): self.iteration = 0 self.dependency_map = {} # type: Dict[int, List[object]] - self.protected_addresses = {} # type: Set[int] + self.protected_addresses = set() # type: Set[int] def update_dependency_map(self, path: [int], target_location: object) -> None: """Update the dependency map for the block offsets on the given path. From 39befe9fd8b010f54f4fa2b4626587b5bccc7da6 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 20:22:23 +0200 Subject: [PATCH 085/108] Give unique names to hook functions --- .../implementations/dependency_pruner.py | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 6d757dda..c6380db7 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -193,59 +193,33 @@ class DependencyPruner(LaserPlugin): self.iteration += 1 @symbolic_vm.post_hook("CALL") - def mutator_hook(state: GlobalState): + def call_hook(state: GlobalState): annotation = get_dependency_annotation(state) annotation.has_call = True self.protect_path(annotation.path) @symbolic_vm.post_hook("JUMP") - def mutator_hook(state: GlobalState): + def jump_hook(state: GlobalState): address = state.get_current_instruction()["address"] annotation = get_dependency_annotation(state) _check_basic_block(address, annotation) @symbolic_vm.pre_hook("JUMPDEST") - def mutator_hook(state: GlobalState): + def jumpdest_hook(state: GlobalState): address = state.get_current_instruction()["address"] annotation = get_dependency_annotation(state) _check_basic_block(address, annotation) @symbolic_vm.post_hook("JUMPI") - def mutator_hook(state: GlobalState): + def jumpi_hook(state: GlobalState): address = state.get_current_instruction()["address"] annotation = get_dependency_annotation(state) _check_basic_block(address, annotation) - def _check_basic_block(address: int, annotation: DependencyAnnotation): - """This method is where the actual pruning happens. - - :param address: Start address (bytecode offset) of the block - :param annotation - """ - - # Don't skip any blocks in the contract creation transaction - if self.iteration < 2: - return - - annotation.path.append(address) - - if self.wanna_execute( - address, annotation.get_storage_write_cache(self.iteration - 1) - ): - return - else: - log.debug( - "Skipping state: Storage slots {} not read in block at address {}".format( - annotation.get_storage_write_cache(self.iteration - 1), address - ) - ) - - raise PluginSkipState - @symbolic_vm.pre_hook("SSTORE") def sstore_hook(state: GlobalState): annotation = get_dependency_annotation(state) @@ -289,6 +263,32 @@ class DependencyPruner(LaserPlugin): for index in annotation.storage_loaded: self.update_dependency_map(annotation.path, index) + def _check_basic_block(address: int, annotation: DependencyAnnotation): + """This method is where the actual pruning happens. + + :param address: Start address (bytecode offset) of the block + :param annotation + """ + + # Don't skip any blocks in the contract creation transaction + if self.iteration < 2: + return + + annotation.path.append(address) + + if self.wanna_execute( + address, annotation.get_storage_write_cache(self.iteration - 1) + ): + return + else: + log.debug( + "Skipping state: Storage slots {} not read in block at address {}".format( + annotation.get_storage_write_cache(self.iteration - 1), address + ) + ) + + raise PluginSkipState + @symbolic_vm.laser_hook("add_world_state") def world_state_filter_hook(state: GlobalState): From 88487c29f2d53b2056da37a4ce2148c31bb9def7 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Sat, 15 Jun 2019 20:32:49 +0200 Subject: [PATCH 086/108] Fix type hints --- .../ethereum/plugins/implementations/dependency_pruner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index c6380db7..20036ed0 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -132,7 +132,7 @@ class DependencyPruner(LaserPlugin): self.dependency_map = {} # type: Dict[int, List[object]] self.protected_addresses = set() # type: Set[int] - def update_dependency_map(self, path: [int], target_location: object) -> None: + def update_dependency_map(self, path: List[int], target_location: object) -> None: """Update the dependency map for the block offsets on the given path. :param path @@ -147,7 +147,7 @@ class DependencyPruner(LaserPlugin): else: self.dependency_map[address] = [target_location] - def protect_path(self, path: [int]) -> None: + def protect_path(self, path: List[int]) -> None: """Prevent an execution path of being pruned. :param path From b65828bf84e78076d07491d6acc528c96309ca0a Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Mon, 17 Jun 2019 15:17:04 +0600 Subject: [PATCH 087/108] Change cli param to ignore-false-positives --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8be4f6b6..7666ad6a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -112,7 +112,7 @@ jobs: --plugin-dir /opt/mythril \ --s3 \ --circle-ci CircleCI/mythril.csv\ - --ignore-regressions $IGNORE_REGRESSIONS + --ignore-false-positives $IGNORE_FALSE_POSITIVES pypi_release: <<: *defaults From da779aa56aad8e93b2c1ad079e5a4a944fe14b98 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Mon, 17 Jun 2019 15:18:04 +0600 Subject: [PATCH 088/108] Added branch to CircleCI config for debugging new changes --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7666ad6a..9937aa62 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -169,6 +169,7 @@ workflows: only: - develop - master + - feature/ignore-regressions tags: only: /v[0-9]+(\.[0-9]+)*/ requires: From a0f0767b157fe0ad4b758f514ad9b47164210811 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 17 Jun 2019 14:57:44 +0530 Subject: [PATCH 089/108] Revert "Merge pull request #1076 from ConsenSys/bugfix/bectoken" This reverts commit 558dd4239085de963f699b512933e76e7b35a20b, reversing changes made to 0107a6212da76c01835cc733200c2203c43a0ca4. --- mythril/laser/ethereum/instructions.py | 70 ++++++++----------- mythril/laser/ethereum/state/account.py | 16 +---- tests/laser/evm_testsuite/evm_test.py | 4 +- tests/laser/state/storage_test.py | 12 ++-- .../metacoin.sol.o.graph.html | 6 +- .../outputs_expected/metacoin.sol.o.json | 16 +---- .../outputs_expected/metacoin.sol.o.jsonv2 | 20 +----- .../outputs_expected/metacoin.sol.o.markdown | 15 +--- .../outputs_expected/metacoin.sol.o.text | 12 +--- 9 files changed, 47 insertions(+), 124 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 8ab3b5a3..e1903c42 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -768,10 +768,18 @@ class Instruction: size_sym = True if size_sym: - size = ( - 320 - ) # This excess stuff will get overwritten as memory is dynamically sized - + state.mem_extend(mstart, 1) + state.memory[mstart] = global_state.new_bitvec( + "calldata_" + + str(environment.active_account.contract_name) + + "[" + + str(dstart) + + ": + " + + str(size) + + "]", + 8, + ) + return [global_state] size = cast(int, size) if size > 0: try: @@ -1381,15 +1389,7 @@ class Instruction: return self._sload_helper(global_state, str(index)) storage_keys = global_state.environment.active_account.storage.keys() - keys = filter(keccak_function_manager.is_keccak, storage_keys) - addr = global_state.get_current_instruction()["address"] - keccak_keys = [ - key - for key in keys - if global_state.environment.active_account.storage.potential_func( - key, addr - ) - ] + keccak_keys = list(filter(keccak_function_manager.is_keccak, storage_keys)) results = [] # type: List[GlobalState] constraints = [] @@ -1427,16 +1427,11 @@ class Instruction: :param constraints: :return: """ - address = global_state.get_current_instruction()["address"] try: - data = global_state.environment.active_account.storage.get( - index, addr=address - ) + data = global_state.environment.active_account.storage[index] except KeyError: data = global_state.new_bitvec("storage_" + str(index), 256) - global_state.environment.active_account.storage.put( - key=index, value=data, addr=address - ) + global_state.environment.active_account.storage[index] = data if constraints is not None: global_state.mstate.constraints += constraints @@ -1480,15 +1475,7 @@ class Instruction: return self._sstore_helper(global_state, str(index), value) storage_keys = global_state.environment.active_account.storage.keys() - keccak_keys = list(filter(keccak_function_manager.is_keccak, storage_keys)) - addr = global_state.get_current_instruction()["address"] - keccak_keys = [ - key - for key in keccak_keys - if global_state.environment.active_account.storage.potential_func( - key, addr - ) - ] + keccak_keys = filter(keccak_function_manager.is_keccak, storage_keys) results = [] # type: List[GlobalState] new = symbol_factory.Bool(False) @@ -1541,18 +1528,19 @@ class Instruction: :param constraint: :return: """ - global_state.environment.active_account = deepcopy( - global_state.environment.active_account - ) - global_state.accounts[ - global_state.environment.active_account.address.value - ] = global_state.environment.active_account - address = global_state.get_current_instruction()["address"] - global_state.environment.active_account.storage.put( - key=index, - value=value if not isinstance(value, Expression) else simplify(value), - addr=address, - ) + try: + global_state.environment.active_account = deepcopy( + global_state.environment.active_account + ) + global_state.accounts[ + global_state.environment.active_account.address.value + ] = global_state.environment.active_account + + global_state.environment.active_account.storage[index] = ( + value if not isinstance(value, Expression) else simplify(value) + ) + except KeyError: + log.debug("Error writing to storage: Invalid index") if constraint is not None: global_state.mstate.constraints.append(constraint) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 93b03831..e806a71e 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -24,12 +24,8 @@ class Storage: self.concrete = concrete self.dynld = dynamic_loader self.address = address - self._storage_opcodes = {} # type: Dict - def get(self, item: Union[str, int], addr: int) -> Any: - if item not in self._storage_opcodes: - self._storage_opcodes[item] = set() - self._storage_opcodes[item].add(addr) + def __getitem__(self, item: Union[str, int]) -> Any: try: return self._storage[item] except KeyError: @@ -60,17 +56,9 @@ class Storage: ) return self._storage[item] - def put(self, key: Union[int, str], value: Any, addr) -> None: - if key not in self._storage_opcodes: - self._storage_opcodes[key] = set() - self._storage_opcodes[key].add(addr) + def __setitem__(self, key: Union[int, str], value: Any) -> None: self._storage[key] = value - def potential_func(self, key, opcode) -> bool: - if key not in self._storage_opcodes: - return False - return opcode in self._storage_opcodes[key] - def keys(self) -> KeysView: """ diff --git a/tests/laser/evm_testsuite/evm_test.py b/tests/laser/evm_testsuite/evm_test.py index 93e927ed..381438cb 100644 --- a/tests/laser/evm_testsuite/evm_test.py +++ b/tests/laser/evm_testsuite/evm_test.py @@ -125,7 +125,7 @@ def test_vmtest( account.code = Disassembly(details["code"][2:]) account.nonce = int(details["nonce"], 16) for key, value in details["storage"].items(): - account.storage.put(int(key, 16), int(value, 16), 10) + account.storage[int(key, 16)] = int(value, 16) world_state.put_account(account) account.set_balance(int(details["balance"], 16)) @@ -175,7 +175,7 @@ def test_vmtest( for index, value in details["storage"].items(): expected = int(value, 16) - actual = account.storage.get(int(index, 16), 0) + actual = account.storage[int(index, 16)] if isinstance(actual, Expression): actual = actual.value diff --git a/tests/laser/state/storage_test.py b/tests/laser/state/storage_test.py index a0da2c94..35e31df3 100644 --- a/tests/laser/state/storage_test.py +++ b/tests/laser/state/storage_test.py @@ -12,7 +12,7 @@ def test_concrete_storage_uninitialized_index(initial_storage, key): storage._storage = initial_storage # Act - value = storage.get(key, 0) + value = storage[key] # Assert assert value == 0 @@ -25,7 +25,7 @@ def test_symbolic_storage_uninitialized_index(initial_storage, key): storage._storage = initial_storage # Act - value = storage.get(key, 0) + value = storage[key] # Assert assert isinstance(value, Expression) @@ -36,10 +36,10 @@ def test_storage_set_item(): storage = Storage() # Act - storage.put(key=1, value=13, addr=10) + storage[1] = 13 # Assert - assert storage.get(item=1, addr=10) == 13 + assert storage[1] == 13 def test_storage_change_item(): @@ -47,7 +47,7 @@ def test_storage_change_item(): storage = Storage() storage._storage = {1: 12} # Act - storage.put(key=1, value=14, addr=10) + storage[1] = 14 # Assert - assert storage.get(item=1, addr=10) == 14 + assert storage[1] == 14 diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.graph.html b/tests/testdata/outputs_expected/metacoin.sol.o.graph.html index 64169495..6ee81b9c 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.graph.html +++ b/tests/testdata/outputs_expected/metacoin.sol.o.graph.html @@ -24,8 +24,8 @@ @@ -59,4 +59,4 @@ }); - \ No newline at end of file + diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.json b/tests/testdata/outputs_expected/metacoin.sol.o.json index 2dd384d1..712f50c1 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.json +++ b/tests/testdata/outputs_expected/metacoin.sol.o.json @@ -1,19 +1,5 @@ { "error": null, - "issues": [ - { - "address": 498, - "contract": "Unknown", - "debug": "", - "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", - "function": "sendToken(address,uint256)", - "max_gas_used": 52806, - "min_gas_used": 11860, - "severity": "High", - "sourceMap": null, - "swc-id": "101", - "title": "Integer Overflow" - } - ], + "issues": [], "success": true } \ No newline at end of file diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 index 9c96d9d3..40de69b4 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/metacoin.sol.o.jsonv2 @@ -1,24 +1,6 @@ [ { - "issues": [ - { - "description": { - "head": "The binary addition can overflow.", - "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." - }, - "extra": { - "discoveryTime": "" - }, - "locations": [ - { - "sourceMap": "498:1:0" - } - ], - "severity": "High", - "swcID": "SWC-101", - "swcTitle": "Integer Overflow and Underflow" - } - ], + "issues": [], "meta": {}, "sourceFormat": "evm-byzantium-bytecode", "sourceList": [ diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.markdown b/tests/testdata/outputs_expected/metacoin.sol.o.markdown index 1eb30c6a..321484fd 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.markdown +++ b/tests/testdata/outputs_expected/metacoin.sol.o.markdown @@ -1,14 +1,3 @@ -# Analysis results for test-filename.sol +# Analysis results for None -## Integer Overflow -- SWC ID: 101 -- Severity: High -- Contract: Unknown -- Function name: `sendToken(address,uint256)` -- PC address: 498 -- Estimated Gas Usage: 11860 - 52806 - -### Description - -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. +The analysis was completed successfully. No issues were detected. diff --git a/tests/testdata/outputs_expected/metacoin.sol.o.text b/tests/testdata/outputs_expected/metacoin.sol.o.text index 412039a2..729320d8 100644 --- a/tests/testdata/outputs_expected/metacoin.sol.o.text +++ b/tests/testdata/outputs_expected/metacoin.sol.o.text @@ -1,11 +1 @@ -==== Integer Overflow ==== -SWC ID: 101 -Severity: High -Contract: Unknown -Function name: sendToken(address,uint256) -PC address: 498 -Estimated Gas Usage: 11860 - 52806 -The binary addition can overflow. -The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. --------------------- - +The analysis was completed successfully. No issues were detected. From 455e66c8356b005ce131d2f604a61b2e1858be2a Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 17 Jun 2019 15:19:49 +0530 Subject: [PATCH 090/108] Fix the calldata's size for symbolic calldata --- mythril/laser/ethereum/instructions.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index e1903c42..54b95b65 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -768,18 +768,8 @@ class Instruction: size_sym = True if size_sym: - state.mem_extend(mstart, 1) - state.memory[mstart] = global_state.new_bitvec( - "calldata_" - + str(environment.active_account.contract_name) - + "[" - + str(dstart) - + ": + " - + str(size) - + "]", - 8, - ) - return [global_state] + size = 320 # The excess size will get overwritten + size = cast(int, size) if size > 0: try: From 106cbfd43dce8cb75a0b177127c6e4e3b09fe1f4 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 17 Jun 2019 12:16:45 +0200 Subject: [PATCH 091/108] Soemthing --- .../laser/ethereum/plugins/implementations/dependency_pruner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 20036ed0..f32a2004 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -229,6 +229,7 @@ class DependencyPruner(LaserPlugin): @symbolic_vm.pre_hook("SLOAD") def sload_hook(state: GlobalState): + annotation = get_dependency_annotation(state) location = state.mstate.stack[-1] @@ -318,3 +319,4 @@ class DependencyPruner(LaserPlugin): annotation.storage_written[self.iteration], ) ) + From 1e07a32f0ac3eda147812d7f6267306260359e13 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 17 Jun 2019 13:00:30 +0200 Subject: [PATCH 092/108] Add FIXME for potential bug --- .../ethereum/plugins/implementations/dependency_pruner.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index f32a2004..b4820b86 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -78,6 +78,12 @@ def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation: if len(annotations) == 0: + """FIXME: Hack for carrying over state annotations from the STOP and RETURN states of + the previous states. The states are pushed on a stack in the world state annotation + and popped off the stack in the subsequent iteration. This might break if any + other strategy than bfs is used (?). + """ + try: world_state_annotation = get_ws_dependency_annotation(state) annotation = world_state_annotation.annotations_stack.pop() @@ -319,4 +325,3 @@ class DependencyPruner(LaserPlugin): annotation.storage_written[self.iteration], ) ) - From b463974b948e3bc981d2b390d43489df92a5e9bd Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Mon, 17 Jun 2019 13:02:33 +0200 Subject: [PATCH 093/108] Reverse the random commit from before --- .../laser/ethereum/plugins/implementations/dependency_pruner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index b4820b86..20523ac5 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -235,7 +235,6 @@ class DependencyPruner(LaserPlugin): @symbolic_vm.pre_hook("SLOAD") def sload_hook(state: GlobalState): - annotation = get_dependency_annotation(state) location = state.mstate.stack[-1] From 7f2d976a7d02b254780789fd66f942a8b8bb7be8 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 17 Jun 2019 18:32:54 +0530 Subject: [PATCH 094/108] Supports tx sequence lists (#1014) * Add the steps in transaction * Fix the account transfer and add initial state * Support for py35 * Support creation account * Support display of txSeed and remove saving world state from plugin * Add type hints to functions * Change the output jsonv2 name * Change report tests * Fix the addresses * Use caller over origin and remove extra statements * Add properties over functions * Use initial storage * Remove usage of tx_template * use tx accounts over laser svm * Remove unused imports * Fix based on the previous merge * Use a better initial state * Add creation transaction * Remove address 0 * Transaction list suggestion (#1044) * add documentation to added functions in report * dump dictionary to string before returning * remove balance write * separate transaction sequence generation into separate functions * apply style rules * fix typo * Fix some issues * Refactor with black * Update mythril/analysis/solver.py Co-Authored-By: JoranHonig * Update mythril/analysis/solver.py Co-Authored-By: JoranHonig * Update mythril/analysis/solver.py Co-Authored-By: JoranHonig * Update mythril/analysis/solver.py Co-Authored-By: JoranHonig * Update mythril/analysis/solver.py Co-Authored-By: JoranHonig * Fix the type hinting * Remove the caller field --- mythril/analysis/modules/delegatecall.py | 13 +- mythril/analysis/modules/ether_thief.py | 4 +- mythril/analysis/modules/exceptions.py | 3 +- mythril/analysis/modules/external_calls.py | 5 +- mythril/analysis/modules/integer.py | 3 +- mythril/analysis/modules/suicide.py | 3 +- mythril/analysis/report.py | 44 +++++- mythril/analysis/solver.py | 142 ++++++++++++------ mythril/analysis/symbolic.py | 37 ++++- .../templates/report_as_markdown.jinja2 | 4 +- .../analysis/templates/report_as_text.jinja2 | 4 +- mythril/laser/ethereum/instructions.py | 9 +- mythril/laser/ethereum/state/account.py | 3 + mythril/laser/ethereum/svm.py | 7 +- .../laser/ethereum/transaction/symbolic.py | 15 +- .../transaction/transaction_models.py | 36 ++++- mythril/laser/smt/bitvec.py | 7 + tests/report_test.py | 4 +- .../outputs_expected/calls.sol.o.json | 36 ++--- .../outputs_expected/calls.sol.o.jsonv2 | 27 ++-- .../outputs_expected/ether_send.sol.o.json | 8 +- .../outputs_expected/ether_send.sol.o.jsonv2 | 6 +- .../outputs_expected/exceptions.sol.o.json | 16 +- .../outputs_expected/exceptions.sol.o.jsonv2 | 12 +- .../kinds_of_calls.sol.o.json | 24 +-- .../kinds_of_calls.sol.o.jsonv2 | 18 ++- .../multi_contracts.sol.o.json | 4 +- .../multi_contracts.sol.o.jsonv2 | 3 +- .../outputs_expected/origin.sol.o.json | 4 +- .../outputs_expected/origin.sol.o.jsonv2 | 3 +- .../outputs_expected/overflow.sol.o.json | 14 +- .../outputs_expected/overflow.sol.o.jsonv2 | 11 +- .../outputs_expected/returnvalue.sol.o.json | 12 +- .../outputs_expected/returnvalue.sol.o.jsonv2 | 9 +- .../outputs_expected/suicide.sol.o.json | 4 +- .../outputs_expected/suicide.sol.o.jsonv2 | 3 +- .../outputs_expected/underflow.sol.o.json | 14 +- .../outputs_expected/underflow.sol.o.jsonv2 | 11 +- 38 files changed, 387 insertions(+), 195 deletions(-) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 71bc0fb4..9fbf9340 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -2,7 +2,7 @@ import json import logging from copy import copy -from typing import List, cast +from typing import List, cast, Dict from mythril.analysis import solver from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT @@ -27,7 +27,7 @@ class DelegateCallAnnotation(StateAnnotation): "retval_{}".format(call_state.get_current_instruction()["address"]), 256 ) - def get_issue(self, global_state: GlobalState, transaction_sequence: str) -> Issue: + def get_issue(self, global_state: GlobalState, transaction_sequence: Dict) -> Issue: """ Returns Issue for the annotation :param global_state: Global State @@ -58,7 +58,7 @@ class DelegateCallAnnotation(StateAnnotation): severity="Medium", description_head=description_head, description_tail=description_tail, - debug=transaction_sequence, + transaction_sequence=transaction_sequence, gas_used=( global_state.mstate.min_gas_used, global_state.mstate.max_gas_used, @@ -128,8 +128,11 @@ def _analyze_states(state: GlobalState) -> List[Issue]: transaction_sequence = solver.get_transaction_sequence( state, state.mstate.constraints + [annotation.return_value == 1] ) - debug = json.dumps(transaction_sequence, indent=4) - issues.append(annotation.get_issue(state, transaction_sequence=debug)) + issues.append( + annotation.get_issue( + state, transaction_sequence=transaction_sequence + ) + ) except UnsatError: continue diff --git a/mythril/analysis/modules/ether_thief.py b/mythril/analysis/modules/ether_thief.py index 3fa65aa7..d8b879d3 100644 --- a/mythril/analysis/modules/ether_thief.py +++ b/mythril/analysis/modules/ether_thief.py @@ -95,8 +95,6 @@ class EtherThief(DetectionModule): transaction_sequence = solver.get_transaction_sequence(state, constraints) - debug = json.dumps(transaction_sequence, indent=4) - issue = Issue( contract=state.environment.active_account.contract_name, function_name=state.environment.active_function_name, @@ -109,7 +107,7 @@ class EtherThief(DetectionModule): description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract" + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + " a vulnerability.", - debug=debug, + transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: diff --git a/mythril/analysis/modules/exceptions.py b/mythril/analysis/modules/exceptions.py index a6ad6f27..f589ba79 100644 --- a/mythril/analysis/modules/exceptions.py +++ b/mythril/analysis/modules/exceptions.py @@ -34,7 +34,6 @@ def _analyze_state(state) -> list: transaction_sequence = solver.get_transaction_sequence( state, state.mstate.constraints ) - debug = json.dumps(transaction_sequence, indent=4) issue = Issue( contract=state.environment.active_account.contract_name, @@ -46,7 +45,7 @@ def _analyze_state(state) -> list: description_head="A reachable exception has been detected.", description_tail=description_tail, bytecode=state.environment.code.bytecode, - debug=debug, + transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) return [issue] diff --git a/mythril/analysis/modules/external_calls.py b/mythril/analysis/modules/external_calls.py index 510dd82c..59f8e7bd 100644 --- a/mythril/analysis/modules/external_calls.py +++ b/mythril/analysis/modules/external_calls.py @@ -47,7 +47,6 @@ def _analyze_state(state): constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF] transaction_sequence = solver.get_transaction_sequence(state, constraints) - debug = json.dumps(transaction_sequence, indent=4) description_head = "A call to a user-supplied address is executed." description_tail = ( "The callee address of an external message call can be set by " @@ -66,7 +65,7 @@ def _analyze_state(state): severity="Medium", description_head=description_head, description_tail=description_tail, - debug=debug, + transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) @@ -95,7 +94,7 @@ def _analyze_state(state): severity="Low", description_head=description_head, description_tail=description_tail, - debug=debug, + transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 8b8a5d87..cbaf144d 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -330,10 +330,9 @@ class IntegerOverflowUnderflowModule(DetectionModule): description_head=self._get_description_head(annotation, _type), description_tail=self._get_description_tail(annotation, _type), gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), + transaction_sequence=transaction_sequence, ) - issue.debug = json.dumps(transaction_sequence, indent=4) - address = _get_address_from_state(ostate) self._overflow_cache[address] = True self._issues.append(issue) diff --git a/mythril/analysis/modules/suicide.py b/mythril/analysis/modules/suicide.py index 57c593e2..ee0f81ee 100644 --- a/mythril/analysis/modules/suicide.py +++ b/mythril/analysis/modules/suicide.py @@ -79,7 +79,6 @@ class SuicideModule(DetectionModule): ) description_tail = "Arbitrary senders can kill this contract." - debug = json.dumps(transaction_sequence, indent=4) self._cache_address[instruction["address"]] = True issue = Issue( @@ -92,7 +91,7 @@ class SuicideModule(DetectionModule): severity="High", description_head=description_head, description_tail=description_tail, - debug=debug, + transaction_sequence=transaction_sequence, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) return [issue] diff --git a/mythril/analysis/report.py b/mythril/analysis/report.py index e1f73887..86f2fbb8 100644 --- a/mythril/analysis/report.py +++ b/mythril/analysis/report.py @@ -31,7 +31,7 @@ class Issue: severity=None, description_head="", description_tail="", - debug="", + transaction_sequence=None, ): """ @@ -55,7 +55,6 @@ class Issue: self.description_tail = description_tail self.description = "%s\n%s" % (description_head, description_tail) self.severity = severity - self.debug = debug self.swc_id = swc_id self.min_gas_used, self.max_gas_used = gas_used self.filename = None @@ -64,6 +63,38 @@ class Issue: self.source_mapping = None self.discovery_time = time() - StartTime().global_start_time self.bytecode_hash = get_code_hash(bytecode) + self.transaction_sequence = transaction_sequence + + @property + def transaction_sequence_users(self): + """ Returns the transaction sequence in json without pre-generated block data""" + return ( + json.dumps(self.transaction_sequence, indent=4) + if self.transaction_sequence + else None + ) + + @property + def transaction_sequence_jsonv2(self): + """ Returns the transaction sequence in json with pre-generated block data""" + return ( + json.dumps(self.add_block_data(self.transaction_sequence), indent=4) + if self.transaction_sequence + else None + ) + + @staticmethod + def add_block_data(transaction_sequence: Dict): + """ Adds sane block data to a transaction_sequence """ + for step in transaction_sequence["steps"]: + step["gasLimit"] = "0x7d000" + step["gasPrice"] = "0x773594000" + step["blockCoinbase"] = "0xcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb" + step["blockDifficulty"] = "0xa7d7343662e26" + step["blockGasLimit"] = "0x7d0000" + step["blockNumber"] = "0x66e393" + step["blockTime"] = "0x5bfa4639" + return transaction_sequence @property def as_dict(self): @@ -79,7 +110,7 @@ class Issue: "function": self.function, "severity": self.severity, "address": self.address, - "debug": self.debug, + "tx_sequence": self.transaction_sequence_users, "min_gas_used": self.min_gas_used, "max_gas_used": self.max_gas_used, "sourceMap": self.source_mapping, @@ -168,6 +199,7 @@ class Report: """ name = self._file_name() template = Report.environment.get_template("report_as_text.jinja2") + return template.render( filename=name, issues=self.sorted_issues(), verbose=self.verbose ) @@ -203,7 +235,9 @@ class Report: title = SWC_TO_TITLE[issue.swc_id] except KeyError: title = "Unspecified Security Issue" - + extra = {"discoveryTime": int(issue.discovery_time * 10 ** 9)} + if issue.transaction_sequence_jsonv2: + extra["testCase"] = str(issue.transaction_sequence_jsonv2) _issues.append( { "swcID": "SWC-" + issue.swc_id, @@ -214,7 +248,7 @@ class Report: }, "severity": issue.severity, "locations": [{"sourceMap": "%d:1:%d" % (issue.address, idx)}], - "extra": {"discoveryTime": int(issue.discovery_time * 10 ** 9)}, + "extra": extra, } ) meta_data = self._get_exception_data() diff --git a/mythril/analysis/solver.py b/mythril/analysis/solver.py index 98d3633a..16a74eae 100644 --- a/mythril/analysis/solver.py +++ b/mythril/analysis/solver.py @@ -1,8 +1,13 @@ """This module contains analysis module helpers to solve path constraints.""" +from typing import Dict, List, Union from z3 import sat, unknown, FuncInterp import z3 -from mythril.laser.smt import simplify, UGE, Optimize, symbol_factory +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import Account +from mythril.laser.ethereum.state.constraints import Constraints +from mythril.laser.ethereum.transaction import BaseTransaction +from mythril.laser.smt import UGE, Optimize, symbol_factory from mythril.laser.ethereum.time_handler import time_handler from mythril.exceptions import UnsatError from mythril.laser.ethereum.transaction.transaction_models import ( @@ -51,7 +56,7 @@ def get_model(constraints, minimize=(), maximize=(), enforce_execution_time=True def pretty_print_model(model): - """ + """ Pretty prints a z3 model :param model: :return: @@ -74,7 +79,9 @@ def pretty_print_model(model): return ret -def get_transaction_sequence(global_state, constraints): +def get_transaction_sequence( + global_state: GlobalState, constraints: Constraints +) -> Dict: """Generate concrete transaction sequence. :param global_state: GlobalState to generate transaction sequence for @@ -83,55 +90,102 @@ def get_transaction_sequence(global_state, constraints): transaction_sequence = global_state.world_state.transaction_sequence - # gaslimit & gasprice don't exist yet - tx_template = { - "calldata": None, - "call_value": None, - "caller": "0xCA11EDEADBEEF37E636E6CA11EDEADBEEFCA11ED", - } + concrete_transactions = [] - concrete_transactions = {} - creation_tx_ids = [] - tx_constraints = constraints.copy() - minimize = [] + tx_constraints, minimize = _set_minimisation_constraints( + transaction_sequence, constraints.copy(), [], 5000 + ) + model = get_model(tx_constraints, minimize=minimize) - transactions = [] + min_price_dict = {} # type: Dict[str, int] for transaction in transaction_sequence: - tx_id = str(transaction.id) - if not isinstance(transaction, ContractCreationTransaction): - transactions.append(transaction) - # Constrain calldatasize - max_calldatasize = symbol_factory.BitVecVal(5000, 256) - tx_constraints.append( - UGE(max_calldatasize, transaction.call_data.calldatasize) - ) + concrete_transaction = _get_concrete_transaction(model, transaction) + concrete_transactions.append(concrete_transaction) + + caller = concrete_transaction["origin"] + value = int(concrete_transaction["value"], 16) + min_price_dict[caller] = min_price_dict.get(caller, 0) + value + + if isinstance(transaction_sequence[0], ContractCreationTransaction): + initial_accounts = transaction_sequence[0].prev_world_state.accounts + else: + initial_accounts = transaction_sequence[0].world_state.accounts + + concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict) + + steps = {"initialState": concrete_initial_state, "steps": concrete_transactions} + + return steps + + +def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]): + """ Gets a concrete state """ + accounts = {} + for address, account in initial_accounts.items(): + # Skip empty default account + + data = dict() # type: Dict[str, Union[int, str]] + data["nonce"] = account.nonce + data["code"] = account.code.bytecode + data["storage"] = str(account.storage) + data["balance"] = min_price_dict.get(address, 0) + accounts[hex(address)] = data + return accounts + + +def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction): + """ Gets a concrete transaction from a transaction and z3 model""" + # Get concrete values from transaction + address = hex(transaction.callee_account.address.value) + value = model.eval(transaction.call_value.raw, model_completion=True).as_long() + caller = "0x" + ( + "%x" % model.eval(transaction.caller.raw, model_completion=True).as_long() + ).zfill(40) + + if isinstance(transaction, ContractCreationTransaction): + address = "" + input_ = transaction.code.bytecode + else: + input_ = "".join( + [ + hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] + for b in transaction.call_data.concrete(model) + ] + ) - minimize.append(transaction.call_data.calldatasize) - minimize.append(transaction.call_value) + # Create concrete transaction dict + concrete_transaction = dict() # type: Dict[str, str] + concrete_transaction["input"] = "0x" + input_ + concrete_transaction["value"] = "0x%x" % value + # Fixme: base origin assignment on origin symbol + concrete_transaction["origin"] = caller + concrete_transaction["address"] = "%s" % address - concrete_transactions[tx_id] = tx_template.copy() + return concrete_transaction - else: - creation_tx_ids.append(tx_id) - model = get_model(tx_constraints, minimize=minimize) +def _set_minimisation_constraints( + transaction_sequence, constraints, minimize, max_size +): + """ Set constraints that minimise key transaction values - for transaction in transactions: - tx_id = str(transaction.id) + Constraints generated: + - Upper bound on calldata size + - Minimisation of call value's and calldata sizes - concrete_transactions[tx_id]["calldata"] = "0x" + "".join( - [ - hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] - for b in transaction.call_data.concrete(model) - ] - ) + :param transaction_sequence: Transaction for which the constraints should be applied + :param constraints: The constraints array which should contain any added constraints + :param minimize: The minimisation array which should contain any variables that should be minimised + :param max_size: The max size of the calldata array + :return: updated constraints, minimize + """ + for transaction in transaction_sequence: + # Set upper bound on calldata size + max_calldata_size = symbol_factory.BitVecVal(max_size, 256) + constraints.append(UGE(max_calldata_size, transaction.call_data.calldatasize)) - concrete_transactions[tx_id]["call_value"] = ( - "0x%x" - % model.eval(transaction.call_value.raw, model_completion=True).as_long() - ) - concrete_transactions[tx_id]["caller"] = "0x" + ( - "%x" % model.eval(transaction.caller.raw, model_completion=True).as_long() - ).zfill(40) + # Minimize + minimize.append(transaction.call_data.calldatasize) + minimize.append(transaction.call_value) - return concrete_transactions + return constraints, minimize diff --git a/mythril/analysis/symbolic.py b/mythril/analysis/symbolic.py index 55ebf39d..6114b423 100644 --- a/mythril/analysis/symbolic.py +++ b/mythril/analysis/symbolic.py @@ -16,6 +16,16 @@ from mythril.laser.ethereum.strategy.basic import ( ReturnWeightedRandomStrategy, BasicSearchStrategy, ) + +from mythril.laser.ethereum.transaction.symbolic import ( + ATTACKER_ADDRESS, + CREATOR_ADDRESS, +) + + +from mythril.laser.ethereum.plugins.plugin_factory import PluginFactory +from mythril.laser.ethereum.plugins.plugin_loader import LaserPluginLoader + from mythril.laser.ethereum.strategy.extensions.bounded_loops import ( BoundedLoopsStrategy, ) @@ -76,9 +86,23 @@ class SymExecWrapper: else: raise ValueError("Invalid strategy argument supplied") + creator_account = Account( + hex(CREATOR_ADDRESS), "", dynamic_loader=dynloader, contract_name=None + ) + attacker_account = Account( + hex(ATTACKER_ADDRESS), "", dynamic_loader=dynloader, contract_name=None + ) + requires_statespace = ( compulsory_statespace or len(get_detection_modules("post", modules)) > 0 ) + if not contract.creation_code: + self.accounts = {hex(ATTACKER_ADDRESS): attacker_account} + else: + self.accounts = { + hex(CREATOR_ADDRESS): creator_account, + hex(ATTACKER_ADDRESS): attacker_account, + } self.laser = svm.LaserEVM( dynamic_loader=dynloader, @@ -98,6 +122,10 @@ class SymExecWrapper: plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) plugin_loader.load(PluginFactory.build_instruction_coverage_plugin()) + world_state = WorldState() + for account in self.accounts.values(): + world_state.put_account(account) + if run_analysis_modules: self.laser.register_hooks( hook_type="pre", @@ -110,11 +138,15 @@ class SymExecWrapper: if isinstance(contract, SolidityContract): self.laser.sym_exec( - creation_code=contract.creation_code, contract_name=contract.name + creation_code=contract.creation_code, + contract_name=contract.name, + world_state=world_state, ) elif isinstance(contract, EVMContract) and contract.creation_code: self.laser.sym_exec( - creation_code=contract.creation_code, contract_name=contract.name + creation_code=contract.creation_code, + contract_name=contract.name, + world_state=world_state, ) else: account = Account( @@ -124,7 +156,6 @@ class SymExecWrapper: contract_name=contract.name, concrete_storage=False, ) - world_state = WorldState() world_state.put_account(account) self.laser.sym_exec(world_state=world_state, target_address=address.value) diff --git a/mythril/analysis/templates/report_as_markdown.jinja2 b/mythril/analysis/templates/report_as_markdown.jinja2 index e6952535..289d1871 100644 --- a/mythril/analysis/templates/report_as_markdown.jinja2 +++ b/mythril/analysis/templates/report_as_markdown.jinja2 @@ -24,11 +24,11 @@ In file: {{ issue.filename }}:{{ issue.lineno }} {{ issue.code }} ``` {% endif %} -{% if verbose and issue.debug %} +{% if verbose and issue.tx_sequence %} -------------------- ### Debugging Information: -{{ issue.debug }} +{{ issue.tx_sequence }} {% endif %} {% endfor %} diff --git a/mythril/analysis/templates/report_as_text.jinja2 b/mythril/analysis/templates/report_as_text.jinja2 index 08edb6cb..da962583 100644 --- a/mythril/analysis/templates/report_as_text.jinja2 +++ b/mythril/analysis/templates/report_as_text.jinja2 @@ -18,11 +18,11 @@ In file: {{ issue.filename }}:{{ issue.lineno }} -------------------- {% endif %} -{% if verbose and issue.debug %} +{% if verbose and issue.tx_sequence %} -------------------- Transaction Sequence: -{{ issue.debug }} +{{ issue.tx_sequence }} {% endif %} {% endfor %} diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index 54b95b65..d2e75b56 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1842,6 +1842,14 @@ class Instruction: callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader, True ) + + if callee_account is not None and callee_account.code.bytecode == "": + log.debug("The call is related to ether transfer between accounts") + global_state.mstate.stack.append( + global_state.new_bitvec("retval_" + str(instr["address"]), 256) + ) + return [global_state] + except ValueError as e: log.debug( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( @@ -1859,7 +1867,6 @@ class Instruction: ) if native_result: return native_result - transaction = MessageCallTransaction( world_state=global_state.world_state, gas_price=environment.gasprice, diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index e806a71e..c85726d5 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -73,6 +73,9 @@ class Storage: storage._storage = copy(self._storage) return storage + def __str__(self): + return str(self._storage) + class Account: """Account class representing ethereum accounts.""" diff --git a/mythril/laser/ethereum/svm.py b/mythril/laser/ethereum/svm.py index a13cf5b7..f47152c7 100644 --- a/mythril/laser/ethereum/svm.py +++ b/mythril/laser/ethereum/svm.py @@ -69,6 +69,7 @@ class LaserEVM: :param enable_iprof: Variable indicating whether instruction profiling should be turned on """ self.open_states = [] # type: List[WorldState] + self.total_states = 0 self.dynamic_loader = dynamic_loader @@ -125,7 +126,7 @@ class LaserEVM: :param creation_code The creation code to create the target contract in the symbolic environment :param contract_name The name that the created account should be associated with """ - pre_configuration_mode = world_state is not None and target_address is not None + pre_configuration_mode = target_address is not None scratch_mode = creation_code is not None and contract_name is not None if pre_configuration_mode == scratch_mode: raise ValueError("Symbolic execution started with invalid parameters") @@ -144,14 +145,16 @@ class LaserEVM: elif scratch_mode: log.info("Starting contract creation transaction") + created_account = execute_contract_creation( - self, creation_code, contract_name + self, creation_code, contract_name, world_state=world_state ) log.info( "Finished contract creation, found {} open states".format( len(self.open_states) ) ) + if len(self.open_states) == 0: log.warning( "No contract was created during the execution of contract creation " diff --git a/mythril/laser/ethereum/transaction/symbolic.py b/mythril/laser/ethereum/transaction/symbolic.py index 9a8ccc5d..1a895048 100644 --- a/mythril/laser/ethereum/transaction/symbolic.py +++ b/mythril/laser/ethereum/transaction/symbolic.py @@ -68,7 +68,7 @@ def execute_message_call(laser_evm, callee_address: BitVec) -> None: def execute_contract_creation( - laser_evm, contract_initialization_code, contract_name=None + laser_evm, contract_initialization_code, contract_name=None, world_state=None ) -> Account: """Executes a contract creation transaction from all open states. @@ -80,15 +80,9 @@ def execute_contract_creation( # TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here del laser_evm.open_states[:] - world_state = WorldState() + world_state = world_state or WorldState() open_states = [world_state] - new_account = world_state.create_account( - 0, concrete_storage=True, dynamic_loader=None, creator=CREATOR_ADDRESS - ) - - if contract_name: - new_account.contract_name = contract_name - + new_account = None for open_world_state in open_states: next_transaction_id = get_next_transaction_id() transaction = ContractCreationTransaction( @@ -103,13 +97,14 @@ def execute_contract_creation( ), code=Disassembly(contract_initialization_code), caller=symbol_factory.BitVecVal(CREATOR_ADDRESS, 256), - callee_account=new_account, + contract_name=contract_name, call_data=[], call_value=symbol_factory.BitVecSym( "call_value{}".format(next_transaction_id), 256 ), ) _setup_global_state_for_execution(laser_evm, transaction) + new_account = new_account or transaction.callee_account laser_evm.exec(True) return new_account diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index f5863c1c..93cc7e3b 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -2,6 +2,7 @@ execution.""" import array +from copy import deepcopy from z3 import ExprRef from typing import Union, Optional, cast @@ -161,12 +162,37 @@ class MessageCallTransaction(BaseTransaction): class ContractCreationTransaction(BaseTransaction): """Transaction object models an transaction.""" - def __init__(self, *args, **kwargs) -> None: - # Remove ignore after https://github.com/python/mypy/issues/4335 is fixed - super().__init__(*args, **kwargs, init_call_data=False) # type: ignore + def __init__( + self, + world_state: WorldState, + caller: ExprRef = None, + call_data=None, + identifier: Optional[str] = None, + gas_price=None, + gas_limit=None, + origin=None, + code=None, + call_value=None, + contract_name=None, + ) -> None: + self.prev_world_state = deepcopy(world_state) + callee_account = world_state.create_account( + 0, concrete_storage=True, creator=caller.value + ) + callee_account.contract_name = contract_name # TODO: set correct balance for new account - self.callee_account = self.callee_account or self.world_state.create_account( - 0, concrete_storage=True + super().__init__( + world_state=world_state, + callee_account=callee_account, + caller=caller, + call_data=call_data, + identifier=identifier, + gas_price=gas_price, + gas_limit=gas_limit, + origin=origin, + code=code, + call_value=call_value, + init_call_data=False, ) def initial_global_state(self) -> GlobalState: diff --git a/mythril/laser/smt/bitvec.py b/mythril/laser/smt/bitvec.py index 05081137..1dd15191 100644 --- a/mythril/laser/smt/bitvec.py +++ b/mythril/laser/smt/bitvec.py @@ -243,6 +243,13 @@ class BitVec(Expression[z3.BitVecRef]): """ return self._handle_shift(other, rshift) + def __hash__(self) -> int: + """ + + :return: + """ + return self.raw.__hash__() + def _comparison_helper( a: BitVec, b: BitVec, operation: Callable, default_value: bool, inputs_equal: bool diff --git a/tests/report_test.py b/tests/report_test.py index e83f3d67..73554880 100644 --- a/tests/report_test.py +++ b/tests/report_test.py @@ -17,7 +17,8 @@ def _fix_path(text): def _fix_debug_data(json_str): read_json = json.loads(json_str) for issue in read_json["issues"]: - issue["debug"] = "" + issue["tx_sequence"] = "" + return json.dumps(read_json, sort_keys=True, indent=4) @@ -25,6 +26,7 @@ def _add_jsonv2_stubs(json_str): read_json = json.loads(json_str) for issue in read_json[0]["issues"]: issue["extra"]["discoveryTime"] = "" + issue["extra"]["testCase"] = "" return json.dumps(read_json, sort_keys=True, indent=4) diff --git a/tests/testdata/outputs_expected/calls.sol.o.json b/tests/testdata/outputs_expected/calls.sol.o.json index 93fce2b7..0219f575 100644 --- a/tests/testdata/outputs_expected/calls.sol.o.json +++ b/tests/testdata/outputs_expected/calls.sol.o.json @@ -4,7 +4,6 @@ { "address": 661, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "thisisfine()", "max_gas_used": 1254, @@ -12,12 +11,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 661, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "thisisfine()", "max_gas_used": 35972, @@ -25,12 +24,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" }, { "address": 779, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "callstoredaddress()", "max_gas_used": 1298, @@ -38,12 +37,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 779, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "callstoredaddress()", "max_gas_used": 36016, @@ -51,12 +50,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" }, { "address": 858, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "reentrancy()", "max_gas_used": 1320, @@ -64,12 +63,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 858, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "reentrancy()", "max_gas_used": 61052, @@ -77,12 +76,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" }, { "address": 869, "contract": "Unknown", - "debug": "", "description": "The contract account state is changed after an external call. \nConsider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities.", "function": "reentrancy()", "max_gas_used": null, @@ -90,12 +89,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "State change after external call" + "title": "State change after external call", + "tx_sequence": "" }, { "address": 912, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "calluseraddress(address)", "max_gas_used": 616, @@ -103,12 +102,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 912, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "calluseraddress(address)", "max_gas_used": 35336, @@ -116,7 +115,8 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/calls.sol.o.jsonv2 index 9acb8f18..9bab6f6a 100644 --- a/tests/testdata/outputs_expected/calls.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/calls.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -58,7 +61,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -75,7 +79,8 @@ "tail": "Consider that the called contract could re-enter the function before this state change takes place. This can lead to business logic vulnerabilities." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -92,7 +97,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -109,7 +115,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -126,7 +133,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -143,7 +151,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.json b/tests/testdata/outputs_expected/ether_send.sol.o.json index 3f7072fe..1d2e4a19 100644 --- a/tests/testdata/outputs_expected/ether_send.sol.o.json +++ b/tests/testdata/outputs_expected/ether_send.sol.o.json @@ -4,7 +4,6 @@ { "address": 722, "contract": "Unknown", - "debug": "", "description": "Anyone can withdraw ETH from the contract account.\nArbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability.", "function": "withdrawfunds()", "max_gas_used": 1749, @@ -12,12 +11,12 @@ "severity": "High", "sourceMap": null, "swc-id": "105", - "title": "Unprotected Ether Withdrawal" + "title": "Unprotected Ether Withdrawal", + "tx_sequence": "" }, { "address": 883, "contract": "Unknown", - "debug": "", "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", "function": "invest()", "max_gas_used": 26883, @@ -25,7 +24,8 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Overflow" + "title": "Integer Overflow", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 b/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 index a92e3c21..e848bd2f 100644 --- a/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/ether_send.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "Arbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.json b/tests/testdata/outputs_expected/exceptions.sol.o.json index e529a0ae..19030e55 100644 --- a/tests/testdata/outputs_expected/exceptions.sol.o.json +++ b/tests/testdata/outputs_expected/exceptions.sol.o.json @@ -4,7 +4,6 @@ { "address": 446, "contract": "Unknown", - "debug": "", "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", "function": "assert3(uint256)", "max_gas_used": 301, @@ -12,12 +11,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "110", - "title": "Exception State" + "title": "Exception State", + "tx_sequence": "" }, { "address": 484, "contract": "Unknown", - "debug": "", "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", "function": "arrayaccess(uint256)", "max_gas_used": 351, @@ -25,12 +24,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "110", - "title": "Exception State" + "title": "Exception State", + "tx_sequence": "" }, { "address": 506, "contract": "Unknown", - "debug": "", "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", "function": "divisionby0(uint256)", "max_gas_used": 367, @@ -38,12 +37,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "110", - "title": "Exception State" + "title": "Exception State", + "tx_sequence": "" }, { "address": 531, "contract": "Unknown", - "debug": "", "description": "A reachable exception has been detected.\nIt is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking.", "function": "assert1()", "max_gas_used": 363, @@ -51,7 +50,8 @@ "severity": "Low", "sourceMap": null, "swc-id": "110", - "title": "Exception State" + "title": "Exception State", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 b/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 index 032cfc01..43b6ca48 100644 --- a/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/exceptions.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -58,7 +61,8 @@ "tail": "It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit `assert()` should only be used to check invariants. Use `require()` for regular input checking." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json index 72ac1e67..c2ee1fd0 100644 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json +++ b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.json @@ -4,7 +4,6 @@ { "address": 618, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "_function_0x141f32ff", "max_gas_used": 35865, @@ -12,12 +11,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" }, { "address": 618, "contract": "Unknown", - "debug": "", "description": "Use of callcode is deprecated.\nThe callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead.", "function": "_function_0x141f32ff", "max_gas_used": 1141, @@ -25,12 +24,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "111", - "title": "Use of callcode" + "title": "Use of callcode", + "tx_sequence": "" }, { "address": 849, "contract": "Unknown", - "debug": "", "description": "The contract delegates execution to another contract with a user-supplied address.\nThe smart contract delegates execution to a user-supplied address. Note that callers can execute arbitrary contracts and that the callee contract can access the storage of the calling contract. ", "function": "_function_0x9b58bc26", "max_gas_used": 35928, @@ -38,12 +37,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "112", - "title": "Delegatecall Proxy To User-Supplied Address" + "title": "Delegatecall Proxy To User-Supplied Address", + "tx_sequence": "" }, { "address": 849, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "_function_0x9b58bc26", "max_gas_used": 35928, @@ -51,12 +50,12 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" }, { "address": 1038, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "_function_0xeea4c864", "max_gas_used": 1229, @@ -64,12 +63,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 1038, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "_function_0xeea4c864", "max_gas_used": 35953, @@ -77,7 +76,8 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 index cf80cc34..d4f5cf82 100644 --- a/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/kinds_of_calls.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The smart contract delegates execution to a user-supplied address. Note that callers can execute arbitrary contracts and that the callee contract can access the storage of the calling contract. " }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The callcode method executes code of another contract in the context of the caller account. Due to a bug in the implementation it does not persist sender and value over the call. It was therefore deprecated and may be removed in the future. Use the delegatecall method instead." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -58,7 +61,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -75,7 +79,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -92,7 +97,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.json b/tests/testdata/outputs_expected/multi_contracts.sol.o.json index 8ade4afb..cf2fd3af 100644 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.json +++ b/tests/testdata/outputs_expected/multi_contracts.sol.o.json @@ -4,7 +4,6 @@ { "address": 142, "contract": "Unknown", - "debug": "", "description": "Anyone can withdraw ETH from the contract account.\nArbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability.", "function": "transfer()", "max_gas_used": 467, @@ -12,7 +11,8 @@ "severity": "High", "sourceMap": null, "swc-id": "105", - "title": "Unprotected Ether Withdrawal" + "title": "Unprotected Ether Withdrawal", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 b/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 index 21672449..ec36d8ca 100644 --- a/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/multi_contracts.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "Arbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/origin.sol.o.json b/tests/testdata/outputs_expected/origin.sol.o.json index d9c74bc1..6d79baf7 100644 --- a/tests/testdata/outputs_expected/origin.sol.o.json +++ b/tests/testdata/outputs_expected/origin.sol.o.json @@ -4,7 +4,6 @@ { "address": 317, "contract": "Unknown", - "debug": "", "description": "Use of tx.origin is deprecated.\nThe smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin", "function": "transferOwnership(address)", "max_gas_used": 1051, @@ -12,7 +11,8 @@ "severity": "Medium", "sourceMap": null, "swc-id": "111", - "title": "Use of tx.origin" + "title": "Use of tx.origin", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 b/tests/testdata/outputs_expected/origin.sol.o.jsonv2 index 27322fde..ec679550 100644 --- a/tests/testdata/outputs_expected/origin.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/origin.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The smart contract retrieves the transaction origin (tx.origin) using msg.origin. Use of msg.origin is deprecated and the instruction may be removed in the future. Use msg.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin" }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/overflow.sol.o.json b/tests/testdata/outputs_expected/overflow.sol.o.json index c8d029f7..16a2253b 100644 --- a/tests/testdata/outputs_expected/overflow.sol.o.json +++ b/tests/testdata/outputs_expected/overflow.sol.o.json @@ -4,7 +4,6 @@ { "address": 567, "contract": "Unknown", - "debug": "", "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 78155, @@ -12,12 +11,12 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Underflow" + "title": "Integer Underflow", + "tx_sequence": "" }, { "address": 649, "contract": "Unknown", - "debug": "", "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 78155, @@ -25,12 +24,12 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Underflow" + "title": "Integer Underflow", + "tx_sequence": "" }, { "address": 725, "contract": "Unknown", - "debug": "", "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 78155, @@ -38,8 +37,9 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Overflow" + "title": "Integer Overflow", + "tx_sequence": "" } ], "success": true -} \ No newline at end of file +} diff --git a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 index 570fdeba..53028f4a 100644 --- a/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/overflow.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -60,4 +63,4 @@ ], "sourceType": "raw-bytecode" } -] \ No newline at end of file +] diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.json b/tests/testdata/outputs_expected/returnvalue.sol.o.json index 1f01da4b..bd7c8a97 100644 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.json +++ b/tests/testdata/outputs_expected/returnvalue.sol.o.json @@ -4,7 +4,6 @@ { "address": 196, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "callchecked()", "max_gas_used": 1210, @@ -12,12 +11,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 285, "contract": "Unknown", - "debug": "", "description": "A call to a user-supplied address is executed.\nThe callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state.", "function": "callnotchecked()", "max_gas_used": 1232, @@ -25,12 +24,12 @@ "severity": "Medium", "sourceMap": null, "swc-id": "107", - "title": "External Call To User-Supplied Address" + "title": "External Call To User-Supplied Address", + "tx_sequence": "" }, { "address": 285, "contract": "Unknown", - "debug": "", "description": "The return value of a message call is not checked.\nExternal calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states.", "function": "callnotchecked()", "max_gas_used": 35950, @@ -38,7 +37,8 @@ "severity": "Low", "sourceMap": null, "swc-id": "104", - "title": "Unchecked Call Return Value" + "title": "Unchecked Call Return Value", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 b/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 index 9c245482..8e5bf428 100644 --- a/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/returnvalue.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The callee address of an external message call can be set by the caller. Note that the callee can contain arbitrary code and may re-enter any function in this contract. Review the business logic carefully to prevent averse effects on the contract state." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "External calls return a boolean value. If the callee contract halts with an exception, 'false' is returned and execution continues in the caller. It is usually recommended to wrap external calls into a require statement to prevent unexpected states." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/suicide.sol.o.json b/tests/testdata/outputs_expected/suicide.sol.o.json index 054f1981..1c98a444 100644 --- a/tests/testdata/outputs_expected/suicide.sol.o.json +++ b/tests/testdata/outputs_expected/suicide.sol.o.json @@ -4,7 +4,6 @@ { "address": 146, "contract": "Unknown", - "debug": "", "description": "The contract can be killed by anyone.\nAnyone can kill this contract and withdraw its balance to an arbitrary address.", "function": "kill(address)", "max_gas_used": 263, @@ -12,7 +11,8 @@ "severity": "High", "sourceMap": null, "swc-id": "106", - "title": "Unprotected Selfdestruct" + "title": "Unprotected Selfdestruct", + "tx_sequence": "" } ], "success": true diff --git a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 b/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 index c492c24c..30daf88a 100644 --- a/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/suicide.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "Anyone can kill this contract and withdraw its balance to an arbitrary address." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { diff --git a/tests/testdata/outputs_expected/underflow.sol.o.json b/tests/testdata/outputs_expected/underflow.sol.o.json index a0042598..416d1176 100644 --- a/tests/testdata/outputs_expected/underflow.sol.o.json +++ b/tests/testdata/outputs_expected/underflow.sol.o.json @@ -4,7 +4,6 @@ { "address": 567, "contract": "Unknown", - "debug": "", "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 52861, @@ -12,12 +11,12 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Underflow" + "title": "Integer Underflow", + "tx_sequence": "" }, { "address": 649, "contract": "Unknown", - "debug": "", "description": "The binary subtraction can underflow.\nThe operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 52861, @@ -25,12 +24,12 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Underflow" + "title": "Integer Underflow", + "tx_sequence": "" }, { "address": 725, "contract": "Unknown", - "debug": "", "description": "The binary addition can overflow.\nThe operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.", "function": "sendeth(address,uint256)", "max_gas_used": 52861, @@ -38,8 +37,9 @@ "severity": "High", "sourceMap": null, "swc-id": "101", - "title": "Integer Overflow" + "title": "Integer Overflow", + "tx_sequence": "" } ], "success": true -} \ No newline at end of file +} diff --git a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 b/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 index b6611bdd..c99aae49 100644 --- a/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 +++ b/tests/testdata/outputs_expected/underflow.sol.o.jsonv2 @@ -7,7 +7,8 @@ "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -24,7 +25,8 @@ "tail": "The operands of the subtraction operation are not sufficiently constrained. The subtraction could therefore result in an integer underflow. Prevent the underflow by checking inputs or ensure sure that the underflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -41,7 +43,8 @@ "tail": "The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion." }, "extra": { - "discoveryTime": "" + "discoveryTime": "", + "testCase": "" }, "locations": [ { @@ -60,4 +63,4 @@ ], "sourceType": "raw-bytecode" } -] \ No newline at end of file +] From 01b221f914e522b11ab658412d19921961bb4710 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Tue, 18 Jun 2019 12:21:45 +0600 Subject: [PATCH 095/108] Pass list of files to ignore false positives --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9937aa62..0ea11528 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -112,7 +112,7 @@ jobs: --plugin-dir /opt/mythril \ --s3 \ --circle-ci CircleCI/mythril.csv\ - --ignore-false-positives $IGNORE_FALSE_POSITIVES + --ignore-false-positives guess_the_random_number_fixed.sol simple_dao.sol old_blockhash.sol pypi_release: <<: *defaults From 9b960a298501b786b5fb4903d960d847274dcae6 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Tue, 18 Jun 2019 17:45:42 +0200 Subject: [PATCH 096/108] Add missing line to fix recursive intializaton :) --- mythril/analysis/modules/delegatecall.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mythril/analysis/modules/delegatecall.py b/mythril/analysis/modules/delegatecall.py index 8404b21a..52b656ee 100644 --- a/mythril/analysis/modules/delegatecall.py +++ b/mythril/analysis/modules/delegatecall.py @@ -32,6 +32,7 @@ class DelegateCallAnnotation(StateAnnotation): "retval_{}".format(call_state.get_current_instruction()["address"]), 256 ) + def _copy__(self): return DelegateCallAnnotation(self.call_state, copy(self.constraints)) def get_issue(self, global_state: GlobalState, transaction_sequence: Dict) -> Issue: From 90af6dbd39f929573a91955781d36123ffa2ad18 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Wed, 19 Jun 2019 11:23:25 +0600 Subject: [PATCH 097/108] Update Circle CI config --- .circleci/config.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ea11528..c6c674be 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,8 +111,9 @@ jobs: --output-dir /opt/edelweiss \ --plugin-dir /opt/mythril \ --s3 \ - --circle-ci CircleCI/mythril.csv\ - --ignore-false-positives guess_the_random_number_fixed.sol simple_dao.sol old_blockhash.sol + --circle-ci CircleCI/mythril.csv \ + --ignore-false-positives $IGNORE_FALSE_POSITVES \ + --ignore-regressions $IGNORE_REGRESSIONS pypi_release: <<: *defaults From 9dc390d2db3180c598d4700b5ef60ea1de56c9f7 Mon Sep 17 00:00:00 2001 From: Aleksandr Sobolev Date: Wed, 19 Jun 2019 11:24:32 +0600 Subject: [PATCH 098/108] Fix typo in env name and remove branch from workflow --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c6c674be..29d89e06 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -112,7 +112,7 @@ jobs: --plugin-dir /opt/mythril \ --s3 \ --circle-ci CircleCI/mythril.csv \ - --ignore-false-positives $IGNORE_FALSE_POSITVES \ + --ignore-false-positives $IGNORE_FALSE_POSITIVES \ --ignore-regressions $IGNORE_REGRESSIONS pypi_release: @@ -170,7 +170,6 @@ workflows: only: - develop - master - - feature/ignore-regressions tags: only: /v[0-9]+(\.[0-9]+)*/ requires: From 8843d7d0da35e053b623b8df95264f8a2cf0b7fb Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 19 Jun 2019 11:27:47 +0200 Subject: [PATCH 099/108] Handle z3 exceptions when updating dependency map --- .../implementations/dependency_pruner.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py index 20523ac5..267abb99 100644 --- a/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py +++ b/mythril/laser/ethereum/plugins/implementations/dependency_pruner.py @@ -7,6 +7,7 @@ from mythril.laser.ethereum.transaction.transaction_models import ( ContractCreationTransaction, ) from mythril.exceptions import UnsatError +from z3.z3types import Z3Exception from mythril.analysis import solver from typing import cast, List, Dict, Set from copy import copy @@ -145,13 +146,16 @@ class DependencyPruner(LaserPlugin): :param target_location """ - for address in path: - - if address in self.dependency_map: - if target_location not in self.dependency_map[address]: - self.dependency_map[address].append(target_location) - else: - self.dependency_map[address] = [target_location] + try: + for address in path: + if address in self.dependency_map: + if target_location not in self.dependency_map[address]: + self.dependency_map[address].append(target_location) + else: + self.dependency_map[address] = [target_location] + except Z3Exception as e: + # This should not happen unless there's a bug in laser, such as an invalid type being generated. + log.debug("Error updating dependency map: {}".format(e)) def protect_path(self, path: List[int]) -> None: """Prevent an execution path of being pruned. From 6a271f6cc8a602aaabdde02376970eb55bb980f5 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 19 Jun 2019 13:09:57 +0200 Subject: [PATCH 100/108] Bump version --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 0917f2ac..03a1afc5 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.20.9" +__version__ = "v0.21.0" From 25dd625e5c6c18b0a697130a9cf6fa88028abcdb Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 19 Jun 2019 16:30:19 +0200 Subject: [PATCH 101/108] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 29d89e06..9770236d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -107,7 +107,7 @@ jobs: -e CIRCLE_BUILD_URL=$CIRCLE_BUILD_URL \ -e CIRCLE_WEBHOOK_URL=$CIRCLE_WEBHOOK_URL \ --rm edelweiss-mythril:latest \ - --timeout 90 \ + --timeout 30 \ --output-dir /opt/edelweiss \ --plugin-dir /opt/mythril \ --s3 \ From 9a3bf665cc8cf67d7d6ae581560941517242cd0c Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 19 Jun 2019 17:12:00 +0200 Subject: [PATCH 102/108] Update __version__.py --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 03a1afc5..98d54729 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.0" +__version__ = "v0.21.2" From d86468c830cd19e1f510288627ed4e18016844b1 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Wed, 19 Jun 2019 20:58:54 +0200 Subject: [PATCH 103/108] Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 58931527..d76f546e 100755 --- a/setup.py +++ b/setup.py @@ -94,9 +94,9 @@ class VerifyVersionCommand(install): """""" tag = os.getenv("CIRCLE_TAG") - if tag != VERSION: + if tag != about["__version__"]: info = "Git tag: {0} does not match the version of this app: {1}".format( - tag, VERSION + tag, about["__version__"] ) sys.exit(info) From ef16ac834ec039b2fd44f5b7af698e661a1f43cd Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Jun 2019 12:19:27 -0700 Subject: [PATCH 104/108] Fix lookup of on-chain storage --- mythril/laser/ethereum/call.py | 2 +- mythril/laser/ethereum/state/account.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mythril/laser/ethereum/call.py b/mythril/laser/ethereum/call.py index 08ef8773..945a83a7 100644 --- a/mythril/laser/ethereum/call.py +++ b/mythril/laser/ethereum/call.py @@ -96,7 +96,7 @@ def get_callee_address( # attempt to read the contract address from instance storage try: callee_address = dynamic_loader.read_storage( - str(hex(environment.active_account.address.value)), index + hex(environment.active_account.address.value), index ) # TODO: verify whether this happens or not except: diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index c85726d5..99feb70f 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -38,7 +38,7 @@ class Storage: self._storage[item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( - contract_address=self.address, index=int(item) + contract_address=hex(self.address.value), index=int(item) ), 16, ), From 49ef7482ba69b532ebdde221b10904cb1d61a644 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 19 Jun 2019 12:27:40 -0700 Subject: [PATCH 105/108] Apply black --- mythril/laser/ethereum/state/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mythril/laser/ethereum/state/account.py b/mythril/laser/ethereum/state/account.py index 99feb70f..191f0641 100644 --- a/mythril/laser/ethereum/state/account.py +++ b/mythril/laser/ethereum/state/account.py @@ -38,7 +38,8 @@ class Storage: self._storage[item] = symbol_factory.BitVecVal( int( self.dynld.read_storage( - contract_address=hex(self.address.value), index=int(item) + contract_address=hex(self.address.value), + index=int(item), ), 16, ), From e5b45060d28371aac1b7abd16d4a977e9d2f2403 Mon Sep 17 00:00:00 2001 From: Bernhard Mueller Date: Fri, 21 Jun 2019 17:57:07 +0200 Subject: [PATCH 106/108] Update __version__.py --- mythril/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mythril/__version__.py b/mythril/__version__.py index 98d54729..45907f40 100644 --- a/mythril/__version__.py +++ b/mythril/__version__.py @@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well as for importing into Python. """ -__version__ = "v0.21.2" +__version__ = "v0.21.3" From 5252a05524353805f2ec8a812829900db894b5a4 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Sat, 22 Jun 2019 01:27:21 +0530 Subject: [PATCH 107/108] Implement EXTCODECOPY (#928) * Implement EXTCODECOPY * Cache the code received from Dynld * Reuse code_copy_helper return value * Add tests to cover the extcodecopy cases * Shift the addr function to WorldState * Refactor with black * Change based on recent PRs * Add docstrings * Shorten the code --- mythril/laser/ethereum/instructions.py | 120 ++++++++++++------ mythril/laser/ethereum/state/world_state.py | 22 ++++ .../world_state_account_exist_load_test.py | 50 ++++++++ 3 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 tests/laser/state/world_state_account_exist_load_test.py diff --git a/mythril/laser/ethereum/instructions.py b/mythril/laser/ethereum/instructions.py index d2e75b56..c084be0b 100644 --- a/mythril/laser/ethereum/instructions.py +++ b/mythril/laser/ethereum/instructions.py @@ -1009,16 +1009,61 @@ class Instruction: global_state.mstate.stack.pop(), global_state.mstate.stack.pop(), ) + return self._code_copy_helper( + code=global_state.environment.code.bytecode, + memory_offset=memory_offset, + code_offset=code_offset, + size=size, + op="CODECOPY", + global_state=global_state, + ) + + @StateTransition() + def extcodesize_(self, global_state: GlobalState) -> List[GlobalState]: + """ + :param global_state: + :return: + """ + state = global_state.mstate + addr = state.stack.pop() + try: + addr = hex(helper.get_concrete_int(addr)) + except TypeError: + log.debug("unsupported symbolic address for EXTCODESIZE") + state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256)) + return [global_state] + try: + code = global_state.world_state.accounts_exist_or_load( + addr, self.dynamic_loader + ) + except (ValueError, AttributeError) as e: + log.debug("error accessing contract storage due to: " + str(e)) + state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256)) + return [global_state] + + state.stack.append(len(code) // 2) + + return [global_state] + + @staticmethod + def _code_copy_helper( + code: str, + memory_offset: BitVec, + code_offset: BitVec, + size: BitVec, + op: str, + global_state: GlobalState, + ) -> List[GlobalState]: try: concrete_memory_offset = helper.get_concrete_int(memory_offset) except TypeError: - log.debug("Unsupported symbolic memory offset in CODECOPY") + log.debug("Unsupported symbolic memory offset in {}".format(op)) return [global_state] try: - size = helper.get_concrete_int(size) - global_state.mstate.mem_extend(concrete_memory_offset, size) + concrete_size = helper.get_concrete_int(size) + global_state.mstate.mem_extend(concrete_memory_offset, concrete_size) except TypeError: # except both attribute error and Exception @@ -1036,9 +1081,9 @@ class Instruction: try: concrete_code_offset = helper.get_concrete_int(code_offset) except TypeError: - log.debug("Unsupported symbolic code offset in CODECOPY") - global_state.mstate.mem_extend(concrete_memory_offset, size) - for i in range(size): + log.debug("Unsupported symbolic code offset in {}".format(op)) + global_state.mstate.mem_extend(concrete_memory_offset, concrete_size) + for i in range(concrete_size): global_state.mstate.memory[ concrete_memory_offset + i ] = global_state.new_bitvec( @@ -1049,21 +1094,20 @@ class Instruction: ) return [global_state] - bytecode = global_state.environment.code.bytecode - if bytecode[0:2] == "0x": - bytecode = bytecode[2:] + if code[0:2] == "0x": + code = code[2:] - if size == 0 and isinstance( + if concrete_size == 0 and isinstance( global_state.current_transaction, ContractCreationTransaction ): - if concrete_code_offset >= len(bytecode) // 2: - self._handle_symbolic_args(global_state, concrete_memory_offset) + if concrete_code_offset >= len(code) // 2: + Instruction._handle_symbolic_args(global_state, concrete_memory_offset) return [global_state] - for i in range(size): - if 2 * (concrete_code_offset + i + 1) <= len(bytecode): + for i in range(concrete_size): + if 2 * (concrete_code_offset + i + 1) <= len(code): global_state.mstate.memory[concrete_memory_offset + i] = int( - bytecode[ + code[ 2 * (concrete_code_offset + i) : 2 * (concrete_code_offset + i + 1) @@ -1083,35 +1127,41 @@ class Instruction: return [global_state] @StateTransition() - def extcodesize_(self, global_state: GlobalState) -> List[GlobalState]: + def extcodecopy_(self, global_state: GlobalState) -> List[GlobalState]: """ :param global_state: :return: """ state = global_state.mstate - addr = state.stack.pop() - environment = global_state.environment + addr, memory_offset, code_offset, size = ( + state.stack.pop(), + state.stack.pop(), + state.stack.pop(), + state.stack.pop(), + ) try: addr = hex(helper.get_concrete_int(addr)) except TypeError: - log.debug("unsupported symbolic address for EXTCODESIZE") - state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256)) + log.debug("unsupported symbolic address for EXTCODECOPY") return [global_state] try: - code = self.dynamic_loader.dynld(addr) + code = global_state.world_state.accounts_exist_or_load( + addr, self.dynamic_loader + ) except (ValueError, AttributeError) as e: log.debug("error accessing contract storage due to: " + str(e)) - state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256)) return [global_state] - if code is None: - state.stack.append(0) - else: - state.stack.append(len(code.bytecode) // 2) - - return [global_state] + return self._code_copy_helper( + code=code, + memory_offset=memory_offset, + code_offset=code_offset, + size=size, + op="EXTCODECOPY", + global_state=global_state, + ) @StateTransition def extcodehash_(self, global_state: GlobalState) -> List[GlobalState]: @@ -1127,20 +1177,6 @@ class Instruction: ) return [global_state] - @StateTransition() - def extcodecopy_(self, global_state: GlobalState) -> List[GlobalState]: - """ - - :param global_state: - :return: - """ - # FIXME: not implemented - state = global_state.mstate - addr = state.stack.pop() - start, s2, size = state.stack.pop(), state.stack.pop(), state.stack.pop() - - return [global_state] - @StateTransition() def returndatacopy_(self, global_state: GlobalState) -> List[GlobalState]: """ diff --git a/mythril/laser/ethereum/state/world_state.py b/mythril/laser/ethereum/state/world_state.py index ba8611d0..236da526 100644 --- a/mythril/laser/ethereum/state/world_state.py +++ b/mythril/laser/ethereum/state/world_state.py @@ -3,6 +3,7 @@ from copy import copy from random import randint from typing import Dict, List, Iterator, Optional, TYPE_CHECKING +from mythril.support.loader import DynLoader from mythril.laser.smt import symbol_factory, Array, BitVec from ethereum.utils import mk_contract_address from mythril.laser.ethereum.state.account import Account @@ -64,6 +65,27 @@ class WorldState: new_world_state.node = self.node return new_world_state + def accounts_exist_or_load(self, addr: str, dynamic_loader: DynLoader) -> str: + """ + returns account if it exists, else it loads from the dynamic loader + :param addr: address + :param dynamic_loader: Dynamic Loader + :return: The code + """ + addr_bitvec = symbol_factory.BitVecVal(int(addr, 16), 256) + if addr_bitvec.value in self.accounts: + code = self.accounts[addr_bitvec.value].code + else: + code = dynamic_loader.dynld(addr) + self.create_account( + balance=0, address=addr_bitvec.value, dynamic_loader=dynamic_loader + ) + if code is None: + code = "" + else: + code = code.bytecode + return code + def create_account( self, balance=0, diff --git a/tests/laser/state/world_state_account_exist_load_test.py b/tests/laser/state/world_state_account_exist_load_test.py new file mode 100644 index 00000000..c201a794 --- /dev/null +++ b/tests/laser/state/world_state_account_exist_load_test.py @@ -0,0 +1,50 @@ +import pytest + +from mythril.disassembler.disassembly import Disassembly +from mythril.laser.ethereum.state.environment import Environment +from mythril.laser.ethereum.state.account import Account +from mythril.laser.ethereum.state.machine_state import MachineState +from mythril.laser.ethereum.state.global_state import GlobalState +from mythril.laser.ethereum.state.world_state import WorldState +from mythril.support.loader import DynLoader +from mythril.ethereum.interface.rpc.client import EthJsonRpc +from mythril.laser.ethereum.instructions import Instruction + + +def _get_global_state(): + active_account = Account("0x0", code=Disassembly("60606040")) + passive_account = Account( + "0x325345346564645654645", code=Disassembly("6060604061626364") + ) + environment = Environment(active_account, None, None, None, None, None) + world_state = WorldState() + world_state.put_account(active_account) + world_state.put_account(passive_account) + return GlobalState(world_state, environment, None, MachineState(gas_limit=8000000)) + + +@pytest.mark.parametrize( + "addr, eth, code_len", + [ + ( + "0xb09C477eCDAd49DD5Ac26c2C64914C3a6693843a", + EthJsonRpc("rinkeby.infura.io", 443, True), + 1548, + ), + ( + "0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4", + EthJsonRpc("mainnet.infura.io", 443, True), + 0, + ), + ( + "0x325345346564645654645", + EthJsonRpc("mainnet.infura.io", 443, True), + 16, + ), # This contract tests Address Cache + ], +) +def test_extraction(addr, eth, code_len): + global_state = _get_global_state() + dynamic_loader = DynLoader(eth=eth) + code = global_state.world_state.accounts_exist_or_load(addr, dynamic_loader) + assert len(code) == code_len From ae26b1462d8144e6903faf4867761ace0f621b73 Mon Sep 17 00:00:00 2001 From: Nikhil Parasaram Date: Mon, 24 Jun 2019 23:48:13 +0530 Subject: [PATCH 108/108] Refactor cli (#1033) * Refactor cli * enhance cli * Re-Refactor the cli structure for more flexibility * Add cli tests for error * Move the cmd_line_test to the previous directory * Add documentation * Add more tests and change docs * Add more tests for storage slots and execution * support a for analyze and add more tests * Improve cli interface * Fix previous errors * Refactor with black * Add help command * Add new tests, fix an edge case and improve code and help messages --- mythril/interfaces/cli.py | 570 ++++++++++++++++--------- tests/{ => cli_tests}/test_cli_opts.py | 4 +- tests/cmd_line_test.py | 58 ++- 3 files changed, 410 insertions(+), 222 deletions(-) rename tests/{ => cli_tests}/test_cli_opts.py (88%) diff --git a/mythril/interfaces/cli.py b/mythril/interfaces/cli.py index 4c9d4222..cf8a2e08 100644 --- a/mythril/interfaces/cli.py +++ b/mythril/interfaces/cli.py @@ -15,6 +15,7 @@ import coloredlogs import traceback import mythril.support.signatures as sigs +from argparse import ArgumentParser, Namespace from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.mythril import ( MythrilAnalyzer, @@ -24,13 +25,31 @@ from mythril.mythril import ( ) from mythril.__version__ import __version__ as VERSION +ANALYZE_LIST = ("analyze", "a") +DISASSEMBLE_LIST = ("disassemble", "d") + log = logging.getLogger(__name__) +COMMAND_LIST = ( + ANALYZE_LIST + + DISASSEMBLE_LIST + + ( + "read-storage", + "leveldb-search", + "function-to-hash", + "hash-to-address", + "version", + "truffle", + "help", + ) +) + def exit_with_error(format_, message): """ - :param format_: - :param message: + Exits with error + :param format_: The format of the message + :param message: message """ if format_ == "text" or format_ == "markdown": log.error(message) @@ -53,94 +72,46 @@ def exit_with_error(format_, message): sys.exit() -def main() -> None: - """The main CLI interface entry point.""" - parser = argparse.ArgumentParser( - description="Security analysis of Ethereum smart contracts" - ) - create_parser(parser) - - # Get config values - - args = parser.parse_args() - parse_args(parser=parser, args=args) - - -def create_parser(parser: argparse.ArgumentParser) -> None: +def get_input_parser() -> ArgumentParser: """ - Creates the parser by setting all the possible arguments - :param parser: The parser + Returns Parser which handles input + :return: Parser which handles input """ - parser.add_argument("solidity_file", nargs="*") - - commands = parser.add_argument_group("commands") - commands.add_argument("-g", "--graph", help="generate a control flow graph") - commands.add_argument( - "-V", - "--version", - action="store_true", - help="print the Mythril version number and exit", - ) - commands.add_argument( - "-x", - "--fire-lasers", - action="store_true", - help="detect vulnerabilities, use with -c, -a or solidity file(s)", - ) - commands.add_argument( - "--truffle", - action="store_true", - help="analyze a truffle project (run from project dir)", - ) - commands.add_argument( - "-d", "--disassemble", action="store_true", help="print disassembly" - ) - commands.add_argument( - "-j", - "--statespace-json", - help="dumps the statespace json", - metavar="OUTPUT_FILE", - ) - - inputs = parser.add_argument_group("input arguments") - inputs.add_argument( + parser = ArgumentParser(add_help=False) + parser.add_argument( "-c", "--code", help='hex-encoded bytecode string ("6060604052...")', metavar="BYTECODE", ) - inputs.add_argument( + parser.add_argument( "-f", "--codefile", help="file containing hex-encoded bytecode string", metavar="BYTECODEFILE", type=argparse.FileType("r"), ) - inputs.add_argument( + parser.add_argument( "-a", "--address", help="pull contract from the blockchain", metavar="CONTRACT_ADDRESS", ) - inputs.add_argument( - "-l", - "--dynld", - action="store_true", - help="auto-load dependencies from the blockchain", - ) - inputs.add_argument( - "--no-onchain-storage-access", - action="store_true", - help="turns off getting the data from onchain contracts", - ) - inputs.add_argument( + parser.add_argument( "--bin-runtime", action="store_true", help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.", ) + return parser + - outputs = parser.add_argument_group("output formats") - outputs.add_argument( +def get_output_parser() -> ArgumentParser: + """ + Get parser which handles output + :return: Parser which handles output + """ + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( "-o", "--outform", choices=["text", "markdown", "json", "jsonv2"], @@ -148,43 +119,199 @@ def create_parser(parser: argparse.ArgumentParser) -> None: help="report output format", metavar="", ) - outputs.add_argument( + parser.add_argument( "--verbose-report", action="store_true", help="Include debugging information in report", ) + return parser + + +def get_rpc_parser() -> ArgumentParser: + """ + Get parser which handles RPC flags + :return: Parser which handles rpc inputs + """ + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument( + "--rpc", + help="custom RPC settings", + metavar="HOST:PORT / ganache / infura-[network_name]", + default="infura-mainnet", + ) + parser.add_argument( + "--rpctls", type=bool, default=False, help="RPC connection over TLS" + ) + return parser + + +def get_utilities_parser() -> ArgumentParser: + """ + Get parser which handles utilities flags + :return: Parser which handles utility flags + """ + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--solc-args", help="Extra arguments for solc") + parser.add_argument( + "--solv", + help="specify solidity compiler version. If not present, will try to install it (Experimental)", + metavar="SOLV", + ) + return parser + + +def main() -> None: + """The main CLI interface entry point.""" + + rpc_parser = get_rpc_parser() + utilities_parser = get_utilities_parser() + input_parser = get_input_parser() + output_parser = get_output_parser() + parser = argparse.ArgumentParser( + description="Security analysis of Ethereum smart contracts" + ) + parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) + parser.add_argument( + "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + ) + + subparsers = parser.add_subparsers(dest="command", help="Commands") + analyzer_parser = subparsers.add_parser( + ANALYZE_LIST[0], + help="Triggers the analysis of the smart contract", + parents=[rpc_parser, utilities_parser, input_parser, output_parser], + aliases=ANALYZE_LIST[1:], + ) + create_analyzer_parser(analyzer_parser) + + disassemble_parser = subparsers.add_parser( + DISASSEMBLE_LIST[0], + help="Disassembles the smart contract", + aliases=DISASSEMBLE_LIST[1:], + parents=[rpc_parser, utilities_parser, input_parser], + ) + create_disassemble_parser(disassemble_parser) + + read_storage_parser = subparsers.add_parser( + "read-storage", + help="Retrieves storage slots from a given address through rpc", + parents=[rpc_parser], + ) + leveldb_search_parser = subparsers.add_parser( + "leveldb-search", help="Searches the code fragment in local leveldb" + ) + contract_func_to_hash = subparsers.add_parser( + "function-to-hash", help="Returns the hash signature of the function" + ) + contract_hash_to_addr = subparsers.add_parser( + "hash-to-address", + help="converts the hashes in the blockchain to ethereum address", + ) + subparsers.add_parser( + "version", parents=[output_parser], help="Outputs the version" + ) + create_read_storage_parser(read_storage_parser) + create_hash_to_addr_parser(contract_hash_to_addr) + create_func_to_hash_parser(contract_func_to_hash) + create_leveldb_parser(leveldb_search_parser) + + subparsers.add_parser("truffle", parents=[analyzer_parser], add_help=False) + subparsers.add_parser("help", add_help=False) + + # Get config values + + args = parser.parse_args() + parse_args_and_execute(parser=parser, args=args) + + +def create_disassemble_parser(parser: ArgumentParser): + """ + Modify parser to handle disassembly + :param parser: + :return: + """ + parser.add_argument("solidity_file", nargs="*") + + +def create_read_storage_parser(read_storage_parser: ArgumentParser): + """ + Modify parser to handle storage slots + :param read_storage_parser: + :return: + """ - database = parser.add_argument_group("local contracts database") - database.add_argument( - "-s", "--search", help="search the contract database", metavar="EXPRESSION" + read_storage_parser.add_argument( + "storage_slots", + help="read state variables from storage index", + metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", + ) + read_storage_parser.add_argument( + "address", help="contract address", metavar="ADDRESS" ) - database.add_argument( + + +def create_leveldb_parser(parser: ArgumentParser): + """ + Modify parser to handle leveldb-search + :param parser: + :return: + """ + parser.add_argument("search") + parser.add_argument( "--leveldb-dir", help="specify leveldb directory for search or direct access operations", metavar="LEVELDB_PATH", ) - utilities = parser.add_argument_group("utilities") - utilities.add_argument( - "--hash", help="calculate function signature hash", metavar="SIGNATURE" - ) - utilities.add_argument( - "--storage", - help="read state variables from storage index, use with -a", - metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]", + +def create_func_to_hash_parser(parser: ArgumentParser): + """ + Modify parser to handle func_to_hash command + :param parser: + :return: + """ + parser.add_argument( + "func_name", help="calculate function signature hash", metavar="SIGNATURE" ) - utilities.add_argument( - "--solv", - help="specify solidity compiler version. If not present, will try to install it (Experimental)", - metavar="SOLV", + + +def create_hash_to_addr_parser(hash_parser: ArgumentParser): + """ + Modify parser to handle hash_to_addr command + :param hash_parser: + :return: + """ + hash_parser.add_argument( + "hash", help="Find the address from hash", metavar="FUNCTION_NAME" ) - utilities.add_argument( - "--contract-hash-to-address", - help="returns corresponding address for a contract address hash", - metavar="SHA3_TO_LOOK_FOR", + hash_parser.add_argument( + "--leveldb-dir", + help="specify leveldb directory for search or direct access operations", + metavar="LEVELDB_PATH", ) - options = parser.add_argument_group("options") + +def create_analyzer_parser(analyzer_parser: ArgumentParser): + """ + Modify parser to handle analyze command + :param analyzer_parser: + :return: + """ + analyzer_parser.add_argument("solidity_file", nargs="*") + commands = analyzer_parser.add_argument_group("commands") + commands.add_argument("-g", "--graph", help="generate a control flow graph") + commands.add_argument( + "-j", + "--statespace-json", + help="dumps the statespace json", + metavar="OUTPUT_FILE", + ) + commands.add_argument( + "--truffle", + action="store_true", + help="analyze a truffle project (run from project dir)", + ) + options = analyzer_parser.add_argument_group("options") options.add_argument( "-m", "--modules", @@ -230,15 +357,23 @@ def create_parser(parser: argparse.ArgumentParser) -> None: default=10, help="The amount of seconds to spend on " "the initial contract creation", ) - options.add_argument("--solc-args", help="Extra arguments for solc") options.add_argument( - "--phrack", action="store_true", help="Phrack-style call graph" + "-l", + "--dynld", + action="store_true", + help="auto-load dependencies from the blockchain", ) options.add_argument( - "--enable-physics", action="store_true", help="enable graph physics simulation" + "--no-onchain-storage-access", + action="store_true", + help="turns off getting the data from onchain contracts", ) + options.add_argument( - "-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2 + "--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( "-q", @@ -255,36 +390,14 @@ def create_parser(parser: argparse.ArgumentParser) -> None: help="Deactivate dependency-based pruning", ) - rpc = parser.add_argument_group("RPC options") - - rpc.add_argument( - "--rpc", - help="custom RPC settings", - metavar="HOST:PORT / ganache / infura-[network_name]", - default="infura-mainnet", - ) - rpc.add_argument( - "--rpctls", type=bool, default=False, help="RPC connection over TLS" - ) - parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS) - -def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): - if not ( - args.search - or args.hash - or args.disassemble - or args.graph - or args.fire_lasers - or args.storage - or args.truffle - or args.statespace_json - or args.contract_hash_to_address - ): - parser.print_help() - sys.exit() - - if args.v: +def validate_args(args: Namespace): + """ + Validate cli args + :param args: + :return: + """ + if args.__dict__.get("v", False): if 0 <= args.v < 6: log_levels = [ logging.NOTSET, @@ -303,81 +416,92 @@ def validate_args(parser: argparse.ArgumentParser, args: argparse.Namespace): args.outform, "Invalid -v value, you can find valid values in usage" ) - if args.query_signature: - if sigs.ethereum_input_decoder is None: + if args.command in ANALYZE_LIST: + if args.query_signature and sigs.ethereum_input_decoder is None: exit_with_error( args.outform, "The --query-signature function requires the python package ethereum-input-decoder", ) - if args.enable_iprof: - if args.v < 4: + if args.enable_iprof and args.v < 4: exit_with_error( args.outform, "--enable-iprof must be used with -v LOG_LEVEL where LOG_LEVEL >= 4", ) - elif not (args.graph or args.fire_lasers or args.statespace_json): - exit_with_error( - args.outform, - "--enable-iprof must be used with one of -g, --graph, -x, --fire-lasers, -j and --statespace-json", - ) - - -def quick_commands(args: argparse.Namespace): - if args.hash: - print(MythrilDisassembler.hash_for_function_signature(args.hash)) - sys.exit() -def set_config(args: argparse.Namespace): +def set_config(args: Namespace): + """ + Set config based on args + :param args: + :return: modified config + """ config = MythrilConfig() - if args.dynld or not args.no_onchain_storage_access and not (args.rpc or args.i): + if ( + args.command in ANALYZE_LIST + and (args.dynld or not args.no_onchain_storage_access) + ) and not (args.rpc or args.i): config.set_api_from_config_path() - if args.address: + if args.__dict__.get("address", None): # Establish RPC connection if necessary config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls) - elif args.search or args.contract_hash_to_address: + if args.command in ("hash-to-address", "leveldb-search"): # Open LevelDB if necessary - config.set_api_leveldb( - config.leveldb_dir if not args.leveldb_dir else args.leveldb_dir - ) + if not args.__dict__.get("leveldb_dir", None): + leveldb_dir = config.leveldb_dir + else: + leveldb_dir = args.leveldb_dir + config.set_api_leveldb(leveldb_dir) return config -def leveldb_search(config: MythrilConfig, args: argparse.Namespace): - if args.search or args.contract_hash_to_address: +def leveldb_search(config: MythrilConfig, args: Namespace): + """ + Handle leveldb search + :param config: + :param args: + :return: + """ + if args.command in ("hash-to-address", "leveldb-search"): leveldb_searcher = MythrilLevelDB(config.eth_db) - if args.search: + if args.command == "leveldb-search": # Database search ops leveldb_searcher.search_db(args.search) else: # search corresponding address try: - leveldb_searcher.contract_hash_to_address(args.contract_hash_to_address) + leveldb_searcher.contract_hash_to_address(args.hash) except AddressNotFoundError: print("Address not found.") sys.exit() -def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): +def load_code(disassembler: MythrilDisassembler, args: Namespace): + """ + Loads code into disassembly and returns address + :param disassembler: + :param args: + :return: Address + """ + address = None - if args.code: + if args.__dict__.get("code", False): # Load from bytecode code = args.code[2:] if args.code.startswith("0x") else args.code address, _ = disassembler.load_from_bytecode(code, args.bin_runtime) - elif args.codefile: + elif args.__dict__.get("codefile", False): bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0]) bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode address, _ = disassembler.load_from_bytecode(bytecode, args.bin_runtime) - elif args.address: + elif args.__dict__.get("address", False): # Get bytecode from a contract address address, _ = disassembler.load_from_address(args.address) - elif args.solidity_file: + elif args.__dict__.get("solidity_file", False): # Compile Solidity source file(s) - if args.graph and len(args.solidity_file) > 1: + if args.command in ANALYZE_LIST and args.graph and len(args.solidity_file) > 1: exit_with_error( args.outform, "Cannot generate call graphs from multiple input files. Please do it one at a time.", @@ -387,8 +511,8 @@ def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): ) # list of files else: exit_with_error( - args.outform, - "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES", + args.__dict__.get("outform", "text"), + "No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, -f BYTECODE_FILE or ", ) return address @@ -396,45 +520,44 @@ def get_code(disassembler: MythrilDisassembler, args: argparse.Namespace): def execute_command( disassembler: MythrilDisassembler, address: str, - parser: argparse.ArgumentParser, - args: argparse.Namespace, + parser: ArgumentParser, + args: Namespace, ): + """ + Execute command + :param disassembler: + :param address: + :param parser: + :param args: + :return: + """ - if args.storage: - if not args.address: - exit_with_error( - args.outform, - "To read storage, provide the address of a deployed contract with the -a option.", - ) - + if args.command == "read-storage": storage = disassembler.get_state_variable_from_storage( - address=address, params=[a.strip() for a in args.storage.strip().split(",")] + address=address, + params=[a.strip() for a in args.storage_slots.strip().split(",")], ) print(storage) - return - - analyzer = MythrilAnalyzer( - strategy=args.strategy, - disassembler=disassembler, - address=address, - max_depth=args.max_depth, - execution_timeout=args.execution_timeout, - loop_bound=args.loop_bound, - create_timeout=args.create_timeout, - enable_iprof=args.enable_iprof, - disable_dependency_pruning=args.disable_dependency_pruning, - onchain_storage_access=not args.no_onchain_storage_access, - ) - - if args.disassemble: - # or mythril.disassemble(mythril.contracts[0]) + elif args.command in DISASSEMBLE_LIST: if disassembler.contracts[0].code: print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) if disassembler.contracts[0].creation_code: print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm()) - elif args.graph or args.fire_lasers: + elif args.command in ANALYZE_LIST: + analyzer = MythrilAnalyzer( + strategy=args.strategy, + disassembler=disassembler, + address=address, + max_depth=args.max_depth, + execution_timeout=args.execution_timeout, + create_timeout=args.create_timeout, + enable_iprof=args.enable_iprof, + disable_dependency_pruning=args.disable_dependency_pruning, + onchain_storage_access=not args.no_onchain_storage_access, + ) + if not disassembler.contracts: exit_with_error( args.outform, "input files do not contain any valid contracts" @@ -454,6 +577,21 @@ def execute_command( except Exception as e: exit_with_error(args.outform, "Error saving graph: " + str(e)) + elif args.statespace_json: + + if not analyzer.contracts: + exit_with_error( + args.outform, "input files do not contain any valid contracts" + ) + + statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) + + try: + with open(args.statespace_json, "w") as f: + json.dump(statespace, f) + except Exception as e: + exit_with_error(args.outform, "Error saving json: " + str(e)) + else: try: report = analyzer.fire_lasers( @@ -472,29 +610,24 @@ def execute_command( print(outputs[args.outform]) except ModuleNotFoundError as e: exit_with_error( - args.outform, "Error loading analyis modules: " + format(e) + args.outform, "Error loading analysis modules: " + format(e) ) - elif args.statespace_json: - - if not analyzer.contracts: - exit_with_error( - args.outform, "input files do not contain any valid contracts" - ) - - statespace = analyzer.dump_statespace(contract=analyzer.contracts[0]) - - try: - with open(args.statespace_json, "w") as f: - json.dump(statespace, f) - except Exception as e: - exit_with_error(args.outform, "Error saving json: " + str(e)) - else: parser.print_help() -def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None: +def contract_hash_to_address(args: Namespace): + """ + prints the hash from function signature + :param args: + :return: + """ + print(MythrilDisassembler.hash_for_function_signature(args.func_name)) + sys.exit() + + +def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None: """ Parses the arguments :param parser: The parser @@ -507,42 +640,55 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py") sys.exit() - if args.version: + if args.command not in COMMAND_LIST or args.command is None: + parser.print_help() + sys.exit() + + if args.command == "version": if args.outform == "json": print(json.dumps({"version_str": VERSION})) else: print("Mythril version {}".format(VERSION)) sys.exit() + if args.command == "help": + parser.print_help() + sys.exit() + # Parse cmdline args - validate_args(parser, args) + validate_args(args) try: - quick_commands(args) + if args.command == "function-to-hash": + contract_hash_to_address(args) config = set_config(args) leveldb_search(config, args) + query_signature = args.__dict__.get("query_signature", None) + solc_args = args.__dict__.get("solc_args", None) + solv = args.__dict__.get("solv", None) disassembler = MythrilDisassembler( eth=config.eth, - solc_version=args.solv, - solc_args=args.solc_args, - enable_online_lookup=args.query_signature, + solc_version=solv, + solc_args=solc_args, + enable_online_lookup=query_signature, ) - if args.truffle: + if args.command == "truffle": try: disassembler.analyze_truffle_project(args) except FileNotFoundError: print( - "Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully." + "Build directory not found. Make sure that you start the analysis from the project root, " + "and that 'truffle compile' has executed successfully." ) sys.exit() - address = get_code(disassembler, args) + address = load_code(disassembler, args) execute_command( disassembler=disassembler, address=address, parser=parser, args=args ) except CriticalError as ce: - exit_with_error(args.outform, str(ce)) + exit_with_error(args.__dict__.get("outform", "text"), str(ce)) except Exception: - exit_with_error(args.outform, traceback.format_exc()) + exit_with_error(args.__dict__.get("outform", "text"), traceback.format_exc()) if __name__ == "__main__": diff --git a/tests/test_cli_opts.py b/tests/cli_tests/test_cli_opts.py similarity index 88% rename from tests/test_cli_opts.py rename to tests/cli_tests/test_cli_opts.py index 5de6cdcd..1985e45e 100644 --- a/tests/test_cli_opts.py +++ b/tests/cli_tests/test_cli_opts.py @@ -8,7 +8,7 @@ import sys def test_version_opt(capsys): # Check that "myth --version" returns a string with the word # "version" in it - sys.argv = ["mythril", "--version"] + sys.argv = ["mythril", "version"] with pytest.raises(SystemExit) as pytest_wrapped_e: main() assert pytest_wrapped_e.type == SystemExit @@ -16,7 +16,7 @@ def test_version_opt(capsys): assert captured.out.find(" version ") >= 1 # Check that "myth --version -o json" returns a JSON object - sys.argv = ["mythril", "--version", "-o", "json"] + sys.argv = ["mythril", "version", "-o", "json"] with pytest.raises(SystemExit) as pytest_wrapped_e: main() assert pytest_wrapped_e.type == SystemExit diff --git a/tests/cmd_line_test.py b/tests/cmd_line_test.py index 0862cf9e..258a6882 100644 --- a/tests/cmd_line_test.py +++ b/tests/cmd_line_test.py @@ -1,5 +1,6 @@ from subprocess import check_output from tests import BaseTestCase, TESTDATA, PROJECT_DIR, TESTS_DIR +from mock import patch MYTH = str(PROJECT_DIR / "myth") @@ -15,23 +16,64 @@ def output_of(command): class CommandLineToolTestCase(BaseTestCase): def test_disassemble_code_correctly(self): - command = "python3 {} MYTH -d --bin-runtime -c 0x5050 --solv 0.5.0".format(MYTH) + command = "python3 {} disassemble --bin-runtime -c 0x5050".format(MYTH) self.assertIn("0 POP\n1 POP\n", output_of(command)) def test_disassemble_solidity_file_correctly(self): solidity_file = str(TESTDATA / "input_contracts" / "metacoin.sol") - command = "python3 {} -d {} --solv 0.5.0".format(MYTH, solidity_file) + command = "python3 {} disassemble {}".format(MYTH, solidity_file) self.assertIn("2 PUSH1 0x40\n4 MSTORE", output_of(command)) def test_hash_a_function_correctly(self): - command = "python3 {} --solv 0.5.0 --hash 'setOwner(address)'".format(MYTH) + command = "python3 {} function-to-hash 'setOwner(address)'".format(MYTH) self.assertIn("0x13af4035\n", output_of(command)) + def test_failure_json(self): + command = "python3 {} analyze doesnt_exist.sol -o json".format(MYTH) + print(output_of(command)) + self.assertIn(""""success": false""", output_of(command)) + + def test_failure_text(self): + command = "python3 {} analyze doesnt_exist.sol".format(MYTH) + assert output_of(command) == "" + + def test_failure_jsonv2(self): + command = "python3 {} analyze doesnt_exist.sol -o jsonv2".format(MYTH) + self.assertIn(""""level": "error""" "", output_of(command)) + + def test_analyze(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = "python3 {} analyze {}".format(MYTH, solidity_file) + self.assertIn("111", output_of(command)) + + def test_analyze_bytecode(self): + solidity_file = str(TESTDATA / "inputs" / "origin.sol.o") + command = "python3 {} analyze --bin-runtime -f {}".format(MYTH, solidity_file) + self.assertIn("111", output_of(command)) + + def test_invalid_args_iprof(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = "python3 {} analyze {} --enable-iprof -o json".format( + MYTH, solidity_file + ) + self.assertIn(""""success": false""", output_of(command)) + + def test_only_epic(self): + command = "python3 {}".format(MYTH) + self.assertIn("usage: ", output_of(command)) + + def test_storage(self): + solidity_file = str(TESTDATA / "input_contracts" / "origin.sol") + command = """python3 {} read-storage "438767356, 3" 0x76799f77587738bfeef09452df215b63d2cfb08a """.format( + MYTH + ) + self.assertIn("0x1a270efc", output_of(command)) + class TruffleTestCase(BaseTestCase): def test_analysis_truffle_project(self): truffle_project_root = str(TESTS_DIR / "truffle_project") - command = "cd {}; truffle compile; python3 {} --truffle -t 2".format( + command = "cd {}; truffle compile; python3 {} truffle -t 2".format( truffle_project_root, MYTH ) self.assertIn("=== Unprotected Ether Withdrawal ====", output_of(command)) @@ -39,7 +81,7 @@ class TruffleTestCase(BaseTestCase): class InfuraTestCase(BaseTestCase): def test_infura_mainnet(self): - command = "python3 {} --rpc infura-mainnet -d -a 0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208".format( + command = "python3 {} disassemble --rpc infura-mainnet -a 0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208".format( MYTH ) output = output_of(command) @@ -47,21 +89,21 @@ class InfuraTestCase(BaseTestCase): self.assertIn("7278 POP\n7279 POP\n7280 JUMP\n7281 STOP", output) def test_infura_rinkeby(self): - command = "python3 {} --rpc infura-rinkeby -d -a 0xB6f2bFED892a662bBF26258ceDD443f50Fa307F5".format( + command = "python3 {} disassemble --rpc infura-rinkeby -a 0xB6f2bFED892a662bBF26258ceDD443f50Fa307F5".format( MYTH ) output = output_of(command) self.assertIn("34 JUMPDEST\n35 CALLVALUE", output) def test_infura_kovan(self): - command = "python3 {} --rpc infura-kovan -d -a 0xE6bBF9B5A3451242F82f8cd458675092617a1235".format( + command = "python3 {} disassemble --rpc infura-kovan -a 0xE6bBF9B5A3451242F82f8cd458675092617a1235".format( MYTH ) output = output_of(command) self.assertIn("9999 PUSH1 0x00\n10001 NOT\n10002 AND\n10003 PUSH1 0x00", output) def test_infura_ropsten(self): - command = "python3 {} --rpc infura-ropsten -d -a 0x6e0E0e02377Bc1d90E8a7c21f12BA385C2C35f78".format( + command = "python3 {} disassemble --rpc infura-ropsten -a 0x6e0E0e02377Bc1d90E8a7c21f12BA385C2C35f78".format( MYTH ) output = output_of(command)