## 前言
最近在折腾 Pixel 2 (walleye) 上的 HTTPS 抓包环境,需要将代理工具(Reqable/Charles)的 CA 证书安装为系统信任证书。方案是通过 KernelSU 的 MoveCertificate 模块,在开机时自动把证书挂载到 `/system/etc/security/cacerts/` 目录下。
内核编译一切顺利,KernelSU v0.9.5 + kernel 4.4.210,刷入后 `su` 命令正常工作,Manager 里模块也显示已安装——但所有模块就是不生效。证书纹丝不动,抓包依旧提示证书不受信任。
这篇文章记录了完整的排查过程。如果你也在老设备上编译 KernelSU 并遇到了模块不工作的问题,希望能帮到你。
---
## 环境信息
| 项目 | 值 |
|------|-----|
| 设备 | Google Pixel 2 (walleye) |
| Android 版本 | Android 10 |
| 内核版本 | 4.4.210 |
| KernelSU 版本 | v0.9.5 |
| 目标模块 | MoveCertificate(系统证书挂载) |
---
## 问题现象
刷入自编译的 KernelSU 内核后:
- `su` 命令正常,root 权限没有问题
- KernelSU Manager 中模块状态显示正常
- **但所有模块均无效果** —— ksud 守护进程在开机时根本没有成功执行
- 具体表现:MoveCertificate 模块无法将代理 CA 证书移动到系统信任存储区
---
## 排查过程
### 第一步:查看 dmesg 日志
遇到问题,第一反应当然是看内核日志。
```bash
adb shell su -c dmesg | grep ksud
```
输出:
```
[ 3.367392] init: starting service 'exec 9 (/data/adb/ksud post-fs-data)'...
[ 3.368582] init: cannot execve('/data/adb/ksud'): Permission denied
```
三个阶段(post-fs-data、services、boot-completed)的 ksud 执行全部失败,错误一致:**Permission denied**。
好,问题明确了——ksud 二进制文件无法被 init 进程执行。
### 第二步:初步假设 —— SELinux 文件上下文问题
检查 ksud 的 SELinux 上下文:
```bash
adb shell su -c 'ls -Z /data/adb/ksud'
```
发现其标签为 `u:object_r:adb_data_file:s0`。直觉告诉我,init 进程可能没有权限执行 `adb_data_file` 类型的文件。
尝试修改:
```bash
adb shell su -c 'chcon u:object_r:system_file:s0 /data/adb/ksud'
```
修改后手动执行可以了,但**重启后标签恢复原样**,问题依旧。这说明文件上下文不是根本原因,只是表象之一。
### 第三步:确认 KernelSU 的 SELinux 规则是否注入成功
KernelSU 的工作原理之一是在 init second_stage 时注入自定义的 SELinux 规则。检查 dmesg:
```
[ 1.182693] KernelSU: /system/bin/init second_stage executed
```
说明 `apply_kernelsu_rules()` 确实被调用了。查看 KernelSU 源码,这些规则包含:
```c
ksu_allow(db, "init", "adb_data_file", "file", ALL);
ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL);
```
理论上 init 对 `adb_data_file` 类型的文件应该拥有完整权限。那为什么还是 Permission denied?
### 第四步:关闭 SELinux 测试
既然怀疑是 SELinux 的问题,那就把它彻底关掉试试:
```bash
adb shell su -c 'setenforce 0 && runcon u:r:init:s0 /data/adb/ksud --help'
```
结果令人意外:
```
Permission denied
```
**即使 SELinux 处于 Permissive 模式,以 init 上下文执行 ksud 依然被拒绝!**
这就排除了普通 SELinux allow 规则的问题。如果是 allow 规则不足,Permissive 模式下最多只会打印 avc denied 日志,而不会真正阻止执行。一定有其他机制在起作用。
### 第五步:发现真正的元凶 —— security_bounded_transition
继续翻 dmesg 和 audit 日志,终于找到了关键线索:
```
type=1401 audit(...): op=security_bounded_transition seresult=denied
oldcontext=u:r:init:s0 newcontext=u:r:su:s0
```
**Typebounds!**
这里需要解释一下背景。KernelSU 在注入 init RC 服务时,ksud 的执行上下文被指定为:
```
exec u:r:su:s0 root -- /data/adb/ksud
```
当 init 进程 fork 子进程并尝试从 `u:r:init:s0` 转换到 `u:r:su:s0` 时,内核不仅检查常规的 `allow` 规则和 `type_transition` 规则,还会检查 **typebounds 约束**。
---
## 根因分析:Typebounds 与 Allow 规则的本质区别
这里是这次排查最核心的技术点,值得展开讲讲。
### SELinux 的两层访问控制
大多数人熟悉的 SELinux 机制是 **allow 规则**:
```
allow init adb_data_file:file { read execute open };
```
这是最常见的访问控制,KernelSU 的 `ksu_allow()` 函数就是往运行时 policydb 中注入这类规则。
但 SELinux 还有一层不太为人知的约束机制——**typebounds**(类型边界)。
### 什么是 Typebounds
Typebounds 定义了类型之间的层级关系。当一个域类型(如 `init`)被标记为有界类型(bounded type)时,它只能转换到那些权限不超过其边界类型的域。
简单来说:
- **allow 规则**:回答"这个域能不能做某件事"
- **typebounds 约束**:回答"这个域转换到新域后,新域是否在允许的权限范围内"
Typebounds 的检查发生在 `security_bounded_transition()` 函数中,它比普通 allow 规则具有**更高的优先级**。即使你通过 `ksu_allow()` 注入了所有需要的 allow 规则,如果 typebounds 检查不通过,域转换依然会被拒绝。
### 为什么 setenforce 0 也不管用
这是让我最初困惑的地方。通常 Permissive 模式会放行所有 SELinux 检查,只记录违规不阻止。但 `security_bounded_transition()` 的实现中,**即使在 Permissive 模式下也会返回 -EPERM**。
这是 SELinux 的一个设计决策:typebounds 被视为结构性约束,而非运行时策略,不受 enforcing/permissive 模式切换的影响。
### Android 10 的特殊性
Android 10 的 SELinux 策略对 `init` 域有严格的 typebounds 约束。`init` 的边界类型限制了它能转换到的目标域集合,而 `su` 域不在这个集合中。这就是为什么 init 无法完成到 `u:r:su:s0` 的域转换。
在较新的内核(5.x+)和 Android 版本上,SELinux 架构有所不同,这个问题可能不会出现。但在 kernel 4.4 + Android 10 的组合下,这是一个实实在在的拦路虎。
---
## 修复方案
### 补丁一:绕过 su 域的 Typebounds 检查
修改内核源码 `security/selinux/ss/services.c` 中的 `security_bounded_transition()` 函数,对目标域为 `su` 的转换提前放行:
```c
int security_bounded_transition(u32 old_sid, u32 new_sid)
{
// ... 原有变量声明 ...
if (!ss_initialized)
return 0;
/* KernelSU: allow transition to su domain, bypass bounds check */
{
struct context *nc = sidtab_search(&sidtab, new_sid);
if (nc) {
char *name = sym_name(&policydb, SYM_TYPES, nc->type - 1);
if (name && strcmp(name, "su") == 0)
return 0;
}
}
// ... 原函数剩余逻辑 ...
}
```
这段代码的逻辑很简单:在 typebounds 正式检查之前,先看目标域是不是 `su`,如果是就直接返回 0(允许),跳过后续的边界检查。
### 补丁二:重置 AVC 缓存
在 `KernelSU/kernel/selinux/rules.c` 中,`apply_kernelsu_rules()` 执行完毕后需要调用 `reset_avc_cache()` 来清除 AVC(Access Vector Cache),确保新注入的规则立即生效:
```c
// 在文件顶部添加前向声明
extern void reset_avc_cache(void);
// 在 apply_kernelsu_rules() 末尾添加
reset_avc_cache();
```
AVC 是 SELinux 的访问决策缓存。如果不清除,之前缓存的拒绝决策可能会继续生效,导致新规则"看起来没用"。
### 补充说明:为什么是内核补丁
在 kernel 4.4 上,`CONFIG_KPROBES` 通常是关闭的(Pixel 2 的 defconfig 就是如此),所以 KernelSU 无法使用 kprobe 方式动态 hook 内核函数,必须直接修改内核源码进行编译。这也是为什么我们需要手动 patch `services.c`,而不能通过更优雅的运行时 hook 方式实现。
---
## 验证结果
重新编译内核并刷入后,检查 dmesg:
```
[ 2.923878] init: starting service 'exec 9 (/data/adb/ksud post-fs-data)'...
[ 3.085072] init: Service 'exec 9 (/data/adb/ksud post-fs-data)' (pid 757) exited with status 0
```
三个阶段全部 **exited with status 0**,ksud 成功执行!
KernelSU Manager 中模块状态也变为正常工作。
---
## 证书安装与验证
修复 KernelSU 模块功能后,MoveCertificate 模块的工作流程如下:
1. 将代理工具(Reqable/Charles)的 CA 证书导出,转换为 Android 系统证书格式(PEM,以哈希命名,如 `d210006b.0`)
2. 放置到模块目录:
```
/data/adb/modules/MoveCertificate/system/etc/security/cacerts/
```
该目录需要包含**所有**原系统证书加上你的代理 CA 证书。
3. 重启设备后,KernelSU 的 ksud 会在 post-fs-data 阶段将这个目录以 overlay 方式挂载到 `/system/etc/security/cacerts/`,使代理证书对系统来说与原生系统证书无异。
4. 验证:
```bash
adb shell ls /system/etc/security/cacerts/ | grep d210006b
```
能看到证书文件即表示成功。此时 HTTPS 抓包工具可以正常解密流量。

