#include "std_include.hpp" #include "windows_emulator.hpp" #include "cpu_context.hpp" #include #include #include #include "exception_dispatch.hpp" #include "apiset/apiset.hpp" #include "network/static_socket_factory.hpp" #include "memory_permission_ext.hpp" constexpr auto MAX_INSTRUCTIONS_PER_TIME_SLICE = 0x20000; namespace { void adjust_working_directory(application_settings& app_settings) { if (!app_settings.working_directory.empty()) { // Do nothing } #ifdef OS_WINDOWS else if (app_settings.application.is_relative()) { app_settings.working_directory = std::filesystem::current_path(); } #endif else { app_settings.working_directory = app_settings.application.parent(); } } void adjust_application(application_settings& app_settings) { if (app_settings.application.is_relative()) { app_settings.application = app_settings.working_directory / app_settings.application; } } void fixup_application_settings(application_settings& app_settings) { adjust_working_directory(app_settings); adjust_application(app_settings); } void perform_context_switch_work(windows_emulator& win_emu) { auto& threads = win_emu.process.threads; auto*& active = win_emu.process.active_thread; for (auto it = threads.begin(); it != threads.end();) { if (!it->second.is_terminated() || it->second.ref_count > 0) { ++it; continue; } if (active == &it->second) { active = nullptr; } const auto [new_it, deleted] = threads.erase(it); if (!deleted) { ++it; } else { it = new_it; } } auto& devices = win_emu.process.devices; // Crappy mechanism to prevent mutation while iterating. const auto was_blocked = devices.block_mutation(true); const auto _ = utils::finally([&] { devices.block_mutation(was_blocked); }); for (auto& dev : devices | std::views::values) { dev.work(win_emu); } } emulator_thread* get_thread_by_id(process_context& process, const uint32_t id) { for (auto& t : process.threads | std::views::values) { if (t.id == id) { return &t; } } return nullptr; } void dispatch_next_apc(windows_emulator& win_emu, emulator_thread& thread) { assert(&win_emu.current_thread() == &thread); auto& emu = win_emu.emu(); auto& apcs = thread.pending_apcs; if (apcs.empty()) { return; } thread.setup_if_necessary(win_emu.emu(), win_emu.process); win_emu.callbacks.on_generic_activity("APC Dispatch"); const auto next_apx = apcs.front(); apcs.erase(apcs.begin()); struct { CONTEXT64 context{}; CONTEXT_EX context_ex{}; KCONTINUE_ARGUMENT continue_argument{}; } stack_layout; static_assert(offsetof(decltype(stack_layout), continue_argument) == 0x4F0); stack_layout.context.P1Home = next_apx.apc_argument1; stack_layout.context.P2Home = next_apx.apc_argument2; stack_layout.context.P3Home = next_apx.apc_argument3; stack_layout.context.P4Home = next_apx.apc_routine; stack_layout.continue_argument.ContinueFlags |= KCONTINUE_FLAG_TEST_ALERT; auto& ctx = stack_layout.context; ctx.ContextFlags = CONTEXT64_ALL; cpu_context::save(emu, ctx); const auto initial_sp = emu.reg(x86_register::rsp); const auto new_sp = align_down(initial_sp - sizeof(stack_layout), 0x100); emu.write_memory(new_sp, stack_layout); emu.reg(x86_register::rsp, new_sp); emu.reg(x86_register::rip, win_emu.process.ki_user_apc_dispatcher); } bool switch_to_thread(windows_emulator& win_emu, emulator_thread& thread, const bool force = false) { if (thread.is_terminated()) { return false; } auto& emu = win_emu.emu(); auto& context = win_emu.process; const auto is_ready = thread.is_thread_ready(context, win_emu.clock()); const auto can_dispatch_apcs = thread.apc_alertable && !thread.pending_apcs.empty(); if (!is_ready && !force && !can_dispatch_apcs) { return false; } auto* active_thread = context.active_thread; if (active_thread != &thread) { if (active_thread) { win_emu.callbacks.on_thread_switch(*active_thread, thread); active_thread->save(emu); } context.active_thread = &thread; thread.restore(emu); } thread.setup_if_necessary(emu, context); if (can_dispatch_apcs) { thread.mark_as_ready(STATUS_USER_APC); dispatch_next_apc(win_emu, thread); } thread.apc_alertable = false; return true; } bool switch_to_thread(windows_emulator& win_emu, const handle thread_handle) { auto* thread = win_emu.process.threads.get(thread_handle); if (!thread) { throw std::runtime_error("Bad thread handle"); } return switch_to_thread(win_emu, *thread); } bool switch_to_next_thread(windows_emulator& win_emu) { perform_context_switch_work(win_emu); auto& context = win_emu.process; bool next_thread = false; for (auto& t : context.threads | std::views::values) { if (next_thread) { if (switch_to_thread(win_emu, t)) { return true; } continue; } if (&t == context.active_thread) { next_thread = true; } } for (auto& t : context.threads | std::views::values) { if (switch_to_thread(win_emu, t)) { return true; } } return false; } struct instruction_tick_clock : utils::tick_clock { const uint64_t* instructions_{}; instruction_tick_clock(const uint64_t& instructions, const system_time_point system_start = {}, const steady_time_point steady_start = {}) : tick_clock(1000, system_start, steady_start), instructions_(&instructions) { } uint64_t ticks() override { return *this->instructions_; } }; std::unique_ptr get_clock(emulator_interfaces& interfaces, const uint64_t& instructions, const bool use_relative_time) { if (interfaces.clock) { return std::move(interfaces.clock); } if (use_relative_time) { return std::make_unique(instructions); } return std::make_unique(); } std::unique_ptr get_socket_factory(emulator_interfaces& interfaces) { if (interfaces.socket_factory) { return std::move(interfaces.socket_factory); } #ifdef OS_EMSCRIPTEN return network::create_static_socket_factory(); #else return std::make_unique(); #endif } } windows_emulator::windows_emulator(std::unique_ptr emu, application_settings app_settings, const emulator_settings& settings, emulator_callbacks callbacks, emulator_interfaces interfaces) : windows_emulator(std::move(emu), settings, std::move(callbacks), std::move(interfaces)) { fixup_application_settings(app_settings); this->application_settings_ = std::move(app_settings); } windows_emulator::windows_emulator(std::unique_ptr emu, const emulator_settings& settings, emulator_callbacks callbacks, emulator_interfaces interfaces) : emu_(std::move(emu)), clock_(get_clock(interfaces, this->executed_instructions_, settings.use_relative_time)), socket_factory_(get_socket_factory(interfaces)), emulation_root{settings.emulation_root.empty() ? settings.emulation_root : absolute(settings.emulation_root)}, callbacks(std::move(callbacks)), file_sys(emulation_root.empty() ? emulation_root : emulation_root / "filesys"), memory(*this->emu_), registry(emulation_root.empty() ? settings.registry_directory : emulation_root / "registry"), mod_manager(memory, file_sys, this->callbacks), process(*this->emu_, memory, *this->clock_, this->callbacks), use_relative_time_(settings.use_relative_time) { #ifndef OS_WINDOWS if (this->emulation_root.empty()) { throw std::runtime_error("Emulation root directory can not be empty!"); } #endif for (const auto& mapping : settings.path_mappings) { this->file_sys.map(mapping.first, mapping.second); } for (const auto& mapping : settings.port_mappings) { this->map_port(mapping.first, mapping.second); } this->setup_hooks(); } windows_emulator::~windows_emulator() = default; void windows_emulator::setup_process_if_necessary() { if (!this->application_settings_) { return; } auto app_settings = std::move(*this->application_settings_); this->application_settings_ = {}; this->setup_process(app_settings); } void windows_emulator::setup_process(const application_settings& app_settings) { const auto& emu = this->emu(); auto& context = this->process; this->version.load_from_registry(this->registry, this->log); this->mod_manager.map_main_modules(app_settings.application, this->version, context, this->log); const auto* executable = this->mod_manager.executable; const auto* ntdll = this->mod_manager.ntdll; const auto* win32u = this->mod_manager.win32u; const auto apiset_data = apiset::obtain(this->emulation_root); this->process.setup(this->emu(), this->memory, this->registry, this->file_sys, this->version, app_settings, *executable, *ntdll, apiset_data, this->mod_manager.wow64_modules_.ntdll32); const auto ntdll_data = emu.read_memory(ntdll->image_base, static_cast(ntdll->size_of_image)); const auto win32u_data = emu.read_memory(win32u->image_base, static_cast(win32u->size_of_image)); this->dispatcher.setup(ntdll->exports, ntdll_data, win32u->exports, win32u_data); const auto main_thread_id = context.create_thread(this->memory, this->mod_manager.executable->entry_point, 0, this->mod_manager.executable->size_of_stack_commit, 0, true); switch_to_thread(*this, main_thread_id); } void windows_emulator::yield_thread(const bool alertable) { this->switch_thread_ = true; this->current_thread().apc_alertable = alertable; this->emu().stop(); } bool windows_emulator::perform_thread_switch() { const auto needed_switch = std::exchange(this->switch_thread_, false); this->switch_thread_ = false; while (!switch_to_next_thread(*this)) { if (this->use_relative_time_) { this->executed_instructions_ += MAX_INSTRUCTIONS_PER_TIME_SLICE; } else { std::this_thread::sleep_for(1ms); } if (this->should_stop) { this->switch_thread_ = needed_switch; return false; } } return true; } bool windows_emulator::activate_thread(const uint32_t id) { auto* thread = get_thread_by_id(this->process, id); if (!thread) { return false; } return switch_to_thread(*this, *thread, true); } void windows_emulator::on_instruction_execution(const uint64_t address) { auto& thread = this->current_thread(); ++this->executed_instructions_; const auto thread_insts = ++thread.executed_instructions; if (thread_insts % MAX_INSTRUCTIONS_PER_TIME_SLICE == 0) { this->yield_thread(); } thread.previous_ip = thread.current_ip; thread.current_ip = this->emu().read_instruction_pointer(); this->callbacks.on_instruction(address); } void windows_emulator::setup_hooks() { this->emu().hook_instruction(x86_hookable_instructions::syscall, [&] { this->dispatcher.dispatch(*this); return instruction_hook_continuation::skip_instruction; }); this->emu().hook_instruction(x86_hookable_instructions::rdtscp, [&] { this->callbacks.on_rdtscp(); const auto ticks = this->clock_->timestamp_counter(); this->emu().reg(x86_register::rax, static_cast(ticks)); this->emu().reg(x86_register::rdx, static_cast(ticks >> 32)); // Return the IA32_TSC_AUX value in RCX (low 32 bits) auto tsc_aux = 0; // Need to replace this with proper CPUID later this->emu().reg(x86_register::rcx, tsc_aux); return instruction_hook_continuation::skip_instruction; }); this->emu().hook_instruction(x86_hookable_instructions::rdtsc, [&] { this->callbacks.on_rdtsc(); const auto ticks = this->clock_->timestamp_counter(); this->emu().reg(x86_register::rax, static_cast(ticks)); this->emu().reg(x86_register::rdx, static_cast(ticks >> 32)); return instruction_hook_continuation::skip_instruction; }); // TODO: Unicorn needs this - This should be handled in the backend this->emu().hook_instruction(x86_hookable_instructions::invalid, [&] { // TODO: Unify icicle & unicorn handling dispatch_illegal_instruction_violation(*this); return instruction_hook_continuation::skip_instruction; // }); this->emu().hook_interrupt([&](const int interrupt) { this->callbacks.on_exception(); const auto eflags = this->emu().reg(x86_register::eflags); switch (interrupt) { case 0: dispatch_integer_division_by_zero(*this); return; case 1: if ((eflags & 0x100) != 0) { this->emu().reg(x86_register::eflags, eflags & ~0x100); } this->callbacks.on_suspicious_activity("Singlestep"); dispatch_single_step(*this); return; case 3: this->callbacks.on_suspicious_activity("Breakpoint"); dispatch_breakpoint(*this); return; case 6: this->callbacks.on_suspicious_activity("Illegal instruction"); dispatch_illegal_instruction_violation(*this); return; case 45: this->callbacks.on_suspicious_activity("DbgPrint"); dispatch_breakpoint(*this); return; default: if (this->callbacks.on_generic_activity) { this->callbacks.on_generic_activity("Interrupt " + std::to_string(interrupt)); } break; } }); this->emu().hook_memory_violation( [&](const uint64_t address, const size_t size, const memory_operation operation, const memory_violation_type type) { if (this->emu().reg(x86_register::cs) == 0x33) { // loading gs selector only works in 64-bit mode const auto required_gs_base = this->current_thread().gs_segment->get_base(); const auto actual_gs_base = this->emu().get_segment_base(x86_register::gs); if (actual_gs_base != required_gs_base) { this->emu().set_segment_base(x86_register::gs, required_gs_base); return memory_violation_continuation::restart; } } auto region = this->memory.get_region_info(address); if (region.permissions.is_guarded()) { // Unset the GUARD_PAGE flag and dispatch a STATUS_GUARD_PAGE_VIOLATION this->memory.protect_memory(region.allocation_base, region.length, region.permissions & ~memory_permission_ext::guard); dispatch_guard_page_violation(*this, address, operation); } else { this->callbacks.on_memory_violate(address, size, operation, type); dispatch_access_violation(*this, address, operation); } return memory_violation_continuation::resume; }); this->emu().hook_memory_execution([&](const uint64_t address) { this->on_instruction_execution(address); // }); } void windows_emulator::start(size_t count) { this->should_stop = false; this->setup_process_if_necessary(); this->process.setup_callback_hook(*this, this->memory); const auto use_count = count > 0; const auto start_instructions = this->executed_instructions_; const auto target_instructions = start_instructions + count; while (!this->should_stop) { if (this->switch_thread_ || !this->current_thread().is_thread_ready(this->process, this->clock())) { if (!this->perform_thread_switch()) { break; } } this->emu().start(count); if (!this->switch_thread_ && !this->emu().has_violation()) { break; } if (use_count) { const auto current_instructions = this->executed_instructions_; if (current_instructions >= target_instructions) { break; } count = static_cast(target_instructions - current_instructions); } } } void windows_emulator::stop() { this->should_stop = true; this->emu().stop(); } void windows_emulator::register_factories(utils::buffer_deserializer& buffer) { buffer.register_factory([this] { return memory_manager_wrapper{this->memory}; // }); buffer.register_factory([this] { return module_manager_wrapper{this->mod_manager}; // }); buffer.register_factory([this] { return x64_emulator_wrapper{this->emu()}; // }); buffer.register_factory([this] { return windows_emulator_wrapper{*this}; // }); buffer.register_factory([this] { return clock_wrapper{this->clock()}; // }); buffer.register_factory([this] { return socket_factory_wrapper{this->socket_factory()}; // }); } void windows_emulator::serialize(utils::buffer_serializer& buffer) const { buffer.write_optional(this->application_settings_); buffer.write(this->executed_instructions_); buffer.write(this->switch_thread_); buffer.write(this->use_relative_time_); this->version.serialize(buffer); this->emu().serialize_state(buffer, false); this->memory.serialize_memory_state(buffer, false); this->mod_manager.serialize(buffer); this->process.serialize(buffer); this->dispatcher.serialize(buffer); } void windows_emulator::deserialize(utils::buffer_deserializer& buffer) { this->register_factories(buffer); buffer.read_optional(this->application_settings_); buffer.read(this->executed_instructions_); buffer.read(this->switch_thread_); const auto old_relative_time = this->use_relative_time_; buffer.read(this->use_relative_time_); if (old_relative_time != this->use_relative_time_) { throw std::runtime_error("Can not deserialize emulator with different time dimensions"); } this->version.deserialize(buffer); this->memory.unmap_all_memory(); this->emu().deserialize_state(buffer, false); this->memory.deserialize_memory_state(buffer, false); this->mod_manager.deserialize(buffer); this->process.deserialize(buffer); this->dispatcher.deserialize(buffer); } void windows_emulator::save_snapshot() { utils::buffer_serializer buffer{}; buffer.write_optional(this->application_settings_); buffer.write(this->executed_instructions_); buffer.write(this->switch_thread_); this->version.serialize(buffer); this->emu().serialize_state(buffer, true); this->memory.serialize_memory_state(buffer, true); this->mod_manager.serialize(buffer); this->process.serialize(buffer); this->process_snapshot_ = buffer.move_buffer(); // TODO: Make process copyable // this->process_snapshot_ = this->process; } void windows_emulator::restore_snapshot() { if (this->process_snapshot_.empty()) { assert(false); return; } utils::buffer_deserializer buffer{this->process_snapshot_}; this->register_factories(buffer); buffer.read_optional(this->application_settings_); buffer.read(this->executed_instructions_); buffer.read(this->switch_thread_); this->version.deserialize(buffer); this->emu().deserialize_state(buffer, true); this->memory.deserialize_memory_state(buffer, true); this->mod_manager.deserialize(buffer); this->process.deserialize(buffer); // this->process = *this->process_snapshot_; }