Skip to Content
Shadow Cheat EngineDev HistoryPtrscan v4 终版实现计划

指针扫描完整实现计划 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 → OOMcap 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 关键修正

  1. visited hash set:push 前检查 if (visited[src]) continue,防止指数爆炸
  2. per-level cap 50K:防止 OOM
  3. inner loop break when nxt_level full:防止百万次无用迭代
  4. module lookup: 只认 rw- 段为 static base(防止代码段指令被误认为指针)
  5. saved_levels malloc: check NULL, break BFS gracefully
  6. 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_user50ms
qsort300-600ms
BFS200-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 指令 0x94000071xxxxxxxx untag 后 = 有效堆地址
  • 修复:只认 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