3-28 通宵 Session: 指针扫描从零到完整功能
时间线
Phase 1: 指针链验证 (开始)
- 从已知的 GEngine 弹药链出发,MCP read_pointer_chain 验证 ammo=17
- 修复
_resolve_pointer的addr -= offset→addr += offset(CE 标准语义) - 发现客户端解算方向反了,这是个存在已久的 bug
Phase 2: BFS per-level 实现 (走错路)
- 实现了 “d5 方案”:每层重扫全内存 + binary search sorted targets
- 32K/level cap → 找到 4096 条 depth 2-3 的短链,全部验证通过
- 但 GEngine depth-8 链因 per-level cap 随机截断永远找不到
- 教训:不该用 per-level BFS,应该直接做 v4 collect-then-BFS
Phase 3: 各种参数调优 (浪费时间)
- 128K/level → 还是不够
- 1M/level → OOM 杀了游戏进程
- 256K/level + min_depth → 找到 depth 8 链但不是 GEngine
- offset_max 从 8192 缩到 5200 → 减少分支但仍然找不到
- max_per_node=3/64/256 → 要么 queue 爆要么路径被抢
- 教训:per-level BFS 对 UE4 dense heap 根本不行
Phase 4: v4 Collect-then-BFS (正路)
- 内核收集 ALL pointer pairs: 30M pairs in 82ms
- Kernel heapsort: 26s (瓶颈)
- BFS on sorted pair graph: 2-13s
- 找到 881 条 depth 8-10 的链
- 但 GEngine 精确链因 BFS visited 最短路径机制被 skip
Phase 5: PC 端暴力搜索 (撞墙)
- CMD 0xD7 dump pairs to PC (318MB in 10.7s)
- C 程序 BFS: 128M queue, 44M nodes, 29K chains → 没有 GEngine
- C 程序 DFS: 100K chains in 0.18s → 全是 linked list 噪声
- Dijkstra (offset-sum as cost): 25K chains → visited 还是挡路
- Beam search: K=50000, stride sampling → 还是不行
- 结论:纯图搜索在 UE4 dense heap 上找不到 depth-8 特定链
Phase 6: IDA Agent 分析 (突破)
- 5 个 subagent 并行扫 IDA
- Agent 1 (ammo strings): 找到 ALPSPMagazine::GetAmmo, TotalAmmo, AddAmmo
- Agent 2 (GetAmmo decompile): 确认 Magazine+0x300=Ammo, +0x2C8=TotalAmmo
- Agent 4 (GEngine chain): 发现 GWorld (0xB038CC0) 比 GEngine 更短
- Agent 5 (class info): 完整 SDK offset map, BP 层 vs C++ 层区别
- 发现 GWorld → GameState 5-hop 链,比 GEngine 8-hop 短 3 跳
Phase 7: GWorld 链验证
- GWorld chain (6 hops): +0x180 +0x38 +0x0 +0x30 +0x260 +0x750 +0x1440 → ammo ✓
- GameState chain (5 hops): +0x120 +0x238 +0x0 +0x280 +0x750 +0x1440 → ammo ✓
- BFS depth 3 自动扫到 GWorld 3-hop chain → ammo=40 ✓
Phase 8: 功能完善
- PTRSCAN_MAX_RESULTS: 4096 → 100000 (实测 95793 chains)
- Results window: Value 列 + QTimer 1.5s 刷新可见行
- CMD 0xD8 CHAIN_RESCAN: 批量走链 (2 chains in 14ms, 无 TCP round-trip)
- Rescan: TCP 探活 + adb pidof fallback + 重连 + 批量过滤
- Reconnect 按钮: 手动重连
- Save/Load .ptr: CE 兼容格式 (magic 0xCE, v2, uncompressed)
- Fix: 双击/Add 不再关闭 results window (signal-based)
- Fix: 多选批量 Add to Address List
Phase 9: Rescan 验证 (最终成果)
- 首次扫描: 25088 chains in 25s
- 换局 rescan (expected=40): 11 chains survive
- 重启游戏 rescan: 5 条链跨重启存活
- 这 5 条链共享尾部 +0x260 +0x10 +0x298 +0x1440
- 来自未知 static global (0xAF0A7xx),ptrscan 自动发现的全新链
已验证的指针链
Ammo 链
| 名称 | 跳数 | Base | Offsets |
|---|---|---|---|
| GameState | 5 | libUE4.so+0xB038CC0 | +0x120 +0x238 +0x0 +0x280 +0x750 +0x1440 |
| GWorld-GI | 6 | libUE4.so+0xB038CC0 | +0x180 +0x38 +0x0 +0x30 +0x260 +0x750 +0x1440 |
| GEngine | 8 | libUE4.so+0xB034CD8 | +0x780 +0x78 +0x38 +0x0 +0x30 +0x260 +0x750 +0x1440 |
| ptrscan发现 | 6 | libUE4.so+0xAF0A7C8 | +0x8 +0x98 +0x260 +0x10 +0x298 +0x1440 |
HP 链
| 名称 | 跳数 | Base | Offsets |
|---|---|---|---|
| HP | 6 | libUE4.so+0xB038CC0 | +0x120 +0x238 +0x0 +0x280 +0x4C0 +0xB0 |
SDK Offset Map
UE4 Engine
| Struct | Field | Offset |
|---|---|---|
| UWorld | AuthorityGameMode | +0x118 |
| UWorld | GameState | +0x120 |
| UWorld | OwningGameInstance | +0x180 |
| AGameStateBase | PlayerArray | +0x238 |
| APlayerState | PawnPrivate | +0x280 |
| AController | Pawn | +0x250 |
| AController | Character | +0x260 |
| UGameInstance | LocalPlayers | +0x38 |
| UPlayer | PlayerController | +0x30 |
Game Specific (FPS2 激战)
| Struct | Field | Offset |
|---|---|---|
| ABP_LPSP_PCH_C | Actor Weapon | +0x750 |
| ABP_LPSP_PCH_C | Unlimited Ammo | +0x6C0 |
| ABP_LPSP_PCH_C | HealthComponent | +0x4C0 |
| ABP_LPSP_WEP_C | Ammunition Current | +0x1440 |
| ABP_LPSP_WEP_C | Damage | +0x14C4 |
| ABP_LPSP_WEP_C | Weapon Settings | +0x6E0 |
| UHealthComponent_C | HP | +0xB0 |
| UHealthComponent_C | Stamina | +0xB4 |
| UHealthComponent_C | MaxHP | +0xB8 |
| ALPSPMagazine | Ammo (C++) | +0x300 |
| ALPSPMagazine | TotalAmmo (C++) | +0x2C8 |
| ALPSPCharacter | Magazine (C++, stale) | +0x508 |
Static Globals (BSS)
| Name | File Offset |
|---|---|
| GEngine | libUE4.so+0xB034CD8 |
| GWorld | libUE4.so+0xB038CC0 |
关键发现
- BFS per-level 对 UE4 不行: 堆指针密度 100-500 per node,per-level cap 必然截断深层链
- v4 collect-then-BFS 可行: 30M pairs 82ms 收集,BFS 图遍历秒级
- BFS visited 最短路径是根本限制: 不是 cap 问题,是算法设计导致特定深链被 skip
- Rescan 是正确方法: scan → rescan 几轮筛选 → 自动发现跨重启稳定链
- IDA agent 分析发现更短链: GWorld/GameState 链比 GEngine 短 2-3 跳
- BP 层 offset 是动态的: execInstanceVariable 从 UProperty 运行时读 offset,但 struct field offset 在编译时固定
- C++ 层 ALPSPCharacter+0x508 死亡后 stale: BP 层 ABP_LPSP_PCH_C+0x750 才是正确的
- 0x1440 不存在于 C++ 代码: 是纯 Blueprint property offset
代码变更
内核 (shadow_ce.c + shadow_ce.h)
- do_ptrscan: v4 collect ALL pairs → sort → BFS graph traversal
- PTRSCAN_F_DUMP: 导出 raw pairs 模式
- PTRSCAN_MAX_RESULTS: 100000
- Depth-adaptive offset, max_per_node visited hash
Server (server.c)
- CMD 0xD6: ptrscan BFS
- CMD 0xD7: dump raw pairs
- CMD 0xD8: batch chain rescan (零 TCP round-trip)
Client
- ce_client.py: ptrscan(), chain_rescan(), min_depth
- ptrscan_dialog.py: Settings + Results + Worker + Save/Load .ptr + Rescan + Reconnect
- main_window.py: 右键 “Pointer scan for this address”, _resolve_pointer += fix
清理
- 删除: ptrscan_test.c, ptrscan_stress.c, pc_bfs.py, ptrscan_diag.py
- 删除: /tmp/pc_*.c (BFS/DFS/Dijkstra/meet-in-middle 实验)
- 保留: chain_read.c (调试工具), markdown/ (文档)
Last updated on