Skip to content
Please update to the latest release 0.77.2 to address Multiple CVEs.
Windows.Detection.Keylogger

Windows.Detection.Keylogger

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.

  • 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

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