* [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-06-19 7:45 ` sashiko-bot 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
* Re: [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection 2026-06-19 7:36 ` [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection krishgulati7 @ 2026-06-19 7:45 ` sashiko-bot 0 siblings, 0 replies; 6+ messages in thread From: sashiko-bot @ 2026-06-19 7:45 UTC (permalink / raw) To: krishgulati7; +Cc: dmitry.torokhov, linux-input Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider: - [Critical] Evasion of detector via large HID descriptors with padded payloads. - [High] Missing Report ID validation causes false positives and state corruption on composite devices. - [High] Mismatched report size rounding causes silent event dropping and logical out-of-bounds reads. - [High] LRU map eviction permanently disables detection for a device instead of resetting its state. - [High] Uninitialized padding bytes in the stack-allocated `struct dev_info` will cause the BPF verifier to reject the program. - [High] Keystroke heuristic fails to detect keys when no empty report is sent between strokes (morphed keys). - [Medium] Missing `bpf_spin_lock` protection allows data races between concurrent `kdb_hook` or `probe` invocations. -- commit 103b2fc551543479bbac0ff3ea588c3508776a4c Author: Krish Gulati <krishgulati7@gmail.com> HID: BPF: add keyboard behavioral anomaly detection 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 an LRU hash map keyed by the HID ID. Link: https://lore.kernel.org/linux-input/adSxXidgeWF0-Ewn@beelink/ > diff --git a/src/bpf/testing/0010-Generic__keyboard.bpf.c b/src/bpf/testing/0010-Generic__keyboard.bpf.c > index 0000000000000..91145870768f6 > --- /dev/null > +++ b/src/bpf/testing/0010-Generic__keyboard.bpf.c [ ... ] > +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++; [Severity: Medium] Will this lockless compound update be corrupted by concurrent executions? The dev_details LRU hash map stores per-device state which kdb_hook() reads and modifies directly via a map value pointer locklessly. Updating Welford variance (count, mean, M2) is a compound operation. If kdb_hook() executes concurrently for the same device, such as across multiple endpoints on a composite device, or if the userspace probe() program is executed simultaneously, torn reads and writes will occur. Does this structure require a bpf_spin_lock to prevent data races? > + delta = x - dev_state->mean; [ ... ] > +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; [Severity: High] Does an LRU map eviction permanently disable detection for a device instead of resetting its state? The dev_details map uses BPF_MAP_TYPE_LRU_HASH with 128 max entries. When a device is evicted, bpf_map_lookup_elem() returns NULL, and kdb_hook() immediately returns 0, completely ignoring the event. Because elements are only inserted by the userspace probe() program, an evicted device is never automatically re-inserted by the kernel hook. Could an attacker deliberately present 128 dummy HID interfaces to flood the map, permanently evict the payload device, and bypass the guard entirely? > + > + __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; [Severity: Critical] Does hardcapping the analysis size to 32 bytes allow attackers to evade detection by padding their malicious HID descriptor? An attacker could craft a custom USB HID descriptor that defines dummy fields or padding for the first 32 bytes, placing the actual Keyboard Usage page array at offset 32 or higher. The loop will only iterate over the static padding and completely ignore the keystrokes. Meanwhile, the Linux HID subsystem correctly parses the full report and processes the hidden keycodes, allowing arbitrary keystroke injection that bypasses this guard. > + > + data = hid_bpf_get_data(hctx, 0, fetch_size); > + > + if (!data) > + return 0; [Severity: High] Will this size rounding cause silent event dropping and logical out-of-bounds reads? The BPF program rounds info->report_size up to a power-of-two fetch_size (8, 16, or 32). If a keyboard has a 9-byte report, fetch_size becomes 16. If the kernel's ctx->allocated_size for this device is exactly 9 bytes, hid_bpf_get_data(hctx, 0, 16) will exceed the bounds and return NULL. The program then returns 0, silently skipping analysis for the report while the kernel continues processing the keystrokes. If allocated_size is large enough, the loop uses fetch_size as the upper bound, reading past valid report data into padding or uninitialized memory, polluting the active key tracking. > + 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; [Severity: High] Will this logic indiscriminately process all incoming raw reports, even those not belonging to the keyboard? Many HID keyboards are composite devices, such as wireless keyboard and mouse receivers, that send multiple report types over the same interface. Since the report ID is observed in probe() but not stored in dev_info, this hook counts all non-zero bytes across all input reports as keystrokes. Mouse movements, which send reports with changing non-zero bytes, will be incorrectly interpreted as rapid keystrokes. Will this corrupt the Welford variance tracking and cause false positive anomaly detections? > + 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) { [Severity: High] Does this heuristic fail to detect keys when no empty report is sent between strokes? The detection logic relies strictly on an increase in the total number of non-zero bytes to identify a keypress. Automated injection tools routinely optimize injections by morphing keycodes directly without sending an intervening empty (all-zeros) report. If an attacker's tool changes key 'A' to key 'B' in consecutive reports, both reports have exactly one non-zero byte. The condition 1 > 1 evaluates to false, causing the keystroke to be missed and bypassing the variance check entirely. [ ... ] > +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; [Severity: High] Is the report ID needed here to validate incoming reports later? The report ID is observed here but not stored in the map, making it impossible for kdb_hook() to distinguish between keyboard reports and other HID reports on a composite device. > + > + 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 > + }; [Severity: High] Will the uninitialized padding bytes in this struct cause the BPF verifier to reject the program? The dev_info object is initialized using a designated initializer list. While this zero-initializes unspecified members, it does not guarantee that the compiler will zero the 4 bytes of padding between report_size and prev_report. The BPF verifier strictly tracks initialization of stack memory at the byte level. When bpf_map_update_elem() reads the struct from the stack, the verifier will detect the uninitialized padding and reject the program with an 'invalid indirect read from stack' error. Should this structure be explicitly zeroed using __builtin_memset? > + bpf_map_update_elem(&dev_details, &key, > + &info, BPF_ANY); -- Sashiko AI review · https://sashiko.dev/#/patchset/20260619073607.393248-1-krishgulati7@gmail.com?part=1 ^ permalink raw reply [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-06-19 7:45 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-06-19 7:45 ` sashiko-bot 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