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