# 06. Ghi chép Dịch Ngược Tĩnh PathOfExile.exe — Build 6A9E477A (Static Disassembly — Build 6A9E477A)

> **Ngày**: 10/09/2026 | **Client**: PathOfExile.exe (TimeDateStamp `0x6A9E477A` = 07/09/2026 05:11:22 UTC)
> **Công cụ**: `tools/re_pipeline/fingerprint.py` (parse PE thuần), PowerShell `BinaryReader`, Ghidra 12.1.3 + PyGhidra 3.1.0
> **Kết quả**: Đã xác lập fingerprint gốc rễ và chứng minh cơ chế sinh AOB signature tự mở rộng (INV-SIG-UNIQUE)

---

## 1. Fingerprint PE thật của client đang cài (đo độc lập 3 nguồn — khớp 100%)

| Thuộc tính | Giá trị đo thật |
|---|---|
| Đường dẫn | `C:\Program Files (x86)\Grinding Gear Games\Path of Exile 2\PathOfExile.exe` |
| Kích thước tệp | **78.319.448 bytes** (LastWrite 09/07/2026 13:14:35) |
| SHA-256 | `e0cde00d395dbb339f2c2e0812c3d872fe5425c04a88af544ab06e19b07a7504` |
| Machine / Sections | `0x8664` (AMD64) / **7** |
| TimeDateStamp | **`0x6A9E477A`** |
| SizeOfImage / Headers | `0x4C9C000` (~80,33 MB) / `0x400` |
| `.text` | VA=`0x1000` VSize=`0x2E83B6E` (~48,77 MB) Char=`0x60000020` |
| `.rdata` | VA=`0x2E85000` VSize=`0xB7C366` (~12,04 MB) |
| `.data` | VA=`0x3A02000` VSize=`0xD1655C` (~13,72 MB) Char=`0xC0000040` |
| `.pdata` | VA=`0x4719000` VSize=`0x1C4AE4` → ≈ **154.515 hàm** (1.854.180/12 byte RUNTIME_FUNCTION) |
| `_RDATA` / `.rsrc` / `.reloc` | `0x48DE000` / `0x48E3000` (~2,62 MB) / `0x4B63000` (~1,28 MB) |
| SectionLayoutHash (FNV-1a) | **`0x5C4322F5CDC0551A`** — đo từ stdout C++ `AutoPOE2_Tests.exe` Test 60 và `fingerprint.py`, khớp tuyệt đối |

Ba nguồn đo độc lập: (1) PowerShell `BinaryReader` parse PE header; (2) `pe_fingerprint.cpp` trong Test 60;
(3) `tools/re_pipeline/fingerprint.py --hash`. Cả ba cho cùng một SectionLayoutHash.

## 2. So sánh hai build client (bằng chứng `memprobe_report.txt`)

| Thuộc tính | BUILD CŨ (5 phiên) | BUILD MỚI (1 phiên) | Chênh |
|---|---|---|---|
| `.text` VSize | `0x2E840BE` | `0x2E83B6E` | **−0x550** |
| `.rdata` VA | `0x2E86000` | `0x2E85000` | **−0x1000** |
| `.data` VA | `0x3A03000` | `0x3A02000` | **−0x1000** |
| Module size | `0x4C9D000` | `0x4C9C000` (SizeOfImage) | −0x1000 |
| TimeDateStamp | KHÔNG được MemProbe ghi nhận | `0x6A9E477A` | — |

→ 8 static root RVA hardcode (đoạn `kStaticRVAs` trong `tools/memprobe.cpp`, đồng bộ
`kKnownStaticRoots` trong `pointer_chain_resolver.hpp`) suy ra từ build cũ trở nên lệch
hoàn toàn trên build mới; cả 4 AOB pattern trong `docs/development/06` mục 3 `[MISS]`.
Hậu quả thực địa: `captures/20260910_080001_393_PERIODIC_ANALYSIS_RAM.json` (PID 36104) — player `0x0`.

## 3. Smoke test pipeline Ghidra (auto-analysis reuse, 12.3s)

Binary: `bin/Release/AutoPOE2_MemProbe.exe` (binary thật, không phải mock):
- Functions: **360** (Ghidra dựng từ `.pdata`); References quét: **15.725**; Global slots: **681**.
- Thực nghiệm INV-SIG-UNIQUE: pattern ngưỡng `FF 15 ?? ?? ?? ??` (import thunk) khớp **677 lần**,
  `48 8B 05 ?? ?? ?? ??` khớp **57 lần** → bắt buộc mở rộng ngữ cảnh.
