Merge pull request #3 from ConsenSys/master

merge upstream
pull/50/head
step21 7 years ago committed by GitHub
commit 3313d1f9b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 2
      LICENSE
  3. 158
      README.md
  4. 91
      examples/discover_writes.py
  5. 72
      examples/find-fallback-dcl.py
  6. 85
      examples/wallet.sol
  7. 357
      examples/walletlibrary.sol
  8. 271
      myth
  9. 0
      mythril/analysis/__init__.py
  10. 54
      mythril/analysis/callgraph.py
  11. 0
      mythril/analysis/modules/__init__.py
  12. 73
      mythril/analysis/modules/call_to_dynamic_with_gas.py
  13. 53
      mythril/analysis/modules/delegatecall_forward.py
  14. 67
      mythril/analysis/modules/delegatecall_to_dynamic.py
  15. 133
      mythril/analysis/modules/ether_send.py
  16. 76
      mythril/analysis/modules/integer_underflow.py
  17. 31
      mythril/analysis/modules/tx_origin.py
  18. 83
      mythril/analysis/modules/unchecked_retval.py
  19. 112
      mythril/analysis/modules/unchecked_suicide.py
  20. 123
      mythril/analysis/modules/weak_random.py
  21. 61
      mythril/analysis/ops.py
  22. 59
      mythril/analysis/report.py
  23. 29
      mythril/analysis/security.py
  24. 17
      mythril/analysis/solver.py
  25. 133
      mythril/analysis/symbolic.py
  26. 19
      mythril/disassembler/disassembly.py
  27. 1
      mythril/disassembler/signatures.json
  28. 10
      mythril/ether/asm.py
  29. 19
      mythril/ether/contractstorage.py
  30. 17
      mythril/ether/ethcontract.py
  31. 26
      mythril/ether/util.py
  32. 3
      mythril/exceptions.py
  33. 40
      mythril/support/loader.py
  34. 41
      mythril/support/signatures.py
  35. 87
      mythril/support/truffle.py
  36. 2
      requirements.txt
  37. 19
      security_checks.md
  38. 283
      setup.py
  39. 1
      signatures.json
  40. 34
      solidity_examples/ether_send.sol
  41. 35
      solidity_examples/origin.sol
  42. 23
      solidity_examples/reentrancy.sol
  43. 20
      solidity_examples/underflow.sol
  44. 49
      solidity_examples/weak_random.sol
  45. 2
      tests/disassembler_test.py
  46. 2
      tests/ethcontract_test.py
  47. 17
      tests/svm_test.py

3
.gitignore vendored

@ -11,4 +11,5 @@ dist
*.lock *.lock
*.svg *.svg
laser* laser*
lol.py lol*
.idea*

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-present Dan Abramov Copyright (c) 2017-present Bernhard Mueller
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

