首页
社区
课程
招聘
[原创]linux/android 利用shadow内存无痕hook方法
发表于: 2天前 3162

[原创]linux/android 利用shadow内存无痕hook方法

2天前
3162

R^X 分页:在 ARM64 上实现对进程完全透明的代码修改

从一个真实的困境说起

假设你正在做一件很常见的事:hook 一个 Android 应用里的某个函数。

方案很成熟——找到目标函数入口,把前几个字节改成一条跳转指令,跳到你的 hook 函数里。处理完之后再跳回去执行原始逻辑。这就是 inline hook,几乎所有 hook 框架的基本套路。

你写好了 hook,注入进去,完美运行。函数被成功劫持,返回值被修改,一切如你所愿。

然后你发现目标应用闪退了。

翻日志,发现应用在启动后会做一轮"完整性自检"——它把自己 .text 段的每个字节读出来,算一遍 CRC,和编译时的预期值对比。你写入的那条跳转指令,就这样被发现了。

这不是什么罕见的对抗手段。很多加固方案、反作弊系统、甚至一些金融类 App 都会做类似的事:读自己的代码段,检查有没有被人动过。手段可以很简单(CRC、MD5),也可以很暴力(逐字节比对、mmap 后重新校验)。

你的 hook 面临一个尴尬的处境:它必须存在于代码段里才能生效,但只要它存在于代码段里就会被发现。

不只是 inline hook。调试器断点(BRK 指令)、热补丁(NOP 替换)、uprobe——任何需要修改代码页的技术,都面临同一个根本矛盾。

破局:一个地址,两份物理页面

ARM64 的内存管理单元(MMU)负责把虚拟地址翻译成物理地址。翻译的依据是页表,页表的每一项(PTE)记录了两个关键信息:这个虚拟页对应哪个物理页帧,以及访问权限是什么。

通常,一个虚拟地址只对应一个物理页面。但如果我们能在不同的时机,让 PTE 指向不同的物理页面呢?

思路就是这么直接:

准备两个物理页面。 一个是原始代码的完整拷贝,在上面写入你需要的修改(跳转指令、BRK、NOP,随便什么)——叫它 shadow page。另一个就是原封不动的原始页面——叫它 original page

然后,根据访问类型动态切换 PTE

                    虚拟地址 0x1000
                         │
              ┌──────────┴──────────┐
              │                     │
         CPU 取指令             进程 read()
              │                     │
              ▼                     ▼
    ┌──────────────────┐   ┌──────────────────┐
    │   Shadow Page    │   │  Original Page   │
    │                  │   │                  │
    │  ... 原始代码 ... │   │  ... 原始代码 ... │
    │  B <hook_entry>  │   │  原始指令         │  ← 同一偏移
    │  ... 原始代码 ... │   │  ... 原始代码 ... │
    │                  │   │                  │
    │  权限: 仅执行     │   │  权限: 仅读取     │
    └──────────────────┘   └──────────────────┘

为什么 ARM64 能做到"仅执行、不可读"?

ARM64 的 PTE 里有三个和权限相关的关键位:

Bit名称作用
54UXNUser Execute Never,置 1 就禁止用户态执行
7AP[2]置 1 就是只读
6AP[1]置 1 就允许 EL0(用户态)进行数据访问

这里有一个关键的架构特性:ARM64 的指令 fetch 不检查 AP 位

也就是说,即使你不设 AP[1](不允许 EL0 数据访问),用户态程序的 read() 会被拒绝,但 CPU 的指令 fetch 不受影响——只要 UXN 没有置 1,这个页面上的代码就能正常执行。

这就给了我们构造"仅执行"页面的能力:

// Shadow page: 仅执行
// 不设 AP[1] → 用户态无法数据访问(read 会 fault)
// 不设 UXN  → 用户态可以执行(指令 fetch 不看 AP)
shadow_pte = make_pte(shadow_pfn, 0);

// Original page: 仅读取
// 设 AP[1] + AP[2] → 用户态可读、只读
// 设 UXN           → 禁止执行
original_pte = make_pte(orig_pfn, PTE_USER | PTE_RDONLY | PTE_UXN);

有了这组权限编码,"仅执行"和"仅读取"就不再是概念,而是可以写进页表的具体比特位。

读取隐藏:怎么自动切换?

理解了权限编码之后,关键问题是:谁来触发切换?答案是——CPU 自己

正常运行时,代码页处于 SHADOW_X 状态(PTE 指向 shadow page,权限 --x)。程序正常跑着,CPU 取指令一切正常。直到有人试图读这个页面——两个状态之间就开始来回切换:

      ┌──────────────────────────────────────┐
      │                                      │
      │  read fault                exec fault│
      │  (进程读取代码)            (恢复执行) │
      │                                      │
      ▼                                      │
  ┌────────────┐                      ┌──────┴───────┐
  │ ORIGINAL   │ ────exec fault────>  │  SHADOW_X    │
  │   (r--)    │                      │   (--x)      │
  │  原始代码  │  <───read fault────  │  修改后代码  │
  └────────────┘                      └──────────────┘

