VKBDevCfg // Explained

Debrief // reverse-engineering a joystick

Your stick is 4096 bytes

VKB's configuration program is sixty-nine windows wrapped around one idea it never states. We took it apart, worked out how it talks to the hardware, and then did the same thing without it — reading a Gladiator EVO's configuration from Linux, changing a setting, and putting it back.

Open the config tool Change what your stick's buttons do and how its axes respond, in the browser. Nothing to install, and there is a demo.

Independent, unaffiliated interoperability research, for configuring hardware you own. New to the program itself? Start with the pilot's guide — a plain-language tour of what every tab does. This page is what is underneath.
4096
bytes of configuration, total
0x59
the one HID report that carries it
33
frames to read or write it all
3 / 0
RC4 layers / actual secrets
2.7 s
stick restart after a write

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.

How to read this page

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:

KindDirectionUsed for
Inputdevice → computer, continuouslyThe live state: axes, buttons, hat
Outputcomputer → deviceThings like keyboard LEDs or rumble
Featureeither way, only when askedSettings. 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.

THE STICK STM32 microcontroller + firmware THE COMPUTER Magnetic sensors stick, twist, throttle Switches up to 128 “lines” Grip, modules internal serial bus FIRMWARE 1 Calibrate and filter sensors 2 Apply curves and deadzones 3 Give each line a function button, toggle, hat, shift, encoder… 4 Resolve shifts, run macros 5 Map to axes and buttons 6 Build the input report and drive the LEDs PARAMETER BLOCK 4096 bytes in flash memory axes · buttons · LEDs · curves calibration · everything tells the firmware what to do DATA PLANE interrupt IN endpoint input report 0x01 1000 times a second one way only Operating system generic HID driver — Windows, Linux, macOS no VKB driver, no VKB software The game sees an ordinary joystick: axes, buttons, a hat. Nothing else. MANAGEMENT PLANE control endpoint 0 feature report 0x59 only when asked read and write A configuration tool VKBDevCfg — or anything else that can send a feature report reads the block · edits it · writes it back .cfg profile the same 4096 bytes, saved to a file

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

PropertyValue
DeviceVKBsim Gladiator EVO R — 231d:0200
Firmware2.20.C
InterfacesExactly one, class HID
EndpointsControl endpoint 0, plus one interrupt IN (64 bytes, polled every 1 ms). No OUT endpoint.
ProcessorSTM32 — 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.

ReportTypeSizeRole
0x01Input63 BLive state: six axes, a hat, 128 button bits — and, in bytes the descriptor calls padding, which of the 128 lines are active
0x08Input63 BService packets, off by default: bus statistics, encoder positions
0x0CInput63 BThe 128 lines after the stick's own processing. Sent on every change
0x0BInput63 BOff by default; not decoded
0x58Output63 BNot used for configuration
0x59Feature128 BAll 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.

Parameter block 4096 bytes · sizes in bytes · not to scale
0x000
Marker · type4
CRC-324
Firmware2
Global options · mouse · POV hats50
0x03C
Logical axes 0–78 × 8
Physical axes 0–78 × 8
Response curves8 × 10
0x10C
Physical button table — what each wired line does128 × 2
0x20C
Logical button table — what each line sends to the PC128 × 2
0x30C
Axis → buttons8 × 2
Virtual buttons · address20
External devices — the grip and modules8 × 16
0x3B0
LED masks8
LED events — colour, blink, trigger52 × 6
0x4F0
Boolean functions240
More options · POV 2–358
0x61A
Logical axes 8–1564
Physical axes 8–1564
Curve #32
Not decoded214
Trimmer source64
LED gauges48
0x800
Button table extension128 × 2
“Advanced Mapping” virtual layer — format not decoded512
0xB00
Detents690
Unused590
Header Options Axes Buttons External devices LEDs Detents Confirmed on hardware

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.

Physical axis record 8 bytes · at 0x07C + 8n
byte 0–1
ACn1
ACl1
Inv1
In1
H-filter3
Trm1
Multiplier4
Ext1
Filter grade3
byte 2–3
Dynamic filter threshold8
Base2
Dir1
In1
Eq1
Trim2
In1
byte 4–5
Kd high — calibration8
Kd low — calibration8
byte 6–7
Bias (signed) — calibration13
–3
Logical axis record 8 bytes · at 0x03C + 8n
byte 0–1
Physical axis4
En1
Vis1
Inv1
Ctr1
HID usage4
FL1
–1
2D1
–1
byte 2–3
Precision4
R1
Rel ×2
Crv1
Deadzone, low / centre8
byte 4–5
Deadzone, high8
Comb. axis3
Combine type4
±1
byte 6–7
Combine %7
Rel1
FA3 fixed value8

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.

Physical button entry 2 bytes · at 0x10C + 2 × line
function
Variant3
Function type5
Parameter8
or button
Line under SHIFT 17
11
Line under SHIFT 27
?1
Logical button entry, joystick-button form 2 bytes · at 0x20C + 2 × line
byte 0–1
Joystick button − 17
11
Second button, or FF8

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

LED event 6 bytes · at 0x3B8 + 6n
byte 0–1
R13
G13
B13
R23
G23
B21
byte 2–3
B22
Blink mode3
Colour mode3
Event type4
Inv1
T1
Alt2
byte 4–5
Event parameter8
LED number7
–1

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.

How far to trust these diagrams

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.

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:

LayerKeyProtects
Handshake reply13 bytes, constant, in the programThe stick's serial number, in transit
Block on the cableThe stick's own serial number — which it hands to anyone who asksYour configuration, in transit
.cfg on disk9 bytes of a constant + the file's own checksumYour 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.

#TargetWhat was sentResult
1RAMThe stick's own block, unchangedAccepted; nothing changed
2FlashThe stick's own block, unchangedAccepted; stored block identical
3RAMOne setting changedAccepted; nothing changed
4FlashOne setting changedAccepted; stored, and still there after the restart
5FlashThe originalRestored
6FlashThe original, with a deliberately “wrong” CRC wordAccepted; the stick stored its own CRC
7–8FlashThe change and the restore again, through the published toolAs 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.

Not sent

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.

Caution

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.