Files
Gamemax-linux-apps/docs/reverse-engineering.md
T
oxpoeandREA (managed-code analysis) f58a011f4d GAMEMAX IceBurg LCD Linux bridge: sender + tray app
Reverse-engineered Linux driver for the LCD panel of the GAMEMAX IceBurg
Digital AIO cooler (HID 5131:2007). Includes:

- src/gamemax_report.py  pure-stdlib sender: reads hwmon/proc sensors and
  streams the 65-byte HID report the firmware understands (no deps)
- src/gamemax_gui.py     PySide6 tray app: animated monitor, fan/temperature
  channel pickers, autostart, blank mode, single instance, crash log
- packaging/             AppImage build script, udev rule, systemd user unit
- tools/                 IL dump helpers used to read the vendor MSIL
- docs/                  full reverse-engineering analysis
- DISCLAIMER.md          Linux-compatibility project, not affiliated with vendor

Co-authored-by: REA (managed-code analysis)
2026-10-13 06:26:52 +07:00

205 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GAMEMAX PcInfoMonitorPro — Recon & HID Protocol
Target: `GAMEMAX_V1.0.0/app/` (installer `GAMEMAX_V1.0.0.04(1).exe`)
Analisis: 2026-10-13, REA 6.4.0 + Ghidra 12.1.2 (managed static provider), IL dumper lokal.
## 1. Isi target
| File | Peran | Teknologi |
|---|---|---|
| `PcInfoMonitorPro.exe` | Aplikasi utama (GUI) | PE32 mixed-mode C++/CLI + Qt5, **logika = MSIL** (457 method body) |
| `allComputerInfoGetPro.exe` | Helper console (dipanggil terpisah) | mixed-mode C++/CLI, sebagian besar CRT |
| `WitmodHardwareInfoLibrary.dll` | Model data sensor (CPU/GPU/mobo) | .NET Framework, CIL murni |
| `LibreHardwareMonitorLib.dll` | Sumber data sensor | OSS pihak ketiga (bukan kode vendor) |
| `HidSharp.dll` | HID library .NET | OSS pihak ketiga (tidak dipakai jalur utama) |
| `hidapi.dll` | I/O HID | OSS, **dipakai** untuk buka/tulis device |
| `Qt5*.dll`, `opengl32sw.dll`, `D3Dcompiler_47.dll` | Framework UI | Qt 5 native |
| `configDir/appConfig.ini` | Konfigurasi app | `[AppInfo] AppName=GAMEMAX, AppVersion=V_1.0.0.04, AppID=ABCDEF6543A1`, warna tema, font Microsoft YaHei |
Catatan: vendor internal tampil sebagai `MyCompanyWitmod` / `WITMOD`; GAMEMAX adalah merek OEM-nya.
## 2. Arsitektur logika
Semua logika aplikasi dikompilasi ke **MSIL** (mixed-mode `/clr`), sehingga
decompile native (Ghidra) pada metode aplikasi menghasilkan sampah. Metode
dideklarasikan sebagai fungsi global bernama bertitik, mis.
`MainHidInterFace.hidApi_Write`, `DMainWidget.onGetPcInfoOk`, di bawah tipe `<Module>`.
Native hanya: Qt DLL, hidapi, CRT.
Alur HID:
```
DMainWidget.onGetPcInfoOk (bangun report 65 byte dari sensor)
-> MainHidInterFace.hidApi_Write (mutex + copy 65 B + QList enqueue)
-> MainHidInterFace.run (thread: QList.takeFirst -> hid_write, poll 500 us)
```
## 3. Device target & pembukaan
`MainHidInterFace.getuUsbHidDeviceList`:
1. `hid_enumerate(0, 0)` — semua HID
2. filter `vendor_id == 20785 (0x5131)` **dan** `product_id == 8199 (0x2007)`
3. ambil `path` (offset 0, string ASCII) → `hid_open_path`
4. daftar device aktif dimuat via `RegisterDeviceNotificationW` (plug/unplug, sinyal
`sigHidDeviceConnectChanging_Into/Remove`, `sigOpenHidSuccessful`)
Tulis **satu arah**: tidak ada `hid_read*` yang diimpor — app hanya mengirim.
## 4. Protokol tulis (terverifikasi dari IL)
- Transfer `hid_write(handle, buf, 65)`: 65 byte = 1 byte report ID + 64 byte payload
- `buf[0] = 0` (report ID 0)
- `buf[1] = 64` (0x40, panjang payload)
- Nilai 16-bit **big-endian** (high byte dulu)
- Suhu: jika checkbox unit tercentang → nilai dipakai apa adanya; jika tidak →
`round((v - 32) / 1.8) | 0x80`. **Bit 0x80 menandakan nilai Fahrenheit.**
- Pengiriman berulang tiap 500 us selama device terbuka
### Layout report (dari `DMainWidget.onGetPcInfoOk`)
| Offset | Isi | Sumber |
|---|---|---|
| 0 | 0 | report ID |
| 1 | 64 | panjang payload |
| 2 | CPU temp | `GetCpu_Temperature` (+flag 0x80 bila °F) |
| 3 | CPU load | `GetCpu_Load` |
| 4..5 | CPU clock (MHz, BE16) | `GetCpu_Clock` |
| 6..7 | CPU fan (RPM, BE16) | `GetCpu_Fan` |
| 8..9 | BE16 dari struct lokal | belum dipetakan |
| 10 | GPU temp | `GetGpu_Temperature` (+flag 0x80) |
| 11 | GPU load | `GetGpu_CoreLoad` |
| 12..13 | GPU clock (BE16) | `GetGpu_CoreClock` |
| 14..15 | GPU fan (BE16) | `GetGpu_Fan` |
| 16..17 | GPU power (BE16) | `GetGpu_Power` |
| 18 | u8 dari struct lokal | belum dipetakan |
| 19..20 | BE16 dari struct lokal | belum dipetakan |
| 21 | u8 dari struct lokal | belum dipetakan |
| 22..23 | BE16 dari struct lokal | belum dipetakan |
| 24..64 | 0 | tidak ditulis jalur ini |
Nilai sensor berasal dari `WitmodHardwareInfoLibrary.Monitor` (wrapper tipis di atas
LibreHardwareMonitor): CPU/GPU Temperature, Load, CoreLoad, Clock, Clock_Max, Fan,
Fan_Max, Power, Power_Max.
## 5. Bukti & artefak
- `analysis/evidence/hid-write-protocol.json` — Evidence bundle REA (12 records, 5.5 MB)
- `analysis/snapshots/pcinfomonitorpro.json` — snapshot Ghidra (bisa dibuka ulang tanpa import)
- `analysis/tools/ildump.py` — disassembler IL (dnfile+dncil) untuk membaca MSIL dengan
operand terselesaikan; venv: `uv venv ilenv && uv pip install --python ilenv/bin/python dnfile dncil`
## 6. Hasil lanjutan
### 6.1 Hanya ada satu jenis perintah HID
Seluruh IL (457 body) dipindai: setiap fungsi hidapi dipanggil tepat sekali
(`hid_init`, `hid_enumerate`, `hid_free_enumeration`, `hid_open_path`,
`hid_set_nonblocking`, `hid_write`, `hid_close`, `hid_exit`) dan `hidApi_Write`
hanya punya **satu pemanggil** (`DMainWidget.onGetPcInfoOk`). Tidak ada
`hid_read*`/`hid_get_feature_report` — tidak ada perintah lain (brightness,
mode, dll.) di jalur ini.
### 6.2 Auto-start + self-elevation (Task Scheduler)
`DMainWidget.initThemeData` (fungsi startup besar, offset IL ~4380-4520):
1. Menulis penanda ke QSettings: `StartupTaskOKver---`
2. `ShellExecuteW(verb="runas", file="schtasks", params=`<br>
`/create /tn "%1" /tr "\"%2\" %3" /sc onlogon /rl highest`
- `%2` = `QCoreApplication.applicationFilePath()` (path exe sendiri)
- `%3` = `---StartupByTaskMinWindow` (penanda argumen)
- verb `runas` → UAC sekali di awal, task berjalan dengan token tertinggi tiap logon
3. `main` memindai `argv` untuk `---StartupByTaskMinWindow`, lalu mencetak
"Program was started by Task Scheduler." / "Program was not started by Task Scheduler."
dan memulai aplikasi terminimalkan
4. Manifest exe: `requestedExecutionLevel level='requireAdministrator'`
Efek: aplikasi otomatis berjalan sebagai admin tanpa prompt UAC di setiap logon.
### 6.3 `allComputerInfoGetPro.exe` = stub test
`main`: `new Monitor(); GetComputerInfo(); Sleep(500); printf("Over ... ..."); getchar();`
— harness uji developer untuk `WitmodHardwareInfoLibrary`, bukan bagian alur runtime.
Nama file ini **tidak dirujuk** oleh PcInfoMonitorPro.exe (ASCII maupun UTF-16) dan
tidak ada di installer.
### 6.4 Layout report — LENGKAP (terverifikasi di device)
| Offset | Isi | Sumber di aplikasi | Terverifikasi |
|---|---|---|---|
| 0 | 0 | report ID | ✅ |
| 1 | 64 | panjang payload | ✅ |
| 2 | CPU temp (0..127) | `GetCpu_Temperature` | ✅ tampil di layar |
| 3 | CPU load % | `GetCpu_Load` | ✅ |
| 4..5 | CPU clock MHz (BE16) | `GetCpu_Clock` | ✅ — **device merender byte ini di slot "RPM"** |
| 6..7 | CPU fan RPM (BE16) | `GetCpu_Fan` | ✅ (device tidak menampilkannya di halaman ini) |
| 8..9 | **CPU power W** (BE16) | `GetCpu_Power` | ✅ |
| 10 | GPU temp (0..127) | `GetGpu_Temperature` | ✅ |
| 11 | GPU load % | `GetGpu_CoreLoad` | ✅ |
| 12..13 | GPU clock MHz (BE16) | `GetGpu_CoreClock` | ✅ |
| 14..15 | GPU fan RPM (BE16) | `GetGpu_Fan` | ✅ |
| 16..17 | GPU power W (BE16) | `GetGpu_Power` | ✅ |
| 18 | RAM usage % | `GlobalMemoryStatusEx` → `dwMemoryLoad` | ✅ |
| 19..20 | total RAM MiB (BE16) | `ullTotalPhys` × 2⁻²⁰ | ✅ |
| 21 | disk free % | `GetDiskFreeSpaceExW` (free/total × 100) | ✅ |
| 22..23 | total disk GB (BE16) | total bytes >> 30 | ✅ |
| 24..64 | nol | tidak dipakai | ✅ |
Catatan pembalikan dari dugaan awal: byte 8..9 **bukan** counter waktu melainkan
CPU power; 19..20 dan 22..23 adalah RAM total dan ukuran disk, bukan waktu.
### 6.5 Perilaku firmware yang ditemukan saat pengujian
1. **Nilai ≥ 128 ditolak.** Aplikasi asli menulis suhu Celsius dengan bit `0x80`
aktif. Firmware IceBurg **mengabaikan field itu** dan mempertahankan nilai terakhir
yang valid (pengujian: 99 °C mentok sampai nilai tanpa flag dikirim). Jadi kirim
suhu 0..127 tanpa flag.
2. **Kedip = indikator "nilai tidak berubah".** Saat semua field statis, angka tengah
berkedip ~1×/detik; saat nilai berubah (suhu naik 40→90) kedip hilang. Bukan efek
laju kirim — kedip tetap ada walau kita berhenti mengirim.
3. **Plafon laju ~8 report/detik.** Endpoint interrupt OUT `bInterval 160 ms`; setiap
`write()` hidraw menunggu URB selesai (~125 ms). Aplikasi asli menumpuk antrean dari
loop 500 µs (`QTimer.setInterval(10)` + `QThread.usleep(500)`), efektifnya sama.
4. **Selalu "streaming"**, tidak ada handshake/init: buka lalu tulis 64 byte.
5. **Slot RPM salah byte.** Diuji silang di hardware: kirim clock=4000 dan fan=1500 →
layar menampilkan 4000 di slot RPM. Jadi firmware memakai byte 4..5 untuk slot itu,
padahal aplikasi asli menaruh clock CPU di sana (bug pasangan app/firmware).
Implementasi pengganti menukar clock/fan agar layar menampilkan RPM fan sungguhan.
6. **Akurasi dibandingkan system monitor**: suhu cocok (selisih hanya pembulatan ke
integer), load cocok; keduanya tampil dengan delay beberapa detik akibat jendela
sampling dan laju device.
## 7. Implementasi pengganti (jalan di host ini)
`analysis/tools/gamemax_report.py` — Python murni, tanpa dependensi:
- auto-detect node hidraw dari VID:PID (`0x5131:0x2007`)
- baca sensor Linux: hwmon (CPU = maksimum Tctl/Tccd*, GPU, fan, power), `/proc/stat`,
cpufreq, `/proc/meminfo`, `statvfs("/")`
- bangun report 65 byte sesuai tabel di 6.4, tulis 64 byte payload ke hidraw
- `--dry-run` mencetak report + nilai; `--listen N` membaca pesan device→host
(device ini tidak pernah mengirim apa pun); `--count`, `--interval`, `--unit c|f`,
`--flag-celsius` (fidelitas ke aplikasi asli), override tiap field via `--cpu-temp` dll.
- `self_check()` mengunci header, urutan big-endian, dan semantik satuan
`analysis/tools/70-gamemax.rules` — udev rule `TAG+="uaccess"` untuk node hidraw.
`analysis/tools/gamemax-report.service` — unit systemd **user** (padanan
`schtasks /sc onlogon` aplikasi asli), sudah aktif di mesin ini:
```bash
systemctl --user status gamemax-report.service
journalctl --user -u gamemax-report -f
systemctl --user disable --now gamemax-report.service # matikan
```
Uji yang sudah lolos: dry-run + assert, kirim nyata, device menampilkan load/RPM/suhu
sesuai kiriman, dan tampilan mengikuti perubahan nilai.
## 8. Device: GameMax IceBurg 240 Digital BK
Ditemukan di mesin ini sebagai `Bus 001 Device 005: ID 5131:2007` (usb.ids salah
menamainya "MSR-101U card reader" — descriptor-nya vendor-defined 0xFF00, 64 byte IN
+ 64 byte OUT, tanpa report ID). Produknya: **GameMax IceBurg 240 Digital BK**, AIO
240 mm dengan blok LCD; "IceBurg 240 Digital Software" dari halaman produk
(gamemaxpc.com/cpu-liquid-cooler/1550.html) adalah `PcInfoMonitorPro.exe` yang kita
bongkar. Layar menampilkan halaman CPU (ikon CPU + load %, suhu besar, RPM, 6 titik
indikator halaman) dan menganimasikan busur kiri/kanan.
Verifikasi: setelah `gamemax-report.service` aktif, device menampilkan suhu/load/RPM
dari sensor Linux, mengikuti perubahan nilai, dan berkedip hanya saat nilai statis
(perilaku firmware, disepakati dibiarkan apa adanya).