服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
844 lines
31 KiB
C++
844 lines
31 KiB
C++
#include "90CN_dungeon_skill_gate_fix.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
|
|
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<DWORD>(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<DWORD>(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<unsigned char*>(
|
|
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<unsigned char*>(
|
|
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<unsigned char*>(
|
|
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<unsigned char*>(
|
|
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<uintptr_t>(
|
|
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<uintptr_t>(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<uintptr_t>(
|
|
GetModuleHandleW(nullptr));
|
|
const unsigned int callerRva = (base && caller >= base)
|
|
? static_cast<unsigned int>(caller - base) : 0;
|
|
|
|
unsigned int state2fc = 0xFFFFFFFFu;
|
|
uintptr_t manager = 0;
|
|
__try {
|
|
if (self) {
|
|
state2fc = static_cast<unsigned int>(
|
|
*reinterpret_cast<volatile unsigned char*>(self + 0x2FC));
|
|
manager = *reinterpret_cast<volatile uintptr_t*>(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<unsigned int>(self), callerRva,
|
|
state2fc, static_cast<unsigned int>(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<uintptr_t>(
|
|
GetModuleHandleW(nullptr));
|
|
const unsigned int callerRva = (base && caller >= base)
|
|
? static_cast<unsigned int>(caller - base) : 0;
|
|
unsigned int arg0 = 0xFFFFFFFFu;
|
|
unsigned int arg1 = 0xFFFFFFFFu;
|
|
unsigned int arg2 = 0xFFFFFFFFu;
|
|
__try {
|
|
if (originalStack) {
|
|
arg0 = *reinterpret_cast<volatile unsigned int*>(originalStack + 4);
|
|
arg1 = *reinterpret_cast<volatile unsigned int*>(originalStack + 8);
|
|
arg2 = *reinterpret_cast<volatile unsigned int*>(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<unsigned int>(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<unsigned char*>(
|
|
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<unsigned int>(rva));
|
|
WriteLogLine(line);
|
|
return false;
|
|
}
|
|
|
|
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
|
|
nullptr, patchLength + 5, MEM_COMMIT | MEM_RESERVE,
|
|
PAGE_EXECUTE_READWRITE));
|
|
if (!trampoline) return false;
|
|
memcpy(trampoline, target, patchLength);
|
|
trampoline[patchLength] = 0xE9;
|
|
*reinterpret_cast<int32_t*>(trampoline + patchLength + 1) =
|
|
static_cast<int32_t>(reinterpret_cast<intptr_t>(target + patchLength) -
|
|
reinterpret_cast<intptr_t>(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<int32_t*>(jump + 1) =
|
|
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
|
|
reinterpret_cast<intptr_t>(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<unsigned int>(rva),
|
|
static_cast<unsigned int>(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<unsigned char*>(
|
|
g_currentProbeBase + rva);
|
|
unsigned char jump[5] = { 0xE9 };
|
|
*reinterpret_cast<int32_t*>(jump + 1) =
|
|
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
|
|
reinterpret_cast<intptr_t>(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<unsigned int>(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<unsigned int>(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<unsigned char*>(
|
|
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<unsigned char*>(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<int32_t*>(trampoline +
|
|
kCurrentSelectorProbePatchLength + 1) = static_cast<int32_t>(
|
|
reinterpret_cast<intptr_t>(target + kCurrentSelectorProbePatchLength) -
|
|
reinterpret_cast<intptr_t>(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<int32_t*>(jump + 1) = static_cast<int32_t>(
|
|
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
|
|
reinterpret_cast<intptr_t>(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<int32_t*>(jump + 1) = static_cast<int32_t>(
|
|
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
|
|
reinterpret_cast<intptr_t>(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<unsigned char*>(
|
|
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<uintptr_t>(
|
|
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);
|
|
}
|