整个过程对进程完全透明。它读到的永远是干净的原始代码,CRC、MD5、逐字节比对——全部通过。但实际执行的是 shadow page 上被修改过的指令。

切换通过 page fault 自动触发,不需要任何额外的定时器或轮询。读了就切到原始页,执行了就切回 shadow 页,像一个跷跷板。

除了用户态的 read(),还需要处理内核态的读取路径。/proc/pid/memprocess_vm_readv、ptrace 的 PEEKDATA 走的是 GUP(Get User Pages)路径,直接解析页表拿物理页,不触发 fault。对此,我们 hook GUP 的核心函数 follow_page_pte,在读取前临时将 PTE 指向 original page,读完立刻恢复,确保所有读取路径都拿到原始代码。

还有一点值得强调:R^X 的权限拆分仅发生在页表(PTE)层面,VMA 层面完全不受影响。 我们没有修改任何 vm_area_struct 的 vm_flags,所以进程通过 /proc/self/maps 看到的内存权限仍然是原始的 r-xp——不会突然变成 --xp 或 r--p。同理,mprotect 的查询、mincore、以及内核内部遍历 VMA 的逻辑,都看不到任何异常。权限的"跷跷板"只在硬件页表这一层悄悄发生,内核的内存管理子系统对此一无所知。

三条路:R^X 的不同实现方式

R^X 分页的核心思路确定了,但"在哪一层做权限拆分"还有选择。根据干预的层级不同,至少有三种实现路径,各有取舍。

方式一:Hypervisor 方案——在 EL2 做 Stage-2 页表拆分

如果目标进程跑在 ARM 虚拟机里,还有一条更优雅的路。

ARM64 的虚拟化架构有两级地址翻译。Guest 内核管理的是 Stage-1 页表(GVA → GPA,Guest 虚拟地址到 Guest 物理地址),而 Hypervisor 在 EL2 管理 Stage-2 页表(GPA → HPA/HVA,Guest 物理地址到宿主物理地址)。

R^X 的权限拆分可以完全在 Stage-2 页表上做:Hypervisor 为目标 GPA 准备两个 HPA 映射,在 Stage-2 层面控制 R 和 X 权限的切换。Guest 内核的 Stage-1 页表完全不用动。

这是三种方案里最干净的。Guest 内核的页表状态始终一致,不会出现任何异常的 PTE 修改,内核的内存管理子系统感知不到任何异常。从 Guest 的视角看,无论是内核还是用户态,几乎不可能察觉到 R^X 的存在。

方式二:定制内核方案——在内核中原生支持

最直接的思路:既然要操作页表、拦截 fault、处理 TLB,那就直接改内核代码,把 R^X 分页做成内核的原生能力。

好处是实现最完整。你拥有内核的全部上下文——所有结构体定义、所有内部 API、完整的锁和同步原语。不需要猜偏移量,不需要 hook 函数,代码写起来清晰直接。实现难度居中——比 Hypervisor 方案困难(不用搞虚拟化那一套),比 KPM 方案稍简单一些(需要能编译和替换内核)。

代价是设备适配。Android 设备的内核碎片化是出了名的——不同厂商、不同芯片、不同 Android 版本,内核配置和补丁集都不一样。一个 Pixel 上能编译通过的内核,换到三星或小米上可能就不行了。你可能需要为每个目标设备单独编译一个定制内核,维护成本很高。

方式三:KPM 方案——运行时动态加载的内核模块

第三条路是折中:不改内核源码,而是在运行时动态加载一个内核模块,通过 hook 内核函数和直接操作页表来实现 R^X 分页。

具体来说,这里使用 KernelPatch(KP)框架。KP 允许在已 root 的 Android 设备上动态加载内核模块(KPM),提供 hook 系统调用、hook 内核函数、访问 kallsyms 等能力。R^X 分页以 KPM 形式加载,无需重编译内核,也无需重启设备。

优势是通用性相对最好。同一个模块可以跑在不同厂商、不同版本的内核上,前提是内核开启了 CONFIG_KALLSYMS(可以通过 cat /proc/kallsyms 确认)。不需要为每个设备单独编译内核,用户只需要 root + 加载 KPM 就行。

代价是你没有内核的编译期信息。结构体的字段偏移量需要运行时探测,很多内部函数没有导出符号,有些结构体的布局只能靠特征扫描来猜。虽然有 KP 框架兜底,实现门槛比定制内核低,但调试和排查问题的成本不小——定制内核方案里一个 offsetof() 就能解决的事,在 KPM 里需要从 init_task 出发搜 list_head、用 VDSO 的 ELF magic 交叉验证 ASID 偏移等等。

三种方案对比


Hypervisor (EL2)定制内核KPM 模块
干预层级Stage-2 页表内核原生运行时 hook + 直接操作页表
对 Guest/内核的侵入几乎为零中等(改内核源码)中等(hook 内核函数)
实现难度最低(需要 Hypervisor 开发能力)中等最高(依赖 KP 框架)
设备通用性差(需要虚拟机环境)中等(每设备编译内核)较好(需 root + kallsyms)
适用场景Arm服务器/Mac m系列自有设备/可控环境通用 Android 设备

