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. 2
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json
  9. 2
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json
  10. 2
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json

@ -32,7 +32,7 @@ Install from Pypi:
$ 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
@ -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).

@ -1,21 +1,16 @@
"""This module contains the detection code for unauthorized ether
withdrawal."""
import logging
import json
from copy import copy
from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.laser.ethereum.transaction.symbolic import (
ATTACKER_ADDRESS,
CREATOR_ADDRESS,
)
from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.transaction import ContractCreationTransaction
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__)
@ -74,54 +69,40 @@ class EtherThief(DetectionModule):
:param state:
:return:
"""
state = copy(state)
instruction = state.get_current_instruction()
if instruction["opcode"] != "CALL":
return []
value = state.mstate.stack[-3]
target = state.mstate.stack[-2]
eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256)
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
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 += [
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,
state.current_transaction.caller == ATTACKER_ADDRESS,
]
try:
transaction_sequence = solver.get_transaction_sequence(state, constraints)
issue = Issue(

@ -95,7 +95,7 @@ def get_transaction_sequence(
concrete_transactions = []
tx_constraints, minimize = _set_minimisation_constraints(
transaction_sequence, constraints.copy(), [], 5000
transaction_sequence, constraints.copy(), [], 5000, global_state.world_state
)
try:
@ -103,19 +103,23 @@ def get_transaction_sequence(
except 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:
concrete_transaction = _get_concrete_transaction(model, transaction)
concrete_transactions.append(concrete_transaction)
caller = concrete_transaction["origin"]
value = int(concrete_transaction["value"], 16)
min_price_dict[caller] = min_price_dict.get(caller, 0) + value
if isinstance(transaction_sequence[0], ContractCreationTransaction):
initial_accounts = transaction_sequence[0].prev_world_state.accounts
else:
initial_accounts = transaction_sequence[0].world_state.accounts
min_price_dict = {} # type: Dict[str, int]
for address in initial_accounts.keys():
min_price_dict[address] = model.eval(
initial_world_state.starting_balances[
symbol_factory.BitVecVal(address, 256)
].raw,
model_completion=True,
).as_long()
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(
transaction_sequence, constraints, minimize, max_size
transaction_sequence, constraints, minimize, max_size, world_state
) -> Tuple[Constraints, tuple]:
""" Set constraints that minimise key transaction values
@ -192,6 +196,22 @@ def _set_minimisation_constraints(
# Minimize
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)

@ -49,7 +49,11 @@ def get_call_parameters(
callee_account = None
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(
global_state, callee_address, dynamic_loader
)
@ -84,12 +88,11 @@ def get_callee_address(
except TypeError:
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)))
if match is None or dynamic_loader is None:
raise ValueError()
return symbolic_to_address
index = int(match.group(1))
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
)
# TODO: verify whether this happens or not
except Exception:
log.debug("Error accessing contract storage.")
raise ValueError
except:
return symbolic_to_address
# testrpc simply returns the address, geth response is more elaborate.
if not re.match(r"^0x[0-9a-f]{40}$", callee_address):
@ -112,7 +114,9 @@ def get_callee_address(
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.
@ -123,6 +127,12 @@ def get_callee_account(
"""
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:
return global_state.accounts[int(callee_address, 16)]
except KeyError:
@ -209,12 +219,12 @@ def get_call_data(
def native_call(
global_state: GlobalState,
callee_address: str,
callee_address: Union[str, BitVec],
call_data: BaseCalldata,
memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression],
) -> Optional[List[GlobalState]]:
if not 0 < int(callee_address, 16) < 5:
) -> Union[List[GlobalState], None]:
if isinstance(callee_address, BitVec) or not 0 < int(callee_address, 16) < 5:
return None
log.debug("Native contract called: " + callee_address)

@ -25,6 +25,7 @@ from mythril.laser.smt import (
Bool,
Not,
LShR,
UGE,
)
from mythril.laser.smt import symbol_factory
@ -56,6 +57,30 @@ TT256 = 2 ** 256
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):
"""Decorator that handles global state copy and original return.
@ -842,7 +867,11 @@ class Instruction:
"""
state = global_state.mstate
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]
@StateTransition()
@ -1629,7 +1658,7 @@ class Instruction:
transfer_amount = global_state.environment.active_account.balance()
# Often the target of the suicide instruction will be symbolic
# 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
@ -1703,6 +1732,10 @@ class Instruction:
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)
)
@ -1818,6 +1851,18 @@ class Instruction:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters(
global_state, self.dynamic_loader, True
)
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
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:
log.debug(
"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(
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:
log.debug(
"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(
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:
log.debug(
"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.balances = Array("balance", 256, 256)
self.starting_balances = copy(self.balances)
self.node = None # type: Optional['Node']
self.transaction_sequence = transaction_sequence or []
@ -60,6 +61,7 @@ class WorldState:
annotations=new_annotations,
)
new_world_state.balances = copy(self.balances)
new_world_state.starting_balances = copy(self.starting_balances)
for account in self._accounts.values():
new_world_state.put_account(copy(account))
new_world_state.node = self.node
@ -115,7 +117,7 @@ class WorldState:
concrete_storage=concrete_storage,
)
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)
return new_account

@ -4,7 +4,7 @@ execution."""
import array
from copy import deepcopy
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.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.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.smt import symbol_factory
from mythril.laser.smt import symbol_factory, UGE, BitVec
import logging
log = logging.getLogger(__name__)
_next_transaction_id = 0
@ -115,10 +117,17 @@ class BaseTransaction:
sender = environment.sender
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[sender] -= value
return global_state
@ -180,7 +189,6 @@ class ContractCreationTransaction(BaseTransaction):
0, concrete_storage=True, creator=caller.value
)
callee_account.contract_name = contract_name
# TODO: set correct balance for new account
super().__init__(
world_state=world_state,
callee_account=callee_account,

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

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

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

Loading…
Cancel
Save