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