def init_contexts(): logger.debug("Iterating contexts for call level: %d", depth) if not depth: yield init_context return func = utils.Function(ea) yielded = False for call_ea in func.calls_to: if call_ea in func: logger.warning( "Ignoring recursive function call at 0x%08X", call_ea) continue if not ida_funcs.get_func(call_ea): logger.warning( "Ignoring call at 0x%08X. Not in a function", call_ea) continue for context in self.iter_context_at(call_ea, depth=depth - 1, exhaustive=exhaustive, follow_loops=follow_loops, init_context=init_context, _first_call=False): if issubclass(self._context_class, x86_64ProcessorContext): # increase the sp to account for the return address that gets pushed # onto the stack so that we are aligned correctly. context.sp -= context.byteness # yield a context containing the caller executed first. yield context yielded = True # If we didn't yield, then we hit a function that has no callers or valid contexts. if not yielded: yield init_context
def test_context_depth(): """Tests depth feature in iter_context_at()""" from kordesii import utils from kordesii.utils import function_tracing emulator = function_tracing.Emulator() ea = 0x00405901 # Address in function that contains multiple paths. num_paths_first_depth = 3 num_paths_second_depth = 25 # First ensure paths are calculated correctly. flowchart = utils.Flowchart(ea) block = flowchart.find_block(ea) assert len(list(block.paths())) == num_paths_first_depth func = utils.Function(ea) call_eas = list(func.calls_to) assert len(call_eas) == 1 call_ea = call_eas[0] flowchart = utils.Flowchart(call_ea) block = flowchart.find_block(call_ea) assert len(list(block.paths())) == num_paths_second_depth # Now show that we get the correct number of contexts based on depth and other parameters. # Test getting contexts for only the first depth. ctxs = list(emulator.iter_context_at(ea)) assert len(ctxs) == num_paths_first_depth # (exhaustive has no affect on final call level) ctxs = list(emulator.iter_context_at(ea, exhaustive=False)) assert len(ctxs) == num_paths_first_depth # Test getting contexts with 2 depths. ctxs = list(emulator.iter_context_at(ea, depth=1)) assert len(ctxs) == num_paths_first_depth * num_paths_second_depth ctxs = list(emulator.iter_context_at(ea, depth=1, exhaustive=False)) assert len(ctxs) == num_paths_first_depth
def create_emulated(self, func_ea, return_type=None, return_size=None, enforce_args=False): """ Creates a Python function that emulates the execution of the given function. :param func_ea: Address of function to emulate. :param return_type: If set, return value will be dereferenced using set type before returning. (e.g. function_tracing.DWORD) :param return_size: If set, return value will be dereferenced as a bytes string of the given size before returning. :param enforce_args: Whether to enforce that the correct number of positional arguments was provided. :returns: A python function that emulates the function. This function will accept the same number (and type) of arguments as the emulate function as well as return the result as the emulated function. :raises RuntimeError: If the maximum number of allowed instruction executions have been reached. """ func_obj = utils.Function(func_ea) def emulated_function(*args, context: ProcessorContext = None): """ Emulates a function and returns result in rax. :param *args: Arguments to pass into function before emulation. If enforce_args is not enabled, the number of arguments provided can be less or more than the number of arguments required by the function. Any arguments not provided will default to whatever is set in the context. :param context: CPU context to use. If not provided an empty context will be used. If you would like to examine the context after emulation, you must provide your own. :returns: Value or derefernced value in rax. :raises TypeError: If enforce_args is enabled and incorrect number of positional args have been provided. """ if context and context.emulator != self: raise ValueError( "Supplied context must be created from same emulator.") context = context or self.new_context() # (ip must be set in order to get correct function arguments in signature.) context.ip = func_obj.start_ea # Temporarily turn off branch tracking since it is unneeded and will just waste time. orig_branch_tracking = self.branch_tracking self.branch_tracking = False # Fill in context with argument values. func_sig = context.get_function_signature(func_obj.start_ea) if enforce_args and len(func_sig.args) != len(args): raise TypeError( f"Function takes {len(func_sig.args)} positional arguments, but {len(args)} were given." ) logger.debug(f'Emulating {func_sig.name}') for arg, func_arg in zip(args, func_sig.args): if isinstance(arg, int): logger.debug( f'Setting argument {func_arg.name} = {repr(arg)}') func_arg.value = arg elif isinstance(arg, bytes): ptr = context.mem_alloc(len(arg)) context.mem_write(ptr, arg) logger.debug( f'Setting argument {func_arg.name} = {hex(ptr)} ({repr(arg)})' ) func_arg.value = ptr else: raise TypeError(f'Invalid arg type {type(arg)}') context.execute(func_obj.start_ea, end=func_obj.end_ea, max_instructions=self.max_instructions) if return_type is not None or return_size is not None: result = context.read_data(context.ret, size=return_size, data_type=return_type) else: result = context.ret logger.debug(f'Returned: {repr(result)}') self.branch_tracking = orig_branch_tracking return result return emulated_function
def _execute_to(self, ea, *, context: ProcessorContext = None) -> ProcessorContext: """ Creates a cpu_context (or emulates on top of the given one) for instructions up to, but not including, the given ea within the current function. This function is a hybrid approach to the non-loop following mode in which it will force the other branch to be taken if the branch it wants to take will not lead to the desired end address. This is an internal function used as a helper for iter_context_at() when following loops. :param int ea: ea of interest :param context: ProcessorContext to use during emulation, a new one will be created if not provided. :raises RuntimeError: If maximum number of instructions have been hit. """ if not context: context = self.new_context() flowchart = Flowchart.from_cache(ea) func_obj = utils.Function(ea) start_block = flowchart.find_block(func_obj.start_ea) end_block = flowchart.find_block(ea) valid_blocks = end_block.ancestors() valid_blocks.add(end_block) count = self.max_instructions # Starting from start_block, we are going to emulate each instruction in each basic block # until we get to the end_block. # If execution tries to branch us into a block that can't lead us to the end_block, # we will force the branch to go in the other direction. current_block = start_block while current_block != end_block: # We can't use execute() with start and end here because the end_ea of a block # is not actually in the block. for _ea in current_block.heads(): context.execute(_ea) count -= 1 if count <= 0: raise RuntimeError("Hit maximum number of instructions.") # Get the successor block that execution branched to as well as # is a valid block that can reach the end block. # If no such block exists, just pick the first valid successor block. valid_successors = [ bb for bb in current_block.succs() if bb in valid_blocks ] assert valid_successors, "Expected there to be at least 1 valid successor block." for successor in valid_successors: if context.ip == successor.start_ea: break else: # If no valid successor, force branch. successor = valid_successors[0] context.ip = successor.start_ea context.prep_for_branch(successor.start_ea) current_block = successor # Emulate the instructions in the final block. context.execute(start=current_block.start_ea, end=ea) return context
def test_function_signature(): """Tests FunctionSignature object.""" import idc from kordesii import utils from kordesii.utils import function_tracing emulator = function_tracing.Emulator() xor_func_ea = 0x00401000 # Basic tests. context = emulator.context_at(xor_func_ea) func_sig = context.get_function_signature(func_ea=xor_func_ea) assert func_sig.declaration == "_BYTE *__cdecl sub_401000(_BYTE *a1, char a2);" assert func_sig.arg_types == ("_BYTE * a1", "char a2") args = func_sig.args assert len(args) == 2 assert args[0].name == "a1" assert args[0].type == "_BYTE *" assert args[0].value == 0 assert args[1].name == "a2" assert args[1].type == "char" assert args[1].value == 0 # Test that we can manipulate signature. func_sig.arg_types += ("int new_arg",) assert func_sig.declaration == "_BYTE *__cdecl sub_401000(_BYTE *a1, char a2, int new_arg);" args = func_sig.args assert len(args) == 3 assert args[2].name == "new_arg" assert args[2].type == "int" assert args[2].value == 0 # Now test using iter_function_args # First force an incorrect number of arguments. idc.SetType(xor_func_ea, " _BYTE *__cdecl sub_401000(_BYTE *a1)") func = utils.Function(xor_func_ea) # Then test we can force 2 arguments anyway. results = [] for ea in func.calls_to: for context in emulator.iter_context_at(ea): # The function signature only gives 1 argument now. func_sig = context.get_function_signature() assert len(func_sig.args) == 1 # But we force 2. args = context.get_function_args(num_args=2) assert len(args) == 2 results.append(args) assert results == [ [4243456, 1], [4243472, 2], [4243500, 3], [4243548, 4], [4243584, 5], [4243616, 6], [4243652, 19], [4243696, 23], [4243732, 26], [4243744, 35], [4243760, 39], [4243768, 64], [4243776, 70], [4243804, 115], [4243828, 117], [4243868, 119], [4243908, 122], [4243960, 127], ] # Test that we can force function signatures. with pytest.raises(RuntimeError): context.get_function_args(0xFFF) with pytest.raises(RuntimeError): context.get_function_signature(0xFFF) assert len(context.get_function_args(0xFFF, num_args=3)) == 3 func_sig = context.get_function_signature(0xFFF, force=True) assert func_sig.declaration == 'int __cdecl no_name();'