Initial commit: Complete open-source Decal rebuild
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>
This commit is contained in:
commit
d1442e3747
1382 changed files with 170725 additions and 0 deletions
327
Native/LauncherHookModern/LauncherHook.cpp
Normal file
327
Native/LauncherHookModern/LauncherHook.cpp
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue