public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
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);

  reply	other threads:[~2026-04-04 13:38 UTC|newest]

Thread overview: 4+ 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

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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox