def test_instruction_semantics_call(self): # call #0xdead raw = b'\xb0\x12\xad\xde' ip = 0xc0de ins, _ = decode_instruction(ip, raw) state = blank_state() state.cpu.registers['R0'] = BitVecVal(ip + len(raw), 16) # ip preincrement state.cpu.registers['R1'] = BitVecVal(0x1234, 16) new_states = state.cpu.step_call(state, ins) self.assertEqual(len(new_states), 1) new_state = new_states[0] lo = new_state.memory[0x1232] hi = new_state.memory[0x1233] pushed_val = Concat(hi, lo) self.assertEqual(intval(pushed_val), ip + len(raw)) self.assertEqual(intval(new_state.cpu.registers['R1']), 0x1232) self.assertEqual(intval(new_state.cpu.registers['R0']), 0xdead)
def may_write_to(self, z3_index, z3_value, storage, constraint_list, consider_length): z3_index = simplify(get_bv(z3_index)) z3_value = simplify(get_bv(z3_value)) same_slot = False add_constraints = [] # z3-index directly pointing to this slot concret_index = BitVecVal(self.slot_counter, 256) # Compare expression equivalence if eq(concret_index, z3_index): same_slot = True # If not structurally equivalent, check if there is an assignment that allows them to be equivalent if not same_slot and not are_z3_satisfiable(constraint_list + [z3_index == concret_index]): return False, None add_constraints.append(simplify(z3_index == self.slot_counter)) index_str = str(z3_index) # Rule out keccak symbolic variable as the function prevents someone from arbitrarily controlling the index if len(z3_index.children()) < 2 and index_str.startswith("keccak") \ or "+" in index_str and index_str[:index_str.index("+")].strip() in keccak_map : return False, None # Todo Here I might do something more elaborate if I see that it does actually not solve critical writings # Problem because writing to an array has a keccak offset, but if the index can be arbitrarely choosen z3 finds # a solution for the controllable symbolic variable to match the index to any slot. #sym_ind_name = extract_sym_names(z3_index) #if any([name for name in sym_ind_name if name.startswith("keccak")]) and any([name for name in sym_ind_name if not name.startswith("keccak")]): # return False, None # If the slot is or may be the same and the slot we currently analyze is the same, we found a possible write if self.bitlength == 256: return True, add_constraints # If not, the slot is still written in its entirety but the observed chunk is loaded and overwritten by itself to_bit, from_bit = self.bit_counter + self.bitlength - 1, self.bit_counter # to_bit, from_bit = BitVecVal(self.bit_counter + self.bitlength - 1, 256), BitVecVal(self.bit_counter, 256) chunk_writing = Extract(to_bit, from_bit, z3_value) chunk_content = Extract(to_bit, from_bit, get_bv(get_storage_slot(BitVecVal(self.slot_counter, 256), storage))) # if the current content of the observed chunk and the respective chunk of the written value can be different # like by a different variable assignment, then we found it if are_z3_satisfiable(constraint_list + [Not(chunk_content == chunk_writing)]): # It is actually not important to use the constraint that the values are different, overwriting with the same # value is still writing. On the other hand it avoids references to storage that later have to be solved with # intertransactional analysis although the violation can be triggered in one transaction # add_constraints.append(simplify(Not(chunk_content == chunk_writing))) return True, add_constraints # For the 256-bit chunks the last step should not be necessary, but a compiler could generate some code that # overwrites a slot content with itself. This function would have a false positive in that case. return False, None
def and_(self, global_state): stack = global_state.mstate.stack op1, op2 = stack.pop(), 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)) stack.append(op1 & op2) return [global_state]
def amino_bitvec_unary_restriction( amino_list: List[AminoRef] = z3_enum_aminos ) -> List[ConstraintRef]: amino_sort_size = amino_list[0].sort().size() - 1 return [ Implies( Extract(i + 1, i + 1, amino) == BitVecVal(1, 1), Extract(i, i, amino) == BitVecVal(1, 1), ) for i in range(amino_sort_size) for amino in amino_list ]
def and_(self, global_state): try: stack = global_state.mstate.stack op1, op2 = stack.pop(), 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)) stack.append(op1 & op2) except IndexError: raise StackUnderflowException() return [global_state]
def byte_(self, global_state): mstate = global_state.mstate op0, op1 = mstate.stack.pop(), mstate.stack.pop() if not isinstance(op1, ExprRef): op1 = BitVecVal(op1, 256) try: index = util.get_concrete_int(op0) offset = (31 - index) * 8 result = Concat(BitVecVal(0, 248), Extract(offset + 7, offset, op1)) except AttributeError: logging.debug("BYTE: Unsupported symbolic byte offset") result = BitVec(str(simplify(op1)) + "[" + str(simplify(op0)) + "]", 256) mstate.stack.append(simplify(result)) return [global_state]
def set_function_id(self, fid: BitVecRef = None): if isinstance(fid, str) and len(fid) == 8: fid = BitVecVal(int(fid, 16), 32) elif isinstance(fid, int): fid = BitVecVal(fid, 32) elif isinstance(fid, BitVecRef) and fid.size() == 32: pass elif fid is None: fid = BitVec('function_id', 32) else: raise SettingError('illegal function id given') for i in range(4): fragment = Extract(i * 8 + 7, i * 8, fid) self.mstore8(3 - i, fragment)
def div_(self, global_state): op0, op1 = util.pop_bitvec(global_state.mstate), util.pop_bitvec(global_state.mstate) if op1 == 0: global_state.mstate.stack.append(BitVecVal(0, 256)) else: global_state.mstate.stack.append(UDiv(op0, op1)) return [global_state]
def execute_contract_creation(laser_evm, contract_initialization_code, contract_name=None) -> Account: """ Executes a contract creation transaction from all open states""" # TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here open_states = laser_evm.open_states[:] del laser_evm.open_states[:] new_account = laser_evm.world_state.create_account(0, concrete_storage=True, dynamic_loader=None) if contract_name: new_account.contract_name = contract_name for open_world_state in open_states: next_transaction_id = get_next_transaction_id() transaction = ContractCreationTransaction( world_state=open_world_state, identifier=next_transaction_id, gas_price=BitVec("gas_price{}".format(next_transaction_id), 256), gas_limit=8000000, # block gas limit origin=BitVec("origin{}".format(next_transaction_id), 256), code=Disassembly(contract_initialization_code), caller=BitVecVal(CREATOR_ADDRESS, 256), callee_account=new_account, call_data=[], call_data_type=CalldataType.SYMBOLIC, call_value=BitVec("call_value{}".format(next_transaction_id), 256), ) _setup_global_state_for_execution(laser_evm, transaction) laser_evm.exec(True) return new_account
def execute_message_call(laser_evm, callee_address: str) -> None: """ Executes a message call transaction from all open states """ # TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here open_states = laser_evm.open_states[:] del laser_evm.open_states[:] for open_world_state in open_states: if open_world_state[callee_address].deleted: debug("Can not execute dead contract, skipping.") continue next_transaction_id = get_next_transaction_id() transaction = MessageCallTransaction( world_state=open_world_state, identifier=next_transaction_id, gas_price=BitVec("gas_price{}".format(next_transaction_id), 256), gas_limit=8000000, # block gas limit origin=BitVec("origin{}".format(next_transaction_id), 256), caller=BitVecVal(ATTACKER_ADDRESS, 256), callee_account=open_world_state[callee_address], call_data=SymbolicCalldata(next_transaction_id), call_data_type=CalldataType.SYMBOLIC, call_value=BitVec("call_value{}".format(next_transaction_id), 256), ) _setup_global_state_for_execution(laser_evm, transaction) laser_evm.exec()
def BYTE(i, x): bit = (i + 1) * 8 return If( UGT(i, x.size() / 8 - 1), BitVecVal(0, x.size()), (LShR(x, (x.size() - bit))) & 0xff )
def sha3_(self, global_state): global keccak_function_manager state = global_state.mstate environment = global_state.environment op0, op1 = state.stack.pop(), state.stack.pop() try: index, length = util.get_concrete_int(op0), util.get_concrete_int(op1) # FIXME: broad exception catch except: # Can't access symbolic memory offsets if is_expr(op0): op0 = simplify(op0) state.stack.append(BitVec("KECCAC_mem[" + str(op0) + "]", 256)) return [global_state] try: state.mem_extend(index, length) data = b''.join([util.get_concrete_int(i).to_bytes(1, byteorder='big') for i in state.memory[index: index + length]]) except AttributeError: argument = str(state.memory[index]).replace(" ", "_") result = BitVec("KECCAC[{}]".format(argument), 256) keccak_function_manager.add_keccak(result, state.memory[index]) state.stack.append(result) return [global_state] keccak = utils.sha3(utils.bytearray_to_bytestr(data)) logging.debug("Computed SHA3 Hash: " + str(binascii.hexlify(keccak))) state.stack.append(BitVecVal(util.concrete_int_from_bytes(keccak, 0), 256)) return [global_state]
def __init__( self, active_account, sender, calldata, gasprice, callvalue, origin, code=None, calldata_type=CalldataType.SYMBOLIC, ): # Metadata self.active_account = active_account self.active_function_name = "" self.address = BitVecVal(int(active_account.address, 16), 256) # Ib self.code = active_account.code if code is None else code self.sender = sender self.calldata = calldata self.calldata_type = calldata_type self.gasprice = gasprice self.origin = origin self.callvalue = callvalue
def eq_(self, global_state): state = global_state.mstate 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) return [global_state]
def zero_extension(formula, bit_places): """Set the rest of bits on the left to 0. """ complement = BitVecVal(0, formula.size() - bit_places) formula = Concat(complement, (Extract(bit_places - 1, 0, formula))) return formula
def _load(self, item: Union[int, ExprRef], clean=False) -> Any: x = BitVecVal(item, 256) if isinstance(item, int) else item symbolic_base_value = If( x > self._size, BitVecVal(0, 8), BitVec("{}_calldata_{}".format(self.tx_id, str(item)), 8), ) return_value = symbolic_base_value for r_index, r_value in self._reads: return_value = If(r_index == item, r_value, return_value) if not clean: self._reads.append((item, symbolic_base_value)) return simplify(return_value)
def stop_(self, global_state): state = global_state.mstate state.stack.append(BitVecVal(0, 256)) if len(global_state.call_stack) is 0: return [] global_state.mstate.pc = global_state.call_stack.pop() return [global_state]
def test_instruction_semantics_jl(self): # jl #0x124e raw = b'\x0c\x38' ip = 0x1234 ins, ins_len = decode_instruction(ip, raw) state = blank_state() state.cpu.registers['R0'] = BitVecVal( ip + ins_len, 16) # ip is always preincremented expected_taken = 0x124e expected_not_taken = 0x1236 new_states = state.cpu.step_jl(state, ins) self.assertEqual(len(new_states), 2) taken_states = [ st for st in new_states if intval(st.cpu.registers['R0']) == expected_taken ] not_taken_states = [ st for st in new_states if intval(st.cpu.registers['R0']) == expected_not_taken ] self.assertEqual(len(taken_states), 1) self.assertEqual(len(not_taken_states), 1)
def sdiv_(self, global_state): s0, s1 = util.pop_bitvec(global_state.mstate), util.pop_bitvec(global_state.mstate) if s1 == 0: global_state.mstate.stack.append(BitVecVal(0, 256)) else: global_state.mstate.stack.append(s0 / s1) return [global_state]
def test_checkBitVecRef256(): from z3 import BitVec, BitVecVal print(checkBitVecRef256(BitVecVal(111,256))) print(checkBitVecRef256(BitVec('x',256))) #print(isBitVecRef256(34))a print(checkBitVecRef256(BitVec('y',128))) print('hoge')
def array_to_bv64(array): return Concat(Select(array, BitVecVal(7, 32)), Select(array, BitVecVal(6, 32)), Select(array, BitVecVal(5, 32)), Select(array, BitVecVal(4, 32)), Select(array, BitVecVal(3, 32)), Select(array, BitVecVal(2, 32)), Select(array, BitVecVal(1, 32)), Select(array, BitVecVal(0, 32)))
def __init__(self, value, size): Expr.__init__(self) if (isinstance(value, int) or isinstance(value, long)): self.value = value else: self.value = int(value, 16) self.size = size self.z3 = BitVecVal(value, size)
def calldatasize_(self, global_state): state = global_state.mstate environment = global_state.environment 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)) return [global_state]
def calldatasize(self) -> ExprRef: """ :return: Calldata size for this calldata object """ result = self.size if isinstance(result, int): return BitVecVal(result, 256) return result
def call_(self, global_state): instr = global_state.get_current_instruction() environment = global_state.environment try: callee_address, callee_account, call_data, value, call_data_type, gas, memory_out_offset, memory_out_size = get_call_parameters( global_state, self.dynamic_loader, True) except ValueError as e: logging.info( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(e) ) # TODO: decide what to do in this case global_state.mstate.stack.append(global_state.new_bitvec("retval_" + str(instr['address']), 256)) return [global_state] global_state.mstate.stack.append(global_state.new_bitvec("retval_" + str(instr['address']), 256)) if 0 < int(callee_address, 16) < 5: logging.info("Native contract called: " + callee_address) if call_data == [] and call_data_type == CalldataType.SYMBOLIC: logging.debug("CALL with symbolic data not supported") return [global_state] try: mem_out_start = helper.get_concrete_int(memory_out_offset) mem_out_sz = memory_out_size.as_long() except AttributeError: logging.debug("CALL with symbolic start or offset not supported") return [global_state] global_state.mstate.mem_extend(mem_out_start, mem_out_sz) call_address_int = int(callee_address, 16) try: data = natives.native_contracts(call_address_int, call_data) except natives.NativeContractException: contract_list = ['ecerecover', 'sha256', 'ripemd160', 'identity'] for i in range(mem_out_sz): global_state.mstate.memory[mem_out_start + i] = global_state.new_bitvec(contract_list[call_address_int - 1] + "(" + str(call_data) + ")", 256) return [global_state] for i in range(min(len(data), mem_out_sz)): # If more data is used then it's chopped off global_state.mstate.memory[mem_out_start + i] = data[i] # TODO: maybe use BitVec here constrained to 1 return [global_state] transaction = MessageCallTransaction(global_state.world_state, callee_account, BitVecVal(int(environment.active_account.address, 16), 256), call_data=call_data, gas_price=environment.gasprice, call_value=value, origin=environment.origin, call_data_type=call_data_type) raise TransactionStartSignal(transaction, self.op_code)
def extract_function_id(self, condition, block_state): if self.extracting_fid: m = get_model_and_time(condition) if m: fid = m[BitVec('function_id', 32)] # the case contract has only one function if fid is None: self.function_ids.add(BitVecVal(0, 32)) if 'callable' in block_state: self.callable_function_ids.add(BitVecVal(0, 32)) else: self.function_ids.add(fid) if 'callable' in block_state: self.callable_function_ids.add(fid) return True return False
def sanitize_expr(exp): """ Converts ints to bit vector expressions. :param exp: :return: """ if type(exp) in [int]: return BitVecVal(exp, 256) return exp
def right_one_extension(formula, bit_places): """Set the rest of bits on the right to 1. """ complement = BitVecVal(0, formula.size() - bit_places) - 1 formula = Concat( Extract(formula.size() - 1, formula.size() - bit_places, formula), complement) return formula
def set_region_bit(bv, p): i = region_names.index(REGIONS[p.y][p.x]) chunks = [] if i < bits - 1: chunks.append(Extract(bits - 1, i + 1, bv)) chunks.append(BitVecVal(1, 1)) if i > 0: chunks.append(Extract(i - 1, 0, bv)) return Concat(*chunks)
def _analyze_state(state): instruction = state.get_current_instruction() node = state.node if instruction["opcode"] != "CALL": return [] call_value = state.mstate.stack[-3] target = state.mstate.stack[-2] eth_sent_total = BitVecVal(0, 256) constraints = copy(node.constraints) for tx in state.world_state.transaction_sequence: if tx.caller == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF: # There's sometimes no overflow check on balances added. # But we don't care about attacks that require more 2^^256 ETH to be sent. constraints += [ BVAddNoOverflow(eth_sent_total, tx.call_value, False) ] eth_sent_total = Sum(eth_sent_total, tx.call_value) constraints += [ UGT(call_value, eth_sent_total), target == state.environment.sender ] try: transaction_sequence = solver.get_transaction_sequence( state, constraints) debug = str(transaction_sequence) issue = Issue( contract=node.contract_name, function_name=node.function_name, address=instruction["address"], swc_id=UNPROTECTED_ETHER_WITHDRAWAL, title="Ether thief", _type="Warning", bytecode=state.environment.code.bytecode, description= "Arbitrary senders other than the contract creator can withdraw ETH from the contract" + " account without previously having sent an equivalent amount of ETH to it. This is likely to be" + " a vulnerability.", debug=debug, gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), ) except UnsatError: logging.debug("[ETHER_THIEF] no model found") return [] return [issue]