Merge with develop

pull/894/head
Nikhil Parasaram 6 years ago
commit ef2a56c898
  1. 41
      .circleci/config.yml
  2. 5
      .gitattributes
  3. 17
      .github/ISSUE_TEMPLATE/analysis-module.md
  4. 2
      .github/ISSUE_TEMPLATE/bug-report.md
  5. 10
      .github/ISSUE_TEMPLATE/feature-request.md
  6. 5
      .gitignore
  7. 4
      CONTRIBUTING.md
  8. 5
      Dockerfile
  9. 2
      MANIFEST.in
  10. 41
      README.md
  11. 12
      docker_build_and_deploy.sh
  12. 19
      docs/Makefile
  13. 35
      docs/make.bat
  14. 190
      docs/source/conf.py
  15. 17
      docs/source/index.rst
  16. 7
      docs/source/modules.rst
  17. 110
      docs/source/mythril.analysis.modules.rst
  18. 85
      docs/source/mythril.analysis.rst
  19. 30
      docs/source/mythril.disassembler.rst
  20. 46
      docs/source/mythril.ethereum.interface.leveldb.rst
  21. 54
      docs/source/mythril.ethereum.interface.rpc.rst
  22. 18
      docs/source/mythril.ethereum.interface.rst
  23. 37
      docs/source/mythril.ethereum.rst
  24. 30
      docs/source/mythril.interfaces.rst
  25. 103
      docs/source/mythril.laser.ethereum.rst
  26. 86
      docs/source/mythril.laser.ethereum.state.rst
  27. 22
      docs/source/mythril.laser.ethereum.strategy.rst
  28. 38
      docs/source/mythril.laser.ethereum.transaction.rst
  29. 18
      docs/source/mythril.laser.rst
  30. 38
      docs/source/mythril.laser.smt.rst
  31. 51
      docs/source/mythril.rst
  32. 22
      docs/source/mythril.solidity.rst
  33. 46
      docs/source/mythril.support.rst
  34. 2
      mythril/__init__.py
  35. 25
      mythril/alarm.py
  36. 59
      mythril/analysis/call_helpers.py
  37. 301
      mythril/analysis/callgraph.py
  38. 69
      mythril/analysis/modules/base.py
  39. 132
      mythril/analysis/modules/delegatecall.py
  40. 305
      mythril/analysis/modules/dependence_on_predictable_vars.py
  41. 93
      mythril/analysis/modules/deprecated_ops.py
  42. 127
      mythril/analysis/modules/ether_send.py
  43. 127
      mythril/analysis/modules/ether_thief.py
  44. 95
      mythril/analysis/modules/exceptions.py
  45. 258
      mythril/analysis/modules/external_calls.py
  46. 529
      mythril/analysis/modules/integer.py
  47. 136
      mythril/analysis/modules/multiple_sends.py
  48. 159
      mythril/analysis/modules/suicide.py
  49. 314
      mythril/analysis/modules/transaction_order_dependence.py
  50. 201
      mythril/analysis/modules/unchecked_retval.py
  51. 57
      mythril/analysis/ops.py
  52. 207
      mythril/analysis/report.py
  53. 104
      mythril/analysis/security.py
  54. 113
      mythril/analysis/solver.py
  55. 86
      mythril/analysis/swc_data.py
  56. 196
      mythril/analysis/symbolic.py
  57. 7
      mythril/analysis/templates/report_as_markdown.jinja2
  58. 9
      mythril/analysis/templates/report_as_text.jinja2
  59. 166
      mythril/analysis/traceexplore.py
  60. 127
      mythril/disassembler/asm.py
  61. 132
      mythril/disassembler/disassembly.py
  62. 3
      mythril/ether/__init__.py
  63. 152
      mythril/ether/asm.py
  64. 71
      mythril/ether/ethcontract.py
  65. 75
      mythril/ether/evm.py
  66. 125
      mythril/ether/soliditycontract.py
  67. 139
      mythril/ethereum/evmcontract.py
  68. 106
      mythril/ethereum/interface/leveldb/accountindexing.py
  69. 243
      mythril/ethereum/interface/leveldb/client.py
  70. 18
      mythril/ethereum/interface/leveldb/eth_db.py
  71. 131
      mythril/ethereum/interface/leveldb/state.py
  72. 59
      mythril/ethereum/interface/rpc/base_client.py
  73. 61
      mythril/ethereum/interface/rpc/client.py
  74. 14
      mythril/ethereum/interface/rpc/constants.py
  75. 17
      mythril/ethereum/interface/rpc/exceptions.py
  76. 42
      mythril/ethereum/interface/rpc/utils.py
  77. 78
      mythril/ethereum/util.py
  78. 23
      mythril/exceptions.py
  79. 476
      mythril/interfaces/cli.py
  80. 288
      mythril/interfaces/epic.py
  81. 199
      mythril/laser/ethereum/call.py
  82. 67
      mythril/laser/ethereum/cfg.py
  83. 19
      mythril/laser/ethereum/evm_exceptions.py
  84. 188
      mythril/laser/ethereum/gas.py
  85. 1482
      mythril/laser/ethereum/instructions.py
  86. 79
      mythril/laser/ethereum/iprof.py
  87. 28
      mythril/laser/ethereum/keccak.py
  88. 94
      mythril/laser/ethereum/natives.py
  89. 355
      mythril/laser/ethereum/state.py
  90. 1
      mythril/laser/ethereum/state/__init__.py
  91. 130
      mythril/laser/ethereum/state/account.py
  92. 40
      mythril/laser/ethereum/state/annotation.py
  93. 307
      mythril/laser/ethereum/state/calldata.py
  94. 77
      mythril/laser/ethereum/state/constraints.py
  95. 73
      mythril/laser/ethereum/state/environment.py
  96. 143
      mythril/laser/ethereum/state/global_state.py
  97. 236
      mythril/laser/ethereum/state/machine_state.py
  98. 142
      mythril/laser/ethereum/state/memory.py
  99. 126
      mythril/laser/ethereum/state/world_state.py
  100. 28
      mythril/laser/ethereum/strategy/__init__.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -33,6 +33,12 @@ jobs:
- .tox/py*
- /root/.cache/pip/wheels/
- run:
name: Black style check
command: |
pip3 install --user black
python3 -m black --check /home/mythril/
- run:
background: true
name: Launch of background geth instance
@ -42,9 +48,11 @@ jobs:
name: Unit-testing
command: tox
working_directory: /home/mythril
no_output_timeout: 10m
environment:
LC_ALL: en_US.ASCII
LANG: en_US.ASCII
MYTHRIL_DIR: '/home/mythril'
- store_test_results:
path: /home/mythril/.tox/output
@ -61,10 +69,16 @@ jobs:
name: Sonar analysis
command: if [ -z "$CIRCLE_PR_NUMBER" ]; then if [ -z "$CIRCLE_TAG" ]; then sonar-scanner -Dsonar.projectKey=$SONAR_PROJECT_KEY -Dsonar.organization=$SONAR_ORGANIZATION -Dsonar.branch.name=$CIRCLE_BRANCH -Dsonar.projectBaseDir=/home/mythril -Dsonar.sources=mythril -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.tests=/home/mythril/tests -Dsonar.login=$SONAR_LOGIN; fi; fi
# - run:
# name: Integration tests
# command: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./run-integration-tests.sh; fi
# working_directory: /home
- run:
name: Integration tests
command: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./run-integration-tests.sh; fi
working_directory: /home
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
pypi_release:
<<: *defaults
@ -84,6 +98,8 @@ jobs:
command: twine upload dist/*
working_directory: /home/mythril
# Release of the mainstream (current stable) version as mythril/myth
# container.
dockerhub_release:
docker:
- image: docker:stable
@ -92,7 +108,18 @@ jobs:
- setup_remote_docker
- run:
name: Building Docker Image
command: ./docker_build_and_deploy.sh
command: ./docker_build_and_deploy.sh mythril/myth
# Release of the latest development version as mythril/myth-dev container.
dockerhub_dev_release:
docker:
- image: docker:stable
steps:
- checkout
- setup_remote_docker
- run:
name: Building Docker Image
command: ./docker_build_and_deploy.sh mythril/myth-dev
workflows:
version: 2
@ -110,6 +137,12 @@ workflows:
only: /v[0-9]+(\.[0-9]+)*/
requires:
- test
- dockerhub_dev_release:
filters:
branches:
only: develop
# requires:
# - test
- dockerhub_release:
filters:
branches:

5
.gitattributes vendored

