def analyze_truffle_project(args): project_root = os.getcwd() build_dir = os.path.join(project_root, "build", "contracts") files = os.listdir(build_dir) for filename in files: if re.match(r'.*\.json$', filename) and filename != "Migrations.json": with open(os.path.join(build_dir, filename)) as cf: contractdata = json.load(cf) try: name = contractdata['contractName'] bytecode = contractdata['deployedBytecode'] except: print("Unable to parse contract data. Please use Truffle 4 to compile your project.") sys.exit() if (len(bytecode) < 4): continue ethcontract = ETHContract(bytecode, name=name) address = util.get_indexed_address(0) sym = SymExecWrapper(ethcontract, address, max_depth=10) issues = fire_lasers(sym) if not len(issues): if (args.outform == 'text' or args.outform == 'markdown'): print("# Analysis result for " + name + "\n\nNo issues found.") else: result = {'contract': name, 'result': {'success': True, 'error': None, 'issues': []}} print(json.dumps(result)) else: report = Report() # augment with source code disassembly = ethcontract.disassembly source = contractdata['source'] deployedSourceMap = contractdata['deployedSourceMap'].split(";") mappings = [] for item in deployedSourceMap: mapping = item.split(":") if len(mapping) > 0 and len(mapping[0]) > 0: offset = int(mapping[0]) if len(mapping) > 1 and len(mapping[1]) > 0: length = int(mapping[1]) if len(mapping) > 2 and len(mapping[2]) > 0: idx = int(mapping[2]) lineno = source[0:offset].count('\n') + 1 mappings.append(SourceMapping(idx, offset, length, lineno)) for issue in issues: index = helper.get_instruction_index(disassembly.instruction_list, issue.address) if index: try: offset = mappings[index].offset length = mappings[index].length issue.filename = filename issue.code = source[offset:offset + length] issue.lineno = mappings[index].lineno except IndexError: logging.debug("No code mapping at index %d", index) report.append_issue(issue) if (args.outform == 'json'): result = {'contract': name, 'result': {'success': True, 'error': None, 'issues': list(map(lambda x: x.as_dict(), issues))}} print(json.dumps(result)) else: if (args.outform == 'text'): print("# Analysis result for " + name + ":\n\n" + report.as_text()) elif (args.outform == 'markdown'): print(report.as_markdown())
def _sym_exec(self, gblState): environment = gblState.environment disassembly = environment.code state = gblState.mstate start_addr = disassembly.instruction_list[state.pc]['address'] node = Node(environment.active_account.contract_name, start_addr, copy.deepcopy(state.constraints)) if start_addr == 0: environment.active_function_name = "fallback" logging.debug("- Entering node " + str(node.uid) + ", index = " + str(state.pc) + ", address = " + str(start_addr) + ", depth = " + str(state.depth)) if start_addr in disassembly.addr_to_func: # Enter a new function environment.active_function_name = disassembly.addr_to_func[start_addr] node.flags |= NodeFlags.FUNC_ENTRY logging.info("- Entering function " + environment.active_account.contract_name + ":" + node.function_name) node.function_name = environment.active_function_name halt = False while not halt: try: instr = disassembly.instruction_list[state.pc] except IndexError: logging.debug("Invalid PC") return node # Save state before modifying anything node.states.append(gblState) gblState = LaserEVM.copy_global_state(gblState) state = gblState.mstate self.total_states += 1 # Point program counter to next instruction state.pc += 1 op = instr['opcode'] # logging.debug("[" + environment.active_account.contract_name + "] " + helper.get_trace_line(instr, state)) # slows down execution significantly. # Stack ops if op.startswith("PUSH"): value = BitVecVal(int(instr['argument'][2:], 16), 256) state.stack.append(value) elif op.startswith('DUP'): dpth = int(op[3:]) try: state.stack.append(state.stack[-dpth]) except: halt = True elif op.startswith('SWAP'): dpth = int(op[4:]) try: temp = state.stack[-dpth - 1] state.stack[-dpth - 1] = state.stack[-1] state.stack[-1] = temp except IndexError: # Stack underflow halt = True elif op == 'POP': try: state.stack.pop() except IndexError: # Stack underflow halt = True # Bitwise ops elif op == 'AND': try: op1, op2 = state.stack.pop(), state.stack.pop() if (type(op1) == BoolRef): op1 = If(op1, BitVecVal(1, 256), BitVecVal(0, 256)) if (type(op2) == BoolRef): op2 = If(op2, BitVecVal(1, 256), BitVecVal(0, 256)) state.stack.append(op1 & op2) except IndexError: # Stack underflow halt = True elif op == 'OR': try: op1, op2 = state.stack.pop(), state.stack.pop() if (type(op1) == BoolRef): op1 = If(op1, BitVecVal(1, 256), BitVecVal(0, 256)) if (type(op2) == BoolRef): op2 = If(op2, BitVecVal(1, 256), BitVecVal(0, 256)) state.stack.append(op1 | op2) except IndexError: # Stack underflow halt = True elif op == 'XOR': state.stack.append(state.stack.pop() ^ state.stack.pop()) elif op == 'NOT': state.stack.append(TT256M1 - state.stack.pop()) elif op == 'BYTE': s0, s1 = state.stack.pop(), state.stack.pop() try: n = helper.get_concrete_int(s0) oft = (31 - n) * 8 result = Concat(BitVecVal(0, 248), Extract(oft + 7, oft, s1)) except AttributeError: logging.debug("BYTE: Unsupported symbolic byte offset") result = BitVec(str(simplify(s1)) + "_" + str(simplify(s0)), 256) state.stack.append(simplify(result)) # Arithmetics elif op == 'ADD': state.stack.append((helper.pop_bitvec(state) + helper.pop_bitvec(state))) elif op == 'SUB': state.stack.append((helper.pop_bitvec(state) - helper.pop_bitvec(state))) elif op == 'MUL': state.stack.append(helper.pop_bitvec(state) * helper.pop_bitvec(state)) elif op == 'DIV': state.stack.append(UDiv(helper.pop_bitvec(state), helper.pop_bitvec(state))) elif op == 'MOD': s0, s1 = helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append(0 if s1 == 0 else URem(s0, s1)) elif op == 'SDIV': s0, s1 = helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append(s0 / s1) elif op == 'SMOD': s0, s1 = helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append(0 if s1 == 0 else s0 % s1) elif op == 'ADDMOD': s0, s1, s2 = helper.pop_bitvec(state), helper.pop_bitvec(state), helper.pop_bitvec(state) logging.info(str(type)) state.stack.append((s0 + s1) % s2) elif op == 'MULMOD': s0, s1, s2 = helper.pop_bitvec(state), helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append((s0 * s1) % s2 if s2 else 0) elif op == 'EXP': # we only implement 2 ** x base, exponent = helper.pop_bitvec(state), helper.pop_bitvec(state) if (type(base) != BitVecNumRef) or (type(exponent) != BitVecNumRef): state.stack.append(BitVec(str(base) + "_EXP_" + str(exponent), 256)) elif (base.as_long() == 2): if exponent.as_long() == 0: state.stack.append(BitVecVal(1, 256)) else: state.stack.append(base << (exponent - 1)) else: state.stack.append(base) elif op == 'SIGNEXTEND': s0, s1 = state.stack.pop(), state.stack.pop() try: s0 = helper.get_concrete_int(s0) s1 = helper.get_concrete_int(s1) if s0 <= 31: testbit = s0 * 8 + 7 if s1 & (1 << testbit): state.stack.append(s1 | (TT256 - (1 << testbit))) else: state.stack.append(s1 & ((1 << testbit) - 1)) else: state.stack.append(s1) except: halt = True continue # Comparisons elif op == 'LT': exp = ULT(helper.pop_bitvec(state), helper.pop_bitvec(state)) state.stack.append(exp) elif op == 'GT': exp = UGT(helper.pop_bitvec(state), helper.pop_bitvec(state)) state.stack.append(exp) elif op == 'SLT': exp = helper.pop_bitvec(state) < helper.pop_bitvec(state) state.stack.append(exp) elif op == 'SGT': exp = helper.pop_bitvec(state) > helper.pop_bitvec(state) state.stack.append(exp) elif op == 'EQ': op1 = state.stack.pop() op2 = state.stack.pop() if(type(op1) == BoolRef): op1 = If(op1, BitVecVal(1, 256), BitVecVal(0, 256)) if(type(op2) == BoolRef): op2 = If(op2, BitVecVal(1, 256), BitVecVal(0, 256)) exp = op1 == op2 state.stack.append(exp) elif op == 'ISZERO': val = state.stack.pop() if (type(val) == BoolRef): exp = val == False else: exp = val == 0 state.stack.append(exp) # Call data elif op == 'CALLVALUE': state.stack.append(environment.callvalue) elif op == 'CALLDATALOAD': # unpack 32 bytes from calldata into a word and put it on the stack op0 = state.stack.pop() try: offset = helper.get_concrete_int(simplify(op0)) b = environment.calldata[offset] except AttributeError: logging.debug("CALLDATALOAD: Unsupported symbolic index") state.stack.append(BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(op0), 256)) continue except IndexError: logging.debug("Calldata not set, using symbolic variable instead") state.stack.append(BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(op0), 256)) continue if type(b) == int: val = b'' try: for i in range(offset, offset + 32): val += environment.calldata[i].to_bytes(1, byteorder='big') logging.debug("Final value: " + str(int.from_bytes(val, byteorder='big'))) state.stack.append(BitVecVal(int.from_bytes(val, byteorder='big'), 256)) except: state.stack.append(BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(op0), 256)) else: # symbolic variable state.stack.append(BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(op0), 256)) elif op == 'CALLDATASIZE': if environment.calldata_type == CalldataType.SYMBOLIC: state.stack.append(BitVec("calldatasize_" + environment.active_account.contract_name, 256)) else: state.stack.append(BitVecVal(len(environment.calldata), 256)) elif op == 'CALLDATACOPY': op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop() try: mstart = helper.get_concrete_int(op0) except: logging.debug("Unsupported symbolic memory offset in CALLDATACOPY") continue try: dstart = helper.get_concrete_int(op1) except: logging.debug("Unsupported symbolic calldata offset in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec("calldata_" + str(environment.active_account.contract_name) + "_cpy", 256) continue try: size = helper.get_concrete_int(op2) except: logging.debug("Unsupported symbolic size in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) continue if size > 0: try: state.mem_extend(mstart, size) except: logging.debug("Memory allocation error: mstart = " + str(mstart) + ", size = " + str(size)) state.mem_extend(mstart, 1) state.memory[mstart] = BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) continue try: i_data = environment.calldata[dstart] for i in range(mstart, mstart + size): state.memory[i] = environment.calldata[i_data] i_data += 1 except: logging.debug("Exception copying calldata to memory") state.memory[mstart] = BitVec("calldata_" + str(environment.active_account.contract_name) + "_" + str(dstart), 256) elif op == 'STOP': if len(self.call_stack): self.pending_returns[self.call_stack[-1]].append(node.uid) halt = True continue # Environment elif op == 'ADDRESS': state.stack.append(environment.address) elif op == 'BALANCE': addr = state.stack.pop() state.stack.append(BitVec("balance_at_" + str(addr), 256)) elif op == 'ORIGIN': state.stack.append(environment.origin) elif op == 'CALLER': state.stack.append(environment.sender) elif op == 'CODESIZE': state.stack.append(len(disassembly.instruction_list)) if op == 'SHA3': op0, op1 = state.stack.pop(), state.stack.pop() try: index, length = helper.get_concrete_int(op0), helper.get_concrete_int(op1) except: # Can't access symbolic memory offsets state.stack.append(BitVec("KECCAC_mem_" + str(op0) + ")", 256)) continue try: data = b'' for i in range(index, index + length): data += helper.get_concrete_int(state.memory[i]).to_bytes(1, byteorder='big') i += 1 except: svar = str(state.memory[index]) svar = svar.replace(" ", "_") state.stack.append(BitVec("keccac_" + svar, 256)) continue keccac = utils.sha3(utils.bytearray_to_bytestr(data)) logging.debug("Computed SHA3 Hash: " + str(binascii.hexlify(keccac))) state.stack.append(BitVecVal(helper.concrete_int_from_bytes(keccac, 0), 256)) elif op == 'GASPRICE': state.stack.append(BitVec("gasprice", 256)) elif op == 'CODECOPY': # Not implemented start, s1, size = state.stack.pop(), state.stack.pop(), state.stack.pop() elif op == 'EXTCODESIZE': addr = state.stack.pop() state.stack.append(BitVec("extcodesize", 256)) elif op == 'EXTCODECOPY': # Not implemented addr = state.stack.pop() start, s2, size = state.stack.pop(), state.stack.pop(), state.stack.pop() elif op == 'RETURNDATASIZE': state.stack.append(BitVec("returndatasize", 256)) elif op == 'BLOCKHASH': blocknumber = state.stack.pop() state.stack.append(BitVec("blockhash_block_" + str(blocknumber), 256)) elif op == 'COINBASE': state.stack.append(BitVec("coinbase", 256)) elif op == 'TIMESTAMP': state.stack.append(BitVec("timestamp", 256)) elif op == 'NUMBER': state.stack.append(BitVec("block_number", 256)) elif op == 'DIFFICULTY': state.stack.append(BitVec("block_difficulty", 256)) elif op == 'GASLIMIT': state.stack.append(BitVec("block_gaslimit", 256)) elif op == 'MLOAD': op0 = state.stack.pop() logging.debug("MLOAD[" + str(op0) + "]") try: offset = helper.get_concrete_int(op0) except AttributeError: logging.debug("Can't MLOAD from symbolic index") data = BitVec("mem_" + str(op0), 256) state.stack.append(data) continue try: data = helper.concrete_int_from_bytes(state.memory, offset) except IndexError: # Memory slot not allocated data = BitVec("mem_" + str(offset), 256) except TypeError: # Symbolic memory data = state.memory[offset] logging.debug("Load from memory[" + str(offset) + "]: " + str(data)) state.stack.append(data) elif op == 'MSTORE': op0, value = state.stack.pop(), state.stack.pop() try: mstart = helper.get_concrete_int(op0) except AttributeError: logging.debug("MSTORE to symbolic index. Not supported") continue try: state.mem_extend(mstart, 32) except Exception: logging.debug("Error extending memory, mstart = " + str(mstart) + ", size = 32") logging.debug("MSTORE to mem[" + str(mstart) + "]: " + str(value)) try: # Attempt to concretize value _bytes = helper.concrete_int_to_bytes(value) i = 0 for b in _bytes: state.memory[mstart + i] = _bytes[i] i += 1 except: try: state.memory[mstart] = value except: logging.debug("Invalid memory access") continue elif op == 'MSTORE8': op0, value = state.stack.pop(), state.stack.pop() try: offset = helper.get_concrete_int(op0) except AttributeError: logging.debug("MSTORE to symbolic index. Not supported") continue state.mem_extend(offset, 1) state.memory[offset] = value % 256 elif op == 'SLOAD': index = state.stack.pop() logging.debug("Storage access at index " + str(index)) try: index = helper.get_concrete_int(index) except AttributeError: index = str(index) try: data = gblState.environment.active_account.storage[index] except KeyError: data = BitVec("storage_" + str(index), 256) gblState.environment.active_account.storage[index] = data state.stack.append(data) elif op == 'SSTORE': index, value = state.stack.pop(), state.stack.pop() logging.debug("Write to storage[" + str(index) + "] at node " + str(start_addr)) try: index = helper.get_concrete_int(index) except AttributeError: index = str(index) try: # Create a fresh copy of the account object before modifying storage for k in gblState.accounts: if gblState.accounts[k] == gblState.environment.active_account: gblState.accounts[k] = copy.deepcopy(gblState.accounts[k]) gblState.environment.active_account = gblState.accounts[k] break gblState.environment.active_account.storage[index] = value except KeyError: logging.debug("Error writing to storage: Invalid index") continue elif op == 'JUMP': try: jump_addr = helper.get_concrete_int(state.stack.pop()) except AttributeError: logging.debug("Invalid jump argument (symbolic address)") halt = True continue except IndexError: # Stack Underflow halt = True continue if (state.depth < self.max_depth): i = helper.get_instruction_index(disassembly.instruction_list, jump_addr) if i is None: logging.debug("JUMP to invalid address") halt = True continue opcode = disassembly.instruction_list[i]['opcode'] if opcode == "JUMPDEST": new_gblState = LaserEVM.copy_global_state(gblState) new_gblState.mstate.pc = i new_gblState.mstate.depth += 1 new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node self.edges.append(Edge(node.uid, new_node.uid, JumpType.UNCONDITIONAL)) halt = True continue else: logging.debug("Skipping JUMP to invalid destination (not JUMPDEST): " + str(jump_addr)) halt = True # continue else: logging.debug("Max depth reached, skipping JUMP") halt = True # continue elif op == 'JUMPI': op0, condition = state.stack.pop(), state.stack.pop() try: jump_addr = helper.get_concrete_int(op0) except: logging.debug("Skipping JUMPI to invalid destination.") if (state.depth < self.max_depth): i = helper.get_instruction_index(disassembly.instruction_list, jump_addr) if not i: logging.debug("Invalid jump destination: " + str(jump_addr)) else: instr = disassembly.instruction_list[i] if instr['opcode'] != "JUMPDEST": logging.debug("Invalid jump destination: " + str(jump_addr)) halt = True continue elif (type(condition) == BoolRef): if not is_false(simplify(condition)): # Create new node for condition == True new_gblState = LaserEVM.copy_global_state(gblState) new_gblState.mstate.pc = i new_gblState.mstate.constraints.append(condition) new_gblState.mstate.depth += 1 new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node self.edges.append(Edge(node.uid, new_node.uid, JumpType.CONDITIONAL, condition)) else: logging.debug("Pruned unreachable states.") else: logging.debug("Invalid condition: " + str(condition) + "(type " + str(type(condition)) + ")") halt = True continue new_gblState = LaserEVM.copy_global_state(gblState) if (type(condition) == BoolRef): negated = Not(condition) else: negated = condition == 0 if not is_false(simplify(negated)): new_gblState.mstate.constraints.append(negated) new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node self.edges.append(Edge(node.uid, new_node.uid, JumpType.CONDITIONAL, negated)) halt = True # continue else: logging.debug("Max depth reached, skipping JUMPI") elif op == 'PC': state.stack.append(state.pc - 1) elif op == 'MSIZE': state.stack.append(BitVec("msize", 256)) elif op == 'GAS': state.stack.append(BitVec("gas", 256)) elif op.startswith('LOG'): dpth = int(op[3:]) state.stack.pop(), state.stack.pop() [state.stack.pop() for x in range(dpth)] # Not supported elif op == 'CREATE': state.stack.pop(), state.stack.pop(), state.stack.pop() # Not supported state.stack.append(0) elif op in ('CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'): if op in ('CALL', 'CALLCODE'): gas, to, value, meminstart, meminsz, memoutstart, memoutsz = \ state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop() else: gas, to, meminstart, meminsz, memoutstart, memoutsz = \ state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop() try: callee_address = hex(helper.get_concrete_int(to)) except AttributeError: # Not a concrete call address. Call target may be an address in storage. m = re.search(r'storage_(\d+)', str(simplify(to))) logging.debug("CALL to: " + str(simplify(to))) if (m and self.dynamic_loader is not None): idx = int(m.group(1)) logging.info("Dynamic contract address at storage index " + str(idx)) # attempt to read the contract address from instance storage try: callee_address = self.dynamic_loader.read_storage(environment.active_account.address, idx) except: logging.debug("Error accessing contract storage.") ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue # testrpc simply returns the address, geth response is more elaborate. if not re.match(r"^0x[0-9a-f]{40}$", callee_address): callee_address = "0x" + callee_address[26:] else: ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) # Set output memory logging.debug("memoutstart: "+ str(memoutstart)) if not isinstance(memoutstart, ExprRef): state.mem_extend(memoutstart, 1) state.memory[memoutstart] = ret else: logging.debug("Unsupported memory symbolic index") continue if not re.match(r"^0x[0-9a-f]{40}", callee_address): logging.debug("Invalid address: " + str(callee_address)) ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue if (int(callee_address, 16) < 5): logging.info("Native contract called: " + callee_address) # Todo: Implement native contracts ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue try: callee_account = self.accounts[callee_address] except KeyError: # We have a valid call address, but contract is not in the modules list logging.info("Module with address " + callee_address + " not loaded.") if self.dynamic_loader is not None: logging.info("Attempting to load dependency") try: code = self.dynamic_loader.dynld(environment.active_account.address, callee_address) except Exception as e: logging.info("Unable to execute dynamic loader.") if code is None: logging.info("No code returned, not a contract account?") ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue # New contract bytecode loaded successfully, create a new contract account self.accounts[callee_address] = Account(callee_address, code, callee_address) logging.info("Dependency loaded: " + callee_address) else: logging.info("Dynamic loader unavailable. Skipping call") ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue logging.info("Executing " + op + " to: " + callee_address) try: callee_account = self.accounts[callee_address] except KeyError: logging.info("Contract " + str(callee_address) + " not loaded.") logging.info((str(self.accounts))) ret = BitVec("retval_" + str(instr['address']), 256) state.stack.append(ret) continue try: # TODO: This only allows for either fully concrete or fully symbolic calldata. # Improve management of memory and callata to support a mix between both types. calldata = state.memory[helper.get_concrete_int(meminstart):helper.get_concrete_int(meminstart + meminsz)] if (len(calldata) < 32): calldata += [0] * (32 - len(calldata)) calldata_type = CalldataType.CONCRETE logging.debug("Calldata: " + str(calldata)) except AttributeError: logging.info("Unsupported symbolic calldata offset") calldata_type = CalldataType.SYMBOLIC calldata = [] self.call_stack.append(instr['address']) self.pending_returns[instr['address']] = [] if (op == 'CALL'): callee_environment = Environment(callee_account, BitVecVal(int(environment.active_account.address, 16), 256), calldata, environment.gasprice, value, environment.origin, calldata_type=calldata_type) new_gblState = GlobalState(gblState.accounts, callee_environment, MachineState(gas)) new_gblState.mstate.depth = state.depth + 1 new_gblState.mstate.constraints = copy.deepcopy(state.constraints) new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node elif (op == 'CALLCODE'): temp_callvalue = environment.callvalue temp_caller = environment.caller temp_calldata = environment.calldata environment.callvalue = value environment.caller = environment.address environment.calldata = calldata new_gblState = GlobalState(gblState.accounts, environment, MachineState(gas)) new_gblState.mstate.depth = state.depth + 1 new_gblState.mstate.constraints = copy.deepcopy(state.constraints) new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node environment.callvalue = temp_callvalue environment.caller = temp_caller environment.calldata = temp_calldata elif (op == 'DELEGATECALL'): temp_code = environment.code temp_calldata = environment.calldata environment.code = callee_account.code environment.calldata = calldata new_gblState = GlobalState(gblState.accounts, environment, MachineState(gas)) new_gblState.mstate.depth = state.depth + 1 new_gblState.mstate.constraints = copy.deepcopy(state.constraints) new_node = self._sym_exec(new_gblState) self.nodes[new_node.uid] = new_node environment.code = temp_code environment.calldata = temp_calldata self.edges.append(Edge(node.uid, new_node.uid, JumpType.CALL)) ''' There may be multiple possible returns from the callee contract. Currently, we don't create separate nodes on the CFG for each of them. Instead, a single "return node" is created and a separate edge is added for each return path. The return value is always symbolic. ''' ret = BitVec("retval_" + str(disassembly.instruction_list[state.pc]['address']), 256) state.stack.append(ret) return_address = self.call_stack.pop() new_gblState = LaserEVM.copy_global_state(gblState) new_gblState.mstate.depth += 1 new_node = self._sym_exec(new_gblState) new_node.flags |= NodeFlags.CALL_RETURN self.nodes[new_node.uid] = new_node for ret_uid in self.pending_returns[return_address]: self.edges.append(Edge(ret_uid, new_node.uid, JumpType.RETURN)) state.stack.append(BitVec("retval", 256)) halt = True elif op == 'RETURN': offset, length = state.stack.pop(), state.stack.pop() try: self.last_returned = state.memory[helper.get_concrete_int(offset):helper.get_concrete_int(offset + length)] except AttributeError: logging.debug("Return with symbolic length or offset. Not supported") if len(self.call_stack): self.pending_returns[self.call_stack[-1]].append(node.uid) halt = True elif op == 'SUICIDE': halt = True elif op == 'REVERT': if len(self.call_stack): self.pending_returns[self.call_stack[-1]].append(node.uid) halt = True elif op == 'ASSERT_FAIL' or op == 'INVALID': if len(self.call_stack): self.pending_returns[self.call_stack[-1]].append(node.uid) halt = True logging.debug("Returning from node " + str(node.uid)) return node
def execute(statespace): logging.debug("Executing module: UNCHECKED_RETVAL") issues = [] visited = [] for call in statespace.calls: # Only needs to be checked once per call instructions (it's essentially just static analysis) if call.addr in visited: continue else: visited.append(call.addr) # The instructions executed in each node (basic block) are saved in node.instruction_list, e.g.: # [{address: "132", opcode: "CALL"}, {address: "133", opcode: "ISZERO"}] start_index = helper.get_instruction_index(call.node.instruction_list, call.addr) + 1 retval_checked = False # ISZERO retval should be found within the next few instructions. for i in range(0, 10): try: instr = call.node.instruction_list[start_index + i] except IndexError: break if (instr['opcode'] == 'ISZERO' and re.search( r'retval', str( call.node.states[instr['address']].stack[-1]))): retval_checked = True break if not retval_checked: issue = Issue(call.node.module_name, call.node.function_name, call.addr, "Unchecked CALL return value") if (call.to.type == VarType.CONCRETE): receiver = hex(call.to.val) elif (re.search(r"caller", str(call.to))): receiver = "msg.sender" elif (re.search(r"storage", str(call.to))): receiver = "an address obtained from storage" else: receiver = str(call.to) issue.description = \ "The function " + call.node.function_name + " contains a call to " + receiver + ".\n" \ "The return value of this call is not checked. Note that the function will continue to execute with a return value of '0' if the called contract throws." issues.append(issue) return issues
def analyze_truffle_project(): project_root = os.getcwd() build_dir = os.path.join(project_root, "build", "contracts") files = os.listdir(build_dir) for filename in files: if re.match(r'.*\.json$', filename) and filename != "Migrations.json": with open(os.path.join(build_dir, filename)) as cf: contractdata = json.load(cf) try: name = contractdata['contractName'] bytecode = contractdata['deployedBytecode'] except: print("Unable to parse contract data. Please use Truffle 4 to compile your project.") sys.exit() if (len(bytecode) < 4): continue ethcontract= ETHContract(bytecode, name=name, address = util.get_indexed_address(0)) contracts = [ethcontract] states = StateSpace(contracts, max_depth = 10) report = fire_lasers(states) # augment with source code disassembly = ethcontract.get_disassembly() source = contractdata['source'] deployedSourceMap = contractdata['deployedSourceMap'].split(";") mappings = [] i = 0 while(i < len(deployedSourceMap)): m = re.search(r"^(\d+):*(\d+)", deployedSourceMap[i]) if (m): offset = m.group(1) length = m.group(2) else: m = re.search(r"^:(\d+)", deployedSourceMap[i]) if m: length = m.group(1) mappings.append((int(offset), int(length))) i += 1 for key, issue in report.issues.items(): index = helper.get_instruction_index(disassembly.instruction_list, issue.pc) if index: issue.code_start = mappings[index][0] issue.code_length = mappings[index][1] issue.code = source[mappings[index][0]: mappings[index][0] + mappings[index][1]] if len(report.issues): print("Analysis result for " + name + ":\n" + report.as_text()) else: print("Analysis result for " + name + ": No issues found.")
def analyze_truffle_project(args): project_root = os.getcwd() build_dir = os.path.join(project_root, "build", "contracts") files = os.listdir(build_dir) for filename in files: if re.match(r'.*\.json$', filename) and filename != "Migrations.json": with open(os.path.join(build_dir, filename)) as cf: contractdata = json.load(cf) try: name = contractdata['contractName'] bytecode = contractdata['deployedBytecode'] except: print( "Unable to parse contract data. Please use Truffle 4 to compile your project." ) sys.exit() if (len(bytecode) < 4): continue ethcontract = ETHContract(bytecode, name=name, address=util.get_indexed_address(0)) states = StateSpace([ethcontract], max_depth=10) issues = fire_lasers(states) if not len(issues): if (args.outform == 'text' or args.outform == 'markdown'): print("Analysis result for " + name + ": No issues found.") else: result = { 'contract': name, 'result': { 'success': True, 'error': None, 'issues': [] } } print(json.dumps(result)) else: report = Report() # augment with source code disassembly = ethcontract.get_disassembly() source = contractdata['source'] deployedSourceMap = contractdata['deployedSourceMap'].split( ";") mappings = [] i = 0 for item in deployedSourceMap: mapping = item.split(":") if len(mapping) > 0 and len(mapping[0]) > 0: offset = int(mapping[0]) if len(mapping) > 1 and len(mapping[1]) > 0: length = int(mapping[1]) mappings.append((int(offset), int(length))) for issue in issues: index = helper.get_instruction_index( disassembly.instruction_list, issue.pc) if index: issue.code_start = mappings[index][0] issue.code_length = mappings[index][1] issue.code = source[ mappings[index][0]:mappings[index][0] + mappings[index][1]] report.append_issue(issue) if (args.outform == 'json'): result = { 'contract': name, 'result': { 'success': True, 'error': None, 'issues': list(map(lambda x: x.as_dict(), issues)) } } print(json.dumps(result)) else: if (args.outform == 'text'): print("Analysis result for " + name + ":\n" + report.as_text()) elif (args.outform == 'markdown'): print("Analysis result for " + name + ":\n" + report.as_markdown())
def _sym_exec(self, context, state, depth=0, constraints=[]): disassembly = context.module['disassembly'] depth = depth start_addr = disassembly.instruction_list[state.pc]['address'] if start_addr == 0: self.execution_state['current_func'] = "fallback" self.execution_state['current_func_addr'] = start_addr node = Node(context.module['name'], start_addr, constraints) logging.debug("DEBUG Node(type=" + str(type(node)) + "): " + str(node)) logging.debug("- Entering block " + str(node.uid) + ", index = " + str(state.pc) + ", address = " + str(start_addr) + ", depth = " + str(depth)) if start_addr in disassembly.addr_to_func: # Enter a new function function_name = disassembly.addr_to_func[start_addr] self.execution_state['current_func'] = function_name logging.info("- Entering function " + context.module['name'] + ":" + function_name) node.instruction_list.append({ 'opcode': function_name, 'address': disassembly.instruction_list[state.pc]['address'] }) state.pc += 1 node.function_name = self.execution_state['current_func'] halt = False instr = disassembly.instruction_list[state.pc] while not halt: try: instr = disassembly.instruction_list[state.pc] except IndexError: logging.debug("Invalid PC") return node # Save instruction and state node.instruction_list.append(instr) node.states[instr['address']] = state state = copy.deepcopy(state) self.total_states += 1 state.pc += 1 op = instr['opcode'] logging.debug("[" + context.module['name'] + "] " + helper.get_trace_line(instr, state)) # slows down execution significantly. # stack ops if op.startswith("PUSH"): value = BitVecVal(int(instr['argument'][2:], 16), 256) state.stack.append(value) elif op.startswith('DUP'): dpth = int(op[3:]) try: state.stack.append(state.stack[-dpth]) except: halt = True continue elif op.startswith('SWAP'): dpth = int(op[4:]) try: temp = state.stack[-dpth - 1] except IndexError: # Stack underflow halt = True continue state.stack[-dpth - 1] = state.stack[-1] state.stack[-1] = temp elif op == 'POP': try: state.stack.pop() except IndexError: # Stack underflow halt = True continue # Bitwise ops elif op == 'AND': try: state.stack.append(state.stack.pop() & state.stack.pop()) except IndexError: # Stack underflow halt = True continue elif op == 'OR': try: op1, op2 = state.stack.pop(), state.stack.pop() except IndexError: # Stack underflow halt = True continue if (type(op1) == BoolRef): op1 = If(op1, BitVecVal(1, 256), BitVecVal(0, 256)) if (type(op2) == BoolRef): op2 = If(op2, BitVecVal(1, 256), BitVecVal(0, 256)) state.stack.append(op1 | op2) elif op == 'XOR': state.stack.append(state.stack.pop() ^ state.stack.pop()) elif op == 'NOT': state.stack.append(TT256M1 - state.stack.pop()) elif op == 'BYTE': s0, s1 = state.stack.pop(), state.stack.pop() state.stack.append(BitVecVal(0, 256)) # Arithmetics elif op == 'ADD': state.stack.append( (helper.pop_bitvec(state) + helper.pop_bitvec(state))) elif op == 'SUB': state.stack.append( (helper.pop_bitvec(state) - helper.pop_bitvec(state))) elif op == 'MUL': state.stack.append( helper.pop_bitvec(state) * helper.pop_bitvec(state)) elif op == 'DIV': s0, s1 = helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append(UDiv(s0, s1)) elif op == 'MOD': s0, s1 = helper.pop_bitvec(state), helper.pop_bitvec(state) state.stack.append(0 if s1 == 0 else s0 % s1) elif op == 'SDIV': s0, s1 = helper.to_signed( helper.pop_bitvec(state)), helper.to_signed( helper.pop_bitvec(state)) state.stack.append(0 if s1 == 0 else ( abs(s0) // abs(s1) * (-1 if s0 * s1 < 0 else 1)) & TT256M1) elif op == 'SMOD': s0, s1 = helper.to_signed( helper.pop_bitvec(state)), helper.to_signed( helper.pop_bitvec(state)) state.stack.append(0 if s1 == 0 else (abs(s0) % abs(s1) * (-1 if s0 < 0 else 1)) & TT256M1) elif op == 'ADDMOD': s0, s1, s2 = helper.pop_bitvec(state), helper.pop_bitvec( state), helper.pop_bitvec(state) state.stack.append((s0 + s1) % s2 if s2 else 0) elif op == 'MULMOD': s0, s1, s2 = helper.pop_bitvec(state), helper.pop_bitvec( state), helper.pop_bitvec(state) state.stack.append((s0 * s1) % s2 if s2 else 0) elif op == 'EXP': # we only implement 2 ** x base, exponent = state.stack.pop(), state.stack.pop() if (type(base) != BitVecNumRef): state.stack.append( BitVec(str(base) + "_EXP_" + str(exponent), 256)) elif (base.as_long() == 2): if exponent == 0: state.stack.append(BitVecVal(1, 256)) else: state.stack.append(base << (exponent - 1)) else: state.stack.append(base) elif op == 'SIGNEXTEND': s0, s1 = state.stack.pop(), state.stack.pop() try: s0 = get_concrete_int(s0) s1 = get_concrete_int(s1) except: halt = True continue if s0 <= 31: testbit = s0 * 8 + 7 if s1 & (1 << testbit): state.stack.append(s1 | (TT256 - (1 << testbit))) else: state.stack.append(s1 & ((1 << testbit) - 1)) else: state.stack.append(s1) # Comparisons elif op == 'LT': exp = ULT(helper.pop_bitvec(state), helper.pop_bitvec(state)) state.stack.append(exp) elif op == 'GT': exp = UGT(helper.pop_bitvec(state), helper.pop_bitvec(state)) state.stack.append(exp) elif op == 'SLT': exp = helper.pop_bitvec(state) < helper.pop_bitvec(state) state.stack.append(exp) elif op == 'SGT': exp = helper.pop_bitvec(state) > helper.pop_bitvec(state) state.stack.append(exp) elif op == 'EQ': op1 = state.stack.pop() op2 = state.stack.pop() if (type(op1) == BoolRef): op1 = If(op1, BitVecVal(1, 256), BitVecVal(0, 256)) if (type(op2) == BoolRef): op2 = If(op2, BitVecVal(1, 256), BitVecVal(0, 256)) exp = op1 == op2 state.stack.append(exp) elif op == 'ISZERO': val = state.stack.pop() if (type(val) == BoolRef): exp = val == False else: exp = val == 0 state.stack.append(exp) # Call data elif op == 'CALLVALUE': state.stack.append(context.callvalue) elif op == 'CALLDATALOAD': # unpack 32 bytes from calldata into a word and put it on the stack op0 = state.stack.pop() try: offset = helper.get_concrete_int(simplify(op0)) except AttributeError: logging.debug("CALLDATALOAD: Unsupported symbolic index") state.stack.append( BitVec( "calldata_" + str(context.module['name']) + "_" + str(op0), 256)) continue try: b = context.calldata[offset] except IndexError: logging.debug( "Calldata not set, using symbolic variable instead") state.stack.append( BitVec( "calldata_" + str(context.module['name']) + "_" + str(op0), 256)) continue if type(b) == int: # 32 byte concrete value val = b'' try: for i in range(offset, offset + 32): val += context.calldata[i].to_bytes( 1, byteorder='big') state.stack.append( BitVecVal(int.from_bytes(val, byteorder='big'), 256)) except: state.stack.append(b) else: # symbolic variable state.stack.append(b) elif op == 'CALLDATASIZE': if context.calldata_type == CalldataType.SYMBOLIC: state.stack.append( BitVec("calldatasize_" + context.module['name'], 256)) else: state.stack.append(BitVecVal(len(context.calldata), 256)) elif op == 'CALLDATACOPY': op0, op1, op2 = state.stack.pop(), state.stack.pop( ), state.stack.pop() try: mstart = helper.get_concrete_int(op0) except: logging.debug( "Unsupported symbolic memory offset in CALLDATACOPY") continue try: dstart = helper.get_concrete_int(op1) except: logging.debug( "Unsupported symbolic calldata offset in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(context.module['name']) + "_cpy", 256) continue try: size = helper.get_concrete_int(op2) except: logging.debug("Unsupported symbolic size in CALLDATACOPY") state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(context.module['name']) + "_" + str(dstart), 256) continue if size > 0: try: state.mem_extend(mstart, size) except: logging.debug("Memory allocation error: mstart = " + str(mstart) + ", size = " + str(size)) state.mem_extend(mstart, 1) state.memory[mstart] = BitVec( "calldata_" + str(context.module['name']) + "_" + str(dstart), 256) continue try: i_data = context.calldata[dstart] for i in range(mstart, mstart + size): state.memory[i] = context.calldata[i_data] i_data += 1 except: logging.debug("Exception copying calldata to memory") state.memory[mstart] = BitVec( "calldata_" + str(context.module['name']) + "_" + str(dstart), 256) # continue # Control flow elif op == 'STOP': if self.last_call_address is not None: self.pending_returns[self.last_call_address].append( node.uid) halt = True continue # Environment elif op == 'ADDRESS': state.stack.append(context.address) elif op == 'BALANCE': addr = state.stack.pop() state.stack.append(BitVec("balance_at_" + str(addr), 256)) elif op == 'ORIGIN': state.stack.append(context.origin) elif op == 'CALLER': state.stack.append(context.caller) elif op == 'CODESIZE': state.stack.append(len(disassembly.instruction_list)) if op == 'SHA3': op0, op1 = state.stack.pop(), state.stack.pop() try: index, length = helper.get_concrete_int( op0), helper.get_concrete_int(op1) except: # Can't access symbolic memory offsets state.stack.append( BitVec("KECCAC_mem_" + str(op0) + ")", 256)) continue try: data = b'' for i in range(index, index + length): data += helper.get_concrete_int( state.memory[i]).to_bytes(1, byteorder='big') i += 1 except: svar = str(state.memory[index]) svar = svar.replace(" ", "_") state.stack.append(BitVec("keccac_" + svar, 256)) continue logging.debug("SHA3 Data: " + str(data)) keccac = utils.sha3(utils.bytearray_to_bytestr(data)) logging.debug("SHA3 Hash: " + str(binascii.hexlify(keccac))) state.stack.append( BitVecVal(helper.concrete_int_from_bytes(keccac, 0), 256)) elif op == 'GASPRICE': state.stack.append(BitVecVal(1, 256)) elif op == 'CODECOPY': # Not implemented start, s1, size = state.stack.pop(), state.stack.pop( ), state.stack.pop() elif op == 'EXTCODESIZE': addr = state.stack.pop() state.stack.append(BitVec("extcodesize", 256)) elif op == 'EXTCODECOPY': # Not implemented addr = state.stack.pop() start, s2, size = state.stack.pop(), state.stack.pop( ), state.stack.pop() elif op == 'BLOCKHASH': blocknumber = state.stack.pop() state.stack.append( BitVec("blockhash_block_" + str(blocknumber), 256)) elif op == 'COINBASE': state.stack.append(BitVec("coinbase", 256)) elif op == 'TIMESTAMP': state.stack.append(BitVec("timestamp", 256)) elif op == 'NUMBER': state.stack.append(BitVec("block_number", 256)) elif op == 'DIFFICULTY': state.stack.append(BitVec("block_difficulty", 256)) elif op == 'GASLIMIT': state.stack.append(BitVec("block_gaslimit", 256)) elif op == 'MLOAD': op0 = state.stack.pop() logging.debug("MLOAD[" + str(op0) + "]") try: offset = helper.get_concrete_int(op0) except AttributeError: logging.debug("Can't MLOAD from symbolic index") data = BitVec("mem_" + str(op0), 256) continue try: data = helper.concrete_int_from_bytes(state.memory, offset) except IndexError: # Memory slot not allocated data = BitVec("mem_" + str(offset), 256) except TypeError: # Symbolic memory data = state.memory[offset] logging.debug("Load from memory[" + str(offset) + "]: " + str(data)) state.stack.append(data) elif op == 'MSTORE': op0, value = state.stack.pop(), state.stack.pop() try: mstart = helper.get_concrete_int(op0) except AttributeError: logging.debug("MSTORE to symbolic index. Not supported") continue try: state.mem_extend(mstart, 32) except Exception: logging.debug("Error extending memory, mstart = " + str(mstart) + ", size = 32") logging.debug("MSTORE to mem[" + str(mstart) + "]: " + str(value)) try: # Attempt to concretize value _bytes = helper.concrete_int_to_bytes(value) i = 0 for b in _bytes: state.memory[mstart + i] = _bytes[i] i += 1 except: try: state.memory[mstart] = value except: logging.debug("Invalid memory access") continue # logging.debug("MEM: " + str(state.memory)) elif op == 'MSTORE8': # Is this ever used? op0, value = state.stack.pop(), state.stack.pop() try: offset = helper.get_concrete_int(op0) except AttributeError: logging.debug("MSTORE to symbolic index. Not supported") continue state.mem_extend(offset, 1) state.memory[offset] = value % 256 elif op == 'SLOAD': index = state.stack.pop() logging.debug("Storage access at index " + str(index)) if type(index) == BitVecRef: # SLOAD from hash offset # k = sha3.keccak_512() # k.update(bytes(str(index), 'utf-8')) # index = k.hexdigest()[:8] index = str(index) try: data = state.storage[index] except KeyError: data = BitVec("storage_" + str(index), 256) state.storage[index] = data state.stack.append(data) elif op == 'SSTORE': index, value = state.stack.pop(), state.stack.pop() logging.debug("Write to storage[" + str(index) + "] at node " + str(start_addr)) if type(index) == BitVecRef: index = str(index) try: state.storage[index] = value except KeyError: logging.debug("Error writing to storage: Invalid index") continue elif op == 'JUMP': try: jump_addr = helper.get_concrete_int(state.stack.pop()) except AttributeError: logging.debug("Invalid jump argument (symbolic address)") halt = True continue except IndexError: # Stack Underflow halt = True continue if (depth < self.max_depth): i = helper.get_instruction_index( disassembly.instruction_list, jump_addr) if i is None: logging.debug("JUMP to invalid address") halt = True continue opcode = disassembly.instruction_list[i]['opcode'] if opcode == "JUMPDEST": if (self.can_jump(jump_addr)): new_state = copy.deepcopy(state) new_state.pc = i new_node = self._sym_exec(context, new_state, depth=depth + 1, constraints=constraints) self.nodes[new_node.uid] = new_node self.edges.append( Edge(node.uid, new_node.uid, JumpType.UNCONDITIONAL)) halt = True continue else: logging.debug("JUMP target limit reached") halt = True continue else: logging.debug( "Skipping JUMP to invalid destination (not JUMPDEST): " + str(jump_addr)) halt = True continue else: logging.debug("Max depth reached, skipping JUMP") halt = True continue elif op == 'JUMPI': op0, condition = state.stack.pop(), state.stack.pop() try: jump_addr = helper.get_concrete_int(op0) except: logging.debug("Skipping JUMPI to invalid destination.") if (depth < self.max_depth): i = helper.get_instruction_index( disassembly.instruction_list, jump_addr) if not i: logging.debug("Invalid jump destination: " + str(jump_addr)) else: instr = disassembly.instruction_list[i] # Add new node for condition == True if instr['opcode'] != "JUMPDEST": logging.debug("Invalid jump destination: " + str(jump_addr)) else: if (type(condition) == bool): logging.debug("BOOL CONDITION TYPE") # continue elif (type(condition) == BoolRef): if (self.can_jump(jump_addr)): new_state = copy.deepcopy(state) new_state.pc = i new_constraints = copy.deepcopy( constraints) new_constraints.append(condition) new_node = self._sym_exec( context, new_state, depth=depth + 1, constraints=new_constraints) self.nodes[new_node.uid] = new_node self.edges.append( Edge(node.uid, new_node.uid, JumpType.CONDITIONAL, condition)) else: logging.debug( "JUMP target limit reached (JUMPI)") else: logging.debug("Invalid condition: " + str(condition) + "(type " + str(type(condition)) + ")") halt = True continue new_state = copy.deepcopy(state) if (type(condition) == BoolRef): negated = Not(condition) else: negated = condition == 0 new_constraints = copy.deepcopy(constraints) new_constraints.append(negated) new_node = self._sym_exec(context, new_state, depth=depth, constraints=new_constraints) self.nodes[new_node.uid] = new_node self.edges.append( Edge(node.uid, new_node.uid, JumpType.CONDITIONAL, negated)) halt = True continue else: logging.debug("Max depth reached, skipping JUMPI") elif op == 'PC': state.stack.append(state.pc - 1) elif op == 'MSIZE': state.stack.append(BitVec("msize", 256)) elif op == 'GAS': state.stack.append(10000000) elif op.startswith('LOG'): dpth = int(op[3:]) state.stack.pop(), state.stack.pop() [state.stack.pop() for x in range(dpth)] # Not supported elif op == 'CREATE': state.stack.pop(), state.stack.pop(), state.stack.pop() # Not supported state.stack.append(0) elif op in ('CALL', 'CALLCODE', 'DELEGATECALL', 'STATICCALL'): if op in ('CALL', 'CALLCODE'): gas, to, value, meminstart, meminsz, memoutstart, memoutsz = \ state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop() else: gas, to, meminstart, meminsz, memoutstart, memoutsz = \ state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop(), state.stack.pop() try: callee_address = hex(helper.get_concrete_int(to)) except AttributeError: # Not a concrete call address. Call target may be an address in storage. m = re.search(r'storage_(\d+)', str(simplify(to))) logging.debug("CALL to: " + str(simplify(to))) if (m and self.dynamic_loader is not None): idx = int(m.group(1)) logging.info( "Dynamic contract address at storage index " + str(idx)) # attempt to read the contract address from instance storage callee_address = self.dynamic_loader.read_storage( context.module['address'], idx) # testrpc simply returns the address, geth response is more elaborate. if not re.match(r"^0x[0-9a-f]{40}$", callee_address): callee_address = "0x" + callee_address[26:] else: logging.info("Unable to resolve address from storage.") ret = BitVec( "retval_" + str(disassembly.instruction_list[ state.pc]['address']), 256) state.stack.append(ret) continue if (int(callee_address, 16) < 5): logging.info("Native contract called: " + callee_address) # Todo: Implement native contracts ret = BitVec( "retval_" + str(disassembly.instruction_list[state.pc]['address']), 256) state.stack.append(ret) continue if not re.match(r"^0x[0-9a-f]{40}", callee_address): logging.debug("Invalid address: " + str(callee_address)) ret = BitVec( "retval_" + str(disassembly.instruction_list[state.pc]['address']), 256) state.stack.append(ret) continue try: module = self.modules[callee_address] except KeyError: # We have a valid call address, but contract is not in the modules list logging.info("Module with address " + callee_address + " not loaded.") if self.dynamic_loader is not None: logging.info("Attempting to load dependency") module = self.dynamic_loader.dynld( context.module['address'], callee_address) if module is None: logging.info( "No code returned, not a contract account?") ret = BitVec( "retval_" + str(disassembly.instruction_list[ state.pc]['address']), 256) state.stack.append(ret) continue # New contract loaded successfully, add it to the modules list self.modules[callee_address] = module self.addr_visited[callee_address] = [] logging.info("Dependency loaded: " + module['address']) else: logging.info( "Dynamic loader unavailable. Skipping call") ret = BitVec( "retval_" + str(disassembly.instruction_list[ state.pc]['address']), 256) state.stack.append(ret) continue logging.info("Executing " + op + " to: " + callee_address) try: callee_module = self.modules[callee_address] except KeyError: logging.info("Contract " + str(callee_address) + " not loaded.") logging.info((str(self.modules))) ret = BitVec( "retval_" + str(disassembly.instruction_list[state.pc]['address']), 256) state.stack.append(ret) continue # Attempt to write concrete calldata try: calldata = state.memory[helper.get_concrete_int( meminstart):helper.get_concrete_int(meminstart + meminsz)] calldata_type = CalldataType.CONCRETE logging.debug("calldata: " + str(calldata)) except AttributeError: logging.info("Unsupported symbolic calldata offset") calldata_type = CalldataType.SYMBOLIC calldata = [] self.last_call_address = disassembly.instruction_list[ state.pc]['address'] self.pending_returns[self.last_call_address] = [] callee_context = Context(callee_module, calldata=calldata, caller=context.address, origin=context.origin, calldata_type=calldata_type) if (op == 'CALL'): new_node = self._sym_exec(callee_context, State(), depth=depth + 1, constraints=constraints) self.nodes[new_node.uid] = new_node elif (op == 'CALLCODE'): temp_module = context.module temp_callvalue = context.callvalue temp_caller = context.caller temp_calldata = context.calldata context.module = callee_module context.callvalue = value context.caller = context.address context.calldata = calldata new_node = self._sym_exec(context, State(), depth=depth + 1, constraints=constraints) self.nodes[new_node.uid] = new_node context.module = temp_module context.callvalue = temp_callvalue context.caller = temp_caller context.calldata = temp_calldata elif (op == 'DELEGATECALL'): temp_module = context.module temp_calldata = context.calldata context.module = callee_module context.calldata = calldata new_node = self._sym_exec(context, State(), depth=depth + 1, constraints=constraints) self.nodes[new_node.uid] = new_node context.module = temp_module context.calldata = temp_calldata self.edges.append(Edge(node.uid, new_node.uid, JumpType.CALL)) ret = BitVec( "retval_" + str(disassembly.instruction_list[state.pc]['address']), 256) state.stack.append(ret) new_state = copy.deepcopy(state) new_node = self._sym_exec(context, new_state, depth=depth + 1, constraints=constraints) self.nodes[new_node.uid] = new_node for ret_uid in self.pending_returns[self.last_call_address]: self.edges.append( Edge(ret_uid, new_node.uid, JumpType.RETURN)) state.stack.append(BitVec("retval", 256)) continue elif op == 'RETURN': offset, length = state.stack.pop(), state.stack.pop() try: self.last_returned = state.memory[helper.get_concrete_int( offset):helper.get_concrete_int(offset + length)] except AttributeError: logging.debug( "Return with symbolic length or offset. Not supported") if self.last_call_address is not None: self.pending_returns[self.last_call_address].append( node.uid) halt = True continue elif op == 'SUICIDE': halt = True continue elif op == 'REVERT': if self.last_call_address is not None: self.pending_returns[self.last_call_address].append( node.uid) halt = True continue elif op == 'INVALID': halt = True continue logging.debug("Returning from node " + str(node.uid)) return node