diff --git a/examples/scripts/dllscollector.bat b/examples/scripts/dllscollector.bat index 2b85a83e9..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 @@ -131,6 +134,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 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; +} diff --git a/qiling/arch/x86_utils.py b/qiling/arch/x86_utils.py index 4726ef745..1bc9f8953 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,8 @@ 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)) + self.is_long_mode = ql.arch.type is QL_ARCH.X8664 + self.array = GDTArray(ql.mem, base, num_entries) @staticmethod @@ -93,7 +96,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 +152,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 +188,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 diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 7e6746de6..8ea5884bf 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,6 +30,13 @@ from logging import Logger from qiling import Qiling +class ForwardedExport(NamedTuple): + source_dll: str + source_ordinal: str + source_symbol: str + target_dll: str + target_symbol: str + class QlPeCacheEntry(NamedTuple): ba: int @@ -79,6 +87,16 @@ class Process: export_symbols: MutableMapping[int, Dict[str, Any]] libcache: Optional[QlPeCache] + # maps image base to RVA of its function table + function_table_lookup: Dict[int, int] + + # 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 @@ -105,6 +123,108 @@ 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 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 is not QL_ARCH.X86: + + # 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 = list(pe.DIRECTORY_ENTRY_EXCEPTION) + + if image_base not in self.function_tables: + 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. + 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: + 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 + + 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 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) @@ -195,6 +315,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) @@ -203,6 +326,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, @@ -227,6 +375,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 @@ -281,8 +431,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: @@ -674,12 +824,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' @@ -709,6 +861,9 @@ def run(self): self.export_symbols = {} self.import_address_table = {} 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 @@ -841,6 +996,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]") diff --git a/qiling/os/windows/const.py b/qiling/os/windows/const.py index d001925e5..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 # ... @@ -638,6 +639,7 @@ ProcessDebugObjectHandle = 30 ProcessDebugFlags = 31 ProcessExecuteFlags = 34 +ProcessCookie = 36 ProcessImageInformation = 37 ProcessMitigationPolicy = 52 ProcessFaultInformation = 63 diff --git a/qiling/os/windows/dlls/kernel32/errhandlingapi.py b/qiling/os/windows/dlls/kernel32/errhandlingapi.py index 4dba7efb6..ece53aa80 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 -}) -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 @@ -151,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) diff --git a/qiling/os/windows/dlls/kernel32/heapapi.py b/qiling/os/windows/dlls/kernel32/heapapi.py index c48e5aa8f..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,45 @@ 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"] + return _HeapAlloc(ql, address, params) - ptr = ql.os.heap.alloc(dwBytes) +# 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"] - if ptr and (dwFlags & HEAP_ZERO_MEMORY): - __zero_mem(ql.mem, ptr, dwBytes) + if not base: + return _HeapAlloc(ql, address, params) + + if newSize == 0: + ql.os.heap.free(base) + + return 0 - return ptr + 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, @@ -120,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/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 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, diff --git a/qiling/os/windows/dlls/msvcrt.py b/qiling/os/windows/dlls/msvcrt.py index 2db18455f..e0ad4bebf 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 ( @@ -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={}) @@ -174,17 +173,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 +182,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): @@ -233,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={ @@ -303,13 +272,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'] @@ -368,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, @@ -379,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={ @@ -412,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, @@ -423,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 @@ -478,43 +488,42 @@ 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 __free(ql: Qiling, address: int, params): - address = params['address'] + return - ql.os.heap.free(address) +# void* __cdecl _realloc_base( +# void* const block, +# size_t const size +# ) +@winsdkapi(cc=CDECL, params={ + 'block' : POINTER, + 'size' : UINT +}, passthru=True) +def hook__realloc_base(ql: Qiling, address: int, params): + 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 @@ -530,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, @@ -539,32 +569,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, @@ -573,9 +587,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, @@ -586,12 +600,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 @@ -644,3 +655,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 diff --git a/qiling/os/windows/dlls/ntdll.py b/qiling/os/windows/dlls/ntdll.py index 92ba4a84b..16956f73f 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, @@ -39,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'] @@ -68,6 +70,24 @@ def _QueryInformationProcess(ql: Qiling, address: int, params): res_data = bytes(pci_obj) + + elif flag == ProcessCookie: + hCurrentProcess = (1 << ql.arch.bits) - 1 + + 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}') @@ -452,4 +472,429 @@ 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) + + base_addr = containing_image.base if containing_image else 0 + + 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, +# [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 is QL_ARCH.X86: + raise QlErrorNotImplemented("RtlLookupFunctionEntry is not implemented for x86") + + 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 + + # 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 is QL_ARCH.X86: + raise QlErrorNotImplemented("RtlLookupFunctionTable is not implemented for x86") + + base_addr, function_table_addr = _FindImageBaseAndFunctionTable(ql, control_pc, image_base_ptr) + + # 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 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 + +# NTSYSAPI +# NTSTATUS +# ZwRaiseException ( +# IN PEXCEPTION_RECORD ExceptionRecord, +# IN PCONTEXT ContextRecord, +# IN BOOLEAN FirstChance +# ); +@winsdkapi(cc=STDCALL, 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'] + 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. + # 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 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() + return + + 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. + + # 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) + +# 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 + +# 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 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 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