From 370665b52081a4c0e2d34ed428f1e1dbcbf2778e Mon Sep 17 00:00:00 2001 From: momo5502 Date: Fri, 13 Sep 2024 13:48:21 +0200 Subject: [PATCH] Introduce windows emulation context This fixes #6 --- src/windows_emulator/main.cpp | 577 +------------------- src/windows_emulator/process_context.hpp | 8 +- src/windows_emulator/syscalls.cpp | 9 +- src/windows_emulator/syscalls.hpp | 3 + src/windows_emulator/windows_emulator.cpp | 611 ++++++++++++++++++++++ src/windows_emulator/windows_emulator.hpp | 63 +++ 6 files changed, 703 insertions(+), 568 deletions(-) create mode 100644 src/windows_emulator/windows_emulator.cpp create mode 100644 src/windows_emulator/windows_emulator.hpp diff --git a/src/windows_emulator/main.cpp b/src/windows_emulator/main.cpp index 3ae3b7a..b68ff45 100644 --- a/src/windows_emulator/main.cpp +++ b/src/windows_emulator/main.cpp @@ -9,27 +9,11 @@ #include "reflect_extension.hpp" #include -#include -#include - -#include "context_frame.hpp" +#include "windows_emulator.hpp" #include "debugging/x64_gdb_stub_handler.hpp" -#define GS_SEGMENT_ADDR 0x6000000ULL -#define GS_SEGMENT_SIZE (20 << 20) // 20 MB - -#define IA32_GS_BASE_MSR 0xC0000101 - -#define STACK_SIZE 0x40000 -#define STACK_ADDRESS (0x80000000000 - STACK_SIZE) -#define KUSD_ADDRESS 0x7ffe0000 - -#define GDT_ADDR 0x30000 -#define GDT_LIMIT 0x1000 -#define GDT_ENTRY_SIZE 0x8 - bool use_gdb = false; namespace @@ -100,556 +84,19 @@ namespace }); } - template - emulator_object allocate_object_on_stack(x64_emulator& emu) - { - const auto old_sp = emu.reg(x64_register::rsp); - const auto new_sp = align_down(old_sp - sizeof(CONTEXT), - std::max(alignof(CONTEXT), alignof(x64_emulator::pointer_type))); - emu.reg(x64_register::rsp, new_sp); - - return {emu, new_sp}; - } - - void unalign_stack(x64_emulator& emu) - { - auto sp = emu.reg(x64_register::rsp); - sp = align_down(sp - 0x10, 0x10) + 8; - emu.reg(x64_register::rsp, sp); - } - - void setup_stack(x64_emulator& emu, const uint64_t stack_base, const size_t stack_size) - { - emu.allocate_memory(stack_base, stack_size, memory_permission::read_write); - - const uint64_t stack_end = stack_base + stack_size; - emu.reg(x64_register::rsp, stack_end); - } - - emulator_allocator setup_gs_segment(x64_emulator& emu, const uint64_t segment_base, const uint64_t size) - { - struct msr_value - { - uint32_t id; - uint64_t value; - }; - - const msr_value value{ - IA32_GS_BASE_MSR, - segment_base - }; - - emu.write_register(x64_register::msr, &value, sizeof(value)); - emu.allocate_memory(segment_base, size, memory_permission::read_write); - - return {emu, segment_base, size}; - } - - emulator_object setup_kusd(x64_emulator& emu) - { - emu.allocate_memory(KUSD_ADDRESS, page_align_up(sizeof(KUSER_SHARED_DATA)), memory_permission::read); - - const emulator_object kusd_object{emu, KUSD_ADDRESS}; - kusd_object.access([](KUSER_SHARED_DATA& kusd) - { - const auto& real_kusd = *reinterpret_cast(KUSD_ADDRESS); - - memcpy(&kusd, &real_kusd, sizeof(kusd)); - - kusd.ImageNumberLow = IMAGE_FILE_MACHINE_I386; - kusd.ImageNumberHigh = IMAGE_FILE_MACHINE_AMD64; - - memset(&kusd.ProcessorFeatures, 0, sizeof(kusd.ProcessorFeatures)); - - // ... - }); - - return kusd_object; - } - - uint64_t copy_string(x64_emulator& emu, emulator_allocator& allocator, const void* base_ptr, const uint64_t offset, - const size_t length) - { - if (!length) - { - return 0; - } - - const auto length_to_allocate = length + 2; - const auto str_obj = allocator.reserve(length_to_allocate); - emu.write_memory(str_obj, static_cast(base_ptr) + offset, length); - - return str_obj; - } - - ULONG copy_string_as_relative(x64_emulator& emu, emulator_allocator& allocator, const uint64_t result_base, - const void* base_ptr, const uint64_t offset, - const size_t length) - { - const auto address = copy_string(emu, allocator, base_ptr, offset, length); - if (!address) - { - return 0; - } - - assert(address > result_base); - return static_cast(address - result_base); - } - - emulator_object clone_api_set_map(x64_emulator& emu, emulator_allocator& allocator, - const API_SET_NAMESPACE& orig_api_set_map) - { - const auto api_set_map_obj = allocator.reserve(); - const auto ns_entries_obj = allocator.reserve(orig_api_set_map.Count); - const auto hash_entries_obj = allocator.reserve(orig_api_set_map.Count); - - api_set_map_obj.access([&](API_SET_NAMESPACE& api_set) - { - api_set = orig_api_set_map; - api_set.EntryOffset = static_cast(ns_entries_obj.value() - api_set_map_obj.value()); - api_set.HashOffset = static_cast(hash_entries_obj.value() - api_set_map_obj.value()); - }); - - const auto orig_ns_entries = offset_pointer(&orig_api_set_map, - orig_api_set_map.EntryOffset); - const auto orig_hash_entries = offset_pointer(&orig_api_set_map, - orig_api_set_map.HashOffset); - - for (ULONG i = 0; i < orig_api_set_map.Count; ++i) - { - auto ns_entry = orig_ns_entries[i]; - const auto hash_entry = orig_hash_entries[i]; - - ns_entry.NameOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), &orig_api_set_map, - ns_entry.NameOffset, ns_entry.NameLength); - - if (!ns_entry.ValueCount) - { - continue; - } - - const auto values_obj = allocator.reserve(ns_entry.ValueCount); - const auto orig_values = offset_pointer(&orig_api_set_map, - ns_entry.ValueOffset); - - ns_entry.ValueOffset = static_cast(values_obj.value() - api_set_map_obj.value()); - - for (ULONG j = 0; j < ns_entry.ValueCount; ++j) - { - auto value = orig_values[j]; - - value.ValueOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), &orig_api_set_map, - value.ValueOffset, value.ValueLength); - - if (value.NameLength) - { - value.NameOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), - &orig_api_set_map, - value.NameOffset, value.NameLength); - } - - values_obj.write(value, j); - } - - ns_entries_obj.write(ns_entry, i); - hash_entries_obj.write(hash_entry, i); - } - - //watch_object(emu, api_set_map_obj); - - return api_set_map_obj; - } - - emulator_object build_api_set_map(x64_emulator& emu, emulator_allocator& allocator) - { - const auto& orig_api_set_map = *NtCurrentTeb()->ProcessEnvironmentBlock->ApiSetMap; - return clone_api_set_map(emu, allocator, orig_api_set_map); - } - - emulator_allocator create_allocator(emulator& emu, const size_t size) - { - const auto base = emu.find_free_allocation_base(size); - emu.allocate_memory(base, size, memory_permission::read_write); - - return emulator_allocator{emu, base, size}; - } - - void setup_gdt(x64_emulator& emu) - { - constexpr uint64_t gdtr[4] = {0, GDT_ADDR, GDT_LIMIT, 0}; - emu.write_register(x64_register::gdtr, &gdtr, sizeof(gdtr)); - emu.allocate_memory(GDT_ADDR, GDT_LIMIT, memory_permission::read); - - emu.write_memory(GDT_ADDR + 6 * (sizeof(uint64_t)), 0xEFFE000000FFFF); - emu.reg(x64_register::cs, 0x33); - - emu.write_memory(GDT_ADDR + 5 * (sizeof(uint64_t)), 0xEFF6000000FFFF); - emu.reg(x64_register::ss, 0x2B); - } - - process_context setup_context(x64_emulator& emu, const std::filesystem::path& file) - { - process_context context{emu}; - - setup_stack(emu, STACK_ADDRESS, STACK_SIZE); - setup_gdt(emu); - - context.kusd = setup_kusd(emu); - context.gs_segment = setup_gs_segment(emu, GS_SEGMENT_ADDR, GS_SEGMENT_SIZE); - - auto allocator = create_allocator(emu, 1 << 20); - - auto& gs = context.gs_segment; - - context.teb = gs.reserve(); - context.peb = gs.reserve(); - context.process_params = gs.reserve(); - - context.teb.access([&](TEB& teb) - { - teb.ClientId.UniqueProcess = reinterpret_cast(1); - teb.ClientId.UniqueThread = reinterpret_cast(2); - teb.NtTib.StackLimit = reinterpret_cast(STACK_ADDRESS); - teb.NtTib.StackBase = reinterpret_cast((STACK_ADDRESS + STACK_SIZE)); - teb.NtTib.Self = &context.teb.ptr()->NtTib; - teb.ProcessEnvironmentBlock = context.peb.ptr(); - }); - - context.process_params.access([&](RTL_USER_PROCESS_PARAMETERS& proc_params) - { - proc_params.Length = sizeof(proc_params); - proc_params.Flags = 0x6001 | 0x80000000; // Prevent CsrClientConnectToServer - - proc_params.ConsoleHandle = CONSOLE_HANDLE.h; - proc_params.StandardOutput = STDOUT_HANDLE.h; - proc_params.StandardInput = STDIN_HANDLE.h; - proc_params.StandardError = proc_params.StandardOutput; - - gs.make_unicode_string(proc_params.CurrentDirectory.DosPath, file.parent_path().wstring()); - gs.make_unicode_string(proc_params.ImagePathName, file.wstring()); - gs.make_unicode_string(proc_params.CommandLine, file.wstring()); - }); - - context.peb.access([&](PEB& peb) - { - peb.ImageBaseAddress = nullptr; - peb.ProcessParameters = context.process_params.ptr(); - peb.ApiSetMap = build_api_set_map(emu, allocator).ptr(); - - peb.ProcessHeap = nullptr; - peb.ProcessHeaps = nullptr; - peb.HeapSegmentReserve = 0x0000000000100000; // TODO: Read from executable - peb.HeapSegmentCommit = 0x0000000000002000; - peb.HeapDeCommitTotalFreeThreshold = 0x0000000000010000; - peb.HeapDeCommitFreeBlockThreshold = 0x0000000000001000; - peb.NumberOfHeaps = 0x00000000; - peb.MaximumNumberOfHeaps = 0x00000010; - }); - - return context; - } - - uint64_t find_exported_function(const std::vector& exports, const std::string_view name) - { - for (auto& symbol : exports) - { - if (symbol.name == name) - { - return symbol.address; - } - } - - return 0; - } - - using exception_record_map = std::unordered_map>; - - emulator_object save_exception_record(emulator_allocator& allocator, - const EXCEPTION_RECORD& record, - exception_record_map& record_mapping) - { - const auto record_obj = allocator.reserve(); - record_obj.write(record); - - if (record.ExceptionRecord) - { - record_mapping[&record] = record_obj; - - emulator_object nested_record_obj{}; - const auto nested_record = record_mapping.find(record.ExceptionRecord); - - if (nested_record != record_mapping.end()) - { - nested_record_obj = nested_record->second; - } - else - { - nested_record_obj = save_exception_record(allocator, *record.ExceptionRecord, - record_mapping); - } - - record_obj.access([&](EXCEPTION_RECORD& r) - { - r.ExceptionRecord = nested_record_obj.ptr(); - }); - } - - return record_obj; - } - - emulator_object save_exception_record(emulator_allocator& allocator, - const EXCEPTION_RECORD& record) - { - exception_record_map record_mapping{}; - return save_exception_record(allocator, record, record_mapping); - } - - uint32_t map_violation_operation_to_parameter(const memory_operation operation) - { - switch (operation) - { - default: - case memory_operation::read: - return 0; - case memory_operation::write: - return 1; - case memory_operation::exec: - return 1; - } - } - - size_t calculate_exception_record_size(const EXCEPTION_RECORD& record) - { - std::unordered_set records{}; - size_t total_size = 0; - - const EXCEPTION_RECORD* current_record = &record; - while (current_record) - { - if (!records.insert(current_record).second) - { - break; - } - - total_size += sizeof(*current_record); - current_record = record.ExceptionRecord; - } - - return total_size; - } - - struct machine_frame - { - uint64_t rip; - uint64_t cs; - uint64_t eflags; - uint64_t rsp; - uint64_t ss; - }; - - void dispatch_exception_pointers(x64_emulator& emu, const uint64_t dispatcher, const EXCEPTION_POINTERS pointers) - { - constexpr auto mach_frame_size = 0x40; - constexpr auto context_record_size = 0x4F0; - const auto exception_record_size = calculate_exception_record_size(*pointers.ExceptionRecord); - const auto combined_size = align_up(exception_record_size + context_record_size, 0x10); - - assert(combined_size == 0x590); - - const auto allocation_size = combined_size + mach_frame_size; - - const auto initial_sp = emu.reg(x64_register::rsp); - const auto new_sp = align_down(initial_sp - allocation_size, 0x100); - - const auto total_size = initial_sp - new_sp; - assert(total_size >= allocation_size); - - std::vector zero_memory{}; - zero_memory.resize(total_size, 0); - - emu.write_memory(new_sp, zero_memory.data(), zero_memory.size()); - - emu.reg(x64_register::rsp, new_sp); - emu.reg(x64_register::rip, dispatcher); - - const emulator_object context_record_obj{emu, new_sp}; - context_record_obj.write(*pointers.ContextRecord); - - emulator_allocator allocator{emu, new_sp + context_record_size, exception_record_size}; - const auto exception_record_obj = save_exception_record(allocator, *pointers.ExceptionRecord); - - if (exception_record_obj.value() != allocator.get_base()) - { - throw std::runtime_error("Bad exception record position on stack"); - } - - const emulator_object machine_frame_obj{emu, new_sp + combined_size}; - machine_frame_obj.access([&](machine_frame& frame) - { - frame.rip = pointers.ContextRecord->Rip; - frame.rsp = pointers.ContextRecord->Rsp; - frame.ss = pointers.ContextRecord->SegSs; - frame.cs = pointers.ContextRecord->SegCs; - frame.eflags = pointers.ContextRecord->EFlags; - }); - - printf("ContextRecord: %llX\n", context_record_obj.value()); - printf("ExceptionRecord: %llX\n", exception_record_obj.value()); - } - - void dispatch_access_violation(x64_emulator& emu, const uint64_t dispatcher, const uint64_t address, - const memory_operation operation) - { - CONTEXT ctx{}; - ctx.ContextFlags = CONTEXT_ALL; - context_frame::save(emu, ctx); - - EXCEPTION_RECORD record{}; - memset(&record, 0, sizeof(record)); - record.ExceptionCode = static_cast(STATUS_ACCESS_VIOLATION); - record.ExceptionFlags = 0; - record.ExceptionRecord = nullptr; - record.ExceptionAddress = reinterpret_cast(emu.read_instruction_pointer()); - record.NumberParameters = 2; - record.ExceptionInformation[0] = map_violation_operation_to_parameter(operation); - record.ExceptionInformation[1] = address; - - EXCEPTION_POINTERS pointers{}; - pointers.ContextRecord = &ctx; - pointers.ExceptionRecord = &record; - - dispatch_exception_pointers(emu, dispatcher, pointers); - } - void run() { - const auto emu = unicorn::create_x64_emulator(); - const std::filesystem::path application = R"(C:\Program Files (x86)\Steam\steamapps\common\Hogwarts Legacy\Phoenix\Binaries\Win64\HogwartsLegacy.exe)"; - auto context = setup_context(*emu, application); - context.module_manager = module_manager(*emu); - - context.executable = context.module_manager.map_module(application); - - context.peb.access([&](PEB& peb) - { - peb.ImageBaseAddress = reinterpret_cast(context.executable->image_base); - }); - - context.ntdll = context.module_manager.map_module(R"(C:\Windows\System32\ntdll.dll)"); - context.win32u = context.module_manager.map_module(R"(C:\Windows\System32\win32u.dll)"); - - const auto ldr_initialize_thunk = find_exported_function(context.ntdll->exports, "LdrInitializeThunk"); - const auto rtl_user_thread_start = find_exported_function(context.ntdll->exports, "RtlUserThreadStart"); - const auto ki_user_exception_dispatcher = find_exported_function( - context.ntdll->exports, "KiUserExceptionDispatcher"); - - syscall_dispatcher dispatcher{context.ntdll->exports, context.win32u->exports}; - - emu->hook_instruction(x64_hookable_instructions::syscall, [&] - { - dispatcher.dispatch(*emu, context); - return instruction_hook_continuation::skip_instruction; - }); - - emu->hook_instruction(x64_hookable_instructions::invalid, [&] - { - const auto ip = emu->read_instruction_pointer(); - printf("Invalid instruction at: %llX\n", ip); - return instruction_hook_continuation::skip_instruction; - }); - - emu->hook_interrupt([&](int interrupt) - { - printf("Interrupt: %i %llX\n", interrupt, emu->read_instruction_pointer()); - }); - - emu->hook_memory_violation([&](const uint64_t address, const size_t size, const memory_operation operation, - const memory_violation_type type) - { - const auto permission = get_permission_string(operation); - const auto ip = emu->read_instruction_pointer(); - - - if (type == memory_violation_type::protection) - { - printf("Protection violation: %llX (%zX) - %s at %llX\n", address, size, permission.c_str(), ip); - } - else if (type == memory_violation_type::unmapped) - { - printf("Mapping violation: %llX (%zX) - %s at %llX\n", address, size, permission.c_str(), ip); - } + windows_emulator win_emu{application}; - dispatch_access_violation(*emu, ki_user_exception_dispatcher, address, operation); - return memory_violation_continuation::resume; - }); + watch_object(win_emu.emu(), win_emu.process().teb); + watch_object(win_emu.emu(), win_emu.process().peb); + watch_object(win_emu.emu(), win_emu.process().process_params); + watch_object(win_emu.emu(), win_emu.process().kusd); - watch_object(*emu, context.teb); - watch_object(*emu, context.peb); - watch_object(*emu, context.process_params); - watch_object(*emu, context.kusd); - - context.verbose = false; - - emu->hook_memory_execution(0, std::numeric_limits::max(), [&](const uint64_t address, const size_t) - { - ++context.executed_instructions; - - if(address == 0x1800629BA) - { - puts("Bad dll"); - emu->stop(); - } - - const auto* binary = context.module_manager.find_by_address(address); - - if (binary) - { - const auto export_entry = binary->address_names.find(address); - if (export_entry != binary->address_names.end()) - { - printf("Executing function: %s - %s (%llX)\n", binary->name.c_str(), export_entry->second.c_str(), - address); - } - else if (address == binary->entry_point) - { - printf("Executing entry point: %s (%llX)\n", binary->name.c_str(), address); - } - } - - if (!context.verbose) - { - return; - } - - printf( - "Inst: %16llX - RAX: %16llX - RBX: %16llX - RCX: %16llX - RDX: %16llX - R8: %16llX - R9: %16llX - RDI: %16llX - RSI: %16llX - %s\n", - address, - emu->reg(x64_register::rax), emu->reg(x64_register::rbx), emu->reg(x64_register::rcx), - emu->reg(x64_register::rdx), emu->reg(x64_register::r8), emu->reg(x64_register::r9), - emu->reg(x64_register::rdi), emu->reg(x64_register::rsi), binary ? binary->name.c_str() : ""); - }); - - CONTEXT ctx{}; - ctx.ContextFlags = CONTEXT_ALL; - - unalign_stack(*emu); - - context_frame::save(*emu, ctx); - - ctx.Rip = rtl_user_thread_start; - ctx.Rcx = context.executable->entry_point; - - const auto ctx_obj = allocate_object_on_stack(*emu); - ctx_obj.write(ctx); - - unalign_stack(*emu); - - emu->reg(x64_register::rcx, ctx_obj.value()); - emu->reg(x64_register::rdx, context.ntdll->image_base); - emu->reg(x64_register::rip, ldr_initialize_thunk); + win_emu.set_verbose(false); try { @@ -657,17 +104,17 @@ namespace { puts("Launching gdb stub..."); - x64_gdb_stub_handler handler{*emu}; + x64_gdb_stub_handler handler{win_emu.emu()}; run_gdb_stub(handler, "i386:x86-64", gdb_registers.size(), "0.0.0.0:28960"); } else { - emu->start_from_ip(); + win_emu.emu().start_from_ip(); } } catch (...) { - printf("Emulation failed at: %llX\n", emu->reg(x64_register::rip)); + printf("Emulation failed at: %llX\n", win_emu.emu().read_instruction_pointer()); throw; } @@ -693,8 +140,8 @@ int main(int /*argc*/, char** /*argv*/) { puts(e.what()); -#ifdef _WIN32 - //MessageBoxA(nullptr, e.what(), "ERROR", MB_ICONERROR); +#if defined(_WIN32) && 0 + MessageBoxA(nullptr, e.what(), "ERROR", MB_ICONERROR); #endif } diff --git a/src/windows_emulator/process_context.hpp b/src/windows_emulator/process_context.hpp index e068f28..4ac4423 100644 --- a/src/windows_emulator/process_context.hpp +++ b/src/windows_emulator/process_context.hpp @@ -100,6 +100,8 @@ struct process_context mapped_module* ntdll{}; mapped_module* win32u{}; + uint64_t ki_user_exception_dispatcher{}; + uint64_t shared_section_size{}; handle_store events{}; @@ -108,8 +110,6 @@ struct process_context std::map atoms{}; emulator_allocator gs_segment; - bool verbose{false}; - void serialize(utils::buffer_serializer& buffer) const { buffer.write(this->executed_instructions); @@ -123,6 +123,8 @@ struct process_context buffer.write(this->ntdll->image_base); buffer.write(this->win32u->image_base); + buffer.write(this->ki_user_exception_dispatcher); + buffer.write(this->shared_section_size); buffer.write(this->events); buffer.write(this->files); @@ -148,6 +150,8 @@ struct process_context this->ntdll = this->module_manager.find_by_address(ntdll_base); this->win32u = this->module_manager.find_by_address(win32u_base); + buffer.read(this->ki_user_exception_dispatcher); + buffer.read(this->shared_section_size); buffer.read(this->events); buffer.read(this->files); diff --git a/src/windows_emulator/syscalls.cpp b/src/windows_emulator/syscalls.cpp index fa76106..6f7a116 100644 --- a/src/windows_emulator/syscalls.cpp +++ b/src/windows_emulator/syscalls.cpp @@ -1577,8 +1577,10 @@ namespace } } -syscall_dispatcher::syscall_dispatcher(const exported_symbols& ntdll_exports, const exported_symbols& win32u_exports) +void syscall_dispatcher::setup(const exported_symbols& ntdll_exports, const exported_symbols& win32u_exports) { + this->handlers_ = {}; + const auto ntdll_syscalls = find_syscalls(ntdll_exports); const auto win32u_syscalls = find_syscalls(win32u_exports); @@ -1588,6 +1590,11 @@ syscall_dispatcher::syscall_dispatcher(const exported_symbols& ntdll_exports, co this->add_handlers(); } +syscall_dispatcher::syscall_dispatcher(const exported_symbols& ntdll_exports, const exported_symbols& win32u_exports) +{ + this->setup(ntdll_exports, win32u_exports); +} + void syscall_dispatcher::add_handlers() { std::unordered_map handler_mapping{}; diff --git a/src/windows_emulator/syscalls.hpp b/src/windows_emulator/syscalls.hpp index 0b29050..b8b2a3e 100644 --- a/src/windows_emulator/syscalls.hpp +++ b/src/windows_emulator/syscalls.hpp @@ -16,6 +16,7 @@ struct syscall_handler_entry class syscall_dispatcher { public: + syscall_dispatcher() = default; syscall_dispatcher(const exported_symbols& ntdll_exports, const exported_symbols& win32u_exports); void dispatch(x64_emulator& emu, process_context& context); @@ -23,6 +24,8 @@ class syscall_dispatcher void serialize(utils::buffer_serializer& buffer) const; void deserialize(utils::buffer_deserializer& buffer); + void setup(const exported_symbols& ntdll_exports, const exported_symbols& win32u_exports); + private: std::unordered_map handlers_{}; diff --git a/src/windows_emulator/windows_emulator.cpp b/src/windows_emulator/windows_emulator.cpp new file mode 100644 index 0000000..c6e9e04 --- /dev/null +++ b/src/windows_emulator/windows_emulator.cpp @@ -0,0 +1,611 @@ +#include "std_include.hpp" +#include "windows_emulator.hpp" +#include "context_frame.hpp" + +#define GS_SEGMENT_ADDR 0x6000000ULL +#define GS_SEGMENT_SIZE (20 << 20) // 20 MB + +#define IA32_GS_BASE_MSR 0xC0000101 + +#define STACK_SIZE 0x40000 +#define STACK_ADDRESS (0x80000000000 - STACK_SIZE) +#define KUSD_ADDRESS 0x7ffe0000 + +#define GDT_ADDR 0x30000 +#define GDT_LIMIT 0x1000 +#define GDT_ENTRY_SIZE 0x8 + +namespace +{ + template + emulator_object allocate_object_on_stack(x64_emulator& emu) + { + const auto old_sp = emu.reg(x64_register::rsp); + const auto new_sp = align_down(old_sp - sizeof(CONTEXT), + std::max(alignof(CONTEXT), alignof(x64_emulator::pointer_type))); + emu.reg(x64_register::rsp, new_sp); + + return {emu, new_sp}; + } + + void unalign_stack(x64_emulator& emu) + { + auto sp = emu.reg(x64_register::rsp); + sp = align_down(sp - 0x10, 0x10) + 8; + emu.reg(x64_register::rsp, sp); + } + + void setup_stack(x64_emulator& emu, const uint64_t stack_base, const size_t stack_size) + { + emu.allocate_memory(stack_base, stack_size, memory_permission::read_write); + + const uint64_t stack_end = stack_base + stack_size; + emu.reg(x64_register::rsp, stack_end); + } + + emulator_allocator setup_gs_segment(x64_emulator& emu, const uint64_t segment_base, const uint64_t size) + { + struct msr_value + { + uint32_t id; + uint64_t value; + }; + + const msr_value value{ + IA32_GS_BASE_MSR, + segment_base + }; + + emu.write_register(x64_register::msr, &value, sizeof(value)); + emu.allocate_memory(segment_base, size, memory_permission::read_write); + + return {emu, segment_base, size}; + } + + emulator_object setup_kusd(x64_emulator& emu) + { + emu.allocate_memory(KUSD_ADDRESS, page_align_up(sizeof(KUSER_SHARED_DATA)), memory_permission::read); + + const emulator_object kusd_object{emu, KUSD_ADDRESS}; + kusd_object.access([](KUSER_SHARED_DATA& kusd) + { + const auto& real_kusd = *reinterpret_cast(KUSD_ADDRESS); + + memcpy(&kusd, &real_kusd, sizeof(kusd)); + + kusd.ImageNumberLow = IMAGE_FILE_MACHINE_I386; + kusd.ImageNumberHigh = IMAGE_FILE_MACHINE_AMD64; + + memset(&kusd.ProcessorFeatures, 0, sizeof(kusd.ProcessorFeatures)); + + // ... + }); + + return kusd_object; + } + + uint64_t copy_string(x64_emulator& emu, emulator_allocator& allocator, const void* base_ptr, const uint64_t offset, + const size_t length) + { + if (!length) + { + return 0; + } + + const auto length_to_allocate = length + 2; + const auto str_obj = allocator.reserve(length_to_allocate); + emu.write_memory(str_obj, static_cast(base_ptr) + offset, length); + + return str_obj; + } + + ULONG copy_string_as_relative(x64_emulator& emu, emulator_allocator& allocator, const uint64_t result_base, + const void* base_ptr, const uint64_t offset, + const size_t length) + { + const auto address = copy_string(emu, allocator, base_ptr, offset, length); + if (!address) + { + return 0; + } + + assert(address > result_base); + return static_cast(address - result_base); + } + + emulator_object clone_api_set_map(x64_emulator& emu, emulator_allocator& allocator, + const API_SET_NAMESPACE& orig_api_set_map) + { + const auto api_set_map_obj = allocator.reserve(); + const auto ns_entries_obj = allocator.reserve(orig_api_set_map.Count); + const auto hash_entries_obj = allocator.reserve(orig_api_set_map.Count); + + api_set_map_obj.access([&](API_SET_NAMESPACE& api_set) + { + api_set = orig_api_set_map; + api_set.EntryOffset = static_cast(ns_entries_obj.value() - api_set_map_obj.value()); + api_set.HashOffset = static_cast(hash_entries_obj.value() - api_set_map_obj.value()); + }); + + const auto orig_ns_entries = offset_pointer(&orig_api_set_map, + orig_api_set_map.EntryOffset); + const auto orig_hash_entries = offset_pointer(&orig_api_set_map, + orig_api_set_map.HashOffset); + + for (ULONG i = 0; i < orig_api_set_map.Count; ++i) + { + auto ns_entry = orig_ns_entries[i]; + const auto hash_entry = orig_hash_entries[i]; + + ns_entry.NameOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), &orig_api_set_map, + ns_entry.NameOffset, ns_entry.NameLength); + + if (!ns_entry.ValueCount) + { + continue; + } + + const auto values_obj = allocator.reserve(ns_entry.ValueCount); + const auto orig_values = offset_pointer(&orig_api_set_map, + ns_entry.ValueOffset); + + ns_entry.ValueOffset = static_cast(values_obj.value() - api_set_map_obj.value()); + + for (ULONG j = 0; j < ns_entry.ValueCount; ++j) + { + auto value = orig_values[j]; + + value.ValueOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), &orig_api_set_map, + value.ValueOffset, value.ValueLength); + + if (value.NameLength) + { + value.NameOffset = copy_string_as_relative(emu, allocator, api_set_map_obj.value(), + &orig_api_set_map, + value.NameOffset, value.NameLength); + } + + values_obj.write(value, j); + } + + ns_entries_obj.write(ns_entry, i); + hash_entries_obj.write(hash_entry, i); + } + + //watch_object(emu, api_set_map_obj); + + return api_set_map_obj; + } + + emulator_object build_api_set_map(x64_emulator& emu, emulator_allocator& allocator) + { + const auto& orig_api_set_map = *NtCurrentTeb()->ProcessEnvironmentBlock->ApiSetMap; + return clone_api_set_map(emu, allocator, orig_api_set_map); + } + + emulator_allocator create_allocator(emulator& emu, const size_t size) + { + const auto base = emu.find_free_allocation_base(size); + emu.allocate_memory(base, size, memory_permission::read_write); + + return emulator_allocator{emu, base, size}; + } + + void setup_gdt(x64_emulator& emu) + { + constexpr uint64_t gdtr[4] = {0, GDT_ADDR, GDT_LIMIT, 0}; + emu.write_register(x64_register::gdtr, &gdtr, sizeof(gdtr)); + emu.allocate_memory(GDT_ADDR, GDT_LIMIT, memory_permission::read); + + emu.write_memory(GDT_ADDR + 6 * (sizeof(uint64_t)), 0xEFFE000000FFFF); + emu.reg(x64_register::cs, 0x33); + + emu.write_memory(GDT_ADDR + 5 * (sizeof(uint64_t)), 0xEFF6000000FFFF); + emu.reg(x64_register::ss, 0x2B); + } + + void setup_context(process_context& context, x64_emulator& emu, const std::filesystem::path& file) + { + setup_stack(emu, STACK_ADDRESS, STACK_SIZE); + setup_gdt(emu); + + context.kusd = setup_kusd(emu); + context.gs_segment = setup_gs_segment(emu, GS_SEGMENT_ADDR, GS_SEGMENT_SIZE); + + auto allocator = create_allocator(emu, 1 << 20); + + auto& gs = context.gs_segment; + + context.teb = gs.reserve(); + context.peb = gs.reserve(); + context.process_params = gs.reserve(); + + context.teb.access([&](TEB& teb) + { + teb.ClientId.UniqueProcess = reinterpret_cast(1); + teb.ClientId.UniqueThread = reinterpret_cast(2); + teb.NtTib.StackLimit = reinterpret_cast(STACK_ADDRESS); + teb.NtTib.StackBase = reinterpret_cast((STACK_ADDRESS + STACK_SIZE)); + teb.NtTib.Self = &context.teb.ptr()->NtTib; + teb.ProcessEnvironmentBlock = context.peb.ptr(); + }); + + context.process_params.access([&](RTL_USER_PROCESS_PARAMETERS& proc_params) + { + proc_params.Length = sizeof(proc_params); + proc_params.Flags = 0x6001 | 0x80000000; // Prevent CsrClientConnectToServer + + proc_params.ConsoleHandle = CONSOLE_HANDLE.h; + proc_params.StandardOutput = STDOUT_HANDLE.h; + proc_params.StandardInput = STDIN_HANDLE.h; + proc_params.StandardError = proc_params.StandardOutput; + + gs.make_unicode_string(proc_params.CurrentDirectory.DosPath, file.parent_path().wstring()); + gs.make_unicode_string(proc_params.ImagePathName, file.wstring()); + gs.make_unicode_string(proc_params.CommandLine, file.wstring()); + }); + + context.peb.access([&](PEB& peb) + { + peb.ImageBaseAddress = nullptr; + peb.ProcessParameters = context.process_params.ptr(); + peb.ApiSetMap = build_api_set_map(emu, allocator).ptr(); + + peb.ProcessHeap = nullptr; + peb.ProcessHeaps = nullptr; + peb.HeapSegmentReserve = 0x0000000000100000; // TODO: Read from executable + peb.HeapSegmentCommit = 0x0000000000002000; + peb.HeapDeCommitTotalFreeThreshold = 0x0000000000010000; + peb.HeapDeCommitFreeBlockThreshold = 0x0000000000001000; + peb.NumberOfHeaps = 0x00000000; + peb.MaximumNumberOfHeaps = 0x00000010; + }); + } + + uint64_t find_exported_function(const std::vector& exports, const std::string_view name) + { + for (auto& symbol : exports) + { + if (symbol.name == name) + { + return symbol.address; + } + } + + return 0; + } + + using exception_record_map = std::unordered_map>; + + emulator_object save_exception_record(emulator_allocator& allocator, + const EXCEPTION_RECORD& record, + exception_record_map& record_mapping) + { + const auto record_obj = allocator.reserve(); + record_obj.write(record); + + if (record.ExceptionRecord) + { + record_mapping[&record] = record_obj; + + emulator_object nested_record_obj{}; + const auto nested_record = record_mapping.find(record.ExceptionRecord); + + if (nested_record != record_mapping.end()) + { + nested_record_obj = nested_record->second; + } + else + { + nested_record_obj = save_exception_record(allocator, *record.ExceptionRecord, + record_mapping); + } + + record_obj.access([&](EXCEPTION_RECORD& r) + { + r.ExceptionRecord = nested_record_obj.ptr(); + }); + } + + return record_obj; + } + + emulator_object save_exception_record(emulator_allocator& allocator, + const EXCEPTION_RECORD& record) + { + exception_record_map record_mapping{}; + return save_exception_record(allocator, record, record_mapping); + } + + uint32_t map_violation_operation_to_parameter(const memory_operation operation) + { + switch (operation) + { + default: + case memory_operation::read: + return 0; + case memory_operation::write: + return 1; + case memory_operation::exec: + return 1; + } + } + + size_t calculate_exception_record_size(const EXCEPTION_RECORD& record) + { + std::unordered_set records{}; + size_t total_size = 0; + + const EXCEPTION_RECORD* current_record = &record; + while (current_record) + { + if (!records.insert(current_record).second) + { + break; + } + + total_size += sizeof(*current_record); + current_record = record.ExceptionRecord; + } + + return total_size; + } + + struct machine_frame + { + uint64_t rip; + uint64_t cs; + uint64_t eflags; + uint64_t rsp; + uint64_t ss; + }; + + void dispatch_exception_pointers(x64_emulator& emu, const uint64_t dispatcher, const EXCEPTION_POINTERS pointers) + { + constexpr auto mach_frame_size = 0x40; + constexpr auto context_record_size = 0x4F0; + const auto exception_record_size = calculate_exception_record_size(*pointers.ExceptionRecord); + const auto combined_size = align_up(exception_record_size + context_record_size, 0x10); + + assert(combined_size == 0x590); + + const auto allocation_size = combined_size + mach_frame_size; + + const auto initial_sp = emu.reg(x64_register::rsp); + const auto new_sp = align_down(initial_sp - allocation_size, 0x100); + + const auto total_size = initial_sp - new_sp; + assert(total_size >= allocation_size); + + std::vector zero_memory{}; + zero_memory.resize(total_size, 0); + + emu.write_memory(new_sp, zero_memory.data(), zero_memory.size()); + + emu.reg(x64_register::rsp, new_sp); + emu.reg(x64_register::rip, dispatcher); + + const emulator_object context_record_obj{emu, new_sp}; + context_record_obj.write(*pointers.ContextRecord); + + emulator_allocator allocator{emu, new_sp + context_record_size, exception_record_size}; + const auto exception_record_obj = save_exception_record(allocator, *pointers.ExceptionRecord); + + if (exception_record_obj.value() != allocator.get_base()) + { + throw std::runtime_error("Bad exception record position on stack"); + } + + const emulator_object machine_frame_obj{emu, new_sp + combined_size}; + machine_frame_obj.access([&](machine_frame& frame) + { + frame.rip = pointers.ContextRecord->Rip; + frame.rsp = pointers.ContextRecord->Rsp; + frame.ss = pointers.ContextRecord->SegSs; + frame.cs = pointers.ContextRecord->SegCs; + frame.eflags = pointers.ContextRecord->EFlags; + }); + + printf("ContextRecord: %llX\n", context_record_obj.value()); + printf("ExceptionRecord: %llX\n", exception_record_obj.value()); + } + + void dispatch_access_violation(x64_emulator& emu, const uint64_t dispatcher, const uint64_t address, + const memory_operation operation) + { + CONTEXT ctx{}; + ctx.ContextFlags = CONTEXT_ALL; + context_frame::save(emu, ctx); + + EXCEPTION_RECORD record{}; + memset(&record, 0, sizeof(record)); + record.ExceptionCode = static_cast(STATUS_ACCESS_VIOLATION); + record.ExceptionFlags = 0; + record.ExceptionRecord = nullptr; + record.ExceptionAddress = reinterpret_cast(emu.read_instruction_pointer()); + record.NumberParameters = 2; + record.ExceptionInformation[0] = map_violation_operation_to_parameter(operation); + record.ExceptionInformation[1] = address; + + EXCEPTION_POINTERS pointers{}; + pointers.ContextRecord = &ctx; + pointers.ExceptionRecord = &record; + + dispatch_exception_pointers(emu, dispatcher, pointers); + } +} + +windows_emulator::windows_emulator(const std::filesystem::path& application, std::unique_ptr emu) + : windows_emulator(std::move(emu)) +{ + this->setup_process(application); +} + +windows_emulator::windows_emulator(std::unique_ptr emu) + : emu_(std::move(emu)) + , process_(*emu_) +{ + this->setup_hooks(); +} + +void windows_emulator::setup_process(const std::filesystem::path& application) +{ + auto& emu = this->emu(); + + auto& context = this->process(); + context.module_manager = module_manager(emu); // TODO: Cleanup module manager + + setup_context(context, emu, application); + + context.executable = context.module_manager.map_module(application); + + context.peb.access([&](PEB& peb) + { + peb.ImageBaseAddress = reinterpret_cast(context.executable->image_base); + }); + + context.ntdll = context.module_manager.map_module(R"(C:\Windows\System32\ntdll.dll)"); + context.win32u = context.module_manager.map_module(R"(C:\Windows\System32\win32u.dll)"); + + this->dispatcher_.setup(context.ntdll->exports, context.win32u->exports); + + const auto ldr_initialize_thunk = find_exported_function(context.ntdll->exports, "LdrInitializeThunk"); + const auto rtl_user_thread_start = find_exported_function(context.ntdll->exports, "RtlUserThreadStart"); + context.ki_user_exception_dispatcher = find_exported_function(context.ntdll->exports, "KiUserExceptionDispatcher"); + + CONTEXT ctx{}; + ctx.ContextFlags = CONTEXT_ALL; + + unalign_stack(emu); + context_frame::save(emu, ctx); + + ctx.Rip = rtl_user_thread_start; + ctx.Rcx = context.executable->entry_point; + + const auto ctx_obj = allocate_object_on_stack(emu); + ctx_obj.write(ctx); + + unalign_stack(emu); + + emu.reg(x64_register::rcx, ctx_obj.value()); + emu.reg(x64_register::rdx, context.ntdll->image_base); + emu.reg(x64_register::rip, ldr_initialize_thunk); +} + +void windows_emulator::setup_hooks() +{ + this->emu().hook_instruction(x64_hookable_instructions::syscall, [&] + { + this->dispatcher_.dispatch(this->emu(), this->process()); + return instruction_hook_continuation::skip_instruction; + }); + + this->emu().hook_instruction(x64_hookable_instructions::invalid, [&] + { + const auto ip = this->emu().read_instruction_pointer(); + printf("Invalid instruction at: %llX\n", ip); + return instruction_hook_continuation::skip_instruction; + }); + + this->emu().hook_interrupt([&](const int interrupt) + { + printf("Interrupt: %i %llX\n", interrupt, this->emu().read_instruction_pointer()); + }); + + this->emu().hook_memory_violation([&](const uint64_t address, const size_t size, const memory_operation operation, + const memory_violation_type type) + { + const auto permission = get_permission_string(operation); + const auto ip = this->emu().read_instruction_pointer(); + + + if (type == memory_violation_type::protection) + { + printf("Protection violation: %llX (%zX) - %s at %llX\n", address, size, permission.c_str(), ip); + } + else if (type == memory_violation_type::unmapped) + { + printf("Mapping violation: %llX (%zX) - %s at %llX\n", address, size, permission.c_str(), ip); + } + + dispatch_access_violation(this->emu(), this->process().ki_user_exception_dispatcher, address, operation); + return memory_violation_continuation::resume; + }); + + this->emu().hook_memory_execution(0, std::numeric_limits::max(), [&](const uint64_t address, const size_t) + { + ++this->process().executed_instructions; + + const auto* binary = this->process().module_manager.find_by_address(address); + + if (binary) + { + const auto export_entry = binary->address_names.find(address); + if (export_entry != binary->address_names.end()) + { + printf("Executing function: %s - %s (%llX)\n", binary->name.c_str(), export_entry->second.c_str(), + address); + } + else if (address == binary->entry_point) + { + printf("Executing entry point: %s (%llX)\n", binary->name.c_str(), address); + } + } + + if (!this->verbose_) + { + return; + } + + auto& emu = this->emu(); + + printf( + "Inst: %16llX - RAX: %16llX - RBX: %16llX - RCX: %16llX - RDX: %16llX - R8: %16llX - R9: %16llX - RDI: %16llX - RSI: %16llX - %s\n", + address, + emu.reg(x64_register::rax), emu.reg(x64_register::rbx), emu.reg(x64_register::rcx), + emu.reg(x64_register::rdx), emu.reg(x64_register::r8), emu.reg(x64_register::r9), + emu.reg(x64_register::rdi), emu.reg(x64_register::rsi), binary ? binary->name.c_str() : ""); + }); +} + +void windows_emulator::serialize(utils::buffer_serializer& buffer) const +{ + this->emu().serialize(buffer); + this->process_.serialize(buffer); + this->dispatcher_.serialize(buffer); +} + +void windows_emulator::deserialize(utils::buffer_deserializer& buffer) +{ + this->emu().deserialize(buffer); + this->process_.deserialize(buffer); + this->dispatcher_.deserialize(buffer); +} + +void windows_emulator::save_snapshot() +{ + this->emu().save_snapshot(); + + utils::buffer_serializer serializer{}; + this->process_.serialize(serializer); + + this->process_snapshot_ = serializer.move_buffer(); + + // TODO: Make process copyable + //this->process_snapshot_ = this->process(); +} + +void windows_emulator::restore_snapshot() +{ + if (this->process_snapshot_.empty()) + { + assert(false); + return; + } + + this->emu().restore_snapshot(); + + utils::buffer_deserializer deserializer{this->process_snapshot_}; + this->process_.deserialize(deserializer); + //this->process_ = *this->process_snapshot_; +} diff --git a/src/windows_emulator/windows_emulator.hpp b/src/windows_emulator/windows_emulator.hpp new file mode 100644 index 0000000..4762445 --- /dev/null +++ b/src/windows_emulator/windows_emulator.hpp @@ -0,0 +1,63 @@ +#pragma once +#include +#include + +#include "syscalls.hpp" +#include "process_context.hpp" + +class windows_emulator +{ +public: + windows_emulator(std::unique_ptr emu = unicorn::create_x64_emulator()); + windows_emulator(const std::filesystem::path& application, + std::unique_ptr emu = unicorn::create_x64_emulator()); + + windows_emulator(windows_emulator&&) = delete; + windows_emulator(const windows_emulator&) = delete; + windows_emulator& operator=(windows_emulator&&) = delete; + windows_emulator& operator=(const windows_emulator&) = delete; + + x64_emulator& emu() + { + return *this->emu_; + } + + const x64_emulator& emu() const + { + return *this->emu_; + } + + process_context& process() + { + return this->process_; + } + + const process_context& process() const + { + return this->process_; + } + + void serialize(utils::buffer_serializer& buffer) const; + void deserialize(utils::buffer_deserializer& buffer); + + void save_snapshot(); + void restore_snapshot(); + + void set_verbose(const bool verbose) + { + this->verbose_ = verbose; + } + +private: + bool verbose_{false}; + std::unique_ptr emu_{}; + + process_context process_; + syscall_dispatcher dispatcher_; + + std::vector process_snapshot_{}; + //std::optional process_snapshot_{}; + + void setup_hooks(); + void setup_process(const std::filesystem::path& application); +};