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:

  1. Validate and allocate a hook record win32k.sys creates an internal HOOK structure, fills in the filter type, module handle, thread/desktop IDs, and inserts the structure at the head of the global hook chain for that type

  2. Decide 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 internal WM_* 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: win32k queues an asynchronous load request to csrss.exe, which in turn calls LoadLibraryEx inside 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 calls LdrLoadDll directly. – The first time the target thread is about to return to user mode, the kernel APCs the loader, so the DLL’s DllMain runs in the context of the victim process.

  3. Event routing at runtime When the monitored event occurs, win32k walks 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.

  4. Mandatory CallNextHookEx Each hook handler must call CallNextHookEx or 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

  1. Start a background thread

  2. Create a zero-sized message-only window (HWND_MESSAGE)

  3. Register keyboard raw-input with RIDEV_INPUTSINK

  4. Pump the message queue and parse WM_INPUT

  5. Exfil

  6. Profit?

Because no hook is installed, this technique:

  • does not appear in !hook

  • leaves no cross-process DLL mapping

  • is invisible to most EDR hook-chain sensors

Kernel-mode implementation

  1. Sets oplock

  2. Validates parameters

  3. Allocates kernel copy

  4. Calls internal worker

  5. Emits ETW event

  6. Cleanup

IOCs

  • ETW event from win32kfull.sys

  • Raw-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