参考实现:wxshadow

上述 R^X 分页技术已在 wxshadow 中以第三种方案(KPM 模块)完整实现,约 7500 行 C,纯内核态,无外部依赖。

wxshadow 在 shadow page 上支持两类修改:断点(写入 BRK #7,触发时可自动修改寄存器)和任意补丁(写入任意字节序列——inline hook 跳板、NOP、指令替换等)。两者可以共存于同一页面。

用户态通过 hook prctl 系统调用进行操作,客户端工具 wxshadow_client 封装了常用测试命令:

# 查看进程可执行区域
wxshadow_client -p <pid> -m

# 设置隐藏断点(按库名+偏移)
wxshadow_client -p <pid> -b libc.so -o 0x12345

# 写入 inline hook 跳板
wxshadow_client -p <pid> -a 0x... --patch 05000014

# NOP 掉一条指令
wxshadow_client -p <pid> -a 0x... --patch d503201f

# 释放 shadow 映射
wxshadow_client -p <pid> -a 0x... --release项目地址

目前 wxshadow 配合 rustFrida 已经实现了基本的 Java 层和native无痕 hook。项目仍在持续优化中,在部分Android版本可能存在一些问题,欢迎反馈。





[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 2天前 被wx_唔.编辑 ,原因:
收藏
免费 42
支持
分享
最新回复 (30)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
2天前
0
雪    币: 10
活跃值: (2453)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
首评666
2天前
0
雪    币: 1408
活跃值: (2433)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
夜深灯冷代码长,
反编译里见锋芒。
字节翻飞寻旧迹,
栈帧回溯破迷障。

影子一页藏天地,
Hook落指改阴阳。
众人只见程序跑,
谁识其间妙手匠。

彪天帝名传逆向界,
一行补丁定四方。
安卓千层迷雾里,
提剑而行是彪郎。
2天前
0
雪    币: 1674
活跃值: (1091)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
夜深灯冷代码长,
反编译里见锋芒。
字节翻飞寻旧迹,
栈帧回溯破迷障。

影子一页藏天地,
Hook落指改阴阳。
众人只见程序跑,
谁识其间妙手匠。

彪天帝名传逆向界,
一行补丁定四方。
安卓千层迷雾里,
提剑而行是彪郎。
2天前
0
雪    币: 1
活跃值: (151)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
牛逼 大佬????‍♂️
2天前
0
雪    币: 5255
活跃值: (6345)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
牛逼
2天前
0
雪    币: 51
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
tql
2天前
0
雪    币: 18
活跃值: (1467)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
tql
2天前
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
夜深灯冷代码长,
反编译里见锋芒。
字节翻飞寻旧迹,
栈帧回溯破迷障。

影子一页藏天地,
Hook落指改阴阳。
众人只见程序跑,
谁识其间妙手匠。

彪天帝名传逆向界,
一行补丁定四方。
安卓千层迷雾里,
提剑而行是彪郎。
2天前
0
雪    币: 212
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
夜深灯冷代码长,
反编译里见锋芒。
字节翻飞寻旧迹,
栈帧回溯破迷障。

影子一页藏天地,
Hook落指改阴阳。
众人只见程序跑,
谁识其间妙手匠。

彪天帝名传逆向界,
一行补丁定四方。
安卓千层迷雾里,
提剑而行是彪郎。
2天前
0
雪    币: 1408
活跃值: (2433)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
彪天帝牛逼
2天前
0
雪    币: 2789
活跃值: (5825)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
13
夜深灯冷代码长,
反编译里见锋芒。
字节翻飞寻旧迹,
栈帧回溯破迷障。

影子一页藏天地,
Hook落指改阴阳。
众人只见程序跑,
谁识其间妙手匠。

彪天帝名传逆向界,
一行补丁定四方。
安卓千层迷雾里,
提剑而行是彪郎。
2天前
0
雪    币: 198
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
tql
2天前
0
雪    币: 1752
活跃值: (3169)
能力值: ( LV4,RANK:49 )
在线值:
发帖
回帖
粉丝
15
tql
2天前
0
雪    币: 5434
活跃值: (15938)
能力值: ( LV9,RANK:230 )
在线值:
发帖
回帖
粉丝
16
点赞,一个硬件断点一个PET ,都是可以不修改内存实现hook,各有各的好 ,能结合一下是最好的。
2天前
1
雪    币: 656
活跃值: (960)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
LittleQ发来贺电
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
6666
2天前
0
雪    币: 764
活跃值: (3242)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
6666
2天前
0
雪    币: 21
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20

我不是卷王发来贺电
2天前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
硬件断点命中了直接pc到自己的逻辑去,是一个比较成熟的无痕hook方案了
2天前
0
雪    币: 25
活跃值: (7613)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
无敌了
2天前
0
雪    币: 24
活跃值: (1508)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
开始掀桌子了
2天前
0
雪    币: 104
活跃值: (7852)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
tql
2天前
0
雪    币: 3397
活跃值: (6720)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
25
你这个思路很润,我拿走了,哈哈
2天前
0
游客
登录 | 注册 方可回帖
返回