// 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 #include #include // --------------------------------------------------------------------------- // 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; }