01The short version
VKB sticks are configured with VKBDevCfg, a Windows program with no documentation of how it talks to the hardware. Two questions: what does it actually do? and could there be a better interface? The pilot's guide answers the first. The answer to the second is yes — without VKBDevCfg at all.
One block
A VKB stick's entire configuration — every axis, button, LED and curve — is a single 4096-byte block in its flash memory. Get reads it. Set writes all of it back.
One report
It travels over one HID feature report, 0x59, in both directions, in frames
with a simple eight-byte header. No driver, no vendor library.
No secrets
It is encrypted on the cable and on disk, with RC4. Every key is either built into the program or handed out by the stick on request.
Proven on hardware
A Gladiator EVO R was read, written back unchanged, changed by one setting, and restored to a state byte-identical to where it started.
A better configuration tool is therefore an editor for 4096 bytes, plus about three hundred lines of transport that already exist in this project's repository.
Two labels appear throughout. Verified means it was observed on the wire, on a real stick. Static means it was read out of the vendor program's code and has not been exercised on hardware. The difference matters, and most of the field-by-field detail is still the second kind.
02Ground school
Four ideas carry the rest of the page.
What a joystick is, to a computer
A joystick reports three kinds of thing, about a thousand times a second:
Axis
One number that varies continuously — how far the stick is pushed left or right, how far the throttle is forward. Here, 0 to 4095. Stick X, stick Y, twist and throttle are four axes.
Button
One bit: pressed or not. This stick's report has room for 128.
Hat
A small thumb switch that reports one of eight directions, or centred. Also called a POV.
The stick hardware has none of these. It has magnetic sensors that output voltages, and switches that close contacts. Turning one into the other is the job of a microcontroller inside the base, and how it does so — which sensor is which axis, how it is filtered and curved, which contact is which button — is the configuration.
HID: why there is no driver
HID, the Human Interface Device class, is the part of USB that keyboards, mice and game controllers speak. Its trick is that the device describes itself. On connection it hands the computer a report descriptor: a compact table saying “I send 64-byte packets; bytes 1–2 are an axis called X, bytes 17–32 are 128 buttons…”. Every operating system ships one generic driver that reads that table, so a HID device works everywhere with no vendor software.
The packets are called reports, each tagged with an ID, and come in three kinds:
| Kind | Direction | Used for |
|---|---|---|
| Input | device → computer, continuously | The live state: axes, buttons, hat |
| Output | computer → device | Things like keyboard LEDs or rumble |
| Feature | either way, only when asked | Settings. Not part of the live stream |
Endpoints: what “no OUT endpoint” means
A USB device talks through numbered channels called endpoints, each with a fixed direction, named from the computer's point of view: IN carries data in to the computer, OUT carries data out to the device.
- Endpoint 0, the control endpoint. Every USB device has one. It is two-way and works by request and reply: the computer asks, the device answers. It exists for housekeeping — “who are you?”, “send me your descriptor” — and is slow by design.
- An interrupt IN endpoint. The fast lane to the computer. The computer polls it on a fixed schedule — here every millisecond — and collects an input report.
- An interrupt OUT endpoint. The fast lane the other way, for devices that need a steady stream from the computer. Optional.
This stick has endpoint 0 and one interrupt IN endpoint, and nothing else. So there is no
fast lane to the stick: anything the computer wants to tell it has to go through the
housekeeping channel, as a SET_REPORT request, and anything it wants back
beyond the live stream has to be fetched with GET_REPORT. That is exactly what
feature reports are for, and it is why the whole configuration protocol is built on one.
Where the work happens
A cheap stick sends raw readings and leaves the rest to software on the PC. A VKB stick does the opposite. Its firmware calibrates and filters each sensor, applies response curves, resolves shift layers, times macros and drives LEDs, and only then builds the report. By the time data reaches the computer it is finished. The PC-side program is not in that path at all — it only edits the table the firmware works from.
03How the pieces fit
Two separate conversations share one cable. Flying uses the first. Configuring uses the second. They do not touch.
Data plane
Sensors and switches in, finished joystick reports out, a thousand times a second. Standard HID. The game never learns there is anything more to the device.
Management plane
A second, private conversation on the housekeeping channel, used only to read or replace the parameter block. This is the part VKB never documented, and the subject of this page.
The join
The parameter block. The management plane writes it; the firmware reads it. Change the block and the stick behaves differently, on any computer, with nothing installed.
This is why a configured VKB stick works on a Linux box, a friend's PC or a fresh Windows install: the configuration travels in the stick. It is also why remapping in software on the PC is the wrong layer — the interesting processing has already happened.
04The target
The program
VKBDevCfg-C.exe v0.93.89 is a 23 MB, 32-bit Windows program written in
C++Builder on top of the Delphi VCL. It has no command line, no scripting,
no plugin interface — nothing to automate.
What it does have is names. Every window class carries a table mapping its event handlers to
code addresses, and reading those tables straight out of the file recovers 1,277 of them.
That is the difference between staring at FUN_0041133c and reading:
TMainForm::btnReadParamClick 0x0041133C // the Get button
TMainForm::btnWriteParamClick 0x00410814 // the Set button
TMainForm::ReadCPUID 0x00410E90 // the handshake
TMainForm::btnSaveParamClick 0x00413B20 // Save to .cfg
With names, the read, write, save and load paths can simply be followed. About 2,000
functions decompile cleanly. Eighteen do not — they are giant switch statements
that exhaust the decompiler — and those had to be read as raw assembly.
The stick
| Property | Value |
|---|---|
| Device | VKBsim Gladiator EVO R — 231d:0200 |
| Firmware | 2.20.C |
| Interfaces | Exactly one, class HID |
| Endpoints | Control endpoint 0, plus one interrupt IN (64 bytes, polled every 1 ms). No OUT endpoint. |
| Processor | STM32 — the handshake returns its 96-bit unique ID |
With no OUT endpoint, everything the computer sends goes through endpoint 0 as a control request (ground school). That one fact shapes the whole protocol.
| Report | Type | Size | Role |
|---|---|---|---|
0x01 | Input | 63 B | Live state: six axes, a hat, 128 button bits — and, in bytes the descriptor calls padding, which of the 128 lines are active |
0x08 | Input | 63 B | Service packets, off by default: bus statistics, encoder positions |
0x0C | Input | 63 B | The 128 lines after the stick's own processing. Sent on every change |
0x0B | Input | 63 B | Off by default; not decoded |
0x58 | Output | 63 B | Not used for configuration |
0x59 | Feature | 128 B | All configuration traffic, both directions |
05The block
The table the firmware works from. Everything the stick does differently from its factory behaviour is somewhere in these 4096 bytes.
Roughly four fifths of it is accounted for, at least by purpose. Most of the block is tables of small fixed-size records, and the records are where the settings live. Four of them carry nearly everything a pilot touches. Bits are drawn low bit first; the number under each field is its width.
An axis, twice
Every axis is described two ways. The physical record is the sensor: how to filter it, and the calibration that makes this particular unit read true. The logical record is what the PC sees: which sensor feeds it, which HID axis it appears as, its deadzones.
En = enabled, Vis = visible to the PC, Inv = inverted, Ctr = has a centre, In = the three bits of the sensor type, FL = forced limit, 2D = circular deadzone, Rel = relative mode, Eq = response curve on. The three calibration fields differ between two units of the same stick; nothing else in the record does.
A button, twice
The same split. Each of the 128 wired lines has a physical entry saying what kind of thing it is — a button, a toggle, a hat direction, a shift key — and a logical entry saying what it sends.
A physical entry is either a function — 5-bit type (Toggle, Tempo, SHIFT, POV, Encoder, about two dozen in all) plus a variant and a parameter — or, with the top bit of its first byte set, a plain button that names the lines to use while a shift is held. Logical entries with the top bit clear are keystrokes, hats, macros and mouse actions instead.
An LED event
Two colours at three bits a channel, how to blink, and the condition that lights it — a shift state, a button, calibration in progress, a bus error. The first three bytes use the same packing as the LED command VKB documents publicly, which is good corroboration.
One field here has been changed on a real stick and read back: the
filter grade, outlined above. The header's CRC and firmware words are
confirmed too. Everything else comes from reading the vendor program, which creates each
settings control at run time and copies it into the block in giant switch
functions. Decoded this way, real blocks read as a sensible stick — X and Y on the
contactless sensors, the twist on Rot Z, thirty buttons each used once — which is good
evidence the layout is broadly right, and not proof that any particular bit is. The
row-by-row tables, with evidence and a confidence for each, are in the
data bank.
06The link
The management plane is one feature report, ID 0x59: one ID byte and 128 bytes of data. Sent with
SET_REPORT, fetched with GET_REPORT. On Windows that is
HidD_SetFeature / HidD_GetFeature; on Linux, two hidraw
ioctls. Verified
The frame
Host to device starts A5; the device answers 5A. Command flag
0xC0 means read, 0x80 means write. The CRC is CRC-16/MODBUS.
Step 1 — the handshake
The program first asks for something its own log messages call READ_CPUID:
command 0x0B. Sixteen bytes of the reply, decrypted with a key that is
compiled into the program, are the session token — and its first twelve
bytes are the microcontroller's factory serial number. The first fifteen bytes of the token
are the key for everything that follows.
Step 2 — reading the configuration
That CRC-32 is the proof. It is the STM32's built-in hardware CRC, computed by the stick over its own flash; when a decrypted block satisfies it, the framing, the handshake and the cipher have all been understood correctly. It matched on the first full read.
Step 3 — writing it back
That restart is why VKBDevCfg seems to lose the stick for a moment after every Set. It is the device applying its configuration, not a fault.
| Status word | Meaning | Seen |
|---|---|---|
5A A5 88 00 | Block written | yes |
22 22 22 22 | CRC error | yes |
11 5A A5 11 | “No new data” | no |
33 5A A5 33 | Non-compatible profile | no |
31 AD DE 31 | Flash erase error | no |
33 AD DE 33 | Flash write error | no |
Things the stick does that nobody documented
- It guards the transfer, not the contents. A wrong CRC-16 gets “CRC error” and a truncated transfer is dropped; neither touches flash. But a block with the wrong model ID was accepted — the stick kept its own ID and stored everything else. The only check against writing another model's configuration is in the software on the PC.
- It recomputes the CRC itself. Send a block with the wrong checksum word and it stores the right one. A host never has to compute it.
- The request after the handshake gets someone else's answer. Every time. Ask again and it works. The vendor program never notices, because it never checks.
- The handshake reply leaks. The bytes around the token are whatever was last in the stick's transfer buffer — including, after a read, part of your configuration in plain text.
Two targets: flash and RAM
A microcontroller has two kinds of memory. Flash is permanent: it survives power-off, and it is where the parameter block is kept. RAM is working memory, wiped at every power-up. The usual design — assumed here, since the firmware itself was not examined — is that the firmware copies the block from flash into RAM when it starts, and runs from the copy.
VKBDevCfg knows about both. Tucked away in its window is a drop-down with two entries,
EEPROM and RAM, that changes nothing but the command number.
(“EEPROM” is the program's word. The stick's own error codes say “erase FLASH” and “write
FLASH”.)
| Target | Read | Write | What the stick did |
|---|---|---|---|
| Flash (“EEPROM”) | 0x02 | 0x24 |
Writes take effect. The new block reads back from both targets at once, before the restart, and again after it. |
| RAM | 0x03 | 0x25 |
Reads work, and always matched flash byte for byte. Writes are answered “block written”, the stick restarts — and neither copy has changed, before the restart or after. |
The obvious guess is that the RAM target is a “try it without saving” mode: change the running configuration, keep the stored one. That is not what happens. A one-setting change sent to RAM was read back a quarter of a second later, before the stick had restarted, and was already gone — or was never applied.
Four variations were then tried, each sending the same one-setting change to RAM:
| Variation | Status | Restart | Change visible |
|---|---|---|---|
“Select profile” (0x17) first, plain and token-bearing | block written | yes, 0.24 s | no |
| Only the 8 bytes that changed, at their offset | block written | yes, 0.27 s | no |
| The same 8 bytes, encrypted at the full block's keystream position | block written | yes, 0.24 s | no |
“Partial-write mask” (0x14) first — acknowledged 5A A5 FE 00 | block written | yes, 0.27 s | no |
Two things follow. “Block written” means only that the transfer arrived intact — the stick says it even for eight bytes it then ignores. And the restart follows any write to this target, however small, so it is not a consequence of replacing the whole block. Sending “select profile” on its own causes no restart and no change.
What remains possible:
- It applies, to something the read does not show. Reading the RAM target has never returned anything different from reading flash. If that read does not reflect the running configuration, a change could take effect for a quarter of a second and be invisible.
- It is for something else. Other device families in the program keep several profiles on the stick; on those, the second target may address a profile slot.
- It is vestigial. A developer feature this firmware acknowledges and no longer implements.
What is established is narrower: on this stick and firmware, flash is the only write target that changes anything. Verified
07The profile file
A .cfg is what Save writes. For a long time it looked like the hard
part: 4096 bytes of noise, 7.95 bits of entropy per byte, and two profiles from nearly
identical configurations that shared almost nothing.
It is the same block, with its CRC word removed, under RC4:
plain = block[0:4] + block[8:4096] // the block, minus its CRC word
file CRC = ~CRC32(plain) // stored in the clear, first four bytes
key = K[0:7] + file CRC + K[11:13] // K is a constant in the program
file = file CRC + RC4(plain, key)
The twist is the checksum in the key. Change one setting and the checksum changes, and with it every encrypted byte in the file. That is why two profiles looked unrelated — and why an early analysis wrongly concluded the files were randomised on every save.
Decrypted, those two sample profiles differ in exactly 2 bytes.
Not locked to a stick
Nothing in the file depends on the device it came from. Only a one-byte model ID inside the block is compared before writing.
Can be diffed
Save, change one setting, save again, compare. This is now the cheapest way to learn what any setting does.
Can be created
Re-encrypting the decrypted samples reproduces both vendor files byte for byte. Verified
08The “encryption”
Not one cipher. Three uses of the same small hand-written RC4 routine, with three keys:
| Layer | Key | Protects |
|---|---|---|
| Handshake reply | 13 bytes, constant, in the program | The stick's serial number, in transit |
| Block on the cable | The stick's own serial number — which it hands to anyone who asks | Your configuration, in transit |
.cfg on disk | 9 bytes of a constant + the file's own checksum | Your configuration, at rest |
Every key is either compiled into a program anyone can download or volunteered by the device. There is no secret anywhere in the system. It is not DRM — the owner holds the stick and the tool — and it is not authentication: the stick accepted reads and writes from a client that is not VKBDevCfg.
The likeliest purpose is the boring one: keeping the format out of casual hands, so that nobody hex-edits a profile and files a support ticket. Keying the cable on the serial number probably does one useful thing by design — a captured write should decrypt to noise on any other stick, which makes it harder to push one unit's calibration into another by accident.
09Writing to a real stick
Reading cannot hurt a device. Writing replaces its whole configuration, calibration included. So writes came last — after the read path had proved the framing, the cipher and both checksums, and after a backup — and the first one sent the stick nothing but its own bytes.
| # | Target | What was sent | Result |
|---|---|---|---|
| 1 | RAM | The stick's own block, unchanged | Accepted; nothing changed |
| 2 | Flash | The stick's own block, unchanged | Accepted; stored block identical |
| 3 | RAM | One setting changed | Accepted; nothing changed |
| 4 | Flash | One setting changed | Accepted; stored, and still there after the restart |
| 5 | Flash | The original | Restored |
| 6 | Flash | The original, with a deliberately “wrong” CRC word | Accepted; the stick stored its own CRC |
| 7–8 | Flash | The change and the restore again, through the published tool | As 4 and 5 |
The setting was the filter grade of physical axis slot 7 — three bits at
offset 0x0B5, on a slot this stick does not use. After the last step the
configuration hashed identically to the backup taken before the first, and the stick
enumerated and reported input normally.
Pressing buttons with nobody there
Confirming a field means seeing its effect, and the effect of a button setting shows when someone presses the button. Nobody was at the stick. Three tricks stood in.
- A held line. The firmware can split an axis into zones, each holding a line down while the axis sits in it. The throttle and the centred stick each rest in a zone, which gives two lines that are permanently pressed.
- A clock. A held line only tests settings that respond to a level. A toggle responds to a press. But one of the stick's own functions, a pulse generator, runs for as long as its line is held — so a held line can be made to tick.
- A wire. The firmware has a table of logic gates. One gate copies the ticking output onto any other line, as if that line's switch were being pressed twice a second. That line then carries whatever is being tested.
About sixty configurations went through this rig: write, wait out the restart, watch the input reports for a few seconds, move on. Pulse widths came out within a millisecond or two of the configured values. Most of the button functions are now confirmed, along with the logic-gate table itself; the misses are listed in the button notes.
The rig also flushed out a report. At rest the stick sends one kind of input report. Once a
line started ticking it sent a second, 0x0C, on every change: the state of all
128 lines after the stick's own processing. It had always been there; nothing had ever
changed for it to announce.
No firmware was flashed. Of the program's short commands, three benign ones were sent: “select profile” and “partial-write mask”, while probing the RAM target, and the one that turns on bus statistics. Nothing that resets a stick to factory defaults, starts a calibration or enters the bootloader — those are catalogued from the code, with a danger rating, and left alone.
10Wrong turns
Five conclusions reached along the way were overturned by evidence. They are recorded rather than quietly fixed, because the way they went wrong is the useful part.
“The program imports no cryptographic functions, so it cannot be encrypting anything.”
A hand-written RC4 is about 190 bytes of code and leaves no trace in an import table.
“Linux blocks feature reports for this device. This needs a Windows machine.”
The request was malformed — the wrong ioctl number. The kernel rejected it before any device was involved, which looked exactly like a refusal.
“The profile key is derived at run time from device state, so a profile may be locked to its stick.”
Right routine, wrong caller. That was the key for the cable. The file key was sitting in the program the whole time.
“Profiles are randomised on every save, so two of them can never be compared.”
The checksum is part of the key. From outside, that is indistinguishable from a random nonce.
“Set sends output report 0x58; Get fetches feature report 0x59.”
A tidy guess from the descriptor. Both use 0x59.
Three of the five have the same shape: not finding the obvious evidence was taken as evidence that there was none.
11Tools
Python 3, standard library only. Everything is in the repository's tools/ directory.
# read a stick — cannot change it
sudo python3 tools/vkb_hid.py info
sudo python3 tools/vkb_hid.py read # -> vkb_feature_block.bin
# profiles, offline, on any OS
python3 tools/vkb_cfg.py decrypt profile.cfg block.bin
python3 tools/vkb_cfg.py encrypt block.bin backup.cfg
python3 tools/vkb_cfg.py diff before.cfg after.cfg
# write — a dry run unless you say otherwise
sudo python3 tools/vkb_hid.py write new.bin
sudo python3 tools/vkb_hid.py write new.bin --yes
write shows what would change and stops. With --yes it saves the
current block first, refuses anything meant for a different model or firmware, checks the
stick's status word, waits out the restart, reads the block back and compares.
If a write goes wrong
While the stick still enumerates, the fix is another write: vkb_hid.py write
with the backup it saved. If a bad configuration stops the stick appearing at all, the
management plane is gone with it, and the way back is the one VKB's manual documents: force
the controller into its bootloader with the hardware control on the board — a BOOT jumper on
most controllers, a button combination held while plugging in on some — then reflash the
firmware with VKB's own ZBootloader. The manual warns that some firmware versions reset
the configuration to defaults, so keep the saved block. From the manual
That route has not been exercised here, and the manual on hand does not give the combination for the Gladiator NXT EVO base. Find it before you need it.
Verified on one stick at one firmware version. The vendor program's own
warning text says an incompatible profile “may cause device inoperability”. Start with
info, keep a backup, change one thing at a time, and never mix any of this with
firmware flashing.
12Open contracts
Confirm the field map
Confirmed on the stick: axes, curves, calibration gains, most button functions, hats, the shift layers and the logic-gate table. Not confirmed: LEDs, the functions that act on axes, and keyboard or media-key output, which never appeared on the wire.
Next: the config tool's live view, for fields that need someone to press a button or move the stick.
Find out what the stick refuses
A damaged or truncated transfer bounces. A block carrying the wrong model ID does not: the stick keeps its own ID and stores the rest. Nothing suggests it checks the contents.
To do: confirm the recovery path below on this model before testing a deliberately bad field.
Other models, other firmware
A second stick, a left-hand Gladiator Omni on the same firmware, reads and writes with the same tool. It reports the same model ID, 0x11, so that byte names a family and not a model.
The program also has branches for older block sizes and a different layout for another device family. None of those was tested.
The rest of the command set
Reset, defaults, calibration, bootloader, macros. Catalogued from the code; never sent.
Which control is which line
The vendor program draws a labelled photo of each stick from tables in its code: trigger on line 10, hat A1 on 17–20, and so on. The owner of a left-hand stick pressed through it and the table held, with one exception (next). Nobody has pressed through a right-hand grip.
The left grip's swapped hat
The vendor's table for the left-hand grip has the top hat's left and right exchanged, in both places the table appears. On the one left-hand stick tested they are not exchanged: it is wired like the right-hand grip.
Either the vendor's labelled diagram is wrong for every left grip, or an older revision really was wired that way. To do: press that hat on a second left-hand stick, ideally an older one, with the live view open.
Buttons, the stragglers
One-shot generators produced no pulse, the NOT gate did not invert, and the Cyclic Switch does nothing on this firmware. The Advanced Mapping layer is undecoded.
What is the RAM target for?
The stick accepts writes to a second target, says “block written”, and changes nothing.
Ruled out so far: a missing “select profile”, a partial write, the partial-write mask. Every variation restarts the stick. To do: find a setting whose effect shows in the live input reports, and watch them across a RAM write.
The editor itself
The actual goal. Everything above is what it needs to be safe.
13Data bank
The full technical record, with addresses, evidence and confidence levels.
Findings
Everything established, split into observed and inferred. Start here.
Protocol
Framing, handshake, read, write, status words, quirks.
.cfg format
The profile container and its cipher.
Parameter block
Region map, and what is verified versus static.
Block: axes
Logical and physical axis records, curves, calibration.
Block: buttons
Physical and logical tables, function types, shift.
Block: global
Header, options, external devices, LED events.
Commands
Every other command, with a danger rating.
Encryption
What the three RC4 layers are for.
Prior art
What was already public: LED control, mostly.
Tools
The device and profile tools, and the analysis scripts.
Pilot's guide
What every tab of VKBDevCfg does, in plain language.