#include "90CN_dungeon_skill_gate_fix.h" #include #include #include #include namespace { // v7/v8 live evidence on the current NoPack EXE: // sub_340BCF1 calls the native skill-state precheck, then executes // test al, al // je loc_340BD2E // A non-zero result falls through into sub_99F840, which opens owner 0x49 // (the "initial skill state" warning) and then early-returns from the portal // transition before ENUM_CMDPACKET_ENTER_SELECT_DUNGEON/msg15 is emitted. // Force only this post-test branch to the native normal continuation. The // precheck function itself still runs, preserving any side effects/state reads. constexpr uintptr_t kSkillGateBranchRva = 0x0300BCFF; // 0340BCFF at base 00400000 constexpr DWORD kInstallDelayMs = 1200; const unsigned char kExpectedBranch[2] = { 0x74, 0x2D }; // je +0x2D const unsigned char kBypassBranch[2] = { 0xEB, 0x2D }; // jmp +0x2D HMODULE g_selfModule = nullptr; uintptr_t g_exeBase = 0; HANDLE g_worker = nullptr; volatile LONG g_stop = 0; volatile LONG g_installed = 0; // The current 111 MB DB4 client does not contain the legacy 74 2D branch. // Keep the first observation pass strictly passive: this function is the // current selector/list state routine identified by the version-locked // prologue at RVA 0300BB80. The probe records entry state and returns to the // original function through a trampoline; it never changes a branch result. constexpr uintptr_t kCurrentSelectorProbeRva = 0x0300BB80; constexpr size_t kCurrentSelectorProbePatchLength = 10; const unsigned char kCurrentSelectorProbePrologue[ kCurrentSelectorProbePatchLength] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0xF8, 0x44, 0x27, 0x04 }; constexpr LONG kCurrentSelectorProbeMaxLoggedCalls = 4096; HMODULE g_currentProbeModule = nullptr; uintptr_t g_currentProbeBase = 0; void* g_currentSelectorProbeOriginal = nullptr; HANDLE g_currentProbeWorker = nullptr; volatile LONG g_currentProbeStop = 0; volatile LONG g_currentProbeInstalled = 0; volatile LONG g_currentProbeCallCount = 0; // The DB4 client no longer uses the legacy 0300BCFF branch. The first // observation pass also showed that 0300BB80 is only a startup list routine. // These two current-EXE owner/notice builders are therefore traced separately // while reproducing the native "initial skill state" dialog. They remain // observe-only: no return value, owner, or dialog state is changed. constexpr uintptr_t kCurrentOwnerManagerProbeRva = 0x004A1170; // VA 008A1170 constexpr size_t kCurrentOwnerManagerProbePatchLength = 14; const unsigned char kCurrentOwnerManagerProbePrologue[ kCurrentOwnerManagerProbePatchLength] = { 0x55, 0x8B, 0xEC, 0x83, 0xE4, 0xF8, 0x83, 0xEC, 0x18, 0x56, 0x57, 0x8D, 0x45, 0x0C, }; constexpr uintptr_t kCurrentNoticeBuilderProbeRva = 0x023B65B0; // VA 027B65B0 constexpr size_t kCurrentNoticeBuilderProbePatchLength = 12; const unsigned char kCurrentNoticeBuilderProbePrologue[ kCurrentNoticeBuilderProbePatchLength] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x18, 0x83, 0x7D, 0x08, 0x00, 0x56, 0x8B, }; constexpr LONG kCurrentOwnerProbeMaxLoggedCalls = 1024; void* g_currentOwnerManagerProbeOriginal = nullptr; void* g_currentNoticeBuilderProbeOriginal = nullptr; volatile LONG g_currentOwnerProbeInstalled = 0; volatile LONG g_currentNoticeProbeInstalled = 0; volatile LONG g_currentOwnerProbeCallCount = 0; // Current 111 MB DB4 client. In the owner-0x73 dungeon-selector function, // the native count comparison at VA 0x02248CAC uses `jle 0x02248CE8` to // continue normally. The fall-through path builds text id 0x11C79 and sends // event 0x2D6 (the "skill is in initial state" notice) before hiding the // selector owner. Force only this local branch to the already existing normal // continuation. The exact prologue/branch bytes are checked at runtime. constexpr uintptr_t kCurrentSkillGateBranchRva = 0x01E48CAC; const unsigned char kCurrentSkillGateExpected[2] = { 0x7E, 0x3A }; const unsigned char kCurrentSkillGateBypass[2] = { 0xEB, 0x3A }; HMODULE g_currentSkillGateModule = nullptr; uintptr_t g_currentSkillGateBase = 0; HANDLE g_currentSkillGateWorker = nullptr; volatile LONG g_currentSkillGateStop = 0; volatile LONG g_currentSkillGateInstalled = 0; void BuildLogPath(wchar_t* output, size_t outputCount) { if (!output || outputCount == 0) return; output[0] = L'\0'; DWORD length = g_selfModule ? GetModuleFileNameW(g_selfModule, output, static_cast(outputCount)) : 0; if (!length || length >= outputCount) { wcscpy_s(output, outputCount, L"90CN_fix.log"); return; } wchar_t* slash = wcsrchr(output, L'\\'); wchar_t* slash2 = wcsrchr(output, L'/'); if (slash2 && (!slash || slash2 > slash)) slash = slash2; if (slash) { slash[1] = L'\0'; wcscat_s(output, outputCount, L"90CN_fix.log"); } else { wcscpy_s(output, outputCount, L"90CN_fix.log"); } } void WriteLogLine(const char* text) { if (!text) return; wchar_t path[MAX_PATH] = {}; BuildLogPath(path, _countof(path)); HANDLE file = CreateFileW(path, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) return; DWORD written = 0; WriteFile(file, text, static_cast(strlen(text)), &written, nullptr); CloseHandle(file); } bool PatchBytes(unsigned char* target, const unsigned char* bytes, size_t count) { DWORD oldProtection = 0; if (!VirtualProtect(target, count, PAGE_EXECUTE_READWRITE, &oldProtection)) { return false; } memcpy(target, bytes, count); DWORD ignoredProtection = 0; VirtualProtect(target, count, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, count); return memcmp(target, bytes, count) == 0; } // 探针开关:DLL 同目录出现 90CN_gate_probe.flag 时**不**打这个旁路,让客户端 // 按原逻辑走“技能处于初始状态”分支。用来确认该检查对当前角色是否真的成立, // 不改任何数据、删掉 flag 即恢复。2026-10-19 蛇腹剑形态排查用。 static bool GateProbeBypassDisabled() { wchar_t path[MAX_PATH] = {}; DWORD length = g_selfModule ? GetModuleFileNameW(g_selfModule, path, MAX_PATH) : 0; if (!length || length >= MAX_PATH) return false; wchar_t* slash = wcsrchr(path, L'\\'); if (!slash) return false; slash[1] = L'\0'; if (wcscat_s(path, L"90CN_gate_probe.flag") != 0) return false; return GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES; } bool InstallBypass() { if (!g_exeBase) return false; if (GateProbeBypassDisabled()) { WriteLogLine( "dungeon-skill-gate-fix probe_mode=1 bypass_disabled=true " "target=0340BCFF reason=90CN_gate_probe.flag\r\n"); return true; } unsigned char* target = reinterpret_cast( g_exeBase + kSkillGateBranchRva); unsigned char observed[2] = {}; __try { memcpy(observed, target, sizeof(observed)); if (memcmp(observed, kBypassBranch, sizeof(kBypassBranch)) == 0) { InterlockedExchange(&g_installed, 1); WriteLogLine( "dungeon-skill-gate-fix already_installed target=0340BCFF bytes=EB2D\r\n"); return true; } if (memcmp(observed, kExpectedBranch, sizeof(kExpectedBranch)) != 0) { char line[256] = {}; sprintf_s(line, "dungeon-skill-gate-fix install_failed target=0340BCFF " "observed=%02X%02X expected=742D\r\n", observed[0], observed[1]); WriteLogLine(line); return false; } if (!PatchBytes(target, kBypassBranch, sizeof(kBypassBranch))) { WriteLogLine( "dungeon-skill-gate-fix install_failed reason=patch-write target=0340BCFF\r\n"); return false; } } __except (EXCEPTION_EXECUTE_HANDLER) { WriteLogLine( "dungeon-skill-gate-fix install_failed reason=exception target=0340BCFF\r\n"); return false; } InterlockedExchange(&g_installed, 1); WriteLogLine( "dungeon-skill-gate-fix installed target=0340BCFF original=742D replacement=EB2D " "scope=initial-skill-selector-precheck-only preserves=native-skill-check-call-and-other-dungeon-gates\r\n"); return true; } void RestoreBypass() { if (InterlockedCompareExchange(&g_installed, 0, 0) == 0 || !g_exeBase) return; unsigned char* target = reinterpret_cast( g_exeBase + kSkillGateBranchRva); __try { if (memcmp(target, kBypassBranch, sizeof(kBypassBranch)) == 0) { if (PatchBytes(target, kExpectedBranch, sizeof(kExpectedBranch))) { WriteLogLine( "dungeon-skill-gate-fix restored target=0340BCFF bytes=742D\r\n"); } else { WriteLogLine( "dungeon-skill-gate-fix restore_failed target=0340BCFF\r\n"); } } else { char line[256] = {}; sprintf_s(line, "dungeon-skill-gate-fix restore_skipped target=0340BCFF current=%02X%02X\r\n", target[0], target[1]); WriteLogLine(line); } } __except (EXCEPTION_EXECUTE_HANDLER) { WriteLogLine( "dungeon-skill-gate-fix restore_failed reason=exception target=0340BCFF\r\n"); } InterlockedExchange(&g_installed, 0); } bool InstallCurrentSkillGateBypass() { if (!g_currentSkillGateBase) return false; unsigned char* target = reinterpret_cast( g_currentSkillGateBase + kCurrentSkillGateBranchRva); unsigned char observed[2] = {}; __try { memcpy(observed, target, sizeof(observed)); if (memcmp(observed, kCurrentSkillGateBypass, sizeof(kCurrentSkillGateBypass)) == 0) { InterlockedExchange(&g_currentSkillGateInstalled, 1); WriteLogLine( "new-client dungeon-skill-gate already_installed " "target=02248CAC bytes=EB3A scope=owner-73-selector\r\n"); return true; } if (memcmp(observed, kCurrentSkillGateExpected, sizeof(kCurrentSkillGateExpected)) != 0) { char line[256] = {}; sprintf_s(line, "new-client dungeon-skill-gate install_failed " "target=02248CAC observed=%02X%02X expected=7E3A\r\n", observed[0], observed[1]); WriteLogLine(line); return false; } if (!PatchBytes(target, kCurrentSkillGateBypass, sizeof(kCurrentSkillGateBypass))) { WriteLogLine( "new-client dungeon-skill-gate install_failed " "reason=patch-write target=02248CAC\r\n"); return false; } } __except (EXCEPTION_EXECUTE_HANDLER) { WriteLogLine( "new-client dungeon-skill-gate install_failed " "reason=exception target=02248CAC\r\n"); return false; } InterlockedExchange(&g_currentSkillGateInstalled, 1); WriteLogLine( "new-client dungeon-skill-gate installed target=02248CAC " "original=7E3A replacement=EB3A " "scope=owner-73-dungeon-selector-initial-skill-notice\r\n"); return true; } void RestoreCurrentSkillGateBypass() { if (InterlockedCompareExchange(&g_currentSkillGateInstalled, 0, 0) == 0 || !g_currentSkillGateBase) return; unsigned char* target = reinterpret_cast( g_currentSkillGateBase + kCurrentSkillGateBranchRva); __try { if (memcmp(target, kCurrentSkillGateBypass, sizeof(kCurrentSkillGateBypass)) == 0) { if (PatchBytes(target, kCurrentSkillGateExpected, sizeof(kCurrentSkillGateExpected))) { WriteLogLine( "new-client dungeon-skill-gate restored " "target=02248CAC bytes=7E3A\r\n"); } else { WriteLogLine( "new-client dungeon-skill-gate restore_failed " "target=02248CAC\r\n"); } } else { char line[256] = {}; sprintf_s(line, "new-client dungeon-skill-gate restore_skipped " "target=02248CAC current=%02X%02X\r\n", target[0], target[1]); WriteLogLine(line); } } __except (EXCEPTION_EXECUTE_HANDLER) { WriteLogLine( "new-client dungeon-skill-gate restore_failed " "reason=exception target=02248CAC\r\n"); } InterlockedExchange(&g_currentSkillGateInstalled, 0); } DWORD WINAPI InstallCurrentSkillGateWorker(LPVOID) { g_currentSkillGateBase = reinterpret_cast( GetModuleHandleW(nullptr)); if (!g_currentSkillGateBase || !g_currentSkillGateModule) return 0; DWORD slept = 0; while (slept < kInstallDelayMs && InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) { Sleep(100); slept += 100; } if (InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) { InstallCurrentSkillGateBypass(); } return 0; } DWORD WINAPI InstallWorker(LPVOID) { g_exeBase = reinterpret_cast(GetModuleHandleW(nullptr)); if (!g_exeBase || !g_selfModule) return 0; DWORD slept = 0; while (slept < kInstallDelayMs && InterlockedCompareExchange(&g_stop, 0, 0) == 0) { Sleep(100); slept += 100; } if (InterlockedCompareExchange(&g_stop, 0, 0) == 0) { InstallBypass(); } return 0; } void LogCurrentSelectorProbe(uintptr_t self, uintptr_t caller) { LONG callNumber = InterlockedIncrement(&g_currentProbeCallCount); if (callNumber > kCurrentSelectorProbeMaxLoggedCalls) return; const uintptr_t base = reinterpret_cast( GetModuleHandleW(nullptr)); const unsigned int callerRva = (base && caller >= base) ? static_cast(caller - base) : 0; unsigned int state2fc = 0xFFFFFFFFu; uintptr_t manager = 0; __try { if (self) { state2fc = static_cast( *reinterpret_cast(self + 0x2FC)); manager = *reinterpret_cast(self + 0xFC8); } } __except (EXCEPTION_EXECUTE_HANDLER) { state2fc = 0xFFFFFFFEu; manager = 0; } // The two statically identified callers are the useful selector/list // paths. Keep a short prefix of other callers so a changed call graph is // visible without flooding the client trace during startup. const bool selectorCaller = (callerRva >= 0x03011740u && callerRva <= 0x03011790u) || (callerRva >= 0x03012720u && callerRva <= 0x03012770u); if (!selectorCaller && callNumber > 64) return; char line[384] = {}; sprintf_s(line, "new-client dungeon-selector-probe call=%ld self=0x%08X " "caller_rva=0x%08X state_2fc=%u manager=0x%08X\r\n", callNumber, static_cast(self), callerRva, state2fc, static_cast(manager)); WriteLogLine(line); } bool CurrentProbeBytesMatch(const unsigned char* target, const unsigned char* expected, size_t length); void LogCurrentOwnerProbe(unsigned int kind, uintptr_t self, uintptr_t caller, uintptr_t originalStack) { LONG callNumber = InterlockedIncrement(&g_currentOwnerProbeCallCount); if (callNumber > kCurrentOwnerProbeMaxLoggedCalls) return; const uintptr_t base = reinterpret_cast( GetModuleHandleW(nullptr)); const unsigned int callerRva = (base && caller >= base) ? static_cast(caller - base) : 0; unsigned int arg0 = 0xFFFFFFFFu; unsigned int arg1 = 0xFFFFFFFFu; unsigned int arg2 = 0xFFFFFFFFu; __try { if (originalStack) { arg0 = *reinterpret_cast(originalStack + 4); arg1 = *reinterpret_cast(originalStack + 8); arg2 = *reinterpret_cast(originalStack + 0xC); } } __except (EXCEPTION_EXECUTE_HANDLER) { arg0 = arg1 = arg2 = 0xFFFFFFFEu; } // Keep the normal call prefix short, but retain every call carrying the // legacy warning owner/message values so the click window is unambiguous. const bool suspicious = arg0 == 0x49u || arg1 == 0x49u || arg2 == 0x49u || arg0 == 0x73u || arg1 == 0x73u || arg2 == 0x73u || arg0 == 0xF6u || arg1 == 0xF6u || arg2 == 0xF6u; if (!suspicious && callNumber > 96) return; char line[448] = {}; sprintf_s(line, "new-client dungeon-owner-probe kind=%u call=%ld self=0x%08X " "caller_rva=0x%08X arg0=0x%08X arg1=0x%08X arg2=0x%08X " "suspicious=%u\r\n", kind, callNumber, static_cast(self), callerRva, arg0, arg1, arg2, suspicious ? 1u : 0u); WriteLogLine(line); } __declspec(naked) void ProxyCurrentOwnerManagerProbe() { __asm { pushfd pushad mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd) mov eax, [ecx + 4] // native return address at original ESP mov edx, [esp + 18h] // saved native ECX (this) add ecx, 4 // original ESP, points at native return address push ecx push eax push edx push 1 call LogCurrentOwnerProbe add esp, 10h popad popfd jmp dword ptr [g_currentOwnerManagerProbeOriginal] } } __declspec(naked) void ProxyCurrentNoticeBuilderProbe() { __asm { pushfd pushad mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd) mov eax, [ecx + 4] // native return address at original ESP mov edx, [esp + 18h] // saved native ECX (this) add ecx, 4 // original ESP, points at native return address push ecx push eax push edx push 2 call LogCurrentOwnerProbe add esp, 10h popad popfd jmp dword ptr [g_currentNoticeBuilderProbeOriginal] } } bool InstallCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected, size_t patchLength, void* detour, void** original, const char* label, volatile LONG* installed) { if (!g_currentProbeBase || !expected || !detour || !original || !installed || patchLength < 5) return false; unsigned char* target = reinterpret_cast( g_currentProbeBase + rva); if (!CurrentProbeBytesMatch(target, expected, patchLength)) { char line[256] = {}; sprintf_s(line, "new-client dungeon-owner-probe skipped label=%s " "reason=prologue-mismatch rva=0x%08X\r\n", label ? label : "unknown", static_cast(rva)); WriteLogLine(line); return false; } unsigned char* trampoline = static_cast(VirtualAlloc( nullptr, patchLength + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!trampoline) return false; memcpy(trampoline, target, patchLength); trampoline[patchLength] = 0xE9; *reinterpret_cast(trampoline + patchLength + 1) = static_cast(reinterpret_cast(target + patchLength) - reinterpret_cast(trampoline + patchLength + 5)); DWORD oldProtection = 0; if (!VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE, &oldProtection)) { VirtualFree(trampoline, 0, MEM_RELEASE); return false; } unsigned char jump[5] = { 0xE9 }; *reinterpret_cast(jump + 1) = static_cast(reinterpret_cast(detour) - reinterpret_cast(target + 5)); memcpy(target, jump, sizeof(jump)); for (size_t i = sizeof(jump); i < patchLength; ++i) target[i] = 0x90; DWORD ignoredProtection = 0; VirtualProtect(target, patchLength, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, patchLength); FlushInstructionCache(GetCurrentProcess(), trampoline, patchLength + 5); *original = trampoline; InterlockedExchange(installed, 1); char line[256] = {}; sprintf_s(line, "new-client dungeon-owner-probe installed label=%s " "rva=0x%08X patch_length=%u mode=observe-only\r\n", label ? label : "unknown", static_cast(rva), static_cast(patchLength)); WriteLogLine(line); return true; } void RestoreCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected, size_t patchLength, void* detour, void** original, const char* label, volatile LONG* installed) { if (!g_currentProbeBase || !expected || !detour || !original || !installed || InterlockedCompareExchange(installed, 0, 0) == 0) return; unsigned char* target = reinterpret_cast( g_currentProbeBase + rva); unsigned char jump[5] = { 0xE9 }; *reinterpret_cast(jump + 1) = static_cast(reinterpret_cast(detour) - reinterpret_cast(target + 5)); bool ours = CurrentProbeBytesMatch(target, jump, sizeof(jump)); for (size_t i = sizeof(jump); ours && i < patchLength; ++i) { ours = target[i] == 0x90; } if (ours) { DWORD oldProtection = 0; if (VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE, &oldProtection)) { memcpy(target, expected, patchLength); DWORD ignoredProtection = 0; VirtualProtect(target, patchLength, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, patchLength); char line[256] = {}; sprintf_s(line, "new-client dungeon-owner-probe restored label=%s " "rva=0x%08X\r\n", label ? label : "unknown", static_cast(rva)); WriteLogLine(line); } } else { char line[256] = {}; sprintf_s(line, "new-client dungeon-owner-probe restore-skipped label=%s " "reason=target-changed rva=0x%08X\r\n", label ? label : "unknown", static_cast(rva)); WriteLogLine(line); } if (*original) { VirtualFree(*original, 0, MEM_RELEASE); *original = nullptr; } InterlockedExchange(installed, 0); } // Preserve all native registers, flags, stack cleanup and the original // return address. The stolen prologue contains no relative instruction, so // the generic local trampoline can resume at target+10 safely. __declspec(naked) void ProxyCurrentSelectorProbe() { __asm { pushfd pushad mov eax, [esp + 24h] // native return address mov edx, [esp + 18h] // saved native ECX (this) push eax push edx call LogCurrentSelectorProbe add esp, 8 popad popfd jmp dword ptr [g_currentSelectorProbeOriginal] } } bool CurrentProbeBytesMatch(const unsigned char* target, const unsigned char* expected, size_t length) { if (!target || !expected || length == 0) return false; __try { return memcmp(target, expected, length) == 0; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool InstallCurrentSelectorProbe() { if (!g_currentProbeBase) return false; unsigned char* target = reinterpret_cast( g_currentProbeBase + kCurrentSelectorProbeRva); if (!CurrentProbeBytesMatch(target, kCurrentSelectorProbePrologue, kCurrentSelectorProbePatchLength)) { WriteLogLine( "new-client dungeon-selector-probe skipped: prologue-mismatch " "rva=0300BB80 expected=558BEC6AFF68F8442704\r\n"); return false; } unsigned char* trampoline = static_cast(VirtualAlloc( nullptr, kCurrentSelectorProbePatchLength + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!trampoline) { WriteLogLine( "new-client dungeon-selector-probe failed: trampoline-allocation\r\n"); return false; } memcpy(trampoline, target, kCurrentSelectorProbePatchLength); trampoline[kCurrentSelectorProbePatchLength] = 0xE9; *reinterpret_cast(trampoline + kCurrentSelectorProbePatchLength + 1) = static_cast( reinterpret_cast(target + kCurrentSelectorProbePatchLength) - reinterpret_cast(trampoline + kCurrentSelectorProbePatchLength + 5)); DWORD oldProtection = 0; if (!VirtualProtect(target, kCurrentSelectorProbePatchLength, PAGE_EXECUTE_READWRITE, &oldProtection)) { VirtualFree(trampoline, 0, MEM_RELEASE); WriteLogLine( "new-client dungeon-selector-probe failed: virtualprotect\r\n"); return false; } unsigned char jump[5] = { 0xE9 }; *reinterpret_cast(jump + 1) = static_cast( reinterpret_cast(&ProxyCurrentSelectorProbe) - reinterpret_cast(target + 5)); memcpy(target, jump, sizeof(jump)); for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) { target[i] = 0x90; } DWORD ignoredProtection = 0; VirtualProtect(target, kCurrentSelectorProbePatchLength, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, kCurrentSelectorProbePatchLength); FlushInstructionCache(GetCurrentProcess(), trampoline, kCurrentSelectorProbePatchLength + 5); g_currentSelectorProbeOriginal = trampoline; InterlockedExchange(&g_currentProbeInstalled, 1); WriteLogLine( "new-client dungeon-selector-probe installed rva=0300BB80 " "patch_length=10 mode=observe-only\r\n"); return true; } bool CurrentProbeDetourMatches(const unsigned char* target) { if (!target) return false; unsigned char jump[5] = { 0xE9 }; *reinterpret_cast(jump + 1) = static_cast( reinterpret_cast(&ProxyCurrentSelectorProbe) - reinterpret_cast(target + 5)); if (!CurrentProbeBytesMatch(target, jump, sizeof(jump))) return false; for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) { if (target[i] != 0x90) return false; } return true; } void RestoreCurrentSelectorProbe() { if (InterlockedCompareExchange(&g_currentProbeInstalled, 0, 0) == 0 || !g_currentProbeBase) return; unsigned char* target = reinterpret_cast( g_currentProbeBase + kCurrentSelectorProbeRva); if (CurrentProbeDetourMatches(target)) { DWORD oldProtection = 0; if (VirtualProtect(target, kCurrentSelectorProbePatchLength, PAGE_EXECUTE_READWRITE, &oldProtection)) { memcpy(target, kCurrentSelectorProbePrologue, kCurrentSelectorProbePatchLength); DWORD ignoredProtection = 0; VirtualProtect(target, kCurrentSelectorProbePatchLength, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, kCurrentSelectorProbePatchLength); WriteLogLine( "new-client dungeon-selector-probe restored rva=0300BB80\r\n"); } } else { WriteLogLine( "new-client dungeon-selector-probe restore-skipped target-changed\r\n"); } if (g_currentSelectorProbeOriginal) { VirtualFree(g_currentSelectorProbeOriginal, 0, MEM_RELEASE); g_currentSelectorProbeOriginal = nullptr; } InterlockedExchange(&g_currentProbeInstalled, 0); } DWORD WINAPI CurrentSelectorProbeWorker(LPVOID) { g_currentProbeBase = reinterpret_cast( GetModuleHandleW(nullptr)); if (!g_currentProbeBase) return 0; DWORD slept = 0; while (slept < kInstallDelayMs && InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) { Sleep(100); slept += 100; } if (InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) { InstallCurrentSelectorProbe(); InstallCurrentOwnerProbe( kCurrentOwnerManagerProbeRva, kCurrentOwnerManagerProbePrologue, kCurrentOwnerManagerProbePatchLength, &ProxyCurrentOwnerManagerProbe, &g_currentOwnerManagerProbeOriginal, "owner-manager-008A1170", &g_currentOwnerProbeInstalled); InstallCurrentOwnerProbe( kCurrentNoticeBuilderProbeRva, kCurrentNoticeBuilderProbePrologue, kCurrentNoticeBuilderProbePatchLength, &ProxyCurrentNoticeBuilderProbe, &g_currentNoticeBuilderProbeOriginal, "notice-builder-027B65B0", &g_currentNoticeProbeInstalled); } return 0; } } void StartDungeonSkillGateFix(HMODULE module) { if (g_worker) return; g_selfModule = module; InterlockedExchange(&g_stop, 0); g_worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr); } void StopDungeonSkillGateFix() { InterlockedExchange(&g_stop, 1); if (g_worker) { WaitForSingleObject(g_worker, 1500); CloseHandle(g_worker); g_worker = nullptr; } RestoreBypass(); } void StartCurrentDungeonSkillGateFix(HMODULE module) { if (g_currentSkillGateWorker) return; g_currentSkillGateModule = module; InterlockedExchange(&g_currentSkillGateStop, 0); g_currentSkillGateWorker = CreateThread(nullptr, 0, InstallCurrentSkillGateWorker, nullptr, 0, nullptr); if (!g_currentSkillGateWorker) { WriteLogLine( "new-client dungeon-skill-gate failed: worker-create\r\n"); } } void StopCurrentDungeonSkillGateFix() { InterlockedExchange(&g_currentSkillGateStop, 1); if (g_currentSkillGateWorker) { WaitForSingleObject(g_currentSkillGateWorker, 1500); CloseHandle(g_currentSkillGateWorker); g_currentSkillGateWorker = nullptr; } RestoreCurrentSkillGateBypass(); } void StartCurrentDungeonSelectorProbe(HMODULE module) { if (g_currentProbeWorker) return; g_currentProbeModule = module; InterlockedExchange(&g_currentProbeStop, 0); g_currentProbeWorker = CreateThread(nullptr, 0, CurrentSelectorProbeWorker, nullptr, 0, nullptr); if (!g_currentProbeWorker) { WriteLogLine( "new-client dungeon-selector-probe failed: worker-create\r\n"); } } void StopCurrentDungeonSelectorProbe() { InterlockedExchange(&g_currentProbeStop, 1); if (g_currentProbeWorker) { WaitForSingleObject(g_currentProbeWorker, 1500); CloseHandle(g_currentProbeWorker); g_currentProbeWorker = nullptr; } RestoreCurrentSelectorProbe(); RestoreCurrentOwnerProbe( kCurrentOwnerManagerProbeRva, kCurrentOwnerManagerProbePrologue, kCurrentOwnerManagerProbePatchLength, &ProxyCurrentOwnerManagerProbe, &g_currentOwnerManagerProbeOriginal, "owner-manager-008A1170", &g_currentOwnerProbeInstalled); RestoreCurrentOwnerProbe( kCurrentNoticeBuilderProbeRva, kCurrentNoticeBuilderProbePrologue, kCurrentNoticeBuilderProbePatchLength, &ProxyCurrentNoticeBuilderProbe, &g_currentNoticeBuilderProbeOriginal, "notice-builder-027B65B0", &g_currentNoticeProbeInstalled); }