Skip to content
← ARTICLES
REVERSE ENGINEERING6 min read

Hacking My MSI Laptop's RGB Keyboard to Work on Linux

MSI ships RGB keyboard control as Windows-only vendor software. No datasheet, no Linux driver, no docs. Here's how the USB HID protocol got reverse engineered anyway.

BY ASHLIN DARIUS GOVINDASAMY

An MSI gaming laptop's keyboard lighting is controlled entirely by Windows-only vendor software — SteelSeries GG on the models that use a SteelSeries-made keyboard controller. Boot into Linux and the keyboard doesn't go dark, it just freezes on whatever colour it powered on with. No driver, no config file, no published protocol. The hardware is fully capable — it just never agreed to talk to anything except one vendor's app.

Finding the device

Step one is establishing that the keyboard is a USB HID device sitting on the internal bus like any other peripheral, not something soldered directly to firmware with no addressable interface. lsusb finds it immediately, and the vendor/product ID pair is the anchor for everything that follows.

bash
lsusb | grep -i "steelseries\|1038"
# Bus 001 Device 004: ID 1038:1234 SteelSeries ApS Gaming Keyboard

# Confirm it's a HID device and pull the report descriptor
sudo usbhid-dump -d 1038:1234
sudo lsusb -v -d 1038:1234 | grep -A4 "HID Device Class"
Finding the keyboard controller on the USB bus

Sniffing the protocol

With no documentation, the only source of truth is the traffic the official Windows app actually sends. That means capturing it: Wireshark with usbmon on a Linux host, watching the raw HID reports while a Windows VM with USB passthrough drives the keyboard through the official app — set a zone to red, capture; set it to blue, capture; diff the two payloads byte for byte.

bash
sudo modprobe usbmon
sudo wireshark -i usbmon1 -k -f "usb.device_address == 4"
# Trigger a colour change in the vendor app while capturing,
# then diff the report bytes between two known colours
Capturing HID traffic to/from the controller

The diff is where the protocol actually reveals itself. Two captures that only differ in three consecutive bytes, at the same offset, are almost certainly the R, G and B channels of a colour command. Everything else in the payload — report ID, a command opcode, a zone index — stays constant across the diff and gets isolated the same way, one variable at a time.

text
offset  bytes   meaning
0       0x0D    report ID (feature report)
1       0x01    command: set static colour
2       0x00    zone index (0 = zone 1)
3-5     RR GG BB
6       0x00    effect id (0 = static)
7-63    0x00    padding to report length
Isolated feature report layout, byte offset → meaning

Talking to it from Linux

Once the payload shape is known, replicating it from userspace is the easy part — hidapi opens the device by vendor/product ID and sends the same feature report the Windows app was sending, no kernel driver required. The only real friction is permissions: by default the HID device node is root-owned, so a udev rule granting the right group read/write access is what makes this usable without sudo on every run.

python
import hid

VENDOR, PRODUCT = 0x1038, 0x1234

def set_zone(zone: int, r: int, g: int, b: int):
    report = [0x0D, 0x01, zone, r, g, b, 0x00] + [0x00] * 57
    dev = hid.device()
    dev.open(VENDOR, PRODUCT)
    dev.send_feature_report(report)
    dev.close()

set_zone(zone=0, r=34, g=189, b=242)  # ADGSTUDIOS blue, zone 1
Setting one zone to a colour from Linux

From one zone to a wave effect

  • Per-zone, not per-key — this controller exposes a handful of lighting zones, not individually addressable keys, so a 'wave' effect means phase-shifting the same colour cycle across each zone index rather than each key.
  • Feature reports vs. interrupt transfers — getting the transfer type wrong is a silent failure, not an error: the call returns fine and nothing on the keyboard changes.
  • No root, ever — a udev rule scoped to the exact vendor/product ID is what makes this a normal userspace tool instead of a sudo script.
  • Hardware effect modes exist — byte 6 in the report turns out to select breathing/wave/static on-device, which is far cheaper than pushing a new frame from the host every 16ms.
Vendor lock-in is a business decision, not a technical one. The controller doesn't care what OS is asking it to light up — it only ever cared that nobody told it how to ask.
esc
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
`
1
2
3
4
5
6
7
8
9
0
-
=
tab
Q
W
E
R
T
Y
U
I
O
P
[
]
\
caps
A
S
D
F
G
H
J
K
L
;
'
enter
shift
Z
X
C
V
B
N
M
,
.
/
shift
ctrl
fn
alt
alt
ctrl
ins
home
pgup
del
end
pgdn
num
/
*
-
7
8
9
+
4
5
6
1
2
3
enter
0
.
Every zone, driven straight from Linux userspace — no Windows, no vendor app, no driver.

None of this required a datasheet — just a USB analyzer, a diff, and the discipline to change exactly one variable per capture. That's the same method that applies to any undocumented device: capture the known-good traffic, isolate what changes, replicate it in the smallest form that works.

TAGS

LinuxReverse EngineeringUSB HIDRGBMSI

RELATED