---
## KernelSU 工作原理补充
为了更好地理解上述排查过程,简要回顾一下 KernelSU 涉及的几个关键机制:
### RC 文件注入
KernelSU 通过 hook `vfs_read` 系统调用,在 init 进程读取 `/system/etc/init/atrace.rc` 时,将自定义的 init service 定义注入到 RC 文件内容中。这些 service 定义了 ksud 在各个启动阶段的执行时机和安全上下文。
### SELinux 策略热修改
KernelSU hook 了 `execve` 系统调用,检测 init second_stage 的执行时机,然后直接修改内核中运行的 SELinux policydb,注入必要的 allow 规则。这种方式不需要修改 `/sepolicy` 文件或使用 `magiskpolicy` 之类的用户态工具。
### 模块挂载
ksud 在 post-fs-data 阶段读取 `/data/adb/modules/` 下各模块的目录结构,通过 bind mount 或 overlay mount 的方式将模块文件挂载到对应的系统路径上,实现无损系统修改(systemless)。
---
## 总结与收获
1. **SELinux 不只有 allow 规则**。Typebounds 是一个容易被忽略但威力巨大的约束机制。它在 allow 规则之上施加了额外的域转换限制,且不受 Permissive 模式影响。
2. **`setenforce 0` 不是万能的**。当你发现关闭 SELinux 后问题依然存在,不要急着排除 SELinux,可能是 typebounds、MLS 约束等高级机制在作怪。
3. **仔细阅读 audit 日志**。`op=security_bounded_transition` 这条日志是破案的关键。如果只看 `Permission denied` 而不深入 audit 日志,很容易走弯路。
4. **老设备 + 老内核有独特的挑战**。Kernel 4.4 没有 kprobe 支持、Android 10 有严格的 typebounds 策略,这些组合在一起让 KernelSU 的适配比新设备困难得多。
5. **理解工具的工作原理很重要**。如果不了解 KernelSU 的 RC 注入、SELinux 策略修改、ksud 执行流程,排查这类问题就会无从下手。
希望这篇记录能对同样在老设备上折腾 KernelSU 的朋友有所帮助。如果你有更优雅的解决方案或者不同的见解,欢迎交流讨论。
---
*本文基于 Pixel 2 (walleye) + Android 10 + Kernel 4.4.210 + KernelSU v0.9.5 环境。不同设备和系统版本的情况可能有所不同。*
最后,有钱没钱还是得换新设备,现在技术迭代快,老旧设备的支持越来越差
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!