* [PATCH v2 0/1] HID: add malicious HID device detection driver
@ 2026-04-04 13:37 Zubeyr Almaho
2026-04-04 13:37 ` [PATCH v2 1/1] " Zubeyr Almaho
2026-04-05 5:31 ` [PATCH v2 0/1] HID: add malicious HID device detection driver Greg KH
0 siblings, 2 replies; 6+ messages in thread
From: Zubeyr Almaho @ 2026-04-04 13:37 UTC (permalink / raw)
To: Jiri Kosina
Cc: Zubeyr Almaho, Benjamin Tissoires, linux-input, linux-kernel,
security
Hi Jiri, Benjamin,
This series introduces hid-omg-detect, a passive HID monitor that scores
potentially malicious keyboard-like USB devices (BadUSB / O.MG style)
using:
- keystroke timing entropy,
- plug-and-type latency,
- USB descriptor fingerprinting.
When the configurable threshold is crossed, the module emits a warning
with a userspace mitigation hint (usbguard).
The driver does not block, delay, or modify HID input events.
Changes since v1:
- Replaced global list + mutex with per-device drvdata.
- Removed logging inside spinlock-held regions.
- Moved VID/PID lookup to probe() to avoid hot-path overhead.
- Switched logging to hid_{info,warn,err} helpers.
- Capped timing sample counter at MAX_TIMING_SAMPLES.
- Renamed file to hid-omg-detect.c for kernel naming conventions.
Thanks,
Zubeyr Almaho
---
drivers/hid/hid-omg-detect.c | 435 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 435 insertions(+)
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 1/1] HID: add malicious HID device detection driver 2026-04-04 13:37 [PATCH v2 0/1] HID: add malicious HID device detection driver Zubeyr Almaho @ 2026-04-04 13:37 ` Zubeyr Almaho 2026-04-07 7:59 ` Benjamin Tissoires 2026-04-05 5:31 ` [PATCH v2 0/1] HID: add malicious HID device detection driver Greg KH 1 sibling, 1 reply; 6+ messages in thread From: Zubeyr Almaho @ 2026-04-04 13:37 UTC (permalink / raw) To: Jiri Kosina Cc: Zubeyr Almaho, Benjamin Tissoires, linux-input, linux-kernel, security Add a passive HID driver that computes a suspicion score for keyboard-like USB devices based on: - low keystroke timing entropy, - immediate post-enumeration typing, - known suspicious VID/PID and descriptor anomalies. If the score exceeds a tunable threshold, the driver emits a warning and suggests userspace blocking (e.g. usbguard). The module never blocks or modifies HID events. Signed-off-by: Zubeyr Almaho <zybo1000@gmail.com> --- drivers/hid/hid-omg-detect.c | 435 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 drivers/hid/hid-omg-detect.c diff --git a/drivers/hid/hid-omg-detect.c b/drivers/hid/hid-omg-detect.c new file mode 100644 --- /dev/null +++ b/drivers/hid/hid-omg-detect.c @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * hid-omg-detect.c - Malicious HID Device Detection Kernel Module + * + * Detects O.MG Cable and similar BadUSB devices by combining: + * 1. Keystroke timing entropy analysis (human vs machine rhythm) + * 2. Plug-and-type detection (typing immediately after connect) + * 3. USB descriptor fingerprinting (known bad VID/PIDs + anomalies) + * + * When suspicion score >= threshold, logs a kernel warning and suggests + * blocking the device with usbguard. + * + * The driver is purely passive — it does not drop, modify, or delay any + * HID events. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/hid.h> +#include <linux/usb.h> +#include <linux/ktime.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/math64.h> + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Zübeyr Almaho <zybo1000@gmail.com>"); +MODULE_DESCRIPTION("O.MG Cable / Malicious HID Device Detector"); + +/* ============================================================ + * Tuneable parameters (pass as insmod args) + * ============================================================ */ +static int score_threshold = 70; +module_param(score_threshold, int, 0644); +MODULE_PARM_DESC(score_threshold, + "Suspicion score (0-100) to trigger alert (default: 70)"); + +static int plug_type_ms = 500; +module_param(plug_type_ms, int, 0644); +MODULE_PARM_DESC(plug_type_ms, + "Max ms after connect before first key = plug-and-type (default: 500)"); + +static int entropy_variance_low = 500; +module_param(entropy_variance_low, int, 0644); +MODULE_PARM_DESC(entropy_variance_low, + "Variance (us^2) below which typing is machine-like (default: 500)"); + +/* ============================================================ + * Constants + * ============================================================ */ +#define DRIVER_NAME "hid_omg_detect" +#define MAX_TIMING_SAMPLES 64 +#define MIN_SAMPLES 10 + +/* ============================================================ + * Known suspicious VID/PID pairs (O.MG Cable, Rubber Ducky, etc.) + * ============================================================ */ +static const struct { + u16 vid; + u16 pid; + const char *name; +} suspicious_ids[] = { + { 0x16c0, 0x27db, "O.MG Cable (classic)" }, + { 0x1209, 0xa000, "O.MG Cable (Elite)" }, + { 0x16c0, 0x27dc, "USB Rubber Ducky" }, + { 0x1b4f, 0x9208, "LilyPad Arduino BadUSB" }, +}; + +/* ============================================================ + * Per-device state (allocated on probe, freed on remove) + * ============================================================ */ +struct omg_device_state { + struct hid_device *hdev; + + /* --- connection timing --- */ + ktime_t connect_time; + ktime_t first_key_time; + bool first_key_seen; + + /* --- keystroke interval circular buffer --- */ + ktime_t last_key_time; + s64 intervals_us[MAX_TIMING_SAMPLES]; + unsigned int sample_count; + unsigned int sample_head; + + /* --- USB info (immutable after probe) --- */ + u16 vid; + u16 pid; + const char *vid_pid_match_name; /* non-NULL if VID/PID matched */ + bool descriptor_anomaly; + + /* --- verdict --- */ + int score; + bool alerted; + + spinlock_t lock; +}; + +/* ============================================================ + * Math helpers + * ============================================================ */ +static void timing_stats(struct omg_device_state *s, s64 *mean, s64 *variance) +{ + unsigned int i, n; + s64 sum = 0, sq = 0, m; + + n = min(s->sample_count, (unsigned int)MAX_TIMING_SAMPLES); + + if (n < 2) { + *mean = 0; + *variance = 0; + return; + } + + for (i = 0; i < n; i++) + sum += s->intervals_us[i]; + m = div_s64(sum, n); + + for (i = 0; i < n; i++) { + s64 d = s->intervals_us[i] - m; + + sq += d * d; + } + + *mean = m; + *variance = div_s64(sq, n); +} + +/* ============================================================ + * Signal 1: Timing entropy + * + * Humans: high variance (pauses, rhythm changes) + * Machines: low variance (constant clock) + * + * Also penalises extremely fast consistent typing (mean < 15 ms). + * ============================================================ */ +static int score_entropy(struct omg_device_state *s) +{ + s64 mean, var; + int pts = 0; + + if (s->sample_count < MIN_SAMPLES) + return 0; + + timing_stats(s, &mean, &var); + + if (var < (s64)entropy_variance_low) + pts += 35; + else if (var < (s64)entropy_variance_low * 10) + pts += 20; + else if (var < (s64)entropy_variance_low * 40) + pts += 8; + + if (mean > 0 && mean < 5000) + pts += 25; + else if (mean > 0 && mean < 15000) + pts += 10; + + return min(pts, 60); +} + +/* ============================================================ + * Signal 2: Plug-and-type + * + * A legitimate keyboard sits idle for a while after connect. + * A script device starts injecting within milliseconds. + * ============================================================ */ +static int score_plug_type(struct omg_device_state *s) +{ + s64 delta_ms; + + if (!s->first_key_seen) + return 0; + + delta_ms = ktime_to_ms(ktime_sub(s->first_key_time, s->connect_time)); + + if (delta_ms < 100) + return 30; + if (delta_ms < (s64)plug_type_ms) + return 15; + if (delta_ms < 2000) + return 5; + + return 0; +} + +/* ============================================================ + * Signal 3: USB descriptor fingerprint + * ============================================================ */ +static int score_descriptor(struct omg_device_state *s) +{ + int pts = 0; + + if (s->vid_pid_match_name) + pts += 50; + + if (s->descriptor_anomaly) + pts += 20; + + return min(pts, 50); +} + +/* ============================================================ + * Combine all signals and return per-signal breakdown. + * Caller is responsible for alerting outside any lock. + * ============================================================ */ +struct omg_score_result { + int entropy; + int plug_type; + int descriptor; + int total; + bool newly_alerted; +}; + +static void compute_score(struct omg_device_state *s, + struct omg_score_result *res) +{ + res->entropy = score_entropy(s); + res->plug_type = score_plug_type(s); + res->descriptor = score_descriptor(s); + res->total = min(res->entropy + res->plug_type + res->descriptor, + 100); + + s->score = res->total; + res->newly_alerted = false; + + if (res->total >= score_threshold && !s->alerted) { + s->alerted = true; + res->newly_alerted = true; + } +} + +/* Emit alert outside of spinlock */ +static void emit_alert(struct hid_device *hdev, + struct omg_device_state *s, + const struct omg_score_result *res) +{ + hid_warn(hdev, "=============================================\n"); + hid_warn(hdev, "!! SUSPICIOUS HID DEVICE DETECTED !!\n"); + hid_warn(hdev, "Device : %s\n", hdev->name); + hid_warn(hdev, "VID/PID: %04x:%04x\n", s->vid, s->pid); + if (s->vid_pid_match_name) + hid_warn(hdev, "Match : %s\n", s->vid_pid_match_name); + hid_warn(hdev, "Score : %d/100 (threshold=%d)\n", + res->total, score_threshold); + hid_warn(hdev, " Entropy : %d pts\n", res->entropy); + hid_warn(hdev, " Plug-and-type: %d pts\n", res->plug_type); + hid_warn(hdev, " Descriptor : %d pts\n", res->descriptor); + hid_warn(hdev, "Action : sudo usbguard block-device <id>\n"); + hid_warn(hdev, "=============================================\n"); +} + +/* ============================================================ + * HID raw_event hook — called for every incoming HID report + * + * This runs in softirq/BH context — no sleeping locks allowed. + * ============================================================ */ +static int omg_raw_event(struct hid_device *hdev, + struct hid_report *report, + u8 *data, int size) +{ + struct omg_device_state *s = hid_get_drvdata(hdev); + struct omg_score_result res; + ktime_t now; + unsigned long flags; + bool keystroke = false; + int i; + + if (!s) + return 0; + + if (report->type != HID_INPUT_REPORT) + return 0; + + /* Byte 0 = modifier, byte 1 = reserved, bytes 2-7 = keycodes */ + for (i = 2; i < size && i < 8; i++) { + if (data[i] != 0) { + keystroke = true; + break; + } + } + if (size > 0 && data[0] != 0) + keystroke = true; + + if (!keystroke) + return 0; + + now = ktime_get(); + + spin_lock_irqsave(&s->lock, flags); + + if (!s->first_key_seen) { + s->first_key_seen = true; + s->first_key_time = now; + s->last_key_time = now; + } else { + s64 interval = ktime_to_us(ktime_sub(now, s->last_key_time)); + + /* ignore idle gaps > 10 s */ + if (interval < 10000000LL) { + s->intervals_us[s->sample_head] = interval; + s->sample_head = + (s->sample_head + 1) % MAX_TIMING_SAMPLES; + if (s->sample_count < MAX_TIMING_SAMPLES) + s->sample_count++; + } + s->last_key_time = now; + } + + compute_score(s, &res); + + spin_unlock_irqrestore(&s->lock, flags); + + /* Alert outside the spinlock — hid_warn may sleep */ + if (res.newly_alerted) + emit_alert(hdev, s, &res); + + return 0; +} + +/* ============================================================ + * HID probe — device connected + * ============================================================ */ +static int omg_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + struct omg_device_state *s; + struct usb_interface *intf; + struct usb_device *udev; + struct omg_score_result res; + int ret, i; + + if (hdev->bus != BUS_USB) + return -ENODEV; + + s = kzalloc(sizeof(*s), GFP_KERNEL); + if (!s) + return -ENOMEM; + + spin_lock_init(&s->lock); + s->hdev = hdev; + s->connect_time = ktime_get(); + + /* Extract USB descriptor info */ + intf = to_usb_interface(hdev->dev.parent); + udev = interface_to_usbdev(intf); + s->vid = le16_to_cpu(udev->descriptor.idVendor); + s->pid = le16_to_cpu(udev->descriptor.idProduct); + + /* Check known bad VID/PID table (once, at probe time) */ + for (i = 0; i < ARRAY_SIZE(suspicious_ids); i++) { + if (s->vid == suspicious_ids[i].vid && + s->pid == suspicious_ids[i].pid) { + s->vid_pid_match_name = suspicious_ids[i].name; + break; + } + } + + /* + * Anomaly: legitimate HID keyboards use bDeviceClass = 0x00 + * (class defined at interface level). Any other value here + * for a device presenting as a keyboard is suspicious. + */ + s->descriptor_anomaly = + (udev->descriptor.bDeviceClass != 0x00 && + udev->descriptor.bDeviceClass != 0x03); + + hid_set_drvdata(hdev, s); + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "hid_parse failed: %d\n", ret); + goto err_free; + } + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + hid_err(hdev, "hid_hw_start failed: %d\n", ret); + goto err_free; + } + + hid_info(hdev, "Monitoring %s (VID=%04x PID=%04x%s%s)\n", + hdev->name, s->vid, s->pid, + s->vid_pid_match_name ? " KNOWN-BAD:" : "", + s->vid_pid_match_name ? s->vid_pid_match_name : ""); + + if (s->descriptor_anomaly) + hid_warn(hdev, "Descriptor anomaly: bDeviceClass=0x%02x\n", + udev->descriptor.bDeviceClass); + + /* Run initial descriptor-only score */ + compute_score(s, &res); + if (res.newly_alerted) + emit_alert(hdev, s, &res); + + return 0; + +err_free: + hid_set_drvdata(hdev, NULL); + kfree(s); + return ret; +} + +/* ============================================================ + * HID remove — device disconnected + * ============================================================ */ +static void omg_remove(struct hid_device *hdev) +{ + struct omg_device_state *s = hid_get_drvdata(hdev); + + hid_hw_stop(hdev); + + if (s) { + hid_info(hdev, "Removed %s (final score: %d/100)\n", + hdev->name, s->score); + hid_set_drvdata(hdev, NULL); + kfree(s); + } +} + +/* Match all USB HID devices — we inspect and score them all */ +static const struct hid_device_id omg_table[] = { + { HID_USB_DEVICE(HID_ANY_ID, HID_ANY_ID) }, + { } +}; +MODULE_DEVICE_TABLE(hid, omg_table); + +static struct hid_driver omg_driver = { + .name = DRIVER_NAME, + .id_table = omg_table, + .probe = omg_probe, + .remove = omg_remove, + .raw_event = omg_raw_event, +}; + +module_hid_driver(omg_driver); ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2 1/1] HID: add malicious HID device detection driver 2026-04-04 13:37 ` [PATCH v2 1/1] " Zubeyr Almaho @ 2026-04-07 7:59 ` Benjamin Tissoires 2026-06-19 7:36 ` [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection krishgulati7 0 siblings, 1 reply; 6+ messages in thread From: Benjamin Tissoires @ 2026-04-07 7:59 UTC (permalink / raw) To: Zubeyr Almaho; +Cc: Jiri Kosina, linux-input, linux-kernel, security Hi, On Apr 04 2026, Zubeyr Almaho wrote: > Add a passive HID driver that computes a suspicion score for keyboard-like > USB devices based on: > > - low keystroke timing entropy, > - immediate post-enumeration typing, > - known suspicious VID/PID and descriptor anomalies. > > If the score exceeds a tunable threshold, the driver emits a warning and > suggests userspace blocking (e.g. usbguard). The module never blocks or > modifies HID events. > > Signed-off-by: Zubeyr Almaho <zybo1000@gmail.com> As Greg said, a HID-BPF program would be far better in terms of scope because there are numerous pitfal in your implementation. The question will be how to notify userspace of the suspicious HID device in HID-BPF, but here, you are also just emitting a dmesg warning, so it's not doing much either. Anyway, comments inline, but please reach out to the end where I explain why the implementation can't work in it's current state. > --- > drivers/hid/hid-omg-detect.c | 435 ++++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 435 insertions(+) > create mode 100644 drivers/hid/hid-omg-detect.c > > diff --git a/drivers/hid/hid-omg-detect.c b/drivers/hid/hid-omg-detect.c > new file mode 100644 > --- /dev/null > +++ b/drivers/hid/hid-omg-detect.c > @@ -0,0 +1,435 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * hid-omg-detect.c - Malicious HID Device Detection Kernel Module > + * > + * Detects O.MG Cable and similar BadUSB devices by combining: > + * 1. Keystroke timing entropy analysis (human vs machine rhythm) > + * 2. Plug-and-type detection (typing immediately after connect) > + * 3. USB descriptor fingerprinting (known bad VID/PIDs + anomalies) > + * > + * When suspicion score >= threshold, logs a kernel warning and suggests > + * blocking the device with usbguard. > + * > + * The driver is purely passive — it does not drop, modify, or delay any > + * HID events. > + */ > + > +#include <linux/module.h> > +#include <linux/kernel.h> > +#include <linux/hid.h> > +#include <linux/usb.h> > +#include <linux/ktime.h> > +#include <linux/slab.h> > +#include <linux/spinlock.h> > +#include <linux/math64.h> > + > +MODULE_LICENSE("GPL"); > +MODULE_AUTHOR("Zübeyr Almaho <zybo1000@gmail.com>"); > +MODULE_DESCRIPTION("O.MG Cable / Malicious HID Device Detector"); > + > +/* ============================================================ > + * Tuneable parameters (pass as insmod args) > + * ============================================================ */ I'm pretty sure checkpatch.pl would complain about those multiline comments. > +static int score_threshold = 70; > +module_param(score_threshold, int, 0644); > +MODULE_PARM_DESC(score_threshold, > + "Suspicion score (0-100) to trigger alert (default: 70)"); > + > +static int plug_type_ms = 500; > +module_param(plug_type_ms, int, 0644); > +MODULE_PARM_DESC(plug_type_ms, > + "Max ms after connect before first key = plug-and-type (default: 500)"); > + > +static int entropy_variance_low = 500; > +module_param(entropy_variance_low, int, 0644); > +MODULE_PARM_DESC(entropy_variance_low, > + "Variance (us^2) below which typing is machine-like (default: 500)"); > + > +/* ============================================================ > + * Constants > + * ============================================================ */ > +#define DRIVER_NAME "hid_omg_detect" > +#define MAX_TIMING_SAMPLES 64 > +#define MIN_SAMPLES 10 > + > +/* ============================================================ > + * Known suspicious VID/PID pairs (O.MG Cable, Rubber Ducky, etc.) > + * ============================================================ */ > +static const struct { > + u16 vid; > + u16 pid; > + const char *name; > +} suspicious_ids[] = { > + { 0x16c0, 0x27db, "O.MG Cable (classic)" }, > + { 0x1209, 0xa000, "O.MG Cable (Elite)" }, > + { 0x16c0, 0x27dc, "USB Rubber Ducky" }, > + { 0x1b4f, 0x9208, "LilyPad Arduino BadUSB" }, > +}; > + > +/* ============================================================ > + * Per-device state (allocated on probe, freed on remove) > + * ============================================================ */ > +struct omg_device_state { > + struct hid_device *hdev; > + > + /* --- connection timing --- */ > + ktime_t connect_time; > + ktime_t first_key_time; > + bool first_key_seen; > + > + /* --- keystroke interval circular buffer --- */ > + ktime_t last_key_time; > + s64 intervals_us[MAX_TIMING_SAMPLES]; > + unsigned int sample_count; > + unsigned int sample_head; > + > + /* --- USB info (immutable after probe) --- */ > + u16 vid; > + u16 pid; > + const char *vid_pid_match_name; /* non-NULL if VID/PID matched */ > + bool descriptor_anomaly; > + > + /* --- verdict --- */ > + int score; > + bool alerted; > + > + spinlock_t lock; > +}; > + > +/* ============================================================ > + * Math helpers > + * ============================================================ */ > +static void timing_stats(struct omg_device_state *s, s64 *mean, s64 *variance) > +{ > + unsigned int i, n; > + s64 sum = 0, sq = 0, m; > + > + n = min(s->sample_count, (unsigned int)MAX_TIMING_SAMPLES); > + > + if (n < 2) { > + *mean = 0; > + *variance = 0; > + return; > + } > + > + for (i = 0; i < n; i++) > + sum += s->intervals_us[i]; > + m = div_s64(sum, n); > + > + for (i = 0; i < n; i++) { > + s64 d = s->intervals_us[i] - m; > + > + sq += d * d; > + } > + > + *mean = m; > + *variance = div_s64(sq, n); > +} > + > +/* ============================================================ > + * Signal 1: Timing entropy > + * > + * Humans: high variance (pauses, rhythm changes) > + * Machines: low variance (constant clock) > + * > + * Also penalises extremely fast consistent typing (mean < 15 ms). > + * ============================================================ */ > +static int score_entropy(struct omg_device_state *s) > +{ > + s64 mean, var; > + int pts = 0; > + > + if (s->sample_count < MIN_SAMPLES) > + return 0; > + > + timing_stats(s, &mean, &var); > + > + if (var < (s64)entropy_variance_low) > + pts += 35; > + else if (var < (s64)entropy_variance_low * 10) > + pts += 20; > + else if (var < (s64)entropy_variance_low * 40) > + pts += 8; > + > + if (mean > 0 && mean < 5000) > + pts += 25; > + else if (mean > 0 && mean < 15000) > + pts += 10; > + > + return min(pts, 60); > +} > + > +/* ============================================================ > + * Signal 2: Plug-and-type > + * > + * A legitimate keyboard sits idle for a while after connect. > + * A script device starts injecting within milliseconds. > + * ============================================================ */ > +static int score_plug_type(struct omg_device_state *s) > +{ > + s64 delta_ms; > + > + if (!s->first_key_seen) > + return 0; > + > + delta_ms = ktime_to_ms(ktime_sub(s->first_key_time, s->connect_time)); > + > + if (delta_ms < 100) > + return 30; > + if (delta_ms < (s64)plug_type_ms) > + return 15; > + if (delta_ms < 2000) > + return 5; > + > + return 0; > +} > + > +/* ============================================================ > + * Signal 3: USB descriptor fingerprint > + * ============================================================ */ > +static int score_descriptor(struct omg_device_state *s) > +{ > + int pts = 0; > + > + if (s->vid_pid_match_name) > + pts += 50; > + > + if (s->descriptor_anomaly) > + pts += 20; > + > + return min(pts, 50); > +} > + > +/* ============================================================ > + * Combine all signals and return per-signal breakdown. > + * Caller is responsible for alerting outside any lock. > + * ============================================================ */ > +struct omg_score_result { > + int entropy; > + int plug_type; > + int descriptor; > + int total; > + bool newly_alerted; > +}; > + > +static void compute_score(struct omg_device_state *s, > + struct omg_score_result *res) > +{ > + res->entropy = score_entropy(s); > + res->plug_type = score_plug_type(s); > + res->descriptor = score_descriptor(s); > + res->total = min(res->entropy + res->plug_type + res->descriptor, > + 100); > + > + s->score = res->total; > + res->newly_alerted = false; > + > + if (res->total >= score_threshold && !s->alerted) { > + s->alerted = true; > + res->newly_alerted = true; > + } > +} > + > +/* Emit alert outside of spinlock */ > +static void emit_alert(struct hid_device *hdev, > + struct omg_device_state *s, > + const struct omg_score_result *res) > +{ > + hid_warn(hdev, "=============================================\n"); > + hid_warn(hdev, "!! SUSPICIOUS HID DEVICE DETECTED !!\n"); > + hid_warn(hdev, "Device : %s\n", hdev->name); > + hid_warn(hdev, "VID/PID: %04x:%04x\n", s->vid, s->pid); > + if (s->vid_pid_match_name) > + hid_warn(hdev, "Match : %s\n", s->vid_pid_match_name); > + hid_warn(hdev, "Score : %d/100 (threshold=%d)\n", > + res->total, score_threshold); > + hid_warn(hdev, " Entropy : %d pts\n", res->entropy); > + hid_warn(hdev, " Plug-and-type: %d pts\n", res->plug_type); > + hid_warn(hdev, " Descriptor : %d pts\n", res->descriptor); > + hid_warn(hdev, "Action : sudo usbguard block-device <id>\n"); > + hid_warn(hdev, "=============================================\n"); > +} > + > +/* ============================================================ > + * HID raw_event hook — called for every incoming HID report > + * > + * This runs in softirq/BH context — no sleeping locks allowed. > + * ============================================================ */ > +static int omg_raw_event(struct hid_device *hdev, > + struct hid_report *report, > + u8 *data, int size) > +{ > + struct omg_device_state *s = hid_get_drvdata(hdev); > + struct omg_score_result res; > + ktime_t now; > + unsigned long flags; > + bool keystroke = false; > + int i; > + > + if (!s) > + return 0; > + > + if (report->type != HID_INPUT_REPORT) > + return 0; > + > + /* Byte 0 = modifier, byte 1 = reserved, bytes 2-7 = keycodes */ That's a strong assumption. This actually depend on the report descriptor of the device and what you said is generally true for USB keyboards, but not all of HID devices behave like that: - bluetooth keyboards usually have a report ID in byte 0 - some devices are reporting gyro data, so they are sending a stream of events which would likely be tagged as "suspicious" - some devices are reporting touch information, and as such they are also reporting a constant stream of events as long as at least one finger is touching the sensor - yubikeys, other security keys and gaming devices are devices specifically tailored to send a stream of events, being a keyboard macro, or a one time password, or a stored passwored in the key - not all devices are keyboards :) > + for (i = 2; i < size && i < 8; i++) { > + if (data[i] != 0) { > + keystroke = true; > + break; > + } > + } > + if (size > 0 && data[0] != 0) > + keystroke = true; > + > + if (!keystroke) > + return 0; > + > + now = ktime_get(); > + > + spin_lock_irqsave(&s->lock, flags); Why spin_locking? > + > + if (!s->first_key_seen) { > + s->first_key_seen = true; > + s->first_key_time = now; > + s->last_key_time = now; > + } else { > + s64 interval = ktime_to_us(ktime_sub(now, s->last_key_time)); > + > + /* ignore idle gaps > 10 s */ > + if (interval < 10000000LL) { > + s->intervals_us[s->sample_head] = interval; > + s->sample_head = > + (s->sample_head + 1) % MAX_TIMING_SAMPLES; > + if (s->sample_count < MAX_TIMING_SAMPLES) > + s->sample_count++; > + } > + s->last_key_time = now; > + } > + > + compute_score(s, &res); > + > + spin_unlock_irqrestore(&s->lock, flags); > + > + /* Alert outside the spinlock — hid_warn may sleep */ > + if (res.newly_alerted) > + emit_alert(hdev, s, &res); > + > + return 0; > +} > + > +/* ============================================================ > + * HID probe — device connected > + * ============================================================ */ > +static int omg_probe(struct hid_device *hdev, const struct hid_device_id *id) > +{ > + struct omg_device_state *s; > + struct usb_interface *intf; > + struct usb_device *udev; > + struct omg_score_result res; > + int ret, i; > + > + if (hdev->bus != BUS_USB) > + return -ENODEV; Why restricting to BUS_USB only? Bluetooth could be a strong acttack vector as well. > + > + s = kzalloc(sizeof(*s), GFP_KERNEL); devm_kzalloc? > + if (!s) > + return -ENOMEM; > + > + spin_lock_init(&s->lock); > + s->hdev = hdev; > + s->connect_time = ktime_get(); > + > + /* Extract USB descriptor info */ > + intf = to_usb_interface(hdev->dev.parent); You can't call that function unless you are sure that you are talking to ta USB device. If the user is using a uhid device (because they are replaying another), the transport driver is not usbhid, and you'll end up with a lot of issues. There is a function for checking if the transport is usb: hid_is_usb() > + udev = interface_to_usbdev(intf); > + s->vid = le16_to_cpu(udev->descriptor.idVendor); > + s->pid = le16_to_cpu(udev->descriptor.idProduct); > + > + /* Check known bad VID/PID table (once, at probe time) */ > + for (i = 0; i < ARRAY_SIZE(suspicious_ids); i++) { > + if (s->vid == suspicious_ids[i].vid && > + s->pid == suspicious_ids[i].pid) { > + s->vid_pid_match_name = suspicious_ids[i].name; > + break; > + } > + } > + > + /* > + * Anomaly: legitimate HID keyboards use bDeviceClass = 0x00 > + * (class defined at interface level). Any other value here > + * for a device presenting as a keyboard is suspicious. > + */ > + s->descriptor_anomaly = > + (udev->descriptor.bDeviceClass != 0x00 && > + udev->descriptor.bDeviceClass != 0x03); > + > + hid_set_drvdata(hdev, s); > + > + ret = hid_parse(hdev); > + if (ret) { > + hid_err(hdev, "hid_parse failed: %d\n", ret); > + goto err_free; > + } > + > + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); > + if (ret) { > + hid_err(hdev, "hid_hw_start failed: %d\n", ret); > + goto err_free; > + } > + > + hid_info(hdev, "Monitoring %s (VID=%04x PID=%04x%s%s)\n", > + hdev->name, s->vid, s->pid, > + s->vid_pid_match_name ? " KNOWN-BAD:" : "", > + s->vid_pid_match_name ? s->vid_pid_match_name : ""); > + > + if (s->descriptor_anomaly) > + hid_warn(hdev, "Descriptor anomaly: bDeviceClass=0x%02x\n", > + udev->descriptor.bDeviceClass); > + > + /* Run initial descriptor-only score */ > + compute_score(s, &res); > + if (res.newly_alerted) > + emit_alert(hdev, s, &res); > + > + return 0; > + > +err_free: > + hid_set_drvdata(hdev, NULL); > + kfree(s); no need to free when using devm_kzalloc. > + return ret; > +} > + > +/* ============================================================ > + * HID remove — device disconnected > + * ============================================================ */ > +static void omg_remove(struct hid_device *hdev) > +{ > + struct omg_device_state *s = hid_get_drvdata(hdev); > + > + hid_hw_stop(hdev); > + > + if (s) { > + hid_info(hdev, "Removed %s (final score: %d/100)\n", > + hdev->name, s->score); > + hid_set_drvdata(hdev, NULL); > + kfree(s); > + } > +} this omg_remove() function can be removed entirely when using devm_kzalloc. > + > +/* Match all USB HID devices — we inspect and score them all */ > +static const struct hid_device_id omg_table[] = { > + { HID_USB_DEVICE(HID_ANY_ID, HID_ANY_ID) }, ouch. Very much ouch: You are binding to any USB device, but you don't have implemented a .match callback, meaning that you are racing with any other driver to take the ownership of a USB device. This is where your plan falls completely. There can only be one HID driver attached to a specific HID device. And we have multiple HID drivers in the subsystem. So either they get their fixes (the original HID driver), either they'll get the new security driver but they can not have both. And even worse, there is no guarantee one driver will be loaded before the other. hid-generic is a special case as this is a fallback driver. So hid-generic is capable of unbinding itself to let device specific drivers to take over the device. But here, that driver just takes ownership and can't release it (otherwise it's pointless). So definitively, addressing that problem at the driver level is the wrong place: - either make it a HID core feature, but it'll be harder to sell IMO - either make it a separate module that HID core calls in: slightly easier to sell, but still hard - either work at the HID-BPF level: much better because you'll need some userspace eventually and this way you control from userpsace both the HID-BPF program and your userspace part I've recently added the parsed report descriptor injection during probe in the HID-BPF program (assuming you use udev-hid-bpf to load HID-BPF). So in theory, you can create a HID-BPF which would matches on keyboards, chose which bytes are interesting to look at and then bind on those *in addition* to any kernel driver. It happens before any kernel processing so you've got the raw event stream and can decide to block or not the stream. Anyway, even with the various issues I mentioned in the code, this present version can't be merged, as long as you are using a HID driver. Cheers, Benjamin > + { } > +}; > +MODULE_DEVICE_TABLE(hid, omg_table); > + > +static struct hid_driver omg_driver = { > + .name = DRIVER_NAME, > + .id_table = omg_table, > + .probe = omg_probe, > + .remove = omg_remove, > + .raw_event = omg_raw_event, > +}; > + > +module_hid_driver(omg_driver); > ^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection 2026-04-07 7:59 ` Benjamin Tissoires @ 2026-06-19 7:36 ` krishgulati7 2026-07-09 10:49 ` [RFC PATCH v2] " Krish Gulati 0 siblings, 1 reply; 6+ messages in thread From: krishgulati7 @ 2026-06-19 7:36 UTC (permalink / raw) To: bentiss; +Cc: linux-input, linux-kernel, jikos, Krish Gulati From: Krish Gulati <krishgulati7@gmail.com> Implements a HID-BPF struct_ops program that detects behaviorally anomalous keyboard input consistent with automated HID injection attacks (USB Rubber Ducky and similar). This is a direct response to Benjamin Tissoires's recommendation in the hid-omg-detect review thread [1], where a kernel driver approach was rejected on the grounds that a HID driver can only bind exclusively to a device, racing with and displacing the legitimate driver. The HID-BPF struct_ops approach resolves this: the program attaches alongside the existing driver, receives the raw event stream before kernel processing, and never takes exclusive ownership of the device. Two independent detection signals are implemented: Post-Enumeration Delay (PED): Measures the delta between device connection (recorded at probe() time via bpf_ktime_get_ns()) and the first input packet. Legitimate users take hundreds of milliseconds to react after plugging in a device; injection tools begin transmitting within 25-100ms. Events are classified into three bands: SUSPICIOUS (<50ms), WARNING (<300ms), and NORMAL (>=300ms). Thresholds are placeholder values and MUST be empirically calibrated before production use. Welford online variance (inter-keystroke timing): Tracks keydown-to-keydown intervals per device using Welford's single-pass online algorithm. BPF forbids floating-point, so the running mean is maintained in fixed-point (scaled by 1024) to prevent integer truncation at small counts. Intervals exceeding 10 seconds are treated as typing-session breaks and excluded from the variance calculation. Detection fires after HID_GUARD_MIN_SAMPLES (5) samples when variance falls below HID_GUARD_VARIANCE_THRESH_MS2 (1600 ms^2), flagging suspiciously metronomic timing inconsistent with human input. Per-device state (Welford accumulators, PED timestamp, report size) is maintained in a BPF_MAP_TYPE_LRU_HASH keyed by hid->id. The LRU eviction policy prevents map exhaustion at the cost of two documented trade-offs: eviction can silently reset Welford history (potential map-flood attack) and may clear connection_time (PED blindspot on device re-wake after eviction). Detection results are currently surfaced via bpf_printk(). A BPF_MAP_TYPE_RINGBUF interface with configurable userspace daemon is planned; deferred pending validation of detection heuristics. Signal design is grounded in: Neuner et al., "USBlock: Blocking USB-based Keylogger Attacks", DBSec 2018. [1] https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/ Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/ Signed-off-by: Krish Gulati <krishgulati7@gmail.com> --- src/bpf/testing/0010-Generic__keyboard.bpf.c | 285 +++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/bpf/testing/0010-Generic__keyboard.bpf.c diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c new file mode 100644 index 0000000..9114587 --- /dev/null +++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include "hid_bpf.h" +#include "hid_bpf_helpers.h" +#include "hid_report_helpers.h" +#include "hid_usages.h" +#include "vmlinux.h" +#include <bpf/bpf_tracing.h> + +#define HID_GUARD_NSEC_PER_MSEC 1000000ULL +/* + * Post-enumeration delay thresholds (raw nanoseconds, matching + * connection_time and first_packet_time which are both __u64 + * bpf_ktime_get_ns() values). + * + * LIMITATIONS: placeholder values based on rough figures from USB + * Rubber Ducky DETECT_READY research (modern hosts HID-ready in + * ~25-100ms) versus typical human reaction time after device + * connection (hundreds of ms to low seconds). NOT derived from a + * controlled study of this specific signal and MUST be empirically + * calibrated against real device/host combinations before any + * production use. Treat as a tunable, not a validated security + * boundary. + */ +#define HID_GUARD_PED_SUSPICIOUS_THRESH (50 * HID_GUARD_NSEC_PER_MSEC) +#define HID_GUARD_PED_WARNING_THRESH (300 * HID_GUARD_NSEC_PER_MSEC) + +/*not derived from real typing data yet*/ +#define HID_GUARD_MIN_SAMPLES 5 + +/* + * Variance is "too metronomic + * to be a human," expressed in ms^2 so we never need sqrt(). + */ +#define HID_GUARD_VARIANCE_THRESH_MS2 (40ULL * 40ULL) + +/* + * Idle gaps this large are treated as a break in the typing session, + * not a sample — interval excluded from Welford, timer rebased. + * Borrowed threshold from hid-omg-detect (10s); not independently + * validated for this signal. + */ +#define HID_GUARD_IDLE_GAP_THRESH_MS (10ULL * 1000ULL) + +/* + * fixed-point factor: BPF has no floating values, so mean is stored as + * real_mean * HID_GUARD_WELFORD_SCALE. + * Without this, delta/count truncates to 0 + * as soon as count exceeds the typical delta (in millisecond) + */ +#define HID_GUARD_WELFORD_SCALE 1024 + +enum hid_guard_ped_flag { + HID_GUARD_PED_NO_ENTRY = 0, + HID_GUARD_PED_SUSPICIOUS = 1, + HID_GUARD_PED_WARNING = 2, + HID_GUARD_PED_NORMAL = 3, +}; + +struct dev_info { + __u64 connection_time; + __u32 report_size; + __u64 prev_report[32]; + /*welford's variables*/ + __u64 prev_keydown_ts; + __u64 count; + __s64 mean; + __u64 M2; + /* + * unsigned as delta * delta2 is always >= 0 + * (as delta2 is delta scaled by the same sign) + */ +}; + +/* + * BPF_MAP_TYPE_LRU_HASH: + * Using an LRU map automatically prevents exhaustion by silently evicting + * the oldest idle devices. It requires no syntax changes to the rest of the + * code (lookup/update helpers work identically), but introduces behavioral + * trade-offs: + * + * 1. Eviction wipes Welford variance history. An attacker + * could theoretically flood the map to flush their device and reset their + * score. + * 2. PED Blindspot: Eviction deletes the 'connection_time' set during probe(). + * If an evicted device wakes up, it will bypass post-enumeration delay + * checks. + */ +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __type(key, __u32); + __type(value, struct dev_info); + __uint(max_entries, 128); +} dev_details SEC(".maps"); + +/* + * POST ENUMERATION DELAY + * + * Computes the delta between the first input packet timestamp and + * connection_time (set at probe() time), both raw __u64 nanoseconds + * from bpf_ktime_get_ns(). Called once per device, the caller gates + * this on connection_time != 0, and this function clears it to 0 + * afterward so it never runs twice for the same device. + * + * Returns a hid_guard_ped_flag classifying the delay. + */ +static __always_inline enum hid_guard_ped_flag +post_enumeration_delay(struct dev_info *info, __u64 first_packet_time) +{ + __u64 delta; + + if (first_packet_time < info->connection_time) + return HID_GUARD_PED_NO_ENTRY; + + delta = first_packet_time - info->connection_time; + + if (delta < HID_GUARD_PED_SUSPICIOUS_THRESH) + return HID_GUARD_PED_SUSPICIOUS; + if (delta < HID_GUARD_PED_WARNING_THRESH) + return HID_GUARD_PED_WARNING; + + return HID_GUARD_PED_NORMAL; +} + +static __always_inline void welford(struct dev_info *dev_state, + __u64 interval_ms) +{ + __s64 x = (__s64)interval_ms * HID_GUARD_WELFORD_SCALE; + __s64 delta, delta2, delta_abs, signed_div_res; + __u64 div_res; + + dev_state->count++; + delta = x - dev_state->mean; + delta_abs = delta < 0 ? -delta : delta; + div_res = (__u64)delta_abs / (__u64)dev_state->count; + signed_div_res = delta < 0 ? -(__s64)div_res : (__s64)div_res; + + dev_state->mean += signed_div_res; + delta2 = x - dev_state->mean; + + dev_state->M2 += (__u64)(delta * delta2); + + bpf_printk("W[Count:%llu] Int:%llu ms, scaled_x:%lld\n", + dev_state->count, interval_ms, x); + bpf_printk(" -> delta1:%lld, mean:%lld\n", delta, dev_state->mean); + bpf_printk(" -> delta2:%lld, M2:%llu\n", delta2, dev_state->M2); +} + +HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY), + HID_DEVICE(BUS_BLUETOOTH, HID_GROUP_ANY, HID_VID_ANY, + HID_PID_ANY)); + +SEC(HID_BPF_DEVICE_EVENT) +int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx) +{ + __u32 hid_id = hctx->hid->id; + + struct dev_info *info; + + info = bpf_map_lookup_elem(&dev_details, &hid_id); + + if (!info) + return 0; + + __u32 size = info->report_size; + __u32 fetch_size; + __u8 *data; + + if (size <= 8) + fetch_size = 8; + else if (size <= 16) + fetch_size = 16; + else + fetch_size = 32; + + data = hid_bpf_get_data(hctx, 0, fetch_size); + + if (!data) + return 0; + int now_active_ks = 0, was_active_ks = 0; +#pragma unroll + for (int i = 0; i < 32; i++) { + if (i >= fetch_size) + break; + now_active_ks += ((__u8)data[i] + 255) >> 8; + was_active_ks += ((__u8)info->prev_report[i] + 255) >> 8; + + info->prev_report[i] = data[i]; + } + + __u64 current_ms = bpf_ktime_get_ns() / HID_GUARD_NSEC_PER_MSEC; + + if (now_active_ks > was_active_ks) { + if (info->prev_keydown_ts != 0) { + __u64 interval_ms = current_ms - info->prev_keydown_ts; + + if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) { + welford(info, interval_ms); + } else { + bpf_printk( + "hid %d: idle gap %llu ms excluded from sample\n", + hid_id, interval_ms); + } + } + + if (info->count >= HID_GUARD_MIN_SAMPLES) { + __u64 variance_m2 = + info->M2 / + ((__u64)HID_GUARD_WELFORD_SCALE * + HID_GUARD_WELFORD_SCALE * (info->count - 1)); + /* + * the initial interval x was multiplied by HID_GUARD_WELFORD_SCALE, both + * delta and delta2 are also scaled by that factor, + * thus scale^2 in the denominator + */ + if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) { + bpf_printk( + "hid %d: Suspeciously regular typing, variance=%llu ms^2\n", + hid_id, variance_m2); + } + } + + info->prev_keydown_ts = current_ms; + } + + if (info->connection_time != 0) { + enum hid_guard_ped_flag ped_flag = + post_enumeration_delay(info, bpf_ktime_get_ns()); + + bpf_printk("PED flag for hid %d: %d\n", hid_id, ped_flag); + + /* + * Prevent re-evaluation on subsequent packets for this device + */ + info->connection_time = 0; + } + return 0; +} + +HID_BPF_OPS(hook_keyboard) = { + .hid_device_event = (void *)kdb_hook, +}; + +struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR; + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + struct hid_rdesc_report *input; + struct hid_rdesc_field *field; + struct hid_rdesc_collection *col; + + hid_bpf_for_each_input_report(&HID_REPORT_DESCRIPTOR, input) { + __u32 size_in_bytes = (input->size_in_bits + 7) / 8; + + bpf_printk("Report size: %d\n", size_in_bytes); + if (input->report_id != 0) + size_in_bytes += 1; + + bpf_printk("Report size after report_id: %d\n", size_in_bytes); + + hid_bpf_for_each_field(input, field) { + hid_bpf_for_each_collection(field, col) { + if (col->usage_page == + HidUsagePage_GenericDesktop && + col->usage_id == HidUsage_GD_Keyboard) { + __u32 key = ctx->hid; + struct dev_info info = { + .connection_time = + bpf_ktime_get_ns(), + .count = 0, + .report_size = size_in_bytes + }; + bpf_map_update_elem(&dev_details, &key, + &info, BPF_ANY); + ctx->retval = 0; + return 0; + } + } + } + } + ctx->retval = -EINVAL; + return 0; +} + +char _license[] SEC("license") = "GPL"; -- 2.54.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH v2] HID: BPF: add keyboard behavioral anomaly detection 2026-06-19 7:36 ` [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection krishgulati7 @ 2026-07-09 10:49 ` Krish Gulati 0 siblings, 0 replies; 6+ messages in thread From: Krish Gulati @ 2026-07-09 10:49 UTC (permalink / raw) To: bentiss, jikos; +Cc: linux-input, linux-kernel, Krish Gulati This patch implements a HID-BPF struct_ops program that detects automated HID injection attacks. It does this by measuring post-enumeration delay and tracking inter-keystroke timing using Welford's online variance algorithm. State is stored in a hash map keyed by the HID ID. Detection results are currently surfaced via bpf_printk(). A BPF_MAP_TYPE_RINGBUF interface with configurable userspace daemon is planned; deferred pending validation of detection heuristics. Signal design is grounded in: Neuner et al., "USBlock: Blocking USB-based Keylogger Attacks", DBSec 2018. Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/ Changes since v1: - Added bpf_spin_lock/unlock around the Welford compound update in kbd_hook() to close a data race on concurrent execution (was lockless read-modify-write). - Switched dev_details from LRU_HASH to plain HASH so eviction under map pressure fails the insert instead of silently displacing an existing tracked device. - Store report_id in dev_info and gate kbd_hook() on it, so composite devices no longer misattribute other report types' bytes as keystrokes. - Zero dev_info with __builtin_memset before populating in probe() to satisfy the verifier's stack-init tracking (was relying on a partial designated initializer). - Replaced active-byte-count heuristic with bytewise diff/mask (standard_boot_keypress/nkro_keypress) so consecutive keystrokes without an intervening empty report are still detected. - Verified bucket-size rounding (8/16/32/64) does not drop events or read out-of-bounds, tested via hid-replay with synthetic report sizes (9, 14 bytes, etc.). Known limitations, not addressed in this version: - No cleanup on device disconnect: hid_bpf_ops has no disconnect hook, so dev_details entries persist until the map fills. Deferred pending maintainer guidance on the right approach. - find_keyboard_field() returns on the first Keyboard-typed (GenericDesktop/Keyboard) Application Collection found; a device with multiple such collections within a single report descriptor (as opposed to multiple HID interfaces, which are already handled correctly and verified on real composite hardware) will only have the first one tracked. Untested; the only hardware available for testing exhibits the multiple-interface pattern rather than the single-descriptor multi-collection pattern, so this path has not been exercised. Signed-off-by: Krish Gulati <krishgulati7@gmail.com> Suggested-by: Sashiko-bot <sashiko-bot@kernel.org> --- src/bpf/testing/0010-Generic__keyboard.bpf.c | 311 +++++++++++++------ src/bpf/testing/meson.build | 1 + 2 files changed, 217 insertions(+), 95 deletions(-) diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c index 9114587..c3f2364 100644 --- a/src/bpf/testing/0010-Generic__keyboard.bpf.c +++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c @@ -24,11 +24,11 @@ #define HID_GUARD_PED_SUSPICIOUS_THRESH (50 * HID_GUARD_NSEC_PER_MSEC) #define HID_GUARD_PED_WARNING_THRESH (300 * HID_GUARD_NSEC_PER_MSEC) -/*not derived from real typing data yet*/ +/*not derived from real typing raw_buffer yet*/ #define HID_GUARD_MIN_SAMPLES 5 /* - * Variance is "too metronomic + * Variance is "too metronomic" * to be a human," expressed in ms^2 so we never need sqrt(). */ #define HID_GUARD_VARIANCE_THRESH_MS2 (40ULL * 40ULL) @@ -57,9 +57,17 @@ enum hid_guard_ped_flag { }; struct dev_info { - __u64 connection_time; + struct bpf_spin_lock lock; + __u8 report_id; + __u8 field_type; + __u8 prev_report[64]; + __u16 bits_start; + __u16 bits_end; + __u16 usage_id; __u32 report_size; - __u64 prev_report[32]; + __u32 keyboard_offset; + __u64 connection_time; + __u64 raw_buffer_length; /*welford's variables*/ __u64 prev_keydown_ts; __u64 count; @@ -71,22 +79,8 @@ struct dev_info { */ }; -/* - * BPF_MAP_TYPE_LRU_HASH: - * Using an LRU map automatically prevents exhaustion by silently evicting - * the oldest idle devices. It requires no syntax changes to the rest of the - * code (lookup/update helpers work identically), but introduces behavioral - * trade-offs: - * - * 1. Eviction wipes Welford variance history. An attacker - * could theoretically flood the map to flush their device and reset their - * score. - * 2. PED Blindspot: Eviction deletes the 'connection_time' set during probe(). - * If an evicted device wakes up, it will bypass post-enumeration delay - * checks. - */ struct { - __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(type, BPF_MAP_TYPE_HASH); __type(key, __u32); __type(value, struct dev_info); __uint(max_entries, 128); @@ -138,11 +132,29 @@ static __always_inline void welford(struct dev_info *dev_state, delta2 = x - dev_state->mean; dev_state->M2 += (__u64)(delta * delta2); +} - bpf_printk("W[Count:%llu] Int:%llu ms, scaled_x:%lld\n", - dev_state->count, interval_ms, x); - bpf_printk(" -> delta1:%lld, mean:%lld\n", delta, dev_state->mean); - bpf_printk(" -> delta2:%lld, M2:%llu\n", delta2, dev_state->M2); +static __always_inline bool standard_boot_keypress(__u8 *current_report, + __u8 *prev_report) +{ + for (int i = 2; i < 8; i++) { + if (current_report[i] != prev_report[i]) + return 1; + } + return 0; +} + +static __always_inline bool +nkro_keypress(__u8 *current_report, __u8 *prev_report, __u64 buffer_length) +{ + for (int i = 0; i < 64; i++) { + if (i >= buffer_length) + break; + + if (current_report[i] & ~prev_report[i]) + return 1; + } + return 0; } HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY), @@ -150,7 +162,7 @@ HID_BPF_CONFIG(HID_DEVICE(BUS_USB, HID_GROUP_ANY, HID_VID_ANY, HID_PID_ANY), HID_PID_ANY)); SEC(HID_BPF_DEVICE_EVENT) -int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx) +int BPF_PROG(kbd_hook, struct hid_bpf_ctx *hctx) { __u32 hid_id = hctx->hid->id; @@ -161,124 +173,233 @@ int BPF_PROG(kdb_hook, struct hid_bpf_ctx *hctx) if (!info) return 0; - __u32 size = info->report_size; - __u32 fetch_size; - __u8 *data; + __u8 *raw_buffer; - if (size <= 8) - fetch_size = 8; - else if (size <= 16) - fetch_size = 16; - else - fetch_size = 32; + __u8 *report_id_data = hid_bpf_get_data(hctx, 0, 1); - data = hid_bpf_get_data(hctx, 0, fetch_size); + if (!report_id_data) + return 0; - if (!data) + if (info->report_id != 0 && report_id_data[0] != info->report_id) return 0; - int now_active_ks = 0, was_active_ks = 0; -#pragma unroll - for (int i = 0; i < 32; i++) { - if (i >= fetch_size) - break; - now_active_ks += ((__u8)data[i] + 255) >> 8; - was_active_ks += ((__u8)info->prev_report[i] + 255) >> 8; - info->prev_report[i] = data[i]; + __u32 offset = info->keyboard_offset + (info->report_id != 0 ? 1 : 0); + + __u64 buffer_length = info->raw_buffer_length; + + if (buffer_length <= 8) + raw_buffer = hid_bpf_get_data(hctx, offset, 8); + else if (buffer_length <= 16) + raw_buffer = hid_bpf_get_data(hctx, offset, 16); + else if (buffer_length <= 32) + raw_buffer = hid_bpf_get_data(hctx, offset, 32); + else + raw_buffer = hid_bpf_get_data(hctx, offset, 64); + + if (!raw_buffer) + return 0; + + bool new_key_pressed = false; + + if (info->report_id == 0) { + new_key_pressed = + standard_boot_keypress(raw_buffer, info->prev_report); + } else { + new_key_pressed = nkro_keypress(raw_buffer, info->prev_report, + buffer_length); } - __u64 current_ms = bpf_ktime_get_ns() / HID_GUARD_NSEC_PER_MSEC; + __u64 now = bpf_ktime_get_ns(); - if (now_active_ks > was_active_ks) { + __u64 interval_ms = 0; + __u64 variance_m2 = 0; + bool is_idle_gap = false; + bool is_suspicious = false; + enum hid_guard_ped_flag ped_flag = HID_GUARD_PED_NO_ENTRY; + bool run_ped = false; + + bpf_spin_lock(&info->lock); + + if (new_key_pressed) { if (info->prev_keydown_ts != 0) { - __u64 interval_ms = current_ms - info->prev_keydown_ts; + interval_ms = (now - info->prev_keydown_ts) / + HID_GUARD_NSEC_PER_MSEC; - if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) { + if (interval_ms < HID_GUARD_IDLE_GAP_THRESH_MS) welford(info, interval_ms); - } else { - bpf_printk( - "hid %d: idle gap %llu ms excluded from sample\n", - hid_id, interval_ms); - } + else + is_idle_gap = true; } + /* + * Variance check runs after welford() so the current + * sample is already folded in before we decide. + * Guard on MIN_SAMPLES: Welford's unbiased estimator + * (M2 / (count - 1)) is undefined for count < 2, and + * unreliable until a few samples have accumulated. + */ if (info->count >= HID_GUARD_MIN_SAMPLES) { - __u64 variance_m2 = + variance_m2 = info->M2 / ((__u64)HID_GUARD_WELFORD_SCALE * HID_GUARD_WELFORD_SCALE * (info->count - 1)); - /* - * the initial interval x was multiplied by HID_GUARD_WELFORD_SCALE, both - * delta and delta2 are also scaled by that factor, - * thus scale^2 in the denominator - */ - if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) { - bpf_printk( - "hid %d: Suspeciously regular typing, variance=%llu ms^2\n", - hid_id, variance_m2); - } + if (variance_m2 < HID_GUARD_VARIANCE_THRESH_MS2) + is_suspicious = true; } - info->prev_keydown_ts = current_ms; + info->prev_keydown_ts = now; } - if (info->connection_time != 0) { - enum hid_guard_ped_flag ped_flag = - post_enumeration_delay(info, bpf_ktime_get_ns()); - - bpf_printk("PED flag for hid %d: %d\n", hid_id, ped_flag); + for (int i = 0; i < 64; i++) { + if (i >= buffer_length) + break; + info->prev_report[i] = raw_buffer[i]; + } - /* - * Prevent re-evaluation on subsequent packets for this device - */ + /* + * PED: fires exactly once per device lifetime. connection_time + * is set at probe() time; we clear it here so subsequent events + * skip this branch entirely. + */ + if (info->connection_time != 0) { + ped_flag = post_enumeration_delay(info, now); info->connection_time = 0; + run_ped = true; } + + bpf_spin_unlock(&info->lock); + + if (new_key_pressed) { + if (is_idle_gap) + bpf_printk( + "hid %d: idle gap %llu ms excluded from sample\n", + hid_id, interval_ms); + if (is_suspicious) + bpf_printk( + "hid %d: suspiciously regular typing, variance=%llu ms^2\n", + hid_id, variance_m2); + } + + if (run_ped) + bpf_printk("hid %d: PED flag=%d\n", hid_id, ped_flag); + return 0; } HID_BPF_OPS(hook_keyboard) = { - .hid_device_event = (void *)kdb_hook, + .hid_device_event = (void *)kbd_hook, }; struct hid_rdesc_descriptor HID_REPORT_DESCRIPTOR; -SEC("syscall") -int probe(struct hid_bpf_probe_args *ctx) +static __always_inline bool find_keyboard_field(__u16 *bits_start, + __u16 *bits_end, + __u8 *report_id, + __u32 *size_in_bytes) { struct hid_rdesc_report *input; struct hid_rdesc_field *field; struct hid_rdesc_collection *col; hid_bpf_for_each_input_report(&HID_REPORT_DESCRIPTOR, input) { - __u32 size_in_bytes = (input->size_in_bits + 7) / 8; - - bpf_printk("Report size: %d\n", size_in_bytes); - if (input->report_id != 0) - size_in_bytes += 1; - - bpf_printk("Report size after report_id: %d\n", size_in_bytes); - hid_bpf_for_each_field(input, field) { hid_bpf_for_each_collection(field, col) { if (col->usage_page == HidUsagePage_GenericDesktop && col->usage_id == HidUsage_GD_Keyboard) { - __u32 key = ctx->hid; - struct dev_info info = { - .connection_time = - bpf_ktime_get_ns(), - .count = 0, - .report_size = size_in_bytes - }; - bpf_map_update_elem(&dev_details, &key, - &info, BPF_ANY); - ctx->retval = 0; - return 0; + *size_in_bytes = + input->size_in_bits / 8; + + if (input->report_id != 0) + *size_in_bytes += 1; + + *bits_start = field->bits_start; + *bits_end = field->bits_end; + *report_id = input->report_id; + return true; } } } } - ctx->retval = -EINVAL; + return false; +} + +SEC("syscall") +int probe(struct hid_bpf_probe_args *ctx) +{ + __u8 report_id; + __u16 bits_start, bits_end; + __u32 size_in_bytes; + __u32 hid_id = ctx->hid; + + struct dev_info *existing_info = + bpf_map_lookup_elem(&dev_details, &hid_id); + + struct dev_info info; + + __builtin_memset(&info, 0, sizeof(info)); + + if (!find_keyboard_field(&bits_start, &bits_end, &report_id, + &size_in_bytes)) { + ctx->retval = -EINVAL; + return 0; + } + + if (existing_info != NULL) { + __u64 now = bpf_ktime_get_ns(); + + bpf_spin_lock(&existing_info->lock); + + existing_info->connection_time = now; + + existing_info->bits_start = bits_start; + existing_info->bits_end = bits_end; + existing_info->keyboard_offset = bits_start / 8; + existing_info->report_size = size_in_bytes; + + if (existing_info->keyboard_offset > + existing_info->report_size) { + bpf_spin_unlock(&existing_info->lock); + ctx->retval = -EINVAL; + return 0; + } + + existing_info->report_id = report_id; + existing_info->raw_buffer_length = + existing_info->report_size - + (existing_info->keyboard_offset - + (report_id != 0 ? 1 : 0)); + + existing_info->count = 0; + existing_info->mean = 0; + existing_info->M2 = 0; + existing_info->prev_keydown_ts = 0; + + __builtin_memset(existing_info->prev_report, 0, + sizeof(existing_info->prev_report)); + + bpf_spin_unlock(&existing_info->lock); + ctx->retval = 0; + return 0; + } + + info.connection_time = bpf_ktime_get_ns(); + info.bits_start = bits_start; + info.bits_end = bits_end; + info.keyboard_offset = bits_start / 8; + info.report_size = size_in_bytes; + + if (info.keyboard_offset > info.report_size) { + ctx->retval = -EINVAL; + return 0; + } + + info.report_id = report_id; + info.raw_buffer_length = info.report_size - (info.keyboard_offset - + (report_id != 0 ? 1 : 0)); + + bpf_map_update_elem(&dev_details, &hid_id, &info, BPF_NOEXIST); + ctx->retval = 0; return 0; } diff --git a/src/bpf/testing/meson.build b/src/bpf/testing/meson.build index 9d1b5d3..2586aea 100644 --- a/src/bpf/testing/meson.build +++ b/src/bpf/testing/meson.build @@ -11,6 +11,7 @@ tracing_sources = [ # 'sources' are BPF programs only compatible with # struct_ops (kernel v6.11+) sources = [ + '0010-Generic__keyboard.bpf.c', ] foreach bpf: tracing_sources -- 2.55.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v2 0/1] HID: add malicious HID device detection driver 2026-04-04 13:37 [PATCH v2 0/1] HID: add malicious HID device detection driver Zubeyr Almaho 2026-04-04 13:37 ` [PATCH v2 1/1] " Zubeyr Almaho @ 2026-04-05 5:31 ` Greg KH 1 sibling, 0 replies; 6+ messages in thread From: Greg KH @ 2026-04-05 5:31 UTC (permalink / raw) To: Zubeyr Almaho Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel, security On Sat, Apr 04, 2026 at 04:37:44PM +0300, Zubeyr Almaho wrote: > Hi Jiri, Benjamin, > > This series introduces hid-omg-detect, a passive HID monitor that scores > potentially malicious keyboard-like USB devices (BadUSB / O.MG style) > using: > > - keystroke timing entropy, > - plug-and-type latency, > - USB descriptor fingerprinting. > > When the configurable threshold is crossed, the module emits a warning > with a userspace mitigation hint (usbguard). > > The driver does not block, delay, or modify HID input events. That's cute, but no need to get security@kernel.org involved as this is a new feature, not a bug triage. Also, why not just do this as an ebpf program instead as you have full access to the hid data stream there? thanks, greg k-h ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-09 10:53 UTC | newest] Thread overview: 6+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-04-04 13:37 [PATCH v2 0/1] HID: add malicious HID device detection driver Zubeyr Almaho 2026-04-04 13:37 ` [PATCH v2 1/1] " Zubeyr Almaho 2026-04-07 7:59 ` Benjamin Tissoires 2026-06-19 7:36 ` [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection krishgulati7 2026-07-09 10:49 ` [RFC PATCH v2] " Krish Gulati 2026-04-05 5:31 ` [PATCH v2 0/1] HID: add malicious HID device detection driver Greg KH
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox