Merge branch 'bugfix/bectoken' into bugfix/taint_active_account

# Conflicts:
#	mythril/laser/ethereum/taint_analysis.py
pull/375/head
Joran Honig 6 years ago
commit 7c247e15c1
  1. 4
      .circleci/config.yml
  2. 20
      mythril/analysis/modules/ether_send.py
  3. 2
      mythril/analysis/modules/exceptions.py
  4. 1
      mythril/analysis/modules/integer.py
  5. 2
      mythril/analysis/solver.py
  6. 7
      mythril/disassembler/disassembly.py
  7. 22
      mythril/ether/ethcontract.py
  8. 1
      mythril/laser/ethereum/cfg.py
  9. 16
      mythril/laser/ethereum/instructions.py
  10. 74
      mythril/laser/ethereum/state.py
  11. 48
      mythril/laser/ethereum/svm.py
  12. 2
      mythril/laser/ethereum/taint_analysis.py
  13. 60
      mythril/laser/ethereum/transaction.py
  14. 9
      mythril/leveldb/client.py
  15. 2
      tests/native_test.py
  16. 2
      tests/svm_test.py

@ -57,6 +57,10 @@ jobs:
command: python3 setup.py install command: python3 setup.py install
working_directory: /home/mythril working_directory: /home/mythril
- run:
name: Sonar analysis
command: if [ -z "CIRCLE_PR_NUMBER" ]; then sonar-scanner -Dsonar.projectKey=$SONAR_PROJECT_KEY -Dsonar.organization=$SONAR_ORGANIZATION -Dsonar.sources=/home/mythril -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$(SONAR_LOGIN); fi
- run: - run:
name: Integration tests name: Integration tests
command: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./run-integration-tests.sh; fi command: if [ -z "$CIRCLE_PR_NUMBER" ]; then ./run-integration-tests.sh; fi

@ -27,14 +27,13 @@ def execute(statespace):
state = call.state state = call.state
address = state.get_current_instruction()['address'] address = state.get_current_instruction()['address']
if ("callvalue" in str(call.value)): if "callvalue" in str(call.value):
logging.debug("[ETHER_SEND] Skipping refund function") logging.debug("[ETHER_SEND] Skipping refund function")
continue continue
# We're only interested in calls that send Ether # We're only interested in calls that send Ether
if call.value.type == VarType.CONCRETE: if call.value.type == VarType.CONCRETE and call.value.val == 0:
if call.value.val == 0:
continue continue
interesting = False interesting = False
@ -52,14 +51,14 @@ def execute(statespace):
else: else:
m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to)) m = re.search(r'storage_([a-z0-9_&^]+)', str(call.to))
if (m): if m:
idx = m.group(1) idx = m.group(1)
description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(idx) + ".\n" description += "a non-zero amount of Ether is sent to an address taken from storage slot " + str(idx) + ".\n"
func = statespace.find_storage_write(state.environment.active_account.address, idx) func = statespace.find_storage_write(state.environment.active_account.address, idx)
if (func): if func:
description += "There is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`.\n" description += "There is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`.\n"
interesting = True interesting = True
else: else:
@ -74,7 +73,7 @@ def execute(statespace):
index = 0 index = 0
while(can_solve and index < len(node.constraints)): while can_solve and index < len(node.constraints):
constraint = node.constraints[index] constraint = node.constraints[index]
index += 1 index += 1
@ -82,14 +81,14 @@ def execute(statespace):
m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint)) m = re.search(r'storage_([a-z0-9_&^]+)', str(constraint))
if (m): if m:
constrained = True constrained = True
idx = m.group(1) idx = m.group(1)
func = statespace.find_storage_write(state.environment.active_account.address, idx) func = statespace.find_storage_write(state.environment.active_account.address, idx)
if (func): if func:
description += "\nThere is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`." description += "\nThere is a check on storage index " + str(idx) + ". This storage slot can be written to by calling the function `" + func + "`."
else: else:
logging.debug("[ETHER_SEND] No storage writes to index " + str(idx)) logging.debug("[ETHER_SEND] No storage writes to index " + str(idx))
@ -98,7 +97,7 @@ def execute(statespace):
# CALLER may also be constrained to hardcoded address. I.e. 'caller' and some integer # 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))): elif re.search(r"caller", str(constraint)) and re.search(r'[0-9]{20}', str(constraint)):
constrained = True constrained = True
can_solve = False can_solve = False
break break
@ -116,7 +115,8 @@ def execute(statespace):
debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(model) debug = "SOLVER OUTPUT:\n" + solver.pretty_print_model(model)
issue = Issue(call.node.contract_name, call.node.function_name, address, "Ether send", "Warning", description, debug) issue = Issue(call.node.contract_name, call.node.function_name, address, "Ether send", "Warning",
description, debug)
issues.append(issue) issues.append(issue)
except UnsatError: except UnsatError:

