Low Level Keylogger Architecture
Case study of different keylogger implementations, how to implement them and their individual IOCs.
SetWindowHookEx
Majority of malware uses user32.dll!SetWindowHookEx to create a global hook event. this modifies an internal structure in win32k.sys.
Internally, SetWindowsHookEx is just a user-mode wrapper around NtUserSetWindowsHookEx (which itself wraps around zzzzNtUserSetWindowsHookEx) in win32k.sys. What happens after you call it depends on the hook type you request but the sequence is always the same four steps:
Validate and allocate a hook record
win32k.syscreates an internalHOOKstructure, fills in the filter type, module handle, thread/desktop IDs, and inserts the structure at the head of the global hook chain for that typeDecide whether the hook procedure must live in the target process
Low-level hooks (
WH_KEYBOARD_LL,WH_MOUSE_LL) – NO injection. – The system leaves the hook DLL in the original caller’s address space and simply delivers the event to that process via an internalWM_*message posted to its hidden “ghost” window.All other global hooks (
WH_KEYBOARD,WH_CBT,WH_GETMESSAGE, …) – YES injection required. – For every process that satisfies the filter (same desktop, matching bitness):In/before Vista:
win32kqueues an asynchronous load request tocsrss.exe, which in turn callsLoadLibraryExinside the target process.After Vista: The target process is added to a pending-load list inside
win32k; the first user-mode exit from kernel to that process takes the APC and callsLdrLoadDlldirectly. – The first time the target thread is about to return to user mode, the kernel APCs the loader, so the DLL’sDllMainruns in the context of the victim process.
Event routing at runtime When the monitored event occurs,
win32kwalks the hook chain inside the thread that owns the input queue.If the hook procedure lives in that process, the kernel calls the address inside the injected DLL.
If the procedure lives in another process, the kernel marshals the raw parameters into an internal message and posts it to the installing thread’s message queue.
Mandatory
CallNextHookExEach hook handler must callCallNextHookExor the chain breaks.
TLDR
Low-level hooks look stealthy because no foreign code is mapped, but they pin the installing thread and are trivially detected.
Regular global hooks achieve true code injection, but leave mapped DLL artefacts.
The hook chain is global per desktop.
IOCs
Hook in user32
Additional VAD entry
Mapped or on-disk DLL
NtUserSetWindowsHookEx / zzzzNtUserSetWindowsHookEx
Same as above but you're directly calling the lower-level function. Same IOCs, really.
Session boundary: raw-input registration is per-session, not per-desktop.
IOCs
Additional VAD entry (theoretical)
Mapped or on-disk DLL
NtUserRegisterRawInputDevices / RegisterRawInputDevices
Tells the window manager to deliver raw HID packets to one specific HWND.
Practical abuse scenario
Start a background thread
Create a zero-sized message-only window (
HWND_MESSAGE)Register keyboard raw-input with
RIDEV_INPUTSINKPump the message queue and parse
WM_INPUTExfil
Profit?
Because no hook is installed, this technique:
does not appear in
!hookleaves no cross-process DLL mapping
is invisible to most EDR hook-chain sensors
Kernel-mode implementation
Sets oplock
Validates parameters
Allocates kernel copy
Calls internal worker
Emits ETW event
Cleanup
IOCs
ETW event from
win32kfull.sysRaw-input requires desktop
Services must create hidden desktop or open
\Device\KeyboardClass0
Capturing current window's name
To filter interesting keystrokes.
GetWindowTextA
Extremely detected
Wraps
NtUserInternalGetWindowText
NtUserInternalGetWindowText
Low-level syscall
Defined in
win32u.dll
Last updated