From f141b73c7dc00f3a127e0ab8c4eb4c800c2145ab Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 24 Mar 2025 00:43:47 +0100 Subject: [PATCH 01/35] Add HeapReAlloc hook --- qiling/os/windows/dlls/kernel32/heapapi.py | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/qiling/os/windows/dlls/kernel32/heapapi.py b/qiling/os/windows/dlls/kernel32/heapapi.py index c48e5aa8f..6746c6d8c 100644 --- a/qiling/os/windows/dlls/kernel32/heapapi.py +++ b/qiling/os/windows/dlls/kernel32/heapapi.py @@ -70,6 +70,36 @@ def hook_HeapAlloc(ql: Qiling, address: int, params): return ptr +# DECLSPEC_ALLOCATOR LPVOID HeapReAlloc( +# HANDLE hHeap, +# DWORD dwFlags, +# _Frees_ptr_opt_ LPVOID lpMem, +# SIZE_T dwBytes +# ); +@winsdkapi(cc=STDCALL, params={ + 'hHeap' : HANDLE, + 'dwFlags' : DWORD, + 'lpMem': LPVOID, + 'dwBytes' : SIZE_T +}) +def hook_HeapReAlloc(ql: Qiling, address: int, params): + base = params["lpMem"] + newSize = params["dwBytes"] + + oldSize = ql.os.heap.size(base) + oldData = bytes(ql.mem.read(base, oldSize)) + + ql.os.heap.free(base) + + if newSize < oldSize: + oldData = oldData[0:newSize] + + newBase = ql.os.heap.alloc(newSize) + if newBase: + ql.mem.write(newBase, oldData) + + return newBase + # SIZE_T HeapSize( # HANDLE hHeap, # DWORD dwFlags, From 742630f724f4e1faecc329bff275bb78d373a886 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 24 Mar 2025 00:45:33 +0100 Subject: [PATCH 02/35] Add _realloc_base hook --- qiling/os/windows/dlls/msvcrt.py | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 2db18455f..fffc58c08 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -498,6 +498,42 @@ def hook_malloc(ql: Qiling, address: int, params): return ql.os.heap.alloc(size) +def __realloc(ql: Qiling, address: int, params): + block = params['block'] + size = params['size'] + + if not block: + return ql.os.heap.alloc(size) + + if size == 0: + ql.os.heap.free(block) + return 0 + + oldSize = ql.os.heap.size(block) + oldData = bytes(ql.mem.read(block, size)) + ql.os.heap.free(block) + + if size < oldSize: + oldData = oldData[0:size] + + newBase = ql.os.heap.alloc(size) + + if newBase: + ql.mem.write(newBase, oldData) + + return newBase + +# void* __cdecl _realloc_base( +# void* const block, +# size_t const size +# ) +@winsdkapi(cc=CDECL, params={ + 'block' : POINTER, + 'size' : UINT +}) +def hook__realloc_base(ql: Qiling, address: int, params): + return __realloc(ql, address, params) + def __free(ql: Qiling, address: int, params): address = params['address'] From 8101061622a40f35508731fdc22aa8e660a60fe9 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 24 Mar 2025 00:46:29 +0100 Subject: [PATCH 03/35] Add RtlPcToFileHeader hook --- qiling/os/windows/dlls/ntdll.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 92ba4a84b..5560308cd 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -452,4 +452,26 @@ def hook_wcsstr(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={}) def hook_CsrGetProcessId(ql: Qiling, address: int, params): pid = ql.os.profile["PROCESSES"].getint("csrss.exe", fallback=12345) - return pid \ No newline at end of file + return pid + +# NTSYSAPI PVOID RtlPcToFileHeader( +# [in] PVOID PcValue, +# [out] PVOID *BaseOfImage +# ); +@winsdkapi(cc=STDCALL, params={ + 'PcValue' : PVOID, + 'BaseOfImage': PVOID +}) +def hook_RtlPcToFileHeader(ql: Qiling, address: int, params): + pc = params["PcValue"] + base_of_image_ptr = params["BaseOfImage"] + + containing_image = ql.loader.find_containing_image(pc) + + if containing_image: + base_addr = containing_image.base + else: + base_addr = 0 + + ql.mem.write_ptr(base_of_image_ptr, base_addr) + return base_addr From fb8128f42c20bcddecca5c83216a1d164257d98b Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 24 Mar 2025 00:47:37 +0100 Subject: [PATCH 04/35] Remove _initterm and _initterm_e hooks --- qiling/os/windows/dlls/msvcrt.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index fffc58c08..48cad23d8 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -174,17 +174,6 @@ def hook_puts(ql: Qiling, address: int, params): def hook__cexit(ql: Qiling, address: int, params): pass -# void __cdecl _initterm( -# PVFV *, -# PVFV * -# ); -@winsdkapi(cc=CDECL, params={ - 'pfbegin' : POINTER, - 'pfend' : POINTER -}) -def hook__initterm(ql: Qiling, address: int, params): - return 0 - # void exit( # int const status # ); @@ -194,17 +183,6 @@ def hook__initterm(ql: Qiling, address: int, params): def hook_exit(ql: Qiling, address: int, params): ql.emu_stop() -# int __cdecl _initterm_e( -# PVFV *, -# PVFV * -# ); -@winsdkapi(cc=CDECL, params={ - 'pfbegin' : POINTER, - 'pfend' : POINTER -}) -def hook__initterm_e(ql: Qiling, address: int, params): - return 0 - # char*** __cdecl __p___argv (void); @winsdkapi(cc=CDECL, params={}) def hook___p___argv(ql: Qiling, address: int, params): From 84b02eeb1ab2474be9502803290d79be58fc9d78 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 24 Mar 2025 00:48:45 +0100 Subject: [PATCH 05/35] Remove __acrt_iob_func hook --- qiling/os/windows/dlls/msvcrt.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 48cad23d8..9b9e1bba4 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -281,13 +281,6 @@ def hook_wprintf(ql: Qiling, address: int, params): return count -# MSVCRT_FILE * CDECL MSVCRT___acrt_iob_func(unsigned idx) -@winsdkapi(cc=CDECL, params={ - 'idx': UINT -}) -def hook___acrt_iob_func(ql: Qiling, address: int, params): - return 0 - def __stdio_common_vfprintf(ql: Qiling, address: int, params, wstring: bool): format = params['_Format'] arglist = params['_ArgList'] From 69a6906c0b8960e73186b808dead92a254d50a28 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Thu, 27 Mar 2025 21:32:25 +0100 Subject: [PATCH 06/35] Add function table parsing and lookup to the PE loader --- qiling/loader/pe.py | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 7e6746de6..8a4b94a8e 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -79,6 +79,12 @@ class Process: export_symbols: MutableMapping[int, Dict[str, Any]] libcache: Optional[QlPeCache] + # maps image base to RVA of its function table + function_table_lookup: MutableMapping[int, int] + + # maps image base to its list of function table entries + function_tables: MutableMapping[int, list] + def __init__(self, ql: Qiling): self.ql = ql @@ -105,6 +111,67 @@ def __get_path_elements(self, name: str) -> Tuple[str, str]: vpath = ntpath.join(dirname, basename) return self.ql.os.path.virtual_to_host_path(vpath), basename.casefold() + + def init_function_tables(self, pe: pefile.PE, image_base: int): + """Parse function table data for the given PE file. + Only works for x64 images. + + Args: + pe: the PE image whose function data should be parsed + image_base: the absolute address at which the image was loaded + """ + if self.ql.arch.type == QL_ARCH.X8664: + + # Check if the PE file has an exception directory + if hasattr(pe, 'DIRECTORY_ENTRY_EXCEPTION'): + exception_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[ + pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXCEPTION'] + ] + + self.function_table_lookup[image_base] = exception_dir.VirtualAddress + + runtime_function_list = [] + + for _, exception_entry in enumerate(pe.DIRECTORY_ENTRY_EXCEPTION, start=1): + runtime_function_list.append(exception_entry) + + if self.function_tables.get(image_base) is None: + self.function_tables[image_base] = [] + + self.function_tables[image_base].extend(runtime_function_list) + + self.ql.log.debug(f'Parsed {len(runtime_function_list)} exception directory entries') + + else: + self.ql.log.debug(f'Image has no exception directory; skipping exception data') + + def lookup_function_entry(self, base_addr: int, control_pc: int): + """Look up a RUNTIME_FUNCTION entry and its index in a module's + function table, such that the given program counter falls within + the entry's begin and end range. + + Args: + base_addr: The base address of the image whose exception directory to search. + control_pc: The program counter. + + Returns: + A tuple (index, runtime_function) + """ + function_table = self.function_tables[base_addr] + + # Initiate a search of the function table for a RUNTIME_FUNCTION + # entry such that the provided PC falls within its start and end range. + for i, runtime_function in enumerate(function_table): + + # Begin and end addresses exist in the entry as RVAs, + # convert them to absolute addresses. + begin_addr = base_addr + runtime_function.struct.BeginAddress + end_addr = base_addr + runtime_function.struct.EndAddress + + if begin_addr <= control_pc < end_addr: + return i, runtime_function + + return None, None def load_dll(self, name: str, is_driver: bool = False) -> int: dll_path, dll_name = self.__get_path_elements(name) @@ -195,6 +262,9 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: with ShowProgress(self.ql.log, 0.1337): dll.relocate_image(image_base) + # initialize the function tables only after possible relocation + self.init_function_tables(dll, image_base) + data = bytearray(dll.get_memory_mapped_image()) assert image_size >= len(data) @@ -709,6 +779,8 @@ def run(self): self.export_symbols = {} self.import_address_table = {} self.ldr_list = [] + self.function_tables = {} + self.function_table_lookup = {} self.pe_image_address = 0 self.pe_image_size = 0 self.dll_size = 0 @@ -841,6 +913,9 @@ def load(self, pe: Optional[pefile.PE]): # set up call frame for DllMain self.ql.os.fcall.call_native(self.entry_point, args, None) + # Initialize the function tables + super().init_function_tables(pe, image_base) + elif pe is None: self.ql.mem.map(self.entry_point, self.ql.os.code_ram_size, info="[shellcode]") From 1f45f9bda36e6177f0d840e3a850d58bd4817e6f Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Thu, 27 Mar 2025 21:35:36 +0100 Subject: [PATCH 07/35] Adjust segment descriptors for x86_64 --- qiling/arch/x86_utils.py | 51 +++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/qiling/arch/x86_utils.py b/qiling/arch/x86_utils.py index 4726ef745..dfd45623d 100644 --- a/qiling/arch/x86_utils.py +++ b/qiling/arch/x86_utils.py @@ -5,6 +5,7 @@ from qiling import Qiling from qiling.arch.x86 import QlArchIntel from qiling.arch.x86_const import * +from qiling.const import QL_ARCH from qiling.exception import QlGDTError, QlMemoryMappedError from qiling.os.memory import QlMemoryManager @@ -60,6 +61,11 @@ def __init__(self, ql: Qiling, base=QL_X86_GDT_ADDR, limit=QL_X86_GDT_LIMIT, num # setup GDT by writing to GDTR ql.arch.regs.write(UC_X86_REG_GDTR, (0, base, limit, 0x0)) + if ql.arch.type == QL_ARCH.X8664: + self.is_long_mode = True + else: + self.is_long_mode = False + self.array = GDTArray(ql.mem, base, num_entries) @staticmethod @@ -93,7 +99,18 @@ def make_selector(idx: int, rpl: int) -> int: return (idx << 3) | QL_X86_SEGSEL_TI_GDT | rpl def register_gdt_segment(self, index: int, seg_base: int, seg_limit: int, access: int) -> int: - flags = QL_X86_F_OPSIZE_32 + is_code = access & QL_X86_A_CODE + + if is_code and self.is_long_mode: + # If this is a code segment and 64-bit long mode is enabled, + # then set the long segment descriptor bit. + # This prevents some strange CPU errors encountered with + # intra-privilege level IRET instructions used for + # context switching on 64-bit Windows. + flags = QL_X86_F_LONG + else: + # Otherwise, OPSIZE_32 should be set. + flags = QL_X86_F_OPSIZE_32 # is this a huge segment? if seg_limit > (1 << 16): @@ -138,16 +155,21 @@ def setup_gs(self, base: int, size: int) -> None: class SegmentManager86(SegmentManager): def setup_cs_ds_ss_es(self, base: int, size: int) -> None: - # While debugging the linux kernel segment, the cs segment was found on the third segment of gdt. + # TODO: 64-bit code segment access bits were adjusted, removing the conforming bit. + # Perhaps make the same change for x86? access = QL_X86_A_PRESENT | QL_X86_A_PRIV_3 | QL_X86_A_DESC_CODE | QL_X86_A_CODE | QL_X86_A_CODE_C | QL_X86_A_CODE_R + # While debugging the linux kernel segment, the cs segment was found on the third segment of gdt. selector = self.gdtm.register_gdt_segment(3, base, size - 1, access) self.arch.regs.cs = selector # TODO : The section permission here should be QL_X86_A_PRIV_3, but I do n’t know why it can only be set to QL_X86_A_PRIV_0. + # TODO: 64-bit data segment access bits were adjusted, removing the direction bit. + # After this change, there were no problems changing the privilege level to ring 3. + # Perhaps make the same change for x86? + access = QL_X86_A_PRESENT | QL_X86_A_PRIV_0 | QL_X86_A_DESC_DATA | QL_X86_A_DATA | QL_X86_A_DATA_E | QL_X86_A_DATA_W # While debugging the Linux kernel segment, I found that the three segments DS, SS, and ES all point to the same location in the GDT table. # This position is the fifth segment table of GDT. - access = QL_X86_A_PRESENT | QL_X86_A_PRIV_0 | QL_X86_A_DESC_DATA | QL_X86_A_DATA | QL_X86_A_DATA_E | QL_X86_A_DATA_W selector = self.gdtm.register_gdt_segment(5, base, size - 1, access) self.arch.regs.ds = selector @@ -169,15 +191,32 @@ def setup_gs(self, base: int, size: int) -> None: class SegmentManager64(SegmentManager): def setup_cs_ds_ss_es(self, base: int, size: int) -> None: + # Code segment access bits: + # * QL_X86_A_PRESENT : Present + # * QL_X86_A_PRIV_3 : Ring 3 (user-mode) + # * QL_X86_A_DESC_CODE : Segment describes a code segment + # * QL_X86_A_CODE : Executable bit set + # * QL_X86_A_CODE_R : Readable + # Not set: + # * QL_X86_A_CODE_C : Conforming bit + # -> unset means code in this segment can only be executed from the ring set in DPL. + access = QL_X86_A_PRESENT | QL_X86_A_PRIV_3 | QL_X86_A_DESC_CODE | QL_X86_A_CODE | QL_X86_A_CODE_R # While debugging the linux kernel segment, the cs segment was found on the sixth segment of gdt. - access = QL_X86_A_PRESENT | QL_X86_A_PRIV_3 | QL_X86_A_DESC_CODE | QL_X86_A_CODE | QL_X86_A_CODE_C | QL_X86_A_CODE_R selector = self.gdtm.register_gdt_segment(6, base, size - 1, access) self.arch.regs.cs = selector - # TODO : The section permission here should be QL_X86_A_PRIV_3, but I do n’t know why it can only be set to QL_X86_A_PRIV_0. + # Data segment access bits: + # * QL_X86_A_PRESENT : Present + # * QL_X86_A_PRIV_3 : Ring 3 (user-mode) + # * QL_X86_A_DESC_DATA : Segment describes a data segment + # * QL_X86_A_DATA : Executable bit NOT set + # * QL_X86_A_DATA_W : Writable + # Not set: + # * QL_X86_A_DATA_E : Direction bit + # -> unset means the data segment grows upward, rather than downward. + access = QL_X86_A_PRESENT | QL_X86_A_PRIV_3 | QL_X86_A_DESC_DATA | QL_X86_A_DATA | QL_X86_A_DATA_W # When I debug the Linux kernel, I find that only the SS is set to the fifth segment table, and the rest are not set. - access = QL_X86_A_PRESENT | QL_X86_A_PRIV_0 | QL_X86_A_DESC_DATA | QL_X86_A_DATA | QL_X86_A_DATA_E | QL_X86_A_DATA_W selector = self.gdtm.register_gdt_segment(5, base, size - 1, access) # self.arch.regs.ds = selector From 27db58de8615d9d3fc3875c603ad72a70cea2b08 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Thu, 27 Mar 2025 21:37:10 +0100 Subject: [PATCH 08/35] Add hooks for function table lookup functions in ntdll --- qiling/os/windows/dlls/ntdll.py | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 5560308cd..f1e95fa71 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -17,6 +17,7 @@ from qiling.os.windows import structs from qiling.os.windows import utils +from unicorn.x86_const import * # void *memcpy( # void *dest, @@ -475,3 +476,125 @@ def hook_RtlPcToFileHeader(ql: Qiling, address: int, params): ql.mem.write_ptr(base_of_image_ptr, base_addr) return base_addr + +# NTSYSAPI PRUNTIME_FUNCTION RtlLookupFunctionEntry( +# [in] DWORD64 ControlPc, +# [out] PDWORD64 ImageBase, +# [out] PUNWIND_HISTORY_TABLE HistoryTable +# ); +@winsdkapi(cc=STDCALL, params={ + 'ControlPc': PVOID, + 'ImageBase': PVOID, + 'HistoryTable': PVOID +}) +def hook_RtlLookupFunctionEntry(ql: Qiling, address: int, params): + control_pc = params["ControlPc"] + image_base_ptr = params["ImageBase"] + + # TODO: Make use of the history table to optimize this function. + # Alternatively, we could add caching to the loader, seeing as the + # loader is responsible for lookups in the function table. + + # For simplicity, we are going to ignore the history table. + # history_table_ptr = params["HistoryTable"] + + # This function should not be getting called on x86. + if ql.arch.type != QL_ARCH.X8664: + return QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") + + containing_image = ql.loader.find_containing_image(control_pc) + + if containing_image: + base_addr = containing_image.base + else: + base_addr = 0 + + return 0 + + # If we got a valid location to write the image base ptr, + # copy it there, and proceed. + if image_base_ptr != 0: + ql.mem.write_ptr(image_base_ptr, base_addr) + + # Get the base address of the function table. + function_table_addr = base_addr + ql.loader.function_table_lookup[base_addr] + + # Look up the RUNTIME_FUNCTION entry; we are interested in the index in the table + # so that we can compute the address. + runtime_function_idx, runtime_function = ql.loader.lookup_function_entry(base_addr, control_pc) + + # If a suitable function entry was found, + # compute its address and return. + if runtime_function: + return function_table_addr + runtime_function_idx * 12 # sizeof(RUNTIME_FUNCTION) + + return 0 + +# NTSYSAPI +# PRUNTIME_FUNCTION +# RtlLookupFunctionTable ( +# IN PVOID ControlPc, +# OUT PVOID *ImageBase, +# OUT PULONG SizeOfTable +# ); +@winsdkapi(cc=STDCALL, params={ + 'ControlPc': PVOID, + 'ImageBase': PVOID, + 'SizeOfTable': PVOID +}) +def hook_RtlLookupFunctionTable(ql: Qiling, address: int, params): + control_pc = params["ControlPc"] + image_base_ptr = params["ImageBase"] + size_of_table_ptr = params["SizeOfTable"] + + # This function should not be getting called on x86. + if ql.arch.type != QL_ARCH.X8664: + return QlErrorNotImplemented("RtlLookupFunctionTable is not implemented for x86") + + containing_image = ql.loader.find_containing_image(control_pc) + + if containing_image: + base_addr = containing_image.base + else: + base_addr = 0 + + return 0 + + # If we got a valid location to write the image base ptr, + # copy it there, and proceed. + if image_base_ptr != 0: + ql.mem.write_ptr(image_base_ptr, base_addr) + + # If image base was 0, we are not going to find a valid function + # table anyway, so just return. + if base_addr == 0: + return 0 + + # Look up the RVA of the function table. + function_table_rva = ql.loader.function_table_lookup[base_addr] + + # The caller is expecting a pointer, so convert the RVA + # to an absolute address. + function_table_addr = int(base_addr + function_table_rva) + + # If a valid pointer for the size was provided, + # we want to figure out the size of the table. + if size_of_table_ptr != 0: + # Look up the function table from the loader, + # and get the number of entries. + function_table = ql.loader.function_tables[base_addr] + + # compute the total size of the table + size_of_table = len(function_table) * 12 # sizeof(RUNTIME_FUNCTION) + + # Write the size to memory at the provided pointer. + ql.mem.write_ptr(size_of_table_ptr, size_of_table, 4) + + return function_table_addr + +@winsdkapi(cc=STDCALL, params={}) +def hook_LdrControlFlowGuardEnforced(ql: Qiling, address: int, params): + # There are some checks in ntdll for whether CFG is enabled. + # We simply bypass these checks by returning 0. + # May not be necessary, but we do it just in case. + return 0 From 22a4b498ffce7dca7868d0320f37e7d0fef10b00 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Thu, 27 Mar 2025 21:37:29 +0100 Subject: [PATCH 09/35] Make RaiseException hook passthru --- qiling/os/windows/dlls/kernel32/errhandlingapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/os/windows/dlls/kernel32/errhandlingapi.py b/qiling/os/windows/dlls/kernel32/errhandlingapi.py index 4dba7efb6..6206d5899 100644 --- a/qiling/os/windows/dlls/kernel32/errhandlingapi.py +++ b/qiling/os/windows/dlls/kernel32/errhandlingapi.py @@ -74,7 +74,7 @@ def hook_SetErrorMode(ql: Qiling, address: int, params): 'dwExceptionFlags' : DWORD, 'nNumberOfArguments' : DWORD, 'lpArguments' : POINTER -}) +}, passthru=True) def hook_RaiseException(ql: Qiling, address: int, params): nNumberOfArguments = params['nNumberOfArguments'] lpArguments = params['lpArguments'] From ce03a8a113dc24c2e5d725a1411f7606b791cd05 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Fri, 28 Mar 2025 22:56:15 +0100 Subject: [PATCH 10/35] Add ProcessCookie case for NtQueryInformationProcess hook --- qiling/os/windows/const.py | 1 + qiling/os/windows/dlls/ntdll.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/qiling/os/windows/const.py b/qiling/os/windows/const.py index d001925e5..8fe29023d 100644 --- a/qiling/os/windows/const.py +++ b/qiling/os/windows/const.py @@ -638,6 +638,7 @@ ProcessDebugObjectHandle = 30 ProcessDebugFlags = 31 ProcessExecuteFlags = 34 +ProcessCookie = 36 ProcessImageInformation = 37 ProcessMitigationPolicy = 52 ProcessFaultInformation = 63 diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index f1e95fa71..34f184049 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -40,6 +40,7 @@ def hook_memcpy(ql: Qiling, address: int, params): return dest def _QueryInformationProcess(ql: Qiling, address: int, params): + handle = params["ProcessHandle"] flag = params["ProcessInformationClass"] obuf_ptr = params["ProcessInformation"] obuf_len = params['ProcessInformationLength'] @@ -69,6 +70,27 @@ def _QueryInformationProcess(ql: Qiling, address: int, params): res_data = bytes(pci_obj) + + elif flag == ProcessCookie: + hCurrentProcess = { + QL_ARCH.X86 : 0xFFFFFFFF, + QL_ARCH.X8664: 0xFFFFFFFFFFFFFFFF + }[ql.arch.type] + + if handle != hCurrentProcess: + # If a process attempts to query the cookie of another + # process, then QueryInformationProcess returns an error. + return STATUS_INVALID_PARAMETER + + # TODO: Change this to something else, + # maybe a static randomly generated value. + res_data = ql.pack32(0x00000001) + + if obuf_len != len(res_data): + # If the buffer length is not ULONG size + # then QueryInformationProcess returns an error. + return STATUS_INFO_LENGTH_MISMATCH + else: # TODO: support more info class ("flag") values ql.log.info(f'QueryInformationProcess: no implementation for info class {flag:#04x}') From 4ac6458dcc9c2597c760e722587bcbb40b2846ac Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Fri, 28 Mar 2025 22:56:51 +0100 Subject: [PATCH 11/35] Remove Encode/DecodePointer hooks --- qiling/os/windows/dlls/kernel32/winbase.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/qiling/os/windows/dlls/kernel32/winbase.py b/qiling/os/windows/dlls/kernel32/winbase.py index 6fe624b31..0c5c1d122 100644 --- a/qiling/os/windows/dlls/kernel32/winbase.py +++ b/qiling/os/windows/dlls/kernel32/winbase.py @@ -159,24 +159,6 @@ def hook__lwrite(ql: Qiling, address: int, params): def hook_FatalExit(ql: Qiling, address: int, params): ql.emu_stop() -# PVOID EncodePointer( -# _In_ PVOID Ptr -# ); -@winsdkapi(cc=STDCALL, params={ - 'Ptr' : PVOID -}) -def hook_EncodePointer(ql: Qiling, address: int, params): - return params['Ptr'] - -# PVOID DecodePointer( -# _In_ PVOID Ptr -# ); -@winsdkapi(cc=STDCALL, params={ - 'Ptr' : PVOID -}) -def hook_DecodePointer(ql: Qiling, address: int, params): - return params['Ptr'] - # UINT WinExec( # LPCSTR lpCmdLine, # UINT uCmdShow From 54564b00be5a9e1ac2737f564d6a92f6c47e1793 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Fri, 28 Mar 2025 23:18:35 +0100 Subject: [PATCH 12/35] Add support for forwarded exports to the PE loader --- qiling/loader/pe.py | 89 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 8a4b94a8e..442a61d56 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -29,6 +29,16 @@ from logging import Logger from qiling import Qiling +class ForwardedExport: + def __init__(self, + source_dll: str, source_ordinal: str, source_symbol: str, + target_dll: str, target_symbol: str): + self.source_dll = source_dll + self.source_ordinal = source_ordinal + self.source_symbol = source_symbol + self.target_dll = target_dll + self.target_symbol = target_symbol + class QlPeCacheEntry(NamedTuple): ba: int @@ -85,6 +95,10 @@ class Process: # maps image base to its list of function table entries function_tables: MutableMapping[int, list] + # List of exports which have been forwarded from + # one DLL to another. + forwarded_exports: list[ForwardedExport] + def __init__(self, ql: Qiling): self.ql = ql @@ -172,6 +186,49 @@ def lookup_function_entry(self, base_addr: int, control_pc: int): return i, runtime_function return None, None + + def resolve_forwarded_exports(self): + while self.forwarded_exports: + forwarded_export = self.forwarded_exports.pop() + + source_dll = forwarded_export.source_dll + source_ordinal = forwarded_export.source_ordinal + source_symbol = forwarded_export.source_symbol + target_dll = forwarded_export.target_dll + target_symbol = forwarded_export.target_symbol + + target_iat = self.import_address_table.get(target_dll) + + if target_iat: + # If we have an existing entry in the process IAT for the code + # this entry forwards to, then we will point the symbol there + # rather than the symbol string in the exporter's data section. + forward_ea = target_iat.get(target_symbol) + + if forward_ea: + self.import_address_table[source_dll][source_symbol] = forward_ea + self.import_address_table[source_dll][source_ordinal] = forward_ea + + # Register the new address as having the source symbol/ordinal. + # This way, hooks on forward source symbols will function + # correctly. + + self.import_symbols[forward_ea] = { + 'name' : source_symbol, + 'ordinal' : source_ordinal, + 'dll' : source_dll.split('.')[0] + } + + # TODO: With the above code, hooks on functions which are + # forward targets may not work correctly. + # The most correct way to resolve this would be to add + # support for addresses to be associated with multiple symbols. + + self.ql.log.debug(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Resolved symbol to ({forward_ea:#x})") + else: + self.ql.log.warning(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Failed to resolve address") + else: + pass # If IAT was not found, it is probably a virtual library. def load_dll(self, name: str, is_driver: bool = False) -> int: dll_path, dll_name = self.__get_path_elements(name) @@ -273,6 +330,31 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: for sym in dll.DIRECTORY_ENTRY_EXPORT.symbols: ea = image_base + sym.address + if sym.forwarder: + # Some exports are forwarders, meaning they + # actually refer to code in other libraries. + # + # For example, calls to + # kernel32.InterlockedPushEntrySList + # should be forwarded to + # ntdll.RtlInterlockedPushEntrySList + # + # If we do not properly account for forwarders then + # calls to these symbols will land in the exporter's + # data section and cause a lot of problems. + forward_str = sym.forwarder + + if b'.' in forward_str: + target_dll_name, target_symbol_name = forward_str.split(b'.', 1) + + target_dll_filename = (target_dll_name.lower() + b'.dll').decode() + + # Remember the forwarded export for later. + forwarded_export = ForwardedExport(dll_name, sym.ordinal, sym.name, + target_dll_filename, target_symbol_name) + + self.forwarded_exports.append(forwarded_export) + import_symbols[ea] = { 'name' : sym.name, 'ordinal' : sym.ordinal, @@ -297,6 +379,8 @@ def load_dll(self, name: str, is_driver: bool = False) -> int: self.import_address_table[dll_name] = import_table self.import_symbols.update(import_symbols) + self.resolve_forwarded_exports() + dll_base = image_base dll_len = image_size @@ -744,12 +828,14 @@ def __init__(self, ql: Qiling, libcache: bool): def run(self): self.init_dlls = ( 'ntdll.dll', - 'kernel32.dll', + 'kernelbase.dll', # kernel32 forwards some exports to kernelbase + 'kernel32.dll', # for efficiency, load kernelbase first 'user32.dll' ) self.sys_dlls = ( 'ntdll.dll', + 'kernelbase.dll', 'kernel32.dll', 'mscoree.dll', 'ucrtbase.dll' @@ -781,6 +867,7 @@ def run(self): self.ldr_list = [] self.function_tables = {} self.function_table_lookup = {} + self.forwarded_exports = [] self.pe_image_address = 0 self.pe_image_size = 0 self.dll_size = 0 From 37288e6d40db2c6cd40731285e6ed4230cb081a3 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Fri, 28 Mar 2025 23:19:45 +0100 Subject: [PATCH 13/35] Add user32 to DllMain blacklist --- qiling/loader/pe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 442a61d56..8c2924cd3 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -435,8 +435,8 @@ def call_dll_entrypoint(self, dll: pefile.PE, dll_base: int, dll_len: int, dll_n # the blacklist may be revisited from time to time to see if any of the file # can be safely unlisted. blacklist = { - 32 : ('gdi32.dll',), - 64 : ('gdi32.dll',) + 32 : ('gdi32.dll','user32.dll',), + 64 : ('gdi32.dll','user32.dll',) }[self.ql.arch.bits] if dll_name in blacklist: From 52a3910d02b16a99ca13d5b99b138b182064c2a5 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Fri, 28 Mar 2025 23:52:08 +0100 Subject: [PATCH 14/35] Fix some typos in ntdll hook code --- qiling/os/windows/dlls/ntdll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 34f184049..94b952f7e 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -522,7 +522,7 @@ def hook_RtlLookupFunctionEntry(ql: Qiling, address: int, params): # This function should not be getting called on x86. if ql.arch.type != QL_ARCH.X8664: - return QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") + raise QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") containing_image = ql.loader.find_containing_image(control_pc) @@ -571,7 +571,7 @@ def hook_RtlLookupFunctionTable(ql: Qiling, address: int, params): # This function should not be getting called on x86. if ql.arch.type != QL_ARCH.X8664: - return QlErrorNotImplemented("RtlLookupFunctionTable is not implemented for x86") + raise QlErrorNotImplemented("RtlLookupFunctionTable is not implemented for x86") containing_image = ql.loader.find_containing_image(control_pc) From 9661ad6326bebbd343cf81c2c12623a0bc44321a Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sat, 29 Mar 2025 15:33:00 +0100 Subject: [PATCH 15/35] Add ZwRaiseException hook, move unhandled exception logic --- .../windows/dlls/kernel32/errhandlingapi.py | 36 -------- qiling/os/windows/dlls/ntdll.py | 90 +++++++++++++++++++ qiling/os/windows/structs.py | 16 ++++ 3 files changed, 106 insertions(+), 36 deletions(-) diff --git a/qiling/os/windows/dlls/kernel32/errhandlingapi.py b/qiling/os/windows/dlls/kernel32/errhandlingapi.py index 6206d5899..084ffabc8 100644 --- a/qiling/os/windows/dlls/kernel32/errhandlingapi.py +++ b/qiling/os/windows/dlls/kernel32/errhandlingapi.py @@ -44,15 +44,6 @@ def hook_GetLastError(ql: Qiling, address: int, params): def hook_SetLastError(ql: Qiling, address: int, params): ql.os.last_error = params['dwErrCode'] -# LONG UnhandledExceptionFilter( -# _EXCEPTION_POINTERS *ExceptionInfo -# ); -@winsdkapi(cc=STDCALL, params={ - 'ExceptionInfo' : POINTER -}) -def hook_UnhandledExceptionFilter(ql: Qiling, address: int, params): - return 1 - # UINT SetErrorMode( # UINT uMode # ); @@ -63,33 +54,6 @@ def hook_SetErrorMode(ql: Qiling, address: int, params): # TODO maybe this need a better implementation return 0 -# __analysis_noreturn VOID RaiseException( -# DWORD dwExceptionCode, -# DWORD dwExceptionFlags, -# DWORD nNumberOfArguments, -# const ULONG_PTR *lpArguments -# ); -@winsdkapi(cc=STDCALL, params={ - 'dwExceptionCode' : DWORD, - 'dwExceptionFlags' : DWORD, - 'nNumberOfArguments' : DWORD, - 'lpArguments' : POINTER -}, passthru=True) -def hook_RaiseException(ql: Qiling, address: int, params): - nNumberOfArguments = params['nNumberOfArguments'] - lpArguments = params['lpArguments'] - - handle = ql.os.handle_manager.search("TopLevelExceptionHandler") - - if handle is None: - ql.log.warning(f'RaiseException: top level exception handler not found') - return - - exception_handler = handle.obj - args = [(PARAM_INTN, ql.mem.read_ptr(lpArguments + i * ql.arch.pointersize)) for i in range(nNumberOfArguments)] if lpArguments else [] - - ql.os.fcall.call_native(exception_handler, args, None) - # PVOID AddVectoredExceptionHandler( # ULONG First, # PVECTORED_EXCEPTION_HANDLER Handler diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 94b952f7e..09b5d1759 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -620,3 +620,93 @@ def hook_LdrControlFlowGuardEnforced(ql: Qiling, address: int, params): # We simply bypass these checks by returning 0. # May not be necessary, but we do it just in case. return 0 + +# NTSYSAPI +# NTSTATUS +# ZwRaiseException ( +# IN PEXCEPTION_RECORD ExceptionRecord, +# IN PCONTEXT ContextRecord, +# IN BOOLEAN FirstChance +# ); +@winsdkapi(cc=STDCALL, params={ + 'ExceptionRecord': PVOID, + 'ContextRecord': PVOID, + 'FirstChance': BOOLEAN +}) +def hook_ZwRaiseException(ql: Qiling, address: int, params): + exception_ptr = params['ExceptionRecord'] + context_ptr = params['ContextRecord'] + first_chance = params['FirstChance'] + + # In Windows, an unhandled exception triggers the + # top-level unhandled exception filter, after which the process + # is terminated and error reporting services are called. + # Regardless of whether an unhandled exception filter is present, + # the process terminates with the same error code that was raised. + + # Our strategy for this hook is to forward second-chance exceptions + # to the registered unhandled exception filter, if one exists. + + if first_chance: + raise QlErrorNotImplemented("ZwRaiseException is not implemented for first-chance exceptions.") + + if exception_ptr: + exception_code = ql.mem.read_ptr(exception_ptr, 4) # exception code is always DWORD + ql.log.debug(f"[ZwRaiseException] ExceptionCode: 0x{exception_code:08X}") + else: + ql.log.debug("[ZwRaiseException] ExceptionRecord is NULL") + + ql.log.debug(f" ContextRecord: 0x{context_ptr:016X}") + ql.log.debug(f" FirstChance: {first_chance}") + + handle = ql.os.handle_manager.search("TopLevelExceptionHandler") + + if handle is None: + ql.log.debug(f'[ZwRaiseException] No top-level exception filter was found.') + ql.log.info(f'The process exited with code 0x{exception_code:08X}.') + + ql.os.exit_code = exception_code + + ql.emu_stop() + + ret_addr = ql.stack_read(0) + + exception_filter = handle.obj + + # allocate some memory for the EXCEPTION_POINTERS struct + epointers_struct = structs.make_exception_pointers(ql.arch.bits) + exception_pointers_ptr = ql.os.heap.alloc(epointers_struct.sizeof()) + + with epointers_struct.ref(ql.mem, exception_pointers_ptr) as epointers_obj: + epointers_obj.ExceptionRecord = exception_ptr + epointers_obj.ContextRecord = context_ptr + + exception_filter = handle.obj + ql.log.debug(f'[ZwRaiseException] Resuming execution at the top-level exception filter at 0x{exception_filter:08X}.') + + # Hack: We are going to fake that the caller of ZwRaiseException + # actually called the unhandled exception filter instead. + + # We will create a hook which will be triggered when the unhandled + # exception filter returns, so that we may terminate execution. + def __post_exception_filter(ql: Qiling): + # Free the exception pointers struct we allocated earlier. + # Might not be needed, since we are going to terminate the process + # soon, but we might as well free it. + ql.os.heap.free(exception_pointers_ptr) + + ql.log.debug(f'[ZwRaiseException] Returned from unhandled exception filter at 0x{exception_filter:08X}.') + ql.log.info(f'The process exited with code 0x{exception_code:08X}.') + + ql.os.exit_code = exception_code + + ql.emu_stop() + + ql.hook_address(__post_exception_filter, ret_addr) + + exception_filter_args = [(POINTER, exception_pointers_ptr)] + + # Resume execution at the registered unhandled exception filter. + # If a program is using a custom unhandled exception filter as an anti-debugging + # trick, then the exception filter might not return. + ql.os.fcall.call_native(exception_filter, exception_filter_args, ret_addr) \ No newline at end of file diff --git a/qiling/os/windows/structs.py b/qiling/os/windows/structs.py index 56d685fc6..ea35c74c8 100644 --- a/qiling/os/windows/structs.py +++ b/qiling/os/windows/structs.py @@ -1545,3 +1545,19 @@ class WIN32_FIND_DATA(Struct): ) return WIN32_FIND_DATA + +# https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-exception_pointers +def make_exception_pointers(archbits: int): + """Generate an EXCEPTION_POINTERS structure class. + """ + + native_type = struct.get_native_type(archbits) + Struct = struct.get_aligned_struct(archbits) + + class EXCEPTION_POINTERS(Struct): + _fields_ = ( + ('ExceptionRecord', native_type), + ('ContextRecord', native_type) + ) + + return EXCEPTION_POINTERS \ No newline at end of file From 32e9fe345f5e62a5052782650358f7aa8cf7e2d7 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 13:16:45 +0200 Subject: [PATCH 16/35] Fix unhandled exception filter not being called correctly --- qiling/os/windows/dlls/ntdll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 09b5d1759..fb8fbe4d5 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -632,7 +632,7 @@ def hook_LdrControlFlowGuardEnforced(ql: Qiling, address: int, params): 'ExceptionRecord': PVOID, 'ContextRecord': PVOID, 'FirstChance': BOOLEAN -}) +}, passthru=True) def hook_ZwRaiseException(ql: Qiling, address: int, params): exception_ptr = params['ExceptionRecord'] context_ptr = params['ContextRecord'] From 0ed88851b65580cc3a0b2d7d58dbc1cfbd0fb2d4 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 17:43:48 +0200 Subject: [PATCH 17/35] Add 64-bit msvcp140 DLLs to dllscollector script --- examples/scripts/dllscollector.bat | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/scripts/dllscollector.bat b/examples/scripts/dllscollector.bat index 2b85a83e9..9433d1e26 100644 --- a/examples/scripts/dllscollector.bat +++ b/examples/scripts/dllscollector.bat @@ -131,6 +131,9 @@ CALL :collect_dll64 win32u.dll CALL :collect_dll64 winhttp.dll CALL :collect_dll64 wininet.dll CALL :collect_dll64 ws2_32.dll +CALL :collect_dll64 msvcp140.dll +CALL :collect_dll64 msvcp140_1.dll +CALL :collect_dll64 msvcp140_2.dll CALL :collect_dll64 downlevel\api-ms-win-crt-heap-l1-1-0.dll CALL :collect_dll64 downlevel\api-ms-win-crt-locale-l1-1-0.dll From 51fc085261beffb9d447cddc2382d0f6175b82b3 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 17:45:49 +0200 Subject: [PATCH 18/35] Add abort hook --- qiling/os/windows/const.py | 1 + qiling/os/windows/dlls/msvcrt.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/qiling/os/windows/const.py b/qiling/os/windows/const.py index 8fe29023d..6cf06d0ef 100644 --- a/qiling/os/windows/const.py +++ b/qiling/os/windows/const.py @@ -38,6 +38,7 @@ STATUS_PROCEDURE_NOT_FOUND = 0xC000007A STATUS_DLL_NOT_FOUND = 0xC0000135 STATUS_PORT_NOT_SET = 0xC0000353 +STATUS_STACK_BUFFER_OVERRUN = 0xC0000409 STATUS_NO_YIELD_PERFORMED = 0x40000024 # ... diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 9b9e1bba4..b209b20d6 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -10,7 +10,7 @@ from qiling.exception import QlErrorNotImplemented from qiling.os.const import * from qiling.os.windows.fncc import * -from qiling.os.windows.const import LOCALE +from qiling.os.windows.const import * from qiling.os.windows.handle import Handle # void __set_app_type ( @@ -651,3 +651,18 @@ def hook__time64(ql: Qiling, address: int, params): ql.mem.write_ptr(dst, time_wasted, 8) return time_wasted + +# void abort( void ); +@winsdkapi(cc=CDECL, params={}) +def hook_abort(ql: Qiling, address: int, params): + # During testing, it was found that programs terminating abnormally + # via abort() terminated with exit code=STATUS_STACK_BUFFER_OVERRUN. + # According to Microsoft's devblog, this does not necessarily mean + # that a stack buffer overrun occurred. + # Rather, it can indicate abnormal program termination in a variety of + # situations, including abort(). + # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655 + # + ql.os.exit_code = STATUS_STACK_BUFFER_OVERRUN + + ql.emu_stop() \ No newline at end of file From 273b46b3674342c3aaa51ad09c0b0c0c307abe55 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 18:03:10 +0200 Subject: [PATCH 19/35] Add 32-bit msvcp140 DLLs to dllscollector script --- examples/scripts/dllscollector.bat | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/scripts/dllscollector.bat b/examples/scripts/dllscollector.bat index 9433d1e26..b0707bba2 100644 --- a/examples/scripts/dllscollector.bat +++ b/examples/scripts/dllscollector.bat @@ -94,6 +94,9 @@ CALL :collect_dll32 wininet.dll CALL :collect_dll32 winmm.dll CALL :collect_dll32 ws2_32.dll CALL :collect_dll32 wsock32.dll +CALL :collect_dll32 msvcp140.dll +CALL :collect_dll32 msvcp140_1.dll +CALL :collect_dll32 msvcp140_2.dll CALL :collect_dll32 downlevel\api-ms-win-core-fibers-l1-1-1.dll CALL :collect_dll32 downlevel\api-ms-win-core-localization-l1-2-1.dll From cf23ef9f81f40dcdda9abac0b6d1930d671176d0 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 19:18:50 +0200 Subject: [PATCH 20/35] Small changes in ZwRaiseException hook --- qiling/os/windows/dlls/ntdll.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index fb8fbe4d5..86c14df05 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -638,6 +638,17 @@ def hook_ZwRaiseException(ql: Qiling, address: int, params): context_ptr = params['ContextRecord'] first_chance = params['FirstChance'] + # The native ZwRaiseException simply uses a syscall to start + # the kernel exception dispatcher. However, Windows syscalls + # are not really working in Qiling right now. + # For now, we just provide a workaround for second-chance + # exceptions to work. + # TODO: Get some kind of solution for kernel exception + # dispatching. This is also needed for first-chance exceptions + # to work properly on 32-bit Windows. + if first_chance: + raise QlErrorNotImplemented("ZwRaiseException is not implemented for first-chance exceptions.") + # In Windows, an unhandled exception triggers the # top-level unhandled exception filter, after which the process # is terminated and error reporting services are called. @@ -647,9 +658,6 @@ def hook_ZwRaiseException(ql: Qiling, address: int, params): # Our strategy for this hook is to forward second-chance exceptions # to the registered unhandled exception filter, if one exists. - if first_chance: - raise QlErrorNotImplemented("ZwRaiseException is not implemented for first-chance exceptions.") - if exception_ptr: exception_code = ql.mem.read_ptr(exception_ptr, 4) # exception code is always DWORD ql.log.debug(f"[ZwRaiseException] ExceptionCode: 0x{exception_code:08X}") @@ -668,6 +676,7 @@ def hook_ZwRaiseException(ql: Qiling, address: int, params): ql.os.exit_code = exception_code ql.emu_stop() + return ret_addr = ql.stack_read(0) From 2b4246684aaf5e88e354a55e1ad26dc01a342f4f Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 19:34:11 +0200 Subject: [PATCH 21/35] Make some requested changes in ntdll hooks --- qiling/os/windows/dlls/ntdll.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 86c14df05..eff4a924a 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -72,10 +72,7 @@ def _QueryInformationProcess(ql: Qiling, address: int, params): elif flag == ProcessCookie: - hCurrentProcess = { - QL_ARCH.X86 : 0xFFFFFFFF, - QL_ARCH.X8664: 0xFFFFFFFFFFFFFFFF - }[ql.arch.type] + hCurrentProcess = (1 << ql.arch.bits) - 1 if handle != hCurrentProcess: # If a process attempts to query the cookie of another @@ -491,10 +488,7 @@ def hook_RtlPcToFileHeader(ql: Qiling, address: int, params): containing_image = ql.loader.find_containing_image(pc) - if containing_image: - base_addr = containing_image.base - else: - base_addr = 0 + base_addr = containing_image.base if containing_image else 0 ql.mem.write_ptr(base_of_image_ptr, base_addr) return base_addr @@ -521,7 +515,7 @@ def hook_RtlLookupFunctionEntry(ql: Qiling, address: int, params): # history_table_ptr = params["HistoryTable"] # This function should not be getting called on x86. - if ql.arch.type != QL_ARCH.X8664: + if ql.arch.type is QL_ARCH.X86: raise QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") containing_image = ql.loader.find_containing_image(control_pc) From 7dd9fcdc74aba6ac8e190ba71799d46881600cf1 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 21:27:53 +0200 Subject: [PATCH 22/35] Add C++ runtime and exception-related tests --- tests/test_windows_cpp_x86.py | 64 +++++++++++ tests/test_windows_cpp_x8664.py | 181 ++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 tests/test_windows_cpp_x86.py create mode 100644 tests/test_windows_cpp_x8664.py diff --git a/tests/test_windows_cpp_x86.py b/tests/test_windows_cpp_x86.py new file mode 100644 index 000000000..ba45466ea --- /dev/null +++ b/tests/test_windows_cpp_x86.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +import sys, unittest + +sys.path.append("..") +from qiling import Qiling +from qiling.const import QL_VERBOSE +from qiling.extensions import pipe + + +def good_bad_count(test_str: str, good_str="GOOD", bad_str="BAD"): + good_count = test_str.count(good_str) + bad_count = test_str.count(bad_str) + + return good_count, bad_count + + +class CppTests_x86(unittest.TestCase): + + def test_cpp_helloworld(self): + """ Test a basic C++ Hello World program which prints "Hello World!" + to the console using std::cout. + """ + ql = Qiling(["../examples/rootfs/x86_windows/bin/except/CppHelloWorld_x86.exe"], "../examples/rootfs/x86_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read() + self.assertEqual(conout, b"Hello World!\x0d\x0a") + + del ql + + def test_cpp_types(self): + """ This program tests several C++ type-related runtime features. + - typeid + - dynamic_cast + - virtual methods + - virtual destructors + """ + ql = Qiling(["../examples/rootfs/x86_windows/bin/except/TestCppTypes_x86.exe"], "../examples/rootfs/x86_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout) + + # the test program should print + # - 'GOOD' 12 times + # - 'BAD' 0 times + self.assertEqual(good_count, 12) + self.assertEqual(bad_count, 0) + + del ql + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_windows_cpp_x8664.py b/tests/test_windows_cpp_x8664.py new file mode 100644 index 000000000..8f89a47e4 --- /dev/null +++ b/tests/test_windows_cpp_x8664.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +import sys, unittest + +sys.path.append("..") +from qiling import Qiling +from qiling.const import QL_VERBOSE +from qiling.extensions import pipe + + +def good_bad_count(test_str: str, good_str="GOOD", bad_str="BAD"): + good_count = test_str.count(good_str) + bad_count = test_str.count(bad_str) + + return good_count, bad_count + + +class CppTests_x8664(unittest.TestCase): + + def test_cpp_helloworld(self): + """ Test a basic C++ Hello World program which prints "Hello World!" + to the console using std::cout. + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/CppHelloWorld.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read() + self.assertEqual(conout, b"Hello World!\x0d\x0a") + + del ql + + def test_cpp_types(self): + """ This program tests several C++ type-related runtime features. + - typeid + - dynamic_cast + - virtual methods + - virtual destructors + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/TestCppTypes.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout) + + # the test program should print + # - 'GOOD' 12 times + # - 'BAD' 0 times + self.assertEqual(good_count, 12) + self.assertEqual(bad_count, 0) + + del ql + + def test_soft_seh(self): + """ Test software SEH. + This test program uses __try..__catch and calls RaiseException with + a custom code. If software SEH is functioning correctly, the program + should be able to invoke its __catch-block and continue execution after. + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/TestSoftSEH.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout) + + # the test program should print + # - 'GOOD' 4 times + # - 'BAD' 0 times + self.assertEqual(good_count, 4) + self.assertEqual(bad_count, 0) + + # If the exception handler was not invoked for some reason, + # the program may terminate abnormally with a non-zero exit + # code. + self.assertEqual(ql.os.exit_code, 0) + + del ql + + def test_soft_cppex(self): + """ Test software C++ exceptions. + This test program tests try..catch in various ways. If exception dispatching + and stack unwinding are functioning correctly, the program will run to completion. + - Simple try..catch + - Try..catch with throw data + - Nested try..catch with throw data + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/TestCppEx.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout, 'y', 'n') + + # the test program should print + # - 'y' 14 times + # - 'n' 0 times + self.assertEqual(good_count, 14) + self.assertEqual(bad_count, 0) + + # If the exception handler was not invoked for some reason, + # the program may terminate abnormally with a non-zero exit + # code. + self.assertEqual(ql.os.exit_code, 0) + + del ql + + def test_cppex_unhandled_filtered(self): + """ Test unhandled C++ exceptions. + This program registers its own unhandled exception filter via + SetUnhandledExceptionFilter, then throws an uncaught exception. + If unhandled exception filters are functioning correctly, + the program's custom exception filter will be reached, but + execution will NOT resume after the exception. + Instead, the program is expected to terminate abnormally + with status code 0xE06D7363 (C++ runtime exception). + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/TestCppExUnhandled.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout) + + # the test program should print + # - 'GOOD' 3 times + # - 'BAD' 0 times + self.assertEqual(good_count, 3) + self.assertEqual(bad_count, 0) + + # The program should have terminated abnormally + # with status code 0xE06D7363 (C++ runtime exception). + self.assertEqual(ql.os.exit_code, 0xE06D7363) + + del ql + + def test_cppex_unhandled_unfiltered(self): + """ Test unhandled C++ exceptions. + This program throws an uncaught C++ exception. + The program is expected to terminate abnormally + with status code 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN). + """ + ql = Qiling(["../examples/rootfs/x8664_windows/bin/except/TestCppExUnhandled2.exe"], "../examples/rootfs/x8664_windows/", verbose=QL_VERBOSE.DEFAULT) + + ql.os.stdout = pipe.SimpleStringBuffer() + + ql.run() + + conout = ql.os.stdout.read().decode('utf-8') + good_count, bad_count = good_bad_count(conout) + + # the test program should print + # - 'GOOD' 1 time + # - 'BAD' 0 times + self.assertEqual(good_count, 1) + self.assertEqual(bad_count, 0) + + # The program is expected to terminate abnormally + # with status code 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) + # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655 + # + self.assertEqual(ql.os.exit_code, 0xC0000409) + + del ql + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 4ea1a0ada6584a4b4cb0a5840a4fa20ee33aac78 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 21:36:09 +0200 Subject: [PATCH 23/35] Make requested change in GDTManager --- qiling/arch/x86_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiling/arch/x86_utils.py b/qiling/arch/x86_utils.py index dfd45623d..1bc9f8953 100644 --- a/qiling/arch/x86_utils.py +++ b/qiling/arch/x86_utils.py @@ -61,10 +61,7 @@ def __init__(self, ql: Qiling, base=QL_X86_GDT_ADDR, limit=QL_X86_GDT_LIMIT, num # setup GDT by writing to GDTR ql.arch.regs.write(UC_X86_REG_GDTR, (0, base, limit, 0x0)) - if ql.arch.type == QL_ARCH.X8664: - self.is_long_mode = True - else: - self.is_long_mode = False + self.is_long_mode = ql.arch.type is QL_ARCH.X8664 self.array = GDTArray(ql.mem, base, num_entries) From b895748a312451de278499fd88b968ae668f7fbc Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Sun, 30 Mar 2025 21:56:38 +0200 Subject: [PATCH 24/35] Make requested changes in PE loader --- qiling/loader/pe.py | 47 +++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 8c2924cd3..6cbf3781e 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -10,7 +10,8 @@ import pickle import secrets import ntpath -from typing import TYPE_CHECKING, Any, Dict, MutableMapping, NamedTuple, Optional, Mapping, Sequence, Tuple, Union +from collections import namedtuple +from typing import TYPE_CHECKING, Any, Dict, List, MutableMapping, NamedTuple, Optional, Mapping, Sequence, Tuple, Union from unicorn import UcError from unicorn.x86_const import UC_X86_REG_CR4, UC_X86_REG_CR8 @@ -29,15 +30,10 @@ from logging import Logger from qiling import Qiling -class ForwardedExport: - def __init__(self, - source_dll: str, source_ordinal: str, source_symbol: str, - target_dll: str, target_symbol: str): - self.source_dll = source_dll - self.source_ordinal = source_ordinal - self.source_symbol = source_symbol - self.target_dll = target_dll - self.target_symbol = target_symbol +ForwardedExport = namedtuple('ForwardedExport', [ + 'source_dll', 'source_ordinal', 'source_symbol', + 'target_dll', 'target_symbol' +]) class QlPeCacheEntry(NamedTuple): @@ -90,14 +86,14 @@ class Process: libcache: Optional[QlPeCache] # maps image base to RVA of its function table - function_table_lookup: MutableMapping[int, int] + function_table_lookup: Dict[int, int] # maps image base to its list of function table entries - function_tables: MutableMapping[int, list] + function_tables: MutableMapping[int, List] # List of exports which have been forwarded from # one DLL to another. - forwarded_exports: list[ForwardedExport] + forwarded_exports: List[ForwardedExport] def __init__(self, ql: Qiling): self.ql = ql @@ -128,13 +124,13 @@ def __get_path_elements(self, name: str) -> Tuple[str, str]: def init_function_tables(self, pe: pefile.PE, image_base: int): """Parse function table data for the given PE file. - Only works for x64 images. + Only really relevant for non-x86 images. Args: pe: the PE image whose function data should be parsed image_base: the absolute address at which the image was loaded """ - if self.ql.arch.type == QL_ARCH.X8664: + if self.ql.arch.type is not QL_ARCH.X86: # Check if the PE file has an exception directory if hasattr(pe, 'DIRECTORY_ENTRY_EXCEPTION'): @@ -144,12 +140,9 @@ def init_function_tables(self, pe: pefile.PE, image_base: int): self.function_table_lookup[image_base] = exception_dir.VirtualAddress - runtime_function_list = [] + runtime_function_list = list(pe.DIRECTORY_ENTRY_EXCEPTION) - for _, exception_entry in enumerate(pe.DIRECTORY_ENTRY_EXCEPTION, start=1): - runtime_function_list.append(exception_entry) - - if self.function_tables.get(image_base) is None: + if image_base not in self.function_tables: self.function_tables[image_base] = [] self.function_tables[image_base].extend(runtime_function_list) @@ -175,17 +168,9 @@ def lookup_function_entry(self, base_addr: int, control_pc: int): # Initiate a search of the function table for a RUNTIME_FUNCTION # entry such that the provided PC falls within its start and end range. - for i, runtime_function in enumerate(function_table): - - # Begin and end addresses exist in the entry as RVAs, - # convert them to absolute addresses. - begin_addr = base_addr + runtime_function.struct.BeginAddress - end_addr = base_addr + runtime_function.struct.EndAddress - - if begin_addr <= control_pc < end_addr: - return i, runtime_function - - return None, None + return next(((i, rtfunc) for i, rtfunc in enumerate(function_table) + if rtfunc.struct.BeginAddress <= control_pc - base_addr < rtfunc.struct.EndAddress), + (None, None)) def resolve_forwarded_exports(self): while self.forwarded_exports: From c0daea100314c975c823a5314cd90a82357c9d84 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 31 Mar 2025 22:42:21 +0200 Subject: [PATCH 25/35] Refactor hooks in ntdll --- qiling/os/windows/dlls/ntdll.py | 75 +++++++++++++++++---------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index eff4a924a..04b52abcf 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -493,6 +493,36 @@ def hook_RtlPcToFileHeader(ql: Qiling, address: int, params): ql.mem.write_ptr(base_of_image_ptr, base_addr) return base_addr +def _FindImageBaseAndFunctionTable(ql: Qiling, control_pc: int, image_base_ptr: int): + """ + Helper function to locate a containing image for `control_pc` as well as its + function table, while writing the image base to `image_base_ptr` (if non-zero). + Returns: + (base_addr, function_table_addr) + if no image is found, otherwise + (0, 0) + """ + containing_image = ql.loader.find_containing_image(control_pc) + + if containing_image: + base_addr = containing_image.base + else: + base_addr = 0 + + # Write base address to the ImageBase pointer, if provided + if image_base_ptr != 0: + ql.mem.write_ptr(image_base_ptr, base_addr) + + # If we don’t have a valid base, abort now + if base_addr == 0: + return 0, 0 + + # Look up the function-table RVA and compute the absolute address + function_table_rva = ql.loader.function_table_lookup.get(base_addr) + function_table_addr = base_addr + function_table_rva if function_table_rva else 0 + + return base_addr, function_table_addr + # NTSYSAPI PRUNTIME_FUNCTION RtlLookupFunctionEntry( # [in] DWORD64 ControlPc, # [out] PDWORD64 ImageBase, @@ -518,22 +548,11 @@ def hook_RtlLookupFunctionEntry(ql: Qiling, address: int, params): if ql.arch.type is QL_ARCH.X86: raise QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") - containing_image = ql.loader.find_containing_image(control_pc) - - if containing_image: - base_addr = containing_image.base - else: - base_addr = 0 + base_addr, function_table_addr = _FindImageBaseAndFunctionTable(ql, control_pc, image_base_ptr) + # If no function table was found, abort. + if function_table_addr == 0: return 0 - - # If we got a valid location to write the image base ptr, - # copy it there, and proceed. - if image_base_ptr != 0: - ql.mem.write_ptr(image_base_ptr, base_addr) - - # Get the base address of the function table. - function_table_addr = base_addr + ql.loader.function_table_lookup[base_addr] # Look up the RUNTIME_FUNCTION entry; we are interested in the index in the table # so that we can compute the address. @@ -564,35 +583,17 @@ def hook_RtlLookupFunctionTable(ql: Qiling, address: int, params): size_of_table_ptr = params["SizeOfTable"] # This function should not be getting called on x86. - if ql.arch.type != QL_ARCH.X8664: + if ql.arch.type is QL_ARCH.X86: raise QlErrorNotImplemented("RtlLookupFunctionTable is not implemented for x86") - containing_image = ql.loader.find_containing_image(control_pc) + base_addr, function_table_addr = _FindImageBaseAndFunctionTable(ql, control_pc, image_base_ptr) - if containing_image: - base_addr = containing_image.base - else: - base_addr = 0 + # If no function table was found, abort. + if function_table_addr == 0: + ql.mem.write_ptr(size_of_table_ptr, 0, 4) return 0 - # If we got a valid location to write the image base ptr, - # copy it there, and proceed. - if image_base_ptr != 0: - ql.mem.write_ptr(image_base_ptr, base_addr) - - # If image base was 0, we are not going to find a valid function - # table anyway, so just return. - if base_addr == 0: - return 0 - - # Look up the RVA of the function table. - function_table_rva = ql.loader.function_table_lookup[base_addr] - - # The caller is expecting a pointer, so convert the RVA - # to an absolute address. - function_table_addr = int(base_addr + function_table_rva) - # If a valid pointer for the size was provided, # we want to figure out the size of the table. if size_of_table_ptr != 0: From 8f92c73b4076b5c6cc5da64f3e53a966bae37fcf Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 31 Mar 2025 22:50:25 +0200 Subject: [PATCH 26/35] Add source for C++ and exception-related test programs --- examples/src/windows/except/CppHelloWorld.cpp | 11 +++ examples/src/windows/except/README | 3 + examples/src/windows/except/TestCppEx.cpp | 95 +++++++++++++++++++ .../src/windows/except/TestCppExUnhandled.cpp | 46 +++++++++ .../windows/except/TestCppExUnhandled2.cpp | 21 ++++ examples/src/windows/except/TestCppTypes.cpp | 93 ++++++++++++++++++ examples/src/windows/except/TestSoftSEH.cpp | 45 +++++++++ 7 files changed, 314 insertions(+) create mode 100644 examples/src/windows/except/CppHelloWorld.cpp create mode 100644 examples/src/windows/except/README create mode 100644 examples/src/windows/except/TestCppEx.cpp create mode 100644 examples/src/windows/except/TestCppExUnhandled.cpp create mode 100644 examples/src/windows/except/TestCppExUnhandled2.cpp create mode 100644 examples/src/windows/except/TestCppTypes.cpp create mode 100644 examples/src/windows/except/TestSoftSEH.cpp diff --git a/examples/src/windows/except/CppHelloWorld.cpp b/examples/src/windows/except/CppHelloWorld.cpp new file mode 100644 index 000000000..4b78ac15d --- /dev/null +++ b/examples/src/windows/except/CppHelloWorld.cpp @@ -0,0 +1,11 @@ +// This is the default Hello World program generated by Visual Studio 2022. + +#include + +int main() +{ + std::cout << "Hello World!\n"; + + return 0; +} + diff --git a/examples/src/windows/except/README b/examples/src/windows/except/README new file mode 100644 index 000000000..8dfda022b --- /dev/null +++ b/examples/src/windows/except/README @@ -0,0 +1,3 @@ +In this folder: Sources for programs intended to help test C++ features and software exceptions. + +Compile with MSVC (Visual Studio 2022) \ No newline at end of file diff --git a/examples/src/windows/except/TestCppEx.cpp b/examples/src/windows/except/TestCppEx.cpp new file mode 100644 index 000000000..bd6fa46e3 --- /dev/null +++ b/examples/src/windows/except/TestCppEx.cpp @@ -0,0 +1,95 @@ +#include +#include + +/* + * Test simple try..catch. + */ +void test1() +{ + std::cout << "y"; + + try { + std::cout << "y"; + throw (unsigned int)0x12345678; + std::cout << "n"; + } + catch(unsigned int n) { + n; + std::cout << "y"; + } + + std::cout << "y"; +} + +/* + * Test simple try..catch with throw. + */ +void test2() +{ + std::cout << "y"; + + try { + std::cout << "y"; + throw (unsigned int)0x12345679; + std::cout << "n"; + } + catch (unsigned int n) { + n; + if (n == 0x12345679) { + std::cout << "y"; + } + else { + std::cout << "n"; + } + } + + std::cout << "y"; +} + +/* + * Test nested try..catch with throw. + */ +void test3() +{ + std::cout << "y"; + + try { + std::cout << "y"; + + try { + std::cout << "y"; + throw (unsigned int)0x1234567A; + std::cout << "n"; + } + catch (unsigned int n) { + n; + if (n == 0x1234567A) { + std::cout << "y"; + } + else { + std::cout << "n"; + } + } + + std::cout << "y"; + } + catch (unsigned int n) { + n; + std::cout << "n"; + } + + std::cout << "y"; +} + +int main() +{ + /* + * For this program, all subtests successful will print: + * - 14 'y' + * - 0 'n' + */ + + test1(); + test2(); + test3(); +} diff --git a/examples/src/windows/except/TestCppExUnhandled.cpp b/examples/src/windows/except/TestCppExUnhandled.cpp new file mode 100644 index 000000000..0074d1f4f --- /dev/null +++ b/examples/src/windows/except/TestCppExUnhandled.cpp @@ -0,0 +1,46 @@ +#include +#include + +LONG WINAPI CustomExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) { + printf("Inside exception filter (GOOD)\n"); + DWORD exceptionCode = (DWORD)ExceptionInfo->ExceptionRecord->ExceptionCode; + printf("Exception Code: 0x%X\n", exceptionCode); + + if (exceptionCode == 0xE06D7363) { // code for C++ exception + printf("Exception code DOES match, GOOD\n"); + } + else { + printf("Exception code DOES NOT match, BAD\n"); + } + + printf("Exception Address: 0x%llx\n", (ULONGLONG)ExceptionInfo->ExceptionRecord->ExceptionAddress); + + printf("After printing exception: (GOOD)\n"); + + return EXCEPTION_EXECUTE_HANDLER; +} + +int main() { + /* + * For this program, all subtests successful will print: + * - 3 'GOOD' + * - 0 'BAD' + * + * It is expected that the program terminates abnormally + * with status code 0xE06D7363 (C++ exception) + */ + + // Set the custom top-level exception filter + SetUnhandledExceptionFilter(CustomExceptionFilter); + + // Throw an unhandled exception. + // It should be caught by our filter. + throw (unsigned int)5; + + // We should never reach this point, because the exception + // dispatcher should terminate the program after our unhandled + // exception filter is called. + printf("After exception filter (BAD)\n"); + + return 0; +} \ No newline at end of file diff --git a/examples/src/windows/except/TestCppExUnhandled2.cpp b/examples/src/windows/except/TestCppExUnhandled2.cpp new file mode 100644 index 000000000..600855cc1 --- /dev/null +++ b/examples/src/windows/except/TestCppExUnhandled2.cpp @@ -0,0 +1,21 @@ +#include +#include + +int main() +{ + /* + * For this program, all subtests successful will print: + * - 1 'GOOD' + * - 0 'BAD' + * + * It is expected that the program terminates abnormally + * with status code 0xC0000409 (stack buffer overrun/security + * check failure) + */ + + printf("Before throw (GOOD)\n"); + + throw (unsigned int)5; + + printf("After throw (BAD)\n"); +} diff --git a/examples/src/windows/except/TestCppTypes.cpp b/examples/src/windows/except/TestCppTypes.cpp new file mode 100644 index 000000000..42b8da21e --- /dev/null +++ b/examples/src/windows/except/TestCppTypes.cpp @@ -0,0 +1,93 @@ +#include + +struct TestStruct { + float q; +}; + +class TestClass { +public: + int x, y; + virtual ~TestClass() { + std::cout << "TestClass destructor, GOOD" << std::endl; + }; + void yyy() { + std::cout << "REALLY GOOD" << std::endl; + } +}; + +class Something { +public: + char z; + virtual ~Something() { + std::cout << "Something destructor, GOOD" << std::endl; + }; + virtual void zzz() { + std::cout << "BAD" << std::endl; + }; +}; + +class TestClass2 : public TestClass, public Something { +public: + int z; + virtual ~TestClass2() { + std::cout << "TestClass2 destructor, GOOD" << std::endl; + }; + virtual void zzz() { + std::cout << "GOOD" << std::endl; + }; +}; + +int main() +{ + /* + * For this program, all subtests successful will print: + * - 12 'GOOD' + * - 0 'BAD' + */ + + int x = 5; + TestClass p; + TestStruct s; + + std::cout << typeid(x).name() << std::endl; + if (strcmp(typeid(x).name(), "int") == 0) { + std::cout << "typeid(x) is int, GOOD" << std::endl; + } + else { + std::cout << "typeid(x) is NOT int, BAD" << std::endl; + } + + std::cout << typeid(p).name() << std::endl; + if (strcmp(typeid(p).name(), "class TestClass") == 0) { + std::cout << "typeid(p) is \"class TestClass\", GOOD" << std::endl; + } + else { + std::cout << "typeid(p) is NOT \"class TestClass\", BAD" << std::endl; + } + + std::cout << typeid(s).name() << std::endl; + if (strcmp(typeid(s).name(), "struct TestStruct") == 0) { + std::cout << "typeid(s) is \"struct TestStruct\", GOOD" << std::endl; + } + else { + std::cout << "typeid(s) is NOT \"struct TestStruct\", BAD" << std::endl; + } + + std::cout << "Reached virtual methods and dynamic_cast test. GOOD" << std::endl; + + TestClass2* kz = new TestClass2; + + Something* ks = static_cast(kz); + + ks->zzz(); + + TestClass* pk = dynamic_cast(ks); + + pk->yyy(); + + std::cout << "Reached virtual destructor test. GOOD" << std::endl; + + delete pk; + + std::cout << "Finished all tests. GOOD" << std::endl; +} diff --git a/examples/src/windows/except/TestSoftSEH.cpp b/examples/src/windows/except/TestSoftSEH.cpp new file mode 100644 index 000000000..2578b7aae --- /dev/null +++ b/examples/src/windows/except/TestSoftSEH.cpp @@ -0,0 +1,45 @@ +#include +#include + +void test1() { + __try { + printf("Inside __try block. (GOOD)\n"); + + RaiseException( + 0xE0000001, + 0, + 0, + nullptr + ); + + printf("After RaiseException. (BAD)\n"); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + printf("In __except block. (GOOD)\n"); + + unsigned long excepCode = GetExceptionCode(); + + printf("Exception code=0x%x\n", excepCode); + + if (excepCode == 0xE0000001) { + printf("Exception code IS same, GOOD\n"); + } + else { + printf("Exception code DOES NOT MATCH, BAD\n"); + } + } + + printf("After __except block. (GOOD)\n"); +} + +int main() { + /* + * For this program, all subtests successful will print: + * - 4 'GOOD' + * - 0 'BAD' + */ + + test1(); + + return 0; +} From 462f68a749039c4d6987343e0477649b0e977b4c Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Mon, 31 Mar 2025 23:04:20 +0200 Subject: [PATCH 27/35] Add note to ZwRaiseException hook --- qiling/os/windows/dlls/ntdll.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 04b52abcf..9d84b5d9b 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -713,4 +713,9 @@ def __post_exception_filter(ql: Qiling): # Resume execution at the registered unhandled exception filter. # If a program is using a custom unhandled exception filter as an anti-debugging # trick, then the exception filter might not return. + + # TODO: This relies on the hook being marked 'passthru' so that Qiling + # doesn't rewind after it returns. However, this is not entirely intended + # behavior of passthru, so this is a bit of a hack. Maybe find some + # way to rewrite without passthru. ql.os.fcall.call_native(exception_filter, exception_filter_args, ret_addr) \ No newline at end of file From 15e3c009cbb936440a2c84bf3db86f7f8fbf6316 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Tue, 1 Apr 2025 22:53:11 +0200 Subject: [PATCH 28/35] Add hook for EtwNotificationRegister --- qiling/os/windows/dlls/ntdll.py | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 9d84b5d9b..33056c742 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -718,4 +718,41 @@ def __post_exception_filter(ql: Qiling): # doesn't rewind after it returns. However, this is not entirely intended # behavior of passthru, so this is a bit of a hack. Maybe find some # way to rewrite without passthru. - ql.os.fcall.call_native(exception_filter, exception_filter_args, ret_addr) \ No newline at end of file + ql.os.fcall.call_native(exception_filter, exception_filter_args, ret_addr) + +# NTSTATUS EtwNotificationRegister( +# LPCGUID ProviderGuid, +# ULONG Type, +# PVOID CallbackFunction, +# PVOID CallbackContext, +# PVOID* RegistrationHandle +# ); +@winsdkapi(cc=STDCALL, params={ + 'ProviderGuid': PVOID, + 'Type': DWORD, + 'CallbackFunction': PVOID, + 'CallbackContext': PVOID, + 'RegistrationHandle': PVOID +}) +def hook_EtwNotificationRegister(ql: Qiling, address: int, params): + reg_handle_ptr = params['RegistrationHandle'] + + # It is very important to have a hook for this function + # because it is called by some Windows DLLs (sechost.dll, + # advapi32.dll) during initialization when the global + # CRT lock is held. + # If a DllMain aborts here, then the global CRT lock is never + # freed and any attempt to lock the global CRT lock *anywhere* + # will crash us. + + # TODO: See if a more thorough implementation + # is needed for this function. + + # For now, just create a dummy handle, and return it. + handle = Handle() + ql.os.handle_manager.append(handle) + + if reg_handle_ptr: + ql.mem.write_ptr(reg_handle_ptr, handle.id) + + return STATUS_SUCCESS From 92cd2d41db3a962a1538144d3c987d4b7889b8b2 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 00:09:25 +0200 Subject: [PATCH 29/35] Fix an issue with forwarded symbols, and improve readability --- qiling/loader/pe.py | 69 +++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 6cbf3781e..ee1ed8a82 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -182,38 +182,47 @@ def resolve_forwarded_exports(self): target_dll = forwarded_export.target_dll target_symbol = forwarded_export.target_symbol + if not source_symbol: + # Some DLLs (shlwapi.dll) have a bunch of forwarded + # exports with ordinals but no symbols. + # These are really annoying to deal with, but they are + # used extremely rarely, so we will ignore them. + continue + target_iat = self.import_address_table.get(target_dll) - if target_iat: - # If we have an existing entry in the process IAT for the code - # this entry forwards to, then we will point the symbol there - # rather than the symbol string in the exporter's data section. - forward_ea = target_iat.get(target_symbol) - - if forward_ea: - self.import_address_table[source_dll][source_symbol] = forward_ea - self.import_address_table[source_dll][source_ordinal] = forward_ea - - # Register the new address as having the source symbol/ordinal. - # This way, hooks on forward source symbols will function - # correctly. - - self.import_symbols[forward_ea] = { - 'name' : source_symbol, - 'ordinal' : source_ordinal, - 'dll' : source_dll.split('.')[0] - } - - # TODO: With the above code, hooks on functions which are - # forward targets may not work correctly. - # The most correct way to resolve this would be to add - # support for addresses to be associated with multiple symbols. - - self.ql.log.debug(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Resolved symbol to ({forward_ea:#x})") - else: - self.ql.log.warning(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Failed to resolve address") - else: - pass # If IAT was not found, it is probably a virtual library. + if not target_iat: + # If IAT was not found, it is probably a virtual library. + continue + + # If we have an existing entry in the process IAT for the code + # this entry forwards to, then we will point the symbol there + # rather than the symbol string in the exporter's data section. + forward_ea = target_iat.get(target_symbol) + + if not forward_ea: + self.ql.log.warning(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Failed to resolve address") + continue + + self.import_address_table[source_dll][source_symbol] = forward_ea + self.import_address_table[source_dll][source_ordinal] = forward_ea + + # Register the new address as having the source symbol/ordinal. + # This way, hooks on forward source symbols will function + # correctly. + + self.import_symbols[forward_ea] = { + 'name' : source_symbol, + 'ordinal' : source_ordinal, + 'dll' : source_dll.split('.')[0] + } + + # TODO: With the above code, hooks on functions which are + # forward targets may not work correctly. + # The most correct way to resolve this would be to add + # support for addresses to be associated with multiple symbols. + + self.ql.log.debug(f"Forwarding symbol {source_dll}.{source_symbol} to {target_dll}.{target_symbol}: Resolved symbol to ({forward_ea:#x})") def load_dll(self, name: str, is_driver: bool = False) -> int: dll_path, dll_name = self.__get_path_elements(name) From 6adafd598b4ee659cdfd37cb1a2ee8a08447f40a Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 16:47:04 +0200 Subject: [PATCH 30/35] Unify heap API hooks, address debug CRT init issues --- qiling/os/windows/dlls/kernel32/heapapi.py | 55 +++++-- qiling/os/windows/dlls/msvcrt.py | 163 +++++++++------------ 2 files changed, 119 insertions(+), 99 deletions(-) diff --git a/qiling/os/windows/dlls/kernel32/heapapi.py b/qiling/os/windows/dlls/kernel32/heapapi.py index 6746c6d8c..871a933fd 100644 --- a/qiling/os/windows/dlls/kernel32/heapapi.py +++ b/qiling/os/windows/dlls/kernel32/heapapi.py @@ -49,6 +49,17 @@ def hook_HeapCreate(ql: Qiling, address: int, params): return ql.os.heap.alloc(dwInitialSize) +def _HeapAlloc(ql: Qiling, address: int, params): + dwFlags = params["dwFlags"] + dwBytes = params["dwBytes"] + + ptr = ql.os.heap.alloc(dwBytes) + + if ptr and (dwFlags & HEAP_ZERO_MEMORY): + __zero_mem(ql.mem, ptr, dwBytes) + + return ptr + # DECLSPEC_ALLOCATOR LPVOID HeapAlloc( # HANDLE hHeap, # DWORD dwFlags, @@ -60,15 +71,7 @@ def hook_HeapCreate(ql: Qiling, address: int, params): 'dwBytes' : SIZE_T }) def hook_HeapAlloc(ql: Qiling, address: int, params): - dwFlags = params["dwFlags"] - dwBytes = params["dwBytes"] - - ptr = ql.os.heap.alloc(dwBytes) - - if ptr and (dwFlags & HEAP_ZERO_MEMORY): - __zero_mem(ql.mem, ptr, dwBytes) - - return ptr + return _HeapAlloc(ql, address, params) # DECLSPEC_ALLOCATOR LPVOID HeapReAlloc( # HANDLE hHeap, @@ -86,6 +89,14 @@ def hook_HeapReAlloc(ql: Qiling, address: int, params): base = params["lpMem"] newSize = params["dwBytes"] + if not base: + return _HeapAlloc(ql, address, params) + + if newSize == 0: + ql.os.heap.free(base) + + return 0 + oldSize = ql.os.heap.size(base) oldData = bytes(ql.mem.read(base, oldSize)) @@ -150,3 +161,29 @@ def hook_HeapSetInformation(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={}) def hook_GetProcessHeap(ql: Qiling, address: int, params): return ql.os.heap.start_address + +# BOOL HeapValidate( +# HANDLE hHeap, +# DWORD dwFlags, +# LPCVOID lpMem +# ); +@winsdkapi(cc=STDCALL, params={ + 'hHeap': PVOID, + 'dwFlags': DWORD, + 'lpMem': PVOID +}) +def hook_HeapValidate(ql: Qiling, address: int, params): + hHeap = params['hHeap'] + lpMem = params['lpMem'] + + if not hHeap: + return 0 + + # TODO: Maybe _find is a heap manager implementation + # detail, in which case we shouldn't rely on it. + chunk = ql.os.heap._find(lpMem) + + if not chunk: + return 0 + + return chunk.inuse diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index b209b20d6..763e74bd0 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -135,10 +135,9 @@ def hook__controlfp(ql: Qiling, address: int, params): # ); @winsdkapi(cc=CDECL, params={ 'func' : POINTER -}) +}, passthru=True) def hook_atexit(ql: Qiling, address: int, params): - ret = 0 - return ret + return # char*** __p__environ(void) @winsdkapi(cc=CDECL, params={}) @@ -211,20 +210,12 @@ def hook___p___argc(ql: Qiling, address: int, params): return ret # TODO: this one belongs to ucrtbase.dll -@winsdkapi(cc=CDECL, params={}) +@winsdkapi(cc=CDECL, params={}, passthru=True) def hook__get_initial_narrow_environment(ql: Qiling, address: int, params): - ret = 0 - - for i, (k, v) in enumerate(ql.env.items()): - entry = bytes(f'{k}={v}', 'ascii') + b'\x00' - p_entry = ql.os.heap.alloc(len(entry)) - - ql.mem.write(p_entry, entry) - - if i == 0: - ret = p_entry - - return ret + # If the native version of this function does not + # get to run, then debug versions of the CRT DLLs can fail + # their initialization. + return # int sprintf ( char * str, const char * format, ... ); @winsdkapi(cc=CDECL, params={ @@ -339,6 +330,18 @@ def __stdio_common_vsprintf(ql: Qiling, address: int, params, wstring: bool): def hook___stdio_common_vsprintf(ql: Qiling, address: int, params): return __stdio_common_vsprintf(ql, address, params, False) +@winsdkapi(cc=CDECL, params={ + '_Options' : PARAM_INT64, + '_Buffer' : POINTER, + '_BufferCount' : SIZE_T, + '_MaxCount' : SIZE_T, + '_Format' : STRING, + '_Locale' : DWORD, + '_ArgList' : POINTER +}) +def hook___stdio_common_vsnprintf(ql: Qiling, address: int, params): + return __stdio_common_vsprintf(ql, address, params, False) + @winsdkapi(cc=CDECL, params={ '_Options' : PARAM_INT64, '_Buffer' : POINTER, @@ -350,6 +353,18 @@ def hook___stdio_common_vsprintf(ql: Qiling, address: int, params): def hook___stdio_common_vswprintf(ql: Qiling, address: int, params): return __stdio_common_vsprintf(ql, address, params, True) +@winsdkapi(cc=CDECL, params={ + '_Options' : PARAM_INT64, + '_Buffer' : POINTER, + '_BufferCount' : SIZE_T, + '_MaxCount' : SIZE_T, + '_Format' : WSTRING, + '_Locale' : DWORD, + '_ArgList' : POINTER +}) +def hook___stdio_common_vsnwprintf(ql: Qiling, address: int, params): + return __stdio_common_vsprintf(ql, address, params, True) + # all the "_s" versions are aliases to their non-"_s" counterparts @winsdkapi(cc=CDECL, params={ @@ -383,6 +398,18 @@ def hook___stdio_common_vfwprintf_s(ql: Qiling, address: int, params): def hook___stdio_common_vsprintf_s(ql: Qiling, address: int, params): return hook___stdio_common_vsprintf.__wrapped__(ql, address, params) +@winsdkapi(cc=CDECL, params={ + '_Options' : PARAM_INT64, + '_Buffer' : POINTER, + '_BufferCount' : SIZE_T, + '_MaxCount' : SIZE_T, + '_Format' : STRING, + '_Locale' : DWORD, + '_ArgList' : POINTER +}) +def hook___stdio_common_vsnprintf_s(ql: Qiling, address: int, params): + return hook___stdio_common_vsnprintf.__wrapped__(ql, address, params) + @winsdkapi(cc=CDECL, params={ '_Options' : PARAM_INT64, '_Buffer' : POINTER, @@ -394,6 +421,18 @@ def hook___stdio_common_vsprintf_s(ql: Qiling, address: int, params): def hook___stdio_common_vswprintf_s(ql: Qiling, address: int, params): return hook___stdio_common_vswprintf.__wrapped__(ql, address, params) +@winsdkapi(cc=CDECL, params={ + '_Options' : PARAM_INT64, + '_Buffer' : POINTER, + '_BufferCount' : SIZE_T, + '_MaxCount' : SIZE_T, + '_Format' : WSTRING, + '_Locale' : DWORD, + '_ArgList' : POINTER +}) +def hook___stdio_common_vsnwprintf_s(ql: Qiling, address: int, params): + return hook___stdio_common_vsnwprintf.__wrapped__(ql, address, params) + @winsdkapi(cc=CDECL, params={}) def hook___lconv_init(ql: Qiling, address: int, params): return 0 @@ -449,50 +488,18 @@ def hook_strncmp(ql: Qiling, address: int, params): return result -def __malloc(ql: Qiling, address: int, params): - size = params['size'] - - return ql.os.heap.alloc(size) - @winsdkapi(cc=CDECL, params={ 'size' : UINT -}) +}, passthru=True) def hook__malloc_base(ql: Qiling, address: int, params): - return __malloc(ql, address, params) + return # void* malloc(unsigned int size) @winsdkapi(cc=CDECL, params={ 'size' : UINT -}) +}, passthru=True) def hook_malloc(ql: Qiling, address: int, params): - size = params['size'] - - return ql.os.heap.alloc(size) - -def __realloc(ql: Qiling, address: int, params): - block = params['block'] - size = params['size'] - - if not block: - return ql.os.heap.alloc(size) - - if size == 0: - ql.os.heap.free(block) - return 0 - - oldSize = ql.os.heap.size(block) - oldData = bytes(ql.mem.read(block, size)) - ql.os.heap.free(block) - - if size < oldSize: - oldData = oldData[0:size] - - newBase = ql.os.heap.alloc(size) - - if newBase: - ql.mem.write(newBase, oldData) - - return newBase + return # void* __cdecl _realloc_base( # void* const block, @@ -501,27 +508,22 @@ def __realloc(ql: Qiling, address: int, params): @winsdkapi(cc=CDECL, params={ 'block' : POINTER, 'size' : UINT -}) +}, passthru=True) def hook__realloc_base(ql: Qiling, address: int, params): - return __realloc(ql, address, params) - -def __free(ql: Qiling, address: int, params): - address = params['address'] - - ql.os.heap.free(address) + return @winsdkapi(cc=CDECL, params={ 'address': POINTER -}) +}, passthru=True) def hook__free_base(ql: Qiling, address: int, params): - return __free(ql, address, params) + return # void* free(void *address) @winsdkapi(cc=CDECL, params={ 'address': POINTER -}) +}, passthru=True) def hook_free(ql: Qiling, address: int, params): - return __free(ql, address, params) + return # _onexit_t _onexit( # _onexit_t function @@ -546,32 +548,16 @@ def hook__onexit(ql: Qiling, address: int, params): 'dest' : POINTER, 'c' : INT, 'count' : SIZE_T -}) +}, passthru=True) def hook_memset(ql: Qiling, address: int, params): - dest = params["dest"] - c = params["c"] - count = params["count"] - - ql.mem.write(dest, bytes([c] * count)) - - return dest - -def __calloc(ql: Qiling, address: int, params): - num = params['num'] - size = params['size'] - - count = num * size - ret = ql.os.heap.alloc(count) - ql.mem.write(ret, bytes([0] * count)) - - return ret + return @winsdkapi(cc=CDECL, params={ 'num' : SIZE_T, 'size' : SIZE_T -}) +}, passthru=True) def hook__calloc_base(ql: Qiling, address: int, params): - return __calloc(ql, address, params) + return # void *calloc( # size_t num, @@ -580,9 +566,9 @@ def hook__calloc_base(ql: Qiling, address: int, params): @winsdkapi(cc=CDECL, params={ 'num' : SIZE_T, 'size' : SIZE_T -}) +}, passthru=True) def hook_calloc(ql: Qiling, address: int, params): - return __calloc(ql, address, params) + return # void * memmove( # void *dest, @@ -593,12 +579,9 @@ def hook_calloc(ql: Qiling, address: int, params): 'dest' : POINTER, 'src' : POINTER, 'num' : SIZE_T -}) +}, passthru=True) def hook_memmove(ql: Qiling, address: int, params): - data = ql.mem.read(params['src'], params['num']) - ql.mem.write(params['dest'], bytes(data)) - - return params['dest'] + return # int _ismbblead( # unsigned int c From a3bccf5bc7d5816c60b6c628127320f2a7ec2560 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 16:50:16 +0200 Subject: [PATCH 31/35] Fix buffer overrun issue in LCMapString implementation --- qiling/os/windows/dlls/kernel32/winnls.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/qiling/os/windows/dlls/kernel32/winnls.py b/qiling/os/windows/dlls/kernel32/winnls.py index 296fe9999..7acebefe2 100644 --- a/qiling/os/windows/dlls/kernel32/winnls.py +++ b/qiling/os/windows/dlls/kernel32/winnls.py @@ -80,18 +80,19 @@ def hook_IsValidCodePage(ql: Qiling, address: int, params): return 1 def __LCMapString(ql: Qiling, address: int, params, wstring: bool): - lpSrcStr: str = params["lpSrcStr"] + lpSrcStr: int = params["lpSrcStr"] + cchSrc: int = params["cchSrc"] lpDestStr: int = params["lpDestStr"] cchDest: int = params["cchDest"] - enc = "utf-16le" if wstring else "utf-8" - res = f'{lpSrcStr}\x00' + char_size = 2 if wstring else 1 + byte_count = cchSrc * char_size if cchDest and lpDestStr: - # TODO maybe do some other check, for now is working - ql.mem.write(lpDestStr, res.encode(enc)) + source_bytes = ql.mem.read(lpSrcStr, byte_count) + ql.mem.write(lpDestStr, bytes(source_bytes)) - return len(res) + return cchSrc # int LCMapStringW( # LCID Locale, @@ -104,9 +105,9 @@ def __LCMapString(ql: Qiling, address: int, params, wstring: bool): @winsdkapi(cc=STDCALL, params={ 'Locale' : LCID, 'dwMapFlags' : DWORD, - 'lpSrcStr' : LPCWSTR, + 'lpSrcStr' : POINTER, 'cchSrc' : INT, - 'lpDestStr' : LPWSTR, + 'lpDestStr' : POINTER, 'cchDest' : INT }) def hook_LCMapStringW(ql: Qiling, address: int, params): @@ -123,9 +124,9 @@ def hook_LCMapStringW(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={ 'Locale' : LCID, 'dwMapFlags' : DWORD, - 'lpSrcStr' : LPCSTR, + 'lpSrcStr' : POINTER, 'cchSrc' : INT, - 'lpDestStr' : LPSTR, + 'lpDestStr' : POINTER, 'cchDest' : INT }) def hook_LCMapStringA(ql: Qiling, address: int, params): @@ -145,9 +146,9 @@ def hook_LCMapStringA(ql: Qiling, address: int, params): @winsdkapi(cc=STDCALL, params={ 'lpLocaleName' : LPCWSTR, 'dwMapFlags' : DWORD, - 'lpSrcStr' : LPCWSTR, + 'lpSrcStr' : POINTER, 'cchSrc' : INT, - 'lpDestStr' : LPWSTR, + 'lpDestStr' : POINTER, 'cchDest' : INT, 'lpVersionInformation' : LPNLSVERSIONINFO, 'lpReserved' : LPVOID, From fe51b28ea85e4823a648e546d313b561b4ca7aaf Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 16:55:17 +0200 Subject: [PATCH 32/35] Make requested change in PE loader --- qiling/loader/pe.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index ee1ed8a82..8ea5884bf 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -30,10 +30,12 @@ from logging import Logger from qiling import Qiling -ForwardedExport = namedtuple('ForwardedExport', [ - 'source_dll', 'source_ordinal', 'source_symbol', - 'target_dll', 'target_symbol' -]) +class ForwardedExport(NamedTuple): + source_dll: str + source_ordinal: str + source_symbol: str + target_dll: str + target_symbol: str class QlPeCacheEntry(NamedTuple): From 68aae4d7211eaa64b3525bc22bb9e5b91812f64a Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 17:06:31 +0200 Subject: [PATCH 33/35] Add __dllonexit hook --- qiling/os/windows/dlls/msvcrt.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 763e74bd0..e0ad4bebf 100644 --- a/qiling/os/windows/dlls/msvcrt.py +++ b/qiling/os/windows/dlls/msvcrt.py @@ -539,6 +539,27 @@ def hook__onexit(ql: Qiling, address: int, params): return addr +# _onexit_t __dllonexit( +# _onexit_t func, +# _PVFV ** pbegin, +# _PVFV ** pend +# ); +@winsdkapi(cc=STDCALL, params={ + 'function': POINTER, + 'pbegin': POINTER, + 'pend': POINTER +}) +def hook___dllonexit(ql: Qiling, address: int, params): + function = params['function'] + + if function: + addr = ql.os.heap.alloc(ql.arch.pointersize) + ql.mem.write_ptr(addr, function) + + return addr + + return 0 + # void *memset( # void *dest, # int c, From 4bd74596bbcc00a3ee2a1e882e35ae9d165ca68e Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 17:34:10 +0200 Subject: [PATCH 34/35] Add passthru exception-related hooks --- qiling/os/windows/dlls/ntdll.py | 142 ++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 33056c742..16956f73f 100644 --- a/qiling/os/windows/dlls/ntdll.py +++ b/qiling/os/windows/dlls/ntdll.py @@ -756,3 +756,145 @@ def hook_EtwNotificationRegister(ql: Qiling, address: int, params): ql.mem.write_ptr(reg_handle_ptr, handle.id) return STATUS_SUCCESS + +# NTSYSAPI +# VOID RtlRaiseException( +# PEXCEPTION_RECORD ExceptionRecord +# ); +@winsdkapi(cc=STDCALL, params={ + 'ExceptionRecord': PVOID +}, passthru=True) +def hook_RtlRaiseException(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# PRUNTIME_FUNCTION RtlVirtualUnwind( +# DWORD HandlerType, +# DWORD64 ImageBase, +# DWORD64 ControlPc, +# PRUNTIME_FUNCTION FunctionEntry, +# PCONTEXT ContextRecord, +# PVOID* HandlerData, +# PDWORD64 EstablisherFrame, +# PKNONVOLATILE_CONTEXT_POINTERS ContextPointers +# ); +@winsdkapi(cc=STDCALL, params={ + 'HandlerType': DWORD, + 'ImageBase': PVOID, + 'ControlPc': PVOID, + 'FunctionEntry': PVOID, + 'ContextRecord': PVOID, + 'HandlerData': PVOID, + 'EstablisherFrame': PVOID, + 'ContextPointers': PVOID +}, passthru=True) +def hook_RtlVirtualUnwind(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# VOID RtlUnwindEx( +# PVOID TargetFrame, +# PVOID TargetIp, +# PEXCEPTION_RECORD ExceptionRecord, +# PVOID ReturnValue, +# PCONTEXT OriginalContext, +# PUNWIND_HISTORY_TABLE HistoryTable +# ); +@winsdkapi(cc=STDCALL, params={ + 'TargetFrame': PVOID, + 'TargetIp': PVOID, + 'ExceptionRecord': PVOID, + 'ReturnValue': PVOID, + 'OriginalContext': PVOID, + 'HistoryTable': PVOID +}, passthru=True) +def hook_RtlUnwindEx(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# BOOLEAN RtlDispatchException( +# PEXCEPTION_RECORD ExceptionRecord, +# PCONTEXT ContextRecord +# ); +@winsdkapi(cc=STDCALL, params={ + 'ExceptionRecord': PVOID, + 'ContextRecord': PVOID +}, passthru=True) +def hook_RtlDispatchException(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# VOID RtlRestoreContext( +# PCONTEXT ContextRecord, +# PEXCEPTION_RECORD ExceptionRecord +# ); +@winsdkapi(cc=CDECL, params={ + 'ContextRecord': PVOID, + 'ExceptionRecord': PVOID +}, passthru=True) +def hook_RtlRestoreContext(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# VOID RtlCaptureContext( +# PCONTEXT ContextRecord +# ); +@winsdkapi(cc=STDCALL, params={ + 'ContextRecord': PVOID +}, passthru=True) +def hook_RtlCaptureContext(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# VOID RtlCaptureContext2( +# PCONTEXT ContextRecord, +# ULONG Flags +# ); +@winsdkapi(cc=STDCALL, params={ + 'ContextRecord': PVOID, + 'Flags': DWORD +}, passthru=True) +def hook_RtlCaptureContext2(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# NTSTATUS RtlInitializeExtendedContext2( +# USHORT Version, +# USHORT ContextFlags, +# ULONG ExtensionCount, +# ULONG *ExtensionSizes, +# ULONG BufferSize, +# PVOID Buffer, +# PCONTEXT Context, +# ULONG *LengthReturned +# ); +@winsdkapi(cc=STDCALL, params={ + 'Version': WORD, + 'ContextFlags': WORD, + 'ExtensionCount': DWORD, + 'ExtensionSizes': PVOID, + 'BufferSize': DWORD, + 'Buffer': PVOID, + 'Context': PVOID, + 'LengthReturned': PVOID +}, passthru=True) +def hook_RtlInitializeExtendedContext2(ql: Qiling, address: int, params): + return + +# NTSYSAPI +# NTSTATUS RtlGetExtendedContextLength2( +# USHORT Version, +# USHORT ContextFlags, +# ULONG ExtensionCount, +# ULONG *ExtensionSizes, +# PULONG Length +# ); +@winsdkapi(cc=STDCALL, params={ + 'Version': WORD, + 'ContextFlags': WORD, + 'ExtensionCount': DWORD, + 'ExtensionSizes': PVOID, + 'Length': PVOID +}, passthru=True) +def hook_RtlGetExtendedContextLength2(ql: Qiling, address: int, params): + return From f390b476516d91e20ebdffc87e4330d0f5580f61 Mon Sep 17 00:00:00 2001 From: Jacob Farnsworth <6502enthusiast@gmail.com> Date: Wed, 2 Apr 2025 18:39:53 +0200 Subject: [PATCH 35/35] Restore old RaiseException hook, add special case for x86 --- .../windows/dlls/kernel32/errhandlingapi.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/qiling/os/windows/dlls/kernel32/errhandlingapi.py b/qiling/os/windows/dlls/kernel32/errhandlingapi.py index 084ffabc8..ece53aa80 100644 --- a/qiling/os/windows/dlls/kernel32/errhandlingapi.py +++ b/qiling/os/windows/dlls/kernel32/errhandlingapi.py @@ -115,3 +115,47 @@ def hook_RemoveVectoredExceptionHandler(ql: Qiling, address: int, params): hook.remove() return 0 + +# VOID RaiseException( +# DWORD dwExceptionCode, +# DWORD dwExceptionFlags, +# DWORD nNumberOfArguments, +# CONST ULONG_PTR* lpArguments +# ); +@winsdkapi(cc=STDCALL, params={ + 'dwExceptionCode': DWORD, + 'dwExceptionFlags': DWORD, + 'nNumberOfArguments': DWORD, + 'lpArguments': PVOID +}, passthru=True) +def hook_RaiseException(ql: Qiling, address: int, params): + # On x86_64, RaiseException will call RtlRaiseException, + # which calls the exception dispatcher directly. The native + # exception dispatching code mostly works correctly + # for software exceptions, so we shall simply continue + # through to the native dispatcher in this case. + if ql.arch.type is not QL_ARCH.X86: + return + + # On x86, the situation is different. RtlRaiseException + # will call ZwRaiseException, which uses a syscall. + # However, Qiling doesn't really support Windows syscalls + # right now. + # We will treat all exceptions as unhandled exceptions, + # which is better than nothing. + # TODO: Get kernel exception dispatching working properly, + # then first-chance software exceptions, SEH, and C++ + # exceptions can work on 32-bit Windows too. + nNumberOfArguments = params['nNumberOfArguments'] + lpArguments = params['lpArguments'] + + handle = ql.os.handle_manager.search("TopLevelExceptionHandler") + + if handle is None: + ql.log.warning(f'RaiseException: top level exception handler not found') + return + + exception_handler = handle.obj + args = [(PARAM_INTN, ql.mem.read_ptr(lpArguments + i * ql.arch.pointersize)) for i in range(nNumberOfArguments)] if lpArguments else [] + + ql.os.fcall.call_native(exception_handler, args, None)