Skip to Content

Shadow CE

ARM64 Android 内核级内存扫描器 + 调试器,类 Cheat Engine。

零 ptrace、零 /proc/pid/mem、零 process_vm_readv。全部通过内核模块页表 walk 实现。

技术栈总览

┌─ Python Client (PyQt6) ──────────────────────────────────┐ │ shadow_ce.py → main_window.py → memory_view.py │ │ ce_client.py (TCP) → bp_engine.py → hwbp_window.py │ │ ptrscan_dialog.py → mcp_server.py (18 AI tools) │ └──────────────┬────────────────────────────────────────────┘ │ TCP (CE 7.x 协议兼容) ┌──────────────▼────────────────────────────────────────────┐ │ ce_server (C, static linked, aarch64) │ │ CMD 0x00-0xD8: CE协议 + HW断点 + PTE断点 + 指针扫描 │ └──────────────┬────────────────────────────────────────────┘ │ ioctl(/dev/shadow_ce) ┌──────────────▼────────────────────────────────────────────┐ │ shadow_ce.ko (内核模块) │ │ 页表walk + kmap读写 + 内核态扫描 + PTE断点 + 单步模拟 │ │ + ldxr/stxr原子指令模拟 + 指针扫描 (radix sort + BFS) │ │ + 批量链走查 (chain_walk) │ └───────────────────────────────────────────────────────────┘

内存读写

原理:不用 ptrace,直接在内核态做页表 walk。

  1. pgd_offset → p4d → pud → pmd → pte_offset_map 拿到目标进程的 PTE
  2. vm_normal_page 验证 PTE 安全性(排除 VM_IO/VM_PFNMAP/special/devmap)
  3. kmap_local_page 映射物理页到内核虚拟地址
  4. copy_to_user / copy_from_user 和用户态 buffer 交换数据

支持 huge page (PMD 2MB / PUD 1GB)、每次最大 256KB、跨页自动分段。

写入可执行页时自动 flush_icache_range

内存扫描

内核态扫描——不用逐字节从用户态读,直接在内核里 kmap 全页比较。

  • 首次扫描:遍历 VMA (VMA_ITERATOR),逐页 walk + kmap + 逐对齐地址比较
  • 二次扫描:只检查上次结果地址列表
  • 支持 exact / bigger / smaller / between / changed / unchanged / increased / decreased
  • Float 近似比较用 ULP 容差(整数位操作,不用 FPU)
  • Chunked mmap_read_lock:每 1024 页释放锁 + cond_resched,最大持锁 ~1ms,游戏渲染零感知

指针扫描

内核态 collect-then-BFS——一次 ioctl 完成全流程。

架构

Phase 1: 收集静态区域(file-backed RW VMA) Phase 2: 收集所有指针对(遍历全 RW 页,提取 8B 对齐值)→ 30M pairs in ~100ms Phase 3: LSD Radix Sort(8-pass 8-bit,O(n))→ 30M pairs in <1s Phase 4: BFS 图遍历(binary search sorted pairs)→ 数千条链 in 2-13s

对比 PC Cheat Engine

PC CE (ReadProcessMemory)Shadow CE (页表 walk + kmap)
收集 30M pairs分钟级(逐页 RPM)~100ms
排序qsort 尚可radix sort <1s(原 heapsort 26s)
syscall 开销每次 ~1-2µs零,全在 ioctl 内核态

Chunked Lock

收集阶段每 2048 页释放 mmap_read_lock + cond_resched,最大持锁 ~2ms。游戏渲染线程的 mmap/munmap/brk 操作永不被阻塞超过一帧。

批量链走查 (chain_walk)

SHADOW_CE_CHAIN_WALK ioctl:单次 syscall 走完所有链,返回 (addr, val) per chain。

  • 一次 get_task_mm,chunked lock 每 256 条链释放
  • 替代旧方案:25K 链 × 6 跳 = 150K 次 ioctl → 1 次 ioctl

Rescan 流程

  1. 首次扫描 → 数万条链
  2. 换局 / 改值 rescan → 过滤到数十条
  3. 重启游戏 rescan → 跨重启存活链(5-10 条)
  4. Save/Load .ptr 文件(CE 兼容格式)

客户端优化

  • PtrScanWorker:独立 socket,不跟主窗口抢锁
  • Rescan 后台线程:独立 socket + poll timer,主线程零阻塞
  • Value 刷新:后台线程 + batch chain_rescan,只刷可见行
  • _populate setUpdatesEnabled:批量插入零重绘

