From 54dba7ae2a7075081e7b778f8b3d1966b72f5dae Mon Sep 17 00:00:00 2001 From: Jonny_Surface Date: Mon, 16 Mar 2026 11:06:00 -0400 Subject: [PATCH] Adding WSLHook --- README.md | 1 + WSLHook/WSLHook.sln | 22 ++ WSLHook/WSLHook/WSLHook.vcxproj | 80 +++++ WSLHook/WSLHook/main.cpp | 556 ++++++++++++++++++++++++++++++++ 4 files changed, 659 insertions(+) create mode 100644 WSLHook/WSLHook.sln create mode 100644 WSLHook/WSLHook/WSLHook.vcxproj create mode 100644 WSLHook/WSLHook/main.cpp diff --git a/README.md b/README.md index 5413068..89e66ea 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,4 @@ This repository holds POCs I have created for projects, blogs, etc. | SuspendThreadDriver | Client for the vulnerable driver - gmer64.sys. Suspends threads given a ThreadId | | LDAPPatch | Patches EtwEventWrite so that LDAP calls don't get logged | | TokenActions | Provides various ways to impersonate tokens and to attempt to change token session ids | +| WSLHook | Leverages RTTI to hook ILxssUserSession::CreateLxProcess method | diff --git a/WSLHook/WSLHook.sln b/WSLHook/WSLHook.sln new file mode 100644 index 0000000..dcfbc7e --- /dev/null +++ b/WSLHook/WSLHook.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35222.181 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSLHook", "WSLHook\WSLHook.vcxproj", "{B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Release|ARM64 = Release|ARM64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6}.Debug|ARM64.Build.0 = Debug|ARM64 + {B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6}.Release|ARM64.ActiveCfg = Release|ARM64 + {B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6}.Release|ARM64.Build.0 = Release|ARM64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/WSLHook/WSLHook/WSLHook.vcxproj b/WSLHook/WSLHook/WSLHook.vcxproj new file mode 100644 index 0000000..2833f52 --- /dev/null +++ b/WSLHook/WSLHook/WSLHook.vcxproj @@ -0,0 +1,80 @@ + + + + + Debug + ARM64 + + + Release + ARM64 + + + + + 17.0 + {B3D7E8F9-1A2C-4D5E-9F80-A1B2C3D4E5F6} + WSLHook + 10.0 + + + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + Level3 + true + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + + + Windows + true + + + + + + Level3 + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + stdcpp17 + true + true + + + Windows + true + true + true + + + + + + + + + diff --git a/WSLHook/WSLHook/main.cpp b/WSLHook/WSLHook/main.cpp new file mode 100644 index 0000000..011a4dd --- /dev/null +++ b/WSLHook/WSLHook/main.cpp @@ -0,0 +1,556 @@ +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "rpcrt4.lib") + +// Types from wslservice.idl +// https://github.com/microsoft/WSL/blob/master/src/windows/wslservice.idl + +typedef enum _LxssHandleType +{ + LxssHandleConsole = 0, + LxssHandleInput = 1, + LxssHandleOutput = 2, +} LxssHandleType; + +typedef struct _LXSS_HANDLE +{ + ULONG Handle; + LxssHandleType HandleType; +} LXSS_HANDLE; + +typedef struct _LXSS_STD_HANDLES +{ + LXSS_HANDLE StdIn; + LXSS_HANDLE StdOut; + LXSS_HANDLE StdErr; +} LXSS_STD_HANDLES, *PLXSS_STD_HANDLES; + +typedef struct _LXSS_ERROR_INFO +{ + ULONG Flags; + ULONGLONG Context; + LPWSTR Message; + LPWSTR Warnings; + ULONG WarningsPipe; +} LXSS_ERROR_INFO, *PLXSS_ERROR_INFO; + +// CreateLxProcess function pointer type (matches vtable slot 16) +typedef HRESULT (STDMETHODCALLTYPE *CreateLxProcess_t)( + IUnknown* This, + const GUID* DistroGuid, + LPCSTR Filename, + ULONG CommandLineCount, + LPCSTR* CommandLine, + LPCWSTR CurrentWorkingDirectory, + LPCWSTR NtPath, + WCHAR* NtEnvironment, + ULONG NtEnvironmentLength, + LPCWSTR Username, + SHORT Columns, + SHORT Rows, + ULONG ConsoleHandle, + PLXSS_STD_HANDLES StdHandles, + ULONG Flags, + GUID* DistributionId, + GUID* InstanceId, + HANDLE* ProcessHandle, + HANDLE* ServerHandle, + HANDLE* StandardIn, + HANDLE* StandardOut, + HANDLE* StandardErr, + HANDLE* CommunicationChannel, + HANDLE* InteropSocket, + LXSS_ERROR_INFO* Error); + +// RTTI CompleteObjectLocator — 64-bit/ARM64 layout +struct RttiCompleteObjectLocator +{ + DWORD signature; // 1 on x64/ARM64 + DWORD offset; // 0 = primary vtable, >0 = secondary (multiple inheritance) + DWORD cdOffset; + INT32 pTypeDescriptor; // RVA to TypeDescriptor + INT32 pClassDescriptor; // RVA to ClassHierarchyDescriptor + INT32 pSelf; // RVA to this COL (for validation) +}; + +static void Log(const wchar_t* fmt, ...) +{ + wchar_t buf[2048]; + va_list va; + va_start(va, fmt); + vswprintf_s(buf, fmt, va); + va_end(va); + + OutputDebugStringW(buf); +} + +// +// LogCaller identifies the COM client via RPC call attributes and token impersonation +// +static void LogCaller() +{ + // Get client PID from the RPC runtime + RPC_CALL_ATTRIBUTES_V2_W callAttrs = {}; + callAttrs.Version = 2; + callAttrs.Flags = RPC_QUERY_CLIENT_PID; + + RPC_STATUS rpcStatus = RpcServerInqCallAttributesW(nullptr, &callAttrs); + if (rpcStatus == RPC_S_OK && callAttrs.ClientPID != 0) + { + DWORD clientPid = HandleToULong(callAttrs.ClientPID); + Log(L"[*] %-18s: %u\n", L"ClientPID", clientPid); + + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, clientPid); + if (hProc) + { + wchar_t imagePath[MAX_PATH] = {}; + DWORD sz = MAX_PATH; + if (QueryFullProcessImageNameW(hProc, 0, imagePath, &sz)) + { + const wchar_t* name = wcsrchr(imagePath, L'\\'); + Log(L"[*] %-18s: %s\n", L"ClientProcess", name ? name + 1 : imagePath); + Log(L"[*] %-18s: %s\n", L"ClientImagePath", imagePath); + } + CloseHandle(hProc); + } + } + + // Get caller SID and session via COM impersonation + if (SUCCEEDED(CoImpersonateClient())) + { + HANDLE hToken = nullptr; + if (OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken)) + { + BYTE tokenUserBuf[256] = {}; + DWORD needed = 0; + if (GetTokenInformation(hToken, TokenUser, tokenUserBuf, sizeof(tokenUserBuf), &needed)) + { + TOKEN_USER* tokenUser = reinterpret_cast(tokenUserBuf); + LPWSTR sidStr = nullptr; + if (ConvertSidToStringSidW(tokenUser->User.Sid, &sidStr)) + { + Log(L"[*] %-18s: %s\n", L"ClientSID", sidStr); + LocalFree(sidStr); + } + + wchar_t userName[256] = {}, domainName[256] = {}; + DWORD userLen = 256, domainLen = 256; + SID_NAME_USE sidType; + if (LookupAccountSidW(nullptr, tokenUser->User.Sid, userName, &userLen, domainName, &domainLen, &sidType)) + { + Log(L"[*] %-18s: %s\\%s\n", L"ClientUser", domainName, userName); + } + } + + DWORD sessionId = 0, needed2 = 0; + if (GetTokenInformation(hToken, TokenSessionId, &sessionId, sizeof(sessionId), &needed2)) { + Log(L"[*] %-18s: %u\n", L"ClientSessionId", sessionId); + } + + CloseHandle(hToken); + } + CoRevertToSelf(); + } +} + + +static void** g_pVtableSlot = nullptr; +static CreateLxProcess_t g_pfnOriginal = nullptr; + +// +// Logging function +// +static void LogGuid(const wchar_t* label, const GUID* g) +{ + if (!g) { + Log(L"[*] %-18s: (null)\n", label); + return; + } + Log(L"[*] %-18s: {%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\n", + label, g->Data1, g->Data2, g->Data3, + g->Data4[0], g->Data4[1], g->Data4[2], g->Data4[3], + g->Data4[4], g->Data4[5], g->Data4[6], g->Data4[7]); +} + +// +// HookCreateLxProcess is the detour function called instead of CreateLxProcess. +// It logs information and then calls the original CreateLxProcess function +// +HRESULT STDMETHODCALLTYPE HookCreateLxProcess( + IUnknown* This, + const GUID* DistroGuid, + LPCSTR Filename, + ULONG CommandLineCount, + LPCSTR* CommandLine, + LPCWSTR CurrentWorkingDirectory, + LPCWSTR NtPath, + WCHAR* NtEnvironment, + ULONG NtEnvironmentLength, + LPCWSTR Username, + SHORT Columns, + SHORT Rows, + ULONG ConsoleHandle, + PLXSS_STD_HANDLES StdHandles, + ULONG Flags, + GUID* DistributionId, + GUID* InstanceId, + HANDLE* ProcessHandle, + HANDLE* ServerHandle, + HANDLE* StandardIn, + HANDLE* StandardOut, + HANDLE* StandardErr, + HANDLE* CommunicationChannel, + HANDLE* InteropSocket, + LXSS_ERROR_INFO* Error) +{ + Log(L"\n[*] ========== CreateLxProcess ==========\n"); + + LogCaller(); + + // Log [in] parameters + LogGuid(L"DistroGuid", DistroGuid); + Log(L"[*] %-18s: %S\n", L"Filename", Filename ? Filename : "(null)"); + Log(L"[*] %-18s: %u\n", L"CommandLineCount", CommandLineCount); + for (ULONG i = 0; i < CommandLineCount; i++) { + Log(L"[*] Arg[%u] : %S\n", + i, (CommandLine && CommandLine[i]) ? CommandLine[i] : "(null)"); + } + Log(L"[*] %-18s: %s\n", L"CWD", CurrentWorkingDirectory ? CurrentWorkingDirectory : L"(null)"); + Log(L"[*] %-18s: %s\n", L"NtPath", NtPath ? NtPath : L"(null)"); + Log(L"[*] %-18s: %s\n", L"Username", Username ? Username : L"(null)"); + Log(L"[*] %-18s: %hdx%hd\n", L"Console Size", Columns, Rows); + Log(L"[*] %-18s: 0x%X\n", L"ConsoleHandle", ConsoleHandle); + Log(L"[*] %-18s: 0x%X\n", L"Flags", Flags); + Log(L"[*] %-18s: %u\n", L"NtEnvLength", NtEnvironmentLength); + + if (StdHandles) + { + Log(L"[*] %-18s: Handle=%u Type=%d\n", L"StdIn", StdHandles->StdIn.Handle, StdHandles->StdIn.HandleType); + Log(L"[*] %-18s: Handle=%u Type=%d\n", L"StdOut", StdHandles->StdOut.Handle, StdHandles->StdOut.HandleType); + Log(L"[*] %-18s: Handle=%u Type=%d\n", L"StdErr", StdHandles->StdErr.Handle, StdHandles->StdErr.HandleType); + } + + // Call the real implementation + HRESULT hr = g_pfnOriginal( + This, DistroGuid, Filename, CommandLineCount, CommandLine, + CurrentWorkingDirectory, NtPath, NtEnvironment, NtEnvironmentLength, + Username, Columns, Rows, ConsoleHandle, StdHandles, Flags, + DistributionId, InstanceId, ProcessHandle, ServerHandle, + StandardIn, StandardOut, StandardErr, CommunicationChannel, + InteropSocket, Error); + + // Log [out] parameters + Log(L"[*] --- out params (hr=0x%08X) ---\n", hr); + LogGuid(L"DistributionId", DistributionId); + LogGuid(L"InstanceId", InstanceId); + Log(L"[*] %-18s: %p\n", L"ProcessHandle", ProcessHandle ? *ProcessHandle : nullptr); + Log(L"[*] %-18s: %p\n", L"ServerHandle", ServerHandle ? *ServerHandle : nullptr); + Log(L"[*] %-18s: %p\n", L"StandardIn", StandardIn ? *StandardIn : nullptr); + Log(L"[*] %-18s: %p\n", L"StandardOut", StandardOut ? *StandardOut : nullptr); + Log(L"[*] %-18s: %p\n", L"StandardErr", StandardErr ? *StandardErr : nullptr); + Log(L"[*] %-18s: %p\n", L"CommunicationChan", CommunicationChannel ? *CommunicationChannel : nullptr); + Log(L"[*] %-18s: %p\n", L"InteropSocket", InteropSocket ? *InteropSocket : nullptr); + + if (Error && Error->Message) { + Log(L"[*] %-18s: %s\n", L"Error.Message", Error->Message); + } + if (Error && Error->Warnings) { + Log(L"[*] %-18s: %s\n", L"Error.Warnings", Error->Warnings); + } + + Log(L"[*] ==========================================\n\n"); + return hr; +} + +// --------------------------------------------------------------------------- +// RTTI vtable scanner +// +// Three-level scan: TypeDescriptor name -> CompleteObjectLocator -> vtable pointer +// Looks for COL.offset == 0 (primary vtable = the ILxssUserSession interface). +// --------------------------------------------------------------------------- + +struct ScanRegion { + uint8_t* start; + SIZE_T size; +}; + +// +// GetReadableSections collects non-executable, readable PE sections (.rdata, .data, etc.) +// +static int GetReadableSections(HMODULE hMod, ScanRegion* out, int maxOut) +{ + auto base = reinterpret_cast(hMod); + auto dos = reinterpret_cast(base); + auto nt = reinterpret_cast(base + dos->e_lfanew); + auto sec = IMAGE_FIRST_SECTION(nt); + int n = 0; + + for (WORD i = 0; i < nt->FileHeader.NumberOfSections && n < maxOut; i++, sec++) + { + DWORD ch = sec->Characteristics; + if (!(ch & IMAGE_SCN_MEM_READ)) { + continue; + } + if (ch & IMAGE_SCN_MEM_EXECUTE) { + continue; + } + + // Add to array of readable memory regions + out[n++] = { base + sec->VirtualAddress, sec->Misc.VirtualSize }; + } + return n; +} + +// +// FindVtableByRTTI finds the vtable address by searching for the TypeDescriptor name -> obtain COL -> finding vtable +// +static void** FindVtableByRTTI(HMODULE hMod, const char* classSubstr) +{ + auto base = reinterpret_cast(hMod); + + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) { + return nullptr; + } + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) { + return nullptr; + } + + ScanRegion regions[32]; + + // Only care if matches come from a readable section + int nRegions = GetReadableSections(hMod, regions, 32); + if (nRegions == 0) { + return nullptr; + } + + // TypeDescriptor: [+0x00] pVFTable, [+0x08] spare, [+0x10] name + static constexpr SIZE_T kNameOffset = 2 * sizeof(void*); + + void** primaryHit = nullptr; + + // 1. Finding TypeDescriptor by name + for (int r = 0; r < nRegions && !primaryHit; r++) + { + uint8_t* data = regions[r].start; + SIZE_T sz = regions[r].size; + + for (SIZE_T i = kNameOffset; i < sz && !primaryHit; i++) + { + const char* p = reinterpret_cast(data + i); + if (p[0] != '.' || p[1] != '?' || p[2] != 'A') { + continue; + } + if (!strstr(p, classSubstr)) { + continue; + } + + auto tdAddr = data + i - kNameOffset; + auto tdOff = static_cast(tdAddr - base); + if (tdOff > UINT32_MAX) { + continue; + } + auto tdOff32 = static_cast(tdOff); + + // TypeDescriptor found + Log(L"[+] TypeDescriptor \"%S\" @ %p (rva 0x%X)\n", p, tdAddr, tdOff32); + + // 2. Finding COL referencing this TypeDescriptor + for (int r2 = 0; r2 < nRegions; r2++) + { + uint8_t* d2 = regions[r2].start; + SIZE_T s2 = regions[r2].size; + + for (SIZE_T j = 0; j + sizeof(RttiCompleteObjectLocator) <= s2; j += 4) + { + auto col = reinterpret_cast(d2 + j); + if (col->signature != 1) { + continue; + } + if (col->pTypeDescriptor != tdOff32) { + continue; + } + + auto colOff = static_cast((d2 + j) - base); + if (colOff > UINT32_MAX) { + continue; + } + if (col->pSelf != static_cast(colOff)) { + continue; + } + + // COL address found + void* colAddr = d2 + j; + Log(L"[+] COL @ %p offset=%u\n", colAddr, col->offset); + + // 3. Finding pointer to COL (vtable[-1]) + for (int r3 = 0; r3 < nRegions; r3++) + { + uint8_t* d3 = regions[r3].start; + SIZE_T s3 = regions[r3].size; + + for (SIZE_T k = sizeof(void*); k + sizeof(void*) <= s3; k += sizeof(void*)) + { + void** slot = reinterpret_cast(d3 + k); + if (*slot != colAddr) { + continue; + } + + // Found vtable + void** vtable = slot + 1; + Log(L"[+] Vtable @ %p (COL.offset=%u)\n", vtable, col->offset); + + // Only care about primary vtable + if (col->offset == 0) { + primaryHit = vtable; + break; + } + } + if (primaryHit) { + break; + } + } + } + } + } + } + + if (!primaryHit) { + Log(L"[-] No vtable found for \"%S\"\n", classSubstr); + } + return primaryHit; +} + +// --------------------------------------------------------------------------- +// Vtable patching +// --------------------------------------------------------------------------- + +// +// PatchVtableEntry hooks CreateLxProcess +// +static bool PatchVtableEntry(void** vtable) +{ + HMODULE hMod = nullptr; + + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(vtable), &hMod); + + ULONGLONG base = reinterpret_cast(hMod); + ULONGLONG rva = reinterpret_cast(vtable) - base; + Log(L"[*] Vtable @ 0x%016llX (base=0x%016llX rva=0x%llX)\n", + reinterpret_cast(vtable), base, rva); + + // CreateLxProcess is the 16th (0-indexed) slot + g_pVtableSlot = &vtable[16]; + g_pfnOriginal = reinterpret_cast(vtable[16]); + + // Updating memory protection before setting hook + DWORD oldProtect = 0; + if (!VirtualProtect(g_pVtableSlot, sizeof(void*), PAGE_READWRITE, &oldProtect)) + { + Log(L"[-] VirtualProtect failed: %u\n", GetLastError()); + return false; + } + + *g_pVtableSlot = reinterpret_cast(&HookCreateLxProcess); + + // Restoring memory protection after setting hook + VirtualProtect(g_pVtableSlot, sizeof(void*), oldProtect, &oldProtect); + + Log(L"[+] Patched slot %d: original=%p detour=%p\n", + 16, g_pfnOriginal, &HookCreateLxProcess); + + return true; +} + +// +// UnpatchVtableEntry unhooks CreateLxProcess +// +static void UnpatchVtableEntry() +{ + if (!g_pVtableSlot || !g_pfnOriginal) { + return; + } + + DWORD old = 0; + + // Updating memory protection before removing hook + VirtualProtect(g_pVtableSlot, sizeof(void*), PAGE_READWRITE, &old); + *g_pVtableSlot = reinterpret_cast(g_pfnOriginal); + + // Updating memory protection after removing hook + VirtualProtect(g_pVtableSlot, sizeof(void*), old, &old); + Log(L"[*] Vtable restored\n"); +} + +// +// HookThreadProc is the function hook worker thread that finds the vtable and hooks function +// +static DWORD WINAPI HookThreadProc(LPVOID) +{ + Log(L"[*] Hook thread started (PID %u)\n", GetCurrentProcessId()); + + HMODULE hWsl = GetModuleHandleW(L"wslservice.exe"); + if (!hWsl) + { + Log(L"[-] GetModuleHandleW(\"wslservice.exe\") failed.\n"); + return 1; + } + + Log(L"[*] Scanning RTTI for \"LxssUserSession\"...\n"); + void** vtable = FindVtableByRTTI(hWsl, "LxssUserSession"); + + if (!vtable) + { + Log(L"[-] RTTI scan failed — vtable not found.\n"); + return 1; + } + + // Hooking Function + if (!PatchVtableEntry(vtable)) + { + return 1; + } + + Log(L"[*] Function Hook Ready\n"); + return 0; +} + + +BOOL WINAPI DllMain(HINSTANCE hInst, DWORD reason, LPVOID lpvReserved) +{ + switch (reason) + { + case DLL_PROCESS_ATTACH: + // Don't care about thread attach/detach + DisableThreadLibraryCalls(hInst); + + Log(L"---DLL_PROCESS_ATTACH---\n"); + Log(L"[*] DLL loaded into PID %u\n", GetCurrentProcessId()); + + // Creating hook thread + CloseHandle( + CreateThread(nullptr, 0, HookThreadProc, nullptr, 0, nullptr) + ); + break; + + case DLL_PROCESS_DETACH: + if (lpvReserved == nullptr) + { + // Unhooking function + UnpatchVtableEntry(); + } + break; + } + + return TRUE; +}