NanoCore: Raw Input Keylogging — Detection

NanoCore Image

NanoCore registers a raw input device for the keyboard with RIDEV_INPUTSINK, so it receives every keystroke even while it has no focus, and it never installs a hook. Hook-based keylogger detections that watch SetWindowsHookEx do not see this at all.

MITRE ATT&CK: T1056.001

Technique write-up: NanoCore_Keylogging

Detection Indicators

Indicator Value
Log path KB_<Environment.TickCount>.dat — under C:\Users\Public\ in this build, configuration-dependent
Naming literal KB_ prefix, decimal tick count, .dat extension
Location any user-writable staging directory — Public, %APPDATA%, %TEMP%, ProgramData
Window hidden — the form is minimised and Visible forced to false on every change
API user32!RegisterRawInputDevices, usage page 0x01, usage 0x06 (keyboard)
Flag RIDEV_INPUTSINK (0x00000100) — deliver input while unfocused
Message WM_INPUT (0x00FF) handled in WndProc
Context the focused window title is recorded alongside the keystrokes

The KB_<ticks>.dat name is shared by all three NanoCore collection TTPs, so one broadened file rule covers keystrokes, clipboard and DNS — but see the caveat below before weighting it.

⚠️ The output path is not a reliable indicator on its own. The recreated test case hardcodes C:\Users\Public\KB_<TickCount>.dat, but where NanoCore stages collected data is configuration-dependent — %APPDATA% is at least as common in the wild, and the naming convention changes between builds. The file rule below is deliberately broadened across every plausible staging directory, and it is supporting evidence. The API-level rules are what actually identify the technique.

Log Sources

Detection Engineering with Defender XDR

The log file is the reliable artefact; the API call itself is not audited.

// Supporting evidence, not proof - see the caveat above.
let staging = dynamic([@"\users\public\", @"\appdata\roaming\", @"\appdata\local\",
                       @"\programdata\", @"\windows\temp\"]);
DeviceFileEvents
| where tolower(FolderPath) has_any (staging)
| where FileName matches regex @"^KB_[0-9]+\.dat$"
      or (FileName endswith ".dat" and tolower(InitiatingProcessFolderPath) has_any (staging))
| project Timestamp, DeviceName, AccountName, ActionType, FileName, FolderPath,
          InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine
| order by Timestamp desc

Detection Engineering with Sysmon

Sysmon file-create on the same path, with the writing image.

WindowsEvent //(sysmon)
| where EventID == 11   // FileCreate
| extend TargetFilename = tostring(EventData.TargetFilename), Image = tostring(EventData.Image)
| where TargetFilename has_any (@"\Users\Public\", @"\AppData\", @"\ProgramData\")
| where TargetFilename endswith ".dat" or TargetFilename endswith ".log"
| where Image has_any (@"\AppData\", @"\Temp\", @"\Users\Public\")
| project TimeGenerated, Computer, Image, TargetFilename

Detection Engineering with SIGMA

Full rule: sigma_nanocore_collector_staging_file.yml

detection:
    selection_name:
        TargetFilename|re: '(?i)\\KB_[0-9]+\.dat$'
    selection_location:
        TargetFilename|contains:
            - '\Users\Public\'
            - '\AppData\Roaming\'
            - '\AppData\Local\'
            - '\ProgramData\'
    condition: all of selection_*

Detection Engineering with YARA

The API surface is the durable half. RegisterRawInputDevices paired with GetRawInputData and a hidden form is the technique regardless of where the output lands.

Rule: nanocore_rawinput_keylogger.yar

$api1 = "RegisterRawInputDevices"    $api2 = "GetRawInputData"
$sink = "RIDEV_INPUTSINK"            $struct = "RAWINPUTDEVICE"

Raw input registration produces no event at run time, so for live telemetry this is a scanning rule for files and process memory, not an alert source.

Notes and tuning

There is no event for RegisterRawInputDevices. Raw input registration is a user-mode call into user32 with no audit path, which is exactly why this technique is attractive. The output file is the detection.

Complementary signals worth pairing:

If you want prevention rather than detection, an application-control policy that stops unsigned binaries executing from %APPDATA% breaks the delivery chain earlier.

Disclaimer

For educational purposes only!!!