All 5 phases of the open-source Decal rebuild: Phase 1: 14 decompiled .NET projects (Interop.*, Adapter, FileService, DecalUtil) Phase 2: 10 native DLLs rewritten as C# COM servers with matching GUIDs - DecalDat, DHS, SpellFilter, DecalInput, DecalNet, DecalFilters - Decal.Core, DecalControls, DecalRender, D3DService Phase 3: C++ shims for Inject.DLL (D3D9 hooking) and LauncherHook.DLL Phase 4: DenAgent WinForms tray application Phase 5: WiX installer and build script 25 C# projects building with 0 errors. Native C++ projects require VS 2022 + Windows SDK (x86). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
327 lines
11 KiB
C++
327 lines
11 KiB
C++
// LauncherHook.cpp - Modern implementation of LauncherHook.DLL for Decal
|
|
//
|
|
// This DLL is loaded into the Asheron's Call launcher/lobby process.
|
|
// It intercepts CreateProcessW to detect when client.exe is launched,
|
|
// then injects Inject.DLL into the suspended game process before resuming it.
|
|
//
|
|
// Exported functions:
|
|
// LauncherHookEnable - Install the CBT hook for injection into launcher
|
|
// LauncherHookDisable - Remove the CBT hook
|
|
// LauncherHookLauncherStartup - Called when launcher starts (no-op placeholder)
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <tchar.h>
|
|
#include <string>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared data section - persists across all processes that load this DLL
|
|
// ---------------------------------------------------------------------------
|
|
#pragma data_seg(".LHookDll")
|
|
static HHOOK g_hHook = NULL;
|
|
static HINSTANCE g_hInstance = NULL;
|
|
#pragma data_seg()
|
|
#pragma comment(linker, "/SECTION:.LHookDll,RWS")
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Import Address Table hooking infrastructure
|
|
// (Simplified from ApiHook.h for this single-purpose DLL)
|
|
// ---------------------------------------------------------------------------
|
|
#define MakePtr(cast, ptr, addValue) (cast)((DWORD_PTR)(ptr) + (DWORD_PTR)(addValue))
|
|
|
|
typedef BOOL(WINAPI* PFN_CreateProcessW)(
|
|
LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,
|
|
BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION);
|
|
|
|
static PFN_CreateProcessW g_pfnOrigCreateProcessW = NULL;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DLL injection via CreateRemoteThread + LoadLibraryW
|
|
// ---------------------------------------------------------------------------
|
|
static BOOL InjectDLL(HANDLE hProcess, const WCHAR* szDllPath)
|
|
{
|
|
SIZE_T cbPath = (wcslen(szDllPath) + 1) * sizeof(WCHAR);
|
|
|
|
// Allocate memory in target process for the DLL path string
|
|
LPVOID pRemotePath = VirtualAllocEx(hProcess, NULL, cbPath, MEM_COMMIT, PAGE_READWRITE);
|
|
if (!pRemotePath)
|
|
return FALSE;
|
|
|
|
// Write the DLL path into the target process
|
|
if (!WriteProcessMemory(hProcess, pRemotePath, szDllPath, cbPath, NULL))
|
|
{
|
|
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
|
|
return FALSE;
|
|
}
|
|
|
|
// Get LoadLibraryW address (same in all processes due to ASLR kernel32 sharing)
|
|
LPTHREAD_START_ROUTINE pfnLoadLibrary =
|
|
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
|
|
|
|
if (!pfnLoadLibrary)
|
|
{
|
|
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
|
|
return FALSE;
|
|
}
|
|
|
|
// Create remote thread that calls LoadLibraryW(dllPath)
|
|
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnLoadLibrary, pRemotePath, 0, NULL);
|
|
if (!hThread)
|
|
{
|
|
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
|
|
return FALSE;
|
|
}
|
|
|
|
// Wait for the remote LoadLibrary to complete
|
|
WaitForSingleObject(hThread, 10000);
|
|
|
|
CloseHandle(hThread);
|
|
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hooked CreateProcessW - intercepts game client launch
|
|
// ---------------------------------------------------------------------------
|
|
static BOOL WINAPI Hooked_CreateProcessW(
|
|
LPCWSTR lpApplicationName,
|
|
LPWSTR lpCommandLine,
|
|
LPSECURITY_ATTRIBUTES lpProcessAttributes,
|
|
LPSECURITY_ATTRIBUTES lpThreadAttributes,
|
|
BOOL bInheritHandles,
|
|
DWORD dwCreationFlags,
|
|
LPVOID lpEnvironment,
|
|
LPCWSTR lpCurrentDirectory,
|
|
LPSTARTUPINFOW lpStartupInfo,
|
|
LPPROCESS_INFORMATION lpProcessInformation)
|
|
{
|
|
static LONG s_recursion = 0;
|
|
|
|
// Check if this is launching client.exe / acclient.exe
|
|
bool bIsClient = false;
|
|
if (lpCommandLine)
|
|
{
|
|
std::wstring cmd(lpCommandLine);
|
|
// Case-insensitive search for client.exe in command line
|
|
for (auto& c : cmd) c = towlower(c);
|
|
bIsClient = (cmd.find(L"client.exe") != std::wstring::npos);
|
|
}
|
|
if (!bIsClient && lpApplicationName)
|
|
{
|
|
std::wstring app(lpApplicationName);
|
|
for (auto& c : app) c = towlower(c);
|
|
bIsClient = (app.find(L"client.exe") != std::wstring::npos);
|
|
}
|
|
|
|
if (InterlockedIncrement(&s_recursion) == 1 && bIsClient)
|
|
{
|
|
// Read the Inject.DLL path from registry
|
|
WCHAR szDllPath[MAX_PATH] = {};
|
|
HKEY hKey = NULL;
|
|
|
|
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Decal\\Agent",
|
|
0, KEY_READ, &hKey) == ERROR_SUCCESS)
|
|
{
|
|
DWORD dwSize = (MAX_PATH - 20) * sizeof(WCHAR);
|
|
if (RegQueryValueExW(hKey, L"AgentPath", NULL, NULL,
|
|
(LPBYTE)szDllPath, &dwSize) == ERROR_SUCCESS)
|
|
{
|
|
// Ensure trailing backslash
|
|
size_t len = wcslen(szDllPath);
|
|
if (len > 0 && szDllPath[len - 1] != L'\\')
|
|
wcscat_s(szDllPath, L"\\");
|
|
wcscat_s(szDllPath, L"Inject.dll");
|
|
}
|
|
RegCloseKey(hKey);
|
|
}
|
|
|
|
// Create the process in SUSPENDED state
|
|
PROCESS_INFORMATION localPI = {};
|
|
LPPROCESS_INFORMATION pPI = lpProcessInformation ? lpProcessInformation : &localPI;
|
|
|
|
BOOL bResult = g_pfnOrigCreateProcessW(
|
|
lpApplicationName, lpCommandLine,
|
|
lpProcessAttributes, lpThreadAttributes,
|
|
bInheritHandles,
|
|
dwCreationFlags | CREATE_SUSPENDED,
|
|
lpEnvironment, lpCurrentDirectory,
|
|
lpStartupInfo, pPI);
|
|
|
|
if (bResult)
|
|
{
|
|
// Only inject if Decal Agent window is running
|
|
if (FindWindowW(NULL, L"Decal Agent") != NULL && szDllPath[0] != L'\0')
|
|
{
|
|
InjectDLL(pPI->hProcess, szDllPath);
|
|
}
|
|
|
|
// Resume the process (unless caller originally wanted it suspended)
|
|
if (!(dwCreationFlags & CREATE_SUSPENDED))
|
|
{
|
|
ResumeThread(pPI->hThread);
|
|
}
|
|
}
|
|
|
|
InterlockedDecrement(&s_recursion);
|
|
return bResult;
|
|
}
|
|
else
|
|
{
|
|
// Not client.exe or recursive call - pass through
|
|
BOOL bResult = g_pfnOrigCreateProcessW(
|
|
lpApplicationName, lpCommandLine,
|
|
lpProcessAttributes, lpThreadAttributes,
|
|
bInheritHandles, dwCreationFlags,
|
|
lpEnvironment, lpCurrentDirectory,
|
|
lpStartupInfo, lpProcessInformation);
|
|
|
|
InterlockedDecrement(&s_recursion);
|
|
return bResult;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IAT patching - hook CreateProcessW in the current process
|
|
// ---------------------------------------------------------------------------
|
|
static PIMAGE_IMPORT_DESCRIPTOR FindImportDescriptor(HMODULE hModule, LPCSTR szImportMod)
|
|
{
|
|
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)hModule;
|
|
PIMAGE_NT_HEADERS pNT = MakePtr(PIMAGE_NT_HEADERS, pDOS, pDOS->e_lfanew);
|
|
|
|
if (pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
|
|
return NULL;
|
|
|
|
PIMAGE_IMPORT_DESCRIPTOR pImport = MakePtr(PIMAGE_IMPORT_DESCRIPTOR, pDOS,
|
|
pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
|
|
|
|
while (pImport->Name)
|
|
{
|
|
LPCSTR szName = MakePtr(LPCSTR, pDOS, pImport->Name);
|
|
if (_stricmp(szName, szImportMod) == 0)
|
|
return pImport;
|
|
pImport++;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static bool PatchIAT(HMODULE hTarget, LPCSTR szModule, LPCSTR szFunction,
|
|
void* pNewFunc, void** ppOrigFunc, bool bInstall)
|
|
{
|
|
PIMAGE_IMPORT_DESCRIPTOR pImport = FindImportDescriptor(hTarget, szModule);
|
|
if (!pImport)
|
|
return false;
|
|
|
|
PIMAGE_THUNK_DATA pOrigThunk = MakePtr(PIMAGE_THUNK_DATA, hTarget, pImport->OriginalFirstThunk);
|
|
PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA, hTarget, pImport->FirstThunk);
|
|
|
|
for (; pOrigThunk->u1.Function; pOrigThunk++, pRealThunk++)
|
|
{
|
|
if (pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
|
|
continue;
|
|
|
|
PIMAGE_IMPORT_BY_NAME pByName = MakePtr(PIMAGE_IMPORT_BY_NAME, hTarget, pOrigThunk->u1.AddressOfData);
|
|
if (pByName->Name[0] == '\0')
|
|
continue;
|
|
|
|
if (_stricmp((const char*)pByName->Name, szFunction) != 0)
|
|
continue;
|
|
|
|
// Found it - patch
|
|
MEMORY_BASIC_INFORMATION mbi;
|
|
VirtualQuery(pRealThunk, &mbi, sizeof(mbi));
|
|
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_READWRITE, &mbi.Protect);
|
|
|
|
if (bInstall)
|
|
{
|
|
if (ppOrigFunc)
|
|
*ppOrigFunc = (void*)pRealThunk->u1.Function;
|
|
pRealThunk->u1.Function = (DWORD_PTR)pNewFunc;
|
|
}
|
|
else if (ppOrigFunc && *ppOrigFunc)
|
|
{
|
|
pRealThunk->u1.Function = (DWORD_PTR)*ppOrigFunc;
|
|
}
|
|
|
|
DWORD dwOld;
|
|
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOld);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void InstallHooks(bool bInstall)
|
|
{
|
|
// Hook CreateProcessW in the main executable
|
|
HMODULE hExe = GetModuleHandle(NULL);
|
|
PatchIAT(hExe, "kernel32.dll", "CreateProcessW",
|
|
(void*)Hooked_CreateProcessW, (void**)&g_pfnOrigCreateProcessW, bInstall);
|
|
|
|
// Also try hooking in AsheronsCall.dll if loaded (legacy launcher)
|
|
HMODULE hAC = GetModuleHandleA("AsheronsCall.dll");
|
|
if (hAC)
|
|
{
|
|
PatchIAT(hAC, "kernel32.dll", "CreateProcessW",
|
|
(void*)Hooked_CreateProcessW, (void**)&g_pfnOrigCreateProcessW, bInstall);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CBT hook procedure - gets us loaded into the launcher process
|
|
// ---------------------------------------------------------------------------
|
|
static LRESULT CALLBACK CBTHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exported functions
|
|
// ---------------------------------------------------------------------------
|
|
extern "C" __declspec(dllexport) void __stdcall LauncherHookEnable()
|
|
{
|
|
if (!g_hHook)
|
|
{
|
|
g_hHook = SetWindowsHookExW(WH_CBT, CBTHookProc, g_hInstance, 0);
|
|
}
|
|
}
|
|
|
|
extern "C" __declspec(dllexport) void __stdcall LauncherHookDisable()
|
|
{
|
|
if (g_hHook)
|
|
{
|
|
UnhookWindowsHookEx(g_hHook);
|
|
g_hHook = NULL;
|
|
}
|
|
}
|
|
|
|
extern "C" __declspec(dllexport) void __stdcall LauncherHookLauncherStartup()
|
|
{
|
|
// Placeholder - called by DenAgent when launcher starts
|
|
// The actual hooking is done in DllMain when we detect the launcher process
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DllMain
|
|
// ---------------------------------------------------------------------------
|
|
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
|
|
{
|
|
switch (dwReason)
|
|
{
|
|
case DLL_PROCESS_ATTACH:
|
|
g_hInstance = hInstance;
|
|
DisableThreadLibraryCalls(hInstance);
|
|
|
|
// Install IAT hooks to intercept CreateProcessW
|
|
InstallHooks(true);
|
|
break;
|
|
|
|
case DLL_PROCESS_DETACH:
|
|
// Remove IAT hooks
|
|
InstallHooks(false);
|
|
break;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|