def get_caller_cond(condition): # checks if the condition has this format: # (EQ (MASK_SHL, 160, 0, 0, 'CALLER'), (STORAGE, size, offset, stor_num)) # if it does, returns the storage data # # also, if condition is IS_ZERO(EQ ...), it turns it into just (EQ ...) # -- technically not correct, but this is a hackathon project, should be good enough :) if opcode(condition) != 'EQ': return None if condition[1] == ('MASK_SHL', 160, 0, 0, 'CALLER'): stor = condition[2] elif condition[2] == ('MASK_SHL', 160, 0, 0, 'CALLER'): stor = condition[1] else: return None if opcode(stor) == 'STORAGE' and len(stor) == 4: # len(stor) == 5 -> indexed storage array, not handling those now return stor elif type(stor) == int: return hex(stor) else: return 'unknown'
def walk_trace(trace, f=print, knows_true=None): ''' walks the trace, calling function f(line, knows_true) for every line knows_true is a list of 'if' conditions that had to be met to reach a given line ''' res = [] knows_true = knows_true or [] for line in trace: found = f(line, knows_true) if found is not None: res.append(found) if opcode(line) == 'IF': condition, if_true, if_false = line[1:] res.extend(walk_trace(if_true, f, knows_true + [condition])) res.extend( walk_trace(if_false, f, knows_true + [is_zero(condition)])) continue if opcode(line) == 'WHILE': condition, trace = line[1:] res.extend(walk_trace(trace, f, knows_true + [is_zero(condition)])) continue if opcode(line) == 'LOOP': trace, label = line[1:] res.extend(walk_trace(trace, f, knows_true)) return res
def find_calls(line, _): # todo: delegatecalls # todo: selfdestructs if opcode(line) != 'CALL': return None _, addr, wei, _, _, _, _, f_name, f_params = line[1:] if addr == ('MASK_SHL', 160, 0, 0, 'CALLER'): # WARN: should check for knows_true, perhaps a caller can only be someone specific addr = 'anyone' elif opcode(addr) != 'STORAGE' or len(addr) > 4: addr = 'unknown' return (addr, wei, f_name, f_params)
def find_stor_req(line, knows_true): # for every line, check if it's (STORE, size, offset, stor_num, _, some value) # if it is, it means that the line writes to storage... if opcode(line) != 'STORE': return None _, size, offset, stor_num, arr_idx, value = line if len(arr_idx) > 0: # we're dealing only with storages that are not arrays for now return None # ok, so it's a storage write - let's backtrack through all the IFs we encountered # before, and see if there were checks for callers there callers = [] for cond in knows_true: caller = get_caller_cond(cond) if caller is not None: callers.append(caller) if len(callers) == 0: callers = ['anyone'] return ('STORAGE', size, offset, stor_num), callers
def add_role(name=None, value=None, definition=None): global roles if name is None: assert definition is not None if opcode(definition) == 'STORAGE': name = f'stor_{definition[3]}' else: name = str(definition) s = definition or name if s in roles: return roles[s] = { 'name': name, 'definition': definition, 'setters': list(), 'funcs': set(), 'withdrawals': set(), 'calls': set(), 'value': value, 'destructs': set(), 'destructs_init': set(), }
def find_caller_req(line, _): # finds IFs: (IF (EQ caller, storage)) if opcode(line) != 'IF': return None condition, if_true, if_false = line[1:] return get_caller_cond(condition) or get_caller_cond(is_zero(condition))
def walk_trace(trace, f=print, knows_true=None): ''' walks the trace, calling function f(line, knows_true) for every line knows_true is a list of 'if' conditions that had to be met to reach a given line ''' res = [] knows_true = knows_true or [] for idx, line in enumerate(trace): found = f(line, knows_true) if found is not None: res.append(found) if opcode(line) == 'IF': condition, if_true, if_false = line[1:] res.extend(walk_trace(if_true, f, knows_true + [condition])) res.extend( walk_trace(if_false, f, knows_true + [is_zero(condition)])) assert idx == len( trace) - 1, trace # IFs always end the trace tree break if opcode(line) == 'WHILE': condition, while_trace = line[1:] res.extend( walk_trace(while_trace, f, knows_true + [is_zero(condition)])) continue if opcode(line) == 'LOOP': loop_trace, label = line[1:] res.extend(walk_trace(loop_trace, f, knows_true)) return res
def find_destructs(line, knows_true): # todo: delegatecalls # todo: selfdestructs if opcode(line) != 'SELFDESTRUCT': return None receiver = line[1] if receiver == ('MASK_SHL', 160, 0, 0, 'CALLER'): receiver = 'anyone' elif opcode(receiver) != 'STORAGE' or len(receiver) > 4: receiver = 'unknown' callers = [] for cond in knows_true: caller = get_caller_cond(cond) if caller is not None: callers.append(caller) if len(callers) == 0: callers = ['anyone'] return receiver, callers
def get_caller_cond(condition): if opcode(condition) != 'EQ': if opcode(condition) != 'ISZERO': return None else: condition = condition[1] if opcode(condition) != 'EQ': return None if condition[1] == ('MASK_SHL', 160, 0, 0, 'CALLER'): stor = condition[2] elif condition[2] == ('MASK_SHL', 160, 0, 0, 'CALLER'): stor = condition[1] else: return None if opcode(stor) == 'STORAGE' and len(stor) == 4: # len(stor) == 5 -> indexed storage array, not handling those now return stor else: return None
def find_stor_req(line, knows_true): if opcode(line) != 'STORE': return None size, offset, stor_num, arr_idx, value = line[1:] if len(arr_idx) > 0: # we're dealing only with storages that are not arrays return None callers = [] for cond in knows_true: caller = get_caller_cond(cond) if caller is not None: callers.append(caller) if len(callers) == 0: callers = ['anyone'] return ('STORAGE', size, offset, stor_num), callers
def find_opcodes(line, _): return opcode(line)
callers.append(caller) if len(callers) == 0: callers = ['anyone'] return ('STORAGE', size, offset, stor_num), callers for f in functions.values(): for (stor, callers) in walk_trace(f['trace'], find_stor_req): affected_roles = set() for r in roles: if opcode(r) != 'STORAGE': continue assert opcode(stor) == 'STORAGE' _, stor_size, stor_offset, stor_num = stor[:4] _, role_size, role_offset, role_num = r[:4] if stor_num == role_num and \ role_offset <= stor_offset < role_offset + role_size: affected_roles.add(r) # ^ for a role (STORAGE, 160, 0, 1), will catch those: # # (STORE, 160, 0, 1, value) (exact match)
def find_storages(exp): if opcode(exp) == 'STORAGE': return exp if opcode(exp) == 'STORE': return ('STORAGE', ) + exp[1:4]
callers = ['anyone'] return ('STORAGE', size, offset, stor_num), callers for f in functions.values(): trace = f['trace'] res = walk_trace(trace, find_stor_req) if len(res) > 0: for (stor, callers) in res: affected_roles = set() for r in roles: if opcode(r) != 'STORAGE': continue stor_offset, stor_size, stor_num = stor[2], stor[1], stor[3] role_offset, role_size, role_num = r[2], r[1], r[3] if stor_offset >= role_offset and stor_offset < role_offset + role_size and stor_num == role_num: affected_roles.add(r) # ^ we can't compare roles to storage writes, because that would miss all the partial writes # to a given storage. see 'digix' contract, and how setOwner is set there setter = (callers, f['name']) for role_id in affected_roles: if setter not in roles[role_id]['setters']: