指针扫描完整实现计划 v4(三轮 Review 终版)
Context
Shadow CE 需要指针扫描功能:从目标地址反向搜索指针链,使地址在游戏重启后可复用。经过 3 轮共 28 个 agent 的 review,所有架构决策、bug 修正、性能优化已确认。
核心优势:纯内核态 page walk + kmap,零用户态 API。预计 0.7-1.7s 完成(CE 需要 10-30s)。
架构
CMD 0xD6 (一条命令全做)
Client → TCP → Server → ioctl → Kernel collect → Server BFS → TCP → Client 展示
内核:page walk + kmap + VMA bitmap O(1) 验证 → 5M 指针对
Server:parallel radix sort → parallel BFS (8线程) → 10K 指针链
Client:纯展示 + 双击添加三轮 Review 确认的关键决策
| 决策 | 状态 |
|---|---|
| Offset 不 reverse | ✅ 亲自 trace 确认正确 |
| vmalloc 非滑动窗口 | ✅ 消除 iterator use-after-free |
| VMA bitmap 非 vma_lookup | ✅ O(1) vs O(log N),省 1s |
| base_offset uint64 非 uint32 | ✅ 防大模块截断 |
| base_addr_str 用 file offset | ✅ ASLR 后 _parse_address 自动转换 |
| BFS visited set | ✅ 防指数爆炸 |
| module_snapshot 含 .rodata | ✅ 防 static 检测漏 const globals |
| 并行 sort + BFS | ✅ server 侧 5-7x 加速 |
两轮 Review 确认的 Bug 修正
P0 — 必须修复
| 缺陷 | 修复 |
|---|---|
| copy_to_user 在 mmap_read_lock 下阻塞目标进程 | 滑动窗口 flush 前 drop lock,flush 后 reacquire(do_listvma 模式)→ 最终改为 vmalloc 模式(不需要 drop lock) |
| UNTAG_ADDR 硬编码 39-bit,48-bit VA 截断指针 | 用内核 untagged_addr() 宏 + val < TASK_SIZE 检查 |
| BFS 队列无上限 → 热门页 branching=2000 → OOM | 每层 cap 50K nodes,总 cap 10K chains |
| static 检测只看第一个 segment → .bss 漏掉 | 遍历所有 module segments(create_module_snapshot 已返回全部) |
P1 — 严重影响可用性
| 缺陷 | 修复 |
|---|---|
| VMA 范围表跨 lock cycle 过时 | 不建表,用 vmalloc 模式(一次 lock,VMA table 始终一致) |
| _open_ptrscan 没传 host/port/pid | 补参数 |
| offset reversed() 双重反转 | 去掉 reversed,BFS 输出已是 CE order |
| max_results=20M → 320MB → OOM | cap 10M |
| signal_pending 缺失 | 内循环加 if (signal_pending(current)) goto out |
内核模块:shadow_ce.h + shadow_ce.c
新增 shadow_ce.h
struct ce_ptr_entry { uint64_t source, target; }; // 16B
struct ce_ptrscan_collect_req {
int pid;
uint32_t flags; // PTRSCAN_ALIGNED_ONLY(1) | PTRSCAN_WRITABLE_ONLY(2)
uint32_t max_results; // 0 = 5M default
unsigned long result_buf;
uint32_t result_count; // out
};
#define SHADOW_CE_PTRSCAN_COLLECT _IOWR(CE_IOC_MAGIC, 20, struct ce_ptrscan_collect_req)do_ptrscan_collect() — vmalloc 模式
关键设计:
- vmalloc 模式(非滑动窗口):vmalloc(80-160MB),一次 mmap_read_lock 完成,unlock 后 copy_to_user
- 两级 VMA bitmap:2MB chunk 粒度,O(1) 验证指针有效性(省 ~1s)
- THP 优化:检测 PMD_SIZE,一次 walk 扫 512 sub-pages(省 ~500ms)
- untagged_addr():内核宏,自动适配 MTE/PAC/TBI
- signal_pending 每页检查
- cond_resched 每 256 页(在 continue 之前,非死代码)
- vmalloc fallback:80MB 失败 → retry 40MB
Server:server.c
CMD_PTRSCAN_RESOLVE (0xD6) — 一条命令全做
线程模型
- 客户端创建专用 TCP 连接(第 5 条)
- server accept → pthread_create → 独立线程处理
- 不影响其他 4 条连接(scan/refresh/freeze/bp)
内存预算
| 数据 | 大小 |
|---|---|
| pairs (10M × 16B) | 160MB |
| cur_level (50K × 16B) | 800KB |
| nxt_level (50K × 16B) | 800KB |
| visited hash set (256K × 8B) | 2MB |
| page_index (256K × 16B) | 4MB |
| chains (10K × ~80B) | 0.8MB |
| 总计 | ~168MB |
BFS 关键修正
- visited hash set:push 前检查
if (visited[src]) continue,防止指数爆炸 - per-level cap 50K:防止 OOM
- inner loop break when nxt_level full:防止百万次无用迭代
- module lookup: 只认 rw- 段为 static base(防止代码段指令被误认为指针)
- saved_levels malloc: check NULL, break BFS gracefully
- memcpy 只复制 depth×4 bytes:不是固定 64 bytes
Wire Protocol
Request: [1B cmd][4B pid][8B target][4B depth][4B offset][4B flags][4B max_chains]
Response: [4B status][4B mod_count][...modules...][4B chain_count][...chains...]
每条链: [4B depth][2B mod_idx][8B base_offset(uint64!)][depth×4B offsets]客户端
ce_client.py: ptrscan_resolve()
- 专用 CEClient 连接(不阻塞其他操作)
- 单次 CMD 0xD6,等待 3-5 秒
- 解析 module header + chains
- base_offset 读 8 bytes (uint64)
ptrscan_results.py: PtrScanWorker
- 用 pyqtSignal(不用 QTimer.singleShot 从子线程调用)
- run() 里创建专用 CEClient → 调 ptrscan_resolve → emit finished
- UI 用 indeterminate progress bar(
setRange(0,0)) - Stop 按钮 close socket → _recv 抛 ConnectionError → 线程退出
双击 → 添加到 addr list
pointer_offsets = offsets(不 reverse,BFS 输出即 CE order)base_addr_str需要正确处理 module segment 映射
性能预估
| 阶段 | 耗时 |
|---|---|
| 内核收集 (bitmap + THP) | 0.5-1.5s |
| copy_to_user | 50ms |
| qsort | 300-600ms |
| BFS | 200-500ms |
| TCP 返回 | <10ms |
| 总计 | 1-3s |
优化方向(未实现)
- VMA 两级 bitmap:O(1) vs O(log N),省 ~1s
- THP 一次 walk 扫 512 页:省 ~500ms
- 并行 radix sort:300-600ms → 40-80ms
- 并行 BFS:200-500ms → 30-80ms
- persistent arena:重复扫描零 malloc
实测发现的问题(2026-03-28 session)
UE4 对象在堆上,不在 .data
- libUE4.so rw- 段只有 84KB
- 全局对象(GEngine/GWorld)通过 .data 间接引用堆
- BFS depth=8 从 target 反向到不了 .data
- 需要 depth > 10 或正向追踪
代码段指令被误认为指针
- ARM64
bl指令0x94000071xxxxxxxxuntag 后 = 有效堆地址 - 修复:只认 rw- 段为 static base
base_offset 解算混乱
- file offset vs segment-relative vs absolute → 三种都试过
- 正确方案未定,可能需要 (module_name, segment_index, offset) 三元组
5M/10M cap 截断
- 4.6GB 进程 → 48M 候选 → 5M 只覆盖 10%
- 10M 覆盖 20%,够找到 level 0 hits
替代架构:内核态 BFS(d5 方案)
不预收集 pair,每层直接 kmap 扫内存:
- 内存 O(4MB) 而不是 O(160MB)
- 每层扫全内存 ~2s × depth 层 = 20s
- 不 OOM,不需要 pair buffer
- 用 sorted cur_level + binary search 检查每个值是否匹配当前层的某个节点
- 最适合大游戏(4GB+)
Last updated on