Custom firmware patch and companion desktop tool for the HelloGanss HS75T wireless mechanical keyboard — born from a single question: can hardware macros have random delays?
The HS75T stores macros directly on the keyboard and plays them back in hardware — no software needs to stay open. The problem is the vendor firmware only supports fixed delays. Fixed delays make macro playback rhythmically identical every time, which is trivially detectable.
The goal was to add a random delay opcode to the onboard macro interpreter so that hardware playback could vary its timing naturally, without any PC involvement after programming.
The keyboard runs a HFD2201KBA MCU — an ARM Cortex-M0 at 48 MHz with 64 KB of flash and 8 KB of SRAM (same die as the SN32F248B). The vendor ships firmware as a self-extracting executable that wraps a raw .bin paired with a SnxHidLib.DLL flasher.
Using Ghidra, the macro interpreter loop was located and fully mapped:
- The interpreter walks a byte buffer from onboard flash offset
0x0414to0x1FFF - Each macro slot is null-terminated (
0x00) — no null bytes allowed inside a payload - Opcodes:
01 02 key= Key Down,01 03 key= Key Up,01 04 lo hi= Fixed Delay (base-255 non-zero encoded) - The fixed delay handler branch at
0x1C8Ewas the injection point
The firmware had 136 bytes of zero-filled space after the last real function — a clean code cave at 0xD864. The patch:
- Redirects the opcode dispatch at
0x1C8Ewith a branch to the cave - The cave checks for the new
0x08opcode - On match: reads
min_lo min_hi max_lo max_hi, decodes base-255 values, generates a pseudo-random number using an XOR-shift seeded from the timestamp and storage offset, and waits a random duration within[min, max]ms - A skip-phase guard prevents the resume-replay mechanism from trapping the interpreter in an infinite delay loop (this was the critical bug discovered in v1)
- Checksum trailer at the end of the binary is recalculated after every modification
- On no match: falls through to the original opcode handler
The result is a new opcode: 01 08 min_lo min_hi max_lo max_hi — Random Delay — fully interoperable with the existing macro buffer format.
A WPF / .NET 9 desktop application that programs macros into the keyboard over USB HID.
- Connect / Read / Restore — pull the live macro buffer from the keyboard via HID
- Save — write the full buffer back; keyboard plays macros independently once saved
- 16 macro slots with per-slot timeline view
- Record — global keyboard and mouse hook captures keystrokes system-wide
- Real delays — optionally inserts Fixed Delays based on actual timing between keystrokes
- Append mode — add to existing rows without clearing
- Mouse clicks / scroll wheel recording
- Action editor — add, reorder, duplicate, delete events; Rebind Key, Toggle Down/Up, Insert Tap After
- Convert All Fixed to Random — replaces every Fixed Delay in a slot with a Random Delay bounded by a configurable ±offset (requires patched firmware)
- Playback modes: Once, Repeat N times, Stop, Held
- Import / Export — save/load slots as
.jsonfiles - Emergency Key Release — sends KeyUp for all HID keycodes to unstick a held key
⚠️ Random Delay and Repeat/Held playback require the custom patched firmware. These features are silently ignored by stock firmware.
hs75t-custom-firmware/
├── src/ # C# source — Hs75tTool WPF app
│ ├── Hs75tTool.sln
│ ├── Hs75tTool.Core/ # HID client, codec, macro model
│ ├── Hs75tTool.Desktop/ # WPF frontend (MainWindow, Services)
│ └── Hs75tTool.Tests/ # Unit tests (MSTest)
├── firmware/
│ ├── HFD2201KBA_SN32F248B_VENDOR_STOCK.bin # Original unmodified firmware
│ └── HFD2201KBA_SN32F248B_patched_v5.bin # Current patched firmware (v5)
├── tools/ # Flashing and bootloader utilities
│ ├── reboot_to_bootloader.py # Software bootloader entry via HID magic bytes
│ ├── CHECK_BOOTLOADER.bat # Verify keyboard is in bootloader mode
│ ├── ENTER_BOOTLOADER.bat # Force keyboard into bootloader
│ ├── SonixFlasherC/ # Open-source Sonix flash tool (third-party)
│ └── ...
├── patching/ # Python analysis and patch verification scripts
│ ├── pack_v2.py # Repack patched binary into vendor SFX format
│ ├── verify_sfx_contents.py # Verify SFX contents match expected binaries
│ └── ...
├── docs/
│ ├── 00_Hardware_Identification.md
│ ├── 02_Official_Flasher_Protocol_Analysis.md
│ ├── 06_Original_Walkthrough2.md # Full RE walkthrough and HID protocol reference
│ ├── 07_Patched_Disassembly.md # Capstone disassembly of the injected cave
│ └── firmware_full_decompiled.c # Ghidra decompiled C output (reference)
└── CHANGELOG.md
| Field | Value |
|---|---|
| Keyboard | HelloGanss HS75T |
| MCU | HFD2201KBA (ARM Cortex-M0, 48 MHz) |
| Flash | 64 KB |
| SRAM | 8 KB |
| USB VID | 0x05AC |
| USB PID | 0x0256 |
| Firmware version | V1.15 (dry battery variant) |
| Hook address | 0x1C8E |
| Code cave address | 0xD864 (136 bytes) |
⚠️ Flash at your own risk. The patched binary has been tested on the dry-battery V1.15 variant only. Flashing the wrong binary can brick the keyboard. Keep the vendor stock binary as a backup.
- Python 3.x
- SonixFlasherC (included)
- libusb (required by SonixFlasherC)
-
Enter bootloader mode
python tools/reboot_to_bootloader.pyOr run
tools/ENTER_BOOTLOADER.bat. The keyboard disconnects and re-enumerates as a Sonix bootloader device. -
Flash the patched firmware
SonixFlasherC -d SN248B -r 0 -t 0 -w firmware/HFD2201KBA_SN32F248B_patched_v5.bin -
Verify the keyboard re-enumerates normally and the tool connects.
To restore stock firmware, flash firmware/HFD2201KBA_SN32F248B_VENDOR_STOCK.bin with the same procedure.
Requires .NET 9 SDK and Windows.
cd src
dotnet build Hs75tTool.sln
dotnet run --project Hs75tTool.Desktop
Macros are stored in onboard flash from offset 0x0414 to 0x1FFF. Each slot is null-terminated. 0x00 must not appear inside a macro payload — it is treated as a slot terminator.
| Bytes | Meaning |
|---|---|
01 02 key |
Key Down |
01 03 key |
Key Up |
01 04 lo hi |
Fixed Delay (base-255 non-zero encoded) |
01 08 min_lo min_hi max_lo max_hi |
Random Delay — custom extension, patched firmware only |
Delay values are encoded to avoid null bytes:
lo = (ms % 255) + 1
hi = (ms // 255) + 1
decoded = (lo - 1) + (hi - 1) * 255
- SonixFlasherC — open-source Sonix HID flasher (included under its original license)
