This artifact is my attempt at implementing keylogger detection based on research presented by Asuka Nakajima at NULLCON using the Microsoft-Windows-Win32k ETW provider.
name: Windows.Detection.Keylogger
author: Zane Gittins
description: |
This artifact is my attempt at implementing keylogger detection based on research presented by [Asuka Nakajima at NULLCON](https://speakerdeck.com/asuna_jp/nullcon-goa-2025-windows-keylogger-detection-targeting-past-and-present-keylogging-techniques) using the Microsoft-Windows-Win32k ETW provider.
* Polling based keyloggers - Event id 1003 (GetAsyncKeyState) with MsSinceLastKeyEvent > 100 and BackgroundCallCount > 400
* Hooking based keyloggers - Event id 1002 (SetWindowsHookEx) with FilterType = WH_KEYBOARD_LL
* RawInput based keyloggers - Event id 1001 (RegisterRawInputDevices) with Flags = RIDEV_INPUT_SINK
# Can be CLIENT, CLIENT_EVENT, SERVER, SERVER_EVENT or NOTEBOOK
type: CLIENT_EVENT
parameters:
- name: ProcessExceptionsRegex
description: Except these processes.
type: string
default: "Explorer.exe"
export: |
LET SuspiciousEvents = SELECT *
FROM delay(query={
SELECT *
FROM watch_etw(guid="{8c416c79-d49b-4f01-a467-e56d3aa8234c}",
description="Microsoft-Windows-Win32k",
level=4,
any=5120)
WHERE (System.ID = 1003
AND atoi(string=EventData.MsSinceLastKeyEvent) > 100
AND atoi(string=EventData.BackgroundCallCount) > 400) OR (
System.ID = 1002
AND EventData.FilterType = "0xD") OR (System.ID = 1001
AND EventData.Flags = "256")
},
delay=5)
// On event id 1003 we must use EventData.PID as the process PID not System.ID.
LET EnrichEvents = SELECT *
FROM foreach(row=SuspiciousEvents,
query={
SELECT *
FROM if(condition=System.ID = 1003,
then={
SELECT timestamp(string=System.TimeStamp) AS Timestamp,
System.ID AS EventID,
EventData.PID AS Pid,
process_tracker_get(id=atoi(string=EventData.PID)).Data AS ProcInfo,
join(array=process_tracker_callchain(
id=atoi(string=EventData.PID)).Data.Name,
sep="->") AS CallChain
FROM scope()
},
else={
SELECT timestamp(string=System.TimeStamp) AS Timestamp,
System.ID AS EventID,
System.ProcessID AS Pid,
process_tracker_get(id=System.ProcessID).Data AS ProcInfo,
join(array=process_tracker_callchain(
id=System.ProcessID).Data.Name,
sep="->") AS CallChain
FROM scope()
})
})
WHERE NOT ProcInfo.Exe =~ ProcessExceptionsRegex
sources:
- precondition:
SELECT OS From info() where OS = 'windows' AND version(plugin="dedup") >= 0
query: |
SELECT *
FROM dedup(query={ SELECT * FROM EnrichEvents }, timeout=5, key="Pid")
- precondition:
SELECT OS From info() where OS = 'windows' AND version(plugin="dedup") = NULL
query: |
SELECT *
FROM EnrichEvents