断点系统

Page Fault 断点(推荐,默认)

内核级页表异常断点——通过操控 PTE 权限制造用户态 page fault,在内核 fault handler 中截获。

类似 Windows CE 的 “Page Exceptions”,但 CE 的是用户态实现(VEH handler,极慢)。我们直接 hook do_page_fault,全程内核态,60Hz × 20 线程零压力

对比 CE Page Exceptions

CE Page ExceptionsShadow CE Page Fault
实现层用户态 VEH/SEH handler内核态 fault hook
每次 fault 开销~100μs(异常分发链)~2μs(直接 hook)
CE 官方评价”Extremely slow, buggy”60Hz 满速
最大断点数无限64 slots / 32 pages
原子指令死循环内核模拟 ✓

架构(v2 两层设计)

page_trap[32] ← 页级:每个唯一 (mm, page_va) 一个,存 saved_pte + 合并 trap_pte bp_slot[64] ← 断点级:每个用户断点一个,引用 page_trap bp_engine.py ← 客户端:单例 poll 线程 + 按 slot_id 分发到 HwbpWindow

多个断点在同一页上共享一个 page_trapcompute_union_trap_pte 合并所有 slot 的权限需求。

S1PIE 权限控制

SM8750 支持 FEAT_S1PIE(Permission Indirection Extension),PIE index = [UXN:PXN:DBM:USER]

需要 trapPTE 操作EL0 权限不受影响
执行 (X)+PTE_UXNR(只读)读写正常
写 (W)pte_wrprotectR(只读)读执行正常
读/读写 (R/RW)清PTE_USERX_O 或 NONE执行可能正常
多类型混合PTE=0全 trapfault handler 分类

单步窗口 PTE (step_pte)

同页同时有 EXEC + 数据断点时,IABORT 恢复的不是 saved_pte(全权限),而是 compute_step_pte——允许执行但仍 trap 数据访问。防止单步窗口期间数据断点被绕过。

ldxr/stxr 原子指令模拟

ARM64 的 exclusive monitor 在任何异常时被硬件自动清除。单步 ldxr→stxr 序列会导致 stxr 永远失败 → cbnz 无限重试 → spinlock 饿死 → 系统卡死。

解决方案:在 fault handler 中检测 load-exclusive / store-exclusive 指令,直接在内核态模拟:

ldxr Rt, [Rn] → 内核读 [Rn] 写入 regs[Rt],PC += 4 stxr Rs, Rt, [Rn] → 内核写 regs[Rt] 到 [Rn],regs[Rs] = 0(成功),PC += 4

零 exclusive monitor 参与,零无限重试。IABORT(PTE=0)和 DABORT(wrprotect)两条路径都覆盖。

Anti-starvation

高频 fault 会饿死 do_ptebp_set 的 ioctl(spinlock 竞争)。设新断点前临时恢复 saved_pte 暂停 fault,设完再 re-arm。

读穿透

PTE 被修改后 shadow_ce 的 read ioctl 可能读不到数据。ptebp_find_trapped_page 用 saved_pte 的物理页做 fallback。

硬件断点 (HWBP, legacy)

用户态 perf_event_open,不需要内核模块。

  • 精确到字节,但 ARM64 最多 4 个
  • 不支持 ldxr/stxr 模拟——原子操作会死循环
  • 保留为 legacy 选项,默认不使用

UI 设计

参考 Cheat Engine 7.x 布局:

  • 主窗口:上方扫描结果表(QTableView 虚拟模型,百万行零卡)+ 下方地址列表(batch read 刷新)
  • Memory View:ARM64 反汇编(capstone)+ hex dump + 语法着色 + inspector
  • 断点捕获窗口:独立 R/W/X/RW 窗口,按 PC 聚合 hit count + 反汇编 + probable pointer + 寄存器快照
  • 断点管理器:Ctrl+B,显示所有 kernel slots + capture windows
  • 指针扫描:Settings → Results(live Value 刷新)→ Rescan → Save/Load .ptr
  • Workspace 面板:最近进程,双击快速 attach

性能优化

  • scan results:QTableView + ResultsModel 虚拟模型,data() O(1),预缓存字符串
  • 值刷新(scan results)CMD_READ_BATCH(一次 TCP 读 N 个地址),后台线程零主线程阻塞
  • 地址列表:指针链用 chain_rescan 一次 ioctl 批量走完(返回 addr+val),非指针用 read_memory_batch,全在后台线程
  • 指针扫描 Value 刷新:后台线程 + batch chain_rescan,只刷可见行,独立 socket
  • ring buffer:lock-free SPSC(fault handler 写,poll 读),atomic_fetch_add 分配槽位