- Sau auto-extension: **11/12 candidate UNIQUE**, ví dụ signature đo được:
  - `48 8B 05 ? ? ? ? 48 33 C4 48 89 85 ? ? ? ? 4D 8B F1` (slot `.data` fan-in=40, khớp 1 lần)
  - `FF 15 ? ? ? ? 48 8B D0 48 85 C0 74 11 48 8B 08 48 8B 41 08 48 8B CA FF 15 ? ? ? ? 90 33 D2`
- Offset Registry smoke: `data/re_out/SMOKE/offsets_6AA21741.toml` (11 signature, build MemProbe).
  SSoT SQLite `data/poe2_offsets.db` sau extract đầy đủ chứa **hai** build: `6AA21741` (11) + `6A9E477A` (28).

## 4. Extract đầy đủ PathOfExile.exe — build `6A9E477A` (DONE 10/09/2026)

| Thuộc tính | Giá trị đo thật |
|---|---|
| Lệnh | `python -u tools/re_pipeline/re_extract.py --exe PathOfExile.exe --max-candidates 40 --pattern-window 40 --project-dir data/re_ghidra/client_6A9E477A` |
| Bắt đầu / kết thúc | 11:00:39 → 13:15:06 (UTC+7) |
| `elapsed_seconds` | **8066.23** (~2h14m); ExitCode=0 |
| SHA-256 / layout hash | `e0cde00d…7504` / `0x5C4322F5CDC0551A` (khớp fingerprint) |
| Functions (Ghidra) | **115,021** (ước lượng `.pdata` ~154,515 RUNTIME_FUNCTION — Ghidra gộp/bỏ padding) |
| References / global slots | **4,602,282** / **115,566** |
| Candidates / UNIQUE | 40 / **29** (`.data` 8, `.rdata` 20, `tdb` 1) |
| Registry in-image | **28** signature + **8** `static_root_*` (loại `tdb` TEB `ff00000058`) |
| Artefact | `data/re_out/6A9E477A/extraction.json`, `extract.log`, `bin/Release/offsets_6A9E477A.toml` |

Sau extract, pipeline xếp hạng theo fan-in: `static_root_1 = 0x45CF0A0` (gần cụm RVA compiled cũ `0x45CFEF8`) **không** phải InGameState. Đối chuẩn live 14:00:17 đã chứng minh InGameState = RVA **`0x45C9C90`** (fan-in cũ `static_root_5`); TOML sau label đưa RVA này lên `static_root_1` để Core thử trước.

## 4.1 Đối chuẩn live MemProbe `label-registry` (10/09/2026 14:00:17 — HP **chưa** khớp nhân vật)

Lệnh: `bin/Release/AutoPOE2_MemProbe.exe label-registry` (tự elevate UAC). Client PID **36104**, build **Match** `6A9E477A`. Artefact: `data/re_out/6A9E477A/live_label_6A9E477A.json`.

**Rút lại ngữ nghĩa InGameState/LocalPlayer (10/09/2026 14:26):** nhân vật đang CI (`maxHP = 1`). Số **1330** do `ResolvePlayer` đè struct giả lập `PlayerStateMemory` (XYZ float rồi HP tại +16/+20), không phải LifeComponent thật (`+0` current / `+4` maxHP, CI = `{1,1}`, ES tại +12/+16). `user_settings.expected_max_hp = 503` cũng không khớp 1330. EntityMap `+0x3E8` thất bại.

| Slot | RVA / offset | Kết luận |
|---|---|---|
| `0x45C9C90` | soft-pass `ValidateInGameState` | **ứng viên**, chưa gán `target` |
| `aob_pattern_20` RIP → `0x45C9C90` | cùng slot | chưa gán |
| `InGameState+0x10` | `ResolvePlayer` overlay giả lập | **không** chứng minh LocalPlayer/Life |
| Fan-in `0x45CF0A0` | không pass validator | `unreviewed` |
| EntityMap `+0x3E8` | `entity_map_resolved=false` | chưa gán |
| AOB compiled docs/06 | `match_va=0x0` | `[MISS]` |

