首页
社区
课程
招聘
[原创]从 Permission Denied 到 Typebounds:记一次 KernelSU 模块在 Pixel 2 (Android 10) 上不生效的排查之旅
发表于: 3天前 524

[原创]从 Permission Denied 到 Typebounds:记一次 KernelSU 模块在 Pixel 2 (Android 10) 上不生效的排查之旅

3天前
524



## 前言


最近在折腾 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实战!

收藏
免费 3
支持
分享
最新回复 (2)
雪    币: 104
活跃值: (8162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
rbq
3天前
0
雪    币: 104
活跃值: (8162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
楼主 赶紧给 KernelSU 提交 commit   写的非常好 这个应该算是适配性BUG了吧
3天前
0
游客
登录 | 注册 方可回帖
返回