Merge branch 'develop' into pending-opcodes

pending-opcodes
e-ngo 5 years ago
commit ccdc1759aa
  1. 4
      README.md
  2. 57
      mythril/analysis/modules/ether_thief.py
  3. 44
      mythril/analysis/solver.py
  4. 32
      mythril/laser/ethereum/call.py
  5. 73
      mythril/laser/ethereum/instructions.py
  6. 4
      mythril/laser/ethereum/state/world_state.py
  7. 20
      mythril/laser/ethereum/transaction/transaction_models.py
  8. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json
  9. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json
  10. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json

@ -32,7 +32,7 @@ Install from Pypi:
$ pip3 install mythril $ pip3 install mythril
``` ```
See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup) for more detailed instructions. See the [docs](https://mythril-classic.readthedocs.io/en/master/installation.html) for more detailed instructions.
## Usage ## Usage
@ -75,7 +75,7 @@ Caller: [ATTACKER], function: commencekilling(), txdata: 0x7c11da20, value: 0x0
``` ```
Instructions for using Mythril are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki). Instructions for using Mythril are found on the [docs](https://mythril-classic.readthedocs.io/en/master/).
For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG). For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG).

@ -1,21 +1,16 @@
"""This module contains the detection code for unauthorized ether """This module contains the detection code for unauthorized ether
withdrawal.""" withdrawal."""
import logging import logging
import json
from copy import copy from copy import copy
from mythril.analysis import solver from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue from mythril.analysis.report import Issue
from mythril.laser.ethereum.transaction.symbolic import ( from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS
ATTACKER_ADDRESS,
CREATOR_ADDRESS,
)
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError from mythril.exceptions import UnsatError
from mythril.laser.ethereum.transaction import ContractCreationTransaction
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import UGT, Sum, symbol_factory, BVAddNoOverflow, If from mythril.laser.smt import UGT, symbol_factory, UGE
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -74,54 +69,40 @@ class EtherThief(DetectionModule):
:param state: :param state:
:return: :return:
""" """
state = copy(state)
instruction = state.get_current_instruction() instruction = state.get_current_instruction()
if instruction["opcode"] != "CALL":
return []
value = state.mstate.stack[-3] value = state.mstate.stack[-3]
target = state.mstate.stack[-2] target = state.mstate.stack[-2]
eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256)
constraints = copy(state.mstate.constraints) constraints = copy(state.mstate.constraints)
for tx in state.world_state.transaction_sequence:
"""
Constraint: The call value must be greater than the sum of Ether sent by the attacker over all
transactions. This prevents false positives caused by legitimate refund functions.
Also constrain the addition from overflowing (otherwise the solver produces solutions with
ridiculously high call values).
"""
constraints += [
BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, signed=False)
]
eth_sent_by_attacker = Sum(
eth_sent_by_attacker,
tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0),
)
"""
Constraint: All transactions must originate from regular users (not the creator/owner).
This prevents false positives where the owner willingly transfers ownership to another address.
"""
if not isinstance(tx, ContractCreationTransaction):
constraints += [tx.caller != CREATOR_ADDRESS]
""" """
Require that the current transaction is sent by the attacker and Require that the current transaction is sent by the attacker and
that the Ether is sent to the attacker's address. that the Ether sent to the attacker's address is greater than the
amount of Ether the attacker sent.
""" """
attacker_address_bitvec = symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256)
constraints += [
UGE(
state.world_state.balances[state.environment.active_account.address],
value,
)
]
state.world_state.balances[attacker_address_bitvec] += value
state.world_state.balances[state.environment.active_account.address] -= value
constraints += [ constraints += [
UGT(value, eth_sent_by_attacker), UGT(
state.world_state.balances[attacker_address_bitvec],
state.world_state.starting_balances[attacker_address_bitvec],
),
target == ATTACKER_ADDRESS, target == ATTACKER_ADDRESS,
state.current_transaction.caller == ATTACKER_ADDRESS, state.current_transaction.caller == ATTACKER_ADDRESS,
] ]
try: try:
transaction_sequence = solver.get_transaction_sequence(state, constraints) transaction_sequence = solver.get_transaction_sequence(state, constraints)
issue = Issue( issue = Issue(

@ -95,7 +95,7 @@ def get_transaction_sequence(
concrete_transactions = [] concrete_transactions = []
tx_constraints, minimize = _set_minimisation_constraints( tx_constraints, minimize = _set_minimisation_constraints(
transaction_sequence, constraints.copy(), [], 5000 transaction_sequence, constraints.copy(), [], 5000, global_state.world_state
) )
try: try:
@ -103,19 +103,23 @@ def get_transaction_sequence(
except UnsatError: except UnsatError:
raise UnsatError raise UnsatError
min_price_dict = {} # type: Dict[str, int] # Include creation account in initial state
# Note: This contains the code, which should not exist until after the first tx
initial_world_state = transaction_sequence[0].world_state
initial_accounts = initial_world_state.accounts
for transaction in transaction_sequence: for transaction in transaction_sequence:
concrete_transaction = _get_concrete_transaction(model, transaction) concrete_transaction = _get_concrete_transaction(model, transaction)
concrete_transactions.append(concrete_transaction) concrete_transactions.append(concrete_transaction)
caller = concrete_transaction["origin"] min_price_dict = {} # type: Dict[str, int]
value = int(concrete_transaction["value"], 16) for address in initial_accounts.keys():
min_price_dict[caller] = min_price_dict.get(caller, 0) + value min_price_dict[address] = model.eval(
initial_world_state.starting_balances[
if isinstance(transaction_sequence[0], ContractCreationTransaction): symbol_factory.BitVecVal(address, 256)
initial_accounts = transaction_sequence[0].prev_world_state.accounts ].raw,
else: model_completion=True,
initial_accounts = transaction_sequence[0].world_state.accounts ).as_long()
concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict) concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict)
@ -171,7 +175,7 @@ def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction):
def _set_minimisation_constraints( def _set_minimisation_constraints(
transaction_sequence, constraints, minimize, max_size transaction_sequence, constraints, minimize, max_size, world_state
) -> Tuple[Constraints, tuple]: ) -> Tuple[Constraints, tuple]:
""" Set constraints that minimise key transaction values """ Set constraints that minimise key transaction values
@ -192,6 +196,22 @@ def _set_minimisation_constraints(
# Minimize # Minimize
minimize.append(transaction.call_data.calldatasize) minimize.append(transaction.call_data.calldatasize)
minimize.append(transaction.call_value)
constraints.append(
UGE(
symbol_factory.BitVecVal(1000000000000000000000, 256),
world_state.starting_balances[transaction.caller],
)
)
for account in world_state.accounts.values():
# Lazy way to prevent overflows and to ensure "reasonable" balances
# Each account starts with less than 100 ETH
constraints.append(
UGE(
symbol_factory.BitVecVal(100000000000000000000, 256),
world_state.starting_balances[account.address],
)
)
return constraints, tuple(minimize) return constraints, tuple(minimize)

@ -49,7 +49,11 @@ def get_call_parameters(
callee_account = None callee_account = None
call_data = get_call_data(global_state, memory_input_offset, memory_input_size) call_data = get_call_data(global_state, memory_input_offset, memory_input_size)
if int(callee_address, 16) >= 5 or int(callee_address, 16) == 0: if (
isinstance(callee_address, BitVec)
or int(callee_address, 16) >= 5
or int(callee_address, 16) == 0
):
callee_account = get_callee_account( callee_account = get_callee_account(
global_state, callee_address, dynamic_loader global_state, callee_address, dynamic_loader
) )
@ -84,12 +88,11 @@ def get_callee_address(
except TypeError: except TypeError:
log.debug("Symbolic call encountered") log.debug("Symbolic call encountered")
# TODO: This is broken. Now it should be Storage[(\d+)]. match = re.search(r"Storage\[(\d+)\]", str(simplify(symbolic_to_address)))
match = re.search(r"storage_(\d+)", str(simplify(symbolic_to_address)))
log.debug("CALL to: " + str(simplify(symbolic_to_address))) log.debug("CALL to: " + str(simplify(symbolic_to_address)))
if match is None or dynamic_loader is None: if match is None or dynamic_loader is None:
raise ValueError() return symbolic_to_address
index = int(match.group(1)) index = int(match.group(1))
log.debug("Dynamic contract address at storage index {}".format(index)) log.debug("Dynamic contract address at storage index {}".format(index))
@ -100,9 +103,8 @@ def get_callee_address(
"0x{:040X}".format(environment.active_account.address.value), index "0x{:040X}".format(environment.active_account.address.value), index
) )
# TODO: verify whether this happens or not # TODO: verify whether this happens or not
except Exception: except:
log.debug("Error accessing contract storage.") return symbolic_to_address
raise ValueError
# testrpc simply returns the address, geth response is more elaborate. # testrpc simply returns the address, geth response is more elaborate.
if not re.match(r"^0x[0-9a-f]{40}$", callee_address): if not re.match(r"^0x[0-9a-f]{40}$", callee_address):
@ -112,7 +114,9 @@ def get_callee_address(
def get_callee_account( def get_callee_account(
global_state: GlobalState, callee_address: str, dynamic_loader: DynLoader global_state: GlobalState,
callee_address: Union[str, BitVec],
dynamic_loader: DynLoader,
): ):
"""Gets the callees account from the global_state. """Gets the callees account from the global_state.
@ -123,6 +127,12 @@ def get_callee_account(
""" """
accounts = global_state.accounts accounts = global_state.accounts
if isinstance(callee_address, BitVec):
if callee_address.symbolic:
return Account(callee_address, balances=global_state.world_state.balances)
else:
callee_address = hex(callee_address.value)[2:]
try: try:
return global_state.accounts[int(callee_address, 16)] return global_state.accounts[int(callee_address, 16)]
except KeyError: except KeyError:
@ -209,12 +219,12 @@ def get_call_data(
def native_call( def native_call(
global_state: GlobalState, global_state: GlobalState,
callee_address: str, callee_address: Union[str, BitVec],
call_data: BaseCalldata, call_data: BaseCalldata,
memory_out_offset: Union[int, Expression], memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression], memory_out_size: Union[int, Expression],
) -> Optional[List[GlobalState]]: ) -> Union[List[GlobalState], None]:
if not 0 < int(callee_address, 16) < 5: if isinstance(callee_address, BitVec) or not 0 < int(callee_address, 16) < 5:
return None return None
log.debug("Native contract called: " + callee_address) log.debug("Native contract called: " + callee_address)

@ -25,6 +25,7 @@ from mythril.laser.smt import (
Bool, Bool,
Not, Not,
LShR, LShR,
UGE,
) )
from mythril.laser.smt import symbol_factory from mythril.laser.smt import symbol_factory
@ -56,6 +57,30 @@ TT256 = 2 ** 256
TT256M1 = 2 ** 256 - 1 TT256M1 = 2 ** 256 - 1
def transfer_ether(
global_state: GlobalState,
sender: BitVec,
receiver: BitVec,
value: Union[int, BitVec],
):
"""
Perform an Ether transfer between two accounts
:param global_state: The global state in which the Ether transfer occurs
:param sender: The sender of the Ether
:param receiver: The recipient of the Ether
:param value: The amount of Ether to send
:return:
"""
value = value if isinstance(value, BitVec) else symbol_factory.BitVecVal(value, 256)
global_state.mstate.constraints.append(
UGE(global_state.world_state.balances[sender], value)
)
global_state.world_state.balances[receiver] += value
global_state.world_state.balances[sender] -= value
class StateTransition(object): class StateTransition(object):
"""Decorator that handles global state copy and original return. """Decorator that handles global state copy and original return.
@ -842,7 +867,11 @@ class Instruction:
""" """
state = global_state.mstate state = global_state.mstate
address = state.stack.pop() address = state.stack.pop()
state.stack.append(global_state.new_bitvec("balance_at_" + str(address), 256))
balance = global_state.world_state.balances[
global_state.environment.active_account.address
]
state.stack.append(balance)
return [global_state] return [global_state]
@StateTransition() @StateTransition()
@ -1629,7 +1658,7 @@ class Instruction:
transfer_amount = global_state.environment.active_account.balance() transfer_amount = global_state.environment.active_account.balance()
# Often the target of the suicide instruction will be symbolic # Often the target of the suicide instruction will be symbolic
# If it isn't then we'll transfer the balance to the indicated contract # If it isn't then we'll transfer the balance to the indicated contract
global_state.world_state[target].add_balance(transfer_amount) global_state.world_state.balances[target] += transfer_amount
global_state.environment.active_account = deepcopy( global_state.environment.active_account = deepcopy(
global_state.environment.active_account global_state.environment.active_account
@ -1703,6 +1732,10 @@ class Instruction:
if callee_account is not None and callee_account.code.bytecode == "": if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts") log.debug("The call is related to ether transfer between accounts")
sender = environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append( global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256) global_state.new_bitvec("retval_" + str(instr["address"]), 256)
) )
@ -1818,6 +1851,18 @@ class Instruction:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters(
global_state, self.dynamic_loader, True global_state, self.dynamic_loader, True
) )
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = global_state.environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
@ -1921,6 +1966,18 @@ class Instruction:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters(
global_state, self.dynamic_loader global_state, self.dynamic_loader
) )
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
@ -2022,6 +2079,18 @@ class Instruction:
callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader global_state, self.dynamic_loader
) )
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = global_state.environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(

@ -27,6 +27,7 @@ class WorldState:
""" """
self._accounts = {} # type: Dict[int, Account] self._accounts = {} # type: Dict[int, Account]
self.balances = Array("balance", 256, 256) self.balances = Array("balance", 256, 256)
self.starting_balances = copy(self.balances)
self.node = None # type: Optional['Node'] self.node = None # type: Optional['Node']
self.transaction_sequence = transaction_sequence or [] self.transaction_sequence = transaction_sequence or []
@ -60,6 +61,7 @@ class WorldState:
annotations=new_annotations, annotations=new_annotations,
) )
new_world_state.balances = copy(self.balances) new_world_state.balances = copy(self.balances)
new_world_state.starting_balances = copy(self.starting_balances)
for account in self._accounts.values(): for account in self._accounts.values():
new_world_state.put_account(copy(account)) new_world_state.put_account(copy(account))
new_world_state.node = self.node new_world_state.node = self.node
@ -115,7 +117,7 @@ class WorldState:
concrete_storage=concrete_storage, concrete_storage=concrete_storage,
) )
if balance: if balance:
new_account.set_balance(symbol_factory.BitVecVal(balance, 256)) new_account.add_balance(symbol_factory.BitVecVal(balance, 256))
self.put_account(new_account) self.put_account(new_account)
return new_account return new_account