@ -1,26 +1,15 @@
# Mythril # Mythril
<img height="60px" align="right" src="/static/mythril.png"/> <img height="120px" align="right" src="/static/mythril.png"/>
Mythril is a reverse engineering and bug hunting framework for the Ethereum blockchain. Mythril is a security analysis tool for Ethereum smart contracts. It uses concolic analysis to detect various types of issues. Use it to analyze source code or as a nmap-style black-box blockchain scanner (an "ethermap" if you will).
* [Installation and setup](#installation-and-setup)
* [Command line usage](#command-line-usage)
+ [Input formats](#input-formats)
- [Working with on-chain contracts](#working-with-on-chain-contracts)
- [Working with Solidity files](#working-with-solidity-files)
+ [Disassembler](#disassembler)
+ [Control flow graph](#control-flow-graph)
+ [Contract search](#contract-search)
- [Searching from the command line](#searching-from-the-command-line)
- [Finding cross-references](#finding-cross-references)
## Installation and setup ## Installation and setup
Install from Pypi: Install from Pypi:
```bash ```bash
$ pip install mythril $ pip3 install mythril
``` ```
Or, clone the GitHub repo to install the newest master branch: Or, clone the GitHub repo to install the newest master branch:
@ -28,78 +17,99 @@ Or, clone the GitHub repo to install the newest master branch:
```bash ```bash
$ git clone https://github.com/b-mueller/mythril/ $ git clone https://github.com/b-mueller/mythril/
$ cd mythril $ cd mythril
$ python setup.py install $ python3 setup.py install
``` ```
Note that Mythril requires Python 3.5 to work. Note that Mythril requires Python 3.5 to work.
## Command line usage ### Function signatures
The Mythril command line tool (aptly named `myth`) allows you to conveniently access most of Mythril's functionality. Whenever you disassemble or analyze binary code, Mythril will try to resolve function names using its local signature database. The database must be provided at `~/.mythril/signatures.json`. You can start out with the [default file](signatures.json) as follows:
### Input formats ```
$ mkdir ~/.mythril
$ cd ~/.mythril
$ wget https://raw.githubusercontent.com/b-mueller/mythril/master/signatures.json
```
Mythril can handle various sources and input formats, including bytecode, addresses of contracts on the blockchain, and Solidity source code files. When you analyze Solidity code, new function signatures are added to the database automatically.
#### Working with on-chain contracts ## Security analysis
To pull contracts from the blockchain you need an Ethereum node that is synced with the network. By default, Mythril will query a local node via RPC. Alternatively, you can connect to a remote service such as [INFURA](https://infura.io): Run `myth -x` with one of the input options described below to run the analysis. This will run the Python modules in the [/analysis/modules](https://github.com/b-mueller/mythril/tree/master/mythril/analysis/modules) directory.
``` Mythril detects a range of [security issues](security_checks.md), including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. However, the analysis will not detect business logic issues and is not equivalent to formal verification.
$ myth --rpchost=mainnet.infura.io/{API-KEY} --rpcport=443 --rpctls=True (... etc ...)
```
The recommended way is to use [go-ethereum](https://github.com/ethereum/go-ethereum). Start your local node as follows: ### Analyzing a Truffle project
```bash [Truffle Suite](http://truffleframework.com) is a popular development framework for Ethereum. To analyze the smart contracts in a Truffle project, change in the project root directory and make run `truffle compile` followed by `myth --truffle`.
$ geth --rpc --rpcapi eth,debug --syncmode fast
```
#### Working with Solidity files ### Analyzing Solidity code
In order to work with Solidity source code files, the [solc command line compiler](http://solidity.readthedocs.io/en/develop/using-the-compiler.html) needs to be installed and in path. You can then provide the source file(s) as positional arguments, e.g.: In order to work with Solidity source code files, the [solc command line compiler](http://solidity.readthedocs.io/en/develop/using-the-compiler.html) needs to be installed and in path. You can then provide the source file(s) as positional arguments, e.g.:
```bash ```bash
$ myth -g ./graph.html myContract.sol $ myth -x myContract.sol
``` ```
### Disassembler Alternatively, compile the code on [Remix](http://remix.ethereum.org) and pass the runtime binary code to Mythril:
Use the `-d` flag to disassemble code. The disassembler accepts a bytecode string or a contract address as its input. ```bash
$ myth -x -c "0x5060(...)"
```
If you have multiple interdependent contracts, pass them to Mythril as separate input files. Mythril will map the first contract to address "0x0000(..)", the second one to "0x1111(...)", and so forth (make sure that contract addresses are set accordingly in the source). The contract passed as the first argument will be used as analysis entrypoint.
```bash ```bash
$ myth -d -c "0x6060" $ myth -x myContract.sol myLibrary.sol
0 PUSH1 0x60 ```
### Working with contracts on the mainnet and testnets
When analyzing contracts on the blockchain, Mythril will by default query a local node via IPC. If you want to analyze contracts on the live Ethereum network, you can also use the built-in [INFURA](https://infura.io) support. Alternatively, you can override the RPC settings with the `--rpc` argument.
To analyze a contract on the mainnet, run:
```
$ myth --infura-mainnet -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd
``` ```
Specifying an address via `-a ADDRESS` will download the contract code from your node. Mythril will try to resolve function names using the signatures in `database/signature.json`: Adding the `-l` flag will cause Mythril to automatically retrieve dependencies, such as dynamically linked library contracts:
```bash ```bash
$ myth -d -a "0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208" $ myth --infura-mainnet -x -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 -l -v1
0 PUSH1 0x60
2 PUSH1 0x40
4 MSTORE
(...)
1135 - FUNCTION safeAdd(uint256,uint256) -
1136 CALLVALUE
1137 ISZERO
``` ```
### Control flow graph ### Speed vs. Coverage
The maximum recursion depth for the symbolic execution engine can be controlled with the `--max-depth` argument. The default value is 12. Lowering this value reduces the analysis time as well as the coverage / number of explored states.
Mythril integrates the LASER symbolic virtual machine. Right now, this is mainly used for CFG generation. The `-g FILENAME` option generates an [interactive jsViz graph](http://htmlpreview.github.io/?https://github.com/b-mueller/mythril/blob/master/static/mythril.html): ```
$ myth --infura-mainnet -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd --max-depth 8
```
## Control flow graph
The `-g FILENAME` option generates an [interactive jsViz graph](http://htmlpreview.github.io/?https://github.com/b-mueller/mythril/blob/master/static/mythril.html):
```bash ```bash
$ myth -g ./graph.html -a "0xFa52274DD61E1643d2205169732f29114BC240b3" $ myth --infura-mainnet -g ./graph.html -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd --max-depth 8
``` ```
![callgraph](https://raw.githubusercontent.com/b-mueller/mythril/master/static/callgraph7.png "Call graph") ![callgraph](https://raw.githubusercontent.com/b-mueller/mythril/master/static/callgraph7.png "Call graph")
~~The "bounce" effect, while awesome (and thus enabled by default), sometimes messes up the graph layout.~~ Try adding the `--enable-physics` flag for a very entertaining "bounce" effect that unfortunately completely destroys usability. ~~The "bounce" effect, while awesome (and thus enabled by default), sometimes messes up the graph layout.~~ Try adding the `--enable-physics` flag for a very entertaining "bounce" effect that unfortunately completely destroys usability.
### Contract search ## Blockchain exploration
Mythril builds its own contract database to enable fast search operations. This is to enable operations like those described in the [legendary "Mitch Brenner" blog post](https://medium.com/@rtaylor30/how-i-snatched-your-153-037-eth-after-a-bad-tinder-date-d1d84422a50b) in ~~seconds~~ minutes instead of days. Unfortunately, the initial sync process is slow. You don't need to sync the whole blockchain right away though: If you abort the syncing process with `ctrl+c`, it will be auto-resumed the next time you run the `--init-db` command. If you are planning to do batch operations or use the contract search features, running a [go-ethereum](https://github.com/ethereum/go-ethereum) node is recommended. Start your local node as follows:
```bash
$ geth --syncmode fast
```
Mythril builds its own contract database to enable fast search operations. This enables operations like those described in the [legendary "Mitch Brenner" blog post](https://medium.com/@rtaylor30/how-i-snatched-your-153-037-eth-after-a-bad-tinder-date-d1d84422a50b) in ~~seconds~~ minutes instead of days. Unfortunately, the initial sync process is slow. You don't need to sync the whole blockchain right away though: If you abort the syncing process with `ctrl+c`, it will be auto-resumed the next time you run the `--init-db` command.
```bash ```bash
$ myth --init-db $ myth --init-db
@ -110,7 +120,7 @@ Processing block 4323000, 3 individual contracts in database
The default behavior is to only sync contracts with a non-zero balance. You can disable this behavior with the `--sync-all` flag, but be aware that this will result in a huge (as in: dozens of GB) database. The default behavior is to only sync contracts with a non-zero balance. You can disable this behavior with the `--sync-all` flag, but be aware that this will result in a huge (as in: dozens of GB) database.
#### Searching from the command line ### Searching from the command line
The search feature allows you to find contract instances that contain specific function calls and opcode sequences. It supports simple boolean expressions, such as: The search feature allows you to find contract instances that contain specific function calls and opcode sequences. It supports simple boolean expressions, such as:
@ -120,7 +130,40 @@ $ myth --search "code#PUSH1 0x50,POP#"
$ myth --search "func#changeMultisig(address)# and code#PUSH1 0x50#" $ myth --search "func#changeMultisig(address)# and code#PUSH1 0x50#"
``` ```
#### Finding cross-references ### Reading contract storage
You can read the contents of storage slots from a deployed contract as follows.
```bash
$ myth --storage 0,1 -a "0x76799f77587738bfeef09452df215b63d2cfb08a"
0x0000000000000000000000000000000000000000000000000000000000000003
```
## Utilities
### Disassembler
Use the `-d` flag to disassemble code. The disassembler accepts a bytecode string or a contract address as its input.
```bash
$ myth -d -c "0x6060"
0 PUSH1 0x60
```
Specifying an address via `-a ADDRESS` will download the contract code from your node.
```bash
$ myth -d -a "0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208"
0 PUSH1 0x60
2 PUSH1 0x40
4 MSTORE
(...)
1135 - FUNCTION safeAdd(uint256,uint256) -
1136 CALLVALUE
1137 ISZERO
```
### Finding cross-references
It is often useful to find other contracts referenced by a particular contract. E.g.: It is often useful to find other contracts referenced by a particular contract. E.g.:
@ -132,12 +175,19 @@ $ myth --xrefs -a 0x76799f77587738bfeef09452df215b63d2cfb08a
5b9e8728e316bbeb692d22daaab74f6cbf2c4691 5b9e8728e316bbeb692d22daaab74f6cbf2c4691
``` ```
### Calculating function hashes
To print the Keccak hash for a given function signature:
```bash
$ myth --hash "setOwner(address)"
0x13af4035
```
## Credit ## Credit
- JSON RPC library is adapted from [ethjsonrpc](https://github.com/ConsenSys/ethjsonrpc) (it doesn't seem to be maintained anymore, and I needed to make some changes to it). - JSON RPC library is adapted from [ethjsonrpc](https://github.com/ConsenSys/ethjsonrpc) (it doesn't seem to be maintained anymore, and I needed to make some changes to it).
- The signature data in `signatures.json` has been obtained from the [Ethereum Function Signature Database](https://www.4byte.directory). - The signature data in `signatures.json` was initially obtained from the [Ethereum Function Signature Database](https://www.4byte.directory).
## Disclaimer: Act responsibly!
The purpose of project is to aid discovery of vulnerable smart contracts on the Ethereum mainnet and support research for novel security flaws. If you do find an exploitable issue or vulnerable contract instances, please [do the right thing](https://en.wikipedia.org/wiki/Responsible_disclosure). Also, note that vulnerability branding ("etherbleed", "chainshock",...) is highly discouraged as it will annoy the author and others in the security community. - Many features, bugfixes and analysis modules have been added by [contributors](https://github.com/b-mueller/mythril/graphs/contributors).

@ -1,91 +0,0 @@
from mythril.ether import util
from mythril.rpc.client import EthJsonRpc
from mythril.ether.contractstorage import get_persistent_storage
from mythril.disassembler.disassembly import Disassembly
from ethereum.abi import encode_abi
import re
import os
# Discover contract functions that write the sender address, or an address passed as an argument, to storage.
# Needs testrpc running on port 8546
# testrpc --port 8546 --gasLimit 0xFFFFFF --account 0x0b6f3fd29ca0e570faf9d0bb8945858b9c337cd2a2ff89d65013eec412a4a811,500000000000000000000 --account 0x2194ac1cd3b9ca6cccc1a90aa2c6f944994b80bb50c82b973adce7f288734d5c,500000000000000000000
addr_knupper= "0xe2beffc4bc7ebb9eae43d59d2b555749d9ce7c54"
addr_schnupper = "0xadc2f8617191ff60a36c3c136170cc69c03e64cd"
contract_storage = get_persistent_storage(os.path.join(os.path.expanduser('~'), ".mythril"))
testrpc = EthJsonRpc("localhost", 8546)
testargs1 = [
([], []),
(['address'], [addr_schnupper]),
(['address', 'uint256'], [addr_schnupper, 1 ]),
(['address', 'uint256', 'uint256'], [addr_schnupper, 1, 1]),
(['address[]'], [[addr_schnupper]]),
(['address[]', 'uint256'], [[addr_schnupper], 1 ]),
(['address[]', 'uint256', 'uint256'], [[addr_schnupper], 1, 1]),
]
def testCase(contract_addr, function_selector, arg_types, args):
if re.match(r'^UNK_0x', function_selector):
args = encode_abi(['address'], [addr_schnupper])
data= function_selector[4:] + args.hex()
else:
data = util.encode_calldata(function_selector, arg_types, args)
tx = testrpc.eth_sendTransaction(to_address=contract_addr, from_address=addr_schnupper, gas=5000000, value=0, data=data)
trace = testrpc.traceTransaction(tx)
if trace:
for t in trace['structLogs']:
if t['op'] == 'SSTORE':
if addr_schnupper[2:] in t['stack'][-2]:
return True
return False
def testDynamic(contract_hash, contract, addresses, balances):
ret = testrpc.eth_sendTransaction(from_address=addr_knupper, gas=5000000, value=0, data=contract.creation_code)
receipt = testrpc.eth_getTransactionReceipt(ret)
contract_addr = receipt['contractAddress']
try:
disas = Disassembly(contract.code)
except:
return
found = False
for function_selector in disas.func_to_addr:
try:
for t in testargs1:
if(testCase(contract_addr, function_selector, t[0], t[1])):
print("Possible write!")
print("Contract hash: " + contract_hash)
print("Selector: " + function_selector)
print("Input data: " + str(t[1]))
for i in range(0, len(addresses)):
print("Address: " + addresses[i] + ", balance: " + str(balances[i]))
found = True
break
if found:
break
except:
break
print("Searching " +str(len(list(contract_storage.contracts))) + " contracts...")
contract_storage.search("code#PUSH#", testDynamic) # Returns all contracts

@ -1,72 +0,0 @@
from mythril.ether import evm
from mythril.ether.contractstorage import get_persistent_storage
from mythril.disassembler.disassembly import Disassembly
from mythril.rpc.client import EthJsonRpc
from mythril.disassembler.callgraph import generate_callgraph
import os
contract_storage = get_persistent_storage()
contract_keys = list(contract_storage.contracts)
homestead = EthJsonRpc()
# Iterate over all contracts in the database
for k in contract_keys:
contract = contract_storage.contracts[k]
# Run each contract in the PyEthereum EVM trace to check whether DELEGATECALL is reached
# To execute the fallback function, we don't provide any input data
ret = evm.trace(contract.code)
if 'DELEGATECALL' in ret:
print("DELEGATECALL in fallback function: Contract 0x" + k.hex() + " deployed at: ")
instance_list = contract_storage.instance_lists[k]
for i in range(1, len(instance_list.addresses)):
print("Address: " + instance_list.addresses[i] + ", balance: " + str(instance_list.balances[i]))
# contract.get_xrefs() should contain the delegateCall() target (library contract)
print("Referenced contracts:")
xrefs = contract.get_xrefs()
print ("\n".join(xrefs))
'''
from here on are many different options!
In this example, we'll only check one of the refernced contracts contains the initWallet() function
If it does, we save the disassembly and callgraph for further analysis
'''
for xref in xrefs:
code = homestead.eth_getCode(xref)
disassembly = Disassembly(code)
if contract.matches_expression("func#initWallet(address[],uint256,uint256)#"):
print ("initWallet() in referenced library contract: " + xref)
# Save list of contracts that forward calls to this library contract
cwd = os.getcwd()
with open("contracts_calling_" + xref + ".txt", "w") as f:
addresses = contract_storage.instance_lists[k].addresses
f.write("\n".join(addresses))
easm = disassembly.get_easm()
with open("library_" + xref + ".easm", "w") as f:
f.write(easm)
generate_callgraph(disassembly, os.path.join(cwd, "library_" + xref))

@ -1,85 +0,0 @@
//sol Wallet
// Multi-sig, daily-limited account proxy/wallet.
// @authors:
// Gav Wood <g@ethdev.com>
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a
// single, or, crucially, each of a number of, designated owners.
// usage:
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed.
pragma solidity ^0.4.9;
contract WalletEvents {
// EVENTS
// this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data, address created);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
}
contract Wallet is WalletEvents {
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
// As return statement unavailable in fallback, explicit the method here
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
function isOwner(address _addr) constant returns (bool) {
return _walletLibrary.delegatecall(msg.data);
}
// FIELDS
address constant _walletLibrary = 0x1111111111111111111111111111111111111111;
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
}

@ -1,357 +0,0 @@
pragma solidity ^0.4.9;
contract WalletLibrary {
// EVENTS
// this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data, address created);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
// TYPES
// struct for the status of a pending operation.
struct PendingState {
uint yetNeeded;
uint ownersDone;
uint index;
}
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
}
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
var pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
RequirementChanged(_newRequired);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) external constant returns (address) {
return address(m_owners[ownerIndex + 1]);
}
function isOwner(address _addr) constant returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
var pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(sha3(msg.data)) external {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(sha3(msg.data)) external {
m_spentToday = 0;
}
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) {
// first, take the opportunity to check that we're under the daily limit.
if ((_data.length == 0 && underLimit(_value)) || m_required == 1) {
// yes - just execute the call.
address created;
if (_to == 0) {
created = create(_value, _data);
} else {
if (!_to.call.value(_value)(_data))
throw;
}
SingleTransact(msg.sender, _value, _to, _data, created);
} else {
// determine our operation hash.
o_hash = sha3(msg.data, block.number);
// store if it's new
if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) {
m_txs[o_hash].to = _to;
m_txs[o_hash].value = _value;
m_txs[o_hash].data = _data;
}
if (!confirm(o_hash)) {
ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
}
}
}
function create(uint _value, bytes _code) internal returns (address o_addr) {
assembly {
o_addr := create(_value, add(_code, 0x20), mload(_code))
// jumpi(invalidJumpLabel, iszero(extcodesize(o_addr)))
}
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) {
if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) {
address created;
if (m_txs[_h].to == 0) {
created = create(m_txs[_h].value, m_txs[_h].data);
} else {
if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data))
throw;
}
MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
var pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private constant returns (uint) { return now / 1 days; }
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i) {
delete m_txs[m_pendingIndex[i]];
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
}
delete m_pendingIndex;
}
// FIELDS
address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe;
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
uint constant c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
}

271
myth

@ -5,18 +5,25 @@
""" """
from mythril.ether import evm, util from mythril.ether import evm, util
from mythril.disassembler.callgraph import generate_callgraph
from mythril.ether.contractstorage import get_persistent_storage from mythril.ether.contractstorage import get_persistent_storage
from mythril.ether.ethcontract import ETHContract from mythril.ether.ethcontract import ETHContract
from mythril.ether.util import compile_solidity from mythril.ether.util import compile_solidity
from mythril.rpc.client import EthJsonRpc from mythril.rpc.client import EthJsonRpc
from mythril.ipc.client import EthIpc from mythril.ipc.client import EthIpc
from mythril.rpc.exceptions import ConnectionError
from mythril.support import signatures
from mythril.support.truffle import analyze_truffle_project
from mythril.support.loader import DynLoader from mythril.support.loader import DynLoader
from mythril.exceptions import CompilerError from mythril.exceptions import CompilerError
from mythril.analysis.symbolic import StateSpace
from mythril.analysis.callgraph import generate_graph
from mythril.analysis.security import fire_lasers
from web3 import Web3
from ethereum import utils from ethereum import utils
from laser.ethereum import svm, laserfree
from pathlib import Path from pathlib import Path
from json.decoder import JSONDecodeError
import logging import logging
import json
import sys import sys
import argparse import argparse
import os import os
@ -35,56 +42,91 @@ def exitWithError(message):
sys.exit() sys.exit()
parser = argparse.ArgumentParser(description='Bug hunting on the Ethereum blockchain') parser = argparse.ArgumentParser(description='Security analysis of Ethereum smart contracts')
parser.add_argument("solidity_file", nargs='*') parser.add_argument("solidity_file", nargs='*')
commands = parser.add_argument_group('commands') commands = parser.add_argument_group('commands')
commands.add_argument('-d', '--disassemble', action='store_true', help='disassemble')
commands.add_argument('-g', '--graph', help='generate a control flow graph', metavar='OUTPUT_FILE') commands.add_argument('-g', '--graph', help='generate a control flow graph', metavar='OUTPUT_FILE')
commands.add_argument('-x', '--fire-lasers', action='store_true', help='detect vulnerabilities') commands.add_argument('-x', '--fire-lasers', action='store_true', help='detect vulnerabilities, use with -c, -a or solidity file(s)')
commands.add_argument('-t', '--trace', action='store_true', help='trace contract, use with --data (optional)') commands.add_argument('-t', '--truffle', action='store_true', help='analyze a truffle project (run from project dir)')
commands.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION')
commands.add_argument('--xrefs', action='store_true', help='get xrefs from a contract')
commands.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE')
commands.add_argument('--init-db', action='store_true', help='initialize the contract database')
inputs = parser.add_argument_group('input arguments') inputs = parser.add_argument_group('input arguments')
inputs.add_argument('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE') inputs.add_argument('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE')
inputs.add_argument('-a', '--address', help='pull contract from the blockchain', metavar='CONTRACT_ADDRESS') 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 (experimental)') inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies from the blockchain')
inputs.add_argument('--data', help='message call input data for tracing')
database = parser.add_argument_group('local contracts database')
database.add_argument('--init-db', action='store_true', help='initialize the contract database')
database.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION')
utils = parser.add_argument_group('utilities')
utils.add_argument('-d', '--disassemble', action='store_true', help='print disassembly')
utils.add_argument('--xrefs', action='store_true', help='get xrefs from a contract')
utils.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE')
utils.add_argument('--storage', help='read state variables from storage index, use with -a', metavar='INDEX,NUM_SLOTS,[array]')
options = parser.add_argument_group('options') options = parser.add_argument_group('options')
options.add_argument('--sync-all', action='store_true', help='Also sync contracts with zero balance') options.add_argument('--sync-all', action='store_true', help='Also sync contracts with zero balance')
options.add_argument('--infura-mainnet', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=mainnet.infura.io --rpcport=443 --rpctls="True"') options.add_argument('--max-depth', type=int, default=12, help='Maximum recursion depth for symbolic execution')
options.add_argument('--infura-rinkeby', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=rinkeby.infura.io --rpcport=443 --rpctls="True"')
options.add_argument('--infura-kovan', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=kovan.infura.io --rpcport=443 --rpctls="True"')
options.add_argument('--infura-ropsten', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=ropsten.infura.io --rpcport=443 --rpctls="True"')
options.add_argument('--rpchost', default='127.0.0.1', help='RPC host')
options.add_argument('--rpcport', type=int, default=8545, help='RPC port')
options.add_argument('--rpctls', type=bool, default=False, help='RPC port')
options.add_argument('--ipc', help='use IPC interface instead of RPC', action='store_true')
options.add_argument('--enable-physics', type=bool, default=False, help='enable graph physics simulation') options.add_argument('--enable-physics', type=bool, default=False, help='enable graph physics simulation')
options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL') options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL')
rpc = parser.add_argument_group('RPC options')
rpc.add_argument('--rpc', help='connect via RPC', metavar='HOST:PORT')
rpc.add_argument('--rpctls', type=bool, default=False, help='RPC connection over TLS')
rpc.add_argument('--ganache', action='store_true', help='Preset: local Ganache')
rpc.add_argument('--infura-mainnet', action='store_true', help='Preset: Infura Node service (Mainnet)')
rpc.add_argument('--infura-rinkeby', action='store_true', help='Preset: Infura Node service (Rinkeby)')
rpc.add_argument('--infura-kovan', action='store_true', help='Preset: Infura Node service (Kovan)')
rpc.add_argument('--infura-ropsten', action='store_true', help='Preset: Infura Node service (Ropsten)')
# Get config values # Get config values
try: try:
db_dir = os.environ['DB_DIR'] mythril_dir = os.environ['MYTHRIL_DIR']
except KeyError: except KeyError:
db_dir = None mythril_dir = os.path.join(os.path.expanduser('~'), ".mythril")
try: try:
solc_binary = os.environ['SOLC'] solc_binary = os.environ['SOLC']
except KeyError: except KeyError:
solc_binary = 'solc' solc_binary = 'solc'
# Initialize data directry and singature database
if not os.path.exists(mythril_dir):
logging.info("Creating mythril data directory")
os.mkdir(mythril_dir)
# If no function signature file exists, create it. Function signatures from Solidity source code are added automatically.
signatures_file = os.path.join(mythril_dir, 'signatures.json')
if not os.path.exists(signatures_file):
print("No signature database found. Creating empty database: " + signatures_file + "\n" \
"Consider replacing it with the pre-initialized database at " \
"https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json")
sigs = {}
with open(signatures_file, 'a') as f:
json.dump({},f)
else:
with open(signatures_file) as f:
try:
sigs = json.load(f)
except JSONDecodeError as e:
exitWithError("Invalid JSON in signatures file " + signatures_file + "\n" + str(e))
# Parse cmdline args # Parse cmdline args
args = parser.parse_args() args = parser.parse_args()
if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.trace): if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.storage or args.truffle):
parser.print_help() parser.print_help()
sys.exit() sys.exit()
@ -96,35 +138,21 @@ elif (args.hash):
print("0x" + utils.sha3(args.hash)[:4].hex()) print("0x" + utils.sha3(args.hash)[:4].hex())
sys.exit() sys.exit()
# Database search ops
if args.search or args.init_db:
contract_storage = get_persistent_storage(db_dir)
if (args.search): if args.truffle:
try: try:
contract_storage.search(args.search, searchCallback) analyze_truffle_project()
except SyntaxError: except FileNotFoundError:
exitWithError("Syntax error in search expression.") print("Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully.")
elif (args.init_db):
contract_storage.initialize(args.rpchost, args.rpcport, args.rpctls, args.sync_all, args.ipc)
sys.exit() sys.exit()
# Establish RPC/IPC connection if necessary # Establish RPC/IPC connection if necessary
if (args.address or len(args.solidity_file)): if (args.address or len(args.solidity_file) or args.init_db):
if args.ipc:
try:
eth = EthIpc()
except Exception as e:
exitWithError("Error establishing IPC connection: " + str(e))
else:
try:
if args.infura_mainnet: if args.infura_mainnet:
eth = EthJsonRpc('mainnet.infura.io', 443, True) eth = EthJsonRpc('mainnet.infura.io', 443, True)
elif args.infura_rinkeby: elif args.infura_rinkeby:
@ -133,11 +161,46 @@ if (args.address or len(args.solidity_file)):
eth = EthJsonRpc('kovan.infura.io', 443, True) eth = EthJsonRpc('kovan.infura.io', 443, True)
elif args.infura_ropsten: elif args.infura_ropsten:
eth = EthJsonRpc('ropsten.infura.io', 443, True) eth = EthJsonRpc('ropsten.infura.io', 443, True)
elif args.ganache:
eth = EthJsonRpc('localhost', 7545, False)
elif args.rpc:
try:
host, port = args.rpc.split(":")
except ValueError:
exitWithError("Invalid RPC argument, use HOST:PORT")
tls = args.rpctls
eth = EthJsonRpc(host, int(port), tls)
else: else:
eth = EthJsonRpc(args.rpchost, args.rpcport, args.rpctls) eth = EthIpc()
# Database search ops
if args.search or args.init_db:
contract_storage = get_persistent_storage(mythril_dir)
if (args.search):
try:
contract_storage.search(args.search, searchCallback)
except SyntaxError:
exitWithError("Syntax error in search expression.")
elif (args.init_db):
try:
contract_storage.initialize(eth, args.sync_all)
except FileNotFoundError as e:
print("Error syncing database over IPC: " + str(e))
except ConnectionError as e:
print("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
sys.exit()
except Exception as e:
exitWithError("Error establishing RPC connection: " + str(e))
# Load / compile input contracts # Load / compile input contracts
@ -146,7 +209,22 @@ contracts = []
if (args.code): if (args.code):
contracts.append(ETHContract(args.code, name="MAIN", address = util.get_indexed_address(0))) contracts.append(ETHContract(args.code, name="MAIN", address = util.get_indexed_address(0)))
elif (args.address): elif (args.address):
contracts.append(ETHContract(eth.eth_getCode(args.address), name=args.address, address = args.address))
if not re.match(r'0x[a-fA-F0-9]{40}', args.address):
exitWithError("Invalid contract address. Expected format is '0x...'.")
try:
code = eth.eth_getCode(args.address)
if (code == "0x"):
exitWithError("Received an empty response from eth_getCode. Check the contract address and verify that you are on the correct chain.")
except FileNotFoundError as e:
exitWithError("IPC error: " + str(e))
except ConnectionError as e:
exitWithError("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
contracts.append(ETHContract(code, name=args.address, address = args.address))
elif (len(args.solidity_file)): elif (len(args.solidity_file)):
index = 0 index = 0
@ -154,12 +232,16 @@ elif (len(args.solidity_file)):
file = file.replace("~", str(Path.home())) # Expand user path file = file.replace("~", str(Path.home())) # Expand user path
signatures.add_signatures_from_file(file, sigs)
logging.debug("Adding function signatures from source code:\n" + str(sigs))
try: try:
name, bytecode = compile_solidity(solc_binary, file) name, bytecode = compile_solidity(file, solc_binary)
except CompilerError as e: except CompilerError as e:
exitWithError(e) exitWithError(e)
# Max. 16 contracts supported! # Max. 16 input files supported!
contract = ETHContract(bytecode, name = name, address = util.get_indexed_address(index)) contract = ETHContract(bytecode, name = name, address = util.get_indexed_address(index))
index += 1 index += 1
@ -167,29 +249,59 @@ elif (len(args.solidity_file)):
contracts.append(contract) contracts.append(contract)
logging.info(contract.name + " at " + contract.address) logging.info(contract.name + " at " + contract.address)
# Save updated signature
with open(signatures_file, 'w') as f:
json.dump(sigs, f)
else: else:
exitWithError("No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES") exitWithError("No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES")
# Commands # Commands
if (args.disassemble): if args.storage:
if not args.address:
easm_text = contracts[0].get_easm() exitWithError("To read storage, provide the address of a deployed contract with the -a option.")
sys.stdout.write(easm_text) else:
position = 0
length = 1
array = 0
elif (args.trace): try:
params = (args.storage).split(",")
if len(params) >= 1 and len(params) <= 3:
position = int(params[0])
if len(params) >= 2 and len(params) <= 3:
length = int(params[1])
if len(params) == 3:
if re.match("array",params[2]):
array = 1
if len(params) >= 4:
exitWithError("Invalid number of parameters.")
except ValueError:
exitWithError("Invalid storage index. Please provide a numeric value.")
if array:
position_formated = str(position).zfill(64)
position = int(Web3.sha3(position_formated),16)
if (args.data):
trace = evm.trace(contracts[0].code, args.data)
try:
if length == 1:
print("{}: ".format(position) + eth.eth_getStorageAt(args.address, position));
else: else:
trace = evm.trace(contracts[0].code) for i in range(position, position + length):
print("{}: ".format(hex(i)) + eth.eth_getStorageAt(args.address, i));
except FileNotFoundError as e:
exitWithError("IPC error: " + str(e))
except ConnectionError as e:
exitWithError("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
for i in trace:
if (re.match(r'^PUSH.*', i['op'])): elif (args.disassemble):
print(str(i['pc']) + " " + i['op'] + " " + i['pushvalue'] + ";\tSTACK: " + i['stack'])
else: easm_text = contracts[0].get_easm()
print(str(i['pc']) + " " + i['op'] + ";\tSTACK: " + i['stack']) sys.stdout.write(easm_text)
elif (args.xrefs): elif (args.xrefs):
@ -197,38 +309,39 @@ elif (args.xrefs):
elif (args.graph) or (args.fire_lasers): elif (args.graph) or (args.fire_lasers):
# Convert to LASER SVM format if (args.graph):
modules = {}
for contract in contracts:
modules[contract.address] = contract.as_dict()
if (args.dynld): if (args.dynld):
loader = DynLoader(eth) states = StateSpace(contracts, dynloader=DynLoader(eth), max_depth=args.max_depth)
_svm = svm.SVM(modules, dynamic_loader=loader)
else: else:
_svm = svm.SVM(modules) states = StateSpace(contracts, max_depth=args.max_depth)
if (args.graph):
_svm.simplify_model = True
if args.enable_physics is not None: if args.enable_physics is not None:
physics = True physics = True
html = generate_callgraph(_svm, contracts[0].address, args.enable_physics) html = generate_graph(states, args.enable_physics)
try: try:
with open(args.graph, "w") as f: with open(args.graph, "w") as f:
f.write(html) f.write(html)
except Exception as e: except Exception as e:
print("Error saving graph: " + str(e)) print("Error saving graph: " + str(e))
else: else:
laserfree.fire(modules, contracts[0].address) if (args.dynld):
states = StateSpace(contracts, dynloader=DynLoader(eth), max_depth=args.max_depth)
else:
states = StateSpace(contracts, max_depth=args.max_depth)
report = fire_lasers(states)
if (len(report.issues)):
print(report.as_text())
else:
print("The analysis was completed successfully. No issues were detected.")
else: else:
parser.print_help() parser.print_help()

@ -78,8 +78,30 @@ graph_html = '''<html>
<p><div id="mynetwork"></div><br /></p> <p><div id="mynetwork"></div><br /></p>
<script type="text/javascript"> <script type="text/javascript">
var container = document.getElementById('mynetwork'); var container = document.getElementById('mynetwork');
var data = {'nodes': nodes, 'edges': edges}
var nodesSet = new vis.DataSet(nodes);
var edgesSet = new vis.DataSet(edges);
var data = {'nodes': nodesSet, 'edges': edgesSet}
var gph = new vis.Network(container, data, options); var gph = new vis.Network(container, data, options);
gph.on("click", function (params) {
// parse node id
var nodeID = params['nodes']['0'];
if (nodeID) {
var clickedNode = nodesSet.get(nodeID);
if(clickedNode.isExpanded) {
clickedNode.label = clickedNode.truncLabel;
}
else {
clickedNode.label = clickedNode.fullLabel;
}
clickedNode.isExpanded = !clickedNode.isExpanded;
nodesSet.update(clickedNode);
}
});
</script> </script>
</body> </body>
</html> </html>
@ -91,25 +113,33 @@ colors = [
"{border: '#9e42b3', background: '#842899', highlight: {border: '#9e42b3', background: '#933da6'}}", "{border: '#9e42b3', background: '#842899', highlight: {border: '#9e42b3', background: '#933da6'}}",
"{border: '#b82323', background: '#991d1d', highlight: {border: '#b82323', background: '#a61f1f'}}", "{border: '#b82323', background: '#991d1d', highlight: {border: '#b82323', background: '#a61f1f'}}",
"{border: '#4753bf', background: '#3b46a1', highlight: {border: '#4753bf', background: '#424db3'}}", "{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'}}",
] ]
def serialize(_svm, color_map): def serialize(statespace, color_map):
nodes = [] nodes = []
edges = [] edges = []
for node_key in _svm.nodes: for node_key in statespace.nodes:
code = _svm.nodes[node_key].as_dict()['code'] code = statespace.nodes[node_key].as_dict()['code']
code = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code) code = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code)
color = color_map[_svm.nodes[node_key].as_dict()['module_name']] code_split = code.split("\\n")
nodes.append("{id: '" + node_key + "', color: " + color + ", size: 150, 'label': '" + code + "'}") truncated_code = code if (len(code_split) < 7) else "\\n".join(code_split[:6]) + "\\n(click to expand +)"
for edge in _svm.edges: color = color_map[statespace.nodes[node_key].as_dict()['module_name']]
nodes.append("{id: '" + str(node_key) + "', color: " + color + ", size: 150, 'label': '" + truncated_code + "', 'fullLabel': '" + code + "', 'truncLabel': '" + truncated_code + "', 'isExpanded': false}")
for edge in statespace.edges:
if (edge.condition is None): if (edge.condition is None):
label = "" label = ""
@ -129,19 +159,17 @@ def serialize(_svm, color_map):
def generate_callgraph(svm, main_address, physics): def generate_graph(statespace, physics = False):
svm.sym_exec(main_address)
i = 0 i = 0
color_map = {} color_map = {}
for k in svm.modules: for k in statespace.modules:
color_map[svm.modules[k]['name']] = colors[i] color_map[statespace.modules[k]['name']] = colors[i]
i += 1 i += 1
html = graph_html.replace("[JS]", serialize(svm, color_map)) html = graph_html.replace("[JS]", serialize(statespace, color_map))
html = html.replace("[ENABLE_PHYSICS]", str(physics).lower()) html = html.replace("[ENABLE_PHYSICS]", str(physics).lower())
return html return html

@ -0,0 +1,73 @@
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
import re
import logging
'''
MODULE DESCRIPTION:
Check for call.value()() to an untrusted address
'''
def execute(statespace):
logging.debug("Executing module: CALL_TO_DYNAMIC_WITH_GAS")
issues = []
for call in statespace.calls:
if (call.type == "CALL"):
logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] Call to: " + str(call.to) + ", value " + str(call.value) + ", gas = " + 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 = "The function " + call.node.function_name + " contains a function call to "
target = str(call.to)
is_valid = 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. "
is_valid = True
else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if (m):
index = m.group(1)
try:
for s in statespace.sstors[index]:
if s.tainted:
description += \
"an address found at storage position " + str(index) + ".\n" + \
"This storage position can be written to by calling the function '" + s.node.function_name + "'.\n" \
"Verify that the contract address cannot be set by untrusted users.\n"
is_valid = True
break
except KeyError:
logging.debug("[CALL_TO_DYNAMIC_WITH_GAS] No storage writes to index " + str(index))
continue
if is_valid:
description += "The available gas is forwarded to the called contract. Make sure that the logic of the calling contract is not adversely affected if the called contract misbehaves (e.g. reentrancy)."
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "CALL with gas to dynamic address", "Warning", description)
issues.append(issue)
return issues

@ -0,0 +1,53 @@
from z3 import *
import re
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
import logging
'''
MODULE DESCRIPTION:
Check for invocations of delegatecall(msg.data) in the fallback function.
'''
def execute(statespace):
logging.debug("Executing module: DELEGATECALL_FORWARD")
issues = []
visited = []
for call in statespace.calls:
# Only needs to be checked once per call instructions (essentially just static analysis)
if call.addr in visited:
continue
else:
visited.append(call.addr)
if (call.type == "DELEGATECALL") and (call.node.function_name == "main"):
stack = call.state.stack
meminstart = get_variable(stack[-3])
if meminstart.type == VarType.CONCRETE:
if (re.search(r'calldata.*_0', str(call.state.memory[meminstart.val]))):
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "CALLDATA forwarded with delegatecall()", "Informational")
issue.description = \
"This contract forwards its calldata 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"
if (call.to.type == VarType.CONCRETE):
issue.description += ("DELEGATECALL target: " + hex(call.to.val))
else:
issue.description += "DELEGATECALL target: " + str(call.to)
issues.append(issue)
return issues

@ -0,0 +1,67 @@
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
import re
import logging
'''
MODULE DESCRIPTION:
Check for invocations of delegatecall/callcode to a user-supplied address
'''
def execute(statespace):
logging.debug("Executing module: DELEGATECALL_TO_DYNAMIC")
issues = []
for call in statespace.calls:
if (call.type == "DELEGATECALL" or call.type == "CALLCODE"):
if (call.to.type == VarType.SYMBOLIC):
if ("calldata" in str(call.to)):
issue = Issue(call.node.module_name, call.node.function_name, call.addr, call.type + " to dynamic address")
issue.description = \
"The function " + call.node.function_name + " delegates execution to a contract address obtained from calldata.\n" \
"Recipient address: " + str(call.to)
issues.append(issue)
else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if (m):
index = m.group(1)
logging.debug("DELEGATECALL to contract address in storage")
try:
for s in statespace.sstors[index]:
if s.tainted:
issue = Issue(call.type + " to dynamic address in storage", "Warning")
issue.description = \
"The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract address stored in a state variable. " \
"There is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'.\n" \
"Make sure that the contract address cannot be set by untrusted users."
issues.append(issue)
break
except KeyError:
logging.debug("[ETHER_SEND] No storage writes to index " + str(index))
else:
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "DELEGATECALL to dynamic address", "Informational")
issue.description = \
"The function " + call.node.function_name + " in contract '" + call.node.module_name + " delegates execution to a contract with a dynamic address." \
"To address:" + str(call.to)
issues.append(issue)
return issues

@ -0,0 +1,133 @@
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis import solver
from mythril.analysis.report import Issue
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:
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:
if 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)
try:
for s in statespace.sstors[idx]:
if s.tainted:
description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(idx) + "." \
" This storage slot can be written to by calling the function '" + s.node.function_name + "'.\n"
interesting = True
continue
except KeyError:
logging.debug("[ETHER_SEND] No storage writes to index " + str(idx))
break
if interesting:
description += "Call value is " + str(call.value) + ".\n"
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))
overwrite = False
if (m):
constrained = True
idx = m.group(1)
func = statespace.find_storage_write(idx)
if (func):
description += "\nThere is a check on storage index " + str(index) + ". This storage slot can be written to by calling the function '" + func + "'."
overwrite = True
else:
logging.debug("[ETHER_SEND] No storage writes to index " + str(index))
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)
logging.debug("[ETHER_SEND] MODEL: " + str(model))
for d in model.decls():
logging.debug("[ETHER_SEND] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Ether send", "Warning", description)
issues.append(issue)
except UnsatError:
logging.debug("[ETHER_SEND] no model found")
return issues

@ -0,0 +1,76 @@
from z3 import *
from mythril.analysis import solver
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
from mythril.exceptions import UnsatError
import re
import logging
'''
MODULE DESCRIPTION:
Check for integer underflows.
For every SUB instruction, check if there's a possible state where op1 > op0.
'''
def execute(statespace):
logging.debug("Executing module: INTEGER_UNDERFLOW")
issues = []
for k in statespace.nodes:
node = statespace.nodes[k]
for instruction in node.instruction_list:
if(instruction['opcode'] == "SUB"):
stack = node.states[instruction['address']].stack
op0 = stack[-1]
op1 = stack[-2]
constraints = copy.deepcopy(node.constraints)
if type(op0) == int and type(op1) == int:
continue
if (re.search(r'calldatasize_', str(op0))) \
or (re.search(r'256\*.*If\(1', str(op0), re.DOTALL) or re.search(r'256\*.*If\(1', str(op1), re.DOTALL)) \
or (re.search(r'32 \+.*calldata', str(op0), re.DOTALL) or re.search(r'32 \+.*calldata', str(op1), re.DOTALL)):
# Filter for patterns that contain possible (but apparently non-exploitable) Integer underflows.
# 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
# Both seem to be standard compiler outputs that exist in many contracts.
continue
logging.debug("[INTEGER_UNDERFLOW] Checking SUB " + str(op0) + ", " + str(op1) + " at address " + str(instruction['address']))
constraints.append(UGT(op1,op0))
try:
model = solver.get_model(constraints)
issue = Issue(node.module_name, node.function_name, instruction['address'], "Integer Underflow", "Warning")
issue.description = "A possible integer underflow exists in the function " + node.function_name + ".\n" \
"The SUB instruction at address " + str(instruction['address']) + " may result in a value < 0."
issue.debug = "(" + str(op0) + ") - (" + str(op1) + ").]"
issues.append(issue)
for d in model.decls():
logging.debug("[INTEGER_UNDERFLOW] model: %s = 0x%x" % (d.name(), model[d].as_long()))
except UnsatError:
logging.debug("[INTEGER_UNDERFLOW] no model found")
return issues

@ -0,0 +1,31 @@
from mythril.analysis.report import Issue
import re
import logging
'''
MODULE DESCRIPTION:
Check for constraints on tx.origin (i.e., access to some functionality is restricted to a specific origin).
'''
def execute(statespace):
logging.debug("Executing module: DEPRECIATED OPCODES")
issues = []
for k in statespace.nodes:
node = statespace.nodes[k]
for instruction in node.instruction_list:
if(instruction['opcode'] == "ORIGIN"):
issue = Issue(node.module_name, node.function_name, None, "Use of tx.origin", "Warning", \
"Function " + node.function_name + " retrieves the transaction origin (tx.origin) using the ORIGIN opcode. Use tx.sender instead.\nSee also: https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin"
)
issues.append(issue)
return issues

@ -0,0 +1,83 @@
from z3 import *
import re
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
import logging
from laser.ethereum import helper
'''
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 = []
visited = []
for call in statespace.calls:
# Only needs to be checked once per call instructions (it's essentially just static analysis)
if call.addr in visited:
continue
else:
visited.append(call.addr)
# The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.:
# [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}]
start_index = helper.get_instruction_index(call.node.instruction_list, call.addr) + 1
retval_checked = False
# ISZERO retval should be found within the next few instructions.
for i in range(0, 10):
try:
instr = call.node.instruction_list[start_index + i]
except IndexError:
break
if (instr['opcode'] == 'ISZERO' and re.search(r'retval', str(call.node.states[instr['address']].stack[-1]))):
retval_checked = True
break
if not retval_checked:
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Unchecked CALL return value")
if (call.to.type == VarType.CONCRETE):
receiver = hex(call.to.val)
elif (re.search(r"caller", str(call.to))):
receiver = "msg.sender"
elif (re.search(r"storage", str(call.to))):
receiver = "an address obtained from storage"
else:
receiver = str(call.to)
issue.description = \
"The function " + call.node.function_name + " contains a call to " + receiver + ".\n" \
"The return value of this call is not checked. Note that the function will continue to execute with a return value of '0' if the called contract throws."
issues.append(issue)
return issues

@ -0,0 +1,112 @@
from z3 import *
from mythril.analysis import solver
from mythril.analysis.ops import *
from mythril.analysis.report import Issue
from mythril.exceptions import UnsatError
import re
import logging
'''
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(statespace):
logging.debug("Executing module: UNCHECKED_SUICIDE")
issues = []
for k in statespace.nodes:
node = statespace.nodes[k]
for instruction in node.instruction_list:
if(instruction['opcode'] == "SUICIDE"):
logging.debug("[UNCHECKED_SUICIDE] suicide in function " + node.function_name)
description = "The function " + node.function_name + " executes the SUICIDE instruction."
state = node.states[instruction['address']]
to = state.stack.pop()
if ("caller" in str(to)):
description += "\nThe remaining Ether is sent to the caller's address.\n"
elif ("storage" in str(to)):
description += "\nThe remaining Ether is sent to a stored address\n"
elif ("calldata" in str(to)):
description += "\nThe remaining Ether is sent to an address provided as a function argument."
elif (type(to) == BitVecNumRef):
description += "\nThe remaining Ether is sent to: " + hex(to.as_long())
else:
description += "\nThe remaining Ether is sent to: " + str(to) + "\n"
constrained = False
can_solve = True
index = 0
while(can_solve and index < len(node.constraints)):
constraint = node.constraints[index]
index += 1
m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))
overwrite = False
if (m):
constrained = True
index = m.group(1)
try:
for s in statespace.sstors[index]:
if s.tainted:
description += "\nThere is a check on storage index " + str(index) + ". This storage index can be written to by calling the function '" + s.node.function_name + "'."
break
if not overwrite:
logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index))
can_solve = False
break
except KeyError:
logging.debug("[UNCHECKED_SUICIDE] No storage writes to index " + str(index))
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))):
can_solve = False
break
if not constrained:
description += "\nIt seems that this function can be called without restrictions."
if can_solve:
try:
model = solver.get_model(node.constraints)
logging.debug("[UNCHECKED_SUICIDE] MODEL: " + str(model))
for d in model.decls():
logging.debug("[UNCHECKED_SUICIDE] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
issue = Issue(node.module_name, node.function_name, instruction['address'], "Unchecked SUICIDE", "Warning", description)
issues.append(issue)
except UnsatError:
logging.debug("[UNCHECKED_SUICIDE] no model found")
return issues

@ -0,0 +1,123 @@
import re
from z3 import *
from mythril.analysis.ops import *
from mythril.analysis import solver
from mythril.analysis.report import Issue
from mythril.exceptions import UnsatError
import logging
'''
MODULE DESCRIPTION:
Check for CALLs that send >0 Ether as a result of computation based on predictable state 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)
'''
def execute(statespace):
logging.debug("Executing module: WEAK_RANDOM")
issues = []
for call in statespace.calls:
if ("callvalue" in str(call.value)):
logging.debug("[WEAK_RANDOM] Skipping refund function")
continue
# We're only interested in calls that send Ether
if call.value.type == VarType.CONCRETE:
if call.value.val == 0:
continue
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):
issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Weak random", "Warning",
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('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(call.node.module_name, call.node.function_name, call.addr, "Weak random",
"Warning", description)
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(call.node.module_name, call.node.function_name, call.addr, "Weak random",
"Informational", description)
issues.append(issue)
break
return issues
def solve(call):
try:
model = solver.get_model(call.node.constraints)
logging.debug("[WEAK_RANDOM] MODEL: " + str(model))
for d in model.decls():
logging.debug("[WEAK_RANDOM] main model: %s = 0x%x" % (d.name(), model[d].as_long()))
return True
except UnsatError:
logging.debug("[WEAK_RANDOM] no model found")
return False

@ -0,0 +1,61 @@
from z3 import *
from enum import Enum
from laser.ethereum import helper
class VarType(Enum):
SYMBOLIC = 1
CONCRETE = 2
class Variable:
def __init__(self, val, _type):
self.val = val
self.type = _type
def __str__(self):
return str(self.val)
class Op:
def __init__(self, node, addr):
self.node = node
self.addr = addr
self.state = node.states[addr]
class Call(Op):
def __init__(self, node, addr, _type, to, gas, value = Variable(0, VarType.CONCRETE), data = None):
super().__init__(node, addr)
self.to = to
self.gas = gas
self.type = _type
self.value = value
self.data = data
class Suicide(Op):
def __init__(self, node, addr, call_type, to, value):
super().__init__(node, addr)
self.to = to
class SStore(Op):
def __init__(self, node, addr, value):
super().__init__(node, addr)
self.value = value
self.tainted = False
def get_variable(i):
try:
return Variable(helper.get_concrete_int(i), VarType.CONCRETE)
except AttributeError:
return Variable(simplify(i), VarType.SYMBOLIC)

@ -0,0 +1,59 @@
import hashlib
class Issue:
def __init__(self, contract, function, pc, title, _type="Informational", description="", debug=""):
self.title = title
self.contract = contract
self.function = function
self.pc = pc
self.description = description
self.type = _type
self.debug = debug
self.code = None
def as_dict(self):
return {'title': self.title, 'description':self.description, 'type': self.type}
class Report:
def __init__(self):
self.issues = {}
pass
def append_issue(self, issue):
m = hashlib.md5()
m.update((issue.contract + str(issue.pc) + issue.title).encode('utf-8'))
self.issues[m.digest()] = issue
def as_text(self):
text = ""
for key, issue in self.issues.items():
text += "==== " + issue.title + " ====\n"
text += "Type: " + issue.type + "\n"
if len(issue.contract):
text += "Contract: " + issue.contract + "\n"
else:
text += "Contract: Unknown\n"
text += "Function name: " + issue.function + "\n"
text += "PC address: " + str(issue.pc) + "\n"
text += issue.description + "\n--------------------\n"
if issue.code:
text += "Affected code:\n\n" + issue.code + "\n--------------------\n"
if len(issue.debug):
text += "++++ Debugging info ++++\n" + issue.debug + "\n"
text+="\n"
return text

@ -0,0 +1,29 @@
from mythril.analysis.report import Report
from mythril.analysis import modules
import pkgutil
import logging
def fire_lasers(statespace):
issues = []
_modules = []
for loader, name, is_pkg in pkgutil.walk_packages(modules.__path__):
_modules.append(loader.find_module(name).load_module(name))
logging.info("Starting analysis")
for module in _modules:
logging.info("Executing " + str(module))
issues += module.execute(statespace)
report = Report()
if (len(issues)):
for i in range(0, len(issues)):
report.append_issue(issues[i])
return report

@ -0,0 +1,17 @@
from z3 import *
from mythril.exceptions import UnsatError
import logging
def get_model(constraints):
s = Solver()
s.set("timeout", 2000)
for constraint in constraints:
s.add(constraint)
if (s.check() == sat):
return s.model()
else:
raise UnsatError

@ -0,0 +1,133 @@
from mythril.analysis import solver
from mythril.exceptions import UnsatError
from laser.ethereum import svm
from .ops import *
import logging
class SStorTaintStatus(Enum):
TAINTED = 1
UNTAINTED = 2
class StateSpace:
'''
Symbolic EVM wrapper
'''
def __init__(self, contracts, dynloader = None, max_depth = 12):
# Convert ETHContract objects to LASER SVM "modules"
modules = {}
for contract in contracts:
modules[contract.address] = contract.as_dict()
self.svm = svm.SVM(modules, dynamic_loader=dynloader, max_depth=max_depth)
self.svm.sym_exec(contracts[0].address)
self.modules = modules
self.nodes = self.svm.nodes
self.edges = self.svm.edges
# Analysis
self.calls = []
self.suicides = []
self.sstors = {}
self.sstor_taint_cache = []
for key in self.svm.nodes:
for instruction in self.nodes[key].instruction_list:
op = instruction['opcode']
if op in ('CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'):
stack = copy.deepcopy(self.svm.nodes[key].states[instruction['address']].stack)
if op in ('CALL', 'CALLCODE'):
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = \
get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop())
if (to.type == VarType.CONCRETE):
if (to.val < 5):
# ignore prebuilts
continue
if (meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE):
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas, value, self.svm.nodes[key].states[instruction['address']].memory[meminstart.val:meminsz.val*4]))
else:
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas, value))
else:
gas, to, meminstart, meminsz, memoutstart, memoutsz = \
get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop()), get_variable(stack.pop())
self.calls.append(Call(self.nodes[key], instruction['address'], op, to, gas))
elif op == 'SSTORE':
stack = copy.deepcopy(self.svm.nodes[key].states[instruction['address']].stack)
index, value = stack.pop(), stack.pop()
try:
self.sstors[str(index)].append(SStore(self.nodes[key], instruction['address'], value))
except KeyError:
self.sstors[str(index)] = [SStore(self.nodes[key], instruction['address'], value)]
# self.sstor_analysis()
'''
def sstor_analysis(self):
logging.info("Analyzing storage operations...")
for index in self.sstors:
for s in self.sstors[index]:
# 'Taint' every 'store' instruction that is reachable without any constraint on msg.sender
taint = True
for constraint in s.node.constraints:
if ("caller" in str(constraint)):
taint = False
break
if taint:
s.tainted = True
try:
solver.get_model(s.node.constraints)
s.tainted = True
except UnsatError:
s.tainted = False
'''
def find_storage_write(self, index):
# Find a an unconstrained SSTOR that writes to storage index "index"
try:
for s in self.sstors[index]:
taint = True
for constraint in s.node.constraints:
if ("caller" in str(constraint)):
taint = False
break
return s.node.function_name
return None
except KeyError:
return None

@ -1,6 +1,7 @@
from mythril.ether import asm,util from mythril.ether import asm,util
import os import os
import json import json
import logging
class Disassembly: class Disassembly:
@ -11,14 +12,24 @@ class Disassembly:
self.func_to_addr = {} self.func_to_addr = {}
self.addr_to_func = {} self.addr_to_func = {}
# Parse jump table & resolve function names try:
mythril_dir = os.environ['MYTHRIL_DIR']
except KeyError:
mythril_dir = os.path.join(os.path.expanduser('~'), ".mythril")
script_dir = os.path.dirname(os.path.realpath(__file__)) # Load function signatures
signature_file = os.path.join(script_dir, 'signatures.json')
with open(signature_file) as f: signatures_file = os.path.join(mythril_dir, 'signatures.json')
if not os.path.exists(signatures_file):
logging.info("Missing function signature file. Resolving of function names disabled.")
signatures = {}
else:
with open(signatures_file) as f:
signatures = json.load(f) signatures = json.load(f)
# Parse jump table & resolve function names
jmptable_indices = asm.find_opcode_sequence(["PUSH4", "EQ"], self.instruction_list) jmptable_indices = asm.find_opcode_sequence(["PUSH4", "EQ"], self.instruction_list)
for i in jmptable_indices: for i in jmptable_indices:

File diff suppressed because one or more lines are too long

@ -91,7 +91,13 @@ def disassemble(bytecode):
instruction_list = [] instruction_list = []
addr = 0 addr = 0
while addr < len(bytecode): length = len(bytecode)
if "bzzr" in str(bytecode[-43:]):
# ignore swarm hash
length -= 43
while addr < length:
instruction = {} instruction = {}
@ -117,8 +123,10 @@ def disassemble(bytecode):
if m: if m:
argument = bytecode[addr+1:addr+1+int(m.group(1))] argument = bytecode[addr+1:addr+1+int(m.group(1))]
instruction['argument'] = "0x" + argument.hex() instruction['argument'] = "0x" + argument.hex()
addr += int(m.group(1)) addr += int(m.group(1))
instruction_list.append(instruction) instruction_list.append(instruction)
addr += 1 addr += 1

@ -3,6 +3,7 @@ from mythril.ipc.client import EthIpc
from mythril.ether.ethcontract import ETHContract, InstanceList from mythril.ether.ethcontract import ETHContract, InstanceList
import hashlib import hashlib
import os import os
import time
import persistent import persistent
import persistent.list import persistent.list
import transaction import transaction
@ -47,11 +48,7 @@ class ContractStorage(persistent.Persistent):
return self.contracts[contract_hash] return self.contracts[contract_hash]
def initialize(self, rpchost, rpcport, rpctls, sync_all, ipc): def initialize(self, eth, sync_all):
if ipc:
eth = EthIpc()
else:
eth = EthJsonRpc(rpchost, rpcport, rpctls)
if self.last_block: if self.last_block:
blockNum = self.last_block blockNum = self.last_block
@ -61,6 +58,16 @@ class ContractStorage(persistent.Persistent):
blockNum = eth.eth_blockNumber() blockNum = eth.eth_blockNumber()
print("Starting synchronization from latest block: " + str(blockNum)) print("Starting synchronization from latest block: " + str(blockNum))
'''
On INFURA, the latest block is not immediately available. Here is a workaround to allow for database sync over INFURA.
Note however that this is extremely slow, contracts should always be loaded from a local node.
'''
block = eth.eth_getBlockByNumber(blockNum)
if not block:
blockNum -= 2
while(blockNum > 0): while(blockNum > 0):
if not blockNum % 1000: if not blockNum % 1000:
@ -81,7 +88,7 @@ class ContractStorage(persistent.Persistent):
contract_code = eth.eth_getCode(contract_address) contract_code = eth.eth_getCode(contract_address)
contract_balance = eth.eth_getBalance(contract_address) contract_balance = eth.eth_getBalance(contract_address)
if not contract_balance or sync_all: if not contract_balance and not sync_all:
# skip contracts with zero balance (disable with --sync-all) # skip contracts with zero balance (disable with --sync-all)
continue continue

@ -8,11 +8,18 @@ class ETHContract(persistent.Persistent):
def __init__(self, code, creation_code="", name="", address=""): def __init__(self, code, creation_code="", name="", address=""):
self.code = code
self.creation_code = creation_code self.creation_code = creation_code
self.name = name self.name = name
self.address = address self.address = address
# Workaround: We currently do not support compile-time linking.
# Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address
code = re.sub(r'(_+[A-Za-z0-9]+_+)', 'aa' * 20, code)
self.code = code
def as_dict(self): def as_dict(self):
return { return {
@ -66,12 +73,12 @@ class ETHContract(persistent.Persistent):
expression = expression.replace(m, sign_hash) expression = expression.replace(m, sign_hash)
tokens = re.split("( and | or )", expression, re.IGNORECASE) tokens = filter(None, re.split("(and|or|not)", expression.replace(" ", ""), re.IGNORECASE))
for token in tokens: for token in tokens:
if token == " and " or token == " or ": if token in ("and", "or", "not"):
str_eval += token str_eval += " " + token + " "
continue continue
m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token) m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token)
@ -88,7 +95,7 @@ class ETHContract(persistent.Persistent):
continue continue
return eval(str_eval) return eval(str_eval.strip())
class InstanceList(persistent.Persistent): class InstanceList(persistent.Persistent):

@ -2,7 +2,6 @@ from ethereum.abi import encode_abi, encode_int
from ethereum.utils import zpad from ethereum.utils import zpad
from ethereum.abi import method_id from ethereum.abi import method_id
from mythril.exceptions import CompilerError from mythril.exceptions import CompilerError
import subprocess
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import binascii import binascii
import os import os
@ -10,17 +9,21 @@ import re
def safe_decode(hex_encoded_string): def safe_decode(hex_encoded_string):
if (hex_encoded_string.startswith("0x")): if (hex_encoded_string.startswith("0x")):
return bytes.fromhex(hex_encoded_string[2:]) return bytes.fromhex(hex_encoded_string[2:])
else: else:
return bytes.fromhex(hex_encoded_string) return bytes.fromhex(hex_encoded_string)
def compile_solidity(solc_binary, file): def compile_solidity(file, solc_binary="solc"):
try: try:
p = Popen(["solc", "--bin-runtime", file], stdout=PIPE, stderr=PIPE) p = Popen([solc_binary, "--bin-runtime", '--allow-paths', ".", file], stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
ret = p.returncode
if ret < 0:
raise CompilerError("The Solidity compiler experienced a fatal error (code %d). Please check the Solidity compiler." % ret)
except FileNotFoundError: 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.")
@ -30,10 +33,21 @@ def compile_solidity(solc_binary, file):
err = "Error compiling input file. Solc returned:\n" + stderr.decode("UTF-8") err = "Error compiling input file. Solc returned:\n" + stderr.decode("UTF-8")
raise CompilerError(err) raise CompilerError(err)
# out = out.replace("[\n\s]", "") m = re.search(r":(.*?) =======\nBinary of the runtime part:", out)
contract_name = m.group(1)
if m:
m = re.search(r":(.*?) =======\nBinary of the runtime part: \n([0-9a-f]+)\n", out) m = re.search(r"runtime part: \n([0-9a-f]+)\n", out)
return [m.group(1), m.group(2)]
if (m):
return [contract_name, m.group(1)]
else:
return [contract_name, "0x00"]
else:
err = "Could not retrieve bytecode from solc output. Solc returned:\n" + stdout.decode("UTF-8")
raise CompilerError(err)
def encode_calldata(func_name, arg_types, args): def encode_calldata(func_name, arg_types, args):

@ -1,2 +1,5 @@
class CompilerError(Exception): class CompilerError(Exception):
pass pass
class UnsatError(Exception):
pass

@ -6,32 +6,44 @@ class DynLoader:
def __init__(self, eth): def __init__(self, eth):
self.eth = eth self.eth = eth
self.storage_cache = {}
def dynld(self, contract_address, dependency_address):
logging.info("Dynld at contract " + contract_address + ": " + dependency_address) def read_storage(self, contract_address, index):
# Hack-ish try:
contract_ref = self.storage_cache[contract_address]
data = contract_ref[index]
m = re.match(r'(0x[0-9a-fA-F]{40})', dependency_address) except KeyError:
if (m): self.storage_cache[contract_address] = {}
dependency_address = m.group(1)
else: data = self.eth.eth_getStorageAt(contract_address, position=index, block='latest')
m = re.search(r'storage_(\d+)', dependency_address)
if (m): self.storage_cache[contract_address][index] = data
idx = int(m.group(1))
logging.info("Dynamic contract address at storage index " + str(idx)) except IndexError:
data = self.eth.eth_getStorageAt(contract_address, position=index, block='latest')
self.storage_cache[contract_address][index] = data
return data
dependency_address = "0x" + self.eth.eth_getStorageAt(contract_address, position=idx, block='latest')[26:]
def dynld(self, contract_address, dependency_address):
logging.info("Dynld at contract " + contract_address + ": " + dependency_address)
m = re.match(r'^(0x[0-9a-fA-F]{40})$', dependency_address)
if (m):
dependency_address = m.group(1)
else: else:
logging.info("Unable to resolve address.")
return None return None
logging.info("Dependency address: " + dependency_address) logging.info("Dependency address: " + dependency_address)
code = self.eth.eth_getCode(dependency_address) code = self.eth.eth_getCode(dependency_address)

@ -0,0 +1,41 @@
import re
from ethereum import utils
def add_signatures_from_file(file, sigs={}):
funcs = []
with open(file, encoding="utf-8") as f:
for line in f:
m = re.search(r'function\s+(.*\))', line)
if m:
funcs.append(m.group(1))
for f in funcs:
m = re.search(r'^([A-Za-z0-9_]+)', f)
if (m):
signature = m.group(1)
m = re.search(r'\((.*)\)', f)
_args = m.group(1).split(",")
types = []
for arg in _args:
_type = arg.lstrip().split(" ")[0]
if _type == "uint":
_type = "uint256"
types.append(_type)
typelist = ",".join(types)
signature += "(" + typelist + ")"
sigs["0x" + utils.sha3(signature)[:4].hex()] = signature

@ -0,0 +1,87 @@
import os
import re
import sys
import json
from mythril.ether import util
from mythril.ether.ethcontract import ETHContract
from mythril.analysis.security import fire_lasers
from mythril.analysis.symbolic import StateSpace
from laser.ethereum import helper
def analyze_truffle_project():
project_root = os.getcwd()
build_dir = os.path.join(project_root, "build", "contracts")
files = os.listdir(build_dir)
for filename in files:
if re.match(r'.*\.json$', filename) and filename != "Migrations.json":
with open(os.path.join(build_dir, filename)) as cf:
contractdata = json.load(cf)
try:
name = contractdata['contractName']
bytecode = contractdata['deployedBytecode']
except:
print("Unable to parse contract data. Please use Truffle 4 to compile your project.")
sys.exit()
if (len(bytecode) < 4):
continue
ethcontract= ETHContract(bytecode, name=name, address = util.get_indexed_address(0))
contracts = [ethcontract]
states = StateSpace(contracts, max_depth = 10)
report = fire_lasers(states)
# augment with source code
disassembly = ethcontract.get_disassembly()
source = contractdata['source']
deployedSourceMap = contractdata['deployedSourceMap'].split(";")
mappings = []
i = 0
while(i < len(deployedSourceMap)):
m = re.search(r"^(\d+):*(\d+)", deployedSourceMap[i])
if (m):
offset = m.group(1)
length = m.group(2)
else:
m = re.search(r"^:(\d+)", deployedSourceMap[i])
if m:
length = m.group(1)
mappings.append((int(offset), int(length)))
i += 1
for key, issue in report.issues.items():
index = helper.get_instruction_index(disassembly.instruction_list, issue.pc)
if index:
issue.code_start = mappings[index][0]
issue.code_length = mappings[index][1]
issue.code = source[mappings[index][0]: mappings[index][0] + mappings[index][1]]
if len(report.issues):
print("Analysis result for " + name + ":\n" + report.as_text())
else:
print("Analysis result for " + name + ": No issues found.")

@ -2,6 +2,6 @@ ethereum>=2.0.4
ZODB>=5.3.0 ZODB>=5.3.0
z3-solver>=4.5 z3-solver>=4.5
web3 web3
laser-ethereum==0.2.4 laser-ethereum==0.4.0
requests requests
BTrees BTrees

@ -0,0 +1,19 @@
# Smart Contract Security Issues
| Issue | Description | Mythril Detection Module(s) | References |
|------:|-------------|------------|----------|
|Unprotected functions| Critical functions such as sends with non-zero value or suicide() calls are callable by anyone, or msg.sender is compared against an address in storage that can be written to. E.g. Parity wallet bugs. | [unchecked_suicide](mythril/analysis/modules/unchecked_suicide.py), [ether_send](mythril/analysis/modules/ether_send.py) | |
|Missing check on CALL return value| | [unchecked_retval](mythril/analysis/modules/unchecked_retval.py) | [Handle errors in external calls](https://consensys.github.io/smart-contract-best-practices/recommendations/#use-caution-when-making-external-calls) |
|Re-entrancy| | [call to untrusted contract with gas](mythril/analysis/modules/call_to_dynamic_with_gas.py) | |
|Multiple sends in a single transaction| External calls can fail accidentally or deliberately. Avoid combining multiple send() calls in a single transaction. | | [Favor pull over push for external calls](https://consensys.github.io/smart-contract-best-practices/recommendations/#favor-pull-over-push-for-external-calls) |
|Function call to untrusted contract| | [call to untrusted contract with gas](mythril/analysis/modules/call_to_dynamic_with_gas.py) | |
|Delegatecall or callcode to untrusted contract| | [delegatecall_forward](mythril/analysis/modules/delegatecall_forward.py), [delegatecall_to_dynamic.py](mythril/analysis/modules/delegatecall_to_dynamic.py) | |
|Integer overflow/underflow| | [integer_underflow](mythril/analysis/modules/integer_underflow.py) | |
|Timestamp dependence| | | |
|Payable transaction does not revert in case of failure | | | |
|Call depth attack| | | |
|Use of `tx.origin`| | [tx_origin](mythril/analysis/modules/tx_origin.py) | [Solidity documentation](https://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin), [Avoid using tx.origin](https://consensys.github.io/smart-contract-best-practices/recommendations/#avoid-using-txorigin) |
|Type confusion| | | |
|Predictable RNG| | | [weak_random](mythril/analysis/modules/weak_random.py) |
|Transaction order dependence| | | | |
|Information exposure| | | |

@ -2,11 +2,10 @@ from setuptools import setup, find_packages
long_description = ''' long_description = '''
Mythril Mythril is a security analysis tool for Ethereum smart contracts. It
======= uses concolic analysis to detect various types of issues. Use it to
analyze source code or as a nmap-style black-box blockchain scanner (an
Mythril is a reverse engineering and bug hunting framework for the "ethermap" if you will).
Ethereum blockchain.
Installation and setup Installation and setup
---------------------- ----------------------
@ -25,23 +24,130 @@ Or, clone the GitHub repo to install the newest master branch:
$ cd mythril $ cd mythril
$ python setup.py install $ python setup.py install
You also need a Note that Mythril requires Python 3.5 to work.
`go-ethereum <https://github.com/ethereum/go-ethereum>`__ node that is
synced with the network (note that Mythril uses non-standard RPC APIs
only supported by go-ethereum, so other clients likely won't work).
Start the node as follows:
.. code:: bash Function signatures
~~~~~~~~~~~~~~~~~~~
Whenever you disassemble or analyze binary code, Mythril will try to
resolve function names using its local signature database. The database
must be provided at ``~/.mythril/signatures.json``. You can start out
with the `default file <signatures.json>`__ as follows:
::
$ cd ~/.mythril
$ wget https://raw.githubusercontent.com/b-mueller/mythril/master/signatures.json
When you analyze Solidity code, new function signatures are added to the
database automatically.
$ geth --rpc --rpcapi eth,admin,debug --syncmode fast Security analysis
-----------------
Database initialization Run ``myth -x`` with one of the input options described below to run the
analysis. This will run the Python modules in the
`/analysis/modules <https://github.com/b-mueller/mythril/tree/master/mythril/analysis/modules>`__
directory.
Mythril detects a range of `security issues <security_checks.md>`__,
including integer underflows, owner-overwrite-to-Ether-withdrawal, and
others. However, the analysis will not detect business logic issues and
is not equivalent to formal verification.
Analyzing Solidity code
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
In order to work with Solidity source code files, the `solc command line
compiler <http://solidity.readthedocs.io/en/develop/using-the-compiler.html>`__
needs to be installed and in path. You can then provide the source
file(s) as positional arguments, e.g.:
.. code:: bash
$ myth -x myContract.sol
Alternatively, compile the code on `Remix <http://remix.ethereum.org>`__
and pass the runtime binary code to Mythril:
.. code:: bash
$ myth -x -c "0x5060(...)"
If you have multiple interdependent contracts, pass them to Mythril as
separate input files. Mythril will map the first contract to address
"0x0000(..)", the second one to "0x1111(...)", and so forth (make sure
that contract addresses are set accordingly in the source). The contract
passed in the first argument will be executed as the "main" contract.
.. code:: bash
$ myth -x myContract.sol myLibrary.sol
Working with on-chain contracts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To analyze contracts on the blockchain you need an Ethereum node. By
default, Mythril will query a local node via RPC. Alternatively, you can
use `INFURA <https://infura.io>`__:
::
$ myth --infura-mainnet -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd
If you are planning to do batch operations or use the contract search
features, running a
`go-ethereum <https://github.com/ethereum/go-ethereum>`__ node is
recommended. Start your local node as follows:
.. code:: bash
$ geth --rpc --rpcapi eth,debug --syncmode fast
Specify the target contract with the ``-a`` option:
.. code:: bash
$ myth -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd -v1
Adding the ``-l`` flag will cause Mythril to automatically retrieve
dependencies, such as library contracts:
.. code:: bash
$ myth -x -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 -l -v1
Control flow graph
------------------
The ``-g FILENAME`` option generates an `interactive jsViz
graph <http://htmlpreview.github.io/?https://github.com/b-mueller/mythril/blob/master/static/mythril.html>`__:
.. code:: bash
$ myth -g ./graph.html -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 -l
.. figure:: https://raw.githubusercontent.com/b-mueller/mythril/master/static/callgraph7.png
:alt: Call graph
callgraph
[STRIKEOUT:The "bounce" effect, while awesome (and thus enabled by
default), sometimes messes up the graph layout.] Try adding the
``--enable-physics`` flag for a very entertaining "bounce" effect that
unfortunately completely destroys usability.
Blockchain exploration
----------------------
Mythril builds its own contract database to enable fast search Mythril builds its own contract database to enable fast search
operations. Unfortunately, this process is slow. You don't need to sync operations. This enables operations like those described in the
the whole blockchain right away though: If you abort the syncing process `legendary "Mitch Brenner" blog
with ``ctrl+c``, it will be auto-resumed the next time you run the post <https://medium.com/@rtaylor30/how-i-snatched-your-153-037-eth-after-a-bad-tinder-date-d1d84422a50b>`__
in [STRIKEOUT:seconds] minutes instead of days. Unfortunately, the
initial sync process is slow. You don't need to sync the whole
blockchain right away though: If you abort the syncing process with
``ctrl+c``, it will be auto-resumed the next time you run the
``--init-db`` command. ``--init-db`` command.
.. code:: bash .. code:: bash
@ -51,21 +157,12 @@ with ``ctrl+c``, it will be auto-resumed the next time you run the
Processing block 4323000, 3 individual contracts in database Processing block 4323000, 3 individual contracts in database
(...) (...)
Note that syncing doesn't take quite as long as it first seems, because
the blocks get smaller towards the beginning of the chain.
The default behavior is to only sync contracts with a non-zero balance. The default behavior is to only sync contracts with a non-zero balance.
You can disable this behavior with the ``--sync-all`` flag, but be aware You can disable this behavior with the ``--sync-all`` flag, but be aware
that this will result in a huge (as in: dozens of GB) database. that this will result in a huge (as in: dozens of GB) database.
Command line usage Searching from the command line
------------------ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Mythril command line tool (aptly named ``myth``) allows you to
conveniently access some of Mythril's functionality.
Searching the database
~~~~~~~~~~~~~~~~~~~~~~
The search feature allows you to find contract instances that contain The search feature allows you to find contract instances that contain
specific function calls and opcode sequences. It supports simple boolean specific function calls and opcode sequences. It supports simple boolean
@ -77,6 +174,20 @@ expressions, such as:
$ myth --search "code#PUSH1 0x50,POP#" $ myth --search "code#PUSH1 0x50,POP#"
$ myth --search "func#changeMultisig(address)# and code#PUSH1 0x50#" $ myth --search "func#changeMultisig(address)# and code#PUSH1 0x50#"
Reading contract storage
~~~~~~~~~~~~~~~~~~~~~~~~
You can read the contents of storage slots from a deployed contract as
follows.
.. code:: bash
./myth --storage 0 -a "0x76799f77587738bfeef09452df215b63d2cfb08a"
0x0000000000000000000000000000000000000000000000000000000000000003
Utilities
---------
Disassembler Disassembler
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -85,12 +196,11 @@ bytecode string or a contract address as its input.
.. code:: bash .. code:: bash
$ myth -d -c "$ ./myth -d -c "5060" $ myth -d -c "0x6060"
0 PUSH1 0x60 0 PUSH1 0x60
Specifying an address via ``-a ADDRESS`` will download the contract code Specifying an address via ``-a ADDRESS`` will download the contract code
from your node. Mythril will try to resolve function names using the from your node.
signatures in ``database/signature.json``:
.. code:: bash .. code:: bash
@ -103,125 +213,50 @@ signatures in ``database/signature.json``:
1136 CALLVALUE 1136 CALLVALUE
1137 ISZERO 1137 ISZERO
Adding the ``-g FILENAME`` option will output a call graph:
.. code:: bash
$ myth -d -a "0xFa52274DD61E1643d2205169732f29114BC240b3" -g ./graph.svg
.. figure:: https://raw.githubusercontent.com/b-mueller/mythril/master/static/callgraph.png
:alt: Call graph
callgraph
Note that currently, Mythril only processes ``JUMP`` and ``JUMPI``
instructions with immediately preceding ``PUSH``, but doesn't understand
dynamic jumps and function calls.
Tracing Code
~~~~~~~~~~~~
You can run a code trace in the PyEthereum virtual machine. Optionally,
input data can be passed via the ``--data`` flag.
.. code:: bash
$ myth -t -a "0x3665f2bf19ee5e207645f3e635bf0f4961d661c0"
vm storage={'storage': {}, 'nonce': '0', 'balance': '0', 'code': '0x'} gas=b'21000' stack=[] address=b'6e\xf2\xbf\x19\xee^ vE\xf3\xe65\xbf\x0fIa\xd6a\xc0' depth=0 steps=0 inst=96 pushvalue=96 pc=b'0' op=PUSH1
vm op=PUSH1 gas=b'20997' stack=[b'96'] depth=0 steps=1 inst=96 pushvalue=64 pc=b'2'
vm op=MSTORE gas=b'20994' stack=[b'96', b'64'] depth=0 steps=2 inst=82 pc=b'4'
Finding cross-references Finding cross-references
^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~~~~~~~~~~~
It is often useful to find other contracts referenced by a particular It is often useful to find other contracts referenced by a particular
contract. Let's assume you want to search for contracts that fulfill contract. E.g.:
conditions similar to the `Parity Multisig Wallet
Bug <http://hackingdistributed.com/2017/07/22/deep-dive-parity-bug/>`__.
First, you want to find a list of contracts that use the
``DELEGATECALL`` opcode:
.. code:: bash .. code:: bash
$ myth --search "code#DELEGATECALL#" $ myth --search "code#DELEGATECALL#"
Matched contract with code hash 07459966443977122e639cbf7804c446 Matched contract with code hash 07459966443977122e639cbf7804c446
Address: 0x76799f77587738bfeef09452df215b63d2cfb08a, balance: 1000000000000000 Address: 0x76799f77587738bfeef09452df215b63d2cfb08a, balance: 1000000000000000
Address: 0x3582d2a3b67d63ed10f1ecaef0dca71b9283b543, balance: 92000000000000000000 $ myth --xrefs -a 0x76799f77587738bfeef09452df215b63d2cfb08a
Address: 0x4b9bc00c35f7cee95c65c3c9836040c37dec9772, balance: 89000000000000000000
Address: 0x156d5687a201affb3f1e632dcfb9fde4b0128211, balance: 29500000000000000000
(...)
Note that "code hash" in the above output refers to the contract's index
in the database. The following lines ("Address: ...") list instances of
same contract deployed on the blockchain.
You can then use the ``--xrefs`` flag to find the addresses of
referenced contracts:
.. code:: bash
$ myth --xrefs 07459966443977122e639cbf7804c446
5b9e8728e316bbeb692d22daaab74f6cbf2c4691 5b9e8728e316bbeb692d22daaab74f6cbf2c4691
The command-line search is useful for identifying contracts with Calculating function hashes
interesting opcode patterns. You can either use this information as a ~~~~~~~~~~~~~~~~~~~~~~~~~~~
starting point for manual analysis, or build more complex static and
dynamic analysis using Mythril and
`PyEthereum <https://github.com/ethereum/pyethereum>`__ modules.
Custom scripts
--------------
TODO
- Add examples for static/dynamic analysis To print the Keccak hash for a given function signature:
- API documentation
Issues .. code:: bash
------
The RPC database sync solution is not very efficient. I explored some
other options, including:
- Using PyEthereum: I encountered issues syncing PyEthereum with
Homestead. Also, PyEthApp only supports Python 2.7, which causes
issues with other important packages.
- Accessing the Go-Ethereum LevelDB: This would be a great option.
However, PyEthereum database code seems unable to deal with
Go-Ethereum's LevelDB. It would take quite a bit of effort to figure
this out.
- IPC might allow for faster sync then RPC - haven't tried it yet.
I'm writing this in my spare time, so contributors would be highly $ myth --hash "setOwner(address)"
welcome! 0x13af4035
Credit Credit
------ ------
JSON RPC library is adapted from - JSON RPC library is adapted from
`ethjsonrpc <https://github.com/ConsenSys/ethjsonrpc>`__ (it doesn't `ethjsonrpc <https://github.com/ConsenSys/ethjsonrpc>`__ (it doesn't
seem to be maintained anymore, and I needed to make some changes to it). seem to be maintained anymore, and I needed to make some changes to
it).
Act responsibly!
---------------- - The signature data in ``signatures.json`` was initially obtained from
the `Ethereum Function Signature
The purpose of project is to aid discovery of vulnerable smart contracts Database <https://www.4byte.directory>`__.
on the Ethereum mainnet and support research for novel security flaws.
If you do find an exploitable issue or vulnerable contract instances,
please `do the right
thing <https://en.wikipedia.org/wiki/Responsible_disclosure>`__. Also,
note that vulnerability branding ("etherbleed", "chainshock",...) is
highly discouraged as it will annoy the author and others in the
security community.
''' '''
setup( setup(
name='mythril', name='mythril',
version='0.7.6', version='0.10.6',
description='A reversing and bug hunting framework for the Ethereum blockchain', description='Security analysis tool for Ethereum smart contracts',
long_description=long_description, long_description=long_description,
url='https://github.com/b-mueller/mythril', url='https://github.com/b-mueller/mythril',
@ -256,7 +291,7 @@ setup(
'web3', 'web3',
'ZODB>=5.3.0', 'ZODB>=5.3.0',
'z3-solver>=4.5', 'z3-solver>=4.5',
'laser-ethereum==0.2.4', 'laser-ethereum==0.4.0',
'requests', 'requests',
'BTrees' 'BTrees'
], ],

@ -0,0 +1 @@
{"0x07f9f7ba": "StandardBounties(address)", "0x8c590917": "contribute(uint256,uint256)", "0x626a413a": "activateBounty(uint256,uint256)", "0x1e688c14": "fulfillBounty(uint256,string)", "0x41ac5dd0": "updateFulfillment(uint256,uint256,string)", "0xd9583497": "acceptFulfillment(uint256,uint256)", "0x16b57509": "killBounty(uint256)", "0x2d1fdef6": "extendDeadline(uint256,uint256)", "0x5d19606e": "transferIssuer(uint256,address)", "0xd6c0ceab": "changeBountyDeadline(uint256,uint256)", "0xf3d3402a": "changeBountyData(uint256,string)", "0x452ccadb": "changeBountyFulfillmentAmount(uint256,uint256)", "0xcdad6576": "changeBountyArbiter(uint256,address)", "0x992a3e75": "changeBountyPaysTokens(uint256,bool,address)", "0x422d4cd6": "increasePayout(uint256,uint256,uint256)", "0xb94b0a3a": "getFulfillment(uint256,uint256)", "0xee8c4bbf": "getBounty(uint256)", "0x86647bac": "getBountyArbiter(uint256)", "0xa60745aa": "getBountyData(uint256)", "0x19dba3d2": "getBountyToken(uint256)", "0x3278ba2f": "getNumBounties()", "0xfbe334f8": "getNumFulfillments(uint256)", "0xdb3b6263": "transitionToState(uint256,BountyStages)", "0x4e3b52fe": "metaCoin()", "0x412664ae": "sendToken(address,uint256)", "0x56885cd8": "crowdfunding()", "0x6c343ffe": "withdrawfunds()", "0xe8b5e51f": "invest()", "0xaa3288f4": "getBalance())", "0xc11a4b47": "Origin()", "0xf2fde38b": "transferOwnership(address)", "0x00362a95": "donate(address)", "0x70a08231": "balanceOf(address)", "0x2e1a7d4d": "withdraw(uint256)", "0x6241bfd1": "Token(uint256)", "0xa3210e87": "sendeth(address,uint256)", "0xcd38aa87": "chooseWinner()", "0xd6d22fa4": "MetaCoin()", "0x90b98a11": "sendCoin(address,uint256)", "0x7bd703e8": "getBalanceInEth(address)", "0xf8b2cb4f": "getBalance(address)", "0xa360b26f": "Migrations()", "0xfdacd576": "setCompleted(uint256)", "0x0900f010": "upgrade(address)", "0xcae9ca51": "approveAndCall(address,uint256,bytes)", "0xa9059cbb": "transfer(address,uint256)", "0x23b872dd": "transferFrom(address,address,uint256)", "0x095ea7b3": "approve(address,uint256)", "0xdd62ed3e": "allowance(address,address)", "0x525f8a5c": "setSaleStartTime(uint256)", "0xd132391a": "setSaleEndTime(uint256)", "0x0a0cd8c8": "setupDone()", "0xd7bb99ba": "contribute()", "0xf0349d5f": "setupStages()", "0x2a4f6533": "createTokenContract())", "0x42a6b21a": "getContributionLimit(address)", "0x1a787915": "startConditions(bytes32)", "0xf3fde261": "onTransition(bytes32)", "0x27816235": "onSaleEnded()", "0x091cde0b": "DisbursementHandler(address)", "0xf3fef3a3": "withdraw(address,uint256)", "0x4bc9fdc2": "calcMaxWithdraw()", "0xc9e61599": "createTarget())", "0x200094e0": "deployContract())", "0x5a048d78": "claim(Target)", "0x16ae6b67": "checkInvariant())", "0x2aa5ed61": "DayLimit(uint256)", "0xe7dde9a3": "_setDailyLimit(uint256)", "0x4a4c82c6": "_resetSpentToday()", "0x180aadb7": "underLimit(uint256)", "0x9d4468ff": "today())", "0x19045a25": "recover(bytes32,bytes)", "0xe92dfb23": "LimitBalance(uint256)", "0xd73dd623": "increaseApproval(address,uint256)", "0x66188463": "decreaseApproval(address,uint256)", "0xabaf5880": "Crowdsale(uint256,uint256,uint256,address)", "0xec8ac4d8": "buyTokens(address)", "0x9d735286": "forwardFunds()", "0x605120cf": "validPurchase())", "0x6e42787f": "hasEnded())", "0xe5c46944": "MultiSigWallet(address[],uint256)", "0x7065cb48": "addOwner(address)", "0x173825d9": "removeOwner(address)", "0xe20056e6": "replaceOwner(address,address)", "0xba51a6df": "changeRequirement(uint256)", "0xc6427474": "submitTransaction(address,uint256,bytes)", "0xc01a8c84": "confirmTransaction(uint256)", "0x20ea8d86": "revokeConfirmation(uint256)", "0xee22610b": "executeTransaction(uint256)", "0x784547a7": "isConfirmed(uint256)", "0xec096f8d": "addTransaction(address,uint256,bytes)", "0x8b51d13f": "getConfirmationCount(uint256)", "0x54741525": "getTransactionCount(bool,bool)", "0xa0e67e2b": "getOwners()", "0xb5dc40c3": "getConfirmations(uint256)", "0xa8abe69a": "getTransactionIds(uint256,uint256,bool,bool)"}

@ -0,0 +1,34 @@
contract Crowdfunding {
mapping(address => uint) public balances;
address public owner;
uint256 INVEST_MIN = 1 ether;
uint256 INVEST_MAX = 10 ether;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function crowdfunding() {
owner = msg.sender;
}
function withdrawfunds() onlyOwner {
msg.sender.transfer(this.balance);
}
function invest() public payable {
require(msg.value > INVEST_MIN && msg.value < INVEST_MAX);
balances[msg.sender] += msg.value;
}
function getBalance() public constant returns (uint) {
return balances[msg.sender];
}
function() public payable {
invest();
}
}

@ -0,0 +1,35 @@
contract Origin {
address public owner;
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Origin() {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
if (tx.origin != owner) {
throw;
}
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) onlyOwner {
if (newOwner != address(0)) {
owner = newOwner;
}
}
}

@ -0,0 +1,23 @@
contract Reentrancy {
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] += msg.value;
}
function balanceOf(address _who) public constant returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
function() payable {}
}

@ -0,0 +1,20 @@
contract Under {
mapping(address => uint) balances;
uint public totalSupply;
function Token(uint _initialSupply) {
balances[msg.sender] = totalSupply = _initialSupply;
}
function sendeth(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public constant returns (uint balance) {
return balances[_owner];
}
}

@ -0,0 +1,49 @@
pragma solidity ^0.4.16;
contract WeakRandom {
struct Contestant {
address addr;
uint gameId;
}
uint public constant prize = 2.5 ether;
uint public constant totalTickets = 50;
uint public constant pricePerTicket = prize / totalTickets;
uint public gameId = 1;
uint public nextTicket = 0;
mapping (uint => Contestant) public contestants;
function () payable public {
uint moneySent = msg.value;
while (moneySent >= pricePerTicket && nextTicket < totalTickets) {
uint currTicket = nextTicket++;
contestants[currTicket] = Contestant(msg.sender, gameId);
moneySent -= pricePerTicket;
}
if (nextTicket == totalTickets) {
chooseWinner();
}
// Send back leftover money
if (moneySent > 0) {
msg.sender.transfer(moneySent);
}
}
function chooseWinner() private {
address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
uint seed3 = block.difficulty;
bytes32 randHash = keccak256(seed1, seed2, seed3);
uint winningNumber = uint(randHash) % totalTickets;
address winningAddress = contestants[winningNumber].addr;
gameId++;
nextTicket = 0;
winningAddress.transfer(prize);
}
}

File diff suppressed because one or more lines are too long

@ -16,7 +16,7 @@ class Getinstruction_listTestCase(ETHContractTestCase):
disassembly = contract.get_disassembly() disassembly = contract.get_disassembly()
self.assertEqual(len(disassembly.instruction_list), 71, 'Error disassembling code using ETHContract.get_instruction_list()') self.assertEqual(len(disassembly.instruction_list), 53, 'Error disassembling code using ETHContract.get_instruction_list()')
class GetEASMTestCase(ETHContractTestCase): class GetEASMTestCase(ETHContractTestCase):

@ -1,19 +1,18 @@
import unittest import unittest
from mythril.disassembler.callgraph import generate_callgraph from mythril.analysis.symbolic import StateSpace
from mythril.disassembler.disassembly import Disassembly from mythril.analysis.callgraph import generate_graph
from laser.ethereum import svm from mythril.ether.ethcontract import ETHContract
class SVMTestCase(unittest.TestCase): class SVMTestCase(unittest.TestCase):
def runTest(self): def runTest(self):
modules = {} code = "0x60606040525b603c5b60006010603e565b9050593681016040523660008237602060003683856040603f5a0204f41560545760206000f35bfe5b50565b005b73c3b2ae46792547a96b9f84405e36d0e07edcd05c5b905600a165627a7a7230582062a884f947232ada573f95940cce9c8bfb7e4e14e21df5af4e884941afb55e590029"
modules['0x0000000000000000000000000000000000000000'] = {'name': 'metaCoin', 'address': '0x0000000000000000000000000000000000000000', 'creation_code': '', 'code': '60606040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806327e235e314610051578063412664ae1461009e575b600080fd5b341561005c57600080fd5b610088600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506100f8565b6040518082815260200191505060405180910390f35b34156100a957600080fd5b6100de600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610110565b604051808215151515815260200191505060405180910390f35b60006020528060005260406000206000915090505481565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561016157600090506101fe565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550600090505b929150505600a165627a7a72305820fd4fa106da498514e90965a45ffecc1da53a0cd8bb7a7135910f8612245a46370029'}
modules['0x0000000000000000000000000000000000000000']['disassembly'] = Disassembly(modules['0x0000000000000000000000000000000000000000']['code'])
_svm = svm.SVM(modules) contract = ETHContract(code)
statespace = StateSpace([contract])
html = generate_callgraph(_svm, '0x0000000000000000000000000000000000000000', False) html = generate_graph(statespace)
self.assertTrue("var nodes = [\n{id: \'metaCoin:" in html) self.assertTrue("0 PUSH1 0x60\\n2 PUSH1 0x40\\n4 MSTORE\\n5 JUMPDEST" in html)

Loading…
Cancel
Save