Added more diagnostics to the native world fixer and py logs system.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user