{
	"id": "896e5a2a-b990-4ab3-ab55-df1946bfd414",
	"created_at": "2026-04-06T02:11:08.936119Z",
	"updated_at": "2026-04-10T13:11:28.988991Z",
	"deleted_at": null,
	"sha1_hash": "3075dd461777f3b4b1d4cedbed863d41dd52e4cd",
	"title": "Windows Keylogger Part 2: Defense against user-land",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1121184,
	"plain_text": "Windows Keylogger Part 2: Defense against user-land\r\nBy Published by Eye of Ra View all posts by Eye of Ra\r\nPublished: 2017-06-27 · Archived: 2026-04-06 01:48:38 UTC\r\nNow, this is the interesting part. Recall from part 1, I had showed you 4 hooking methods using in Windows user-mode and today we will analyze each of them for answering one question: how to detect it? Let’s see!\r\nWindows test machine:\r\nWindows 7 x86: version 6.1.7601.17514 Service Pack 1 Build 7601)\r\nntoskrnl.exe: 6.1.7601.17514 (win7sp1_rtm.101119-1850), md5:\r\n2088D9994332583EDB3C561DE31EA5AD\r\nwin32k.sys: 6.1.7601.17514 (win7sp1_rtm.101119-1850), md5:\r\n687464342342B933D6B7FAA4A907AF4C\r\n*All offset values and structures I used in this part are from test machine.\r\nWindows Hooking: SetWindowsHookEx\r\nWhen we register hook using SetWindowsHookEx, the system saves our hook procedure in a hook chain which is\r\na list of pointers. Because we can register many type of message WH_* so there will be a hook chain for each type\r\nof message. Therefore our targets are:\r\nThe location of hook chains in system’s memory (for WH_KEYBOARD, WH_KEYBOARD_LL message\r\ntype)\r\nHow to find the process name of the found hook\r\nFor the location of hook chain, I have a magic string:\r\n1\r\nnt!_ETHREAD + 0x0 =\u003e nt!_KTHREAD + 0x088 =\u003e nt!_TEB + 0x40 =\u003e win32k!tagTHREADINFO + 0xCC =\u003e\r\nwin32k!tagDESKTOPINFO + 0x10 =\u003e win32k!tagHOOK\r\nEvery structure is clear (thanks for windows symbols :d). Offset values are of my test machine and can be vary on\r\neach Windows version and build number (ntoskrnl and win32k.sys)\r\nFrom nt!_ETHREAD, it must be a GUID thread. We can get GUI thread from “explorer.exe” or create a thread\r\nfor your own.\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 1 of 12\n\nAt the end of magic string, we get a final location of all global hook chains in the system. This is a array pointers\r\nof tagHOOK with 16 entries, the index of array is the value of WH_* message type (actually index = WH_* + 1).\r\nIf entry is not NULL then we found a global hook chain.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\nkd\u003e dt win32k!tagHOOK\r\n+0x000 head : _THRDESKHEAD\r\n+0x014 phkNext : Ptr32 tagHOOK\r\n+0x018 iHook : Int4B\r\n+0x01c offPfn : Uint4B\r\n+0x020 flags : Uint4B\r\n+0x024 ihmod : Int4B\r\n+0x028 ptiHooked : Ptr32 tagTHREADINFO\r\n+0x02c rpdesk : Ptr32 tagDESKTOP\r\n+0x030 nTimeout : Pos 0, 7 Bits\r\n+0x030 fLastHookHung : Pos 7, 1 Bit\r\ndt win32k!_THRDESKHEAD\r\n+0x000 h : Ptr32 Void\r\n+0x004 cLockObj : Uint4B\r\n+0x008 pti : Ptr32 tagTHREADINFO\r\n+0x00c rpdesk : Ptr32 tagDESKTOP\r\n+0x010 pSelf : Ptr32 UChar\r\nFrom _THRDESKHEAD in tagHook we get tagTHREADINFO of the process that set the hook. So we can get\r\nprocess id then process name. Here is another magic string\r\n1 processIdOfHooker = PsGetProcessId(IoThreadToProcess((PETHREAD)(*pCurHook-\u003ehead.pti)));\r\nThe scan result:\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 2 of 12\n\nOkay, that’s all we need for hunting the global hook of Windows message. Oh, what about local hook? :d\r\nHere is the magic string for local hook:\r\n1\r\nnt!_ETHREAD + 0x0 =\u003e nt!_KTHREAD + 0x088 =\u003e nt!_TEB + 0x40 =\u003e win32k!tagTHREADINFO + 0x198\r\n=\u003e win32k!tagHOOK\r\nQuite similar to global hook but you can see the location of local hook chains is in tagTHREADINFO structure of\r\nprocess and it will be local at that process. The hook chains in tagDESKTOPINFO is global for all process in the\r\nsame desktop.\r\nWindows Polling\r\nWell, I actually don’t have any idea for scanning this type of hooking. Why? Because it reads directly keys state\r\nfrom the internal structure and seems there is no way to check who is reading that.\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 3 of 12\n\nHow about API hooking for GetAsyncKeyState(), GetKeyboardState()? Yes, we can detect with API hooking but I\r\ndon’t like it because a global API hooking for all processes in the system is not a good idea. Using API hooking,\r\nwe can check the frequency and range of checked keys for keylogging detection.\r\nRaw Input\r\nWe start analyzing with RegisterRawInputDevices() function in user32.dll. From this API, it will call\r\nNtUserRegisterRawInputDevices() in win32k.sys\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 4 of 12\n\nAfter some checks and synchronizes, we go to _RegisterRawInputDevices()\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 5 of 12\n\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 6 of 12\n\nThat’s quite clear at here. PsGetCurrentProcessWin32Process() return win32k!tagPROCESSINFO structure. It\r\ncheck something at offset 0x1A4, using windbg we have:\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\nkd\u003e dt win32k!tagPROCESSINFO\r\n+0x000 Process : Ptr32 _EPROCESS\r\n…\r\n+0x1a4 pHidTable : Ptr32 tagPROCESS_HID_TABLE\r\n+0x1a8 dwRegisteredClasses : Uint4B\r\n+0x1ac pvwplWndGCList : Ptr32 VWPL\r\nA pointer to win32k!tagPROCESS_HID_TABLE. Interesting!!!\r\nThe lines in range 20-34 validate the registration data (that will be called HID Request)\r\nThe lines in range 36-47 allocate HID Table if it not exist. That means if tagPROCESSINFO-\u003epHidTable is null,\r\nno raw input was be registered in this process.\r\nThe lines in range 48-71 set HID request into HID table\r\nThe remaining of function is update the flags and restart HID device\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 7 of 12\n\nLet’s see the function SetProcDeviceRequest()\r\nThe system allocates a HID Request and insert it to HID Table\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\nkd\u003e dt win32k!tagPROCESS_HID_TABLE\r\n+0x000 link : _LIST_ENTRY\r\n+0x008 InclusionList : _LIST_ENTRY\r\n+0x010 UsagePageList : _LIST_ENTRY\r\n+0x018 ExclusionList : _LIST_ENTRY\r\n+0x020 spwndTargetMouse : Ptr32 tagWND\r\n+0x024 spwndTargetKbd : Ptr32 tagWND\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 8 of 12\n\nThere are 3 lists of HID Request that were used for raw input: InclusionList, UsagePageList and ExclusionList. To\r\nwhich list will be inserted, it depends on dwFlags value of tagRAWINPUTDEVICE structure when we call\r\nRegisterRawInputDevices();\r\nWith keylogger, we using RIDEV_NOLEGACY | RIDEV_INPUTSINK flags therefore the list will be\r\nInclusionList.\r\nThe last structure we concerned is win32k!tagPROCESS_HID_REQUEST\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\nkd\u003e dt win32k!tagPROCESS_HID_REQUEST\r\n+0x000 link : _LIST_ENTRY\r\n+0x008 usUsagePage : Uint2B\r\n+0x00a usUsage : Uint2B\r\n+0x00c fSinkable : Pos 0, 1 Bit\r\n+0x00c fExSinkable : Pos 1, 1 Bit\r\n+0x00c fDevNotify : Pos 2, 1 Bit\r\n+0x00c fExclusiveOrphaned : Pos 3, 1 Bit\r\n+0x010 pTLCInfo : Ptr32 tagHID_TLC_INFO\r\n+0x010 pPORequest : Ptr32 tagHID_PAGEONLY_REQUEST\r\n+0x010 ptr : Ptr32 Void\r\n+0x014 spwndTarget : Ptr32 tagWND\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 9 of 12\n\nWe can see usUsagePage, usUsage and spwndTarget are the params in tagRAWINPUTDEVICE.\r\nBingo!!! For raw input detection we will:\r\n1. Enumerate all process in the system\r\n2. With each process, we will traverse pID -\u003e PEPROCESS -\u003e tagPROCESSINFO -\u003e\r\ntagPROCESS_HID_TABLE -\u003e tagPROCESS_HID_REQUEST\r\n3. If we found an entry with usUsagePage = 1 (generic desktop controls) and usUsage = 6 (keyboard) then\r\nthis process is using raw input keylog.\r\nThe scan result:\r\nDirect Input\r\nWhen checking direct input, I found some interesting signatures in the process registering the hook.\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 10 of 12\n\nWith MSIAfterburner.exe, we found some handles related to direct input (Mutant, Section, Key). From running\r\nthreads, we also found a thread of DINPUT8.dll (a library of Microsoft DirectInput).\r\nDone! For direct input detection we will:\r\n1. Enumerate all processes in the system\r\n2. With each process, enumerate all mutant, section, key that match the handle signatures.\r\n3. If all handle signatures matched, we get start address of all threads in that process. If start address is in the\r\naddress range of DINPUT8.DLL then we found the direct input keylog.\r\nThe scan result:\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 11 of 12\n\nConclusion\r\nWe have a summary table for scanning user-mode keylogger:\r\nScan method Scan from\r\nWindows Hooking (SetWindowsHookEx) Structure scanning Kernel-mode\r\nWindows Polling API hooking\r\nRaw Input Structure scanning Kernel-mode\r\nDirect Input Signature scanning User-mode (Admin required)\r\nSource: https://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nhttps://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://eyeofrablog.wordpress.com/2017/06/27/windows-keylogger-part-2-defense-against-user-land/"
	],
	"report_names": [
		"windows-keylogger-part-2-defense-against-user-land"
	],
	"threat_actors": [],
	"ts_created_at": 1775441468,
	"ts_updated_at": 1775826688,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/3075dd461777f3b4b1d4cedbed863d41dd52e4cd.pdf",
		"text": "https://archive.orkl.eu/3075dd461777f3b4b1d4cedbed863d41dd52e4cd.txt",
		"img": "https://archive.orkl.eu/3075dd461777f3b4b1d4cedbed863d41dd52e4cd.jpg"
	}
}