@ -1,2 +1,5 @@
tests/testdata/* linguist-detectable=false
static/* linguist-documentation
static/* linguist-documentation
# Solidity
*.sol linguist-language=Solidity

@ -8,30 +8,29 @@ about: Create an analysis module feature request
## Description
<!-- Replace this text with a description of an vulnerability that should be
<!-- Add a description of an vulnerability that should be
detected by a Mythril analysis module. -->
## Tests
<!-- This section is optional.
Replace this text with suggestions on how to test the feature,
if it is not obvious. This might require certain Solidity source,
bytecode, or a Truffle project. You can also provide
links to existing code. -->
Suggest how to test the feature, if it is not obvious. This might
require certain Solidity source, bytecode, or a Truffle project. You
can also provide links to existing code. -->
## Implementation details
<!-- This section is optional.
If you have thoughts about how to implement the analysis, feel free
replace this text with that. -->
If you have thoughts about how to implement the analysis, add
this here. -->
## Links
<!-- This section is optional.
Replace this text with any links describing the issue or pointing to resources
that can help in implementing the analysis
Add links describing the issue or pointing to resources that can help
in implementing the analysis.
Thanks for helping! -->

@ -71,7 +71,7 @@ Please modify for your setup
<!-- This section is optional.
Add any other context about the problem here or special environment setup
Add any other context about the problem here or special environment setup.
Thanks for helping!

@ -6,7 +6,7 @@ about: Tell us about a new feature that would make Mythril better
## Description
<!-- Replace this text with a short description of the feature. -->
<!-- Give a short description of the feature. -->
## Background
@ -17,10 +17,10 @@ feature, for example: user scenarios, or the value of the feature. -->
<!-- This section is optional.
Replace this text with suggestions on how to test the feature,
if it is not obvious. This might require certain Solidity source,
bytecode, or a Truffle project. You can also provide
links to existing code.
Suggestion how to test the feature, if it is not obvious.
This might require certain Solidity source, bytecode, or a Truffle
project. You can also provide links to existing code.
Thanks for helping!

5
.gitignore vendored

@ -168,7 +168,6 @@ $RECYCLE.BIN/
# End of https://www.gitignore.io/api/linux,macos,python,windows
*.asm
*.rst
*.lock
*.svg
lol*
@ -176,7 +175,7 @@ lol*
coverage_html_report/
tests/testdata/outputs_current/
tests/testdata/outputs_current_laser_result/
tests/mythril_dir/signatures.json
tests/mythril_dir/signatures.db
# VSCode
.vscode
.vscode

@ -5,12 +5,12 @@ Hi, if you are reading this that means that you probably want to contribute to M
If you have found a problem with Mythril or want to propose a new feature then you can do this using GitHub issues.
We already created some templates to make this process easier, but if your issue/feature request does not fit within the template then feel free to deviate.
If you have a small question or aren't sure if you should create an issue for your problem/suggestion then you can always hop by on our [Gitter channel](https://gitter.im/ConsenSys/mythril).
If you have a small question or aren't sure if you should create an issue for your problem/suggestion then you can always hop by on our [Discord server](https://discord.gg/FGMkcU2).
# Coding
If you want to help out with the development of Mythril then you can take a look at our issues or [Waffle board](https://waffle.io/ConsenSys/mythril).
Before you start working on an issue pkease stop by on Gitter to message a collaborator, this way we can assign you to the issue making sure nobody does double work. We can also provide you with support through Gitter if there are any questions during the development process.
Before you start working on an issue pkease stop by on Discord to message a collaborator, this way we can assign you to the issue making sure nobody does double work. We can also provide you with support through Discord if there are any questions during the development process.
## New ideas
Before you start working on a new idea, it's useful to create an issue on GitHub, that way we know what you want to implement and that you are working on it. Additionally, it might happen that your feature does not fit with our roadmap, in which case it would be unfortunate if you have already spent some time working on it.

@ -1,6 +1,9 @@
FROM ubuntu:bionic
RUN apt-get update \
&& apt-get install -y \
libsqlite3-0 \
libsqlite3-dev \
&& apt-get install -y \
build-essential \
locales \
@ -16,6 +19,7 @@ RUN apt-get update \
python3-dev \
pandoc \
git \
wget \
&& ln -s /usr/bin/python3 /usr/local/bin/python
COPY ./requirements.txt /opt/mythril/requirements.txt
@ -32,4 +36,5 @@ COPY . /opt/mythril
RUN cd /opt/mythril \
&& python setup.py install
COPY signatures.db /root/.mythril/signatures.db
ENTRYPOINT ["/usr/local/bin/myth"]

@ -1,2 +1,2 @@
include mythril/disassembler/signatures.json
include signatures.db
include mythril/analysis/templates/*

@ -1,17 +1,21 @@
# Mythril OSS [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Mythril%20-%20Security%20Analyzer%20for%20Ethereum%20Smart%20Contracts&url=https://www.github.com/ConsenSys/mythril)
# Mythril Classic
<p align="center">
<img src="/static/mythril_new.png" height="320px"/>
</p>
[![Discord](https://img.shields.io/discord/481002907366588416.svg)](https://discord.gg/E3YrVtG)
[![PyPI](https://badge.fury.io/py/mythril.svg)](https://pypi.python.org/pypi/mythril)
![Master Build Status](https://img.shields.io/circleci/project/github/ConsenSys/mythril/master.svg)
[![Waffle.io - Columns and their card count](https://badge.waffle.io/ConsenSys/mythril.svg?columns=In%20Progress)](https://waffle.io/ConsenSys/mythril)
![Master Build Status](https://img.shields.io/circleci/project/github/ConsenSys/mythril-classic/master.svg)
[![Waffle.io - Columns and their card count](https://badge.waffle.io/ConsenSys/mythril-classic.svg?columns=In%20Progress)](https://waffle.io/ConsenSys/mythril-classic/)
[![Sonarcloud - Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mythril&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=mythril)
[![PyPI Statistics](https://pypistats.com/badge/mythril.svg)](https://pypistats.com/package/mythril)
<img height="120px" align="right" src="https://github.com/ConsenSys/mythril/raw/master/static/mythril.png" alt="mythril" />
[![Downloads](https://pepy.tech/badge/mythril)](https://pepy.tech/project/mythril)
Mythril OSS is the classic security analysis tool for Ethereum smart contracts. It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities.
Mythril Classic is an open-source security analysis tool for Ethereum smart contracts. It uses concolic analysis, taint analysis and control flow checking to detect a variety of security vulnerabilities.
Whether you want to contribute, need support, or want to learn what we have cooking for the future, our [Discord server](https://discord.gg/E3YrVtG) will serve your needs!
Whether you want to contribute, need support, or want to learn what we have cooking for the future, our [Discord server](https://discord.gg/E3YrVtG) will serve your needs.
Oh and by the way, we're building an easy-to-use SaaS solution and tools ecosystem for Ethereum developers called [Mythril Platform](https://mythril.ai). You should definitely check that out as well.
Oh and by the way, we're also building an [easy-to-use security analysis platform called MythX](https://mythx.io) that integrates seamlessly with Truffle, Visual Studio Code, Github and other environments. If you're looking for tooling to plug into your SDLC you should check it out.
## Installation and setup
@ -31,21 +35,20 @@ See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup)
## Usage
Instructions for using the 'myth' tool are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki).
Instructions for using Mythril Classic are found on the [Wiki](https://github.com/ConsenSys/mythril-classic/wiki).
For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG).
## Vulnerability Remediation
## Bulding the Documentation
Mythril Classic's documentation is contained in the `docs` folder. It is based on Sphinx and can be built using the Makefile contained in the subdirectory:
Visit the [Smart Contract Vulnerability Classification Registry](https://smartcontractsecurity.github.io/SWC-registry/) to find detailed information and remediation guidance for the vulnerabilities reported.
```
cd docs
make html
```
## Presentations, papers and articles
This will create a `build` output directory containing the HTML output. Alternatively, PDF documentation can be built with `make latexpdf`. The available output format options can be seen with `make help`.
- [Analyzing Ethereum Smart Contracts for Vulnerabilities](https://hackernoon.com/scanning-ethereum-smart-contracts-for-vulnerabilities-b5caefd995df)
- [What Caused the Parity SUICIDE Vulnerability & How to Detect Similar Bugs](https://hackernoon.com/what-caused-the-latest-100-million-ethereum-bug-and-a-detection-tool-for-similar-bugs-7b80f8ab7279)
- [Detecting Integer Overflows in Ethereum Smart Contracts](https://media.consensys.net/detecting-batchoverflow-and-similar-flaws-in-ethereum-smart-contracts-93cf5a5aaac8)
- [How Formal Verification Can Ensure Flawless Smart Contracts](https://media.consensys.net/how-formal-verification-can-ensure-flawless-smart-contracts-cbda8ad99bd1)
- [Smashing Smart Contracts for Fun and Real Profit](https://hackernoon.com/hitb2018ams-smashing-smart-contracts-for-fun-and-real-profit-720f5e3ac777)
- [HITBSecConf 2018 - Presentation video](https://www.youtube.com/watch?v=iqf6epACgds)
- [EDCon Toronto 2018 - Mythril: Find bugs and verify security properties in your contracts](https://www.youtube.com/watch?v=NJ9StJThxZY&feature=youtu.be&t=3h3m18s)
## Vulnerability Remediation
Visit the [Smart Contract Vulnerability Classification Registry](https://smartcontractsecurity.github.io/SWC-registry/) to find detailed information and remediation guidance for the vulnerabilities reported.

@ -1,9 +1,17 @@
#!/bin/sh
set -eo pipefail
NAME=mythril/myth
NAME=$1
if [ ! -z $CIRCLE_TAG ];
then
VERSION=${CIRCLE_TAG#?}
else
VERSION=${CIRCLE_SHA1}
fi
VERSION_TAG=${NAME}:${CIRCLE_TAG#?}
VERSION_TAG=${NAME}:${VERSION}
LATEST_TAG=${NAME}:latest
docker build -t ${VERSION_TAG} .

@ -0,0 +1,19 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath("../../"))
# -- Project information -----------------------------------------------------
project = "Mythril Classic"
copyright = "2018, Bernhard Mueller"
author = "Bernhard Mueller"
# The short X.Y version
version = ""
# The full version, including alpha/beta/rc tags
from mythril.version import VERSION
release = VERSION
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.coverage",
"sphinx.ext.mathjax",
"sphinx.ext.viewcode",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "MythrilClassicdoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (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",
)
]
# -- Options for manual page output ------------------------------------------
# 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)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"MythrilClassic",
"Mythril Classic Documentation",
author,
"MythrilClassic",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------

@ -0,0 +1,17 @@
Welcome to Mythril Classic's documentation!
===========================================
.. toctree::
:maxdepth: 3
:caption: Package Contents:
mythril
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

@ -0,0 +1,7 @@
mythril
=======
.. toctree::
:maxdepth: 4
mythril

@ -0,0 +1,110 @@
mythril.analysis.modules package
================================
Submodules
----------
mythril.analysis.modules.base module
------------------------------------
.. automodule:: mythril.analysis.modules.base
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.delegatecall module
--------------------------------------------
.. automodule:: mythril.analysis.modules.delegatecall
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.dependence\_on\_predictable\_vars module
-----------------------------------------------------------------
.. automodule:: mythril.analysis.modules.dependence_on_predictable_vars
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.deprecated\_ops module
-----------------------------------------------
.. automodule:: mythril.analysis.modules.deprecated_ops
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.ether\_thief module
--------------------------------------------
.. automodule:: mythril.analysis.modules.ether_thief
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.exceptions module
------------------------------------------
.. automodule:: mythril.analysis.modules.exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.external\_calls module
-----------------------------------------------
.. automodule:: mythril.analysis.modules.external_calls
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.integer module
---------------------------------------
.. automodule:: mythril.analysis.modules.integer
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.multiple\_sends module
-----------------------------------------------
.. automodule:: mythril.analysis.modules.multiple_sends
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.suicide module
---------------------------------------
.. automodule:: mythril.analysis.modules.suicide
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.transaction\_order\_dependence module
--------------------------------------------------------------
.. automodule:: mythril.analysis.modules.transaction_order_dependence
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.modules.unchecked\_retval module
-------------------------------------------------
.. automodule:: mythril.analysis.modules.unchecked_retval
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.analysis.modules
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,85 @@
mythril.analysis package
========================
Subpackages
-----------
.. toctree::
mythril.analysis.modules
Submodules
----------
mythril.analysis.callgraph module
---------------------------------
.. automodule:: mythril.analysis.callgraph
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.ops module
---------------------------
.. automodule:: mythril.analysis.ops
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.report module
------------------------------
.. automodule:: mythril.analysis.report
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.security module
--------------------------------
.. automodule:: mythril.analysis.security
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.solver module
------------------------------
.. automodule:: mythril.analysis.solver
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.swc\_data module
---------------------------------
.. automodule:: mythril.analysis.swc_data
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.symbolic module
--------------------------------
.. automodule:: mythril.analysis.symbolic
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.traceexplore module
------------------------------------
.. automodule:: mythril.analysis.traceexplore
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.analysis
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,30 @@
mythril.disassembler package
============================
Submodules
----------
mythril.disassembler.asm module
-------------------------------
.. automodule:: mythril.disassembler.asm
:members:
:undoc-members:
:show-inheritance:
mythril.disassembler.disassembly module
---------------------------------------
.. automodule:: mythril.disassembler.disassembly
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.disassembler
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,46 @@
mythril.ethereum.interface.leveldb package
==========================================
Submodules
----------
mythril.ethereum.interface.leveldb.accountindexing module
---------------------------------------------------------
.. automodule:: mythril.ethereum.interface.leveldb.accountindexing
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.leveldb.client module
------------------------------------------------
.. automodule:: mythril.ethereum.interface.leveldb.client
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.leveldb.eth\_db module
-------------------------------------------------
.. automodule:: mythril.ethereum.interface.leveldb.eth_db
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.leveldb.state module
-----------------------------------------------
.. automodule:: mythril.ethereum.interface.leveldb.state
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.ethereum.interface.leveldb
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,54 @@
mythril.ethereum.interface.rpc package
======================================
Submodules
----------
mythril.ethereum.interface.rpc.base\_client module
--------------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.base_client
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.client module
--------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.client
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.constants module
-----------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.constants
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.exceptions module
------------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.utils module
-------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.ethereum.interface.rpc
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,18 @@
mythril.ethereum.interface package
==================================
Subpackages
-----------
.. toctree::
mythril.ethereum.interface.leveldb
mythril.ethereum.interface.rpc
Module contents
---------------
.. automodule:: mythril.ethereum.interface
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,37 @@
mythril.ethereum package
========================
Subpackages
-----------
.. toctree::
mythril.ethereum.interface
Submodules
----------
mythril.ethereum.evmcontract module
-----------------------------------
.. automodule:: mythril.ethereum.evmcontract
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.util module
----------------------------
.. automodule:: mythril.ethereum.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.ethereum
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,30 @@
mythril.interfaces package
==========================
Submodules
----------
mythril.interfaces.cli module
-----------------------------
.. automodule:: mythril.interfaces.cli
:members:
:undoc-members:
:show-inheritance:
mythril.interfaces.epic module
------------------------------
.. automodule:: mythril.interfaces.epic
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.interfaces
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,103 @@
mythril.laser.ethereum package
==============================
Subpackages
-----------
.. toctree::
mythril.laser.ethereum.state
mythril.laser.ethereum.strategy
mythril.laser.ethereum.transaction
Submodules
----------
mythril.laser.ethereum.call module
----------------------------------
.. automodule:: mythril.laser.ethereum.call
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.cfg module
---------------------------------
.. automodule:: mythril.laser.ethereum.cfg
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.evm\_exceptions module
---------------------------------------------
.. automodule:: mythril.laser.ethereum.evm_exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.gas module
---------------------------------
.. automodule:: mythril.laser.ethereum.gas
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.instructions module
------------------------------------------
.. automodule:: mythril.laser.ethereum.instructions
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.keccak module
------------------------------------
.. automodule:: mythril.laser.ethereum.keccak
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.natives module
-------------------------------------
.. automodule:: mythril.laser.ethereum.natives
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.svm module
---------------------------------
.. automodule:: mythril.laser.ethereum.svm
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.taint\_analysis module
---------------------------------------------
.. automodule:: mythril.laser.ethereum.taint_analysis
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.util module
----------------------------------
.. automodule:: mythril.laser.ethereum.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,86 @@
mythril.laser.ethereum.state package
====================================
Submodules
----------
mythril.laser.ethereum.state.account module
-------------------------------------------
.. automodule:: mythril.laser.ethereum.state.account
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.annotation module
----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.annotation
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.calldata module
--------------------------------------------
.. automodule:: mythril.laser.ethereum.state.calldata
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.constraints module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.constraints
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.environment module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.environment
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.global\_state module
-------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.global_state
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.machine\_state module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.machine_state
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.memory module
------------------------------------------
.. automodule:: mythril.laser.ethereum.state.memory
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.world\_state module
------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.world_state
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.state
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,22 @@
mythril.laser.ethereum.strategy package
=======================================
Submodules
----------
mythril.laser.ethereum.strategy.basic module
--------------------------------------------
.. automodule:: mythril.laser.ethereum.strategy.basic
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.strategy
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,38 @@
mythril.laser.ethereum.transaction package
==========================================
Submodules
----------
mythril.laser.ethereum.transaction.concolic module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.concolic
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.transaction.symbolic module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.symbolic
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.transaction.transaction\_models module
-------------------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.transaction_models
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.transaction
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,18 @@
mythril.laser package
=====================
Subpackages
-----------
.. toctree::
mythril.laser.ethereum
mythril.laser.smt
Module contents
---------------
.. automodule:: mythril.laser
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,38 @@
mythril.laser.smt package
=========================
Submodules
----------
mythril.laser.smt.bitvec module
-------------------------------
.. automodule:: mythril.laser.smt.bitvec
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.bool module
-----------------------------
.. automodule:: mythril.laser.smt.bool
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.expression module
-----------------------------------
.. automodule:: mythril.laser.smt.expression
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.smt
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,51 @@
mythril package
===============
Subpackages
-----------
.. toctree::
mythril.analysis
mythril.disassembler
mythril.ethereum
mythril.interfaces
mythril.laser
mythril.solidity
mythril.support
Submodules
----------
mythril.exceptions module
-------------------------
.. automodule:: mythril.exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.mythril module
----------------------
.. automodule:: mythril.mythril
:members:
:undoc-members:
:show-inheritance:
mythril.version module
----------------------
.. automodule:: mythril.version
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,22 @@
mythril.solidity package
========================
Submodules
----------
mythril.solidity.soliditycontract module
----------------------------------------
.. automodule:: mythril.solidity.soliditycontract
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.solidity
:members:
:undoc-members:
:show-inheritance:

@ -0,0 +1,46 @@
mythril.support package
=======================
Submodules
----------
mythril.support.loader module
-----------------------------
.. automodule:: mythril.support.loader
:members:
:undoc-members:
:show-inheritance:
mythril.support.signatures module
---------------------------------
.. automodule:: mythril.support.signatures
:members:
:undoc-members:
:show-inheritance:
mythril.support.support\_utils module
-------------------------------------
.. automodule:: mythril.support.support_utils
:members:
:undoc-members:
:show-inheritance:
mythril.support.truffle module
------------------------------
.. automodule:: mythril.support.truffle
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.support
:members:
:undoc-members:
:show-inheritance:

@ -1,6 +1,6 @@
# We use RsT document formatting in docstring. For example :param to mark parameters.
# See PEP 287
__docformat__ = 'restructuredtext'
__docformat__ = "restructuredtext"
# Accept mythril.VERSION to get mythril's current version number
from .version import VERSION # NOQA

@ -0,0 +1,25 @@
import signal
from types import FrameType
from mythril.exceptions import OutOfTimeError
def sigalrm_handler(signum: int, frame: FrameType) -> None:
raise OutOfTimeError
def start_timeout(timeout: int) -> None:
"""
Starts a timeout
:param timeout: Time in seconds to set the timeout for
:return: None
"""
signal.signal(signal.SIGALRM, sigalrm_handler)
signal.alarm(timeout)
def disable_timeout() -> None:
"""
Ensures that the timeout is disabled
:return: None
"""
signal.alarm(0)

@ -0,0 +1,59 @@
"""This module provides helper functions for the analysis modules to deal with
call functionality."""
from typing import Union
from mythril.analysis.ops import VarType, Call, get_variable
from mythril.laser.ethereum.state.global_state import GlobalState
def get_call_from_state(state: GlobalState) -> Union[Call, None]:
"""
:param state:
:return:
"""
instruction = state.get_current_instruction()
op = instruction["opcode"]
stack = state.mstate.stack
if op in ("CALL", "CALLCODE"):
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
get_variable(stack[-7]),
)
if to.type == VarType.CONCRETE and to.val < 5:
return None
if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE:
return Call(
state.node,
state,
None,
op,
to,
gas,
value,
state.mstate.memory[meminstart.val : meminsz.val * 4],
)
else:
return Call(state.node, state, None, op, to, gas, value)
else:
gas, to, meminstart, meminsz, memoutstart, memoutsz = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
)
return Call(state.node, state, None, op, to, gas)

@ -1,151 +1,234 @@
"""This module contains the configuration and functions to create call
graphs."""
import re
from jinja2 import Environment, PackageLoader, select_autoescape
from z3 import Z3Exception
from mythril.laser.ethereum.svm import NodeFlags
import z3
from mythril.laser.smt import simplify
default_opts = {
'autoResize': True,
'height': '100%',
'width': '100%',
'manipulation': False,
'layout': {
'improvedLayout': True,
'hierarchical': {
'enabled': True,
'levelSeparation': 450,
'nodeSpacing': 200,
'treeSpacing': 100,
'blockShifting': True,
'edgeMinimization': True,
'parentCentralization': False,
'direction': 'LR',
'sortMethod': 'directed'
}
"autoResize": True,
"height": "100%",
"width": "100%",
"manipulation": False,
"layout": {
"improvedLayout": True,
"hierarchical": {
"enabled": True,
"levelSeparation": 450,
"nodeSpacing": 200,
"treeSpacing": 100,
"blockShifting": True,
"edgeMinimization": True,
"parentCentralization": False,
"direction": "LR",
"sortMethod": "directed",
},
},
'nodes': {
'color': '#000000',
'borderWidth': 1,
'borderWidthSelected': 2,
'chosen': True,
'shape': 'box',
'font': {'align': 'left', 'color': '#FFFFFF'},
"nodes": {
"color": "#000000",
"borderWidth": 1,
"borderWidthSelected": 2,
"chosen": True,
"shape": "box",
"font": {"align": "left", "color": "#FFFFFF"},
},
'edges': {
'font': {
'color': '#FFFFFF',
'face': 'arial',
'background': 'none',
'strokeWidth': 0,
'strokeColor': '#ffffff',
'align': 'horizontal',
'multi': False,
'vadjust': 0,
"edges": {
"font": {
"color": "#FFFFFF",
"face": "arial",
"background": "none",
"strokeWidth": 0,
"strokeColor": "#ffffff",
"align": "horizontal",
"multi": False,
"vadjust": 0,
}
},
'physics': {'enabled': False}
"physics": {"enabled": False},
}
phrack_opts = {
'nodes': {
'color': '#000000',
'borderWidth': 1,
'borderWidthSelected': 1,
'shapeProperties': {
'borderDashes': False,
'borderRadius': 0,
},
'chosen': True,
'shape': 'box',
'font': {'face': 'courier new', 'align': 'left', 'color': '#000000'},
"nodes": {
"color": "#000000",
"borderWidth": 1,
"borderWidthSelected": 1,
"shapeProperties": {"borderDashes": False, "borderRadius": 0},
"chosen": True,
"shape": "box",
"font": {"face": "courier new", "align": "left", "color": "#000000"},
},
'edges': {
'font': {
'color': '#000000',
'face': 'courier new',
'background': 'none',
'strokeWidth': 0,
'strokeColor': '#ffffff',
'align': 'horizontal',
'multi': False,
'vadjust': 0,
"edges": {
"font": {
"color": "#000000",
"face": "courier new",
"background": "none",
"strokeWidth": 0,
"strokeColor": "#ffffff",
"align": "horizontal",
"multi": False,
"vadjust": 0,
}
}
},
}
default_colors = [
{'border': '#26996f', 'background': '#2f7e5b', 'highlight': {'border': '#26996f', 'background': '#28a16f'}},
{'border': '#9e42b3', 'background': '#842899', 'highlight': {'border': '#9e42b3', 'background': '#933da6'}},
{'border': '#b82323', 'background': '#991d1d', 'highlight': {'border': '#b82323', 'background': '#a61f1f'}},
{'border': '#4753bf', 'background': '#3b46a1', 'highlight': {'border': '#4753bf', 'background': '#424db3'}},
{'border': '#26996f', 'background': '#2f7e5b', 'highlight': {'border': '#26996f', 'background': '#28a16f'}},
{'border': '#9e42b3', 'background': '#842899', 'highlight': {'border': '#9e42b3', 'background': '#933da6'}},
{'border': '#b82323', 'background': '#991d1d', 'highlight': {'border': '#b82323', 'background': '#a61f1f'}},
{'border': '#4753bf', 'background': '#3b46a1', 'highlight': {'border': '#4753bf', 'background': '#424db3'}},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#26996f", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#9e42b3", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#b82323", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#4753bf", "background": "#424db3"},
},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#26996f", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#9e42b3", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#b82323", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#4753bf", "background": "#424db3"},
},
]
phrack_color = {'border': '#000000', 'background': '#ffffff',
'highlight': {'border': '#000000', 'background': '#ffffff'}}
phrack_color = {
"border": "#000000",
"background": "#ffffff",
"highlight": {"border": "#000000", "background": "#ffffff"},
}
def extract_nodes(statespace, color_map):
"""
:param statespace:
:param color_map:
:return:
"""
nodes = []
for node_key in statespace.nodes:
node = statespace.nodes[node_key]
instructions = [state.get_current_instruction() for state in node.states]
code_split = []
for instruction in instructions:
if instruction['opcode'].startswith("PUSH"):
code_line = "%d %s %s" % (instruction['address'], instruction['opcode'], instruction['argument'])
elif instruction['opcode'].startswith("JUMPDEST") and NodeFlags.FUNC_ENTRY in node.flags and instruction['address'] == node.start_addr:
if instruction["opcode"].startswith("PUSH"):
code_line = "%d %s %s" % (
instruction["address"],
instruction["opcode"],
instruction["argument"],
)
elif (
instruction["opcode"].startswith("JUMPDEST")
and NodeFlags.FUNC_ENTRY in node.flags
and instruction["address"] == node.start_addr
):
code_line = node.function_name
else:
code_line = "%d %s" % (instruction['address'], instruction['opcode'])
code_line = "%d %s" % (instruction["address"], instruction["opcode"])
code_line = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code_line)
code_line = re.sub(
"([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code_line
)
code_split.append(code_line)
truncated_code = '\n'.join(code_split) if (len(code_split) < 7) \
else '\n'.join(code_split[:6]) + "\n(click to expand +)"
nodes.append({
'id': str(node_key),
'color': color_map[node.get_cfg_dict()['contract_name']],
'size': 150,
'fullLabel': '\n'.join(code_split),
'label': truncated_code,
'truncLabel': truncated_code,
'isExpanded': False
})
truncated_code = (
"\n".join(code_split)
if (len(code_split) < 7)
else "\n".join(code_split[:6]) + "\n(click to expand +)"
)
nodes.append(
{
"id": str(node_key),
"color": color_map[node.get_cfg_dict()["contract_name"]],
"size": 150,
"fullLabel": "\n".join(code_split),
"label": truncated_code,
"truncLabel": truncated_code,
"isExpanded": False,
}
)
return nodes
def extract_edges(statespace):
"""
:param statespace:
:return:
"""
edges = []
for edge in statespace.edges:
if edge.condition is None:
label = ""
else:
try:
label = str(z3.simplify(edge.condition)).replace("\n", "")
except z3.Z3Exception:
label = str(simplify(edge.condition)).replace("\n", "")
except Z3Exception:
label = str(edge.condition).replace("\n", "")
label = re.sub(r'([^_])([\d]{2}\d+)', lambda m: m.group(1) + hex(int(m.group(2))), label)
edges.append({
'from': str(edge.as_dict['from']),
'to': str(edge.as_dict['to']),
'arrows': 'to',
'label': label,
'smooth': {'type': 'cubicBezier'}
})
label = re.sub(
r"([^_])([\d]{2}\d+)", lambda m: m.group(1) + hex(int(m.group(2))), label
)
edges.append(
{
"from": str(edge.as_dict["from"]),
"to": str(edge.as_dict["to"]),
"arrows": "to",
"label": label,
"smooth": {"type": "cubicBezier"},
}
)
return edges
def generate_graph(statespace, title="Mythril / Ethereum LASER Symbolic VM", physics=False, phrackify=False):
env = Environment(loader=PackageLoader('mythril.analysis'), autoescape=select_autoescape(['html', 'xml']))
template = env.get_template('callgraph.html')
def generate_graph(
statespace,
title="Mythril / Ethereum LASER Symbolic VM",
physics=False,
phrackify=False,
):
"""
:param statespace:
:param title:
:param physics:
:param phrackify:
:return:
"""
env = Environment(
loader=PackageLoader("mythril.analysis"),
autoescape=select_autoescape(["html", "xml"]),
)
template = env.get_template("callgraph.html")
graph_opts = default_opts
accounts = statespace.accounts
@ -154,13 +237,17 @@ def generate_graph(statespace, title="Mythril / Ethereum LASER Symbolic VM", phy
color_map = {accounts[k].contract_name: phrack_color for k in accounts}
graph_opts.update(phrack_opts)
else:
color_map = {accounts[k].contract_name: default_colors[i % len(default_colors)] for i, k in enumerate(accounts)}
color_map = {
accounts[k].contract_name: default_colors[i % len(default_colors)]
for i, k in enumerate(accounts)
}
graph_opts['physics']['enabled'] = physics
graph_opts["physics"]["enabled"] = physics
return template.render(title=title,
nodes=extract_nodes(statespace, color_map),
edges=extract_edges(statespace),
phrackify=phrackify,
opts=graph_opts
)
return template.render(
title=title,
nodes=extract_nodes(statespace, color_map),
edges=extract_edges(statespace),
phrackify=phrackify,
opts=graph_opts,
)

@ -0,0 +1,69 @@
"""This module contains the base class for all user-defined detection
modules."""
import logging
from typing import List
log = logging.getLogger(__name__)
class DetectionModule:
"""The base detection module.
All custom-built detection modules must inherit from this class.
"""
def __init__(
self,
name: str,
swc_id: str,
description: str,
entrypoint: str = "post",
pre_hooks: List[str] = None,
post_hooks: List[str] = None,
):
self.name = name
self.swc_id = swc_id
self.pre_hooks = pre_hooks if pre_hooks else []
self.post_hooks = post_hooks if post_hooks else []
self.description = description
if entrypoint not in ("post", "callback"):
log.error(
"Invalid entrypoint in module %s, must be one of {post, callback}",
self.name,
)
self.entrypoint = entrypoint
self._issues = []
@property
def issues(self):
"""
Returns the issues
"""
return self._issues
def reset_module(self):
"""
Resets issues
"""
self._issues = []
def execute(self, statespace):
"""The entry point for execution, which is being called by Mythril.
:param statespace:
:return:
"""
raise NotImplementedError()
def __repr__(self) -> str:
return (
"<"
"DetectionModule "
"name={0.name} "
"swc_id={0.swc_id} "
"pre_hooks={0.pre_hooks} "
"post_hooks={0.post_hooks} "
"description={0.description}"
">"
).format(self)

@ -1,57 +1,96 @@
"""This module contains the detection code for insecure delegate call usage."""
import re
import logging
from typing import List
from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT
from mythril.analysis.ops import get_variable, VarType
from mythril.analysis.ops import get_variable, VarType, Call, Variable
from mythril.analysis.report import Issue
import logging
from mythril.analysis.call_helpers import get_call_from_state
from mythril.analysis.modules.base import DetectionModule
from mythril.laser.ethereum.state.global_state import GlobalState
log = logging.getLogger(__name__)
class DelegateCallModule(DetectionModule):
"""This module detects calldata being forwarded using DELEGATECALL."""
'''
MODULE DESCRIPTION:
def __init__(self):
""""""
super().__init__(
name="DELEGATECALL Usage in Fallback Function",
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT,
description="Check for invocations of delegatecall(msg.data) in the fallback function.",
entrypoint="callback",
pre_hooks=["DELEGATECALL"],
)
Check for invocations of delegatecall(msg.data) in the fallback function.
'''
def execute(self, state: GlobalState) -> list:
"""
:param state:
:return:
"""
log.debug("Executing module: DELEGATE_CALL")
self._issues.extend(_analyze_states(state))
return self.issues
def execute(statespace):
def _analyze_states(state: GlobalState) -> List[Issue]:
"""
Executes analysis module for delegate call analysis module
:param statespace: Statespace to analyse
:return: Found issues
:param state: the current state
:return: returns the issues for that corresponding state
"""
call = get_call_from_state(state)
if call is None:
return []
issues = []
for call in statespace.calls:
if call.type is not "DELEGATECALL":
continue
if call.node.function_name is not "fallback":
continue
state = call.state
address = state.get_current_instruction()['address']
meminstart = get_variable(state.mstate.stack[-3])
if call.type is not "DELEGATECALL":
return []
if call.node.function_name is not "fallback":
return []
if meminstart.type == VarType.CONCRETE:
issues += _concrete_call(call, state, address, meminstart)
state = call.state
address = state.get_current_instruction()["address"]
meminstart = get_variable(state.mstate.stack[-3])
if call.to.type == VarType.SYMBOLIC:
issues += _symbolic_call(call, state, address, statespace)
if meminstart.type == VarType.CONCRETE:
issues += _concrete_call(call, state, address, meminstart)
return issues
def _concrete_call(call, state, address, meminstart):
if not re.search(r'calldata.*_0', str(state.mstate.memory[meminstart.val])):
def _concrete_call(
call: Call, state: GlobalState, address: int, meminstart: Variable
) -> List[Issue]:
"""
:param call: The current call's information
:param state: The current state
:param address: The PC address
:param meminstart: memory starting position
:return: issues
"""
if not re.search(r"calldata.*\[0", str(state.mstate.memory[meminstart.val])):
return []
issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address,
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, title="Call data forwarded with delegatecall()",
_type="Informational")
issue.description = \
"This contract forwards its call data via DELEGATECALL in its fallback function. " \
"This means that any function in the called contract can be executed. Note that the callee contract will have " \
"access to the storage of the calling contract.\n "
issue = Issue(
contract=call.node.contract_name,
function_name=call.node.function_name,
address=address,
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT,
bytecode=state.environment.code.bytecode,
title="Delegatecall Proxy",
severity="Low",
description_head="The contract implements a delegatecall proxy.",
description_tail="The smart contract forwards the received calldata via delegatecall. Note that callers"
"can execute arbitrary functions in the callee contract and that the callee contract "
"can access the storage of the calling contract. "
"Make sure that the callee contract is audited properly.",
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
target = hex(call.to.val) if call.to.type == VarType.CONCRETE else str(call.to)
issue.description += "DELEGATECALL target: {}".format(target)
@ -59,27 +98,4 @@ def _concrete_call(call, state, address, meminstart):
return [issue]
def _symbolic_call(call, state, address, statespace):
issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address,
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, title=call.type + " to a user-supplied address")
if "calldata" in str(call.to):
issue.description = \
"This contract delegates execution to a contract address obtained from calldata. "
else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if m:
idx = m.group(1)
func = statespace.find_storage_write(state.environment.active_account.address, idx)
if func:
issue.description = "This contract delegates execution to a contract address in storage slot " + str(
idx) + ". This storage slot can be written to by calling the function `" + func + "`. "
else:
logging.debug("[DELEGATECALL] No storage writes to index " + str(idx))
issue.description += "Be aware that the called contract gets unrestricted access to this contract's state."
return [issue]
detector = DelegateCallModule()

@ -1,129 +1,224 @@
"""This module contains the detection code for predictable variable
dependence."""
import logging
import re
from z3 import *
from mythril.analysis.ops import VarType
from mythril.analysis import solver
from mythril.analysis.call_helpers import get_call_from_state
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.ops import Call, VarType
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import TIMESTAMP_DEPENDENCE, PREDICTABLE_VARS_DEPENDENCE
from mythril.analysis.swc_data import TIMESTAMP_DEPENDENCE, WEAK_RANDOMNESS
from mythril.exceptions import UnsatError
import logging
from mythril.laser.ethereum.state.global_state import GlobalState
'''
MODULE DESCRIPTION:
log = logging.getLogger(__name__)
Check for CALLs that send >0 Ether as a result of computation based on predictable variables such as
block.coinbase, block.gaslimit, block.timestamp, block.number
TODO:
- block.blockhash(block.number-1)
- block.blockhash(some_block_past_256_blocks_from_now)==0
- external source of random numbers (e.g. Oraclize)
'''
class PredictableDependenceModule(DetectionModule):
"""This module detects whether Ether is sent using predictable
parameters."""
def __init__(self):
""""""
super().__init__(
name="Dependence of Predictable Variables",
swc_id="{} {}".format(TIMESTAMP_DEPENDENCE, WEAK_RANDOMNESS),
description=(
"Check for CALLs that send >0 Ether as a result of computation "
"based on predictable variables such as block.coinbase, "
"block.gaslimit, block.timestamp, block.number"
),
entrypoint="callback",
pre_hooks=["CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"],
)
def execute(statespace):
def execute(self, state: GlobalState) -> list:
"""
logging.debug("Executing module: DEPENDENCE_ON_PREDICTABLE_VARS")
:param state:
:return:
"""
log.debug("Executing module: DEPENDENCE_ON_PREDICTABLE_VARS")
self._issues.extend(_analyze_states(state))
return self.issues
issues = []
for call in statespace.calls:
if "callvalue" in str(call.value):
logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] Skipping refund function")
continue
# We're only interested in calls that send Ether
if call.value.type == VarType.CONCRETE and call.value.val == 0:
continue
address = call.state.get_current_instruction()['address']
description = "In the function `" + call.node.function_name + "` "
description += "the following predictable state variables are used to determine Ether recipient:\n"
# First check: look for predictable state variables in node & call recipient constraints
vars = ["coinbase", "gaslimit", "timestamp", "number"]
found = []
for var in vars:
for constraint in call.node.constraints + [call.to]:
if var in str(constraint):
found.append(var)
if len(found):
for item in found:
description += "- block.{}\n".format(item)
if solve(call):
swc_type = TIMESTAMP_DEPENDENCE if item == 'timestamp' else PREDICTABLE_VARS_DEPENDENCE
issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address,
swc_id=swc_type, title="Dependence on predictable environment variable",
_type="Warning", description=description)
issues.append(issue)
# Second check: blockhash
for constraint in call.node.constraints + [call.to]:
if "blockhash" in str(constraint):
description = "In the function `" + call.node.function_name + "` "
if "number" in str(constraint):
m = re.search(r'blockhash\w+(\s-\s(\d+))*', str(constraint))
if m and solve(call):
found = m.group(1)
if found: # block.blockhash(block.number - N)
description += "predictable expression 'block.blockhash(block.number - " + m.group(2) + \
")' is used to determine Ether recipient"
if int(m.group(2)) > 255:
description += ", this expression will always be equal to zero."
elif "storage" in str(constraint): # block.blockhash(block.number - storage_0)
description += "predictable expression 'block.blockhash(block.number - " + \
"some_storage_var)' is used to determine Ether recipient"
else: # block.blockhash(block.number)
description += "predictable expression 'block.blockhash(block.number)'" + \
" is used to determine Ether recipient"
description += ", this expression will always be equal to zero."
issue = Issue(contract=call.node.contract_name, function=call.node.function_name,
address=address, title="Dependence on predictable variable",
_type="Warning", description=description, swc_id=PREDICTABLE_VARS_DEPENDENCE)
detector = PredictableDependenceModule()
def _analyze_states(state: GlobalState) -> list:
"""
:param state:
:return:
"""
issues = []
call = get_call_from_state(state)
if call is None:
return []
if "callvalue" in str(call.value):
log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] Skipping refund function")
return []
# We're only interested in calls that send Ether
if call.value.type == VarType.CONCRETE and call.value.val == 0:
return []
address = call.state.get_current_instruction()["address"]
description = (
"The contract sends Ether depending on the values of the following variables:\n"
)
# First check: look for predictable state variables in node & call recipient constraints
vars = ["coinbase", "gaslimit", "timestamp", "number"]
found = []
for var in vars:
for constraint in call.node.constraints[:] + [call.to]:
if var in str(constraint):
found.append(var)
if len(found):
for item in found:
description += "- block.{}\n".format(item)
if solve(call):
swc_id = TIMESTAMP_DEPENDENCE if item == "timestamp" else WEAK_RANDOMNESS
description += (
"Note that the values of variables like coinbase, gaslimit, block number and timestamp "
"are predictable and/or can be manipulated by a malicious miner. "
"Don't use them for random number generation or to make critical decisions."
)
issue = Issue(
contract=call.node.contract_name,
function_name=call.node.function_name,
address=address,
swc_id=swc_id,
bytecode=call.state.environment.code.bytecode,
title="Dependence on predictable environment variable",
severity="Low",
description_head="Sending of Ether depends on a predictable variable.",
description_tail=description,
gas_used=(
call.state.mstate.min_gas_used,
call.state.mstate.max_gas_used,
),
)
issues.append(issue)
# Second check: blockhash
for constraint in call.node.constraints[:] + [call.to]:
if "blockhash" in str(constraint):
if "number" in str(constraint):
m = re.search(r"blockhash\w+(\s-\s(\d+))*", str(constraint))
if m and solve(call):
found = m.group(1)
if found: # block.blockhash(block.number - N)
description = (
"The predictable expression 'block.blockhash(block.number - "
+ m.group(2)
+ ")' is used to determine Ether recipient"
)
if int(m.group(2)) > 255:
description += (
", this expression will always be equal to zero."
)
elif "storage" in str(
constraint
): # block.blockhash(block.number - storage_0)
description = (
"The predictable expression 'block.blockhash(block.number - "
+ "some_storage_var)' is used to determine Ether recipient"
)
else: # block.blockhash(block.number)
description = (
"The predictable expression 'block.blockhash(block.number)'"
+ " is used to determine Ether recipient"
)
description += ", this expression will always be equal to zero."
issue = Issue(
contract=call.node.contract_name,
function_name=call.node.function_name,
address=address,
bytecode=call.state.environment.code.bytecode,
title="Dependence on Predictable Variable",
severity="Low",
description_head="Sending of Ether depends on the blockhash.",
description_tail=description,
swc_id=WEAK_RANDOMNESS,
gas_used=(
call.state.mstate.min_gas_used,
call.state.mstate.max_gas_used,
),
)
issues.append(issue)
break
else:
r = re.search(r"storage_([a-z0-9_&^]+)", str(constraint))
if r: # block.blockhash(storage_0)
"""We actually can do better here by adding a constraint
blockhash_block_storage_0 == 0 and checking model
satisfiability.
When this is done, severity can be raised from
'Informational' to 'Warning'. Checking that storage
at given index can be tainted is not necessary,
since it usually contains block.number of the
'commit' transaction in commit-reveal workflow.
"""
index = r.group(1)
if index and solve(call):
description = (
"A block hash is calculated using the block.blockhash(uint blockNumber) method. "
"The block number is obtained from storage index {}".format(
index
)
)
issue = Issue(
contract=call.node.contract_name,
function_name=call.node.function_name,
address=address,
bytecode=call.state.environment.code.bytecode,
title="Dependence on Predictable Variable",
severity="Low",
description_head="Sending of Ether depends on the blockhash.",
description_tail=description,
swc_id=WEAK_RANDOMNESS,
gas_used=(
call.state.mstate.min_gas_used,
call.state.mstate.max_gas_used,
),
)
issues.append(issue)
break
else:
r = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))
if r: # block.blockhash(storage_0)
'''
We actually can do better here by adding a constraint blockhash_block_storage_0 == 0
and checking model satisfiability. When this is done, severity can be raised
from 'Informational' to 'Warning'.
Checking that storage at given index can be tainted is not necessary, since it usually contains
block.number of the 'commit' transaction in commit-reveal workflow.
'''
index = r.group(1)
if index and solve(call):
description += 'block.blockhash() is calculated using a value from storage ' \
'at index {}'.format(index)
issue = Issue(contract=call.node.contract_name, function=call.node.function_name,
address=address, title="Dependence on predictable variable",
_type="Informational", description=description, swc_id=PREDICTABLE_VARS_DEPENDENCE)
issues.append(issue)
break
return issues
def solve(call):
def solve(call: Call) -> bool:
"""
:param call:
:return:
"""
try:
model = solver.get_model(call.node.constraints)
logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model))
log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] MODEL: " + str(model))
pretty_model = solver.pretty_print_model(model)
for d in model.decls():
logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] main model: \n%s" % pretty_model)
return True
except UnsatError:
logging.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] no model found")
log.debug("[DEPENDENCE_ON_PREDICTABLE_VARS] no model found")
return False

@ -1,36 +1,87 @@
"""This module contains the detection code for deprecated op codes."""
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import TX_ORIGIN_USAGE
from mythril.analysis.swc_data import DEPRICATED_FUNCTIONS_USAGE
from mythril.analysis.modules.base import DetectionModule
from mythril.laser.ethereum.state.global_state import GlobalState
import logging
log = logging.getLogger(__name__)
'''
MODULE DESCRIPTION:
DESCRIPTION = """
Check for usage of deprecated opcodes
"""
Check for constraints on tx.origin (i.e., access to some functionality is restricted to a specific origin).
'''
def _analyze_state(state):
"""
def execute(statespace):
:param state:
:return:
"""
node = state.node
instruction = state.get_current_instruction()
logging.debug("Executing module: DEPRECATED OPCODES")
if instruction["opcode"] == "ORIGIN":
log.debug("ORIGIN in function " + node.function_name)
title = "Use of tx.origin"
description_head = "Use of tx.origin is deprecated."
description_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".format(
node.function_name
)
)
swc_id = DEPRICATED_FUNCTIONS_USAGE
issues = []
elif instruction["opcode"] == "CALLCODE":
log.debug("CALLCODE in function " + node.function_name)
title = "Use of callcode"
description_head = "Use of callcode is deprecated."
description_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."
)
swc_id = DEPRICATED_FUNCTIONS_USAGE
for k in statespace.nodes:
node = statespace.nodes[k]
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=instruction["address"],
title=title,
bytecode=state.environment.code.bytecode,
swc_id=swc_id,
severity="Medium",
description_head=description_head,
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
return [issue]
for state in node.states:
instruction = state.get_current_instruction()
class DeprecatedOperationsModule(DetectionModule):
"""This module checks for the usage of deprecated op codes."""
if instruction['opcode'] == "ORIGIN":
description = "The function `{}` retrieves the transaction origin (tx.origin) using the ORIGIN opcode. " \
"Use msg.sender instead.\nSee also: " \
"https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin".format(node.function_name)
def __init__(self):
""""""
super().__init__(
name="Deprecated Operations",
swc_id=DEPRICATED_FUNCTIONS_USAGE,
description=DESCRIPTION,
entrypoint="callback",
pre_hooks=["ORIGIN", "CALLCODE"],
)
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
title="Use of tx.origin", _type="Warning", swc_id=TX_ORIGIN_USAGE,
description=description)
issues.append(issue)
def execute(self, state: GlobalState):
"""
return issues
:param state:
:return:
"""
self._issues.extend(_analyze_state(state))
return self.issues
detector = DeprecatedOperationsModule()

@ -1,127 +0,0 @@
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis import solver
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError
import re
import logging
'''
MODULE DESCRIPTION:
Check for CALLs that send >0 Ether to either the transaction sender, or to an address provided as a function argument.
If msg.sender is checked against a value in storage, check whether that storage index is tainted (i.e. there's an unconstrained write
to that index).
'''
def execute(statespace):
logging.debug("Executing module: ETHER_SEND")
issues = []
for call in statespace.calls:
state = call.state
address = state.get_current_instruction()['address']
if "callvalue" in str(call.value):
logging.debug("[ETHER_SEND] Skipping refund function")
continue
# We're only interested in calls that send Ether
if call.value.type == VarType.CONCRETE and call.value.val == 0:
continue
interesting = False
description = "In the function `" + call.node.function_name + "` "
if re.search(r'caller', str(call.to)):
description += "a non-zero amount of Ether is sent to msg.sender.\n"
interesting = True
elif re.search(r'calldata', str(call.to)):
description += "a non-zero amount of Ether is sent to an address taken from function arguments.\n"
interesting = True
else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if m:
idx = m.group(1)
description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(idx) + ".\n"
func = statespace.find_storage_write(state.environment.active_account.address, idx)
if func:
description += "There is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`.\n"
interesting = True
else:
logging.debug("[ETHER_SEND] No storage writes to index " + str(idx))
if interesting:
node = call.node
can_solve = True
constrained = False
index = 0
while can_solve and index < len(node.constraints):
constraint = node.constraints[index]
index += 1
logging.debug("[ETHER_SEND] Constraint: " + str(constraint))
m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))
if m:
constrained = True
idx = m.group(1)
func = statespace.find_storage_write(state.environment.active_account.address, idx)
if func:
description += "\nThere is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`."
else:
logging.debug("[ETHER_SEND] No storage writes to index " + str(idx))
can_solve = False
break
# CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer
elif re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint)):
constrained = True
can_solve = False
break
if not constrained:
description += "It seems that this function can be called without restrictions."
if can_solve:
try:
model = solver.get_model(node.constraints)
for d in model.decls():
logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(model)
issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address,
title="Ether send", _type="Warning", swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
description=description, debug=debug)
issues.append(issue)
except UnsatError:
logging.debug("[ETHER_SEND] no model found")
return issues

@ -0,0 +1,127 @@
"""This module contains the detection code for unauthorized ether
withdrawal."""
import logging
import json
from copy import copy
from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import UGT, BVAddNoOverflow, Sum, symbol_factory
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for cases where Ether can be withdrawn to a user-specified address.
An issue is reported if:
- The transaction sender does not match contract creator;
- The sender address can be chosen arbitrarily;
- The receiver address is identical to the sender address;
- The sender can withdraw *more* than the total amount they sent over all transactions.
"""
class EtherThief(DetectionModule):
"""This module search for cases where Ether can be withdrawn to a user-
specified address."""
def __init__(self):
""""""
super().__init__(
name="Ether Thief",
swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
description=DESCRIPTION,
entrypoint="callback",
pre_hooks=["CALL"],
)
self._cache_addresses = {}
def reset_module(self):
"""
Resets the module by clearing everything
:return:
"""
super().reset_module()
self._cache_addresses = {}
def execute(self, state: GlobalState):
"""
:param state:
:return:
"""
self._issues.extend(self._analyze_state(state))
return self.issues
def _analyze_state(self, state):
"""
:param state:
:return:
"""
instruction = state.get_current_instruction()
node = state.node
if instruction["opcode"] != "CALL":
return []
address = instruction["address"]
if self._cache_addresses.get(address, False):
return []
call_value = state.mstate.stack[-3]
target = state.mstate.stack[-2]
eth_sent_total = symbol_factory.BitVecVal(0, 256)
constraints = copy(node.constraints)
for tx in state.world_state.transaction_sequence:
if tx.caller == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF:
# There's sometimes no overflow check on balances added.
# But we don't care about attacks that require more 2^^256 ETH to be sent.
constraints += [BVAddNoOverflow(eth_sent_total, tx.call_value, False)]
eth_sent_total = Sum(eth_sent_total, tx.call_value)
constraints += [
UGT(call_value, eth_sent_total),
target == state.environment.sender,
]
try:
transaction_sequence = solver.get_transaction_sequence(state, constraints)
debug = json.dumps(transaction_sequence, indent=4)
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=instruction["address"],
swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
title="Unprotected Ether Withdrawal",
severity="High",
bytecode=state.environment.code.bytecode,
description_head="Anyone can withdraw ETH from the contract account.",
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,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
except UnsatError:
log.debug("[ETHER_THIEF] no model found")
return []
self._cache_addresses[address] = True
return [issue]
detector = EtherThief()

@ -1,51 +1,84 @@
"""This module contains the detection code for reachable exceptions."""
import logging
import json
from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import ASSERT_VIOLATION
from mythril.exceptions import UnsatError
from mythril.analysis import solver
import logging
from mythril.laser.ethereum.state.global_state import GlobalState
log = logging.getLogger(__name__)
'''
MODULE DESCRIPTION:
def _analyze_state(state) -> list:
"""
Checks whether any exception states are reachable.
:param state:
:return:
"""
log.info("Exceptions module: found ASSERT_FAIL instruction")
node = state.node
'''
log.debug("ASSERT_FAIL in function " + node.function_name)
try:
address = state.get_current_instruction()["address"]
def execute(statespace):
description_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."
)
logging.debug("Executing module: EXCEPTIONS")
transaction_sequence = solver.get_transaction_sequence(state, node.constraints)
debug = json.dumps(transaction_sequence, indent=4)
issues = []
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=address,
swc_id=ASSERT_VIOLATION,
title="Exception State",
severity="Low",
description_head="A reachable exception has been detected.",
description_tail=description_tail,
bytecode=state.environment.code.bytecode,
debug=debug,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
return [issue]
for k in statespace.nodes:
node = statespace.nodes[k]
except UnsatError:
log.debug("no model found")
for state in node.states:
return []
instruction = state.get_current_instruction()
if instruction['opcode'] == "ASSERT_FAIL":
try:
model = solver.get_model(node.constraints)
address = state.get_current_instruction()['address']
description = "A reachable exception (opcode 0xfe) has been detected. " \
"This can be caused by type errors, division by zero, " \
"out-of-bounds array access, or assert violations. "
description += "This is acceptable in most situations. " \
"Note however that `assert()` should only be used to check invariants. " \
"Use `require()` for regular input checking. "
class ReachableExceptionsModule(DetectionModule):
""""""
debug = "The exception is triggered under the following conditions:\n\n"
def __init__(self):
""""""
super().__init__(
name="Reachable Exceptions",
swc_id=ASSERT_VIOLATION,
description="Checks whether any exception states are reachable.",
entrypoint="callback",
pre_hooks=["ASSERT_FAIL"],
)
debug += solver.pretty_print_model(model)
def execute(self, state: GlobalState) -> list:
"""
issues.append(Issue(contract=node.contract_name, function=node.function_name, address=address,
swc_id=ASSERT_VIOLATION, title="Exception state", _type="Informational",
description=description, debug=debug))
:param state:
:return:
"""
self._issues.extend(_analyze_state(state))
return self.issues
except UnsatError:
logging.debug("[EXCEPTIONS] no model found")
return issues
detector = ReachableExceptionsModule()

@ -1,134 +1,130 @@
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
"""This module contains the detection code for potentially insecure low-level
calls."""
from mythril.analysis import solver
from mythril.analysis.swc_data import REENTRANCY
import re
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.laser.smt import UGT, symbol_factory
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.exceptions import UnsatError
import logging
'''
MODULE DESCRIPTION:
Check for call.value()() to external addresses
'''
MAX_SEARCH_DEPTH = 64
def search_children(statespace, node, start_index=0, depth=0, results=None):
if results is None:
results = []
logging.debug("SEARCHING NODE %d", node.uid)
if depth < MAX_SEARCH_DEPTH:
n_states = len(node.states)
if n_states > start_index:
for j in range(start_index, n_states):
if node.states[j].get_current_instruction()['opcode'] == 'SSTORE':
results.append(node.states[j].get_current_instruction()['address'])
children = []
for edge in statespace.edges:
if edge.node_from == node.uid:
children.append(statespace.nodes[edge.node_to])
if len(children):
for node in children:
return search_children(statespace, node, depth=depth + 1, results=results)
return results
calls_visited = []
def execute(statespace):
issues = []
for call in statespace.calls:
state = call.state
address = state.get_current_instruction()['address']
if call.type == "CALL":
logging.info("[EXTERNAL_CALLS] Call to: %s, value = %s, gas = %s" % (str(call.to), str(call.value), str(call.gas)))
if call.to.type == VarType.SYMBOLIC and (call.gas.type == VarType.CONCRETE and call.gas.val > 2300) or (call.gas.type == VarType.SYMBOLIC and "2300" not in str(call.gas)):
description = "This contract executes a message call to "
target = str(call.to)
user_supplied = False
if "calldata" in target or "caller" in target:
if "calldata" in target:
description += "an address provided as a function argument. "
else:
description += "the address of the transaction sender. "
user_supplied = True
else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if m:
idx = m.group(1)
func = statespace.find_storage_write(state.environment.active_account.address, idx)
if func:
description += \
"an address found at storage slot " + str(idx) + ". " + \
"This storage slot can be written to by calling the function `" + func + "`. "
user_supplied = True
if user_supplied:
description += "Generally, it is not recommended to call user-supplied addresses using Solidity's call() construct. " \
"Note that attackers might leverage reentrancy attacks to exploit race conditions or manipulate this contract's state."
issue = Issue(contract=call.node.contract_name, function=call.node.function_name,
address=address, title="Message call to external contract", _type="Warning",
description=description, swc_id=REENTRANCY)
else:
description += "to another contract. Make sure that the called contract is trusted and does not execute user-supplied code."
issue = Issue(contract=call.node.contract_name, function=call.node.function_name, address=address,
title="Message call to external contract", _type="Informational",
description=description, swc_id=REENTRANCY)
issues.append(issue)
if address not in calls_visited:
calls_visited.append(address)
logging.debug("[EXTERNAL_CALLS] Checking for state changes starting from " + call.node.function_name)
# Check for SSTORE in remaining instructions in current node & nodes down the CFG
state_change_addresses = search_children(statespace, call.node, call.state_index + 1, depth=0, results=[])
logging.debug("[EXTERNAL_CALLS] Detected state changes at addresses: " + str(state_change_addresses))
if len(state_change_addresses):
for address in state_change_addresses:
description = "The contract account state is changed after an external call. " \
"Consider that the called contract could re-enter the function before this " \
"state change takes place. This can lead to business logic vulnerabilities."
issue = Issue(contract=call.node.contract_name, function=call.node.function_name,
address=address, title="State change after external call", _type="Warning",
description=description, swc_id=REENTRANCY)
issues.append(issue)
return issues
import json
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for low level calls (e.g. call.value()) that forward all gas to the callee.
Report a warning if the callee address can be set by the sender, otherwise create
an informational issue.
"""
def _analyze_state(state):
"""
:param state:
:return:
"""
node = state.node
gas = state.mstate.stack[-1]
to = state.mstate.stack[-2]
address = state.get_current_instruction()["address"]
try:
constraints = node.constraints
transaction_sequence = solver.get_transaction_sequence(
state, constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))]
)
# Check whether we can also set the callee address
try:
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 "
"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."
)
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=address,
swc_id=REENTRANCY,
title="External Call To User-Supplied Address",
bytecode=state.environment.code.bytecode,
severity="Medium",
description_head=description_head,
description_tail=description_tail,
debug=debug,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
except UnsatError:
log.debug(
"[EXTERNAL_CALLS] Callee address cannot be modified. Reporting informational issue."
)
debug = json.dumps(transaction_sequence, indent=4)
description_head = "The contract executes an external message call."
description_tail = (
"An external function call to a fixed contract address is executed. Make sure "
"that the callee contract has been reviewed carefully."
)
issue = Issue(
contract=node.contract_name,
function_name=state.node.function_name,
address=address,
swc_id=REENTRANCY,
title="External Call To Fixed Address",
bytecode=state.environment.code.bytecode,
severity="Low",
description_head=description_head,
description_tail=description_tail,
debug=debug,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
except UnsatError:
log.debug("[EXTERNAL_CALLS] No model found.")
return []
return [issue]
class ExternalCalls(DetectionModule):
"""This module searches for low level calls (e.g. call.value()) that
forward all gas to the callee."""
def __init__(self):
""""""
super().__init__(
name="External calls",
swc_id=REENTRANCY,
description=DESCRIPTION,
entrypoint="callback",
pre_hooks=["CALL"],
)
def execute(self, state: GlobalState):
"""
:param state:
:return:
"""
self._issues.extend(_analyze_state(state))
return self.issues
detector = ExternalCalls()

@ -1,291 +1,290 @@
from z3 import *
"""This module contains the detection code for integer overflows and
underflows."""
import json
from mythril.analysis import solver
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import INTEGER_OVERFLOW_AND_UNDERFLOW
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.taint_analysis import TaintRunner
import re
import copy
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.analysis.modules.base import DetectionModule
from mythril.laser.smt import (
BVAddNoOverflow,
BVSubNoUnderflow,
BVMulNoOverflow,
BitVec,
symbol_factory,
Not,
Expression,
)
import logging
'''
MODULE DESCRIPTION:
Check for integer underflows.
For every SUB instruction, check if there's a possible state where op1 > op0.
For every ADD, MUL instruction, check if there's a possible state where op1 + op0 > 2^32 - 1
'''
def execute(statespace):
"""
Executes analysis module for integer underflow and integer overflow
:param statespace: Statespace to analyse
:return: Found issues
"""
logging.debug("Executing module: INTEGER")
issues = []
for k in statespace.nodes:
node = statespace.nodes[k]
for state in node.states:
issues += _check_integer_underflow(statespace, state, node)
issues += _check_integer_overflow(statespace, state, node)
return issues
def _check_integer_overflow(statespace, state, node):
"""
Checks for integer overflow
:param statespace: statespace that is being examined
:param state: state from node to examine
:param node: node to examine
:return: found issue
"""
issues = []
# Check the instruction
instruction = state.get_current_instruction()
if instruction['opcode'] not in ("ADD", "MUL"):
return issues
# Formulate overflow constraints
stack = state.mstate.stack
op0, op1 = stack[-1], stack[-2]
# An integer overflow is possible if op0 + op1 or op0 * op1 > MAX_UINT
# Do a type check
allowed_types = [int, BitVecRef, BitVecNumRef]
if not (type(op0) in allowed_types and type(op1) in allowed_types):
return issues
# Change ints to BitVec
if type(op0) is int:
op0 = BitVecVal(op0, 256)
if type(op1) is int:
op1 = BitVecVal(op1, 256)
# Formulate expression
if instruction['opcode'] == "ADD":
expr = op0 + op1
else:
expr = op1 * op0
# Check satisfiable
constraint = Or(And(ULT(expr, op0), op1 != 0), And(ULT(expr, op1), op0 != 0))
model = _try_constraints(node.constraints, [constraint])
if model is None:
logging.debug("[INTEGER_OVERFLOW] no model found")
return issues
if not _verify_integer_overflow(statespace, node, expr, state, model, constraint, op0, op1):
return issues
# Build issue
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, title="Integer Overflow", _type="Warning")
issue.description = "A possible integer overflow exists in the function `{}`.\n" \
"The addition or multiplication may result in a value higher than the maximum representable integer.".format(
node.function_name)
issue.debug = solver.pretty_print_model(model)
issues.append(issue)
return issues
def _verify_integer_overflow(statespace, node, expr, state, model, constraint, op0, op1):
""" Verifies existence of integer overflow """
# If we get to this point then there has been an integer overflow
# Find out if the overflowed value is actually used
interesting_usages = _search_children(statespace, node, expr, constraint=[constraint], index=node.states.index(state))
# Stop if it isn't
if len(interesting_usages) == 0:
return False
return _try_constraints(node.constraints, [Not(constraint)]) is not None
def _try_constraints(constraints, new_constraints):
"""
Tries new constraints
:return Model if satisfiable otherwise None
"""
_constraints = copy.deepcopy(constraints)
for constraint in new_constraints:
_constraints.append(copy.deepcopy(constraint))
try:
model = solver.get_model(_constraints)
return model
except UnsatError:
return None
def _check_integer_underflow(statespace, state, node):
"""
Checks for integer underflow
:param state: state from node to examine
:param node: node to examine
:return: found issue
"""
issues = []
instruction = state.get_current_instruction()
if instruction['opcode'] == "SUB":
log = logging.getLogger(__name__)
class OverUnderflowAnnotation:
def __init__(self, overflowing_state: GlobalState, operator: str, constraint):
self.overflowing_state = overflowing_state
self.operator = operator
self.constraint = constraint
class IntegerOverflowUnderflowModule(DetectionModule):
"""This module searches for integer over- and underflows."""
def __init__(self):
""""""
super().__init__(
name="Integer Overflow and Underflow",
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
description=(
"For every SUB instruction, check if there's a possible state "
"where op1 > op0. For every ADD, MUL instruction, check if "
"there's a possible state where op1 + op0 > 2^32 - 1"
),
entrypoint="callback",
pre_hooks=["ADD", "MUL", "SUB", "SSTORE", "JUMPI"],
)
self._overflow_cache = {}
self._underflow_cache = {}
def reset_module(self):
"""
Resets the module
:return:
"""
super().reset_module()
self._overflow_cache = {}
self._underflow_cache = {}
def execute(self, state: GlobalState):
"""Executes analysis module for integer underflow and integer overflow.
: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
if state.get_current_instruction()["opcode"] == "ADD":
self._handle_add(state)
elif state.get_current_instruction()["opcode"] == "MUL":
self._handle_mul(state)
elif state.get_current_instruction()["opcode"] == "SUB":
self._handle_sub(state)
elif state.get_current_instruction()["opcode"] == "SSTORE":
self._handle_sstore(state)
elif state.get_current_instruction()["opcode"] == "JUMPI":
self._handle_jumpi(state)
def _handle_add(self, state):
stack = state.mstate.stack
op0, op1 = (
self._make_bitvec_if_not(stack, -1),
self._make_bitvec_if_not(stack, -2),
)
c = Not(BVAddNoOverflow(op0, op1, False))
# Check satisfiable
model = self._try_constraints(state.node.constraints, [c])
if model is None:
return
annotation = OverUnderflowAnnotation(state, "addition", c)
op0.annotate(annotation)
def _handle_mul(self, state):
stack = state.mstate.stack
op0, op1 = (
self._make_bitvec_if_not(stack, -1),
self._make_bitvec_if_not(stack, -2),
)
op0 = stack[-1]
op1 = stack[-2]
c = Not(BVMulNoOverflow(op0, op1, False))
constraints = copy.deepcopy(node.constraints)
# Check satisfiable
model = self._try_constraints(state.node.constraints, [c])
if model is None:
return
# Filter for patterns that indicate benign underflows
annotation = OverUnderflowAnnotation(state, "multiplication", c)
op0.annotate(annotation)
# Pattern 1: (96 + calldatasize_MAIN) - (96), where (96 + calldatasize_MAIN) would underflow if calldatasize is very large.
# Pattern 2: (256*If(1 & storage_0 == 0, 1, 0)) - 1, this would underlow if storage_0 = 0
if type(op0) == int and type(op1) == int:
return []
if re.search(r'calldatasize_', str(op0)):
return []
if re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL):
return []
if re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL):
return []
def _handle_sub(self, state):
stack = state.mstate.stack
op0, op1 = (
self._make_bitvec_if_not(stack, -1),
self._make_bitvec_if_not(stack, -2),
)
c = Not(BVSubNoUnderflow(op0, op1, False))
# Check satisfiable
model = self._try_constraints(state.node.constraints, [c])
if model is None:
return
annotation = OverUnderflowAnnotation(state, "subtraction", c)
op0.annotate(annotation)
@staticmethod
def _make_bitvec_if_not(stack, index):
value = stack[index]
if isinstance(value, BitVec):
return value
stack[index] = symbol_factory.BitVecVal(value, 256)
return stack[index]
@staticmethod
def _get_description_head(annotation, _type):
return "The binary {} can {}.".format(annotation.operator, _type.lower())
@staticmethod
def _get_description_tail(annotation, _type):
return (
"The operands of the {} operation are not sufficiently constrained. "
"The {} could therefore result in an integer {}. Prevent the {} by checking inputs "
"or ensure sure that the {} is caught by an assertion.".format(
annotation.operator,
annotation.operator,
_type.lower(),
_type.lower(),
_type.lower(),
)
)
@staticmethod
def _get_title(_type):
return "Integer {}".format(_type)
def _handle_sstore(self, state):
stack = state.mstate.stack
value = stack[-2]
logging.debug("[INTEGER_UNDERFLOW] Checking SUB {0}, {1} at address {2}".format(str(op0), str(op1),
str(instruction['address'])))
allowed_types = [int, BitVecRef, BitVecNumRef]
if not isinstance(value, Expression):
return
for annotation in value.annotations:
if not isinstance(annotation, OverUnderflowAnnotation):
continue
if type(op0) in allowed_types and type(op1) in allowed_types:
constraints.append(UGT(op1, op0))
_type = "Underflow" if annotation.operator == "subtraction" else "Overflow"
ostate = annotation.overflowing_state
node = ostate.node
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=ostate.get_current_instruction()["address"],
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
bytecode=ostate.environment.code.bytecode,
title=self._get_title(_type),
severity="High",
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),
)
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:
model = solver.get_model(constraints)
transaction_sequence = solver.get_transaction_sequence(
state, node.constraints + [annotation.constraint]
)
issue.debug = json.dumps(transaction_sequence, indent=4)
except UnsatError:
continue
if annotation.operator == "subtraction":
self._underflow_cache[address] = True
else:
self._overflow_cache[address] = True
self._issues.append(issue)
# If we get to this point then there has been an integer overflow
# Find out if the overflowed value is actually used
interesting_usages = _search_children(statespace, node, (op0 - op1), index=node.states.index(state))
def _handle_jumpi(self, state):
stack = state.mstate.stack
value = stack[-2]
# Stop if it isn't
if len(interesting_usages) == 0:
return issues
for annotation in value.annotations:
if not isinstance(annotation, OverUnderflowAnnotation):
continue
ostate = annotation.overflowing_state
node = ostate.node
_type = "Underflow" if annotation.operator == "subtraction" else "Overflow"
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=ostate.get_current_instruction()["address"],
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
bytecode=ostate.environment.code.bytecode,
title=self._get_title(_type),
severity="High",
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),
)
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
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW, title="Integer Underflow", _type="Warning")
try:
issue.description = "A possible integer underflow exists in the function `" + node.function_name + "`.\n" \
"The subtraction may result in a value < 0."
transaction_sequence = solver.get_transaction_sequence(
state, node.constraints + [annotation.constraint]
)
issue.debug = solver.pretty_print_model(model)
issues.append(issue)
issue.debug = json.dumps(transaction_sequence, indent=4)
except UnsatError:
logging.debug("[INTEGER_UNDERFLOW] no model found")
return issues
def _check_usage(state, taint_result):
"""Delegates checks to _check_{instruction_name}()"""
opcode = state.get_current_instruction()['opcode']
if opcode == 'JUMPI':
if _check_jumpi(state, taint_result):
return [state]
elif opcode == 'SSTORE':
if _check_sstore(state, taint_result):
return [state]
return []
def _check_jumpi(state, taint_result):
""" Check if conditional jump is dependent on the result of expression"""
assert state.get_current_instruction()['opcode'] == 'JUMPI'
return taint_result.check(state, -2)
def _check_sstore(state, taint_result):
""" Check if store operation is dependent on the result of expression"""
assert state.get_current_instruction()['opcode'] == 'SSTORE'
return taint_result.check(state, -2)
def _search_children(statespace, node, expression, taint_result=None, constraint=None, index=0, depth=0, max_depth=64):
"""
Checks the statespace for children states, with JUMPI or SSTORE instuctions,
for dependency on expression
:param statespace: The statespace to explore
:param node: Current node to explore from
:param expression: Expression to look for
:param taint_result: Result of taint analysis
:param index: Current state index node.states[index] == current_state
:param depth: Current depth level
:param max_depth: Max depth to explore
:return: List of states that match the opcodes and are dependent on expression
"""
if constraint is None:
constraint = []
logging.debug("SEARCHING NODE for usage of an overflowed variable %d", node.uid)
if taint_result is None:
state = node.states[index]
taint_stack = [False for _ in state.mstate.stack]
taint_stack[-1] = True
taint_result = TaintRunner.execute(statespace, node, state, initial_stack=taint_stack)
results = []
if depth >= max_depth:
return []
# Explore current node from index
for j in range(index, len(node.states)):
current_state = node.states[j]
current_instruction = current_state.get_current_instruction()
if current_instruction['opcode'] in ('JUMPI', 'SSTORE'):
element = _check_usage(current_state, taint_result)
if len(element) < 1:
continue
if _check_requires(element[0], node, statespace, constraint):
continue
results += element
# Recursively search children
children = \
[
statespace.nodes[edge.node_to]
for edge in statespace.edges
if edge.node_from == node.uid
# and _try_constraints(statespace.nodes[edge.node_to].constraints, constraint) is not None
]
for child in children:
results += _search_children(statespace, child, expression, taint_result, depth=depth + 1, max_depth=max_depth)
return results
def _check_requires(state, node, statespace, constraint):
"""Checks if usage of overflowed statement results in a revert statement"""
instruction = state.get_current_instruction()
if instruction['opcode'] is not "JUMPI":
return False
children = [
statespace.nodes[edge.node_to]
for edge in statespace.edges
if edge.node_from == node.uid
]
for child in children:
opcodes = [s.get_current_instruction()['opcode'] for s in child.states]
if "REVERT" in opcodes or "ASSERT_FAIL" in opcodes:
return True
# I added the following case, bc of false positives if the max depth is not high enough
if len(children) == 0:
return True
return False
if annotation.operator == "subtraction":
self._underflow_cache[address] = True
else:
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()
def _get_address_from_state(state):
return state.get_current_instruction()["address"]

@ -1,61 +1,107 @@
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import *
from mythril.laser.ethereum.cfg import JumpType
"""
MODULE DESCRIPTION:
"""This module contains the detection code to find multiple sends occurring in
a single transaction."""
from copy import copy
Check for multiple sends in a single transaction
"""
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import MULTIPLE_SENDS
from mythril.analysis.modules.base import DetectionModule
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
import logging
from mythril.analysis.call_helpers import get_call_from_state
log = logging.getLogger(__name__)
class MultipleSendsAnnotation(StateAnnotation):
def __init__(self):
self.calls = []
def __copy__(self):
result = MultipleSendsAnnotation()
result.calls = copy(self.calls)
return result
class MultipleSendsModule(DetectionModule):
"""This module checks for multiple sends in a single transaction."""
def __init__(self):
""""""
super().__init__(
name="Multiple Sends",
swc_id=MULTIPLE_SENDS,
description="Check for multiple sends in a single transaction",
entrypoint="callback",
pre_hooks=[
"CALL",
"DELEGATECALL",
"STATICCALL",
"CALLCODE",
"RETURN",
"STOP",
],
)
def execute(self, state: GlobalState):
self._issues.extend(_analyze_state(state))
return self.issues
def execute(statespace):
issues = []
for call in statespace.calls:
findings = []
# explore state
findings += _explore_states(call, statespace)
# explore nodes
findings += _explore_nodes(call, statespace)
def _analyze_state(state: GlobalState):
"""
:param state: the current state
:return: returns the issues for that corresponding state
"""
node = state.node
instruction = state.get_current_instruction()
if len(findings) > 0:
node = call.node
instruction = call.state.get_current_instruction()
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
swc_id=MULTIPLE_SENDS, title="Multiple Calls", _type="Informational")
annotations = [a for a in state.get_annotations(MultipleSendsAnnotation)]
if len(annotations) == 0:
log.debug("Creating annotation for state")
state.annotate(MultipleSendsAnnotation())
annotations = [a for a in state.get_annotations(MultipleSendsAnnotation)]
issue.description = \
"Multiple sends exist in one transaction. Try to isolate each external call into its own transaction," \
" as external calls can fail accidentally or deliberately.\nConsecutive calls: \n"
calls = annotations[0].calls
for finding in findings:
issue.description += \
"Call at address: {}\n".format(finding.state.get_current_instruction()['address'])
if instruction["opcode"] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]:
call = get_call_from_state(state)
if call:
calls += [call]
issues.append(issue)
return issues
else: # RETURN or STOP
if len(calls) > 1:
description_tail = (
"Consecutive calls are executed at the following bytecode offsets:\n"
)
def _explore_nodes(call, statespace):
children = _child_nodes(statespace, call.node)
sending_children = list(filter(lambda call: call.node in children, statespace.calls))
return sending_children
for call in calls:
description_tail += "Offset: {}\n".format(
call.state.get_current_instruction()["address"]
)
description_tail += (
"Try to isolate each external call into its own transaction,"
" as external calls can fail accidentally or deliberately.\n"
)
def _explore_states(call, statespace):
other_calls = list(
filter(lambda other: other.node == call.node and other.state_index > call.state_index, statespace.calls)
)
return other_calls
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=instruction["address"],
swc_id=MULTIPLE_SENDS,
bytecode=state.environment.code.bytecode,
title="Multiple Calls in a Single Transaction",
severity="Medium",
description_head="Multiple sends are executed in one transaction.",
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
return [issue]
def _child_nodes(statespace, node):
result = []
children = [statespace.nodes[edge.node_to] for edge in statespace.edges if edge.node_from == node.uid
and edge.type != JumpType.Transaction]
return []
for child in children:
result.append(child)
result += _child_nodes(statespace, child)
return result
detector = MultipleSendsModule()

@ -1,74 +1,101 @@
from mythril.analysis import solver
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
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
import logging
import json
log = logging.getLogger(__name__)
DESCRIPTION = """
Check if the contact can be 'accidentally' killed by anyone.
For kill-able contracts, also check whether it is possible to direct the contract balance to the attacker.
"""
class SuicideModule(DetectionModule):
"""This module checks if the contact can be 'accidentally' killed by
anyone."""
def __init__(self):
super().__init__(
name="Unprotected Selfdestruct",
swc_id=UNPROTECTED_SELFDESTRUCT,
description=DESCRIPTION,
entrypoint="callback",
pre_hooks=["SUICIDE"],
)
self._cache_address = {}
def reset_module(self):
"""
Resets the module
:return:
"""
super().reset_module()
self._cache_address = {}
def execute(self, state: GlobalState):
"""
:param state:
:return:
"""
self._issues.extend(self._analyze_state(state))
return self.issues
def _analyze_state(self, state):
log.info("Suicide module: Analyzing suicide instruction")
node = state.node
instruction = state.get_current_instruction()
if self._cache_address.get(instruction["address"], False):
return []
to = state.mstate.stack[-1]
log.debug("[SUICIDE] SUICIDE in function " + node.function_name)
description_head = "The contract can be killed by anyone."
try:
try:
transaction_sequence = solver.get_transaction_sequence(
state,
node.constraints
+ [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF],
)
description_tail = "Arbitrary senders can kill this contract and withdraw its balance to their own account."
except UnsatError:
transaction_sequence = solver.get_transaction_sequence(
state, node.constraints
)
description_tail = (
"Arbitrary senders can kill this contract but it is not possible to set the target address to which"
"the contract balance is sent."
)
debug = json.dumps(transaction_sequence, indent=4)
self._cache_address[instruction["address"]] = True
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=instruction["address"],
swc_id=UNPROTECTED_SELFDESTRUCT,
bytecode=state.environment.code.bytecode,
title="Unprotected Selfdestruct",
severity="High",
description_head=description_head,
description_tail=description_tail,
debug=debug,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
return [issue]
except UnsatError:
log.info("[UNCHECKED_SUICIDE] no model found")
'''
MODULE DESCRIPTION:
Check for SUICIDE instructions that either can be reached by anyone, or where msg.sender is checked against a tainted storage index
(i.e. there's a write to that index is unconstrained by msg.sender).
'''
def execute(state_space):
logging.debug("Executing module: UNCHECKED_SUICIDE")
issues = []
for k in state_space.nodes:
node = state_space.nodes[k]
for state in node.states:
issues += _analyze_state(state, node)
return issues
def _analyze_state(state, node):
issues = []
instruction = state.get_current_instruction()
if instruction['opcode'] != "SUICIDE":
return []
to = state.mstate.stack[-1]
logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name)
description = "The function `" + node.function_name + "` executes the SUICIDE instruction. "
if "caller" in str(to):
description += "The remaining Ether is sent to the caller's address.\n"
elif "storage" in str(to):
description += "The remaining Ether is sent to a stored address.\n"
elif "calldata" in str(to):
description += "The remaining Ether is sent to an address provided as a function argument.\n"
elif type(to) == BitVecNumRef:
description += "The remaining Ether is sent to: " + hex(to.as_long()) + "\n"
else:
description += "The remaining Ether is sent to: " + str(to) + "\n"
not_creator_constraints = []
if len(state.world_state.transaction_sequence) > 1:
creator = state.world_state.transaction_sequence[0].caller
for transaction in state.world_state.transaction_sequence[1:]:
not_creator_constraints.append(Not(Extract(159, 0, transaction.caller) == Extract(159, 0, creator)))
not_creator_constraints.append(Not(Extract(159, 0, transaction.caller) == 0))
try:
model = solver.get_model(node.constraints + not_creator_constraints)
debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(model)
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
swc_id=UNPROTECTED_SELFDESTRUCT, title="Unchecked SUICIDE", _type="Warning",
description=description, debug=debug)
issues.append(issue)
except UnsatError:
logging.debug("[UNCHECKED_SUICIDE] no model found")
return issues
detector = SuicideModule()

@ -1,136 +1,198 @@
"""This module contains the detection code to find the existence of transaction
order dependence."""
import copy
import logging
import re
from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import TX_ORDER_DEPENDENCE
from mythril.exceptions import UnsatError
'''
MODULE DESCRIPTION:
This module finds the existance of transaction order dependence vulnerabilities.
The following webpage contains an extensive description of the vulnerability:
https://consensys.github.io/smart-contract-best-practices/known_attacks/#transaction-ordering-dependence-tod-front-running
'''
def execute(statespace):
""" Executes the analysis module"""
logging.debug("Executing module: TOD")
issues = []
for call in statespace.calls:
# Do analysis
interesting_storages = list(_get_influencing_storages(call))
changing_sstores = list(_get_influencing_sstores(statespace, interesting_storages))
# Build issue if necessary
if len(changing_sstores) > 0:
node = call.node
instruction = call.state.get_current_instruction()
issue = Issue(contract=node.contract_name, function=node.function_name, address=instruction['address'],
title="Transaction order dependence", swc_id=TX_ORDER_DEPENDENCE, _type="Warning")
issue.description = \
"A possible transaction order dependence vulnerability exists in function {}. The value or " \
"direction of the call statement is determined from a tainted storage location"\
.format(node.function_name)
issues.append(issue)
return issues
# TODO: move to __init__ or util module
def _get_states_with_opcode(statespace, opcode):
""" Gets all (state, node) tuples in statespace with opcode"""
for k in statespace.nodes:
node = statespace.nodes[k]
for state in node.states:
if state.get_current_instruction()["opcode"] == opcode:
yield state, node
def _dependent_on_storage(expression):
""" Checks if expression is dependent on a storage symbol and returns the influencing storages"""
pattern = re.compile(r"storage_[a-z0-9_&^]*[0-9]+")
return pattern.findall(str(simplify(expression)))
def _get_storage_variable(storage, state):
"""
Get storage z3 object given storage name and the state
:param storage: storage name example: storage_0
:param state: state to retrieve the variable from
:return: z3 object representing storage
"""
index = int(re.search('[0-9]+', storage).group())
try:
return state.environment.active_account.storage[index]
except KeyError:
return None
def _can_change(constraints, variable):
""" Checks if the variable can change given some constraints """
_constraints = copy.deepcopy(constraints)
try:
model = solver.get_model(_constraints)
except UnsatError:
return False
try:
initial_value = int(str(model.eval(variable, model_completion=True)))
return _try_constraints(constraints, [variable != initial_value]) is not None
except AttributeError:
return False
def _get_influencing_storages(call):
""" Examines a Call object and returns an iterator of all storages that influence the call value or direction"""
state = call.state
node = call.node
# Get relevant storages
to, value = call.to, call.value
storages = []
if to.type == VarType.SYMBOLIC:
storages += _dependent_on_storage(to.val)
if value.type == VarType.SYMBOLIC:
storages += _dependent_on_storage(value.val)
# See if they can change within the constraints of the node
for storage in storages:
variable = _get_storage_variable(storage, state)
can_change = _can_change(node.constraints, variable)
if can_change:
yield storage
def _get_influencing_sstores(statespace, interesting_storages):
""" Gets sstore (state, node) tuples that write to interesting_storages"""
for sstore_state, node in _get_states_with_opcode(statespace, 'SSTORE'):
index, value = sstore_state.mstate.stack[-1], sstore_state.mstate.stack[-2]
log = logging.getLogger(__name__)
class TxOrderDependenceModule(DetectionModule):
"""This module finds the existence of transaction order dependence."""
def __init__(self):
super().__init__(
name="Transaction Order Dependence",
swc_id=TX_ORDER_DEPENDENCE,
description=(
"This module finds the existance of transaction order dependence "
"vulnerabilities. The following webpage contains an extensive description "
"of the vulnerability: "
"https://consensys.github.io/smart-contract-best-practices/known_attacks/#transaction-ordering-dependence-tod-front-running"
),
)
def execute(self, statespace):
"""Executes the analysis module.
:param statespace:
:return:
"""
log.debug("Executing module: TOD")
issues = []
for call in statespace.calls:
# Do analysis
interesting_storages = list(self._get_influencing_storages(call))
changing_sstores = list(
self._get_influencing_sstores(statespace, interesting_storages)
)
description_tail = (
"A transaction order dependence vulnerability may exist in this contract. The value or "
"target of the call statement is loaded from a writable storage location."
)
# Build issue if necessary
if len(changing_sstores) > 0:
node = call.node
instruction = call.state.get_current_instruction()
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=instruction["address"],
title="Transaction Order Dependence",
bytecode=call.state.environment.code.bytecode,
swc_id=TX_ORDER_DEPENDENCE,
severity="Medium",
description_head="The call outcome may depend on transaction order.",
description_tail=description_tail,
gas_used=(
call.state.mstate.min_gas_used,
call.state.mstate.max_gas_used,
),
)
issues.append(issue)
return issues
# TODO: move to __init__ or util module
@staticmethod
def _get_states_with_opcode(statespace, opcode):
"""Gets all (state, node) tuples in statespace with opcode.
:param statespace:
:param opcode:
"""
for k in statespace.nodes:
node = statespace.nodes[k]
for state in node.states:
if state.get_current_instruction()["opcode"] == opcode:
yield state, node
@staticmethod
def _dependent_on_storage(expression):
"""Checks if expression is dependent on a storage symbol and returns
the influencing storages.
:param expression:
:return:
"""
pattern = re.compile(r"storage_[a-z0-9_&^]*[0-9]+")
return pattern.findall(str(simplify(expression)))
@staticmethod
def _get_storage_variable(storage, state):
"""Get storage z3 object given storage name and the state.
:param storage: storage name example: storage_0
:param state: state to retrieve the variable from
:return: z3 object representing storage
"""
index = int(re.search("[0-9]+", storage).group())
try:
index = util.get_concrete_int(index)
except TypeError:
index = str(index)
if "storage_{}".format(index) not in interesting_storages:
continue
yield sstore_state, node
# TODO: remove
def _try_constraints(constraints, new_constraints):
"""
Tries new constraints
:return Model if satisfiable otherwise None
"""
_constraints = copy.deepcopy(constraints)
for constraint in new_constraints:
_constraints.append(copy.deepcopy(constraint))
try:
model = solver.get_model(_constraints)
return model
except UnsatError:
return None
return state.environment.active_account.storage[index]
except KeyError:
return None
def _can_change(self, constraints, variable):
"""Checks if the variable can change given some constraints.
:param constraints:
:param variable:
:return:
"""
_constraints = copy.deepcopy(constraints)
try:
model = solver.get_model(_constraints)
except UnsatError:
return False
try:
initial_value = int(str(model.eval(variable, model_completion=True)))
return (
self._try_constraints(constraints, [variable != initial_value])
is not None
)
except AttributeError:
return False
def _get_influencing_storages(self, call):
"""Examines a Call object and returns an iterator of all storages that
influence the call value or direction.
:param call:
"""
state = call.state
node = call.node
# Get relevant storages
to, value = call.to, call.value
storages = []
if to.type == VarType.SYMBOLIC:
storages += self._dependent_on_storage(to.val)
if value.type == VarType.SYMBOLIC:
storages += self._dependent_on_storage(value.val)
# See if they can change within the constraints of the node
for storage in storages:
variable = self._get_storage_variable(storage, state)
can_change = self._can_change(node.constraints, variable)
if can_change:
yield storage
def _get_influencing_sstores(self, statespace, interesting_storages):
"""Gets sstore (state, node) tuples that write to interesting_storages.
:param statespace:
:param interesting_storages:
"""
for sstore_state, node in self._get_states_with_opcode(statespace, "SSTORE"):
index, value = sstore_state.mstate.stack[-1], sstore_state.mstate.stack[-2]
try:
index = util.get_concrete_int(index)
except TypeError:
index = str(index)
if "storage_{}".format(index) not in interesting_storages:
continue
yield sstore_state, node
# TODO: remove
@staticmethod
def _try_constraints(constraints, new_constraints):
"""Tries new constraints.
:param constraints:
:param new_constraints:
:return Model if satisfiable otherwise None
"""
_constraints = copy.deepcopy(constraints)
for constraint in new_constraints:
_constraints.append(copy.deepcopy(constraint))
try:
model = solver.get_model(_constraints)
return model
except UnsatError:
return None
detector = TxOrderDependenceModule()

@ -1,100 +1,111 @@
"""This module contains detection code to find occurrences of calls whose
return value remains unchecked."""
from copy import copy
from mythril.analysis import solver
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import UNCHECKED_RET_VAL
from mythril.analysis.modules.base import DetectionModule
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import NodeFlags
import logging
import re
'''
MODULE DESCRIPTION:
Test whether CALL return value is checked.
For direct calls, the Solidity compiler auto-generates this check. E.g.:
Alice c = Alice(address);
c.ping(42);
Here the CALL will be followed by IZSERO(retval), if retval = ZERO then state is reverted.
For low-level-calls this check is omitted. E.g.:
c.call.value(0)(bytes4(sha3("ping(uint256)")),1);
'''
def execute(statespace):
logging.debug("Executing module: UNCHECKED_RETVAL")
issues = []
for k in statespace.nodes:
node = statespace.nodes[k]
if NodeFlags.CALL_RETURN in node.flags:
retval_checked = False
for state in node.states:
instr = state.get_current_instruction()
if instr['opcode'] == 'ISZERO' and re.search(r'retval', str(state.mstate.stack[-1])):
retval_checked = True
break
if not retval_checked:
address = state.get_current_instruction()['address']
issue = Issue(contract=node.contract_name, function=node.function_name, address=address,
title="Unchecked CALL return value", swc_id=UNCHECKED_RET_VAL)
issue.description = \
"The return value of an external call is not checked. " \
"Note that execution continue even if the called contract throws."
issues.append(issue)
else:
n_states = len(node.states)
for idx in range(0, n_states - 1): # Ignore CALLs at last position in a node
state = node.states[idx]
instr = state.get_current_instruction()
if instr['opcode'] == 'CALL':
retval_checked = False
for _idx in range(idx, idx + 10):
try:
_state = node.states[_idx]
_instr = _state.get_current_instruction()
if _instr['opcode'] == 'ISZERO' and re.search(r'retval', str(_state .mstate.stack[-1])):
retval_checked = True
break
except IndexError:
break
if not retval_checked:
address = instr['address']
issue = Issue(contract=node.contract_name, function=node.function_name,
address=address, title="Unchecked CALL return value", swc_id=UNCHECKED_RET_VAL)
issue.description = \
"The return value of an external call is not checked. " \
"Note that execution continue even if the called contract throws."
issues.append(issue)
return issues
log = logging.getLogger(__name__)
class UncheckedRetvalAnnotation(StateAnnotation):
def __init__(self):
self.retvals = []
def __copy__(self):
result = UncheckedRetvalAnnotation()
result.retvals = copy(self.retvals)
return result
class UncheckedRetvalModule(DetectionModule):
"""A detection module to test whether CALL return value is checked."""
def __init__(self):
super().__init__(
name="Unchecked Return Value",
swc_id=UNCHECKED_RET_VAL,
description=(
"Test whether CALL return value is checked. "
"For direct calls, the Solidity compiler auto-generates this check. E.g.:\n"
" Alice c = Alice(address);\n"
" c.ping(42);\n"
"Here the CALL will be followed by IZSERO(retval), if retval = ZERO then state is reverted. "
"For low-level-calls this check is omitted. E.g.:\n"
' c.call.value(0)(bytes4(sha3("ping(uint256)")),1);'
),
entrypoint="callback",
pre_hooks=["STOP", "RETURN"],
post_hooks=["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"],
)
def execute(self, state: GlobalState) -> list:
"""
:param state:
:return:
"""
self._issues.extend(_analyze_state(state))
return self.issues
def _analyze_state(state: GlobalState) -> list:
instruction = state.get_current_instruction()
node = state.node
annotations = [a for a in state.get_annotations(UncheckedRetvalAnnotation)]
if len(annotations) == 0:
state.annotate(UncheckedRetvalAnnotation())
annotations = [a for a in state.get_annotations(UncheckedRetvalAnnotation)]
retvals = annotations[0].retvals
if instruction["opcode"] in ("STOP", "RETURN"):
issues = []
for retval in retvals:
try:
solver.get_model(node.constraints + [retval["retval"] == 0])
except UnsatError:
continue
description_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."
)
issue = Issue(
contract=node.contract_name,
function_name=node.function_name,
address=retval["address"],
bytecode=state.environment.code.bytecode,
title="Unchecked Call Return Value",
swc_id=UNCHECKED_RET_VAL,
severity="Low",
description_head="The return value of a message call is not checked.",
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
issues.append(issue)
return issues
else:
log.debug("End of call, extracting retval")
assert state.environment.code.instruction_list[state.mstate.pc - 1][
"opcode"
] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]
retval = state.mstate.stack[-1]
retvals.append({"address": state.instruction["address"] - 1, "retval": retval})
return []
detector = UncheckedRetvalModule()

@ -1,24 +1,44 @@
from z3 import *
"""This module contains various helper methods for dealing with EVM
operations."""
from enum import Enum
from mythril.laser.ethereum import util
from mythril.laser.smt import simplify
class VarType(Enum):
"""An enum denoting whether a value is symbolic or concrete."""
SYMBOLIC = 1
CONCRETE = 2
class Variable:
"""The representation of a variable with value and type."""
def __init__(self, val, _type):
"""
:param val:
:param _type:
"""
self.val = val
self.type = _type
def __str__(self):
"""
:return:
"""
return str(self.val)
def get_variable(i):
"""
:param i:
:return:
"""
try:
return Variable(util.get_concrete_int(i), VarType.CONCRETE)
except TypeError:
@ -26,17 +46,45 @@ def get_variable(i):
class Op:
"""The base type for operations referencing current node and state."""
def __init__(self, node, state, state_index):
"""
:param node:
:param state:
:param state_index:
"""
self.node = node
self.state = state
self.state_index = state_index
class Call(Op):
def __init__(self, node, state, state_index, _type, to, gas, value=Variable(0, VarType.CONCRETE), data=None):
"""The representation of a CALL operation."""
def __init__(
self,
node,
state,
state_index,
_type,
to,
gas,
value=Variable(0, VarType.CONCRETE),
data=None,
):
"""
:param node:
:param state:
:param state_index:
:param _type:
:param to:
:param gas:
:param value:
:param data:
"""
super().__init__(node, state, state_index)
self.to = to
self.gas = gas
@ -46,6 +94,7 @@ class Call(Op):
class SStore(Op):
"""The respresentation of an SSTORE operation."""
def __init__(self, node, state, state_index, value):
super().__init__(node, state, state_index)

@ -1,80 +1,237 @@
import hashlib
"""This module provides classes that make up an issue report."""
import logging
import json
import operator
from jinja2 import PackageLoader, Environment
import _pysha3 as sha3
import hashlib
from mythril.solidity.soliditycontract import SolidityContract
from mythril.analysis.swc_data import SWC_TO_TITLE
from mythril.support.source_support import Source
class Issue:
def __init__(self, contract, function, address, swc_id, title, _type="Informational", description="", debug=""):
log = logging.getLogger(__name__)
class Issue:
"""Representation of an issue and its location."""
def __init__(
self,
contract,
function_name,
address,
swc_id,
title,
bytecode,
gas_used=(None, None),
severity=None,
description_head="",
description_tail="",
debug="",
):
"""
:param contract:
:param function_name:
:param address:
:param swc_id:
:param title:
:param bytecode:
:param gas_used:
:param _type:
:param description:
:param debug:
"""
self.title = title
self.contract = contract
self.function = function
self.function = function_name
self.address = address
self.description = description
self.type = _type
self.description_head = description_head
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
self.code = None
self.lineno = None
self.source_mapping = None
try:
keccak = sha3.keccak_256()
keccak.update(bytes.fromhex(bytecode))
self.bytecode_hash = "0x" + keccak.hexdigest()
except ValueError:
log.debug(
"Unable to change the bytecode to bytes. Bytecode: {}".format(bytecode)
)
self.bytecode_hash = ""
@property
def as_dict(self):
issue = {'title': self.title, 'swc_id': self.swc_id, 'contract': self.contract, 'description': self.description,
'function': self.function, 'type': self.type, 'address': self.address, 'debug': self.debug}
"""
:return:
"""
issue = {
"title": self.title,
"swc-id": self.swc_id,
"contract": self.contract,
"description": self.description,
"function": self.function,
"severity": self.severity,
"address": self.address,
"debug": self.debug,
"min_gas_used": self.min_gas_used,
"max_gas_used": self.max_gas_used,
"sourceMap": self.source_mapping,
}
if self.filename and self.lineno:
issue['filename'] = self.filename
issue['lineno'] = self.lineno
issue["filename"] = self.filename
issue["lineno"] = self.lineno
if self.code:
issue['code'] = self.code
issue["code"] = self.code
return issue
def add_code_info(self, contract):
if self.address:
codeinfo = contract.get_source_info(self.address, constructor=(self.function == 'constructor'))
"""
:param contract:
"""
if self.address and isinstance(contract, SolidityContract):
codeinfo = contract.get_source_info(
self.address, constructor=(self.function == "constructor")
)
self.filename = codeinfo.filename
self.code = codeinfo.code
self.lineno = codeinfo.lineno
self.source_mapping = codeinfo.solc_mapping
else:
self.source_mapping = self.address
class Report:
environment = Environment(loader=PackageLoader('mythril.analysis'), trim_blocks=True)
"""A report containing the content of multiple issues."""
environment = Environment(
loader=PackageLoader("mythril.analysis"), trim_blocks=True
)
def __init__(self, verbose=False, source=None):
"""
def __init__(self, verbose=False):
:param verbose:
"""
self.issues = {}
self.verbose = verbose
pass
self.solc_version = ""
self.meta = {}
self.source = source or Source()
def sorted_issues(self):
"""
:return:
"""
issue_list = [issue.as_dict for key, issue in self.issues.items()]
return sorted(issue_list, key=operator.itemgetter('address', 'title'))
return sorted(issue_list, key=operator.itemgetter("address", "title"))
def append_issue(self, issue):
"""
:param issue:
"""
m = hashlib.md5()
m.update((issue.contract + str(issue.address) + issue.title).encode('utf-8'))
m.update((issue.contract + str(issue.address) + issue.title).encode("utf-8"))
self.issues[m.digest()] = issue
def as_text(self):
"""
:return:
"""
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)
template = Report.environment.get_template("report_as_text.jinja2")
return template.render(
filename=name, issues=self.sorted_issues(), verbose=self.verbose
)
def as_json(self):
result = {'success': True, 'error': None, 'issues': self.sorted_issues()}
"""
:return:
"""
result = {"success": True, "error": None, "issues": self.sorted_issues()}
return json.dumps(result, sort_keys=True)
def as_swc_standard_format(self):
"""Format defined for integration and correlation.
:return:
"""
_issues = []
source_list = []
for key, issue in self.issues.items():
if issue.bytecode_hash not in source_list:
idx = len(source_list)
source_list.append(issue.bytecode_hash)
else:
idx = source_list.index(issue.bytecode_hash)
try:
title = SWC_TO_TITLE[issue.swc_id]
except KeyError:
title = "Unspecified Security Issue"
_issues.append(
{
"swcID": "SWC-" + issue.swc_id,
"swcTitle": title,
"description": {
"head": issue.description_head,
"tail": issue.description_tail,
},
"severity": issue.severity,
"locations": [{"sourceMap": "%d:1:%d" % (issue.address, idx)}],
"extra": {},
}
)
result = [
{
"issues": _issues,
"sourceType": "raw-bytecode",
"sourceFormat": "evm-byzantium-bytecode",
"sourceList": source_list,
"meta": {},
}
]
return json.dumps(result, sort_keys=True)
def as_markdown(self):
"""
:return:
"""
filename = self._file_name()
template = Report.environment.get_template('report_as_markdown.jinja2')
return template.render(filename=filename, issues=self.sorted_issues(), verbose=self.verbose)
template = Report.environment.get_template("report_as_markdown.jinja2")
return template.render(
filename=filename, issues=self.sorted_issues(), verbose=self.verbose
)
def _file_name(self):
"""
:return:
"""
if len(self.issues.values()) > 0:
return list(self.issues.values())[0].filename

@ -1,22 +1,106 @@
from mythril.analysis.report import Report
"""This module contains functionality for hooking in detection modules and
executing them."""
from collections import defaultdict
from ethereum.opcodes import opcodes
from mythril.analysis import modules
import pkgutil
import importlib.util
import logging
log = logging.getLogger(__name__)
def fire_lasers(statespace, module_names=None):
OPCODE_LIST = [c[0] for _, c in opcodes.items()]
issues = []
_modules = []
for loader, name, is_pkg in pkgutil.walk_packages(modules.__path__):
_modules.append(loader.find_module(name).load_module(name))
def reset_callback_modules():
"""Clean the issue records of every callback-based module."""
modules = get_detection_modules("callback")
for module in modules:
module.detector.reset_module()
logging.info("Starting analysis")
def get_detection_module_hooks(modules, hook_type="pre"):
hook_dict = defaultdict(list)
_modules = get_detection_modules(entrypoint="callback", include_modules=modules)
for module in _modules:
if not module_names or module.__name__ in module_names:
logging.info("Executing " + str(module))
issues += module.execute(statespace)
hooks = (
module.detector.pre_hooks
if hook_type == "pre"
else module.detector.post_hooks
)
for op_code in map(lambda x: x.upper(), hooks):
if op_code in OPCODE_LIST:
hook_dict[op_code].append(module.detector.execute)
elif op_code.endswith("*"):
to_register = filter(lambda x: x.startswith(op_code[:-1]), OPCODE_LIST)
for actual_hook in to_register:
hook_dict[actual_hook].append(module.detector.execute)
else:
log.error(
"Encountered invalid hook opcode %s in module %s",
op_code,
module.detector.name,
)
return dict(hook_dict)
def get_detection_modules(entrypoint, include_modules=()):
"""
:param entrypoint:
:param include_modules:
:return:
"""
include_modules = list(include_modules)
_modules = []
if not include_modules:
for loader, module_name, _ in pkgutil.walk_packages(modules.__path__):
if module_name != "base":
module = importlib.import_module(
"mythril.analysis.modules." + module_name
)
if module.detector.entrypoint == entrypoint:
_modules.append(module)
else:
for module_name in include_modules:
module = importlib.import_module("mythril.analysis.modules." + module_name)
if module.__name__ != "base" and module.detector.entrypoint == entrypoint:
_modules.append(module)
log.info("Found %s detection modules", len(_modules))
return _modules
def fire_lasers(statespace, module_names=()):
"""
:param statespace:
:param module_names:
:return:
"""
log.info("Starting analysis")
issues = []
for module in get_detection_modules(
entrypoint="post", include_modules=module_names
):
log.info("Executing " + module.detector.name)
issues += module.detector.execute(statespace)
issues += retrieve_callback_issues(module_names)
return issues
def retrieve_callback_issues(module_names=()):
issues = []
for module in get_detection_modules(
entrypoint="callback", include_modules=module_names
):
log.debug("Retrieving results for " + module.detector.name)
issues += module.detector.issues
reset_callback_modules()
return issues

@ -1,32 +1,131 @@
from z3 import Solver, simplify, sat, unknown
"""This module contains analysis module helpers to solve path constraints."""
from z3 import sat, unknown, FuncInterp
import z3
from mythril.laser.smt import simplify, UGE, Optimize, symbol_factory
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
import logging
def get_model(constraints):
s = Solver()
s.set("timeout", 100000)
log = logging.getLogger(__name__)
def get_model(constraints, minimize=(), maximize=()):
"""
:param constraints:
:param minimize:
:param maximize:
:return:
"""
s = Optimize()
s.set_timeout(100000)
for constraint in constraints:
if type(constraint) == bool and not constraint:
raise UnsatError
constraints = [constraint for constraint in constraints if type(constraint) != bool]
for constraint in constraints:
s.add(constraint)
for e in minimize:
s.minimize(e)
for e in maximize:
s.maximize(e)
result = s.check()
if result == sat:
return s.model()
elif result == unknown:
logging.info("Timeout encountered while solving expression using z3")
log.debug("Timeout encountered while solving expression using z3")
raise UnsatError
def pretty_print_model(model):
"""
:param model:
:return:
"""
ret = ""
for d in model.decls():
if type(model[d]) == FuncInterp:
condition = model[d].as_list()
ret += "%s: %s\n" % (d.name(), condition)
continue
try:
condition = "0x%x" % model[d].as_long()
except:
condition = str(simplify(model[d]))
condition = str(z3.simplify(model[d]))
ret += ("%s: %s\n" % (d.name(), condition))
ret += "%s: %s\n" % (d.name(), condition)
return ret
def get_transaction_sequence(global_state, constraints):
"""Generate concrete transaction sequence.
:param global_state: GlobalState to generate transaction sequence for
:param constraints: list of constraints used to generate transaction sequence
"""
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 = {}
creation_tx_ids = []
tx_constraints = constraints.copy()
minimize = []
transactions = []
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)
)
minimize.append(transaction.call_data.calldatasize)
minimize.append(transaction.call_value)
concrete_transactions[tx_id] = tx_template.copy()
else:
creation_tx_ids.append(tx_id)
model = get_model(tx_constraints, minimize=minimize)
for transaction in transactions:
tx_id = str(transaction.id)
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)
]
)
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)
return concrete_transactions

@ -1,25 +1,63 @@
DEFAULT_FUNCTION_VISIBILITY = '100'
INTEGER_OVERFLOW_AND_UNDERFLOW = '101'
OUTDATED_COMPILER_VERSION = '102'
FLOATING_PRAGMA = '103'
UNCHECKED_RET_VAL = '104'
UNPROTECTED_ETHER_WITHDRAWAL = '105'
UNPROTECTED_SELFDESTRUCT = '106'
REENTRANCY = '107'
DEFAULT_STATE_VARIABLE_VISIBILITY = '108'
UNINITIALIZED_STORAGE_POINTER = '109'
ASSERT_VIOLATION = '110'
DEPRICATED_FUNCTIONS_USAGE = '111'
DELEGATECALL_TO_UNTRUSTED_CONTRACT = '112'
MULTIPLE_SENDS = '113'
TX_ORDER_DEPENDENCE = '114'
TX_ORIGIN_USAGE = '115'
TIMESTAMP_DEPENDENCE = '116'
# TODO: SWC ID 116 is missing, Add it if it's added to the https://github.com/SmartContractSecurity/SWC-registry
INCORRECT_CONSTRUCTOR_NAME = '118'
SHADOWING_STATE_VARIABLES = '119'
WEAK_RANDOMNESS = '120'
SIGNATURE_REPLAY = '121'
IMPROPER_VERIFICATION_BASED_ON_MSG_SENDER = '122'
"""This module maps SWC IDs to their registry equivalents."""
PREDICTABLE_VARS_DEPENDENCE = 'N/A' # TODO: Add the swc id when this is added to the SWC Registry
DEFAULT_FUNCTION_VISIBILITY = "100"
INTEGER_OVERFLOW_AND_UNDERFLOW = "101"
OUTDATED_COMPILER_VERSION = "102"
FLOATING_PRAGMA = "103"
UNCHECKED_RET_VAL = "104"
UNPROTECTED_ETHER_WITHDRAWAL = "105"
UNPROTECTED_SELFDESTRUCT = "106"
REENTRANCY = "107"
DEFAULT_STATE_VARIABLE_VISIBILITY = "108"
UNINITIALIZED_STORAGE_POINTER = "109"
ASSERT_VIOLATION = "110"
DEPRICATED_FUNCTIONS_USAGE = "111"
DELEGATECALL_TO_UNTRUSTED_CONTRACT = "112"
MULTIPLE_SENDS = "113"
TX_ORDER_DEPENDENCE = "114"
TX_ORIGIN_USAGE = "115"
TIMESTAMP_DEPENDENCE = "116"
INCORRECT_CONSTRUCTOR_NAME = "118"
SHADOWING_STATE_VARIABLES = "119"
WEAK_RANDOMNESS = "120"
SIGNATURE_REPLAY = "121"
IMPROPER_VERIFICATION_BASED_ON_MSG_SENDER = "122"
REQUIREMENT_VIOLATION = "123"
WRITE_TO_ARBITRARY_STORAGE = "124"
INCORRECT_INHERITANCE_ORDER = "125"
ARBITRARY_JUMP = "127"
DOS_WITH_BLOCK_GAS_LIMIT = "128"
TYPOGRAPHICAL_ERROR = "129"
SWC_TO_TITLE = {
"100": "Function Default Visibility",
"101": "Integer Overflow and Underflow",
"102": "Outdated Compiler Version",
"103": "Floating Pragma",
"104": "Unchecked Call Return Value",
"105": "Unprotected Ether Withdrawal",
"106": "Unprotected SELFDESTRUCT Instruction",
"107": "Reentrancy",
"108": "State Variable Default Visibility",
"109": "Uninitialized Storage Pointer",
"110": "Assert Violation",
"111": "Use of Deprecated Solidity Functions",
"112": "Delegatecall to Untrusted Callee",
"113": "DoS with Failed Call",
"114": "Transaction Order Dependence",
"115": "Authorization through tx.origin",
"116": "Timestamp Dependence",
"117": "Signature Malleability",
"118": "Incorrect Constructor Name",
"119": "Shadowing State Variables",
"120": "Weak Sources of Randomness from Chain Attributes",
"121": "Missing Protection against Signature Replay Attacks",
"122": "Lack of Proper Signature Verification",
"123": "Requirement Violation",
"124": "Write to Arbitrary Storage Location",
"125": "Incorrect Inheritance Order",
"127": "Arbitrary Jump with Function Type Variable",
"128": "DoS With Block Gas Limit",
"129": "Typographical Error",
}

@ -1,43 +1,109 @@
from mythril.laser.ethereum import svm
from mythril.laser.ethereum.state import Account
from mythril.ether.soliditycontract import SolidityContract
"""This module contains a wrapper around LASER for extended analysis
purposes."""
import copy
import logging
from .ops import get_variable, SStore, Call, VarType
from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy, BreadthFirstSearchStrategy
from mythril.analysis.security import get_detection_module_hooks, get_detection_modules
from mythril.laser.ethereum import svm
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.strategy.basic import (
BreadthFirstSearchStrategy,
DepthFirstSearchStrategy,
ReturnRandomNaivelyStrategy,
ReturnWeightedRandomStrategy,
)
from mythril.solidity.soliditycontract import EVMContract, SolidityContract
from .ops import Call, SStore, VarType, get_variable
class SymExecWrapper:
"""Wrapper class for the LASER Symbolic virtual machine.
Symbolically executes the code and does a bit of pre-analysis for
convenience.
"""
Wrapper class for the LASER Symbolic virtual machine. Symbolically executes the code and does a bit of pre-analysis for convenience.
"""
def __init__(self, contract, address, strategy, dynloader=None, max_depth=22,
execution_timeout=None, create_timeout=None, max_transaction_count=3):
s_strategy = None
if strategy == 'dfs':
def __init__(
self,
contract,
address,
strategy,
dynloader=None,
max_depth=22,
execution_timeout=None,
create_timeout=None,
transaction_count=2,
modules=(),
compulsory_statespace=True,
enable_iprof=False,
):
"""
:param contract:
:param address:
:param strategy:
:param dynloader:
:param max_depth:
:param execution_timeout:
:param create_timeout:
:param transaction_count:
:param modules:
"""
if strategy == "dfs":
s_strategy = DepthFirstSearchStrategy
elif strategy == 'bfs':
elif strategy == "bfs":
s_strategy = BreadthFirstSearchStrategy
elif strategy == "naive-random":
s_strategy = ReturnRandomNaivelyStrategy
elif strategy == "weighted-random":
s_strategy = ReturnWeightedRandomStrategy
else:
raise ValueError("Invalid strategy argument supplied")
account = Account(address, contract.disassembly, dynamic_loader=dynloader, contract_name=contract.name)
account = Account(
address,
contract.disassembly,
dynamic_loader=dynloader,
contract_name=contract.name,
)
requires_statespace = (
compulsory_statespace or len(get_detection_modules("post", modules)) > 0
)
self.accounts = {address: account}
self.laser = svm.LaserEVM(self.accounts, dynamic_loader=dynloader, max_depth=max_depth,
execution_timeout=execution_timeout, strategy=s_strategy,
create_timeout=create_timeout,
max_transaction_count=max_transaction_count)
self.laser = svm.LaserEVM(
self.accounts,
dynamic_loader=dynloader,
max_depth=max_depth,
execution_timeout=execution_timeout,
strategy=s_strategy,
create_timeout=create_timeout,
transaction_count=transaction_count,
requires_statespace=requires_statespace,
enable_iprof=enable_iprof,
)
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(creation_code=contract.creation_code, contract_name=contract.name)
self.laser.sym_exec(
creation_code=contract.creation_code, contract_name=contract.name
)
elif isinstance(contract, EVMContract) and contract.creation_code:
self.laser.sym_exec(
creation_code=contract.creation_code, contract_name=contract.name
)
else:
self.laser.sym_exec(address)
if not requires_statespace:
return
self.nodes = self.laser.nodes
self.edges = self.laser.edges
@ -54,31 +120,72 @@ class SymExecWrapper:
instruction = state.get_current_instruction()
op = instruction['opcode']
op = instruction["opcode"]
if op in ('CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'):
if op in ("CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"):
stack = state.mstate.stack
if op in ('CALL', 'CALLCODE'):
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = \
get_variable(stack[-1]), get_variable(stack[-2]), get_variable(stack[-3]), get_variable(stack[-4]), get_variable(stack[-5]), get_variable(stack[-6]), get_variable(stack[-7])
if op in ("CALL", "CALLCODE"):
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
get_variable(stack[-7]),
)
if to.type == VarType.CONCRETE and to.val < 5:
# ignore prebuilts
continue
if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE:
self.calls.append(Call(self.nodes[key], state, state_index, op, to, gas, value, state.mstate.memory[meminstart.val:meminsz.val * 4]))
# ignore prebuilts
continue
if (
meminstart.type == VarType.CONCRETE
and meminsz.type == VarType.CONCRETE
):
self.calls.append(
Call(
self.nodes[key],
state,
state_index,
op,
to,
gas,
value,
state.mstate.memory[
meminstart.val : meminsz.val * 4
],
)
)
else:
self.calls.append(Call(self.nodes[key], state, state_index, op, to, gas, value))
self.calls.append(
Call(
self.nodes[key],
state,
state_index,
op,
to,
gas,
value,
)
)
else:
gas, to, meminstart, meminsz, memoutstart, memoutsz = \
get_variable(stack[-1]), get_variable(stack[-2]), get_variable(stack[-3]), get_variable(stack[-4]), get_variable(stack[-5]), get_variable(stack[-6])
self.calls.append(Call(self.nodes[key], state, state_index, op, to, gas))
elif op == 'SSTORE':
gas, to, meminstart, meminsz, memoutstart, memoutsz = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
)
self.calls.append(
Call(self.nodes[key], state, state_index, op, to, gas)
)
elif op == "SSTORE":
stack = copy.deepcopy(state.mstate.stack)
address = state.environment.active_account.address
@ -90,14 +197,23 @@ class SymExecWrapper:
self.sstors[address] = {}
try:
self.sstors[address][str(index)].append(SStore(self.nodes[key], state, state_index, value))
self.sstors[address][str(index)].append(
SStore(self.nodes[key], state, state_index, value)
)
except KeyError:
self.sstors[address][str(index)] = [SStore(self.nodes[key], state, state_index, value)]
self.sstors[address][str(index)] = [
SStore(self.nodes[key], state, state_index, value)
]
state_index += 1
def find_storage_write(self, address, index):
"""
:param address:
:param index:
:return:
"""
# Find an SSTOR not constrained by caller that writes to storage index "index"
try:

@ -3,11 +3,12 @@
{% for issue in issues %}
## {{ issue.title }}
- SWC ID: {{ issue.swc_id }}
- Type: {{ issue.type }}
- SWC ID: {{ issue['swc-id'] }}
- Severity: {{ issue.severity }}
- Contract: {{ issue.contract | default("Unknown") }}
- Function name: `{{ issue.function }}`
- PC address: {{ issue.address }}
- Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
### Description
@ -34,4 +35,4 @@ In file: {{ issue.filename }}:{{ issue.lineno }}
{% else %}
The analysis was completed successfully. No issues were detected.
{% endif %}
{% endif %}

@ -1,11 +1,12 @@
{% if issues %}
{% for issue in issues %}
==== {{ issue.title }} ====
SWC ID: {{ issue.swc_id }}
Type: {{ issue.type }}
SWC ID: {{ issue['swc-id'] }}
Severity: {{ issue.severity }}
Contract: {{ issue.contract | default("Unknown") }}
Function name: {{ issue.function }}
PC address: {{ issue.address }}
Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
{{ issue.description }}
--------------------
{% if issue.filename and issue.lineno %}
@ -19,7 +20,7 @@ In file: {{ issue.filename }}:{{ issue.lineno }}
{% endif %}
{% if verbose and issue.debug %}
--------------------
DEBUGGING INFORMATION:
Transaction Sequence:
{{ issue.debug }}
{% endif %}
@ -27,4 +28,4 @@ DEBUGGING INFORMATION:
{% endfor %}
{% else %}
The analysis was completed successfully. No issues were detected.
{% endif %}
{% endif %}

@ -1,34 +1,74 @@
from z3 import Z3Exception, simplify
"""This module provides a function to convert a state space into a set of state
nodes and transition edges."""
from z3 import Z3Exception
from mythril.laser.smt import simplify
from mythril.laser.ethereum.svm import NodeFlags
import re
colors = [
{'border': '#26996f', 'background': '#2f7e5b', 'highlight': {'border': '#fff', 'background': '#28a16f'}},
{'border': '#9e42b3', 'background': '#842899', 'highlight': {'border': '#fff', 'background': '#933da6'}},
{'border': '#b82323', 'background': '#991d1d', 'highlight': {'border': '#fff', 'background': '#a61f1f'}},
{'border': '#4753bf', 'background': '#3b46a1', 'highlight': {'border': '#fff', 'background': '#424db3'}},
{'border': '#26996f', 'background': '#2f7e5b', 'highlight': {'border': '#fff', 'background': '#28a16f'}},
{'border': '#9e42b3', 'background': '#842899', 'highlight': {'border': '#fff', 'background': '#933da6'}},
{'border': '#b82323', 'background': '#991d1d', 'highlight': {'border': '#fff', 'background': '#a61f1f'}},
{'border': '#4753bf', 'background': '#3b46a1', 'highlight': {'border': '#fff', 'background': '#424db3'}},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#fff", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#fff", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#fff", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#fff", "background": "#424db3"},
},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#fff", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#fff", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#fff", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#fff", "background": "#424db3"},
},
]
def get_serializable_statespace(statespace):
"""
:param statespace:
:return:
"""
nodes = []
edges = []
color_map = {}
i = 0
for k in statespace.accounts:
color_map[statespace.accounts[k].contract_name] = colors[i]
i += 1
for node_key in statespace.nodes:
node = statespace.nodes[node_key]
code = node.get_cfg_dict()['code']
code = node.get_cfg_dict()["code"]
code = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code)
if NodeFlags.FUNC_ENTRY in node.flags:
@ -36,47 +76,56 @@ def get_serializable_statespace(statespace):
code_split = code.split("\\n")
truncated_code = code if (len(code_split) < 7) else "\\n".join(code_split[:6]) + "\\n(click to expand +)"
truncated_code = (
code
if (len(code_split) < 7)
else "\\n".join(code_split[:6]) + "\\n(click to expand +)"
)
color = color_map[node.get_cfg_dict()['contract_name']]
def get_state_accounts(state):
color = color_map[node.get_cfg_dict()["contract_name"]]
def get_state_accounts(node_state):
"""
:param node_state:
:return:
"""
state_accounts = []
for key in state.accounts:
account = state.accounts[key].as_dict
account.pop('code', None)
account['balance'] = str(account['balance'])
for key in node_state.accounts:
account = node_state.accounts[key].as_dict
account.pop("code", None)
account["balance"] = str(account["balance"])
storage = {}
for storage_key in account['storage']:
storage[str(storage_key)] = str(account['storage'][storage_key])
state_accounts.append({
'address': key,
'storage': storage
})
return state_accounts
states = [{'machine': x.mstate.as_dict, 'accounts': get_state_accounts(x)} for x in node.states]
for storage_key in account["storage"]:
storage[str(storage_key)] = str(account["storage"][storage_key])
state_accounts.append({"address": key, "storage": storage})
return state_accounts
states = [
{"machine": x.mstate.as_dict, "accounts": get_state_accounts(x)}
for x in node.states
]
for state in states:
state['machine']['stack'] = [str(s) for s in state['machine']['stack']]
state['machine']['memory'] = [str(m) for m in state['machine']['memory']]
truncated_code = truncated_code.replace('\\n', '\n')
code = code.replace('\\n', '\n')
state["machine"]["stack"] = [str(s) for s in state["machine"]["stack"]]
state["machine"]["memory"] = [str(m) for m in state["machine"]["memory"]]
truncated_code = truncated_code.replace("\\n", "\n")
code = code.replace("\\n", "\n")
s_node = {
'id': str(node_key),
'func': str(node.function_name),
'label': truncated_code,
'code': code,
'truncated': truncated_code,
'states': states,
'color': color,
'instructions': code.split('\n')
"id": str(node_key),
"func": str(node.function_name),
"label": truncated_code,
"code": code,
"truncated": truncated_code,
"states": states,
"color": color,
"instructions": code.split("\n"),
}
nodes.append(s_node)
for edge in statespace.edges:
@ -90,20 +139,19 @@ def get_serializable_statespace(statespace):
except Z3Exception:
label = str(edge.condition).replace("\n", "")
label = re.sub("([^_])([\d]{2}\d+)", lambda m: m.group(1) + hex(int(m.group(2))), label)
label = re.sub(
"([^_])([\d]{2}\d+)", lambda m: m.group(1) + hex(int(m.group(2))), label
)
code = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code)
s_edge = {
'from': str(edge.as_dict['from']),
'to': str(edge.as_dict['to']),
'arrows': 'to',
'label': label,
'smooth': { 'type': "cubicBezier" }
"from": str(edge.as_dict["from"]),
"to": str(edge.as_dict["to"]),
"arrows": "to",
"label": label,
"smooth": {"type": "cubicBezier"},
}
edges.append(s_edge)
return {
'edges': edges,
'nodes': nodes
}
return {"edges": edges, "nodes": nodes}

@ -0,0 +1,127 @@
"""This module contains various helper classes and functions to deal with EVM
code disassembly."""
import re
from collections import Generator
from ethereum.opcodes import opcodes
regex_PUSH = re.compile(r"^PUSH(\d*)$")
# Additional mnemonic to catch failed assertions
opcodes[254] = ["ASSERT_FAIL", 0, 0, 0]
class EvmInstruction:
"""Model to hold the information of the disassembly."""
def __init__(self, address, op_code, argument=None):
self.address = address
self.op_code = op_code
self.argument = argument
def to_dict(self) -> dict:
"""
:return:
"""
result = {"address": self.address, "opcode": self.op_code}
if self.argument:
result["argument"] = self.argument
return result
def instruction_list_to_easm(instruction_list: list) -> str:
"""Convert a list of instructions into an easm op code string.
:param instruction_list:
:return:
"""
result = ""
for instruction in instruction_list:
result += "{} {}".format(instruction["address"], instruction["opcode"])
if "argument" in instruction:
result += " " + instruction["argument"]
result += "\n"
return result
def get_opcode_from_name(operation_name: str) -> int:
"""Get an op code based on its name.
:param operation_name:
:return:
"""
for op_code, value in opcodes.items():
if operation_name == value[0]:
return op_code
raise RuntimeError("Unknown opcode")
def find_op_code_sequence(pattern: list, instruction_list: list) -> Generator:
"""Returns all indices in instruction_list that point to instruction
sequences following a pattern.
:param pattern: The pattern to look for, e.g. [["PUSH1", "PUSH2"], ["EQ"]] where ["PUSH1", "EQ"] satisfies pattern
:param instruction_list: List of instructions to look in
:return: Indices to the instruction sequences
"""
for i in range(0, len(instruction_list) - len(pattern) + 1):
if is_sequence_match(pattern, instruction_list, i):
yield i
def is_sequence_match(pattern: list, instruction_list: list, index: int) -> bool:
"""Checks if the instructions starting at index follow a pattern.
:param pattern: List of lists describing a pattern, e.g. [["PUSH1", "PUSH2"], ["EQ"]] where ["PUSH1", "EQ"] satisfies pattern
:param instruction_list: List of instructions
:param index: Index to check for
:return: Pattern matched
"""
for index, pattern_slot in enumerate(pattern, start=index):
try:
if not instruction_list[index]["opcode"] in pattern_slot:
return False
except IndexError:
return False
return True
def disassemble(bytecode: str) -> list:
"""Disassembles evm bytecode and returns a list of instructions.
:param bytecode:
:return:
"""
instruction_list = []
address = 0
length = len(bytecode)
if "bzzr" in str(bytecode[-43:]):
# ignore swarm hash
length -= 43
while address < length:
try:
op_code = opcodes[bytecode[address]]
except KeyError:
instruction_list.append(EvmInstruction(address, "INVALID"))
address += 1
continue
op_code_name = op_code[0]
current_instruction = EvmInstruction(address, op_code_name)
match = re.search(regex_PUSH, op_code_name)
if match:
argument_bytes = bytecode[address + 1 : address + 1 + int(match.group(1))]
current_instruction.argument = "0x" + argument_bytes.hex()
address += int(match.group(1))
instruction_list.append(current_instruction)
address += 1
# We use a to_dict() here for compatibility reasons
return [element.to_dict() for element in instruction_list]

@ -1,60 +1,96 @@
from mythril.ether import asm, util
from mythril.support.signatures import SignatureDb
import logging
"""This module contains the class used to represent disassembly code."""
from mythril.ethereum import util
from mythril.disassembler import asm
from mythril.support.signatures import SignatureDB
class Disassembly(object):
"""Disassembly class.
def __init__(self, code, enable_online_lookup=True):
self.instruction_list = asm.disassemble(util.safe_decode(code))
self.func_hashes = []
self.func_to_addr = {}
self.addr_to_func = {}
Stores bytecode, and its disassembly.
Additionally it will gather the following information on the existing functions in the disassembled code:
- function hashes
- function name to entry point mapping
- function entry point to function name mapping
"""
def __init__(self, code: str, enable_online_lookup: bool = False):
"""
:param code:
:param enable_online_lookup:
"""
self.bytecode = code
self.instruction_list = asm.disassemble(util.safe_decode(code))
signatures = SignatureDb(enable_online_lookup=enable_online_lookup) # control if you want to have online sighash lookups
try:
signatures.open() # open from default locations
except FileNotFoundError:
logging.info("Missing function signature file. Resolving of function names from signature file disabled.")
self.func_hashes = []
self.function_name_to_address = {}
self.address_to_function_name = {}
# Parse jump table & resolve function names
# open from default locations
# control if you want to have online signature hash lookups
signatures = SignatureDB(enable_online_lookup=enable_online_lookup)
# Need to take from PUSH1 to PUSH4 because solc seems to remove excess 0s at the beginning for optimizing
jmptable_indices = asm.find_opcode_sequence([("PUSH1", "PUSH2", "PUSH3", "PUSH4"), ("EQ",)],
self.instruction_list)
for i in jmptable_indices:
func_hash = self.instruction_list[i]['argument']
# Append with missing 0s at the beginning
func_hash = "0x" + func_hash[2:].rjust(8, "0")
self.func_hashes.append(func_hash)
try:
# tries local cache, file and optional online lookup
# may return more than one function signature. since we cannot probe for the correct one we'll use the first
func_names = signatures.get(func_hash)
if len(func_names) > 1:
# ambigious result
func_name = "**ambiguous** %s" % func_names[0] # return first hit but note that result was ambiguous
else:
# only one item
func_name = func_names[0]
except KeyError:
func_name = "_function_" + func_hash
try:
offset = self.instruction_list[i + 2]['argument']
jump_target = int(offset, 16)
self.func_to_addr[func_name] = jump_target
self.addr_to_func[jump_target] = func_name
except:
continue
signatures.write() # store resolved signatures (potentially resolved online)
jump_table_indices = asm.find_op_code_sequence(
[("PUSH1", "PUSH2", "PUSH3", "PUSH4"), ("EQ",)], self.instruction_list
)
for index in jump_table_indices:
function_hash, jump_target, function_name = get_function_info(
index, self.instruction_list, signatures
)
self.func_hashes.append(function_hash)
if jump_target is not None and function_name is not None:
self.function_name_to_address[function_name] = jump_target
self.address_to_function_name[jump_target] = function_name
def get_easm(self):
# todo: tintinweb - print funcsig resolved data from self.addr_to_func?
"""
:return:
"""
return asm.instruction_list_to_easm(self.instruction_list)
def get_function_info(
index: int, instruction_list: list, signature_database: SignatureDB
) -> (str, int, str):
"""Finds the function information for a call table entry Solidity uses the
first 4 bytes of the calldata to indicate which function the message call
should execute The generated code that directs execution to the correct
function looks like this:
- PUSH function_hash
- EQ
- PUSH entry_point
- JUMPI
This function takes an index that points to the first instruction, and from that finds out the function hash,
function entry and the function name.
:param index: Start of the entry pattern
:param instruction_list: Instruction list for the contract that is being analyzed
:param signature_database: Database used to map function hashes to their respective function names
:return: function hash, function entry point, function name
"""
# Append with missing 0s at the beginning
function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0")
function_names = signature_database.get(function_hash)
if len(function_names) > 1:
# In this case there was an ambiguous result
function_name = "[{}] (ambiguous)".format(", ".join(function_names))
elif len(function_names) == 1:
function_name = function_names[0]
else:
function_name = "_function_" + function_hash
try:
offset = instruction_list[index + 2]["argument"]
entry_point = int(offset, 16)
except (KeyError, IndexError):
return function_hash, None, None
return function_hash, entry_point, function_name

@ -1,3 +0,0 @@
import time
start_time = time.time()

@ -1,152 +0,0 @@
import sys
import re
from ethereum.opcodes import opcodes
from mythril.ether import util
regex_PUSH = re.compile('^PUSH(\d*)$')
# Additional mnemonic to catch failed assertions
opcodes[254] = ['ASSERT_FAIL', 0, 0, 0]
def instruction_list_to_easm(instruction_list):
easm = ""
for instruction in instruction_list:
easm += str(instruction['address']) + " " + instruction['opcode']
if 'argument' in instruction:
easm += " " + instruction['argument']
easm += "\n"
return easm
def easm_to_instruction_list(easm):
regex_CODELINE = re.compile('^([A-Z0-9]+)(?:\s+([0-9a-fA-Fx]+))?$')
instruction_list = []
codelines = easm.split('\n')
for line in codelines:
m = re.search(regex_CODELINE, line)
if not m:
# Invalid code line
continue
instruction = {'opcode': m.group(1)}
if m.group(2):
instruction['argument'] = m.group(2)[2:]
instruction_list.append(instruction)
return instruction_list
def get_opcode_from_name(name):
for opcode, value in opcodes.items():
if name == value[0]:
return opcode
raise RuntimeError("Unknown opcode")
def find_opcode_sequence(pattern, instruction_list):
match_indexes = []
pattern_length = len(pattern)
for i in range(0, len(instruction_list) - pattern_length + 1):
if instruction_list[i]['opcode'] in pattern[0]:
matched = True
for j in range(1, len(pattern)):
if not (instruction_list[i + j]['opcode'] in pattern[j]):
matched = False
break
if matched:
match_indexes.append(i)
return match_indexes
def disassemble(bytecode):
instruction_list = []
addr = 0
length = len(bytecode)
if "bzzr" in str(bytecode[-43:]):
# ignore swarm hash
length -= 43
while addr < length:
instruction = {'address': addr}
try:
if sys.version_info > (3, 0):
opcode = opcodes[bytecode[addr]]
else:
opcode = opcodes[ord(bytecode[addr])]
except KeyError:
# invalid opcode
instruction_list.append({'address': addr, 'opcode': "INVALID"})
addr += 1
continue
instruction['opcode'] = opcode[0]
m = re.search(regex_PUSH, opcode[0])
if m:
argument = bytecode[addr+1:addr+1+int(m.group(1))]
instruction['argument'] = "0x" + argument.hex()
addr += int(m.group(1))
instruction_list.append(instruction)
addr += 1
return instruction_list
def assemble(instruction_list):
bytecode = b""
for instruction in instruction_list:
try:
opcode = get_opcode_from_name(instruction['opcode'])
except RuntimeError:
opcode = 0xbb
bytecode += opcode.to_bytes(1, byteorder='big')
if 'argument' in instruction:
bytecode += util.safe_decode(instruction['argument'])
return bytecode

@ -1,71 +0,0 @@
from mythril.disassembler.disassembly import Disassembly
from ethereum import utils
import persistent
import re
class ETHContract(persistent.Persistent):
def __init__(self, code, creation_code="", name="Unknown", enable_online_lookup=True):
# Workaround: We currently do not support compile-time linking.
# Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address
# Apply this for creation_code & code
creation_code = re.sub(r'(_{2}.{38})', 'aa' * 20, creation_code)
code = re.sub(r'(_{2}.{38})', 'aa' * 20, code)
self.creation_code = creation_code
self.name = name
self.code = code
self.disassembly = Disassembly(code, enable_online_lookup=enable_online_lookup)
self.creation_disassembly = Disassembly(creation_code, enable_online_lookup=enable_online_lookup)
def as_dict(self):
return {
'address': self.address,
'name': self.name,
'code': self.code,
'creation_code': self.creation_code,
'disassembly': self.disassembly
}
def get_easm(self):
return self.disassembly.get_easm()
def matches_expression(self, expression):
str_eval = ''
easm_code = None
tokens = re.split("\s+(and|or|not)\s+", expression, re.IGNORECASE)
for token in tokens:
if token in ("and", "or", "not"):
str_eval += " " + token + " "
continue
m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token)
if m:
if easm_code is None:
easm_code = self.get_easm()
code = m.group(1).replace(",", "\\n")
str_eval += "\"" + code + "\" in easm_code"
continue
m = re.match(r'^func#([a-zA-Z0-9\s_,(\\)\[\]]+)#$', token)
if m:
sign_hash = "0x" + utils.sha3(m.group(1))[:4].hex()
str_eval += "\"" + sign_hash + "\" in self.disassembly.func_hashes"
continue
return eval(str_eval.strip())

@ -1,75 +0,0 @@
from ethereum import vm, messages, transactions
from ethereum.state import State
from ethereum.slogging import get_logger
from mythril.ether import util
from logging import StreamHandler
from io import StringIO
import re
def trace(code, calldata = ""):
log_handlers = ['eth.vm.op', 'eth.vm.op.stack', 'eth.vm.op.memory', 'eth.vm.op.storage']
output = StringIO()
stream_handler = StreamHandler(output)
for handler in log_handlers:
log_vm_op = get_logger(handler)
log_vm_op.setLevel("TRACE")
log_vm_op.addHandler(stream_handler)
addr = bytes.fromhex('0123456789ABCDEF0123456789ABCDEF01234567')
state = State()
ext = messages.VMExt(state, transactions.Transaction(0, 0, 21000, addr, 0, addr))
message = vm.Message(addr, addr, 0, 21000, calldata)
res, gas, dat = vm.vm_execute(ext, message, util.safe_decode(code))
stream_handler.flush()
ret = output.getvalue()
lines = ret.split("\n")
trace = []
for line in lines:
m = re.search(r'pc=b\'(\d+)\'.*op=([A-Z0-9]+)', line)
if m:
pc = m.group(1)
op = m.group(2)
m = re.match(r'.*stack=(\[.*?\])', line)
if m:
stackitems = re.findall(r'b\'(\d+)\'', m.group(1))
stack = "["
if len(stackitems):
for i in range(0, len(stackitems) - 1):
stack += hex(int(stackitems[i])) + ", "
stack += hex(int(stackitems[-1]))
stack += "]"
else:
stack = "[]"
if re.match(r'^PUSH.*', op):
val = re.search(r'pushvalue=(\d+)', line).group(1)
pushvalue = hex(int(val))
trace.append({'pc': pc, 'op': op, 'stack': stack, 'pushvalue': pushvalue})
else:
trace.append({'pc': pc, 'op': op, 'stack': stack})
return trace

@ -1,125 +0,0 @@
import mythril.laser.ethereum.util as helper
from mythril.ether.ethcontract import ETHContract
from mythril.ether.util import get_solc_json
from mythril.exceptions import NoContractFoundError
class SourceMapping:
def __init__(self, solidity_file_idx, offset, length, lineno):
self.solidity_file_idx = solidity_file_idx
self.offset = offset
self.length = length
self.lineno = lineno
class SolidityFile:
def __init__(self, filename, data):
self.filename = filename
self.data = data
class SourceCodeInfo:
def __init__(self, filename, lineno, code):
self.filename = filename
self.lineno = lineno
self.code = code
def get_contracts_from_file(input_file, solc_args=None):
data = get_solc_json(input_file, solc_args=solc_args)
for key, contract in data['contracts'].items():
filename, name = key.split(":")
if filename == input_file and len(contract['bin-runtime']):
yield SolidityContract(input_file, name, solc_args)
class SolidityContract(ETHContract):
def __init__(self, input_file, name=None, solc_args=None):
data = get_solc_json(input_file, solc_args=solc_args)
self.solidity_files = []
for filename in data['sourceList']:
with open(filename, 'r', encoding='utf-8') as file:
code = file.read()
self.solidity_files.append(SolidityFile(filename, code))
has_contract = False
# If a contract name has been specified, find the bytecode of that specific contract
srcmap_constructor = []
srcmap = []
if name:
for key, contract in sorted(data['contracts'].items()):
filename, _name = key.split(":")
if filename == input_file and name == _name and len(contract['bin-runtime']):
code = contract['bin-runtime']
creation_code = contract['bin']
srcmap = contract['srcmap-runtime'].split(";")
srcmap_constructor = contract['srcmap'].split(";")
has_contract = True
break
# If no contract name is specified, get the last bytecode entry for the input file
else:
for key, contract in sorted(data['contracts'].items()):
filename, name = key.split(":")
if filename == input_file and len(contract['bin-runtime']):
code = contract['bin-runtime']
creation_code = contract['bin']
srcmap = contract['srcmap-runtime'].split(";")
srcmap_constructor = contract['srcmap'].split(";")
has_contract = True
if not has_contract:
raise NoContractFoundError
self.mappings = []
self.constructor_mappings = []
self._get_solc_mappings(srcmap)
self._get_solc_mappings(srcmap_constructor, constructor=True)
super().__init__(code, creation_code, name=name)
def get_source_info(self, address, constructor=False):
disassembly = self.creation_disassembly if constructor else self.disassembly
mappings = self.constructor_mappings if constructor else self.mappings
index = helper.get_instruction_index(disassembly.instruction_list, address)
solidity_file = self.solidity_files[mappings[index].solidity_file_idx]
filename = solidity_file.filename
offset = mappings[index].offset
length = mappings[index].length
code = solidity_file.data.encode('utf-8')[offset:offset + length].decode('utf-8', errors="ignore")
lineno = mappings[index].lineno
return SourceCodeInfo(filename, lineno, code)
def _get_solc_mappings(self, srcmap, constructor=False):
mappings = self.constructor_mappings if constructor else self.mappings
for item in srcmap:
mapping = item.split(":")
if len(mapping) > 0 and len(mapping[0]) > 0:
offset = int(mapping[0])
if len(mapping) > 1 and len(mapping[1]) > 0:
length = int(mapping[1])
if len(mapping) > 2 and len(mapping[2]) > 0:
idx = int(mapping[2])
lineno = self.solidity_files[idx].data.encode('utf-8')[0:offset].count('\n'.encode('utf-8')) + 1
mappings.append(SourceMapping(idx, offset, length, lineno))

@ -0,0 +1,139 @@
"""This module contains the class representing EVM contracts, aka Smart
Contracts."""
import re
import _pysha3 as sha3
import logging
log = logging.getLogger(__name__)
import persistent
from ethereum import utils
from mythril.disassembler.disassembly import Disassembly
class EVMContract(persistent.Persistent):
"""This class represents an address with associated code (Smart
Contract)."""
def __init__(
self, code="", creation_code="", name="Unknown", enable_online_lookup=False
):
"""Create a new contract.
Workaround: We currently do not support compile-time linking.
Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address
Apply this for creation_code & code
:param code:
:param creation_code:
:param name:
:param enable_online_lookup:
"""
creation_code = re.sub(r"(_{2}.{38})", "aa" * 20, creation_code)
code = re.sub(r"(_{2}.{38})", "aa" * 20, code)
self.creation_code = creation_code
self.name = name
self.code = code
self.disassembly = Disassembly(code, enable_online_lookup=enable_online_lookup)
self.creation_disassembly = Disassembly(
creation_code, enable_online_lookup=enable_online_lookup
)
@property
def bytecode_hash(self):
"""
:return: runtime bytecode hash
"""
return self._get_hash(self.code[2:])
@property
def creation_bytecode_hash(self):
"""
:return: Creation bytecode hash
"""
return self._get_hash(self.creation_code[2:])
@staticmethod
def _get_hash(code):
"""
:param code: bytecode
:return: Returns hash of the given bytecode
"""
try:
keccak = sha3.keccak_256()
keccak.update(bytes.fromhex(code[2:]))
return "0x" + keccak.hexdigest()
except ValueError:
log.debug(
"Unable to change the bytecode to bytes. Bytecode: {}".format(code)
)
return ""
def as_dict(self):
"""
:return:
"""
return {
"name": self.name,
"code": self.code,
"creation_code": self.creation_code,
"disassembly": self.disassembly,
}
def get_easm(self):
"""
:return:
"""
return self.disassembly.get_easm()
def get_creation_easm(self):
"""
:return:
"""
return self.creation_disassembly.get_easm()
def matches_expression(self, expression):
"""
:param expression:
:return:
"""
str_eval = ""
easm_code = None
tokens = re.split("\s+(and|or|not)\s+", expression, re.IGNORECASE)
for token in tokens:
if token in ("and", "or", "not"):
str_eval += " " + token + " "
continue
m = re.match(r"^code#([a-zA-Z0-9\s,\[\]]+)#", token)
if m:
if easm_code is None:
easm_code = self.get_easm()
code = m.group(1).replace(",", "\\n")
str_eval += '"' + code + '" in easm_code'
continue
m = re.match(r"^func#([a-zA-Z0-9\s_,(\\)\[\]]+)#$", token)
if m:
sign_hash = "0x" + utils.sha3(m.group(1))[:4].hex()
str_eval += '"' + sign_hash + '" in self.disassembly.func_hashes'
continue
return eval(str_eval.strip())

@ -1,29 +1,48 @@
"""This module contains account indexing functionality.
This includes a sedes class for lists, account storage receipts for
LevelDB and a class for updating account addresses.
"""
import logging
from mythril import ether
import time
from ethereum.messages import Log
import rlp
from rlp.sedes import big_endian_int, binary
from ethereum import utils
from ethereum.utils import hash32, address, int256
from ethereum.messages import Log
from ethereum.utils import address, hash32, int256
from rlp.sedes import big_endian_int, binary
from mythril import ethereum
from mythril.exceptions import AddressNotFoundError
log = logging.getLogger(__name__)
BATCH_SIZE = 8 * 4096
class CountableList(object):
"""A sedes for lists of arbitrary length.
:param element_sedes: when (de-)serializing a list, this sedes will be
applied to all of its elements
:param element_sedes: when (de-)serializing a list, this sedes will be applied to all of its elements
"""
def __init__(self, element_sedes):
self.element_sedes = element_sedes
def serialize(self, obj):
"""
:param obj:
:return:
"""
return [self.element_sedes.serialize(e) for e in obj]
def deserialize(self, serial):
"""
:param serial:
:return:
"""
# needed for 2 reasons:
# 1. empty lists are not zero elements
# 2. underlying logs are stored as list - if empty will also except and receipts will be lost
@ -34,25 +53,21 @@ class CountableList(object):
class ReceiptForStorage(rlp.Serializable):
"""
Receipt format stored in levelDB
"""
"""Receipt format stored in levelDB."""
fields = [
('state_root', binary),
('cumulative_gas_used', big_endian_int),
('bloom', int256),
('tx_hash', hash32),
('contractAddress', address),
('logs', CountableList(Log)),
('gas_used', big_endian_int)
("state_root", binary),
("cumulative_gas_used", big_endian_int),
("bloom", int256),
("tx_hash", hash32),
("contractAddress", address),
("logs", CountableList(Log)),
("gas_used", big_endian_int),
]
class AccountIndexer(object):
"""
Updates address index
"""
"""Updates address index."""
def __init__(self, ethDB):
self.db = ethDB
@ -62,32 +77,30 @@ class AccountIndexer(object):
self.updateIfNeeded()
def get_contract_by_hash(self, contract_hash):
"""
get mapped address by its hash, if not found try indexing
"""
address = self.db.reader._get_address_by_hash(contract_hash)
if address is not None:
return address
"""get mapped contract_address by its hash, if not found try
indexing."""
contract_address = self.db.reader._get_address_by_hash(contract_hash)
if contract_address is not None:
return contract_address
else:
raise AddressNotFoundError
return self.db.reader._get_address_by_hash(contract_hash)
def _process(self, startblock):
"""
Processesing method
"""
logging.debug("Processing blocks %d to %d" % (startblock, startblock + BATCH_SIZE))
"""Processesing method."""
log.debug("Processing blocks %d to %d" % (startblock, startblock + BATCH_SIZE))
addresses = []
for blockNum in range(startblock, startblock + BATCH_SIZE):
hash = self.db.reader._get_block_hash(blockNum)
if hash is not None:
receipts = self.db.reader._get_block_receipts(hash, blockNum)
block_hash = self.db.reader._get_block_hash(blockNum)
if block_hash is not None:
receipts = self.db.reader._get_block_receipts(block_hash, blockNum)
for receipt in receipts:
if receipt.contractAddress is not None and not all(b == 0 for b in receipt.contractAddress):
if receipt.contractAddress is not None and not all(
b == 0 for b in receipt.contractAddress
):
addresses.append(receipt.contractAddress)
else:
if len(addresses) == 0:
@ -96,9 +109,7 @@ class AccountIndexer(object):
return addresses
def updateIfNeeded(self):
"""
update address index
"""
"""update address index."""
headBlock = self.db.reader._get_head_block()
if headBlock is not None:
# avoid restarting search if head block is same & we already initialized
@ -113,15 +124,21 @@ class AccountIndexer(object):
# in fast sync head block is at 0 (e.g. in fastSync), we can't use it to determine length
if self.lastBlock is not None and self.lastBlock == 0:
self.lastBlock = 2e+9
self.lastBlock = 2e9
if self.lastBlock is None or (self.lastProcessedBlock is not None and self.lastBlock <= self.lastProcessedBlock):
if self.lastBlock is None or (
self.lastProcessedBlock is not None
and self.lastBlock <= self.lastProcessedBlock
):
return
blockNum = 0
if self.lastProcessedBlock is not None:
blockNum = self.lastProcessedBlock + 1
print("Updating hash-to-address index from block " + str(self.lastProcessedBlock))
print(
"Updating hash-to-address index from block "
+ str(self.lastProcessedBlock)
)
else:
print("Starting hash-to-address index")
@ -147,8 +164,11 @@ class AccountIndexer(object):
processed += BATCH_SIZE
blockNum = min(blockNum + BATCH_SIZE, self.lastBlock + 1)
cost_time = time.time() - ether.start_time
print("%d blocks processed (in %d seconds), %d unique addresses found, next block: %d" % (processed, cost_time, count, min(self.lastBlock, blockNum)))
cost_time = time.time() - ethereum.start_time
print(
"%d blocks processed (in %d seconds), %d unique addresses found, next block: %d"
% (processed, cost_time, count, min(self.lastBlock, blockNum))
)
self.lastProcessedBlock = blockNum - 1
self.db.writer._set_last_indexed_number(self.lastProcessedBlock)

@ -1,57 +1,64 @@
"""This module contains a LevelDB client."""
import binascii
import rlp
from mythril.ethereum.interface.leveldb.accountindexing import CountableList
from mythril.ethereum.interface.leveldb.accountindexing import ReceiptForStorage, AccountIndexer
from mythril.ethereum.interface.leveldb.accountindexing import (
ReceiptForStorage,
AccountIndexer,
)
import logging
from ethereum import utils
from ethereum.block import BlockHeader, Block
from mythril.ethereum.interface.leveldb.state import State
from mythril.ethereum.interface.leveldb.eth_db import ETH_DB
from mythril.ether.ethcontract import ETHContract
from mythril.ethereum.evmcontract import EVMContract
from mythril.exceptions import AddressNotFoundError
log = logging.getLogger(__name__)
# Per https://github.com/ethereum/go-ethereum/blob/master/core/rawdb/schema.go
# prefixes and suffixes for keys in geth
header_prefix = b'h' # header_prefix + num (uint64 big endian) + hash -> header
body_prefix = b'b' # body_prefix + num (uint64 big endian) + hash -> block body
num_suffix = b'n' # header_prefix + num (uint64 big endian) + num_suffix -> hash
block_hash_prefix = b'H' # block_hash_prefix + hash -> num (uint64 big endian)
block_receipts_prefix = b'r' # block_receipts_prefix + num (uint64 big endian) + hash -> block receipts
header_prefix = b"h" # header_prefix + num (uint64 big endian) + hash -> header
body_prefix = b"b" # body_prefix + num (uint64 big endian) + hash -> block body
num_suffix = b"n" # header_prefix + num (uint64 big endian) + num_suffix -> hash
block_hash_prefix = b"H" # block_hash_prefix + hash -> num (uint64 big endian)
block_receipts_prefix = (
b"r"
) # block_receipts_prefix + num (uint64 big endian) + hash -> block receipts
# known geth keys
head_header_key = b'LastBlock' # head (latest) header hash
head_header_key = b"LastBlock" # head (latest) header hash
# custom prefixes
address_prefix = b'AM' # address_prefix + hash -> address
address_prefix = b"AM" # address_prefix + hash -> address
# custom keys
address_mapping_head_key = b'accountMapping' # head (latest) number of indexed block
address_mapping_head_key = b"accountMapping" # head (latest) number of indexed block
def _format_block_number(number):
"""
formats block number to uint64 big endian
"""
"""Format block number to uint64 big endian."""
return utils.zpad(utils.int_to_big_endian(number), 8)
def _encode_hex(v):
"""
encodes hash as hex
"""
return '0x' + utils.encode_hex(v)
"""Encode a hash string as hex."""
return "0x" + utils.encode_hex(v)
class LevelDBReader(object):
"""
level db reading interface, can be used with snapshot
"""
"""LevelDB reading interface, can be used with snapshot."""
def __init__(self, db):
"""
:param db:
"""
self.db = db
self.head_block_header = None
self.head_state = None
def _get_head_state(self):
"""
gets head state
"""Get head state.
:return:
"""
if not self.head_state:
root = self._get_head_block().state_root
@ -59,137 +66,160 @@ class LevelDBReader(object):
return self.head_state
def _get_account(self, address):
"""
gets account by address
"""Get account by address.
:param address:
:return:
"""
state = self._get_head_state()
account_address = binascii.a2b_hex(utils.remove_0x_head(address))
return state.get_and_cache_account(account_address)
def _get_block_hash(self, number):
"""
gets block hash by block number
"""Get block hash by block number.
:param number:
:return:
"""
num = _format_block_number(number)
hash_key = header_prefix + num + num_suffix
return self.db.get(hash_key)
def _get_head_block(self):
"""
gets head block header
"""Get head block header.
:return:
"""
if not self.head_block_header:
hash = self.db.get(head_header_key)
num = self._get_block_number(hash)
self.head_block_header = self._get_block_header(hash, num)
block_hash = self.db.get(head_header_key)
num = self._get_block_number(block_hash)
self.head_block_header = self._get_block_header(block_hash, num)
# find header with valid state
while not self.db.get(self.head_block_header.state_root) and self.head_block_header.prevhash is not None:
hash = self.head_block_header.prevhash
num = self._get_block_number(hash)
self.head_block_header = self._get_block_header(hash, num)
while (
not self.db.get(self.head_block_header.state_root)
and self.head_block_header.prevhash is not None
):
block_hash = self.head_block_header.prevhash
num = self._get_block_number(block_hash)
self.head_block_header = self._get_block_header(block_hash, num)
return self.head_block_header
def _get_block_number(self, hash):
"""
gets block number by hash
def _get_block_number(self, block_hash):
"""Get block number by its hash.
:param block_hash:
:return:
"""
number_key = block_hash_prefix + hash
number_key = block_hash_prefix + block_hash
return self.db.get(number_key)
def _get_block_header(self, hash, num):
"""
get block header by block header hash & number
def _get_block_header(self, block_hash, num):
"""Get block header by block header hash & number.
:param block_hash:
:param num:
:return:
"""
header_key = header_prefix + num + hash
header_key = header_prefix + num + block_hash
block_header_data = self.db.get(header_key)
header = rlp.decode(block_header_data, sedes=BlockHeader)
return header
def _get_address_by_hash(self, hash):
"""
get mapped address by its hash
def _get_address_by_hash(self, block_hash):
"""Get mapped address by its hash.
:param block_hash:
:return:
"""
address_key = address_prefix + hash
address_key = address_prefix + block_hash
return self.db.get(address_key)
def _get_last_indexed_number(self):
"""
latest indexed block number
"""Get latest indexed block number.
:return:
"""
return self.db.get(address_mapping_head_key)
def _get_block_receipts(self, hash, num):
"""
get block transaction receipts by block header hash & number
def _get_block_receipts(self, block_hash, num):
"""Get block transaction receipts by block header hash & number.
:param block_hash:
:param num:
:return:
"""
number = _format_block_number(num)
receipts_key = block_receipts_prefix + number + hash
receipts_key = block_receipts_prefix + number + block_hash
receipts_data = self.db.get(receipts_key)
receipts = rlp.decode(receipts_data, sedes=CountableList(ReceiptForStorage))
return receipts
class LevelDBWriter(object):
"""
level db writing interface
"""
"""level db writing interface."""
def __init__(self, db):
"""
:param db:
"""
self.db = db
self.wb = None
def _set_last_indexed_number(self, number):
"""
sets latest indexed block number
"""Set latest indexed block number.
:param number:
:return:
"""
return self.db.put(address_mapping_head_key, _format_block_number(number))
def _start_writing(self):
"""
start writing a batch
"""
"""Start writing a batch."""
self.wb = self.db.write_batch()
def _commit_batch(self):
"""
commit batch
"""
"""Commit a batch."""
self.wb.write()
def _store_account_address(self, address):
"""
get block transaction receipts by block header hash & number
"""Get block transaction receipts by block header hash & number.
:param address:
"""
address_key = address_prefix + utils.sha3(address)
self.wb.put(address_key, address)
class EthLevelDB(object):
"""
Go-Ethereum LevelDB client class
"""
"""Go-Ethereum LevelDB client class."""
def __init__(self, path):
"""
:param path:
"""
self.path = path
self.db = ETH_DB(path)
self.reader = LevelDBReader(self.db)
self.writer = LevelDBWriter(self.db)
def get_contracts(self):
"""
iterate through all contracts
"""
"""Iterate through all contracts."""
for account in self.reader._get_head_state().get_all_accounts():
if account.code is not None:
code = _encode_hex(account.code)
contract = ETHContract(code, enable_online_lookup=False)
contract = EVMContract(code, enable_online_lookup=False)
yield contract, account.address, account.balance
def search(self, expression, callback_func):
"""
searches through all contract accounts
"""Search through all contract accounts.
:param expression:
:param callback_func:
"""
cnt = 0
indexer = AccountIndexer(self)
@ -201,11 +231,13 @@ class EthLevelDB(object):
try:
address = _encode_hex(indexer.get_contract_by_hash(address_hash))
except AddressNotFoundError:
'''
The hash->address mapping does not exist in our index. If the index is up-to-date, this likely means
that the contract was created by an internal transaction. Skip this contract as right now we don't
have a good solution for this.
'''
"""The hash->address mapping does not exist in our index.
If the index is up-to-date, this likely means that
the contract was created by an internal transaction.
Skip this contract as right now we don't have a good
solution for this.
"""
continue
@ -214,29 +246,35 @@ class EthLevelDB(object):
cnt += 1
if not cnt % 1000:
logging.info("Searched %d contracts" % cnt)
log.info("Searched %d contracts" % cnt)
def contract_hash_to_address(self, hash):
"""
tries to find corresponding account address
def contract_hash_to_address(self, contract_hash):
"""Try to find corresponding account address.
:param contract_hash:
:return:
"""
address_hash = binascii.a2b_hex(utils.remove_0x_head(hash))
address_hash = binascii.a2b_hex(utils.remove_0x_head(contract_hash))
indexer = AccountIndexer(self)
return _encode_hex(indexer.get_contract_by_hash(address_hash))
def eth_getBlockHeaderByNumber(self, number):
"""Get block header by block number.
:param number:
:return:
"""
gets block header by block number
"""
hash = self.reader._get_block_hash(number)
block_hash = self.reader._get_block_hash(number)
block_number = _format_block_number(number)
return self.reader._get_block_header(hash, block_number)
return self.reader._get_block_header(block_hash, block_number)
def eth_getBlockByNumber(self, number):
"""
gets block body by block number
"""Get block body by block number.
:param number:
:return:
"""
block_hash = self.reader._get_block_hash(number)
block_number = _format_block_number(number)
@ -246,22 +284,31 @@ class EthLevelDB(object):
return body
def eth_getCode(self, address):
"""
gets account code
"""Get account code.
:param address:
:return:
"""
account = self.reader._get_account(address)
return _encode_hex(account.code)
def eth_getBalance(self, address):
"""
gets account balance
"""Get account balance.
:param address:
:return:
"""
account = self.reader._get_account(address)
return account.balance
def eth_getStorageAt(self, address, position):
"""
gets account storage data at position
"""Get account storage data at position.
:param address:
:param position:
:return:
"""
account = self.reader._get_account(address)
return _encode_hex(utils.zpad(utils.encode_int(account.get_storage_data(position)), 32))
return _encode_hex(
utils.zpad(utils.encode_int(account.get_storage_data(position)), 32)
)

@ -1,29 +1,23 @@
"""This module contains the ETH_DB class, which the base database used by
pyethereum."""
import plyvel
from ethereum.db import BaseDB
class ETH_DB(BaseDB):
"""
adopts pythereum BaseDB using plyvel
"""
"""Adopts pythereum BaseDB using plyvel."""
def __init__(self, path):
self.db = plyvel.DB(path)
def get(self, key):
"""
gets value for key
"""
"""gets value for key."""
return self.db.get(key)
def put(self, key, value):
"""
puts value for key
"""
"""puts value for key."""
self.db.put(key, value)
def write_batch(self):
"""
start writing a batch
"""
"""start writing a batch."""
return self.db.write_batch()

@ -1,24 +1,40 @@
"""This module implements wrappers around the pyethereum state for LevelDB."""
import rlp
import binascii
from ethereum.utils import normalize_address, hash32, trie_root, \
big_endian_int, address, int256, encode_hex, encode_int, \
big_endian_to_int, int_to_addr, zpad, parse_as_bin, parse_as_int, \
decode_hex, sha3, is_string, is_numeric
from ethereum.utils import (
normalize_address,
hash32,
trie_root,
big_endian_int,
address,
int256,
encode_hex,
encode_int,
big_endian_to_int,
int_to_addr,
zpad,
parse_as_bin,
parse_as_int,
decode_hex,
sha3,
is_string,
is_numeric,
)
from rlp.sedes import big_endian_int, Binary, binary, CountableList
from ethereum import utils
from ethereum import trie
from ethereum.trie import Trie
from ethereum.securetrie import SecureTrie
BLANK_HASH = utils.sha3(b'')
BLANK_ROOT = utils.sha3rlp(b'')
BLANK_HASH = utils.sha3(b"")
BLANK_ROOT = utils.sha3rlp(b"")
STATE_DEFAULTS = {
"txindex": 0,
"gas_used": 0,
"gas_limit": 3141592,
"block_number": 0,
"block_coinbase": '\x00' * 20,
"block_coinbase": "\x00" * 20,
"block_difficulty": 1,
"timestamp": 0,
"logs": [],
@ -32,20 +48,27 @@ STATE_DEFAULTS = {
class Account(rlp.Serializable):
"""
adjusted account from ethereum.state
"""
"""adjusted account from ethereum.state."""
fields = [
('nonce', big_endian_int),
('balance', big_endian_int),
('storage', trie_root),
('code_hash', hash32)
("nonce", big_endian_int),
("balance", big_endian_int),
("storage", trie_root),
("code_hash", hash32),
]
def __init__(self, nonce, balance, storage, code_hash, db, address):
def __init__(self, nonce, balance, storage, code_hash, db, addr):
"""
:param nonce:
:param balance:
:param storage:
:param code_hash:
:param db:
:param addr:
"""
self.db = db
self.address = address
self.address = addr
super(Account, self).__init__(nonce, balance, storage, code_hash)
self.storage_cache = {}
self.storage_trie = SecureTrie(Trie(self.db))
@ -57,72 +80,86 @@ class Account(rlp.Serializable):
@property
def code(self):
"""
code rlp data
"""
"""code rlp data."""
return self.db.get(self.code_hash)
def get_storage_data(self, key):
"""
get storage data
"""get storage data.
:param key:
:return:
"""
if key not in self.storage_cache:
v = self.storage_trie.get(utils.encode_int32(key))
self.storage_cache[key] = utils.big_endian_to_int(
rlp.decode(v) if v else b'')
rlp.decode(v) if v else b""
)
return self.storage_cache[key]
@classmethod
def blank_account(cls, db, address, initial_nonce=0):
"""
creates a blank account
def blank_account(cls, db, addr, initial_nonce=0):
"""creates a blank account.
:param db:
:param addr:
:param initial_nonce:
:return:
"""
db.put(BLANK_HASH, b'')
o = cls(initial_nonce, 0, trie.BLANK_ROOT, BLANK_HASH, db, address)
db.put(BLANK_HASH, b"")
o = cls(initial_nonce, 0, trie.BLANK_ROOT, BLANK_HASH, db, addr)
o.existent_at_start = False
return o
def is_blank(self):
"""
checks if is a blank account
"""checks if is a blank account.
:return:
"""
return self.nonce == 0 and self.balance == 0 and self.code_hash == BLANK_HASH
class State:
"""
adjusted state from ethereum.state
"""
"""adjusted state from ethereum.state."""
def __init__(self, db, root):
"""
:param db:
:param root:
"""
self.db = db
self.trie = Trie(self.db, root)
self.secure_trie = SecureTrie(self.trie)
self.journal = []
self.cache = {}
def get_and_cache_account(self, address):
"""
gets and caches an account for an addres, creates blank if not found
def get_and_cache_account(self, addr):
"""Gets and caches an account for an addres, creates blank if not
found.
:param addr:
:return:
"""
if address in self.cache:
return self.cache[address]
rlpdata = self.secure_trie.get(address)
if rlpdata == trie.BLANK_NODE and len(address) == 32: # support for hashed addresses
rlpdata = self.trie.get(address)
if addr in self.cache:
return self.cache[addr]
rlpdata = self.secure_trie.get(addr)
if (
rlpdata == trie.BLANK_NODE and len(addr) == 32
): # support for hashed addresses
rlpdata = self.trie.get(addr)
if rlpdata != trie.BLANK_NODE:
o = rlp.decode(rlpdata, Account, db=self.db, address=address)
o = rlp.decode(rlpdata, Account, db=self.db, address=addr)
else:
o = Account.blank_account(
self.db, address, 0)
self.cache[address] = o
o = Account.blank_account(self.db, addr, 0)
self.cache[addr] = o
o._mutable = True
o._cached_rlp = None
return o
def get_all_accounts(self):
"""
iterates through trie to and yields non-blank leafs as accounts
"""
"""iterates through trie to and yields non-blank leafs as accounts."""
for address_hash, rlpdata in self.secure_trie.trie.iter_branch():
if rlpdata != trie.BLANK_NODE:
yield rlp.decode(rlpdata, Account, db=self.db, address=address_hash)

@ -1,6 +1,11 @@
from abc import (abstractmethod)
"""This module provides a basic RPC interface client.
from .constants import BLOCK_TAGS, BLOCK_TAG_LATEST
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
"""
from abc import abstractmethod
from .constants import BLOCK_TAG_LATEST, BLOCK_TAGS
from .utils import hex_to_dec, validate_block
GETH_DEFAULT_RPC_PORT = 8545
@ -8,54 +13,66 @@ ETH_DEFAULT_RPC_PORT = 8545
PARITY_DEFAULT_RPC_PORT = 8545
PYETHAPP_DEFAULT_RPC_PORT = 4000
MAX_RETRIES = 3
JSON_MEDIA_TYPE = 'application/json'
JSON_MEDIA_TYPE = "application/json"
'''
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
'''
class BaseClient(object):
"""The base RPC client class."""
@abstractmethod
def _call(self, method, params=None, _id=1):
"""TODO: documentation
:param method:
:param params:
:param _id:
:return:
"""
pass
def eth_coinbase(self):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase
TESTED
"""
return self._call('eth_coinbase')
return self._call("eth_coinbase")
def eth_blockNumber(self):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_blocknumber
TESTED
"""
return hex_to_dec(self._call('eth_blockNumber'))
return hex_to_dec(self._call("eth_blockNumber"))
def eth_getBalance(self, address=None, block=BLOCK_TAG_LATEST):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance
TESTED
"""
address = address or self.eth_coinbase()
block = validate_block(block)
return hex_to_dec(self._call('eth_getBalance', [address, block]))
return hex_to_dec(self._call("eth_getBalance", [address, block]))
def eth_getStorageAt(self, address=None, position=0, block=BLOCK_TAG_LATEST):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getstorageat
TESTED
"""
block = validate_block(block)
return self._call('eth_getStorageAt', [address, hex(position), block])
return self._call("eth_getStorageAt", [address, hex(position), block])
def eth_getCode(self, address, default_block=BLOCK_TAG_LATEST):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getcode
NEEDS TESTING
@ -63,21 +80,23 @@ class BaseClient(object):
if isinstance(default_block, str):
if default_block not in BLOCK_TAGS:
raise ValueError
return self._call('eth_getCode', [address, default_block])
return self._call("eth_getCode", [address, default_block])
def eth_getBlockByNumber(self, block=BLOCK_TAG_LATEST, tx_objects=True):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber
TESTED
"""
block = validate_block(block)
return self._call('eth_getBlockByNumber', [block, tx_objects])
return self._call("eth_getBlockByNumber", [block, tx_objects])
def eth_getTransactionReceipt(self, tx_hash):
"""
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt
TESTED
"""
return self._call('eth_getTransactionReceipt', [tx_hash])
return self._call("eth_getTransactionReceipt", [tx_hash])

@ -1,27 +1,42 @@
"""This module contains a basic Ethereum RPC client.
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
"""
import json
import logging
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError as RequestsConnectionError
from .exceptions import (ConnectionError, BadStatusCodeError, BadJsonError, BadResponseError)
from .base_client import BaseClient
from .exceptions import (
BadJsonError,
BadResponseError,
BadStatusCodeError,
ConnectionError,
)
log = logging.getLogger(__name__)
GETH_DEFAULT_RPC_PORT = 8545
ETH_DEFAULT_RPC_PORT = 8545
PARITY_DEFAULT_RPC_PORT = 8545
PYETHAPP_DEFAULT_RPC_PORT = 4000
MAX_RETRIES = 3
JSON_MEDIA_TYPE = 'application/json'
JSON_MEDIA_TYPE = "application/json"
'''
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
'''
class EthJsonRpc(BaseClient):
"""
Ethereum JSON-RPC client class
"""
"""Ethereum JSON-RPC client class."""
def __init__(self, host="localhost", port=GETH_DEFAULT_RPC_PORT, tls=False):
"""
def __init__(self, host='localhost', port=GETH_DEFAULT_RPC_PORT, tls=False):
:param host:
:param port:
:param tls:
"""
self.host = host
self.port = port
self.tls = tls
@ -29,20 +44,21 @@ class EthJsonRpc(BaseClient):
self.session.mount(self.host, HTTPAdapter(max_retries=MAX_RETRIES))
def _call(self, method, params=None, _id=1):
"""
:param method:
:param params:
:param _id:
:return:
"""
params = params or []
data = {
'jsonrpc': '2.0',
'method': method,
'params': params,
'id': _id,
}
scheme = 'http'
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": _id}
scheme = "http"
if self.tls:
scheme += 's'
url = '{}://{}:{}'.format(scheme, self.host, self.port)
headers = {'Content-Type': JSON_MEDIA_TYPE}
logging.debug("rpc send: %s" % json.dumps(data))
scheme += "s"
url = "{}://{}:{}".format(scheme, self.host, self.port)
headers = {"Content-Type": JSON_MEDIA_TYPE}
log.debug("rpc send: %s" % json.dumps(data))
try:
r = self.session.post(url, headers=headers, data=json.dumps(data))
except RequestsConnectionError:
@ -51,13 +67,14 @@ class EthJsonRpc(BaseClient):
raise BadStatusCodeError(r.status_code)
try:
response = r.json()
logging.debug("rpc response: %s" % response)
log.debug("rpc response: %s" % response)
except ValueError:
raise BadJsonError(r.text)
try:
return response['result']
return response["result"]
except KeyError:
raise BadResponseError(response)
def close(self):
"""Close the RPC client's session."""
self.session.close()

@ -1,8 +1,6 @@
BLOCK_TAG_EARLIEST = 'earliest'
BLOCK_TAG_LATEST = 'latest'
BLOCK_TAG_PENDING = 'pending'
BLOCK_TAGS = (
BLOCK_TAG_EARLIEST,
BLOCK_TAG_LATEST,
BLOCK_TAG_PENDING,
)
"""This file contains constants used used by the Ethereum JSON RPC
interface."""
BLOCK_TAG_EARLIEST = "earliest"
BLOCK_TAG_LATEST = "latest"
BLOCK_TAG_PENDING = "pending"
BLOCK_TAGS = (BLOCK_TAG_EARLIEST, BLOCK_TAG_LATEST, BLOCK_TAG_PENDING)

@ -1,18 +1,35 @@
"""This module contains exceptions regarding JSON-RPC communication."""
class EthJsonRpcError(Exception):
"""The JSON-RPC base exception type."""
pass
class ConnectionError(EthJsonRpcError):
"""An RPC exception denoting there was an error in connecting to the RPC
instance."""
pass
class BadStatusCodeError(EthJsonRpcError):
"""An RPC exception denoting a bad status code returned by the RPC
instance."""
pass
class BadJsonError(EthJsonRpcError):
"""An RPC exception denoting that the RPC instance returned a bad JSON
object."""
pass
class BadResponseError(EthJsonRpcError):
"""An RPC exception denoting that the RPC instance returned a bad
response."""
pass

@ -1,38 +1,54 @@
"""This module contains various utility functions regarding the RPC data format
and validation."""
from .constants import BLOCK_TAGS
def hex_to_dec(x):
"""
Convert hex to decimal
"""Convert hex to decimal.
:param x:
:return:
"""
return int(x, 16)
def clean_hex(d):
"""Convert decimal to hex and remove the "L" suffix that is appended to
large numbers.
:param d:
:return:
"""
Convert decimal to hex and remove the "L" suffix that is appended to large
numbers
"""
return hex(d).rstrip('L')
return hex(d).rstrip("L")
def validate_block(block):
"""
:param block:
:return:
"""
if isinstance(block, str):
if block not in BLOCK_TAGS:
raise ValueError('invalid block tag')
raise ValueError("invalid block tag")
if isinstance(block, int):
block = hex(block)
return block
def wei_to_ether(wei):
"""Convert wei to ether.
:param wei:
:return:
"""
Convert wei to ether
"""
return 1.0 * wei / 10**18
return 1.0 * wei / 10 ** 18
def ether_to_wei(ether):
"""Convert ether to wei.
:param ether:
:return:
"""
Convert ether to wei
"""
return ether * 10**18
return ether * 10 ** 18

@ -1,15 +1,23 @@
from ethereum.abi import encode_abi, encode_int
from ethereum.utils import zpad
from ethereum.abi import method_id
from mythril.exceptions import CompilerError
from subprocess import Popen, PIPE
"""This module contains various utility functions regarding unit conversion and
solc integration."""
import binascii
import os
import json
import os
from pathlib import Path
from subprocess import PIPE, Popen
from ethereum.abi import encode_abi, encode_int, method_id
from ethereum.utils import zpad
from mythril.exceptions import CompilerError
def safe_decode(hex_encoded_string):
"""
:param hex_encoded_string:
:return:
"""
if hex_encoded_string.startswith("0x"):
return bytes.fromhex(hex_encoded_string[2:])
else:
@ -17,12 +25,18 @@ def safe_decode(hex_encoded_string):
def get_solc_json(file, solc_binary="solc", solc_args=None):
"""
:param file:
:param solc_binary:
:param solc_args:
:return:
"""
cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime"]
if solc_args:
cmd.extend(solc_args.split())
if not "--allow-paths" in cmd:
cmd.extend(["--allow-paths", "."])
else:
@ -39,9 +53,14 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
ret = p.returncode
if ret != 0:
raise CompilerError("Solc experienced a fatal error (code %d).\n\n%s" % (ret, stderr.decode('UTF-8')))
raise CompilerError(
"Solc experienced a fatal error (code %d).\n\n%s"
% (ret, stderr.decode("UTF-8"))
)
except FileNotFoundError:
raise CompilerError("Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable.")
raise CompilerError(
"Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable."
)
out = stdout.decode("UTF-8")
@ -52,6 +71,13 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
def encode_calldata(func_name, arg_types, args):
"""
:param func_name:
:param arg_types:
:param args:
:return:
"""
mid = method_id(func_name, arg_types)
function_selector = zpad(encode_int(mid), 4)
args = encode_abi(arg_types, args)
@ -59,16 +85,38 @@ def encode_calldata(func_name, arg_types, args):
def get_random_address():
return binascii.b2a_hex(os.urandom(20)).decode('UTF-8')
"""
:return:
"""
return binascii.b2a_hex(os.urandom(20)).decode("UTF-8")
def get_indexed_address(index):
"""
:param index:
:return:
"""
return "0x" + (hex(index)[2:] * 40)
def solc_exists(version):
solc_binary = os.path.join(os.environ['HOME'], ".py-solc/solc-v" + version, "bin/solc")
if os.path.exists(solc_binary):
return True
else:
return False
"""
:param version:
:return:
"""
solc_binaries = [
os.path.join(
os.environ.get("HOME", str(Path.home())),
".py-solc/solc-v" + version,
"bin/solc",
) # py-solc setup
]
if version.startswith("0.5"):
# Temporary fix to support v0.5.x with Ubuntu PPA setup
solc_binaries.append("/usr/bin/solc")
for solc_path in solc_binaries:
if os.path.exists(solc_path):
return solc_path

@ -1,22 +1,45 @@
"""This module contains general exceptions used by Mythril."""
class MythrilBaseException(Exception):
"""The Mythril exception base type."""
pass
class OutOfTimeError(MythrilBaseException):
pass
class CompilerError(MythrilBaseException):
"""A Mythril exception denoting an error during code compilation."""
pass
class UnsatError(MythrilBaseException):
"""A Mythril exception denoting the unsatisfiability of a series of
constraints."""
pass
class NoContractFoundError(MythrilBaseException):
"""A Mythril exception denoting that a given contract file was not
found."""
pass
class CriticalError(MythrilBaseException):
"""A Mythril exception denoting an unknown critical error has been
encountered."""
pass
class AddressNotFoundError(MythrilBaseException):
"""A Mythril exception denoting the given smart contract address was not
found."""
pass

@ -5,111 +5,298 @@
http://www.github.com/ConsenSys/mythril
"""
import logging, coloredlogs
import argparse
import json
import logging
import os
import sys
import argparse
# logging.basicConfig(level=logging.DEBUG)
import coloredlogs
from mythril.exceptions import CriticalError, AddressNotFoundError
import mythril.support.signatures as sigs
from mythril.exceptions import AddressNotFoundError, CriticalError
from mythril.mythril import Mythril
from mythril.version import VERSION
# logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
def exit_with_error(format, message):
if format == 'text' or format == 'markdown':
print(message)
def exit_with_error(format_, message):
"""
:param format_:
:param message:
"""
if format_ == "text" or format_ == "markdown":
log.error(message)
else:
result = {'success': False, 'error': str(message), 'issues': []}
result = {"success": False, "error": str(message), "issues": []}
print(json.dumps(result))
sys.exit()
def main():
parser = argparse.ArgumentParser(description='Security analysis of Ethereum smart contracts')
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('-t', '--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('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE')
inputs.add_argument('-f', '--codefile', help='file containing hex-encoded bytecode string',
metavar='BYTECODEFILE', type=argparse.FileType('r'))
inputs.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')
outputs = parser.add_argument_group('output formats')
outputs.add_argument('-o', '--outform', choices=['text', 'markdown', 'json'], default='text',
help='report output format', metavar='<text/markdown/json>')
outputs.add_argument('--verbose-report', action='store_true', help='Include debugging information in report')
database = parser.add_argument_group('local contracts database')
database.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION')
database.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...]')
utilities.add_argument('--solv',
help='specify solidity compiler version. If not present, will try to install it (Experimental)',
metavar='SOLV')
utilities.add_argument('--contract-hash-to-address', help='returns corresponding address for a contract address hash', metavar='SHA3_TO_LOOK_FOR')
options = parser.add_argument_group('options')
options.add_argument('-m', '--modules', help='Comma-separated list of security analysis modules', metavar='MODULES')
options.add_argument('--max-depth', type=int, default=22, help='Maximum recursion depth for symbolic execution')
options.add_argument('--max-transaction-count', type=int, default=3, help='Maximum number of transactions issued by laser')
options.add_argument('--strategy', choices=['dfs', 'bfs'], default='dfs', help='Symbolic execution strategy')
options.add_argument('--execution-timeout', type=int, default=600, help="The amount of seconds to spend on symbolic execution")
options.add_argument('--create-timeout', type=int, default=10, help="The amount of seconds to spend on "
"the initial contract creation")
options.add_argument('--solc-args', help='Extra arguments for solc')
options.add_argument('--phrack', action='store_true', help='Phrack-style call graph')
options.add_argument('--enable-physics', action='store_true', help='enable graph physics simulation')
options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL')
rpc = parser.add_argument_group('RPC options')
rpc.add_argument('-i', action='store_true', help='Preset: Infura Node service (Mainnet)')
rpc.add_argument('--rpc', help='custom RPC settings', metavar='HOST:PORT / ganache / infura-[network_name]')
rpc.add_argument('--rpctls', type=bool, default=False, help='RPC connection over TLS')
"""The main CLI interface entry point."""
parser = argparse.ArgumentParser(
description="Security analysis of Ethereum smart contracts"
)
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(
"-c",
"--code",
help='hex-encoded bytecode string ("6060604052...")',
metavar="BYTECODE",
)
inputs.add_argument(
"-f",
"--codefile",
help="file containing hex-encoded bytecode string",
metavar="BYTECODEFILE",
type=argparse.FileType("r"),
)
inputs.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(
"--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.",
)
outputs = parser.add_argument_group("output formats")
outputs.add_argument(
"-o",
"--outform",
choices=["text", "markdown", "json", "jsonv2"],
default="text",
help="report output format",
metavar="<text/markdown/json/jsonv2>",
)
outputs.add_argument(
"--verbose-report",
action="store_true",
help="Include debugging information in report",
)
database = parser.add_argument_group("local contracts database")
database.add_argument(
"-s", "--search", help="search the contract database", metavar="EXPRESSION"
)
database.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...]",
)
utilities.add_argument(
"--solv",
help="specify solidity compiler version. If not present, will try to install it (Experimental)",
metavar="SOLV",
)
utilities.add_argument(
"--contract-hash-to-address",
help="returns corresponding address for a contract address hash",
metavar="SHA3_TO_LOOK_FOR",
)
options = parser.add_argument_group("options")
options.add_argument(
"-m",
"--modules",
help="Comma-separated list of security analysis modules",
metavar="MODULES",
)
options.add_argument(
"--max-depth",
type=int,
default=22,
help="Maximum recursion depth for symbolic execution",
)
options.add_argument(
"--strategy",
choices=["dfs", "bfs", "naive-random", "weighted-random"],
default="dfs",
help="Symbolic execution strategy",
)
options.add_argument(
"-t",
"--transaction-count",
type=int,
default=2,
help="Maximum number of transactions issued by laser",
)
options.add_argument(
"--execution-timeout",
type=int,
default=600,
help="The amount of seconds to spend on symbolic execution",
)
options.add_argument(
"--create-timeout",
type=int,
default=10,
help="The amount of seconds to spend on " "the initial contract creation",
)
options.add_argument("--solc-args", help="Extra arguments for solc")
options.add_argument(
"--phrack", action="store_true", help="Phrack-style call graph"
)
options.add_argument(
"--enable-physics", action="store_true", help="enable graph physics simulation"
)
options.add_argument(
"-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2
)
options.add_argument(
"-q",
"--query-signature",
action="store_true",
help="Lookup function signatures through www.4byte.directory",
)
options.add_argument(
"--enable-iprof", action="store_true", help="enable the instruction profiler"
)
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)
# Get config values
args = parser.parse_args()
if args.epic:
path = os.path.dirname(os.path.realpath(__file__))
sys.argv.remove("--epic")
os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py")
sys.exit()
if args.version:
if args.outform == 'json':
print(json.dumps({'version_str': VERSION}))
if args.outform == "json":
print(json.dumps({"version_str": VERSION}))
else:
print("Mythril version {}".format(VERSION))
sys.exit()
# Parse cmdline args
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):
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:
if 0 <= args.v < 3:
if 0 <= args.v < 6:
log_levels = [
logging.NOTSET,
logging.CRITICAL,
logging.ERROR,
logging.WARNING,
logging.INFO,
logging.DEBUG,
]
coloredlogs.install(
fmt='%(name)s[%(process)d] %(levelname)s %(message)s',
level=[logging.NOTSET, logging.INFO, logging.DEBUG][args.v]
fmt="%(name)s [%(levelname)s]: %(message)s", level=log_levels[args.v]
)
logging.getLogger("mythril").setLevel(log_levels[args.v])
else:
exit_with_error(args.outform, "Invalid -v value, you can find valid values in usage")
exit_with_error(
args.outform, "Invalid -v value, you can find valid values in usage"
)
if args.query_signature:
if sigs.ethereum_input_decoder == None:
exit_with_error(
args.outform,
"The --query-signature function requires the python package ethereum-input-decoder",
)
if args.enable_iprof:
if 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",
)
# -- commands --
if args.hash:
@ -121,22 +308,28 @@ def main():
# infura = None, rpc = None, rpctls = None
# solc_args = None, dynld = None, max_recursion_depth = 12):
mythril = Mythril(solv=args.solv, dynld=args.dynld,
solc_args=args.solc_args)
if args.dynld and not (args.rpc or args.i):
mythril = Mythril(
solv=args.solv,
dynld=args.dynld,
onchain_storage_access=(not args.no_onchain_storage_access),
solc_args=args.solc_args,
enable_online_lookup=args.query_signature,
)
if (
args.dynld
or not args.no_onchain_storage_access
and not (args.rpc or args.i)
):
mythril.set_api_from_config_path()
if args.address:
# Establish RPC connection if necessary
if args.i:
mythril.set_api_rpc_infura()
elif args.rpc:
mythril.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls)
elif not args.dynld:
mythril.set_api_rpc_localhost()
mythril.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls)
elif args.search or args.contract_hash_to_address:
# Open LevelDB if necessary
mythril.set_api_leveldb(mythril.leveldb_dir if not args.leveldb_dir else args.leveldb_dir)
mythril.set_api_leveldb(
mythril.leveldb_dir if not args.leveldb_dir else args.leveldb_dir
)
if args.search:
# Database search ops
@ -158,7 +351,8 @@ def main():
mythril.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()
# Load / compile input contracts
@ -166,47 +360,70 @@ def main():
if args.code:
# Load from bytecode
address, _ = mythril.load_from_bytecode(args.code)
code = args.code[2:] if args.code.startswith("0x") else args.code
address, _ = mythril.load_from_bytecode(code, args.bin_runtime)
elif args.codefile:
bytecode = ''.join([l.strip() for l in args.codefile if len(l.strip()) > 0])
address, _ = mythril.load_from_bytecode(bytecode)
bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0])
bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode
address, _ = mythril.load_from_bytecode(bytecode, args.bin_runtime)
elif args.address:
# Get bytecode from a contract address
address, _ = mythril.load_from_address(args.address)
elif args.solidity_file:
# Compile Solidity source file(s)
if 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.")
exit_with_error(
args.outform,
"Cannot generate call graphs from multiple input files. Please do it one at a time.",
)
address, _ = mythril.load_from_solidity(args.solidity_file) # 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")
exit_with_error(
args.outform,
"No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES",
)
# Commands
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.")
storage = mythril.get_state_variable_from_storage(address=address,
params=[a.strip() for a in args.storage.strip().split(",")])
exit_with_error(
args.outform,
"To read storage, provide the address of a deployed contract with the -a option.",
)
storage = mythril.get_state_variable_from_storage(
address=address,
params=[a.strip() for a in args.storage.strip().split(",")],
)
print(storage)
elif args.disassemble:
easm_text = mythril.contracts[0].get_easm() # or mythril.disassemble(mythril.contracts[0])
sys.stdout.write(easm_text)
# or mythril.disassemble(mythril.contracts[0])
if mythril.contracts[0].code:
print("Runtime Disassembly: \n" + mythril.contracts[0].get_easm())
if mythril.contracts[0].creation_code:
print("Disassembly: \n" + mythril.contracts[0].get_creation_easm())
elif args.graph or args.fire_lasers:
if not mythril.contracts:
exit_with_error(args.outform, "input files do not contain any valid contracts")
exit_with_error(
args.outform, "input files do not contain any valid contracts"
)
if args.graph:
html = mythril.graph_html(strategy=args.strategy, contract=mythril.contracts[0], address=address,
enable_physics=args.enable_physics, phrackify=args.phrack,
max_depth=args.max_depth, execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout)
html = mythril.graph_html(
strategy=args.strategy,
contract=mythril.contracts[0],
address=address,
enable_physics=args.enable_physics,
phrackify=args.phrack,
max_depth=args.max_depth,
execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout,
enable_iprof=args.enable_iprof,
)
try:
with open(args.graph, "w") as f:
@ -215,27 +432,48 @@ def main():
exit_with_error(args.outform, "Error saving graph: " + str(e))
else:
report = mythril.fire_lasers(strategy=args.strategy, address=address,
modules=[m.strip() for m in args.modules.strip().split(",")] if args.modules else [],
verbose_report=args.verbose_report,
max_depth=args.max_depth, execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout,
max_transaction_count=args.max_transaction_count)
outputs = {
'json': report.as_json(),
'text': report.as_text(),
'markdown': report.as_markdown()
}
print(outputs[args.outform])
try:
report = mythril.fire_lasers(
strategy=args.strategy,
address=address,
modules=[m.strip() for m in args.modules.strip().split(",")]
if args.modules
else [],
verbose_report=args.verbose_report,
max_depth=args.max_depth,
execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout,
transaction_count=args.transaction_count,
enable_iprof=args.enable_iprof,
)
outputs = {
"json": report.as_json(),
"jsonv2": report.as_swc_standard_format(),
"text": report.as_text(),
"markdown": report.as_markdown(),
}
print(outputs[args.outform])
except ModuleNotFoundError as e:
exit_with_error(
args.outform, "Error loading analyis modules: " + format(e)
)
elif args.statespace_json:
if not mythril.contracts:
exit_with_error(args.outform, "input files do not contain any valid contracts")
statespace = mythril.dump_statespace(strategy=args.strategy, contract=mythril.contracts[0], address=address,
max_depth=args.max_depth, execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout)
exit_with_error(
args.outform, "input files do not contain any valid contracts"
)
statespace = mythril.dump_statespace(
strategy=args.strategy,
contract=mythril.contracts[0],
address=address,
max_depth=args.max_depth,
execution_timeout=args.execution_timeout,
create_timeout=args.create_timeout,
enable_iprof=args.enable_iprof,
)
try:
with open(args.statespace_json, "w") as f:

@ -0,0 +1,288 @@
"""Don't ask."""
#!/usr/bin/env python
#
# "THE BEER-WARE LICENSE" (Revision 43~maze)
#
# <maze@pyth0n.org> wrote these files. As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return.
# https://github.com/tehmaze/lolcat
import atexit
import math
import os
import random
import re
import sys
import time
PY3 = sys.version_info >= (3,)
# Reset terminal colors at exit
def reset():
""""""
sys.stdout.write("\x1b[0m")
sys.stdout.flush()
atexit.register(reset)
STRIP_ANSI = re.compile(r"\x1b\[(\d+)(;\d+)?(;\d+)?[m|K]")
COLOR_ANSI = (
(0x00, 0x00, 0x00),
(0xCD, 0x00, 0x00),
(0x00, 0xCD, 0x00),
(0xCD, 0xCD, 0x00),
(0x00, 0x00, 0xEE),
(0xCD, 0x00, 0xCD),
(0x00, 0xCD, 0xCD),
(0xE5, 0xE5, 0xE5),
(0x7F, 0x7F, 0x7F),
(0xFF, 0x00, 0x00),
(0x00, 0xFF, 0x00),
(0xFF, 0xFF, 0x00),
(0x5C, 0x5C, 0xFF),
(0xFF, 0x00, 0xFF),
(0x00, 0xFF, 0xFF),
(0xFF, 0xFF, 0xFF),
)
class LolCat(object):
"""Cats lel."""
def __init__(self, mode=256, output=sys.stdout):
self.mode = mode
self.output = output
def _distance(self, rgb1, rgb2):
return sum(map(lambda c: (c[0] - c[1]) ** 2, zip(rgb1, rgb2)))
def ansi(self, rgb):
"""
:param rgb:
:return:
"""
r, g, b = rgb
if self.mode in (8, 16):
colors = COLOR_ANSI[: self.mode]
matches = [
(self._distance(c, map(int, rgb)), i) for i, c in enumerate(colors)
]
matches.sort()
color = matches[0][1]
return "3%d" % (color,)
else:
gray_possible = True
sep = 2.5
while gray_possible:
if r < sep or g < sep or b < sep:
gray = r < sep and g < sep and b < sep
gray_possible = False
sep += 42.5
if gray:
color = 232 + int(float(sum(rgb) / 33.0))
else:
color = sum(
[16]
+ [
int(6 * float(val) / 256) * mod
for val, mod in zip(rgb, [36, 6, 1])
]
)
return "38;5;%d" % (color,)
def wrap(self, *codes):
"""
:param codes:
:return:
"""
return "\x1b[%sm" % ("".join(codes),)
def rainbow(self, freq, i):
"""
:param freq:
:param i:
:return:
"""
r = math.sin(freq * i) * 127 + 128
g = math.sin(freq * i + 2 * math.pi / 3) * 127 + 128
b = math.sin(freq * i + 4 * math.pi / 3) * 127 + 128
return [r, g, b]
def cat(self, fd, options):
"""
:param fd:
:param options:
"""
if options.animate:
self.output.write("\x1b[?25l")
for line in fd:
options.os += 1
self.println(line, options)
if options.animate:
self.output.write("\x1b[?25h")
def println(self, s, options):
"""
:param s:
:param options:
"""
s = s.rstrip()
if options.force or self.output.isatty():
s = STRIP_ANSI.sub("", s)
if options.animate:
self.println_ani(s, options)
else:
self.println_plain(s, options)
self.output.write("\n")
self.output.flush()
def println_ani(self, s, options):
"""
:param s:
:param options:
:return:
"""
if not s:
return
for i in range(1, options.duration):
self.output.write("\x1b[%dD" % (len(s),))
self.output.flush()
options.os += options.spread
self.println_plain(s, options)
time.sleep(1.0 / options.speed)
def println_plain(self, s, options):
"""
:param s:
:param options:
"""
for i, c in enumerate(s if PY3 else s.decode(options.charset_py2, "replace")):
rgb = self.rainbow(options.freq, options.os + i / options.spread)
self.output.write(
"".join(
[
self.wrap(self.ansi(rgb)),
c if PY3 else c.encode(options.charset_py2, "replace"),
]
)
)
def detect_mode(term_hint="xterm-256color"):
"""Poor-mans color mode detection."""
if "ANSICON" in os.environ:
return 16
elif os.environ.get("ConEmuANSI", "OFF") == "ON":
return 256
else:
term = os.environ.get("TERM", term_hint)
if term.endswith("-256color") or term in ("xterm", "screen"):
return 256
elif term.endswith("-color") or term in ("rxvt",):
return 16
else:
return 256 # optimistic default
def run():
"""Main entry point."""
import optparse
parser = optparse.OptionParser(usage=r"%prog [<options>] [file ...]")
parser.add_option(
"-p", "--spread", type="float", default=3.0, help="Rainbow spread"
)
parser.add_option(
"-F", "--freq", type="float", default=0.1, help="Rainbow frequency"
)
parser.add_option("-S", "--seed", type="int", default=0, help="Rainbow seed")
parser.add_option(
"-a",
"--animate",
action="store_true",
default=False,
help="Enable psychedelics",
)
parser.add_option(
"-d", "--duration", type="int", default=12, help="Animation duration"
)
parser.add_option(
"-s", "--speed", type="float", default=20.0, help="Animation speed"
)
parser.add_option(
"-f",
"--force",
action="store_true",
default=False,
help="Force colour even when stdout is not a tty",
)
parser.add_option(
"-3", action="store_const", dest="mode", const=8, help="Force 3 bit colour mode"
)
parser.add_option(
"-4",
action="store_const",
dest="mode",
const=16,
help="Force 4 bit colour mode",
)
parser.add_option(
"-8",
action="store_const",
dest="mode",
const=256,
help="Force 8 bit colour mode",
)
parser.add_option(
"-c",
"--charset-py2",
default="utf-8",
help="Manually set a charset to convert from, for python 2.7",
)
options, args = parser.parse_args()
options.os = random.randint(0, 256) if options.seed == 0 else options.seed
options.mode = options.mode or detect_mode()
lolcat = LolCat(mode=options.mode)
if not args:
args = ["-"]
for filename in args:
if filename == "-":
lolcat.cat(sys.stdin, options)
else:
try:
with open(filename, "r") as handle:
lolcat.cat(handle, options)
except IOError as error:
sys.stderr.write(str(error) + "\n")
if __name__ == "__main__":
sys.exit(run())

@ -1,8 +1,22 @@
"""This module contains the business logic used by Instruction in
instructions.py to get the necessary elements from the stack and determine the
parameters for the new global state."""
import logging
from typing import Union
from z3 import simplify, BitVecRef, BitVecNumRef, BoolRef
from typing import Union, List
from z3 import Z3Exception
from mythril.laser.ethereum import natives
from mythril.laser.ethereum.gas import OPCODE_GAS
from mythril.laser.smt import simplify, Expression, symbol_factory
import mythril.laser.ethereum.util as util
from mythril.laser.ethereum.state import Account, CalldataType, GlobalState
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import (
BaseCalldata,
SymbolicCalldata,
ConcreteCalldata,
)
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.support.loader import DynLoader
import re
@ -11,11 +25,15 @@ This module contains the business logic used by Instruction in instructions.py
to get the necessary elements from the stack and determine the parameters for the new global state.
"""
log = logging.getLogger(__name__)
def get_call_parameters(
global_state: GlobalState, dynamic_loader: DynLoader, with_value=False
):
"""Gets call parameters from global state Pops the values from the stack
and determines output parameters.
def get_call_parameters(global_state: GlobalState, dynamic_loader: DynLoader, with_value=False):
"""
Gets call parameters from global state
Pops the values from the stack and determines output parameters
:param global_state: state to look in
:param dynamic_loader: dynamic loader to use
:param with_value: whether to pop the value argument from the stack
@ -23,23 +41,38 @@ def get_call_parameters(global_state: GlobalState, dynamic_loader: DynLoader, wi
"""
gas, to = global_state.mstate.pop(2)
value = global_state.mstate.pop() if with_value else 0
memory_input_offset, memory_input_size, memory_out_offset, memory_out_size = global_state.mstate.pop(4)
memory_input_offset, memory_input_size, memory_out_offset, memory_out_size = global_state.mstate.pop(
4
)
callee_address = get_callee_address(global_state, dynamic_loader, to)
callee_account = None
call_data, call_data_type = get_call_data(global_state, memory_input_offset, memory_input_size, False)
call_data = get_call_data(global_state, memory_input_offset, memory_input_size)
if int(callee_address, 16) >= 5 or int(callee_address, 16) == 0:
call_data, call_data_type = get_call_data(global_state, memory_input_offset, memory_input_size)
callee_account = get_callee_account(global_state, callee_address, dynamic_loader)
return callee_address, callee_account, call_data, value, call_data_type, gas, memory_out_offset, memory_out_size
callee_account = get_callee_account(
global_state, callee_address, dynamic_loader
)
return (
callee_address,
callee_account,
call_data,
value,
gas,
memory_out_offset,
memory_out_size,
)
def get_callee_address(
global_state: GlobalState,
dynamic_loader: DynLoader,
symbolic_to_address: Expression,
):
"""Gets the address of the callee.
def get_callee_address(global_state: GlobalState, dynamic_loader: DynLoader, symbolic_to_address: BitVecRef):
"""
Gets the address of the callee
:param global_state: state to look in
:param dynamic_loader: dynamic loader to use
:param symbolic_to_address: The (symbolic) callee address
@ -50,23 +83,25 @@ def get_callee_address(global_state: GlobalState, dynamic_loader: DynLoader, sym
try:
callee_address = hex(util.get_concrete_int(symbolic_to_address))
except TypeError:
logging.debug("Symbolic call encountered")
log.debug("Symbolic call encountered")
match = re.search(r'storage_(\d+)', str(simplify(symbolic_to_address)))
logging.debug("CALL to: " + str(simplify(symbolic_to_address)))
match = re.search(r"storage_(\d+)", str(simplify(symbolic_to_address)))
log.debug("CALL to: " + str(simplify(symbolic_to_address)))
if match is None or dynamic_loader is None:
raise ValueError()
index = int(match.group(1))
logging.debug("Dynamic contract address at storage index {}".format(index))
log.debug("Dynamic contract address at storage index {}".format(index))
# attempt to read the contract address from instance storage
try:
callee_address = dynamic_loader.read_storage(environment.active_account.address, index)
callee_address = dynamic_loader.read_storage(
environment.active_account.address, index
)
# TODO: verify whether this happens or not
except:
logging.debug("Error accessing contract storage.")
log.debug("Error accessing contract storage.")
raise ValueError
# testrpc simply returns the address, geth response is more elaborate.
@ -76,9 +111,11 @@ def get_callee_address(global_state: GlobalState, dynamic_loader: DynLoader, sym
return callee_address
def get_callee_account(global_state: GlobalState, callee_address: str, dynamic_loader: DynLoader):
"""
Gets the callees account from the global_state
def get_callee_account(
global_state: GlobalState, callee_address: str, dynamic_loader: DynLoader
):
"""Gets the callees account from the global_state.
:param global_state: state to look in
:param callee_address: address of the callee
:param dynamic_loader: dynamic loader to use
@ -91,24 +128,26 @@ def get_callee_account(global_state: GlobalState, callee_address: str, dynamic_l
return global_state.accounts[callee_address]
except KeyError:
# We have a valid call address, but contract is not in the modules list
logging.debug("Module with address " + callee_address + " not loaded.")
log.debug("Module with address " + callee_address + " not loaded.")
if dynamic_loader is None:
raise ValueError()
logging.debug("Attempting to load dependency")
log.debug("Attempting to load dependency")
try:
code = dynamic_loader.dynld(environment.active_account.address, callee_address)
except Exception as e:
logging.debug("Unable to execute dynamic loader.")
raise ValueError()
except ValueError as error:
log.debug("Unable to execute dynamic loader because: {}".format(str(error)))
raise error
if code is None:
logging.debug("No code returned, not a contract account?")
log.debug("No code returned, not a contract account?")
raise ValueError()
logging.debug("Dependency loaded: " + callee_address)
log.debug("Dependency loaded: " + callee_address)
callee_account = Account(callee_address, code, callee_address, dynamic_loader=dynamic_loader)
callee_account = Account(
callee_address, code, callee_address, dynamic_loader=dynamic_loader
)
accounts[callee_address] = callee_account
return callee_account
@ -116,30 +155,88 @@ def get_callee_account(global_state: GlobalState, callee_address: str, dynamic_l
def get_call_data(
global_state: GlobalState,
memory_start: Union[int, BitVecNumRef, BoolRef],
memory_size: Union[int, BitVecNumRef, BoolRef],
pad=True
memory_start: Union[int, Expression],
memory_size: Union[int, Expression],
):
"""
Gets call_data from the global_state
"""Gets call_data from the global_state.
:param global_state: state to look in
:param memory_start: Start index
:param memory_size: Size
:param pad: Enable zero padding before the call data
:return: Tuple containing: call_data array from memory or empty array if symbolic, type found
"""
state = global_state.mstate
transaction_id = "{}_internalcall".format(global_state.current_transaction.id)
memory_start = (
symbol_factory.BitVecVal(memory_start, 256)
if isinstance(memory_start, int)
else memory_start
)
memory_size = (
symbol_factory.BitVecVal(memory_size, 256)
if isinstance(memory_size, int)
else memory_size
)
uses_entire_calldata = simplify(
memory_size - global_state.environment.calldata.calldatasize == 0
)
if uses_entire_calldata == True:
return global_state.environment.calldata
try:
# TODO: This only allows for either fully concrete or fully symbolic calldata.
# Improve management of memory and callata to support a mix between both types.
call_data = state.memory[util.get_concrete_int(memory_start):util.get_concrete_int(memory_start + memory_size)]
if len(call_data) < 32 and pad:
call_data += [0] * (32 - len(call_data))
call_data_type = CalldataType.CONCRETE
logging.debug("Calldata: " + str(call_data))
calldata_from_mem = state.memory[
util.get_concrete_int(memory_start) : util.get_concrete_int(
memory_start + memory_size
)
]
return ConcreteCalldata(transaction_id, calldata_from_mem)
except TypeError:
logging.info("Unsupported symbolic calldata offset")
call_data_type = CalldataType.SYMBOLIC
call_data = []
log.debug("Unsupported symbolic calldata offset")
return SymbolicCalldata(transaction_id)
return call_data, call_data_type
def native_call(
global_state: GlobalState,
callee_address: str,
call_data: BaseCalldata,
memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression],
) -> Union[List[GlobalState], None]:
if not 0 < int(callee_address, 16) < 5:
return None
log.debug("Native contract called: " + callee_address)
try:
mem_out_start = util.get_concrete_int(memory_out_offset)
mem_out_sz = util.get_concrete_int(memory_out_size)
except TypeError:
log.debug("CALL with symbolic start or offset not supported")
return [global_state]
contract_list = ["ecrecover", "sha256", "ripemd160", "identity"]
call_address_int = int(callee_address, 16)
native_gas_min, native_gas_max = OPCODE_GAS["NATIVE_COST"](
global_state.mstate.calculate_extension_size(mem_out_start, mem_out_sz),
contract_list[call_address_int - 1],
)
global_state.mstate.min_gas_used += native_gas_min
global_state.mstate.max_gas_used += native_gas_max
global_state.mstate.mem_extend(mem_out_start, mem_out_sz)
try:
data = natives.native_contracts(call_address_int, call_data)
except natives.NativeContractException:
for i in range(mem_out_sz):
global_state.mstate.memory[mem_out_start + i] = global_state.new_bitvec(
contract_list[call_address_int - 1] + "(" + str(call_data) + ")", 8
)
return [global_state]
for i in range(
min(len(data), mem_out_sz)
): # If more data is used then it's chopped off
global_state.mstate.memory[mem_out_start + i] = data[i]
# TODO: maybe use BitVec here constrained to 1
return [global_state]

@ -1,11 +1,15 @@
from flags import Flags
"""This module."""
from enum import Enum
from typing import Dict
from flags import Flags
gbl_next_uid = 0 # node counter
class JumpType(Enum):
"""An enum to represent the types of possible JUMP scenarios."""
CONDITIONAL = 1
UNCONDITIONAL = 2
CALL = 3
@ -14,18 +18,34 @@ class JumpType(Enum):
class NodeFlags(Flags):
"""A collection of flags to denote the type a call graph node can have."""
FUNC_ENTRY = 1
CALL_RETURN = 2
class Node:
def __init__(self, contract_name: str, start_addr=0, constraints=None):
"""The representation of a call graph node."""
def __init__(
self,
contract_name: str,
start_addr=0,
constraints=None,
function_name="unknown",
):
"""
:param contract_name:
:param start_addr:
:param constraints:
"""
constraints = constraints if constraints else []
self.contract_name = contract_name
self.start_addr = start_addr
self.states = []
self.constraints = constraints
self.function_name = "unknown"
self.function_name = function_name
self.flags = NodeFlags()
# Self-assign a unique ID
@ -35,32 +55,61 @@ class Node:
gbl_next_uid += 1
def get_cfg_dict(self) -> Dict:
"""
:return:
"""
code = ""
for state in self.states:
instruction = state.get_current_instruction()
code += str(instruction['address']) + " " + instruction['opcode']
if instruction['opcode'].startswith("PUSH"):
code += " " + instruction['argument']
code += str(instruction["address"]) + " " + instruction["opcode"]
if instruction["opcode"].startswith("PUSH"):
code += " " + instruction["argument"]
code += "\\n"
return dict(
contract_name=self.contract_name,
start_addr=self.start_addr,
function_name=self.function_name,
code=code
code=code,
)
class Edge:
def __init__(self, node_from: int, node_to: int, edge_type=JumpType.UNCONDITIONAL, condition=None):
"""The respresentation of a call graph edge."""
def __init__(
self,
node_from: int,
node_to: int,
edge_type=JumpType.UNCONDITIONAL,
condition=None,
):
"""
:param node_from:
:param node_to:
:param edge_type:
:param condition:
"""
self.node_from = node_from
self.node_to = node_to
self.type = edge_type
self.condition = condition
def __str__(self) -> str:
"""
:return:
"""
return str(self.as_dict)
@property
def as_dict(self) -> Dict[str, int]:
return {"from": self.node_from, 'to': self.node_to}
"""
:return:
"""
return {"from": self.node_from, "to": self.node_to}

@ -1,18 +1,37 @@
"""This module contains EVM exception types used by LASER."""
class VmException(Exception):
"""The base VM exception type."""
pass
class StackUnderflowException(IndexError, VmException):
"""A VM exception regarding stack underflows."""
pass
class StackOverflowException(VmException):
"""A VM exception regarding stack overflows."""
pass
class InvalidJumpDestination(VmException):
"""A VM exception regarding JUMPs to invalid destinations."""
pass
class InvalidInstruction(VmException):
"""A VM exception denoting an invalid op code has been encountered."""
pass
class OutOfGasException(VmException):
"""A VM exception denoting the current execution has run out of gas."""
pass

@ -0,0 +1,188 @@
"""This module contains functions for dynamic gas calculation and a gas cost
table."""
from ethereum import opcodes
from ethereum.utils import ceil32
def calculate_native_gas(size: int, contract: str):
"""
:param size:
:param contract:
:return:
"""
gas_value = None
word_num = ceil32(size) // 32
if contract == "ecrecover":
gas_value = opcodes.GECRECOVER
elif contract == "sha256":
gas_value = opcodes.GSHA256BASE + word_num * opcodes.GSHA256WORD
elif contract == "ripemd160":
gas_value = opcodes.GRIPEMD160BASE + word_num * opcodes.GRIPEMD160WORD
elif contract == "identity":
gas_value = opcodes.GIDENTITYBASE + word_num * opcodes.GIDENTITYWORD
else:
raise ValueError("Unknown contract type {}".format(contract))
return gas_value, gas_value
def calculate_sha3_gas(length: int):
"""
:param length:
:return:
"""
gas_val = 30 + opcodes.GSHA3WORD * (ceil32(length) // 32)
return gas_val, gas_val
# opcode -> (min_gas, max_gas)
OPCODE_GAS = {
"STOP": (0, 0),
"ADD": (3, 3),
"MUL": (5, 5),
"SUB": (3, 3),
"DIV": (5, 5),
"SDIV": (5, 5),
"MOD": (5, 5),
"SMOD": (5, 5),
"ADDMOD": (8, 8),
"MULMOD": (8, 8),
"EXP": (10, 340), # exponent max 2^32
"SIGNEXTEND": (5, 5),
"LT": (3, 3),
"GT": (3, 3),
"SLT": (3, 3),
"SGT": (3, 3),
"EQ": (3, 3),
"ISZERO": (3, 3),
"AND": (3, 3),
"OR": (3, 3),
"XOR": (3, 3),
"NOT": (3, 3),
"BYTE": (3, 3),
"SHA3": (
30,
30 + 6 * 8,
), # max can be larger, but usually storage location with 8 words
"SHA3_FUNC": calculate_sha3_gas,
"ADDRESS": (2, 2),
"BALANCE": (400, 400),
"ORIGIN": (2, 2),
"CALLER": (2, 2),
"CALLVALUE": (2, 2),
"CALLDATALOAD": (3, 3),
"CALLDATASIZE": (2, 2),
"CALLDATACOPY": (2, 2 + 3 * 768), # https://ethereum.stackexchange.com/a/47556
"CODESIZE": (2, 2),
"CODECOPY": (2, 2 + 3 * 768), # https://ethereum.stackexchange.com/a/47556,
"GASPRICE": (2, 2),
"EXTCODESIZE": (700, 700),
"EXTCODECOPY": (700, 700 + 3 * 768), # https://ethereum.stackexchange.com/a/47556
"RETURNDATASIZE": (2, 2),
"RETURNDATACOPY": (3, 3),
"BLOCKHASH": (20, 20),
"COINBASE": (2, 2),
"TIMESTAMP": (2, 2),
"NUMBER": (2, 2),
"DIFFICULTY": (2, 2),
"GASLIMIT": (2, 2),
"POP": (2, 2),
# assume 1KB memory r/w cost as upper bound
"MLOAD": (3, 96),
"MSTORE": (3, 98),
"MSTORE8": (3, 98),
# assume 64 byte r/w cost as upper bound
"SLOAD": (400, 400),
"SSTORE": (5000, 25000),
"JUMP": (8, 8),
"JUMPI": (10, 10),
"PC": (2, 2),
"MSIZE": (2, 2),
"GAS": (2, 2),
"JUMPDEST": (1, 1),
"PUSH1": (3, 3),
"PUSH2": (3, 3),
"PUSH3": (3, 3),
"PUSH4": (3, 3),
"PUSH5": (3, 3),
"PUSH6": (3, 3),
"PUSH7": (3, 3),
"PUSH8": (3, 3),
"PUSH9": (3, 3),
"PUSH10": (3, 3),
"PUSH11": (3, 3),
"PUSH12": (3, 3),
"PUSH13": (3, 3),
"PUSH14": (3, 3),
"PUSH15": (3, 3),
"PUSH16": (3, 3),
"PUSH17": (3, 3),
"PUSH18": (3, 3),
"PUSH19": (3, 3),
"PUSH20": (3, 3),
"PUSH21": (3, 3),
"PUSH22": (3, 3),
"PUSH23": (3, 3),
"PUSH24": (3, 3),
"PUSH25": (3, 3),
"PUSH26": (3, 3),
"PUSH27": (3, 3),
"PUSH28": (3, 3),
"PUSH29": (3, 3),
"PUSH30": (3, 3),
"PUSH31": (3, 3),
"PUSH32": (3, 3),
"DUP1": (3, 3),
"DUP2": (3, 3),
"DUP3": (3, 3),
"DUP4": (3, 3),
"DUP5": (3, 3),
"DUP6": (3, 3),
"DUP7": (3, 3),
"DUP8": (3, 3),
"DUP9": (3, 3),
"DUP10": (3, 3),
"DUP11": (3, 3),
"DUP12": (3, 3),
"DUP13": (3, 3),
"DUP14": (3, 3),
"DUP15": (3, 3),
"DUP16": (3, 3),
"SWAP1": (3, 3),
"SWAP2": (3, 3),
"SWAP3": (3, 3),
"SWAP4": (3, 3),
"SWAP5": (3, 3),
"SWAP6": (3, 3),
"SWAP7": (3, 3),
"SWAP8": (3, 3),
"SWAP9": (3, 3),
"SWAP10": (3, 3),
"SWAP11": (3, 3),
"SWAP12": (3, 3),
"SWAP13": (3, 3),
"SWAP14": (3, 3),
"SWAP15": (3, 3),
"SWAP16": (3, 3),
# apparently Solidity only allows byte32 as input to the log
# function. Virtually it could be as large as the block gas limit
# allows, but let's stick to the reasonable standard here.
# https://ethereum.stackexchange.com/a/1691
"LOG0": (375, 375 + 8 * 32),
"LOG1": (2 * 375, 2 * 375 + 8 * 32),
"LOG2": (3 * 375, 3 * 375 + 8 * 32),
"LOG3": (4 * 375, 4 * 375 + 8 * 32),
"LOG4": (5 * 375, 5 * 375 + 8 * 32),
"CREATE": (32000, 32000),
"CALL": (700, 700 + 9000 + 25000),
"NATIVE_COST": calculate_native_gas,
"CALLCODE": (700, 700 + 9000 + 25000),
"RETURN": (0, 0),
"DELEGATECALL": (700, 700 + 9000 + 25000),
"STATICCALL": (700, 700 + 9000 + 25000),
"REVERT": (0, 0),
"SUICIDE": (5000, 30000),
"ASSERT_FAIL": (0, 0),
"INVALID": (0, 0),
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,79 @@
from collections import namedtuple
from datetime import datetime
from typing import Dict, List, Tuple
# Type annotations:
# start_time: datetime
# end_time: datetime
_InstrExecRecord = namedtuple("_InstrExecRecord", ["start_time", "end_time"])
# Type annotations:
# total_time: float
# total_nr: float
# min_time: float
# max_time: float
_InstrExecStatistic = namedtuple(
"_InstrExecStatistic", ["total_time", "total_nr", "min_time", "max_time"]
)
# Map the instruction opcode to its records if all execution times
_InstrExecRecords = Dict[str, List[_InstrExecRecord]]
# Map the instruction opcode to the statistic of its execution times
_InstrExecStatistics = Dict[str, _InstrExecStatistic]
class InstructionProfiler:
"""Performance profile for the execution of each instruction.
"""
def __init__(self):
self.records = dict()
def record(self, op: int, start_time: datetime, end_time: datetime):
try:
self.records[op].append(_InstrExecRecord(start_time, end_time))
except KeyError:
self.records[op] = [_InstrExecRecord(start_time, end_time)]
def _make_stats(self) -> Tuple[float, _InstrExecStatistics]:
periods = {
op: list(
map(lambda r: r.end_time.timestamp() - r.start_time.timestamp(), rs)
)
for op, rs in self.records.items()
}
stats = dict()
total_time = 0
for _, (op, times) in enumerate(periods.items()):
stat = _InstrExecStatistic(
total_time=sum(times),
total_nr=len(times),
min_time=min(times),
max_time=max(times),
)
total_time += stat.total_time
stats[op] = stat
return total_time, stats
def __str__(self):
total, stats = self._make_stats()
s = "Total: {} s\n".format(total)
for op in sorted(stats):
stat = stats[op]
s += "[{:12s}] {:>8.4f} %, nr {:>6}, total {:>8.4f} s, avg {:>8.4f} s, min {:>8.4f} s, max {:>8.4f} s\n".format(
op,
stat.total_time * 100 / total,
stat.total_nr,
stat.total_time,
stat.total_time / stat.total_nr,
stat.min_time,
stat.max_time,
)
return s

@ -1,18 +1,38 @@
from z3 import ExprRef, BitVecRef
"""This module contains a function manager to deal with symbolic Keccak
values."""
from mythril.laser.smt import Expression
class KeccakFunctionManager:
"""A keccak function manager for symbolic expressions."""
def __init__(self):
""""""
self.keccak_expression_mapping = {}
def is_keccak(self, expression: BitVecRef) -> bool:
def is_keccak(self, expression: Expression) -> bool:
"""
:param expression:
:return:
"""
return str(expression) in self.keccak_expression_mapping.keys()
def get_argument(self, expression: str) -> ExprRef:
def get_argument(self, expression: str) -> Expression:
"""
:param expression:
:return:
"""
if not self.is_keccak(expression):
raise ValueError("Expression is not a recognized keccac result")
return self.keccak_expression_mapping[str(expression)][1]
def add_keccak(self, expression: ExprRef, argument: ExprRef) -> None:
def add_keccak(self, expression: Expression, argument: Expression) -> None:
"""
:param expression:
:param argument:
"""
index = str(expression)
self.keccak_expression_mapping[index] = (expression, argument)

@ -1,6 +1,5 @@
# -*- coding: utf8 -*-
"""This nodule defines helper functions to deal with native calls."""
import copy
import hashlib
import logging
from typing import List, Union
@ -9,30 +8,54 @@ from ethereum.utils import ecrecover_to_pub
from py_ecc.secp256k1 import N as secp256k1n
from rlp.utils import ALL_BYTES
from mythril.laser.ethereum.state.calldata import BaseCalldata, ConcreteCalldata
from mythril.laser.ethereum.util import bytearray_to_int, sha3
from mythril.laser.smt import Concat, simplify
log = logging.getLogger(__name__)
class NativeContractException(Exception):
"""An exception denoting an error during a native call."""
pass
def int_to_32bytes(i: int) -> bytes: # used because int can't fit as bytes function's input
def int_to_32bytes(
i: int
) -> bytes: # used because int can't fit as bytes function's input
"""
:param i:
:return:
"""
o = [0] * 32
for x in range(32):
o[31 - x] = i & 0xff
o[31 - x] = i & 0xFF
i >>= 8
return bytes(o)
def extract32(data: bytearray, i: int) -> int:
"""
:param data:
:param i:
:return:
"""
if i >= len(data):
return 0
o = data[i: min(i + 32, len(data))]
o = data[i : min(i + 32, len(data))]
o.extend(bytearray(32 - len(o)))
return bytearray_to_int(o)
def ecrecover(data: str) -> List:
def ecrecover(data: Union[bytes, str, List[int]]) -> bytes:
"""
:param data:
:return:
"""
# TODO: Add type hints
try:
data = bytearray(data)
@ -42,19 +65,24 @@ def ecrecover(data: str) -> List:
except TypeError:
raise NativeContractException
message = b''.join([ALL_BYTES[x] for x in data[0:32]])
message = b"".join([ALL_BYTES[x] for x in data[0:32]])
if r >= secp256k1n or s >= secp256k1n or v < 27 or v > 28:
return []
try:
pub = ecrecover_to_pub(message, v, r, s)
except Exception as e:
logging.info("An error has occured while extracting public key: "+e)
log.debug("An error has occured while extracting public key: " + e)
return []
o = [0] * 12 + [x for x in sha3(pub)[-20:]]
return o
def sha256(data: Union[bytes, str]) -> bytes:
def sha256(data: Union[bytes, str, List[int]]) -> bytes:
"""
:param data:
:return:
"""
try:
data = bytes(data)
except TypeError:
@ -62,27 +90,51 @@ def sha256(data: Union[bytes, str]) -> bytes:
return hashlib.sha256(data).digest()
def ripemd160(data: Union[bytes, str]) -> bytes:
def ripemd160(data: Union[bytes, str, List[int]]) -> bytes:
"""
:param data:
:return:
"""
try:
data = bytes(data)
except TypeError:
raise NativeContractException
digest = hashlib.new('ripemd160', data).digest()
digest = hashlib.new("ripemd160", data).digest()
padded = 12 * [0] + list(digest)
return bytes(padded)
def identity(data: Union[bytes, str]) -> bytes:
try:
data = bytes(data)
except TypeError:
raise NativeContractException
return copy.copy(data)
def identity(data: Union[bytes, str, List[int]]) -> bytes:
"""
def native_contracts(address: int, data: List):
:param data:
:return:
"""
takes integer address 1, 2, 3, 4
# Group up into an array of 32 byte words instead
# of an array of bytes. If saved to memory, 32 byte
# words are currently needed, but a correct memory
# implementation would be byte indexed for the most
# part.
return data
result = []
for i in range(0, len(data), 32):
result.append(simplify(Concat(data[i : i + 32])))
return result
def native_contracts(address: int, data: BaseCalldata):
"""Takes integer address 1, 2, 3, 4.
:param address:
:param data:
:return:
"""
functions = (ecrecover, sha256, ripemd160, identity)
return functions[address-1](data)
if isinstance(data, ConcreteCalldata):
data = data.concrete(None)
else:
raise NativeContractException()
return functions[address - 1](data)

@ -1,355 +0,0 @@
from z3 import BitVec, BitVecVal, BitVecRef, BitVecNumRef, Solver, ExprRef, sat
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.cfg import Node
from copy import copy, deepcopy
from enum import Enum
from random import randint
from typing import KeysView, Dict, List, Union, Any
from mythril.laser.ethereum.evm_exceptions import StackOverflowException, StackUnderflowException
class CalldataType(Enum):
CONCRETE = 1
SYMBOLIC = 2
class Storage:
"""
Storage class represents the storage of an Account
"""
def __init__(self, concrete=False, address=None, dynamic_loader=None):
"""
Constructor for Storage
:param concrete: bool indicating whether to interpret uninitialized storage as concrete versus symbolic
"""
self._storage = {}
self.concrete = concrete
self.dynld = dynamic_loader
self.address = address
def __getitem__(self, item: int) -> int:
try:
return self._storage[item]
except KeyError:
if self.address and int(self.address[2:], 16) != 0 and self.dynld:
try:
self._storage[item] = int(self.dynld.read_storage(contract_address=self.address, index=int(item)), 16)
return self._storage[item]
except ValueError:
pass
if self.concrete:
return 0
self._storage[item] = BitVec("storage_" + str(item), 256)
return self._storage[item]
def __setitem__(self, key: str, value: BitVecRef) -> None:
self._storage[key] = value
def keys(self) -> KeysView:
return self._storage.keys()
class Account:
"""
Account class representing ethereum accounts
"""
def __init__(self, address: str, code=None, contract_name="unknown", balance=None, concrete_storage=False,
dynamic_loader=None):
"""
Constructor for account
:param address: Address of the account
:param code: The contract code of the account
:param contract_name: The name associated with the account
:param balance: The balance for the account
:param concrete_storage: Interpret storage as concrete
"""
self.nonce = 0
self.code = code or Disassembly("")
self.balance = balance if balance else BitVec("balance", 256)
self.storage = Storage(concrete_storage, address=address, dynamic_loader=dynamic_loader)
# Metadata
self.address = address
self.contract_name = contract_name
self.deleted = False
def __str__(self) -> str:
return str(self.as_dict)
def set_balance(self, balance: BitVecRef) -> None:
self.balance = balance
def add_balance(self, balance: BitVecRef) -> None:
self.balance += balance
@property
def as_dict(self) -> Dict:
return {'nonce': self.nonce, 'code': self.code, 'balance': self.balance, 'storage': self.storage}
class Environment:
"""
The environment class represents the current execution environment for the symbolic executor
"""
def __init__(
self,
active_account: Account,
sender: BitVecRef,
calldata: List,
gasprice: BitVecRef,
callvalue: BitVecRef,
origin: BitVecRef,
code=None,
calldata_type=CalldataType.SYMBOLIC,
):
# Metadata
self.active_account = active_account
self.active_function_name = ""
self.address = BitVecVal(int(active_account.address, 16), 256)
# Ib
self.code = active_account.code if code is None else code
self.sender = sender
self.calldata = calldata
self.calldata_type = calldata_type
self.gasprice = gasprice
self.origin = origin
self.callvalue = callvalue
def __str__(self) -> str:
return str(self.as_dict)
@property
def as_dict(self) -> Dict:
return dict(active_account=self.active_account, sender=self.sender, calldata=self.calldata,
gasprice=self.gasprice, callvalue=self.callvalue, origin=self.origin,
calldata_type=self.calldata_type)
class MachineStack(list):
"""
Defines EVM stack, overrides the default list to handle overflows
"""
STACK_LIMIT = 1024
def __init__(self, default_list=None):
if default_list is None:
default_list = []
super(MachineStack, self).__init__(default_list)
def append(self, element: BitVecNumRef) -> None:
"""
:param element: element to be appended to the list
:function: appends the element to list if the size is less than STACK_LIMIT, else throws an error
"""
if super(MachineStack, self).__len__() >= self.STACK_LIMIT:
raise StackOverflowException("Reached the EVM stack limit of {}, you can't append more "
"elements".format(self.STACK_LIMIT))
super(MachineStack, self).append(element)
def pop(self, index=-1) -> BitVecNumRef:
"""
:param index:index to be popped, same as the list() class.
:returns popped value
:function: same as list() class but throws StackUnderflowException for popping from an empty list
"""
try:
return super(MachineStack, self).pop(index)
except IndexError:
raise StackUnderflowException("Trying to pop from an empty stack")
def __getitem__(self, item: int) -> Any:
try:
return super(MachineStack, self).__getitem__(item)
except IndexError:
raise StackUnderflowException("Trying to access a stack element which doesn't exist")
def __add__(self, other):
"""
Implement list concatenation if needed
"""
raise NotImplementedError('Implement this if needed')
def __iadd__(self, other):
"""
Implement list concatenation if needed
"""
raise NotImplementedError('Implement this if needed')
class MachineState:
"""
MachineState represents current machine state also referenced to as \mu
"""
def __init__(self, gas: int):
""" Constructor for machineState """
self.pc = 0
self.stack = MachineStack()
self.memory = []
self.gas = gas
self.constraints = []
self.depth = 0
def mem_extend(self, start: int, size: int) -> None:
"""
Extends the memory of this machine state
:param start: Start of memory extension
:param size: Size of memory extension
"""
if self.memory_size > start + size:
return
m_extend = (start + size - self.memory_size)
self.memory.extend(bytearray(m_extend))
def memory_write(self, offset: int, data: List[int]) -> None:
""" Writes data to memory starting at offset """
self.mem_extend(offset, len(data))
self.memory[offset:offset+len(data)] = data
def pop(self, amount=1) -> Union[BitVecRef, List[BitVecRef]]:
""" Pops amount elements from the stack"""
if amount >= len(self.stack):
raise StackUnderflowException
values = self.stack[-amount:][::-1]
del self.stack[-amount:]
return values[0] if amount == 1 else values
def __str__(self) -> str:
return str(self.as_dict)
@property
def memory_size(self) -> int:
return len(self.memory)
@property
def as_dict(self) -> Dict:
return dict(pc=self.pc, stack=self.stack, memory=self.memory, memsize=self.memory_size, gas=self.gas)
class GlobalState:
"""
GlobalState represents the current globalstate
"""
def __init__(
self,
world_state: "WorldState",
environment: Environment,
node: Node,
machine_state=None,
transaction_stack=None,
last_return_data=None
):
""" Constructor for GlobalState"""
self.node = node
self.world_state = world_state
self.environment = environment
self.mstate = machine_state if machine_state else MachineState(gas=10000000)
self.transaction_stack = transaction_stack if transaction_stack else []
self.op_code = ""
self.last_return_data = last_return_data
def __copy__(self) -> "GlobalState":
world_state = copy(self.world_state)
environment = copy(self.environment)
mstate = deepcopy(self.mstate)
transaction_stack = copy(self.transaction_stack)
return GlobalState(world_state, environment, self.node, mstate, transaction_stack=transaction_stack,
last_return_data=self.last_return_data)
@property
def accounts(self) -> Dict:
return self.world_state.accounts
# TODO: remove this, as two instructions are confusing
def get_current_instruction(self) -> Dict:
""" Gets the current instruction for this GlobalState"""
instructions = self.environment.code.instruction_list
return instructions[self.mstate.pc]
@property
def current_transaction(self) -> Union["MessageCallTransaction", "ContractCreationTransaction", None]:
# TODO: Remove circular to transaction package to import Transaction classes
try:
return self.transaction_stack[-1][0]
except IndexError:
return None
@property
def instruction(self) -> Dict:
return self.get_current_instruction()
def new_bitvec(self, name: str, size=256) -> BitVec:
transaction_id = self.current_transaction.id
node_id = self.node.uid
return BitVec("{}_{}".format(transaction_id, name), size)
class WorldState:
"""
The WorldState class represents the world state as described in the yellow paper
"""
def __init__(self, transaction_sequence=None):
"""
Constructor for the world state. Initializes the accounts record
"""
self.accounts = {}
self.node = None
self.transaction_sequence = transaction_sequence or []
def __getitem__(self, item: str) -> Account:
"""
Gets an account from the worldstate using item as key
:param item: Address of the account to get
:return: Account associated with the address
"""
return self.accounts[item]
def __copy__(self) -> "WorldState":
new_world_state = WorldState(transaction_sequence=self.transaction_sequence[:])
new_world_state.accounts = copy(self.accounts)
new_world_state.node = self.node
return new_world_state
def create_account(self, balance=0, address=None, concrete_storage=False, dynamic_loader=None) -> Account:
"""
Create non-contract account
:param address: The account's address
:param balance: Initial balance for the account
:param concrete_storage: Interpret account storage as concrete
:param dynamic_loader: used for dynamically loading storage from the block chain
:return: The new account
"""
address = address if address else self._generate_new_address()
new_account = Account(address, balance=balance, dynamic_loader=dynamic_loader, concrete_storage=concrete_storage)
self._put_account(new_account)
return new_account
def create_initialized_contract_account(self, contract_code, storage) -> None:
"""
Creates a new contract account, based on the contract code and storage provided
The contract code only includes the runtime contract bytecode
:param contract_code: Runtime bytecode for the contract
:param storage: Initial storage for the contract
:return: The new account
"""
# TODO: Add type hints
new_account = Account(self._generate_new_address(), code=contract_code, balance=0)
new_account.storage = storage
self._put_account(new_account)
def _generate_new_address(self) -> str:
""" Generates a new address for the global state"""
while True:
address = '0x' + ''.join([str(hex(randint(0, 16)))[-1] for _ in range(20)])
if address not in self.accounts.keys():
return address
def _put_account(self, account: Account) -> None:
self.accounts[account.address] = account

@ -0,0 +1,130 @@
"""This module contains account-related functionality.
This includes classes representing accounts and their storage.
"""
from typing import Any, Dict, KeysView, Union
from z3 import ExprRef
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.smt import symbol_factory
class Storage:
"""Storage class represents the storage of an Account."""
def __init__(self, concrete=False, address=None, dynamic_loader=None):
"""Constructor for Storage.
:param concrete: bool indicating whether to interpret uninitialized storage as concrete versus symbolic
"""
self._storage = {}
self.concrete = concrete
self.dynld = dynamic_loader
self.address = address
def __getitem__(self, item: Union[int, slice]) -> Any:
try:
return self._storage[item]
except KeyError:
if (
self.address
and int(self.address[2:], 16) != 0
and (self.dynld and self.dynld.storage_loading)
):
try:
self._storage[item] = symbol_factory.BitVecVal(
int(
self.dynld.read_storage(
contract_address=self.address, index=int(item)
),
16,
),
256,
)
return self._storage[item]
except ValueError:
pass
if self.concrete:
return symbol_factory.BitVecVal(0, 256)
self._storage[item] = symbol_factory.BitVecVal(0, 256)
return self._storage[item]
def __setitem__(self, key: str, value: ExprRef) -> None:
self._storage[key] = value
def keys(self) -> KeysView:
"""
:return:
"""
return self._storage.keys()
class Account:
"""Account class representing ethereum accounts."""
def __init__(
self,
address: str,
code=None,
contract_name="unknown",
balance=None,
concrete_storage=False,
dynamic_loader=None,
):
"""Constructor for account.
:param address: Address of the account
:param code: The contract code of the account
:param contract_name: The name associated with the account
:param balance: The balance for the account
:param concrete_storage: Interpret storage as concrete
"""
self.nonce = 0
self.code = code or Disassembly("")
self.balance = (
balance
if balance
else symbol_factory.BitVecSym("{}_balance".format(address), 256)
)
self.storage = Storage(
concrete_storage, address=address, dynamic_loader=dynamic_loader
)
# Metadata
self.address = address
self.contract_name = contract_name
self.deleted = False
def __str__(self) -> str:
return str(self.as_dict)
def set_balance(self, balance: ExprRef) -> None:
"""
:param balance:
"""
self.balance = balance
def add_balance(self, balance: ExprRef) -> None:
"""
:param balance:
"""
self.balance += balance
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return {
"nonce": self.nonce,
"code": self.code,
"balance": self.balance,
"storage": self.storage,
}

@ -0,0 +1,40 @@
"""This module includes classes used for annotating trace information.
This includes the base StateAnnotation class, as well as an adaption,
which will not be copied on every new state.
"""
class StateAnnotation:
"""The StateAnnotation class is used to persist information over traces.
This allows modules to reason about traces without the need to
traverse the state space themselves.
"""
@property
def persist_to_world_state(self) -> bool:
"""If this function returns true then laser will also annotate the
world state.
If you want annotations to persist through different user initiated message call transactions
then this should be enabled.
The default is set to False
"""
return False
class NoCopyAnnotation(StateAnnotation):
"""This class provides a base annotation class for annotations that
shouldn't be copied on every new state.
Rather the same object should be propagated. This is very useful if
you are looking to analyze a property over multiple substates
"""
def __copy__(self):
return self
def __deepcopy__(self, _):
return self

@ -0,0 +1,307 @@
"""This module declares classes to represent call data."""
from typing import Union, Any
from mythril.laser.smt import K, Array, If, simplify, Concat, Expression, BitVec
from enum import Enum
from typing import Any, Union
from z3 import Model
from z3.z3types import Z3Exception
from mythril.laser.ethereum.util import get_concrete_int
from mythril.laser.smt import (
Array,
BitVec,
Concat,
Expression,
If,
K,
simplify,
symbol_factory,
)
class BaseCalldata:
"""Base calldata class This represents the calldata provided when sending a
transaction to a contract."""
def __init__(self, tx_id):
"""
:param tx_id:
"""
self.tx_id = tx_id
@property
def calldatasize(self) -> Expression:
"""
:return: Calldata size for this calldata object
"""
result = self.size
if isinstance(result, int):
return symbol_factory.BitVecVal(result, 256)
return result
def get_word_at(self, offset: int) -> Expression:
"""Gets word at offset.
:param offset:
:return:
"""
parts = self[offset : offset + 32]
return simplify(Concat(parts))
def __getitem__(self, item: Union[int, slice]) -> Any:
"""
:param item:
:return:
"""
if isinstance(item, int) or isinstance(item, Expression):
return self._load(item)
if isinstance(item, slice):
start = 0 if item.start is None else item.start
step = 1 if item.step is None else item.step
stop = self.size if item.stop is None else item.stop
try:
current_index = (
start
if isinstance(start, Expression)
else symbol_factory.BitVecVal(start, 256)
)
parts = []
while simplify(current_index != stop):
element = self._load(current_index)
if not isinstance(element, Expression):
element = symbol_factory.BitVecVal(element, 8)
parts.append(element)
current_index = simplify(current_index + step)
except Z3Exception:
raise IndexError("Invalid Calldata Slice")
return parts
raise ValueError
def _load(self, item: Union[int, Expression]) -> Any:
"""
:param item:
"""
raise NotImplementedError()
@property
def size(self) -> Union[Expression, int]:
"""Returns the exact size of this calldata, this is not normalized.
:return: unnormalized call data size
"""
raise NotImplementedError()
def concrete(self, model: Model) -> list:
"""Returns a concrete version of the calldata using the provided model.
:param model:
"""
raise NotImplementedError
class ConcreteCalldata(BaseCalldata):
"""A concrete call data representation."""
def __init__(self, tx_id: int, calldata: list):
"""Initializes the ConcreteCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
:param calldata: The concrete calldata content
"""
self._concrete_calldata = calldata
self._calldata = K(256, 8, 0)
for i, element in enumerate(calldata, 0):
element = (
symbol_factory.BitVecVal(element, 8)
if isinstance(element, int)
else element
)
self._calldata[symbol_factory.BitVecVal(i, 256)] = element
super().__init__(tx_id)
def _load(self, item: Union[int, Expression]) -> BitVec:
"""
:param item:
:return:
"""
item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
return simplify(self._calldata[item])
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
return self._concrete_calldata
@property
def size(self) -> int:
"""
:return:
"""
return len(self._concrete_calldata)
class BasicConcreteCalldata(BaseCalldata):
"""A base class to represent concrete call data."""
def __init__(self, tx_id: int, calldata: list):
"""Initializes the ConcreteCalldata object, that doesn't use z3 arrays.
:param tx_id: Id of the transaction that the calldata is for.
:param calldata: The concrete calldata content
"""
self._calldata = calldata
super().__init__(tx_id)
def _load(self, item: Union[int, Expression]) -> Any:
"""
:param item:
:return:
"""
if isinstance(item, int):
try:
return self._calldata[item]
except IndexError:
return 0
value = symbol_factory.BitVecVal(0x0, 8)
for i in range(self.size):
value = If(item == i, self._calldata[i], value)
return value
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
return self._calldata
@property
def size(self) -> int:
"""
:return:
"""
return len(self._calldata)
class SymbolicCalldata(BaseCalldata):
"""A class for representing symbolic call data."""
def __init__(self, tx_id: int):
"""Initializes the SymbolicCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
"""
self._size = symbol_factory.BitVecSym(str(tx_id) + "_calldatasize", 256)
self._calldata = Array("{}_calldata".format(tx_id), 256, 8)
super().__init__(tx_id)
def _load(self, item: Union[int, Expression]) -> Any:
"""
:param item:
:return:
"""
item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
return simplify(
If(
item < self._size,
simplify(self._calldata[item]),
symbol_factory.BitVecVal(0, 8),
)
)
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
concrete_length = model.eval(self.size.raw, model_completion=True).as_long()
result = []
for i in range(concrete_length):
value = self._load(i)
c_value = model.eval(value.raw, model_completion=True).as_long()
result.append(c_value)
return result
@property
def size(self) -> Expression:
"""
:return:
"""
return self._size
class BasicSymbolicCalldata(BaseCalldata):
"""A basic class representing symbolic call data."""
def __init__(self, tx_id: int):
"""Initializes the SymbolicCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
"""
self._reads = []
self._size = BitVec(str(tx_id) + "_calldatasize", 256)
super().__init__(tx_id)
def _load(self, item: Union[int, Expression], clean=False) -> Any:
x = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
symbolic_base_value = If(
x >= self._size,
symbol_factory.BitVecVal(0, 8),
BitVec("{}_calldata_{}".format(self.tx_id, str(item)), 8),
)
return_value = symbolic_base_value
for r_index, r_value in self._reads:
return_value = If(r_index == item, r_value, return_value)
if not clean:
self._reads.append((item, symbolic_base_value))
return simplify(return_value)
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
concrete_length = get_concrete_int(model.eval(self.size, model_completion=True))
result = []
for i in range(concrete_length):
value = self._load(i, clean=True)
c_value = get_concrete_int(model.eval(value, model_completion=True))
result.append(c_value)
return result
@property
def size(self) -> Expression:
"""
:return:
"""
return self._size

@ -0,0 +1,77 @@
"""This module contains the class used to represent state-change constraints in
the call graph."""
class Constraints(list):
"""This class should maintain a solver and it's constraints, This class
tries to make the Constraints() object as a simple list of constraints with
some background processing.
TODO: add the solver to this class after callback refactor
"""
def __init__(self, constraint_list=None, solver=None, possibility=None):
"""
:param constraint_list:
:param solver:
:param possibility:
"""
super(Constraints, self).__init__(constraint_list or [])
self.solver = solver
self.__possibility = possibility
def check_possibility(self):
"""
:return:
"""
return True
def append(self, constraint):
"""
:param constraint:
"""
super(Constraints, self).append(constraint)
def pop(self, index=-1):
"""
:param index:
"""
raise NotImplementedError
def __copy__(self):
"""
:return:
"""
constraint_list = super(Constraints, self).copy()
return Constraints(constraint_list)
def __deepcopy__(self, memodict=None):
"""
:param memodict:
:return:
"""
return self.__copy__()
def __add__(self, constraints):
"""
:param constraints:
:return:
"""
constraints_list = super(Constraints, self).__add__(constraints)
return Constraints(constraint_list=constraints_list)
def __iadd__(self, constraints):
"""
:param constraints:
:return:
"""
super(Constraints, self).__iadd__(constraints)
return self

@ -0,0 +1,73 @@
"""This module contains the representation for an execution state's
environment."""
from typing import Dict
from z3 import ExprRef
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import BaseCalldata
from mythril.laser.smt import symbol_factory
class Environment:
"""The environment class represents the current execution environment for
the symbolic executor."""
def __init__(
self,
active_account: Account,
sender: ExprRef,
calldata: BaseCalldata,
gasprice: ExprRef,
callvalue: ExprRef,
origin: ExprRef,
code=None,
):
"""
:param active_account:
:param sender:
:param calldata:
:param gasprice:
:param callvalue:
:param origin:
:param code:
:param calldata_type:
"""
# Metadata
self.active_account = active_account
self.active_function_name = ""
self.address = symbol_factory.BitVecVal(int(active_account.address, 16), 256)
# Ib
self.code = active_account.code if code is None else code
self.sender = sender
self.calldata = calldata
self.gasprice = gasprice
self.origin = origin
self.callvalue = callvalue
def __str__(self) -> str:
"""
:return:
"""
return str(self.as_dict)
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return dict(
active_account=self.active_account,
sender=self.sender,
calldata=self.calldata,
gasprice=self.gasprice,
callvalue=self.callvalue,
origin=self.origin,
)

@ -0,0 +1,143 @@
"""This module contains a representation of the global execution state."""
from typing import Dict, Union, List, Iterable
from copy import copy, deepcopy
from z3 import BitVec
from mythril.laser.smt import symbol_factory
from mythril.laser.ethereum.cfg import Node
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.ethereum.state.annotation import StateAnnotation
class GlobalState:
"""GlobalState represents the current globalstate."""
def __init__(
self,
world_state: "WorldState",
environment: Environment,
node: Node,
machine_state=None,
transaction_stack=None,
last_return_data=None,
annotations=None,
):
"""Constructor for GlobalState.
:param world_state:
:param environment:
:param node:
:param machine_state:
:param transaction_stack:
:param last_return_data:
:param annotations:
"""
self.node = node
self.world_state = world_state
self.environment = environment
self.mstate = (
machine_state if machine_state else MachineState(gas_limit=1000000000)
)
self.transaction_stack = transaction_stack if transaction_stack else []
self.op_code = ""
self.last_return_data = last_return_data
self._annotations = annotations or []
def __copy__(self) -> "GlobalState":
"""
:return:
"""
world_state = copy(self.world_state)
environment = copy(self.environment)
mstate = deepcopy(self.mstate)
transaction_stack = copy(self.transaction_stack)
return GlobalState(
world_state,
environment,
self.node,
mstate,
transaction_stack=transaction_stack,
last_return_data=self.last_return_data,
annotations=[copy(a) for a in self._annotations],
)
@property
def accounts(self) -> Dict:
"""
:return:
"""
return self.world_state.accounts
# TODO: remove this, as two instructions are confusing
def get_current_instruction(self) -> Dict:
"""Gets the current instruction for this GlobalState.
:return:
"""
instructions = self.environment.code.instruction_list
return instructions[self.mstate.pc]
@property
def current_transaction(
self
) -> Union["MessageCallTransaction", "ContractCreationTransaction", None]:
"""
:return:
"""
# TODO: Remove circular to transaction package to import Transaction classes
try:
return self.transaction_stack[-1][0]
except IndexError:
return None
@property
def instruction(self) -> Dict:
"""
:return:
"""
return self.get_current_instruction()
def new_bitvec(self, name: str, size=256) -> BitVec:
"""
:param name:
:param size:
:return:
"""
transaction_id = self.current_transaction.id
return symbol_factory.BitVecSym("{}_{}".format(transaction_id, name), size)
def annotate(self, annotation: StateAnnotation) -> None:
"""
:param annotation:
"""
self._annotations.append(annotation)
if annotation.persist_to_world_state:
self.world_state.annotate(annotation)
@property
def annotations(self) -> List[StateAnnotation]:
"""
:return:
"""
return self._annotations
def get_annotations(self, annotation_type: type) -> Iterable[StateAnnotation]:
"""Filters annotations for the queried annotation type. Designed
particularly for modules with annotations:
globalstate.get_annotations(MySpecificModuleAnnotation)
:param annotation_type: The type to filter annotations for
:return: filter of matching annotations
"""
return filter(lambda x: isinstance(x, annotation_type), self.annotations)

@ -0,0 +1,236 @@
"""This module contains a representation of the EVM's machine state and its
stack."""
from copy import copy
from typing import Union, Any, List, Dict
from z3 import BitVec
from ethereum import opcodes, utils
from mythril.laser.ethereum.evm_exceptions import (
StackOverflowException,
StackUnderflowException,
OutOfGasException,
)
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.memory import Memory
class MachineStack(list):
"""Defines EVM stack, overrides the default list to handle overflows."""
STACK_LIMIT = 1024
def __init__(self, default_list=None):
"""
:param default_list:
"""
if default_list is None:
default_list = []
super(MachineStack, self).__init__(default_list)
def append(self, element: BitVec) -> None:
"""
:param element: element to be appended to the list
:function: appends the element to list if the size is less than STACK_LIMIT, else throws an error
"""
if super(MachineStack, self).__len__() >= self.STACK_LIMIT:
raise StackOverflowException(
"Reached the EVM stack limit of {}, you can't append more "
"elements".format(self.STACK_LIMIT)
)
super(MachineStack, self).append(element)
def pop(self, index=-1) -> BitVec:
"""
:param index:index to be popped, same as the list() class.
:returns popped value
:function: same as list() class but throws StackUnderflowException for popping from an empty list
"""
try:
return super(MachineStack, self).pop(index)
except IndexError:
raise StackUnderflowException("Trying to pop from an empty stack")
def __getitem__(self, item: Union[int, slice]) -> Any:
"""
:param item:
:return:
"""
try:
return super(MachineStack, self).__getitem__(item)
except IndexError:
raise StackUnderflowException(
"Trying to access a stack element which doesn't exist"
)
def __add__(self, other):
"""Implement list concatenation if needed.
:param other:
"""
raise NotImplementedError("Implement this if needed")
def __iadd__(self, other):
"""Implement list concatenation if needed.
:param other:
"""
raise NotImplementedError("Implement this if needed")
class MachineState:
"""MachineState represents current machine state also referenced to as \mu.
"""
def __init__(
self,
gas_limit: int,
pc=0,
stack=None,
memory=None,
constraints=None,
depth=0,
max_gas_used=0,
min_gas_used=0,
):
"""Constructor for machineState.
:param gas_limit:
:param pc:
:param stack:
:param memory:
:param constraints:
:param depth:
:param max_gas_used:
:param min_gas_used:
"""
self.pc = pc
self.stack = MachineStack(stack)
self.memory = memory or Memory()
self.gas_limit = gas_limit
self.min_gas_used = min_gas_used # lower gas usage bound
self.max_gas_used = max_gas_used # upper gas usage bound
self.constraints = constraints or Constraints()
self.depth = depth
def calculate_extension_size(self, start: int, size: int) -> int:
"""
:param start:
:param size:
:return:
"""
if self.memory_size > start + size:
return 0
return start + size - self.memory_size
def calculate_memory_gas(self, start: int, size: int):
"""
:param start:
:param size:
:return:
"""
# https://github.com/ethereum/pyethereum/blob/develop/ethereum/vm.py#L148
oldsize = self.memory_size // 32
old_totalfee = (
oldsize * opcodes.GMEMORY + oldsize ** 2 // opcodes.GQUADRATICMEMDENOM
)
newsize = utils.ceil32(start + size) // 32
new_totalfee = (
newsize * opcodes.GMEMORY + newsize ** 2 // opcodes.GQUADRATICMEMDENOM
)
return new_totalfee - old_totalfee
def check_gas(self):
"""Check whether the machine is out of gas."""
if self.min_gas_used > self.gas_limit:
raise OutOfGasException()
def mem_extend(self, start: int, size: int) -> None:
"""Extends the memory of this machine state.
:param start: Start of memory extension
:param size: Size of memory extension
"""
m_extend = self.calculate_extension_size(start, size)
if m_extend:
extend_gas = self.calculate_memory_gas(start, size)
self.min_gas_used += extend_gas
self.max_gas_used += extend_gas
self.check_gas()
self.memory.extend(m_extend)
def memory_write(self, offset: int, data: List[int]) -> None:
"""Writes data to memory starting at offset.
:param offset:
:param data:
"""
self.mem_extend(offset, len(data))
self.memory[offset : offset + len(data)] = data
def pop(self, amount=1) -> Union[BitVec, List[BitVec]]:
"""Pops amount elements from the stack.
:param amount:
:return:
"""
if amount > len(self.stack):
raise StackUnderflowException
values = self.stack[-amount:][::-1]
del self.stack[-amount:]
return values[0] if amount == 1 else values
def __deepcopy__(self, memodict=None):
"""
:param memodict:
:return:
"""
memodict = {} if memodict is None else memodict
return MachineState(
gas_limit=self.gas_limit,
max_gas_used=self.max_gas_used,
min_gas_used=self.min_gas_used,
pc=self.pc,
stack=copy(self.stack),
memory=copy(self.memory),
constraints=copy(self.constraints),
depth=self.depth,
)
def __str__(self):
"""
:return:
"""
return str(self.as_dict)
@property
def memory_size(self) -> int:
"""
:return:
"""
return len(self.memory)
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return dict(
pc=self.pc,
stack=self.stack,
memory=self.memory,
memsize=self.memory_size,
gas=self.gas_limit,
max_gas_used=self.max_gas_used,
min_gas_used=self.min_gas_used,
)

@ -0,0 +1,142 @@
"""This module contains a representation of a smart contract's memory."""
from typing import Union
from z3 import Z3Exception
from mythril.laser.ethereum import util
from mythril.laser.smt import (
BitVec,
Bool,
Concat,
Extract,
If,
simplify,
symbol_factory,
)
class Memory:
"""A class representing contract memory with random access."""
def __init__(self):
""""""
self._memory = []
def __len__(self):
"""
:return:
"""
return len(self._memory)
def extend(self, size):
"""
:param size:
"""
self._memory.extend(bytearray(size))
def get_word_at(self, index: int) -> Union[int, BitVec]:
"""Access a word from a specified memory index.
:param index: integer representing the index to access
:return: 32 byte word at the specified index
"""
try:
return symbol_factory.BitVecVal(
util.concrete_int_from_bytes(
bytes([util.get_concrete_int(b) for b in self[index : index + 32]]),
0,
),
256,
)
except:
result = simplify(
Concat(
[
b if isinstance(b, BitVec) else symbol_factory.BitVecVal(b, 8)
for b in self[index : index + 32]
]
)
)
assert result.size() == 256
return result
def write_word_at(self, index: int, value: Union[int, BitVec, bool, Bool]) -> None:
"""Writes a 32 byte word to memory at the specified index`
:param index: index to write to
:param value: the value to write to memory
"""
try:
# Attempt to concretize value
if isinstance(value, bool):
_bytes = (
int(1).to_bytes(32, byteorder="big")
if value
else int(0).to_bytes(32, byteorder="big")
)
else:
_bytes = util.concrete_int_to_bytes(value)
assert len(_bytes) == 32
self[index : index + 32] = _bytes
except (Z3Exception, AttributeError): # BitVector or BoolRef
if isinstance(value, Bool):
value_to_write = If(
value,
symbol_factory.BitVecVal(1, 256),
symbol_factory.BitVecVal(0, 256),
)
else:
value_to_write = value
assert value_to_write.size() == 256
for i in range(0, value_to_write.size(), 8):
self[index + 31 - (i // 8)] = Extract(i + 7, i, value_to_write)
def __getitem__(self, item: Union[int, slice]) -> Union[BitVec, int, list]:
"""
:param item:
:return:
"""
if isinstance(item, slice):
start, step, stop = item.start, item.step, item.stop
if start is None:
start = 0
if stop is None: # 2**256 is just a bit too big
raise IndexError("Invalid Memory Slice")
if step is None:
step = 1
return [self[i] for i in range(start, stop, step)]
try:
return self._memory[item]
except IndexError:
return 0
def __setitem__(self, key: Union[int, slice], value: Union[BitVec, int, list]):
"""
:param key:
:param value:
"""
if isinstance(key, slice):
start, step, stop = key.start, key.step, key.stop
if start is None:
start = 0
if stop is None:
raise IndexError("Invalid Memory Slice")
if step is None:
step = 1
for i in range(0, stop - start, step):
self[start + i] = value[i]
else:
if isinstance(value, int):
assert 0 <= value <= 0xFF
if isinstance(value, BitVec):
assert value.size() == 8
self._memory[key] = value

@ -0,0 +1,126 @@
"""This module contains a representation of the EVM's world state."""
from copy import copy
from random import randint
from typing import List, Iterator
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.annotation import StateAnnotation
class WorldState:
"""The WorldState class represents the world state as described in the
yellow paper."""
def __init__(
self, transaction_sequence=None, annotations: List[StateAnnotation] = None
) -> None:
"""Constructor for the world state. Initializes the accounts record.
:param transaction_sequence:
:param annotations:
"""
self.accounts = {}
self.node = None
self.transaction_sequence = transaction_sequence or []
self._annotations = annotations or []
def __getitem__(self, item: str) -> Account:
"""Gets an account from the worldstate using item as key.
:param item: Address of the account to get
:return: Account associated with the address
"""
return self.accounts[item]
def __copy__(self) -> "WorldState":
"""
:return:
"""
new_annotations = [copy(a) for a in self._annotations]
new_world_state = WorldState(
transaction_sequence=self.transaction_sequence[:],
annotations=new_annotations,
)
new_world_state.accounts = copy(self.accounts)
new_world_state.node = self.node
return new_world_state
def create_account(
self, balance=0, address=None, concrete_storage=False, dynamic_loader=None
) -> Account:
"""Create non-contract account.
:param address: The account's address
:param balance: Initial balance for the account
:param concrete_storage: Interpret account storage as concrete
:param dynamic_loader: used for dynamically loading storage from the block chain
:return: The new account
"""
address = address if address else self._generate_new_address()
new_account = Account(
address,
balance=balance,
dynamic_loader=dynamic_loader,
concrete_storage=concrete_storage,
)
self._put_account(new_account)
return new_account
def create_initialized_contract_account(self, contract_code, storage) -> None:
"""Creates a new contract account, based on the contract code and
storage provided The contract code only includes the runtime contract
bytecode.
:param contract_code: Runtime bytecode for the contract
:param storage: Initial storage for the contract
:return: The new account
"""
# TODO: Add type hints
new_account = Account(
self._generate_new_address(), code=contract_code, balance=0
)
new_account.storage = storage
self._put_account(new_account)
def annotate(self, annotation: StateAnnotation) -> None:
"""
:param annotation:
"""
self._annotations.append(annotation)
@property
def annotations(self) -> List[StateAnnotation]:
"""
:return:
"""
return self._annotations
def get_annotations(self, annotation_type: type) -> Iterator[StateAnnotation]:
"""Filters annotations for the queried annotation type. Designed
particularly for modules with annotations:
worldstate.get_annotations(MySpecificModuleAnnotation)
:param annotation_type: The type to filter annotations for
:return: filter of matching annotations
"""
return filter(lambda x: isinstance(x, annotation_type), self.annotations)
def _generate_new_address(self) -> str:
"""Generates a new address for the global state.
:return:
"""
while True:
address = "0x" + "".join([str(hex(randint(0, 16)))[-1] for _ in range(20)])
if address not in self.accounts.keys():
return address
def _put_account(self, account: Account) -> None:
"""
:param account:
"""
self.accounts[account.address] = account

@ -0,0 +1,28 @@
from abc import ABC, abstractmethod
class BasicSearchStrategy(ABC):
""""""
__slots__ = "work_list", "max_depth"
def __init__(self, work_list, max_depth):
self.work_list = work_list
self.max_depth = max_depth
def __iter__(self):
return self
@abstractmethod
def get_strategic_global_state(self):
""""""
raise NotImplementedError("Must be implemented by a subclass")
def __next__(self):
try:
global_state = self.get_strategic_global_state()
if global_state.mstate.depth >= self.max_depth:
return self.__next__()
return global_state
except IndexError:
raise StopIteration

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save