DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,843 @@
|
||||
#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);
|
||||
}
|
||||
Reference in new issue
Block a user