Most game cheats live on the computer: they read game memory, inject a DLL, or hook the renderer. Kernel anti-cheat is built to find exactly that. So the most interesting class of cheat, from a defender's point of view, is the one that moves the logic off the host entirely and feeds its decisions back in through a channel the operating system already trusts completely: a USB mouse.
A microcontroller with a native USB stack — here an Arduino Leonardo (ATmega32u4) — can present itself to the PC as an ordinary HID mouse. Pair it with a USB host shield so the player's real mouse plugs into the Arduino instead of the PC, and you have a device that sits physically in the input path. A second process watches the screen, decides where the crosshair should be, and whispers correction deltas to the Arduino over a serial link. The Arduino adds those movements on top of the real mouse's, and emits one clean HID report stream. To Windows, it is a mouse.
The question I set out to answer was not "how do I build this" — that part is well-trodden and public. It was the defender's question: if the input arrives through a genuine HID device, what is actually left to detect? To answer it honestly I had to build the thing end-to-end, on my own bench, against bots and offline targets only — because you can't reason credibly about a technique's detection surface until you've seen exactly where its seams are. This write-up is the result: the hardware, the interfacing code that matters, and — the real point — every place the build fought me, because each of those friction points turned out to be a signal a defender can key on.
The whole technique rests on one feature of the microcontroller: a native USB device
controller. A classic Arduino Uno (ATmega328P) can't do this — it reaches the PC through a
separate USB-to-serial chip and can only ever look like a COM port. The Leonardo / Pro Micro family
uses the ATmega32u4, whose USB peripheral is wired straight into the chip, so the
firmware itself decides what kind of device to enumerate as. With the stock Mouse.h
library it declares an HID mouse interface — the same descriptor class a retail mouse uses.
Mouse.begin(), Mouse.move(dx, dy, wheel), Mouse.press/release. Movement deltas are signed bytes.hidboot.h + usbhub.h.The device therefore plays two roles at once, which is the clever part of the topology:
Mouse.move(), so the player's hand still moves the cursor normally.From the host's side there is only one cursor, moving smoothly, from one USB mouse. The blend of human and machine has already happened in firmware, below the level any software API can see.
Mouse.movebytes → USBStages 1–3 are ordinary computer vision and run on the PC; they are the "brain." Stages 4–5 are the hardware bridge — the part this project actually exists to study, because that boundary is where the detection problem is won or lost. The walkthrough below follows a delta through that boundary and stops at each seam to ask: what does this leave behind?
These are trimmed, representative excerpts to explain the mechanism, not a build kit — the detection discussion is the payload. Each block is paired with what it costs the attacker in detectability.
After detection returns candidate boxes, the brain picks one target and works out how far the crosshair
has to travel. The capture region is centred on the crosshair, so screen-centre is just half the region
(cWidth, cHeight); the move is simply target-minus-centre, nudged up toward the
head:
# nearest target to the crosshair = smallest distance from centre
targets["dist"] = np.sqrt((targets.mid_x - cWidth)**2 + (targets.mid_y - cHeight)**2)
target = targets.sort_values("dist").iloc[0]
# aim a little above box-centre so the shot lands on the head
head_offset = target.height * (0.38 if headshot_mode else 0.20)
# pixel delta from crosshair to aim point
mouseMove = [target.mid_x - cWidth, (target.mid_y - head_offset) - cHeight]
# scale by an "aim amplitude" — lower = softer/slower, less obvious
dx = mouseMove[0] * aaMovementAmp
dy = mouseMove[1] * aaMovementAmp
target selection & aim-vector math — the "brain" output, before it ever reaches hardware
The interesting variable is aaMovementAmp. At 1.0 the crosshair snaps onto the
target in a single frame — lethal, and trivially superhuman. Every attempt to make the cheat
look human is really an attempt to tune this value (and add smoothing) downward until the motion
sits inside a plausible reaction-time and acceleration envelope. That tension is the entire
detection thesis: the knob that makes the cheat effective is the same knob that makes it
detectable, and you cannot turn both down at once. This is where behavioural detection gets its grip —
long before anyone inspects the device.
The PC and the Arduino agree on a tiny protocol. The simplest is two signed bytes — one for X, one for
Y — but the build originally tried to send a human-readable "dx:dy" string, which is where
the boundary first bit back:
ser = serial.Serial('COM5', 115200, timeout=0.01)
...
# activation gate: only steer while the player holds a key (Shift = 0x10)
if win32api.GetKeyState(0x10):
payload = struct.pack('bb', clamp8(int(round(dx))), clamp8(int(round(dy))))
ser.write(payload)
Python side — gated, integer-clamped, byte-packed serial write
Where the boundary bites
Three real errors stood between idea and working link, and all three are the same lesson — a serial
wire carries bytes, not Python objects. numpy.float64 values can't be glued to a
":" string (UFuncNoLoopError); a str won't go down the wire
un-encoded ("unicode strings are not supported, please encode to bytes"); and
Serial.write() takes one bytes argument, not two. The fix is to round to
integers, clamp, and pack to bytes before writing. Mundane — but it forces you to confront
that everything past this point is a hardware protocol with hard limits, not a function call.
Note the GetKeyState gate: the cheat only steers while a key is held. That keeps it subtle,
but it also means the device produces a very particular trace — bursts of correction perfectly
correlated with a held button, separated by stretches of untouched human motion. That correlation is
itself a pattern a server can learn.
On the Arduino, the loop services the host shield (so the real mouse keeps working) and, whenever
correction bytes arrive, turns them into HID movement. The catch is the one in the hardware table:
Mouse.move() takes a signed byte, range −128…127. A flick across the
screen is far larger than 127 pixels, so a single delta has to be walked out in steps:
#include <hidboot.h>
#include <usbhub.h>
#include <Mouse.h>
USB Usb;
HIDBoot<USB_HID_PROTOCOL_MOUSE> HidMouse(&Usb); // real mouse → host shield
void moveBy(int dx, int dy) {
// split into <= 127px steps because HID deltas are signed bytes
while (dx != 0 || dy != 0) {
int sx = constrain(dx, -127, 127);
int sy = constrain(dy, -127, 127);
Mouse.move(sx, sy);
dx -= sx; dy -= sy;
}
}
void loop() {
Usb.Task(); // pass the real mouse through
if (Serial.available() >= 2) { // injected correction from Python
int8_t dx = Serial.read();
int8_t dy = Serial.read();
moveBy(dx, dy);
}
}
Arduino sketch — pass-through + injection in one loop, with byte-limit stepping
That while loop is forced by the hardware, and it leaves a fingerprint. A genuine
high-end mouse reports physically continuous motion at its own polling rate; this firmware emits aim
corrections as a burst of equal-or-127-pixel quantised steps, clocked to the serial
arrival rate rather than to a hand. The quantisation and the timing regularity are exactly the kind of
micro-signature behavioural models are built to notice — the movement is "too clean" in a way that has
a concrete, mechanical cause.
The pass-through half has to parse the HID reports coming off the player's mouse. The textbook layout is four signed bytes — buttons, X, Y, wheel — and a naive parser assumes exactly that:
void Parse(USBHID *hid, bool rpt_id, uint8_t len, uint8_t *buf) {
// assumed report: buf[0]=buttons buf[1]=X buf[2]=Y buf[3]=wheel
int8_t x = (int8_t)buf[1];
int8_t y = (int8_t)buf[2];
Mouse.move(x, y, (int8_t)buf[3]);
}
naive 4-byte HID report parser — correct for a generic mouse, wrong for a fancy one
This worked perfectly with one mouse and failed on a Logitech G Pro X Superlight:
vertical movement came through, horizontal didn't. The cause is the whole point of the project in
miniature. A high-end mouse doesn't use the generic boot-protocol layout — its HID report
descriptor declares a custom format (wider 16-bit axis fields, different byte offsets, vendor
usages), so reading "X" from buf[1] grabbed the wrong half of a two-byte field. Making the
pass-through robust meant actually reading the device's report descriptor and locating the axis fields
instead of assuming them.
The defender's gift
That bug is descriptor fingerprinting handed to you from the other side. If a Superlight and a generic
mouse are distinguishable by their report descriptors alone, then so is an emulator. A stock
Leonardo's HID descriptor, its default VID 0x2341 (Arduino LLC), its reporting
interval, and — the giveaway — the fact that it also enumerates a CDC serial interface
(a real mouse is HID-only; it has no COM port) form a composite signature an anti-cheat can read at
enumeration time. The very wire used to command the device is what exposes it.
The defender's instinct — "find the cheat process / the injected code" — finds nothing, by construction:
So detection can't ask "is a cheat present?" It has to ask the two harder questions the rest of this project kept circling back to: "is this device what it claims to be?" and "is this input being produced by a human?"
| Layer | Signal | Where it came from in this build |
|---|---|---|
| Behavioural | Reaction time under human latency, snap/overshoot signatures, quantised step cadence, correction bursts gated to a held key | STEP 1 (aaMovementAmp) + STEP 3 (127-px stepping) + STEP 2 (the activation gate) |
| Descriptor / firmware | HID report descriptor, VID/PID (default 0x2341), reporting interval, an HID device that also exposes CDC serial | STEP 4 (the Superlight descriptor bug, and the serial back-channel) |
| Timing | Inter-report intervals clocked to a serial link rather than to a hand or a fixed polling rate | STEP 2 → STEP 3 (serial-arrival-driven emission) |
| Server-side | Shot timing, crosshair position at the instant of fire, kill-consistency, reaction-latency distributions across many rounds | aggregate of the behaviour above, observed off the trusted client |
The hierarchy is the lesson. Device and firmware signals are real and useful, but they are an arms race of fingerprints — an attacker can reflash the VID/PID, clone a retail descriptor, and drop the serial interface in favour of a wireless link. Behaviour is the signal of last resort precisely because it is bound to the cheat's usefulness, not to its implementation. A descriptor can be forged for free; human-looking aim can only be forged by making the aim worse.
This isn't hypothetical — it's an active front. Public reporting on Riot's Vanguard describes exactly these setups: a microcontroller masquerading as a mouse or keyboard, running the cheat off-host, and Riot targeting Arduino-class devices for years, with detection reported since roughly 2021–2022 and understood to work in part through firmware-level identification — while cheat developers keep finding circumvention paths. It is textbook detection engineering: defenders ship detection for a hardware-evasion class's observable fingerprints, attackers file off the fingerprints, and the durable signal keeps migrating toward behaviour, which is far harder to fake convincingly than a device ID. Everything in the table above is one turn of that wheel, reproduced on a bench so I could see the seams myself rather than take them on faith.
The transferable skill wasn't "make a mouse move" — it was bridging a real-time vision pipeline, a serial protocol, and a physical HID peripheral into one closed loop, and being forced, by the hardware, to understand every layer it crosses. The reasoning generalises well past games:
Context & ethics. A personal project for educational and research purposes only, studying the detection problem — how anti-cheat identifies hardware-emulated input. Built and bench-tested on my own equipment, against bots and offline targets only; never used against live players and never distributed. The code excerpts are trimmed for explanation, not assembled as a usable tool, and no firmware, model, or end-to-end program is published here. The aim throughout is the defender's: to understand why "it looks like a real mouse" defeats device-layer checks, and to map exactly where that forces detection up the stack into behaviour and server-side analysis.