All of lore.kernel.org
 help / color / mirror / Atom feed
From: krishgulati7@gmail.com
To: bentiss@kernel.org
Cc: linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	jikos@kernel.org, Krish Gulati <krishgulati7@gmail.com>
Subject: [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection
Date: Fri, 19 Jun 2026 13:06:03 +0530	[thread overview]
Message-ID: <20260619073607.393248-1-krishgulati7@gmail.com> (raw)
In-Reply-To: <adSxXidgeWF0-Ewn@beelink>

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


  reply	other threads:[~2026-06-19  7:36 UTC|newest]

Thread overview: 7+ 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 ` [PATCH v2 1/1] " Zubeyr Almaho
2026-04-07  7:59   ` Benjamin Tissoires
2026-06-19  7:36     ` krishgulati7 [this message]
2026-06-19  7:45       ` [RFC PATCH] HID: BPF: add keyboard behavioral anomaly detection sashiko-bot
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

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=20260619073607.393248-1-krishgulati7@gmail.com \
    --to=krishgulati7@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.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.