@ -4,7 +4,7 @@ execution."""
import array import array
from copy import deepcopy from copy import deepcopy
from z3 import ExprRef from z3 import ExprRef
from typing import Union, Optional, cast from typing import Union, Optional
from mythril.laser.ethereum.state.calldata import ConcreteCalldata from mythril.laser.ethereum.state.calldata import ConcreteCalldata
from mythril.laser.ethereum.state.account import Account from mythril.laser.ethereum.state.account import Account
@ -12,8 +12,10 @@ from mythril.laser.ethereum.state.calldata import BaseCalldata, SymbolicCalldata
from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.state.world_state import WorldState
from mythril.disassembler.disassembly import Disassembly from mythril.laser.smt import symbol_factory, UGE, BitVec
from mythril.laser.smt import symbol_factory import logging
log = logging.getLogger(__name__)
_next_transaction_id = 0 _next_transaction_id = 0
@ -115,10 +117,17 @@ class BaseTransaction:
sender = environment.sender sender = environment.sender
receiver = environment.active_account.address receiver = environment.active_account.address
value = environment.callvalue value = (
environment.callvalue
if isinstance(environment.callvalue, BitVec)
else symbol_factory.BitVecVal(environment.callvalue, 256)
)
global_state.world_state.balances[sender] -= value global_state.mstate.constraints.append(
UGE(global_state.world_state.balances[sender], value)
)
global_state.world_state.balances[receiver] += value global_state.world_state.balances[receiver] += value
global_state.world_state.balances[sender] -= value
return global_state return global_state
@ -180,7 +189,6 @@ class ContractCreationTransaction(BaseTransaction):
0, concrete_storage=True, creator=caller.value 0, concrete_storage=True, creator=caller.value
) )
callee_account.contract_name = contract_name callee_account.contract_name = contract_name
# TODO: set correct balance for new account
super().__init__( super().__init__(
world_state=world_state, world_state=world_state,
callee_account=callee_account, callee_account=callee_account,

@ -47,7 +47,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -55,4 +55,4 @@
} }
} }
} }
} }

@ -54,7 +54,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -62,4 +62,4 @@
} }
} }
} }
} }

@ -47,7 +47,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -55,4 +55,4 @@
} }
} }
} }
} }

Loading…
Cancel
Save