## 4.2 Đối chuẩn fingerprint HUD CI (10/09/2026)

HUD 18:18:51 `captures/20260910_181851_HUD_CI_VITALS.png` (Shoreline Hideout, 2560×1440, HWND 854030): Life **1/1**, Shield **2416/2416**, Ward **826/826**, Mana **828/828**, Spirit **8/138**, Rage **0** (không khóa — không có cặp `{cur,max}` độc nhất gần Life).

MemProbe `probe-pools` 18:21:59 và `label-registry` 18:30:15 (PID **36104**, build **Match**) khóa cùng heap report-only **`0x5331D2003E8`**:

| Offset | u32 | Trường |
|---|---|---|
| `+0x00` / `+0x04` | 1 / 1 | curHP / maxHP (CI) |
| `+0x0C` / `+0x10` | 2416 / 2416 | curES / maxES |
| `+0x18` / `+0x1C` | 0 / `0xFFFFFFFF` | **không** phải Mana |
| `+0x20` / `+0x24` | 826 / 826 | curWard / maxWard |
| `+0x420` / `+0x424` / `+0x428` | 828 / 828 / 828 | curMana / maxMana / unreservedMana |
| `+0x43C` | `0xFFFFFFFF` | sentinel giống Life `+0x1C` |
| `+0x44C` / `+0x450` | 8 / 138 | curSpirit / maxSpirit |

`Life+0x58` → `0x5331D200010` toàn 0 — **không** phải VitalStruct mana. Hexdump `READ 0x5331D2007E8` lúc 18:24:51 / 18:30:15.

Nhãn relative (INV-OFFSET-01, không persist heap): `EnergyShield=+0xC`, `Ward=+0x20`, **`Mana=+0x420`**, **`Spirit=+0x44C`**. InGameState vẫn `unreviewed`.

`ptrdeep 5331D2003E8 2000 4` (10/09/2026 15:24–15:30, PID 36104): depth 4, 1.392.750 match, frontier cắt 50.000 — **không** gặp static root trong module.

## 5. Bài học kỹ thuật trong phiên (Rule 11)
1. **PyGhidra 3.1.0 API**: `getReferenceSourceIterator()` trả về ĐỊA CHỈ nguồn (Address),
   phải lấy `Reference` qua `getReferencesFrom()` từng nguồn; `Memory.getBytes(Address,int)`
   không tồn tại — dùng `FlatProgramAPI.getBytes()`.
2. **Ghidra 12.1.3 yêu cầu JDK 25** (Temurin `jdk-25.0.4.101-hotspot`, không phải JDK 21).
3. JPype phải pin **1.5.2** (crash Windows trên 1.6.0 theo release notes của PyGhidra).
4. Pattern ngắn không bao giờ unique trên binary ≥2MB → sinh signature bắt buộc kèm
   cơ chế auto-extension và cổng `match_count == 1`.
5. Block Ghidra `tdb` (GS/TEB) có thể UNIQUE và fan-in cao — **cấm** đưa vào OffsetRegistry;
   chỉ `.data` / `.rdata` / `_RDATA` trong `SizeOfImage`.
6. Project Ghidra client phải **tách** khỏi smoke MemProbe (`data/re_ghidra/client_6A9E477A`
   vs `data/re_ghidra/AutoPOE2_RE`).

## 6. Việc còn lại (lộ trình)
1. ~~Chạy auto-analysis đầy đủ trên `PathOfExile.exe`~~ **DONE** (mục 4).
2. Đối chuẩn live (MemProbe elevated) PID 36104: dump Life CI `{1,1}` + ES 2416 + Ward 826 **DONE** 15:19:27. Mana `+0x420` / Spirit `+0x44C` **DONE** 18:30:15. InGameState/LocalPlayer **chưa** gán — cần ptrdeep từ Life, không dùng overlay `ResolvePlayer`.
3. Xác chứng signature trên ≥2 build (cả hai layout đã có trong `memprobe_report.txt`).
4. Nối `aob_pattern_N` của registry vào Tier 3 của `PointerChainResolver`.
5. Sau cùng: khởi động lại Core trên client thật, xác nhận `player_addr != 0x0` ngay frame đầu (mục tiêu < 1s activation).

Xem kiến trúc đầy đủ: `docs/development/25_static_reverse_engineering_pipeline_and_offset_registry.md`.
