#define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #include #define GAME_DLL_MAP_ENTRY_RVA 0x007c0b40u #define GAME_DLL_LOGIN_RETURN_RVA 0x0079f157u #define GAME_DLL_CODE_CAVE_RVA 0x01195ce0u #define GAME_DLL_LSTRCMPA_IAT_RVA 0x01196280u #define GAME_DLL_VIRTUAL_PROTECT_IAT_RVA 0x01196108u #define GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA 0x01196140u #define GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA 0x01196344u #define GAME_DLL_MINIMUM_IMAGE_SIZE 0x01196348u #define REMOTE_ALLOCATION_SIZE 4096u #define REMOTE_RECLAIM_OFFSET 0x0600u #define REMOTE_MAP_JUMP_OFFSET 0x0680u #define REMOTE_STATE_OFFSET 0x0700u #define REMOTE_OLD_CAVE_PROTECT_OFFSET 0x0704u #define REMOTE_OLD_ENTRY_PROTECT_OFFSET 0x0708u #define REMOTE_TEMP_PROTECT_OFFSET 0x070cu #define WORLD_FIX_WAIT_MILLISECONDS 60000u static const BYTE MAP_ENTRY_ORIGINAL[] = {0x55, 0x8b, 0xec, 0x6a, 0xff}; static const BYTE LOGIN_COMPARE_ORIGINAL[] = { 0x85, 0xc0, 0x0f, 0x85, 0x6b, 0x01, 0x00, 0x00, }; static const BYTE MAP_ENTRY_JUMP[] = {0xe9, 0x9b, 0x51, 0x9d, 0x00}; /* * Exact 97-byte entity-surface reclaim routine verified for V21.75.83. * Its relative calls and return jump are valid at GAME_DLL_CODE_CAVE_RVA. */ static const BYTE PROVEN_RECLAIM_HOOK[] = { 0x9c, 0x60, 0xe8, 0x49, 0x01, 0x36, 0xff, 0x85, 0xc0, 0x74, 0x4a, 0x89, 0xc3, 0x8d, 0x70, 0x08, 0x8d, 0xbe, 0x50, 0x5d, 0x01, 0x00, 0x39, 0xfe, 0x74, 0x3b, 0x80, 0xbe, 0x58, 0x03, 0x00, 0x00, 0x00, 0x74, 0x2a, 0x8b, 0x06, 0x8b, 0x4c, 0x24, 0x28, 0x85, 0xc9, 0x74, 0x08, 0x3b, 0x81, 0xd4, 0x1f, 0x00, 0x00, 0x74, 0x18, 0x8b, 0x4c, 0x24, 0x2c, 0x85, 0xc9, 0x74, 0x08, 0x3b, 0x81, 0xd4, 0x1f, 0x00, 0x00, 0x74, 0x08, 0x50, 0x89, 0xd9, 0xe8, 0xa3, 0x20, 0x36, 0xff, 0x81, 0xc6, 0x50, 0x04, 0x00, 0x00, 0xeb, 0xc1, 0x61, 0x9d, 0x55, 0x89, 0xe5, 0x6a, 0xff, 0xe9, 0x04, 0xae, 0x62, 0xff, }; typedef struct CodeBuffer { BYTE *data; SIZE_T capacity; SIZE_T length; BOOL valid; } CodeBuffer; typedef struct WorldFixInstallation { DWORD remote_state; LPVOID remote_allocation; } WorldFixInstallation; typedef struct StatusLog { HANDLE handle; } StatusLog; static void initialize_status_log(StatusLog *status) { status->handle = INVALID_HANDLE_VALUE; } static BOOL status_path_is_allowed(const wchar_t *path) { static const wchar_t prefix[] = L"save\\launcher\\ptworld-"; static const wchar_t suffix[] = L".jsonl"; SIZE_T path_length; SIZE_T prefix_length = (sizeof(prefix) / sizeof(prefix[0])) - 1; SIZE_T suffix_length = (sizeof(suffix) / sizeof(suffix[0])) - 1; if (path == NULL) { return FALSE; } path_length = wcslen(path); if ( path_length <= prefix_length + suffix_length || path_length >= MAX_PATH || wcsncmp(path, prefix, prefix_length) != 0 || wcscmp(path + path_length - suffix_length, suffix) != 0 ) { return FALSE; } return wcschr(path, L'/') == NULL && wcschr(path, L':') == NULL && wcsstr(path, L"..") == NULL; } static BOOL open_status_log(StatusLog *status, const wchar_t *path) { if (!status_path_is_allowed(path)) { return FALSE; } status->handle = CreateFileW( path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL ); return status->handle != INVALID_HANDLE_VALUE; } static void write_status_event( StatusLog *status, const char *event, const char *detail_format, ... ) { char details[512]; char line[768]; int line_length; DWORD written = 0; va_list arguments; if (status->handle == INVALID_HANDLE_VALUE) { return; } details[0] = '\0'; if (detail_format != NULL) { va_start(arguments, detail_format); vsnprintf(details, sizeof(details), detail_format, arguments); va_end(arguments); details[sizeof(details) - 1] = '\0'; } line_length = snprintf( line, sizeof(line), "{\"protocol\":1,\"event\":\"%s\",\"tick_ms\":%llu%s}\r\n", event, (unsigned long long)GetTickCount64(), details ); if (line_length <= 0 || (SIZE_T)line_length >= sizeof(line)) { return; } WriteFile(status->handle, line, (DWORD)line_length, &written, NULL); FlushFileBuffers(status->handle); } static void close_status_log(StatusLog *status) { if (status->handle != INVALID_HANDLE_VALUE) { CloseHandle(status->handle); status->handle = INVALID_HANDLE_VALUE; } } static DWORD address_from_rva(DWORD base, DWORD rva) { return base + rva; } static DWORD image_dword(const BYTE *image, SIZE_T offset) { DWORD value; memcpy(&value, image + offset, sizeof(value)); return value; } static BOOL read_exact(HANDLE process, DWORD address, void *buffer, SIZE_T size) { SIZE_T read_size = 0; return ReadProcessMemory( process, (LPCVOID)(uintptr_t)address, buffer, size, &read_size ) && read_size == size; } static BOOL write_exact(HANDLE process, DWORD address, const void *buffer, SIZE_T size) { SIZE_T written_size = 0; return WriteProcessMemory( process, (LPVOID)(uintptr_t)address, buffer, size, &written_size ) && written_size == size; } static void emit_byte(CodeBuffer *code, BYTE value) { if (!code->valid || code->length >= code->capacity) { code->valid = FALSE; return; } code->data[code->length++] = value; } static void emit_dword(CodeBuffer *code, DWORD value) { SIZE_T index; for (index = 0; index < sizeof(value); ++index) { emit_byte(code, (BYTE)((value >> (index * 8u)) & 0xffu)); } } static void emit_bytes(CodeBuffer *code, const BYTE *values, SIZE_T size) { SIZE_T index; for (index = 0; index < size; ++index) { emit_byte(code, values[index]); } } static SIZE_T emit_near_jump_placeholder(CodeBuffer *code, BYTE condition) { SIZE_T displacement_offset; if (condition != 0) { emit_byte(code, 0x0f); emit_byte(code, condition); } else { emit_byte(code, 0xe9); } displacement_offset = code->length; emit_dword(code, 0); return displacement_offset; } static void patch_near_jump(CodeBuffer *code, SIZE_T displacement_offset, SIZE_T target_offset) { int32_t displacement; if (!code->valid || displacement_offset + sizeof(displacement) > code->capacity) { code->valid = FALSE; return; } displacement = (int32_t)(target_offset - (displacement_offset + sizeof(displacement))); memcpy(code->data + displacement_offset, &displacement, sizeof(displacement)); } static void emit_push_immediate(CodeBuffer *code, DWORD value) { emit_byte(code, 0x68); emit_dword(code, value); } static void emit_call_iat(CodeBuffer *code, DWORD iat_address) { emit_byte(code, 0xff); emit_byte(code, 0x15); emit_dword(code, iat_address); } static void emit_set_state(CodeBuffer *code, DWORD state_address, BYTE value) { emit_byte(code, 0xc6); emit_byte(code, 0x05); emit_dword(code, state_address); emit_byte(code, value); } static SIZE_T emit_restore_iat( CodeBuffer *code, DWORD iat_address, DWORD original_lstrcmp, DWORD virtual_protect_iat, DWORD original_iat_protect, DWORD temporary_protect_address ) { SIZE_T protect_failure_jump; /* The import table is in publisher .rdata. Make it writable before MOV. */ emit_push_immediate(code, temporary_protect_address); emit_push_immediate(code, PAGE_READWRITE); emit_push_immediate(code, sizeof(DWORD)); emit_push_immediate(code, iat_address); emit_call_iat(code, virtual_protect_iat); emit_bytes(code, (const BYTE[]){0x85, 0xc0}, 2); protect_failure_jump = emit_near_jump_placeholder(code, 0x84); /* je finish */ /* mov dword ptr [iat_address], original_lstrcmp */ emit_byte(code, 0xc7); emit_byte(code, 0x05); emit_dword(code, iat_address); emit_dword(code, original_lstrcmp); emit_push_immediate(code, temporary_protect_address); emit_push_immediate(code, original_iat_protect); emit_push_immediate(code, sizeof(DWORD)); emit_push_immediate(code, iat_address); emit_call_iat(code, virtual_protect_iat); return protect_failure_jump; } static BOOL build_remote_hook( BYTE *image, SIZE_T image_size, DWORD remote_base, DWORD game_dll_base, DWORD original_lstrcmp, DWORD original_iat_protect, SIZE_T *code_size ) { CodeBuffer code; DWORD map_entry = address_from_rva(game_dll_base, GAME_DLL_MAP_ENTRY_RVA); DWORD code_cave = address_from_rva(game_dll_base, GAME_DLL_CODE_CAVE_RVA); DWORD login_return = address_from_rva(game_dll_base, GAME_DLL_LOGIN_RETURN_RVA); DWORD lstrcmp_iat = address_from_rva(game_dll_base, GAME_DLL_LSTRCMPA_IAT_RVA); DWORD virtual_protect_iat = address_from_rva( game_dll_base, GAME_DLL_VIRTUAL_PROTECT_IAT_RVA ); DWORD flush_iat = address_from_rva( game_dll_base, GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA ); DWORD get_process_iat = address_from_rva( game_dll_base, GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA ); DWORD reclaim_source = remote_base + REMOTE_RECLAIM_OFFSET; DWORD jump_source = remote_base + REMOTE_MAP_JUMP_OFFSET; DWORD state_address = remote_base + REMOTE_STATE_OFFSET; DWORD old_cave_protect = remote_base + REMOTE_OLD_CAVE_PROTECT_OFFSET; DWORD old_entry_protect = remote_base + REMOTE_OLD_ENTRY_PROTECT_OFFSET; DWORD temporary_protect = remote_base + REMOTE_TEMP_PROTECT_OFFSET; SIZE_T nonzero_jump; SIZE_T wrong_caller_jump; SIZE_T already_triggered_jump; SIZE_T cave_failure_jump; SIZE_T entry_failure_jump; SIZE_T success_restore_failure_jump; SIZE_T failure_restore_failure_jump; SIZE_T success_jump; SIZE_T finish_offset; SIZE_T entry_failure_offset; SIZE_T failure_offset; SIZE_T success_offset; if (image_size < REMOTE_TEMP_PROTECT_OFFSET + sizeof(DWORD)) { return FALSE; } memset(image, 0, image_size); memcpy( image + REMOTE_RECLAIM_OFFSET, PROVEN_RECLAIM_HOOK, sizeof(PROVEN_RECLAIM_HOOK) ); memcpy(image + REMOTE_MAP_JUMP_OFFSET, MAP_ENTRY_JUMP, sizeof(MAP_ENTRY_JUMP)); code.data = image; code.capacity = REMOTE_RECLAIM_OFFSET; code.length = 0; code.valid = TRUE; /* Standard-call lstrcmpA wrapper, preserving every nonvolatile register. */ emit_bytes(&code, (const BYTE[]){0x55, 0x89, 0xe5, 0x53, 0x56, 0x57}, 6); emit_bytes(&code, (const BYTE[]){0xff, 0x75, 0x0c, 0xff, 0x75, 0x08}, 6); emit_byte(&code, 0xb8); emit_dword(&code, original_lstrcmp); emit_bytes(&code, (const BYTE[]){0xff, 0xd0, 0x85, 0xc0}, 4); nonzero_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ emit_bytes(&code, (const BYTE[]){0x81, 0x7d, 0x04}, 3); emit_dword(&code, login_return); wrong_caller_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ emit_bytes(&code, (const BYTE[]){0x80, 0x3d}, 2); emit_dword(&code, state_address); emit_byte(&code, 0x00); already_triggered_jump = emit_near_jump_placeholder(&code, 0x85); /* jne finish */ emit_set_state(&code, state_address, 1); /* Make the two exact publisher-code ranges writable only after login. */ emit_push_immediate(&code, old_cave_protect); emit_push_immediate(&code, PAGE_EXECUTE_READWRITE); emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); emit_push_immediate(&code, code_cave); emit_call_iat(&code, virtual_protect_iat); emit_bytes(&code, (const BYTE[]){0x85, 0xc0}, 2); cave_failure_jump = emit_near_jump_placeholder(&code, 0x84); /* je failure */ emit_push_immediate(&code, old_entry_protect); emit_push_immediate(&code, PAGE_EXECUTE_READWRITE); emit_push_immediate(&code, sizeof(MAP_ENTRY_JUMP)); emit_push_immediate(&code, map_entry); emit_call_iat(&code, virtual_protect_iat); emit_bytes(&code, (const BYTE[]){0x85, 0xc0}, 2); entry_failure_jump = emit_near_jump_placeholder(&code, 0x84); /* je entry_failure */ /* Copy only after the exact account-ID comparison returned success. */ emit_byte(&code, 0xfc); emit_byte(&code, 0xbe); emit_dword(&code, reclaim_source); emit_byte(&code, 0xbf); emit_dword(&code, code_cave); emit_byte(&code, 0xb9); emit_dword(&code, sizeof(PROVEN_RECLAIM_HOOK)); emit_bytes(&code, (const BYTE[]){0xf3, 0xa4}, 2); emit_byte(&code, 0xbe); emit_dword(&code, jump_source); emit_byte(&code, 0xbf); emit_dword(&code, map_entry); emit_byte(&code, 0xb9); emit_dword(&code, sizeof(MAP_ENTRY_JUMP)); emit_bytes(&code, (const BYTE[]){0xf3, 0xa4}, 2); /* x86 is coherent, but use the publisher's own imports for an explicit flush. */ emit_call_iat(&code, get_process_iat); emit_push_immediate(&code, 0); emit_push_immediate(&code, 0); emit_byte(&code, 0x50); emit_call_iat(&code, flush_iat); /* Restore publisher section protections after the five-byte entry change. */ emit_push_immediate(&code, temporary_protect); emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); emit_dword(&code, old_entry_protect); emit_push_immediate(&code, sizeof(MAP_ENTRY_JUMP)); emit_push_immediate(&code, map_entry); emit_call_iat(&code, virtual_protect_iat); emit_push_immediate(&code, temporary_protect); emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); emit_dword(&code, old_cave_protect); emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); emit_push_immediate(&code, code_cave); emit_call_iat(&code, virtual_protect_iat); emit_set_state(&code, state_address, 2); success_restore_failure_jump = emit_restore_iat( &code, lstrcmp_iat, original_lstrcmp, virtual_protect_iat, original_iat_protect, temporary_protect ); success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */ entry_failure_offset = code.length; emit_push_immediate(&code, temporary_protect); emit_bytes(&code, (const BYTE[]){0xff, 0x35}, 2); emit_dword(&code, old_cave_protect); emit_push_immediate(&code, sizeof(PROVEN_RECLAIM_HOOK)); emit_push_immediate(&code, code_cave); emit_call_iat(&code, virtual_protect_iat); failure_offset = code.length; emit_set_state(&code, state_address, 3); failure_restore_failure_jump = emit_restore_iat( &code, lstrcmp_iat, original_lstrcmp, virtual_protect_iat, original_iat_protect, temporary_protect ); success_offset = code.length; emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */ finish_offset = code.length; emit_bytes(&code, (const BYTE[]){0x5f, 0x5e, 0x5b, 0x89, 0xec, 0x5d, 0xc2, 0x08, 0x00}, 9); patch_near_jump(&code, nonzero_jump, finish_offset); patch_near_jump(&code, wrong_caller_jump, finish_offset); patch_near_jump(&code, already_triggered_jump, success_offset); patch_near_jump(&code, cave_failure_jump, failure_offset); patch_near_jump(&code, entry_failure_jump, entry_failure_offset); patch_near_jump(&code, success_restore_failure_jump, success_offset); patch_near_jump(&code, failure_restore_failure_jump, success_offset); patch_near_jump(&code, success_jump, success_offset); if (!code.valid) { return FALSE; } *code_size = code.length; return TRUE; } static BOOL remote_iat_restore_is_safe( const BYTE *image, SIZE_T code_size, DWORD game_dll_base, DWORD original_lstrcmp ) { DWORD iat_address = address_from_rva( game_dll_base, GAME_DLL_LSTRCMPA_IAT_RVA ); DWORD virtual_protect_iat = address_from_rva( game_dll_base, GAME_DLL_VIRTUAL_PROTECT_IAT_RVA ); SIZE_T restore_count = 0; SIZE_T index; for (index = 0; index + 10 <= code_size; ++index) { if ( image[index] != 0xc7 || image[index + 1] != 0x05 || image_dword(image, index + 2) != iat_address || image_dword(image, index + 6) != original_lstrcmp ) { continue; } ++restore_count; /* * Immediately before every IAT MOV, require: * VirtualProtect(iat, 4, PAGE_READWRITE, ...), TEST, JE. */ if ( index < 29 || image[index - 29] != 0x68 || image_dword(image, index - 28) != PAGE_READWRITE || image[index - 24] != 0x68 || image_dword(image, index - 23) != sizeof(DWORD) || image[index - 19] != 0x68 || image_dword(image, index - 18) != iat_address || image[index - 14] != 0xff || image[index - 13] != 0x15 || image_dword(image, index - 12) != virtual_protect_iat || image[index - 8] != 0x85 || image[index - 7] != 0xc0 || image[index - 6] != 0x0f || image[index - 5] != 0x84 ) { return FALSE; } } return restore_count == 2; } static DWORD find_game_dll(DWORD process_id, DWORD *module_size) { HANDLE snapshot; MODULEENTRY32W module; DWORD result = 0; snapshot = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, process_id ); if (snapshot == INVALID_HANDLE_VALUE) { return 0; } memset(&module, 0, sizeof(module)); module.dwSize = sizeof(module); if (Module32FirstW(snapshot, &module)) { do { if (_wcsicmp(module.szModule, L"game.dll") == 0) { result = (DWORD)(uintptr_t)module.modBaseAddr; *module_size = module.modBaseSize; break; } } while (Module32NextW(snapshot, &module)); } CloseHandle(snapshot); return result; } static DWORD wait_for_game_dll( HANDLE process, DWORD process_id, DWORD *module_size ) { ULONGLONG deadline = GetTickCount64() + WORLD_FIX_WAIT_MILLISECONDS; DWORD base; while (GetTickCount64() < deadline) { if (WaitForSingleObject(process, 25) == WAIT_OBJECT_0) { return 0; } base = find_game_dll(process_id, module_size); if (base != 0) { return base; } Sleep(10); } return 0; } static BOOL verify_game_dll_memory(HANDLE process, DWORD base, DWORD module_size) { BYTE map_entry[sizeof(MAP_ENTRY_ORIGINAL)]; BYTE login_compare[sizeof(LOGIN_COMPARE_ORIGINAL)]; BYTE code_cave[sizeof(PROVEN_RECLAIM_HOOK)]; DWORD iat_values[4]; if (module_size < GAME_DLL_MINIMUM_IMAGE_SIZE) { return FALSE; } if (!read_exact( process, address_from_rva(base, GAME_DLL_MAP_ENTRY_RVA), map_entry, sizeof(map_entry) ) || memcmp(map_entry, MAP_ENTRY_ORIGINAL, sizeof(map_entry)) != 0) { return FALSE; } if (!read_exact( process, address_from_rva(base, GAME_DLL_LOGIN_RETURN_RVA), login_compare, sizeof(login_compare) ) || memcmp(login_compare, LOGIN_COMPARE_ORIGINAL, sizeof(login_compare)) != 0) { return FALSE; } if (!read_exact( process, address_from_rva(base, GAME_DLL_CODE_CAVE_RVA), code_cave, sizeof(code_cave) )) { return FALSE; } for (SIZE_T index = 0; index < sizeof(code_cave); ++index) { if (code_cave[index] != 0) { return FALSE; } } if (!read_exact( process, address_from_rva(base, GAME_DLL_VIRTUAL_PROTECT_IAT_RVA), &iat_values[0], sizeof(iat_values[0]) ) || !read_exact( process, address_from_rva(base, GAME_DLL_FLUSH_INSTRUCTION_CACHE_IAT_RVA), &iat_values[1], sizeof(iat_values[1]) ) || !read_exact( process, address_from_rva(base, GAME_DLL_GET_CURRENT_PROCESS_IAT_RVA), &iat_values[2], sizeof(iat_values[2]) ) || !read_exact( process, address_from_rva(base, GAME_DLL_LSTRCMPA_IAT_RVA), &iat_values[3], sizeof(iat_values[3]) )) { return FALSE; } return iat_values[0] != 0 && iat_values[1] != 0 && iat_values[2] != 0 && iat_values[3] != 0; } static BOOL install_world_fix( HANDLE process, DWORD game_dll_base, WorldFixInstallation *installation ) { BYTE remote_image[REMOTE_ALLOCATION_SIZE]; DWORD original_lstrcmp; DWORD original_iat_protect; DWORD hook_address; DWORD lstrcmp_iat = address_from_rva( game_dll_base, GAME_DLL_LSTRCMPA_IAT_RVA ); LPVOID remote; SIZE_T code_size; if (!read_exact( process, lstrcmp_iat, &original_lstrcmp, sizeof(original_lstrcmp) )) { return FALSE; } remote = VirtualAllocEx( process, NULL, REMOTE_ALLOCATION_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); if (remote == NULL) { return FALSE; } if (!build_remote_hook( remote_image, sizeof(remote_image), (DWORD)(uintptr_t)remote, game_dll_base, original_lstrcmp, PAGE_READONLY, &code_size )) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); return FALSE; } if (!write_exact( process, (DWORD)(uintptr_t)remote, remote_image, sizeof(remote_image) )) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); return FALSE; } if (!VirtualProtectEx( process, (LPVOID)(uintptr_t)lstrcmp_iat, sizeof(DWORD), PAGE_READWRITE, &original_iat_protect )) { VirtualFreeEx(process, remote, 0, MEM_RELEASE); return FALSE; } /* Rebuild so the callback restores the exact protection returned above. */ if (!build_remote_hook( remote_image, sizeof(remote_image), (DWORD)(uintptr_t)remote, game_dll_base, original_lstrcmp, original_iat_protect, &code_size ) || !write_exact( process, (DWORD)(uintptr_t)remote, remote_image, sizeof(remote_image) )) { DWORD ignored; VirtualProtectEx( process, (LPVOID)(uintptr_t)lstrcmp_iat, sizeof(DWORD), original_iat_protect, &ignored ); VirtualFreeEx(process, remote, 0, MEM_RELEASE); return FALSE; } hook_address = (DWORD)(uintptr_t)remote; if (!write_exact(process, lstrcmp_iat, &hook_address, sizeof(hook_address))) { DWORD ignored; VirtualProtectEx( process, (LPVOID)(uintptr_t)lstrcmp_iat, sizeof(DWORD), original_iat_protect, &ignored ); VirtualFreeEx(process, remote, 0, MEM_RELEASE); return FALSE; } FlushInstructionCache(process, remote, code_size); installation->remote_state = (DWORD)(uintptr_t)remote + REMOTE_STATE_OFFSET; installation->remote_allocation = remote; return TRUE; } static void report_login_accepted(StatusLog *status, BOOL *reported) { if (!*reported) { write_status_event(status, "login_accepted", NULL); *reported = TRUE; } } static BOOL report_world_fix_state( StatusLog *status, BYTE state, BOOL *login_reported ) { if (state >= 1 && state <= 3) { report_login_accepted(status, login_reported); } if (state == 2) { fprintf( stderr, "PTWORLD: login accepted; world-transition fix activated in memory.\n" ); write_status_event(status, "world_fix_activated", NULL); return TRUE; } if (state == 3) { fprintf( stderr, "PTWORLD: login succeeded, but the verified memory fix could not be activated.\n" ); write_status_event(status, "world_fix_activation_failed", NULL); return TRUE; } return FALSE; } static DWORD wait_for_game_and_report( HANDLE process, const WorldFixInstallation *installation, StatusLog *status ) { BOOL reported = FALSE; BOOL login_reported = FALSE; for (;;) { DWORD wait_result = WaitForSingleObject(process, 100); if (wait_result == WAIT_OBJECT_0) { DWORD exit_code = 1; if (!reported && installation->remote_state != 0) { BYTE state = 0; if (read_exact(process, installation->remote_state, &state, sizeof(state))) { reported = report_world_fix_state( status, state, &login_reported ); } } if (!reported && installation->remote_state != 0) { write_status_event(status, "world_fix_not_activated", NULL); } GetExitCodeProcess(process, &exit_code); return exit_code; } if (!reported && installation->remote_state != 0) { BYTE state = 0; if (read_exact(process, installation->remote_state, &state, sizeof(state))) { reported = report_world_fix_state( status, state, &login_reported ); } } } } static int run_self_test(void) { BYTE image[REMOTE_ALLOCATION_SIZE]; SIZE_T code_size = 0; if (!build_remote_hook( image, sizeof(image), 0x50000000u, 0x10000000u, 0x7b621000u, PAGE_READONLY, &code_size ) || code_size == 0 || code_size >= REMOTE_RECLAIM_OFFSET) { fprintf(stderr, "PTWORLD-SELFTEST: remote hook construction failed.\n"); return 1; } if (!remote_iat_restore_is_safe( image, code_size, 0x10000000u, 0x7b621000u )) { fprintf( stderr, "PTWORLD-SELFTEST: import-table restore is not write-safe.\n" ); return 1; } if (memcmp( image + REMOTE_RECLAIM_OFFSET, PROVEN_RECLAIM_HOOK, sizeof(PROVEN_RECLAIM_HOOK) ) != 0 || memcmp( image + REMOTE_MAP_JUMP_OFFSET, MAP_ENTRY_JUMP, sizeof(MAP_ENTRY_JUMP) ) != 0 || image[REMOTE_STATE_OFFSET] != 0) { fprintf(stderr, "PTWORLD-SELFTEST: remote payload layout failed.\n"); return 1; } fprintf( stdout, "PTWORLD-SELFTEST: ok; remote code=%lu bytes.\n", (unsigned long)code_size ); return 0; } int wmain(int argument_count, wchar_t **arguments) { STARTUPINFOW startup; PROCESS_INFORMATION process; WorldFixInstallation installation; StatusLog status; const wchar_t *status_path = NULL; WCHAR game_path[MAX_PATH]; WCHAR command_line[MAX_PATH + 64]; DWORD game_dll_base; DWORD game_dll_size = 0; DWORD exit_code; DWORD path_length; if ( argument_count == 2 && wcscmp(arguments[1], L"--self-test") == 0 ) { return run_self_test(); } initialize_status_log(&status); if ( argument_count == 3 && wcscmp(arguments[1], L"--status-file") == 0 ) { status_path = arguments[2]; if (!open_status_log(&status, status_path)) { fprintf( stderr, "PTWORLD: could not create the per-run status file (Windows error %lu).\n", (unsigned long)GetLastError() ); } } else if (argument_count != 1) { fprintf(stderr, "PTWORLD: invalid helper arguments.\n"); return 111; } write_status_event( &status, "helper_started", ",\"process_id\":%lu", (unsigned long)GetCurrentProcessId() ); memset(&startup, 0, sizeof(startup)); memset(&process, 0, sizeof(process)); memset(&installation, 0, sizeof(installation)); startup.cb = sizeof(startup); path_length = GetFullPathNameW(L"Game.exe", MAX_PATH, game_path, NULL); if (path_length == 0 || path_length >= MAX_PATH) { fprintf(stderr, "PTWORLD: Game.exe is not available in the working directory.\n"); write_status_event(&status, "game_path_failed", NULL); close_status_log(&status); return 111; } if (swprintf( command_line, MAX_PATH + 64, L"\"%ls\" PTEU_REBORN", game_path ) < 0) { fprintf(stderr, "PTWORLD: could not construct the game command line.\n"); write_status_event(&status, "game_command_failed", NULL); close_status_log(&status); return 111; } if (!CreateProcessW( game_path, command_line, NULL, NULL, TRUE, 0, NULL, NULL, &startup, &process )) { DWORD error = GetLastError(); fprintf( stderr, "PTWORLD: could not start Game.exe (Windows error %lu).\n", (unsigned long)error ); write_status_event( &status, "game_start_failed", ",\"windows_error\":%lu", (unsigned long)error ); close_status_log(&status); return 111; } CloseHandle(process.hThread); write_status_event( &status, "game_started", ",\"process_id\":%lu", (unsigned long)process.dwProcessId ); game_dll_base = wait_for_game_dll( process.hProcess, process.dwProcessId, &game_dll_size ); if (game_dll_base != 0) { write_status_event( &status, "game_dll_found", ",\"base\":%lu,\"size\":%lu", (unsigned long)game_dll_base, (unsigned long)game_dll_size ); } if (game_dll_base == 0) { fprintf( stderr, "PTWORLD: game.dll was not available for verification; continuing pristine.\n" ); write_status_event(&status, "game_dll_unavailable", NULL); } else if (!verify_game_dll_memory(process.hProcess, game_dll_base, game_dll_size)) { fprintf( stderr, "PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n" ); write_status_event( &status, "game_dll_rejected", ",\"base\":%lu,\"size\":%lu", (unsigned long)game_dll_base, (unsigned long)game_dll_size ); } else { write_status_event( &status, "game_dll_verified", NULL ); if (!install_world_fix(process.hProcess, game_dll_base, &installation)) { DWORD error = GetLastError(); fprintf( stderr, "PTWORLD: the post-login hook could not be armed; continuing pristine.\n" ); write_status_event( &status, "hook_arm_failed", ",\"windows_error\":%lu", (unsigned long)error ); } else { fprintf( stderr, "PTWORLD: publisher game.dll verified; post-login world fix armed.\n" ); write_status_event( &status, "hook_armed", ",\"base\":%lu,\"size\":%lu", (unsigned long)game_dll_base, (unsigned long)game_dll_size ); } } exit_code = wait_for_game_and_report(process.hProcess, &installation, &status); write_status_event( &status, "game_exited", ",\"exit_code\":%lu", (unsigned long)exit_code ); CloseHandle(process.hProcess); close_status_log(&status); return (int)exit_code; }