Reverse engineering the Attack Shark keyboard protocol

Jimmc4141 pts0 comments

Reverse engineering the ROYUAN keyboard HID protocol · dnim.dev Skip to content

All writing<br>The plan was to port my keyboard to QMK. An Attack Shark X86: aluminum, a knob, tri-mode wireless. Attack Shark even markets a configurator at qmk.top, so the firmware must be QMK, right? It is not. qmk.top is a vendor web tool squatting on the name, the board runs stock proprietary firmware, and the real config software is Windows-only. I’m on Linux. So the port was out, and the goal became simpler: configure the board I own from the machine I use.

That meant learning what the software actually says to the keyboard.

Getting a foot in#

The vendor tool is an Electron app, and nothing in its device path is native. Every byte it sends to the keyboard goes out over WebHID, from JavaScript. Three patches to the bundle force the pure-WebHID path, and it runs on Linux with a debug flag that logs every report. That log, plus the prettified bundle, is the whole map.

The transport is plain: 64-byte HID feature reports, report ID 0, on vendor usage page 0xFFFF, usage 2. A request goes down, the board settles for about 10 ms, a reply comes back with the opcode echoed in byte 0. Checksum is 0xFF - (sum & 0xFF).

Two command sets on one wire#

The “Attack Shark Driver” is a reskin of a universal ROYUAN app. The same manufacturer builds boards sold as Epomaker, Akko, Hator, ikbc, NOPPOO and MEETION, and the one driver serves all of them, swapping brand logos off a JSON file over a registry of more than five hundred devices. Most enumerate on USB vendor ID 0x3151.

But there are two command sets in the family, and they reuse opcode numbers for different registers:

commandfamily Afamily Bwrite keymap0x090x0Awrite options bitfield0x060x09write debounce0x110x06write sleep timers0x120x11<br>Send family A’s “write keymap” to a family B board and you have written the options bitfield, which holds the bit that locks the whole keyboard. Nothing rejects it. The board applies the opcode to whatever it means locally and returns a clean reply. So the family has to be resolved per device from a registry, never guessed from the product ID, which the vendors recycle.

Reading the keymap#

For family A, the Windows tool is enough to work from: capture a report, replay it, diff the board. A layer is 128 slots of 4 bytes, 512 per layer, and each slot value is tagged by its first byte.

byte 0meaning0x00HID usage0x03consumer usage (volume, media)0x09macro, [mode, index]0x0Aspecial: [1,0,0] Fn, [12,0,0] power save<br>The knob is just three more slots: on the X86, slot 90 is turn-right, 91 is turn-left, 84 is press. Bind a macro by writing [0x09, mode, index, 0] into a slot, where mode is repeat, toggle, or hold.

When there is no software to capture#

The hall-effect boards use family B, and I wanted the write paths confirmed against something more solid than vendor JavaScript before pointing my own writes at flash. The firmware answers that.

These boards get stuck in bootloader mode when an update fails, because the vendor’s update backend answers “Record not found” for every device ID, so the community keeps an archive of rescued firmware images. One is the X65HE. It reads as encrypted, entropy 7.99, but the vendor’s JS names the format itself with a call to inflateRaw. It is raw-deflate. Inflated, it is a 115 KB ARM Cortex-M image at base 0x08000000.

radare2, thumb mode$ r2 -m 0x08000000 -a arm -b 16 2268_v309.app.bin<br># command dispatch at 0x08010980<br># SET_KEYMATRIX handler at 0x08006f10<br># GET_KEYMATRIX handler at 0x08010d08<br>The dispatch table gives the family B keymap layout, the bulk-write flow, and the bit on the final page that commits the write to flash, all from the handlers rather than inferred from captures. It also names the opcodes to avoid: one erases the flash chip, two pairs are bootloader entry points.

Lighting#

Lighting is one report, SET_LEDPARAM 0x07:

[op, mode, speed, brightness, (option07 04 03 04 87 ff 00 00 # wave, fixed red<br>A few things the wire does that the UI hides: speed inverts, UI 0..4 goes out as 5..1. White 0xFFFFFF transmits as 0xFAFAFA, and any colour below 0x080808 renders as unlit, so “off” is an option bit, not black. There are 18 effects; a handful need host-side data.

Per-key colour lands in flash#

The one write that needs care is per-key colour. It uploads 384 bytes (128 keys, RGB, matrix order) as seven 56-byte pages, and unlike everything else it goes into the keyboard’s flash. Push the pages too fast and the config endpoint stops answering: every report after it fails with a protocol error until the board is re-enumerated on USB.

Measured on the X86:

cadenceresult7 reports every 500 msendpoint dead after ~13 reports7 reports every 3 s42 reports, fine2 uploads per 5 s, mode switch 600 ms afterstalls on the second2 uploads per 10 s, 2 s settle, no redundant switchsurvives<br>The last row is the shipping rate limit, and it belongs in the backend, not the UI, because a limit the frontend enforces is one a...

mode keyboard vendor board from family

Related Articles