[OneDev #28] peripherals: crack feat 0x0c finalize CRC for G-HUB-independent binding writes #6

Closed
opened 2026-05-24 04:46:36 +00:00 by joseph · 5 comments
Owner

Imported from OneDev issue #28 (id 35528)
Original project: internal-joseph
Original state: Closed
Original submitterId: 5
Original submitDate: 2026-05-05T15:31:59.990+00:00
Original lastActivity: {'date': '2026-05-05T21:25:22.561+00:00', 'description': "changed state to 'Closed'", 'userId': 5}

Original fields:

  • Type: New Feature
  • Priority: Normal
  • Assignees: None

Context

We've cracked the 0x8101 OnboardProfiles-kbd simple-bind wire format end-to-end except the CRC at bytes 7-10 of the finalize LONG packet.

Why this matters

Right now we can REPLAY captured G HUB binding writes verbatim (because the CRCs are precomputed in the captured bytes). We CANNOT compose new bindings from scratch — every keymap variant requires re-capturing G HUB applying it.

If we crack the CRC algorithm, we can emit arbitrary bindings (e.g. "G7 → F13, G8 → custom HID usage 0x80, G9 → mouse left click") without ever launching G HUB.

What we know

From the simple-bind 3-call sequence:

SHORT  10 ff 0c 2b   00 0d 00                                                  ← begin write
LONG   11 ff 0c 3b   00 NN 71 01 00 80 00 00 00 80 02 00 KK 00 00 00            ← payload
LONG   11 ff 0c 9b 1b 05 01 02 00 00 0d  XX XX XX XX  00 00 00 00 00            ← finalize + CRC

XX XX XX XX is uniform-distribution per binding — almost certainly CRC32. The 0d byte just before the CRC is the begin offset (matches the SHORT call's offset param).

Approach

  1. Collect ~50 (binding bytes, finalize CRC) pairs from existing captures
  2. Try standard CRC32 polynomials (IEEE 802.3, Castagnoli, etc.) over various window definitions:
    • Just the LONG payload (16 bytes)
    • The full begin + payload sequence
    • With/without HID++ headers
  3. If standard polys don't match, brute-force: CRC32 with arbitrary 32-bit polynomial via crcalc or similar
  4. Validate by composing a new binding's CRC and writing it — confirm the keyboard accepts it (no 0x8F error)

Done when

  • Algorithm + window definition identified
  • Python implementation in peripherals/scripts/g915x_crc.py
  • End-to-end test: write a binding from scratch + read it back via fn=c memory page reads, verify the CRC the keyboard stored matches what we computed

See also

  • peripherals/g915x-nkro-findings.md — wire format details
  • peripherals/scripts/g915x_open_writes.txt — captured CRCs across multiple bindings
Imported from OneDev issue #28 (id 35528) Original project: internal-joseph Original state: Closed Original submitterId: 5 Original submitDate: 2026-05-05T15:31:59.990+00:00 Original lastActivity: {'date': '2026-05-05T21:25:22.561+00:00', 'description': "changed state to 'Closed'", 'userId': 5} Original fields: - Type: New Feature - Priority: Normal - Assignees: None ## Context We've cracked the `0x8101 OnboardProfiles-kbd` simple-bind wire format end-to-end *except* the CRC at bytes 7-10 of the finalize LONG packet. ## Why this matters Right now we can REPLAY captured G HUB binding writes verbatim (because the CRCs are precomputed in the captured bytes). We CANNOT compose new bindings from scratch — every keymap variant requires re-capturing G HUB applying it. If we crack the CRC algorithm, we can emit arbitrary bindings (e.g. "G7 → F13, G8 → custom HID usage 0x80, G9 → mouse left click") without ever launching G HUB. ## What we know From the simple-bind 3-call sequence: ``` SHORT 10 ff 0c 2b 00 0d 00 ← begin write LONG 11 ff 0c 3b 00 NN 71 01 00 80 00 00 00 80 02 00 KK 00 00 00 ← payload LONG 11 ff 0c 9b 1b 05 01 02 00 00 0d XX XX XX XX 00 00 00 00 00 ← finalize + CRC ``` `XX XX XX XX` is uniform-distribution per binding — almost certainly CRC32. The `0d` byte just before the CRC is the begin offset (matches the SHORT call's offset param). ## Approach 1. Collect ~50 (binding bytes, finalize CRC) pairs from existing captures 2. Try standard CRC32 polynomials (IEEE 802.3, Castagnoli, etc.) over various window definitions: - Just the LONG payload (16 bytes) - The full begin + payload sequence - With/without HID++ headers 3. If standard polys don't match, brute-force: CRC32 with arbitrary 32-bit polynomial via `crcalc` or similar 4. Validate by composing a new binding's CRC and writing it — confirm the keyboard accepts it (no `0x8F` error) ## Done when - [ ] Algorithm + window definition identified - [ ] Python implementation in `peripherals/scripts/g915x_crc.py` - [ ] End-to-end test: write a binding from scratch + read it back via fn=c memory page reads, verify the CRC the keyboard stored matches what we computed ## See also - `peripherals/g915x-nkro-findings.md` — wire format details - `peripherals/scripts/g915x_open_writes.txt` — captured CRCs across multiple bindings
Author
Owner

Imported from OneDev comment id 542 on issue #28
Original userId: -1
Original date: 2026-05-05T17:57:22.702+00:00

State changed as code fixing the issue is committed (b6a6a30abbf90f7aec0341ab094ebc6e361d88e9)

Imported from OneDev comment id 542 on issue #28 Original userId: -1 Original date: 2026-05-05T17:57:22.702+00:00 State changed as code fixing the issue is committed (b6a6a30abbf90f7aec0341ab094ebc6e361d88e9)
Author
Owner

Imported from OneDev comment id 547 on issue #28
Original userId: 5
Original date: 2026-05-05T20:31:26.216+00:00

Reopening — the auto-close from b6a6a30 was premature. The "32-bit CRC at bytes 7-10" framing in the original description is also wrong; it's actually two 16-bit fields. See follow-up comment for the partial crack + remaining work.

Imported from OneDev comment id 547 on issue #28 Original userId: 5 Original date: 2026-05-05T20:31:26.216+00:00 Reopening — the auto-close from b6a6a30 was premature. The "32-bit CRC at bytes 7-10" framing in the original description is also wrong; it's actually two 16-bit fields. See follow-up comment for the partial crack + remaining work.
Author
Owner

Imported from OneDev comment id 548 on issue #28
Original userId: 5
Original date: 2026-05-05T20:31:58.053+00:00

2026-05-05 RE session — half cracked

The "CRC at bytes 7-10" is actually two 16-bit fields, not a single 32-bit CRC. Both must validate for the keyboard to accept the profile commit.

Field 1: data CRC at fn=9 params[7:9] — CRACKED

polynomial:   0x1991       (custom Logitech, not in any public CRC catalog)
initial:      0x0000
reflect in:   yes
reflect out:  yes
input:        page[2:]     (skip the 2-byte 0x00 0x09 record-count header)
storage:      fn=9 params[7:9] big-endian

Verified by:

  1. Uniqueness in 65536-polynomial brute-force space — only 0x1991 produces 0x802d at offset 7 BE for the captured page data
  2. CRC self-check property: crc(page[2:] + crc_LE_appended) == 0x0000

Reference implementation: peripherals/scripts/patch_gkey_color_v2.py (crc16_logitech_8101).

Field 2: secondary check at fn=9 params[9:11] — STILL BLOCKED

Pair sector first CRC (cracked) second field (unknown)
A 0x0065 80 2d fe 01
B 0x0002 41 d9 12 ff

Eliminated:

  • CRC over page[2:] with poly=0x1991 (any init in 65k-space) → no match
  • CRC over page[2:] + crc_BE or page[2:] + crc_LE with poly=0x1991 → no match
  • 65k-poly brute force over fn=9 metadata + first CRC → no joint match for both pairs
  • Simple sum, XOR, Fletcher-16, internet checksum, complement of first CRC

Hardware test: patched the page (80 00 80 purple → ff ff ff white) with a recomputed first CRC and unchanged second field → keyboard rejected the profile commit and fell into preset wave animation (HID++ event-emit mode preserved). Confirms the second field IS validated.

Likely remaining shape

  • A CRC under a different polynomial over input that includes session/device-specific bytes (would explain why we can't match from data alone), OR
  • A custom hash with a Logitech-private constant baked in, OR
  • A signature over more than the page data

Concrete next steps

  1. Capture more pairs — write the same data via G HUB with different sectors / different profile_id values, observe how field 2 varies. Two pairs is undertermined; 5+ should triangulate the input variables.
  2. Ghidra into lghub_agent.exe — find the Feature8101OnboardProfiles vftable and locate the second checksum routine. Same approach as the v1-v4 RTTI walks that cracked FKC activation.
  3. Workaround for now — re-capture Frida writes with G HUB at the desired G-key color; the captured CRC + second field are valid for that color. See peripherals/scripts/RECAPTURE.md.

Artifacts in this session

  • scripts/g915x_dual_collection_io.py — proper Col01-SHORT / Col02-LONG routing helpers
  • scripts/g915x_sector_read.py — paginated 0x8101 sector reader (works after fixing offset encoding bug — params[0:2] BE, params[2]=0)
  • scripts/g915x_sector_scan.py — scan readable sector range (confirms 0x0065 is write-only RAM)
  • scripts/g915x_crc_bruteforce*.py — three iterations of CRC search; v3 found 0x1991
  • scripts/patch_gkey_color_v2.py — patcher with CRC1 recompute (works for CRC1, blocked on CRC2)
  • scripts/RECAPTURE.md — workaround procedure for default G-key color via G HUB recapture

Updated docs

  • peripherals/g915x-rgb-control.md — § "G-key LED control — partial crack (2026-05-05)"
  • peripherals/README.md — phases 3.7 (TOML painter shipped) and 3.8 (CRC partial)
Imported from OneDev comment id 548 on issue #28 Original userId: 5 Original date: 2026-05-05T20:31:58.053+00:00 ## 2026-05-05 RE session — half cracked **The "CRC at bytes 7-10" is actually two 16-bit fields**, not a single 32-bit CRC. Both must validate for the keyboard to accept the profile commit. ### Field 1: data CRC at fn=9 params[7:9] — CRACKED ``` polynomial: 0x1991 (custom Logitech, not in any public CRC catalog) initial: 0x0000 reflect in: yes reflect out: yes input: page[2:] (skip the 2-byte 0x00 0x09 record-count header) storage: fn=9 params[7:9] big-endian ``` Verified by: 1. Uniqueness in 65536-polynomial brute-force space — only `0x1991` produces `0x802d` at offset 7 BE for the captured page data 2. CRC self-check property: `crc(page[2:] + crc_LE_appended) == 0x0000` ✓ Reference implementation: `peripherals/scripts/patch_gkey_color_v2.py` (`crc16_logitech_8101`). ### Field 2: secondary check at fn=9 params[9:11] — STILL BLOCKED | Pair | sector | first CRC (cracked) | second field (unknown) | |------|--------|----------------------|------------------------| | A | 0x0065 | `80 2d` | `fe 01` | | B | 0x0002 | `41 d9` | `12 ff` | Eliminated: - CRC over `page[2:]` with poly=0x1991 (any init in 65k-space) → no match - CRC over `page[2:] + crc_BE` or `page[2:] + crc_LE` with poly=0x1991 → no match - 65k-poly brute force over fn=9 metadata + first CRC → no joint match for both pairs - Simple sum, XOR, Fletcher-16, internet checksum, complement of first CRC Hardware test: patched the page (`80 00 80` purple → `ff ff ff` white) with a recomputed first CRC and unchanged second field → keyboard rejected the profile commit and fell into preset wave animation (HID++ event-emit mode preserved). Confirms the second field IS validated. ### Likely remaining shape - A CRC under a *different* polynomial over input that includes session/device-specific bytes (would explain why we can't match from data alone), OR - A custom hash with a Logitech-private constant baked in, OR - A signature over more than the page data ### Concrete next steps 1. **Capture more pairs** — write the same data via G HUB with different sectors / different profile_id values, observe how field 2 varies. Two pairs is undertermined; 5+ should triangulate the input variables. 2. **Ghidra into `lghub_agent.exe`** — find the `Feature8101OnboardProfiles` vftable and locate the second checksum routine. Same approach as the v1-v4 RTTI walks that cracked FKC activation. 3. **Workaround for now** — re-capture Frida writes with G HUB at the desired G-key color; the captured CRC + second field are valid for that color. See `peripherals/scripts/RECAPTURE.md`. ### Artifacts in this session - `scripts/g915x_dual_collection_io.py` — proper Col01-SHORT / Col02-LONG routing helpers - `scripts/g915x_sector_read.py` — paginated 0x8101 sector reader (works after fixing offset encoding bug — params[0:2] BE, params[2]=0) - `scripts/g915x_sector_scan.py` — scan readable sector range (confirms 0x0065 is write-only RAM) - `scripts/g915x_crc_bruteforce*.py` — three iterations of CRC search; v3 found `0x1991` - `scripts/patch_gkey_color_v2.py` — patcher with CRC1 recompute (works for CRC1, blocked on CRC2) - `scripts/RECAPTURE.md` — workaround procedure for default G-key color via G HUB recapture ### Updated docs - `peripherals/g915x-rgb-control.md` — § "G-key LED control — partial crack (2026-05-05)" - `peripherals/README.md` — phases 3.7 (TOML painter shipped) and 3.8 (CRC partial)
Author
Owner

Imported from OneDev comment id 550 on issue #28
Original userId: 5
Original date: 2026-05-05T21:25:19.019+00:00

2026-05-05 — moot for color control

G-key LED color is fully programmable via 0x8081 little-frame at indices 0xB4-0xBC (G1=0xB4 ... G9=0xBC), cribbed from OpenRGB's G915 controller (DeviceUpdateLEDs adds 0xB3 to zone-local G-key indices for the GKEYS zone). No profile-page CRC needed for colors.

Verified end-to-end with rainbow test (peripherals/scripts/g915x_gkey_color_test.py) — G1=red, G2=orange, ... G9=grey, all paint correctly.

Wired into peripherals/scripts/g915x_paint.py (GKEY_NAME_TO_INDEX) and exposed in TOML config as G1-G9.

Status of the actual CRC question

The "we cracked half (poly 0x1991)" framing was wrong — a follow-up capture proved the polynomial doesn't generalize. The original match was a 1-in-65k coincidence. The 0x8101 fn=9 CRC is NOT a clean polynomial CRC over the visible page bytes; the input includes session/device state we don't have.

But this doesn't matter for color control. The 0x8101 profile page only matters for changing G-key bindings (which key emits what), which is issue #27 territory and a separate concern.

Closing this issue. If/when bindings-without-G-HUB becomes a real need, reopen and pursue Ghidra into lghub_agent.exe's Feature8101 checksum routine.

Imported from OneDev comment id 550 on issue #28 Original userId: 5 Original date: 2026-05-05T21:25:19.019+00:00 ## 2026-05-05 — moot for color control G-key LED color is fully programmable via **0x8081 little-frame at indices 0xB4-0xBC** (G1=0xB4 ... G9=0xBC), cribbed from OpenRGB's G915 controller (`DeviceUpdateLEDs` adds 0xB3 to zone-local G-key indices for the GKEYS zone). No profile-page CRC needed for colors. Verified end-to-end with rainbow test (`peripherals/scripts/g915x_gkey_color_test.py`) — G1=red, G2=orange, ... G9=grey, all paint correctly. Wired into `peripherals/scripts/g915x_paint.py` (`GKEY_NAME_TO_INDEX`) and exposed in TOML config as `G1`-`G9`. ### Status of the actual CRC question The "we cracked half (poly 0x1991)" framing was wrong — a follow-up capture proved the polynomial doesn't generalize. The original match was a 1-in-65k coincidence. The 0x8101 fn=9 CRC is NOT a clean polynomial CRC over the visible page bytes; the input includes session/device state we don't have. But this doesn't matter for color control. The 0x8101 profile page only matters for **changing G-key bindings** (which key emits what), which is issue #27 territory and a separate concern. Closing this issue. If/when bindings-without-G-HUB becomes a real need, reopen and pursue Ghidra into `lghub_agent.exe`'s `Feature8101` checksum routine.
Author
Owner

Imported from OneDev comment id 551 on issue #28
Original userId: 5
Original date: 2026-05-05T21:25:21.561+00:00

Moot for color control — G-keys go through 0x8081 little-frame (0xB4-0xBC), not 0x8101. See follow-up comment.

Imported from OneDev comment id 551 on issue #28 Original userId: 5 Original date: 2026-05-05T21:25:21.561+00:00 Moot for color control — G-keys go through 0x8081 little-frame (0xB4-0xBC), not 0x8101. See follow-up comment.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
joseph/internal-joseph#6
No description provided.