@ -24,9 +24,7 @@ def execute(statespace):
for state in node.states: for state in node.states:
instruction = state.get_current_instruction() instruction = state.get_current_instruction()
if(instruction['opcode'] == "ASSERT_FAIL"): if(instruction['opcode'] == "ASSERT_FAIL"):
try: try:
model = solver.get_model(node.constraints) model = solver.get_model(node.constraints)
address = state.get_current_instruction()['address'] address = state.get_current_instruction()['address']

@ -108,7 +108,6 @@ def _verify_integer_overflow(statespace, node, expr, state, model, constraint, o
return _try_constraints(node.constraints, [Not(constraint)]) is not None return _try_constraints(node.constraints, [Not(constraint)]) is not None
def _try_constraints(constraints, new_constraints): def _try_constraints(constraints, new_constraints):
""" """
Tries new constraints Tries new constraints

@ -4,7 +4,7 @@ import logging
def get_model(constraints): def get_model(constraints):
s = Solver() s = Solver()
s.set("timeout", 10000) s.set("timeout", 100000)
for constraint in constraints: for constraint in constraints:
s.add(constraint) s.add(constraint)

@ -1,4 +1,4 @@
from mythril.ether import asm,util from mythril.ether import asm, util
from mythril.support.signatures import SignatureDb from mythril.support.signatures import SignatureDb
import logging import logging
@ -7,7 +7,7 @@ class Disassembly(object):
def __init__(self, code): def __init__(self, code):
self.instruction_list = asm.disassemble(util.safe_decode(code)) self.instruction_list = asm.disassemble(util.safe_decode(code))
self.xrefs = [] self.func_hashes = []
self.func_to_addr = {} self.func_to_addr = {}
self.addr_to_func = {} self.addr_to_func = {}
self.bytecode = code self.bytecode = code
@ -24,6 +24,7 @@ class Disassembly(object):
for i in jmptable_indices: for i in jmptable_indices:
func_hash = self.instruction_list[i]['argument'] func_hash = self.instruction_list[i]['argument']
self.func_hashes.append(func_hash)
try: try:
# tries local cache, file and optional online lookup # tries local cache, file and optional online lookup
# may return more than one function signature. since we cannot probe for the correct one we'll use the first # may return more than one function signature. since we cannot probe for the correct one we'll use the first
@ -38,7 +39,7 @@ class Disassembly(object):
func_name = "_function_" + func_hash func_name = "_function_" + func_hash
try: try:
offset = self.instruction_list[i+2]['argument'] offset = self.instruction_list[i + 2]['argument']
jump_target = int(offset, 16) jump_target = int(offset, 16)
self.func_to_addr[func_name] = jump_target self.func_to_addr[func_name] = jump_target

@ -31,12 +31,12 @@ class ETHContract(persistent.Persistent):
def get_easm(self): def get_easm(self):
return Disassembly(self.code).get_easm() return self.disassembly.get_easm()
def matches_expression(self, expression): def matches_expression(self, expression):
easm_code = self.get_easm()
str_eval = '' str_eval = ''
easm_code = None
matches = re.findall(r'func#([a-zA-Z0-9\s_,(\\)\[\]]+)#', expression) matches = re.findall(r'func#([a-zA-Z0-9\s_,(\\)\[\]]+)#', expression)
@ -58,6 +58,9 @@ class ETHContract(persistent.Persistent):
m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token) m = re.match(r'^code#([a-zA-Z0-9\s,\[\]]+)#', token)
if (m): if (m):
if easm_code is None:
easm_code = self.get_easm()
code = m.group(1).replace(",", "\\n") code = m.group(1).replace(",", "\\n")
str_eval += "\"" + code + "\" in easm_code" str_eval += "\"" + code + "\" in easm_code"
continue continue
@ -65,21 +68,8 @@ class ETHContract(persistent.Persistent):
m = re.match(r'^func#([a-fA-F0-9]+)#$', token) m = re.match(r'^func#([a-fA-F0-9]+)#$', token)
if (m): if (m):
str_eval += "\"" + m.group(1) + "\" in easm_code" str_eval += "\"" + m.group(1) + "\" in self.disassembly.func_hashes"
continue continue
return eval(str_eval.strip()) return eval(str_eval.strip())
class InstanceList(persistent.Persistent):
def __init__(self):
self.addresses = []
self.balances = []
pass
def add(self, address, balance=0):
self.addresses.append(address)
self.balances.append(balance)
self._p_changed = True

