From: Zubeyr Almaho <zybo1000@gmail.com>
To: Jiri Kosina <jikos@kernel.org>
Cc: Zubeyr Almaho <zybo1000@gmail.com>,
Benjamin Tissoires <bentiss@kernel.org>,
linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
security@kernel.org
Subject: [PATCH v2 1/1] HID: add malicious HID device detection driver
Date: Sat, 4 Apr 2026 16:37:45 +0300 [thread overview]
Message-ID: <20260404133746.80914-2-zybo1000@gmail.com> (raw)
In-Reply-To: <20260404133746.80914-1-zybo1000@gmail.com>
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);
next prev parent reply other threads:[~2026-04-04 13:38 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
2026-04-07 7:59 ` [PATCH v2 1/1] " Benjamin Tissoires
2026-04-05 5:31 ` [PATCH v2 0/1] " Greg KH
-- strict thread matches above, loose matches on Subject: below --
2026-04-11 13:47 [PATCH v2 1/1] " kernel test robot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260404133746.80914-2-zybo1000@gmail.com \
--to=zybo1000@gmail.com \
--cc=bentiss@kernel.org \
--cc=jikos@kernel.org \
--cc=linux-input@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=security@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.