CE 协议 CMD 编号

CMD编号用途
GET_VERSION0x00握手
OPEN_PROCESS0x03打开进程
READ_MEMORY0x09读内存
WRITE_MEMORY0x0A写内存
VQE_FULL0x1F枚举 VMA
SCAN0xC8首次扫描(内核态)
RESCAN0xC9二次扫描
MODULE_LIST0xCA批量模块列表
READ_BATCH0xCB批量读(N 地址 1 次 TCP)
HWBP_SET0xCC设 HW 断点
HWBP_CLEAR0xCD清断点
HWBP_CLEAR_ALL0xCE清全部
HWBP_POLL0xCF读断点事件
PTEBP_SET0xD0设 PTE 断点
PTEBP_CLEAR0xD1清 PTE 断点
PTEBP_CLEAR_ALL0xD2清全部 PTE 断点
PTEBP_POLL0xD3读 PTE 断点事件
PTEBP_QUERY0xD4查询 PTE 断点 slot
PTRSCAN0xD6指针扫描(内核 BFS)
PTRSCAN_DUMP0xD7导出原始指针对
CHAIN_RESCAN0xD8批量走指针链(返回 addr+val)

内核 ioctl 编号

ioctl编号用途
SHADOW_CE_READ1页表 walk 读内存
SHADOW_CE_WRITE2页表 walk 写内存
SHADOW_CE_LISTPROC3枚举进程
SHADOW_CE_LISTVMA4枚举 VMA
SHADOW_CE_SCAN5内核态扫描
SHADOW_CE_RESCAN6内核态二次扫描
SHADOW_CE_PTEBP_SET10设 PTE 断点
SHADOW_CE_PTEBP_CLEAR11清 PTE 断点
SHADOW_CE_PTEBP_POLL12读 PTE 断点事件
SHADOW_CE_PTEBP_QUERY13查询 slot 信息
SHADOW_CE_PTRSCAN20指针扫描 (collect + radix sort + BFS)
SHADOW_CE_CHAIN_WALK21批量链走查 (返回 addr+val)

文件结构

shadow_ce/ ├── client/ │ ├── shadow_ce.py # 入口 │ ├── main_window.py # 主窗口(scan results + address list) │ ├── connect.py # 连接向导(进程列表 + 实时刷新) │ ├── ce_client.py # CE 协议 TCP 客户端 │ ├── scanner.py # 扫描引擎 │ ├── ptrscan_dialog.py # 指针扫描(settings + results + rescan + save/load) │ ├── bp_engine.py # 断点引擎(singleton poll + slot 分发) │ ├── bp_manager.py # 断点管理器(Ctrl+B) │ ├── hwbp_window.py # 断点结果窗口(寄存器快照 + probable pointer) │ ├── memory_view.py # Memory View(disasm + hex + inspector) │ ├── disasm_view.py # ARM64 反汇编(capstone, QPixmap offscreen) │ ├── hex_view.py # 十六进制视图(QPixmap offscreen + pre-computed hex) │ ├── mem_cache.py # 页级内存缓存(4KB, LRU 128 页) │ ├── add_address_dialog.py # 添加地址对话框(指针链支持) │ ├── settings.py # 设置对话框 │ ├── style.py # 暗色主题 + CeButton + CeSelectionDelegate │ ├── fonts.py # 全局字体管理(preset + NoAntialias) │ ├── dropdown.py # CE-style 下拉按钮 │ ├── i18n.py # 中英文国际化 │ └── mcp_server.py # MCP server(18 个 AI 工具) ├── server/ │ ├── shadow_ce.c # 内核模块(页表walk + 扫描 + PTE断点 + 指针扫描 + 链走查) │ ├── shadow_ce.h # ioctl 定义 │ ├── server.c # TCP server(CE协议 + 全部 CMD 分发) │ ├── hwbp.h # 硬件断点(perf_event_open) │ ├── chain_read.c # 指针链读取调试工具 │ └── Makefile ├── tests/ │ ├── ptebp_v2_samepage.c # 同页 3 类型断点测试 │ ├── ptebp_v2_test.c # 多断点基础测试 │ ├── ptebp_stress.c # 20 线程 EXEC 压力测试(2 页) │ ├── ptebp_rw_stress.c # 20 线程 RW 压力测试 │ ├── ptebp_w_stress.c # 20 线程 WRITE 压力测试 │ ├── ptebp_r_stress.c # 20 线程 READ 压力测试 │ └── ptebp_mixed_stress.c # 5X+5R+5W+5RW 终极压力测试 ├── markdown/ # 指针扫描设计文档 + session 记录 ├── README.md # 本文件 └── CLAUDE.md # AI 操作手册