@ -8,6 +8,7 @@ class JumpType(Enum):
UNCONDITIONAL = 2 UNCONDITIONAL = 2
CALL = 3 CALL = 3
RETURN = 4 RETURN = 4
Transaction = 5
class NodeFlags(Flags): class NodeFlags(Flags):

@ -707,13 +707,8 @@ class Instruction:
index = str(index) index = str(index)
try: try:
# Create a fresh copy of the account object before modifying storage global_state.environment.active_account = deepcopy(global_state.environment.active_account)
global_state.accounts[global_state.environment.active_account.address] = global_state.environment.active_account
for k in global_state.accounts:
if global_state.accounts[k] == global_state.environment.active_account:
global_state.accounts[k] = deepcopy(global_state.accounts[k])
global_state.environment.active_account = global_state.accounts[k]
break
global_state.environment.active_account.storage[index] = value global_state.environment.active_account.storage[index] = value
except KeyError: except KeyError:
@ -778,7 +773,7 @@ class Instruction:
new_state = copy(global_state) new_state = copy(global_state)
new_state.mstate.pc = index new_state.mstate.pc = index
new_state.mstate.depth += 1 new_state.mstate.depth += 1
new_state.mstate.constraints.append(condi) new_state.mstate.constraints.append(simplify(condi))
states.append(new_state) states.append(new_state)
else: else:
@ -786,12 +781,11 @@ class Instruction:
# False case # False case
negated = Not(condition) if type(condition) == BoolRef else condition == 0 negated = Not(condition) if type(condition) == BoolRef else condition == 0
sat = not is_false(simplify(negated)) if type(condi) == BoolRef else not negated
if sat: if (type(negated) == bool and negated) or (type(negated) == BoolRef and not is_false(simplify(negated))):
new_state = copy(global_state) new_state = copy(global_state)
new_state.mstate.depth += 1 new_state.mstate.depth += 1
new_state.mstate.constraints.append(negated) new_state.mstate.constraints.append(simplify(negated))
states.append(new_state) states.append(new_state)
else: else:
logging.debug("Pruned unreachable states.") logging.debug("Pruned unreachable states.")

