Added more diagnostics to the native world fixer and py logs system.

This commit is contained in:
2026-08-28 20:56:26 +02:00
parent b344506071
commit 010fe14ab4
6 changed files with 590 additions and 37 deletions

View File

@@ -2,6 +2,7 @@
#include <windows.h>
#include <tlhelp32.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
@@ -70,12 +71,131 @@ typedef struct WorldFixInstallation
} 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;
@@ -188,7 +308,7 @@ static void emit_set_state(CodeBuffer *code, DWORD state_address, BYTE value)
}
static void emit_restore_iat(
static SIZE_T emit_restore_iat(
CodeBuffer *code,
DWORD iat_address,
DWORD original_lstrcmp,
@@ -197,6 +317,17 @@ static void emit_restore_iat(
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);
@@ -208,6 +339,7 @@ static void emit_restore_iat(
emit_push_immediate(code, sizeof(DWORD));
emit_push_immediate(code, iat_address);
emit_call_iat(code, virtual_protect_iat);
return protect_failure_jump;
}
@@ -249,6 +381,8 @@ static BOOL build_remote_hook(
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;
@@ -343,7 +477,8 @@ static BOOL build_remote_hook(
emit_push_immediate(&code, code_cave);
emit_call_iat(&code, virtual_protect_iat);
emit_restore_iat(
emit_set_state(&code, state_address, 2);
success_restore_failure_jump = emit_restore_iat(
&code,
lstrcmp_iat,
original_lstrcmp,
@@ -351,7 +486,6 @@ static BOOL build_remote_hook(
original_iat_protect,
temporary_protect
);
emit_set_state(&code, state_address, 2);
success_jump = emit_near_jump_placeholder(&code, 0); /* jmp finish */
entry_failure_offset = code.length;
@@ -363,7 +497,8 @@ static BOOL build_remote_hook(
emit_call_iat(&code, virtual_protect_iat);
failure_offset = code.length;
emit_restore_iat(
emit_set_state(&code, state_address, 3);
failure_restore_failure_jump = emit_restore_iat(
&code,
lstrcmp_iat,
original_lstrcmp,
@@ -371,7 +506,6 @@ static BOOL build_remote_hook(
original_iat_protect,
temporary_protect
);
emit_set_state(&code, state_address, 3);
success_offset = code.length;
emit_bytes(&code, (const BYTE[]){0x31, 0xc0}, 2); /* successful lstrcmpA result */
@@ -383,6 +517,8 @@ static BOOL build_remote_hook(
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)
@@ -394,6 +530,64 @@ static BOOL build_remote_hook(
}
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;
@@ -647,18 +841,78 @@ static BOOL install_world_fix(
}
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
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;
}
@@ -667,22 +921,11 @@ static DWORD wait_for_game_and_report(
BYTE state = 0;
if (read_exact(process, installation->remote_state, &state, sizeof(state)))
{
if (state == 2)
{
fprintf(
stderr,
"PTWORLD: login accepted; world-transition fix activated in memory.\n"
);
reported = TRUE;
}
else if (state == 3)
{
fprintf(
stderr,
"PTWORLD: login succeeded, but the verified memory fix could not be activated.\n"
);
reported = TRUE;
}
reported = report_world_fix_state(
status,
state,
&login_reported
);
}
}
}
@@ -706,6 +949,19 @@ static int run_self_test(void)
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,
@@ -733,6 +989,8 @@ 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;
@@ -747,6 +1005,33 @@ int wmain(int argument_count, wchar_t **arguments)
{
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));
@@ -755,6 +1040,8 @@ int wmain(int argument_count, wchar_t **arguments)
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(
@@ -765,6 +1052,8 @@ int wmain(int argument_count, wchar_t **arguments)
) < 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(
@@ -780,26 +1069,51 @@ int wmain(int argument_count, wchar_t **arguments)
&process
))
{
DWORD error = GetLastError();
fprintf(
stderr,
"PTWORLD: could not start Game.exe (Windows error %lu).\n",
(unsigned long)GetLastError()
(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))
{
@@ -807,23 +1121,59 @@ int wmain(int argument_count, wchar_t **arguments)
stderr,
"PTWORLD: game.dll memory did not match V21.75.83; continuing pristine.\n"
);
}
else if (!install_world_fix(process.hProcess, game_dll_base, &installation))
{
fprintf(
stderr,
"PTWORLD: the post-login hook could not be armed; 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
{
fprintf(
stderr,
"PTWORLD: publisher game.dll verified; post-login world fix armed.\n"
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);
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;
}