当前能力

功能状态描述
内存读写✅ 稳定页表 walk + kmap,支持 huge page,256KB/次
内存扫描✅ 稳定内核态全页比较,exact/range/changed/float,chunked lock
指针扫描✅ 稳定内核 collect + radix sort + BFS,rescan,save/load .ptr
批量链走查✅ 稳定单次 ioctl 走 N 条链,返回 addr+val
Page Fault 断点✅ 稳定64 slots,同页多断点,ldxr 模拟,anti-starvation
硬件断点✅ legacyperf_event_open,精确到字节,4 槽位
Memory View✅ 可用ARM64 反汇编 + hex dump + 语法着色
模块解析✅ 可用文件偏移(匹配 IDA),模糊搜索
中英文✅ 可用i18n 热切换
MCP 工具✅ 可用18 个 AI 工具,Claude Code 直接调用

压力测试结果

测试配置结果
ptebp_mixed_stress5X+5R+5W+5RW 同页全部 60Hz ✓
ptebp_stress20 线程 EXEC × 2 页18 断点全命中 ✓
ptebp_rw_stress20 线程 RW 同页满速,原子模拟 ✓
ptebp_w_stress20 线程 WRITE 同页满速,stxr DABORT 模拟 ✓
ptebp_r_stress20 线程 READ 同页满速,ldxr 模拟 ✓

编译

# 内核模块 cd server export PATH="/path/to/aosp-clang/bin:$PATH" make # server aarch64-linux-gnu-gcc -O2 -static -o server server.c -lpthread # 客户端 pip install PyQt6 capstone python client/shadow_ce.py

需要的内核符号(kallsyms)

shadow_ce.ko 通过 kprobe 引导拿到 kallsyms_lookup_name,然后解析:

符号用途
do_page_faultB-detour hook 目标
enable_debug_monitors单步 MDE 启用
disable_debug_monitors单步 MDE 关闭
register_user_step_hook注册单步回调
unregister_user_step_hook注销单步回调
module_alloctrampoline 内存分配
set_memory_xtrampoline 可执行权限
aarch64_insn_patch_text_nosync内核 text 段安全写入

所有间接调用用 __nocfi wrapper(CONFIG_CFI_CLANG 兼容)。

踩过的坑

  • exclusive monitor 死循环 → ldxr/stxr 单步时 monitor 被异常清除 → stxr 永远失败 → 内核模拟为普通 ldr/str
  • spinlock 饿死 → 高频 fault 霸占 ptebp_lock → ptebp_set ioctl 超时 → 设断点前临时恢复 PTE 暂停 fault
  • DABORT 路径遗漏 → wrprotect 页上 stxr 触发 DABORT 不走模拟 → 扩展到 is_write 路径
  • 直接写内核 text → panic → 改用 aarch64_insn_patch_text_nosync
  • 直接写 MDSCR_EL1 → panic(per-CPU 引用计数不平衡)→ 改用 enable_debug_monitors + cpuhp
  • contpte 合并 → PTE 修改不生效 → contpte_try_unfold 先解除再改
  • MDSCR.SS per-CPU 泄漏 → context switch 后单步丢失 → TIF_SINGLESTEP + 自愈 fault handler
  • heapsort 26s → 30M pairs 内核 heapsort cache 效率极差 → LSD radix sort <1s
  • mmap_read_lock 持锁过长 → 游戏渲染卡帧 → chunked lock 每 1024-2048 页释放
  • chain rescan 150K ioctl → 逐跳读取 → 单次 SHADOW_CE_CHAIN_WALK ioctl 批量走查
  • ptrscan rescan 阻塞 UI → 主线程走 25K 链 → 后台线程 + 独立 socket + poll timer
  • lambda 闭包陷阱 → 右键菜单变量被覆盖 → 用默认参数固定值
  • ring buffer 竞态 → 多连接 poll 同一 buffer → bp_engine 单例 poll + slot_id 分发
Last updated on