@ -1,12 +1,13 @@
from z3 import BitVec, BitVecVal from z3 import BitVec, BitVecVal
from copy import copy, deepcopy from copy import copy, deepcopy
from enum import Enum from enum import Enum
from random import random
class CalldataType(Enum): class CalldataType(Enum):
CONCRETE = 1 CONCRETE = 1
SYMBOLIC = 2 SYMBOLIC = 2
class Account: class Account:
""" """
Account class representing ethereum accounts Account class representing ethereum accounts
@ -31,8 +32,20 @@ class Account:
def __str__(self): def __str__(self):
return str(self.as_dict) return str(self.as_dict)
def get_storage(self, index): def set_balance(self, balance):
return self.storage[index] if index in self.storage.keys() else BitVec("storage_" + str(index), 256) self.balance = balance
def add_balance(self, balance):
self.balance += balance
# def get_storage(self, index):
# return BitVec("storage_" + str(index), 256)
# if index in self.storage.keys():
# return self.storage[index]
# else:
# symbol = BitVec("storage_" + str(index), 256)
# self.storage[index] = symbol
# return symbol
@property @property
def as_dict(self): def as_dict(self):
@ -71,6 +84,7 @@ class Environment:
def __str__(self): def __str__(self):
return str(self.as_dict) return str(self.as_dict)
@property @property
def as_dict(self): def as_dict(self):
return dict(active_account=self.active_account, sender=self.sender, calldata=self.calldata, return dict(active_account=self.active_account, sender=self.sender, calldata=self.calldata,
@ -138,7 +152,7 @@ class GlobalState:
def __copy__(self): def __copy__(self):
accounts = self.accounts accounts = copy(self.accounts)
environment = copy(self.environment) environment = copy(self.environment)
mstate = deepcopy(self.mstate) mstate = deepcopy(self.mstate)
call_stack = copy(self.call_stack) call_stack = copy(self.call_stack)
@ -149,3 +163,55 @@ class GlobalState:
""" Gets the current instruction for this GlobalState""" """ Gets the current instruction for this GlobalState"""
instructions = self.environment.code.instruction_list instructions = self.environment.code.instruction_list
return instructions[self.mstate.pc] return instructions[self.mstate.pc]
class WorldState:
"""
The WorldState class represents the world state as described in the yellow paper
"""
def __init__(self):
"""
Constructor for the world state. Initializes the accounts record
"""
self.accounts = {}
self.node = None
def __getitem__(self, item):
"""
Gets an account from the worldstate using item as key
:param item: Address of the account to get
:return: Account associated with the address
"""
return self.accounts[item]
def create_account(self, balance=0):
"""
Create non-contract account
:param balance: Initial balance for the account
:return: The new account
"""
new_account = Account(self._generate_new_address(), balance=balance)
self._put_account(new_account)
return new_account
def create_initialized_contract_account(self, contract_code, storage):
"""
Creates a new contract account, based on the contract code and storage provided
The contract code only includes the runtime contract bytecode
:param contract_code: Runtime bytecode for the contract
:param storage: Initial storage for the contract
:return: The new account
"""
new_account = Account(self._generate_new_address(), code=contract_code, balance=0)
new_account.storage = storage
self._put_account(new_account)
def _generate_new_address(self):
""" Generates a new address for the global state"""
while True:
address = '0x' + ''.join([str(hex(random(0, 16)))[-1] for _ in range(20)])
if address not in self.accounts.keys():
return address
def _put_account(self, account):
self.accounts[account.address] = account

@ -1,6 +1,7 @@
from z3 import BitVec from z3 import BitVec
import logging import logging
from mythril.laser.ethereum.state import GlobalState, Environment, CalldataType, Account from mythril.laser.ethereum.state import GlobalState, Environment, CalldataType, Account, WorldState
from mythril.laser.ethereum.transaction import MessageCall
from mythril.laser.ethereum.instructions import Instruction from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType
from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy
@ -28,7 +29,12 @@ class LaserEVM:
def __init__(self, accounts, dynamic_loader=None, max_depth=float('inf'), execution_timeout=60, def __init__(self, accounts, dynamic_loader=None, max_depth=float('inf'), execution_timeout=60,
strategy=DepthFirstSearchStrategy): strategy=DepthFirstSearchStrategy):
self.instructions_covered = [] self.instructions_covered = []
self.accounts = accounts
world_state = WorldState()
world_state.accounts = accounts
# this sets the initial world state
self.world_state = world_state
self.open_states = [world_state]
self.nodes = {} self.nodes = {}
self.edges = [] self.edges = []
@ -51,35 +57,12 @@ class LaserEVM:
logging.debug("Starting LASER execution") logging.debug("Starting LASER execution")
self.time = datetime.now() self.time = datetime.now()
# Initialize the execution environment transaction = MessageCall(main_address)
environment = Environment( transaction.run(self.open_states, self)
self.accounts[main_address],
BitVec("caller", 256),
[],
BitVec("gasprice", 256),
BitVec("callvalue", 256),
BitVec("origin", 256),
calldata_type=CalldataType.SYMBOLIC,
)
self.instructions_covered = [False for _ in environment.code.instruction_list]
initial_node = Node(environment.active_account.contract_name)
self.nodes[initial_node.uid] = initial_node
global_state = GlobalState(self.accounts, environment, initial_node)
global_state.environment.active_function_name = "fallback()"
initial_node.states.append(global_state)
# Empty the work_list before starting an execution
self.work_list.append(global_state)
self._sym_exec()
logging.info("Execution complete")
logging.info("Achieved {0:.3g}% coverage".format(self.coverage))
logging.info("%d nodes, %d edges, %d total states", len(self.nodes), len(self.edges), self.total_states) logging.info("%d nodes, %d edges, %d total states", len(self.nodes), len(self.edges), self.total_states)
def _sym_exec(self): def exec(self):
for global_state in self.strategy: for global_state in self.strategy:
if self.execution_timeout: if self.execution_timeout:
if self.time + timedelta(seconds=self.execution_timeout) <= datetime.now(): if self.time + timedelta(seconds=self.execution_timeout) <= datetime.now():
@ -87,9 +70,16 @@ class LaserEVM:
try: try:
new_states, op_code = self.execute_state(global_state) new_states, op_code = self.execute_state(global_state)
except NotImplementedError: except NotImplementedError:
logging.debug("Encountered unimplemented instruction") logging.info("Encountered unimplemented instruction: {}".format(op_code))
continue continue
if len(new_states) == 0:
# TODO: let global state use worldstate
open_world_state = WorldState()
open_world_state.accounts = global_state.accounts
open_world_state.node = global_state.node
self.open_states.append(open_world_state)
self.manage_cfg(op_code, new_states) self.manage_cfg(op_code, new_states)
self.work_list += new_states self.work_list += new_states

@ -118,7 +118,7 @@ class TaintRunner:
direct_children = [statespace.nodes[edge.node_to] for edge in statespace.edges if edge.node_from == node.uid] direct_children = [statespace.nodes[edge.node_to] for edge in statespace.edges if edge.node_from == node.uid]
children = [] children = []
for child in direct_children: for child in direct_children:
if child.states[0].environment.active_account == environment.active_account: if child.states[0].environment.active_account.address == environment.active_account.address:
children.append(child) children.append(child)
else: else:
children += TaintRunner.children(child, statespace, environment) children += TaintRunner.children(child, statespace, environment)

@ -0,0 +1,60 @@
import logging
from mythril.laser.ethereum.state import GlobalState, Environment, CalldataType
from mythril.laser.ethereum.cfg import Node, Edge, JumpType
from z3 import BitVec
class MessageCall:
""" Represents a call value transaction """
def __init__(self, callee_address):
"""
Constructor for Call transaction, sets up all symbolic parameters
:param callee_address: Address of the contract that will be called
"""
self.callee_address = callee_address
self.caller = BitVec("caller", 256)
self.gas_price = BitVec("gasprice", 256)
self.call_value = BitVec("callvalue", 256)
self.origin = BitVec("origin", 256)
self.open_states = None
@property
def has_ran(self):
return self.open_states is not None
def run(self, open_world_states: list, evm):
""" Runs this transaction on the evm starting from the open world states """
# Consume the open states
open_states = open_world_states[:]
del open_world_states[:]
for open_world_state in open_states:
# Initialize the execution environment
environment = Environment(
open_world_state[self.callee_address],
self.caller,
[],
self.gas_price,
self.call_value,
self.origin,
calldata_type=CalldataType.SYMBOLIC,
)
new_node = Node(environment.active_account.contract_name)
evm.instructions_covered = [False for _ in environment.code.instruction_list]
evm.nodes[new_node.uid] = new_node
if open_world_state.node:
evm.edges.append(Edge(open_world_state.node.uid, new_node.uid, edge_type=JumpType.Transaction, condition=None))
global_state = GlobalState(open_world_state.accounts, environment, new_node)
new_node.states.append(global_state)
evm.work_list.append(global_state)
evm.exec()
logging.info("Execution complete")
logging.info("Achieved {0:.3g}% coverage".format(evm.coverage))

@ -1,13 +1,11 @@
import plyvel
import binascii import binascii
import rlp import rlp
import hashlib
import logging import logging
from ethereum import utils from ethereum import utils
from ethereum.block import BlockHeader, Block from ethereum.block import BlockHeader, Block
from mythril.leveldb.state import State, Account from mythril.leveldb.state import State
from mythril.leveldb.eth_db import ETH_DB from mythril.leveldb.eth_db import ETH_DB
from mythril.ether.ethcontract import ETHContract, InstanceList from mythril.ether.ethcontract import ETHContract
# Per https://github.com/ethereum/go-ethereum/blob/master/core/database_util.go # Per https://github.com/ethereum/go-ethereum/blob/master/core/database_util.go
# prefixes and suffixes for keys in geth # prefixes and suffixes for keys in geth
@ -18,18 +16,21 @@ blockHashPrefix = b'H' # blockHashPrefix + hash -> num (uint64 big endian)
# known geth keys # known geth keys
headHeaderKey = b'LastBlock' # head (latest) header hash headHeaderKey = b'LastBlock' # head (latest) header hash
def _formatBlockNumber(number): def _formatBlockNumber(number):
''' '''
formats block number to uint64 big endian formats block number to uint64 big endian
''' '''
return utils.zpad(utils.int_to_big_endian(number), 8) return utils.zpad(utils.int_to_big_endian(number), 8)
def _encode_hex(v): def _encode_hex(v):
''' '''
encodes hash as hex encodes hash as hex
''' '''
return '0x' + utils.encode_hex(v) return '0x' + utils.encode_hex(v)
class EthLevelDB(object): class EthLevelDB(object):
''' '''
Go-Ethereum LevelDB client class Go-Ethereum LevelDB client class

@ -47,7 +47,7 @@ IDENTITY_TEST[3] = (83269476937987, False)
def _all_info(laser): def _all_info(laser):
accounts = {} accounts = {}
for address, _account in laser.accounts.items(): for address, _account in laser.world_state.accounts.items():
account = _account.as_dict account = _account.as_dict
account["code"] = account["code"].instruction_list account["code"] = account["code"].instruction_list
account['balance'] = str(account['balance']) account['balance'] = str(account['balance'])

@ -18,7 +18,7 @@ class LaserEncoder(json.JSONEncoder):
def _all_info(laser): def _all_info(laser):
accounts = {} accounts = {}
for address, _account in laser.accounts.items(): for address, _account in laser.world_state.accounts.items():
account = _account.as_dict account = _account.as_dict
account["code"] = account["code"].instruction_list account["code"] = account["code"].instruction_list
account['balance'] = str(account['balance']) account['balance'] = str(account['balance'])

Loading…
Cancel
Save