首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
12
9
[原创]APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测
发表于: 2026-6-17 20:55
3052
[原创]APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测
blackr0ck
2
2026-6-17 20:55
3052
# APatch KPM 开发实战:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测 ## 摘要 Android Root 环境隐藏攻防已从用户态文件隐藏、进程隐藏演进到内核层 SELinux 策略检测。LSPosed 团队公开的 DirtySepolicy 方案利用 App Zygote 进程的 SELinux 查询权限,直接探测内核中是否存在 Magisk、KernelSU 等注入的"脏"策略规则,号称"用户态无法绕过"。本文记录基于 KernelPatch/APatch 框架开发 KPM 内核模块(`kpm_selinux_filter`)的完整过程:从 inline hook 方案出发,经历 30 余次迭代、内核崩溃和诊断调试,最终采用 syscall 层拦截方案,以 5184 字节的轻量模块实现了对 DirtySepolicy 和 Duck Detector 的完全绕过。 --- ## 1. 技术基础 ### 1.1 KernelPatch / APatch 框架 KernelPatch 是一个 ARM64 Linux 内核热补丁框架,通过在合法内核镜像的 `load_payload` 段注入自定义 kpimg 二进制实现内核级代码执行。APatch 是基于 KernelPatch 的 Android Root 方案,支持动态加载 KPM(Kernel Patch Module)模块。 KPM 使用裸机 ARM64 交叉编译器(`aarch64-none-elf-gcc`)编译为可重定位 ELF 对象(`.kpm`),由 kpimg 内置的模块加载器加载。生命周期通过特殊 ELF section 管理: ```c KPM_NAME("module-name"); KPM_VERSION("1.0.0"); KPM_LICENSE("GPL"); KPM_INIT(init_function); // section ".kpm.init" KPM_EXIT(exit_function); // section ".kpm.exit" ``` ### 1.2 Hook 机制 KernelPatch 提供两种 hook 机制: | 机制 | API | 原理 | 风险 | |------|-----|------|------| | Inline Hook | `hook_wrapN()` | 修改目标函数指令序言,跳转到 trampoline | 修改代码,可能崩溃 | | FP Hook | `fp_hook_syscalln()` | 替换 `sys_call_table` 中的函数指针 | 仅改一个指针,安全 | 两种 hook 均支持 before/after 回调,回调可修改参数(`args->argN`)、返回值(`args->ret`)或跳过原函数(`args->skip_origin=1`)。 内核符号通过 kfunc 机制按需解析。**关键约定**:`kfunc_def(name)` 声明不可加 `extern`,否则编译器只生成声明不分配存储,`.kpm` 中符号标记为 `*UND*`,KPatch 加载器报 `unknown symbol`。 ```c // ❌ 错误 — extern 导致 *UND*,加载失败 extern int kfunc_def(strncpy_from_user)(...); // ✅ 正确 — tentative definition → .bss,加载正常 int kfunc_def(strncpy_from_user)(...); ``` ### 1.3 检测原理 DirtySepolicy 和 Duck Detector 利用 Android App Zygote 进程(`u:r:app_zygote:s0`)在 `doPreload()` 阶段的 SELinux 查询权限,通过以下方式探测注入的 sepolicy 规则: | 检测方式 | 接口 | 检测内容 | |---------|------|---------| | checkSELinuxAccess | Java API `SELinux.checkSELinuxAccess()` | 查询如 `untrusted_app → magisk:s0` 等规则是否 allow | | contextExists | 写 `/proc/self/attr/current` 或 `/sys/fs/selinux/context` | 验证敏感 context(如 `u:r:magisk:s0`)是否存在 | 规则示例: ``` untrusted_app → magisk:s0 binder call → Magisk 检测 untrusted_app → ksu_file:s0 file read → KernelSU 检测 untrusted_app → lsposed_file:s0 file read → LSPosed 检测 ``` 上述两种检测方式最终都通过 **write syscall** 将检测字符串送入内核——这是本方案的核心立足点。 --- ## 2. 方案探索 ### 2.1 Inline Hook 尝试(v1.0–v2.5) 最初方案与同类 KPM 思路一致:用 `hook_wrap` 挂接 SELinux 内核函数,在函数内部修改参数或返回值来过滤查询。 目标 hook 点: - `security_context_to_sid` — 拦截 context 解析(先于 AV 查询) - `security_compute_av_user` — 拦截 selinuxfs access 写入 - `security_setprocattr` — 拦截 /proc/self/attr/current 写入 **结果:模块加载成功,hook 回调从未触发。** 原因:GKI 内核启用了 LTO(链接时优化),`security_context_to_sid` 被内联到唯一调用者 `sel_write_access` 中。inline hook 替换了导出符号地址处的指令,但内核内部调用根本不经过该地址: ``` sel_write_access() { // security_context_to_sid 被 LTO 内联展开 — inline hook 永远拦截不到 rc = sidtab_context_to_sid(...); ... } ``` ### 2.2 崩溃诊断 除 LTO 绕过问题外,部分函数的 inline hook 直接导致内核 panic。通过二分法逐一验证: | 版本 | hook 组合 | 行为 | 结果 | |------|----------|------|------| | v2.3.1 | setprocattr only, 空 callback | - | ✅ | | v2.3.2 | compute_av_user only, 空 callback | - | ✅ | | v2.3.4 | 双 hook, pr_info only | 只读日志 | ✅ | | v2.3.5 | 双 hook, BEFORE, avd->allowed=0 + skip_origin | 修改输出 | ❌ 崩溃 | | v2.4.0 | 双 hook, BEFORE, argX=1 改写 | 修改输入 | ❌ 崩溃 | | v2.4.2 | setprocattr only, BEFORE, arg3=0 | 单 hook, 改输入 | ✅ | | v2.4.4 | compute_av_user only, BEFORE, arg 改写 | 单 hook, 改输入 | ❌ 崩溃 | **结论**: - `security_setprocattr` 的 BEFORE arg 修改是安全的——但被 LTO 绕过,无实际作用 - `security_compute_av_user` 的任何修改(BEFORE/AFTER/arg/avd/ret/skip_origin 全覆盖)均导致内核 panic,崩溃发生在 hook 首次被触发时(约 30–50ms 后)。即使只改 BEFORE 的 `args->arg0` 这种"无害"操作也会崩溃。空 callback 完全正常,排除 hook 安装错误。疑与 ARM64 非标准函数序言或 CFI 有关。 ### 2.3 转向 Syscall 层(v3.0–v3.2) 两个问题叠加意味着 **inline hook 路线在 `5.4.210-qgki` 上完全走不通**。于是转向 `fp_hook_syscalln`——已被 draw-bypass、hideproc 等模块验证安全的函数指针替换机制。 先后 hook 了 openat / write / read / writev / ioctl,追踪检测进程的 syscall 行为。关键发现: 1. **App Zygote 进程(通过 kfunc `security_task_getsecid` 获取 SID=2165 识别)的 openat/write/read 记录始终为 0**——App Zygote 不在 syscall 层操作 selinuxfs 2. **`strncpy_from_user_nofault` 在此 5.4 内核上不存在**——kfunc 指针为 `0x0000000000000000`。替代方案 `strncpy_from_user`(会睡眠的版本)可用,且 write syscall 本身运行在进程上下文,调用安全 3. **替换为 `strncpy_from_user` 后,成功捕获检测流量**。DirtySepolicy 最终通过普通 `write()` syscall(fd=65,即 `/sys/fs/selinux/` 下的文件)将检测字符串送入内核 ``` BLOCK #1 fd=65: 'u:r:shell:s0 u:r:su:s0 2 2' BLOCK #3 fd=65: 'u:r:app_zygote:s0 u:r:adbroot:s0 2 800000' BLOCK #7 fd=65: 'u:r:app_zygote:s0 u:r:magisk:s0 2 800000' ``` ### 2.4 性能踩坑 v3.3.0 上线后系统卡死重启——原因是未过滤 `fd ≤ 2`(stdout/stderr),KPatch 自身的日志输出(包含 "magisk"/"ksu" 关键字)被拦截了 38000+ 次,导致 write syscall 大面积失败。 修复:`fd <= 2` 直接跳过,payload 限制在 10–800 字节。优化后系统稳定。 --- ## 3. 最终实现(v4.0.0) ### 3.1 架构 ``` ┌─────────────────────────────────────────────────┐ │ Userspace │ │ org.lsposed.dirtysepolicy (AppZygote preload) │ │ │ │ │ │ write("u:r:magisk:s0\n", fd=65) │ │ ▼ │ ├─────────────────────────────────────────────────┤ │ Kernel │ │ fp_hook_syscalln(__NR_write, ...) │ │ │ │ │ │ before_write: │ │ │ 1. fd ≤ 2 → skip │ │ │ 2. count ∉ [10, 800] → skip │ │ │ 3. strncpy_from_user() → kbuf │ │ │ 4. find_dirty(kbuf) ? │ │ │ Y → skip_origin=1, ret=-EINVAL │ │ │ N → pass through │ │ ▼ │ │ sys_write() → selinuxfs handler │ │ (仅干净写入到达此层) │ └─────────────────────────────────────────────────┘ ``` ### 3.2 核心代码(5184 字节) ```c static void before_write(hook_fargs4_t *args, void *udata) { unsigned int fd = (unsigned int)syscall_argn(args, 0); const char __user *ubuf = (const char __user *)syscall_argn(args, 1); long long count = (long long)syscall_argn(args, 2); char kbuf[1024]; long copied; (void)udata; // 快速路径:fd≤2(stdin/out/err),count 异常 → 直接放行 if (fd <= 2 || !ubuf || count < 10 || count > 800) return; if (!kfunc(strncpy_from_user)) return; copied = kfunc(strncpy_from_user)(kbuf, ubuf, (long)(count < 800 ? count : 800)); if (copied <= 0) return; // 滑动窗口匹配脏 context → 拦截 if (find_dirty(kbuf, (unsigned long)copied)) { args->skip_origin = 1; args->ret = -22; /* -EINVAL */ } } ``` ### 3.3 脏 Context 列表 ```c static const char *dirty_ctx[] = { /* Magisk */ "u:r:magisk:s0", "u:object_r:magisk_file:s0", /* KernelSU + variants */ "u:r:ksu:s0", "u:object_r:ksu_file:s0", "u:object_r:ksu_exec:s0", "u:object_r:ksu_tmpfs:s0", "u:object_r:ksu_device:s0", "u:r:ksud:s0", "u:r:ksu_daemon:s0", "u:object_r:ksud_file:s0", "u:object_r:ksud_exec:s0", "u:object_r:ksud_tmpfs:s0", /* LSPosed/Xposed */ "u:object_r:lsposed_file:s0", "u:object_r:xposed_data:s0", "u:object_r:xposed_file:s0", /* su / adb root */ "u:r:su:s0", "u:r:adbroot:s0", /* Duck Detector specific */ "u:r:droidspacesd:s0", "u:r:msd_daemon:s0", "u:r:msd_app:s0", "u:r:duckdetector_dirty_policy_sentinel:s0", /* Common injection artifacts */ "u:object_r:tmpfs:s0", "u:object_r:adb_data_file:s0", }; ``` ### 3.4 性能优化要点 | 优化 | 说明 | 效果 | |------|------|------| | `fd ≤ 2` 过滤 | 跳过 stdin/stdout/stderr | 消除 >99% 扫描(之前 38K 次卡死) | | `10 ≤ count ≤ 800` | 排除单字节和大型 I/O | 进一步精简 | | 滑动窗口匹配 | 栈缓冲区原地搜索,零额外分配 | O(n×m),n,m 均极小 | | `fp_hook_syscalln` | 只改一个指针,不改代码 | 无副作用 | --- ## 4. 方案对比 | | 最初方案(inline hook) | 最终方案(syscall hook) | |---|---|---| | **Hook 位置** | 内核 SELinux 函数(深处) | write syscall(用户态入口) | | **Hook 数量** | 4–5 个函数 | 1 个 syscall | | **Hook 机制** | `hook_wrap`(修改代码) | `fp_hook_syscalln`(改指针) | | **过滤方式** | 修改 avd->allowed / 覆盖返回值 | 读 write buffer → 匹配字符串 → 返回 -EINVAL | | **调用者识别** | 需预计算脏 SID、缓存 App Zygote SID | 无需——脏内容不论谁写都拦 | | **状态管理** | 有状态(SID 缓存) | 无状态 | | **模块大小** | ~14KB | 5184 字节 | | **LTO 影响** | 回调不触发 | 不受影响 | | **`compute_av_user` 修改** | 100% 崩溃 | 不涉及 | 最初方案在 `5.4.210-qgki` 上被放弃的原因: 1. **`security_compute_av_user` 的任何修改都导致内核崩溃**——BEFORE/AFTER/arg/avd/ret/skip_origin 全覆盖验证,无一例外 2. **LTO 内联使 hook 回调从不触发**——`security_context_to_sid` 等函数安装成功,但 kallsyms 地址不是实际调用入口 而 syscall 方案可行的原因:DirtySepolicy 无论走什么内核路径,数据都必须通过 `write()` syscall 进入内核——在城门设卡,不受城里修路(LTO 内联)的影响。 **适用场景**:LTO 不激进的较新内核(≥5.10)可考虑 inline hook 方案以获得更细粒度的调用者区分;5.4 或 LTO 激进的内核推荐 syscall 方案。 --- ## 5. 局限性 1. **脏 context 列表需手动维护**:新 Root 方案可能引入新 context 2. **仅拦截 write syscall**:若检测工具改用 `writev`、`pwrite64` 等,需扩展覆盖 3. **不区分调用者**:对所有进程一视同仁。但正常进程不会写脏 context,实际无影响 4. **内核版本依赖**:`strncpy_from_user` 在 5.4 可用;`_nofault` 变体需 5.10+ --- ## 6. 总结 通过 30 余次迭代,从 inline hook 到 syscall 层的完整迁移,最终以 5184 字节的轻量模块成功绕过 DirtySepolicy 和 Duck Detector 的全部 SELinux 检测项。 核心经验: - **Inline hook 在 LTO 内核上不可靠**——导出符号不等于实际调用入口 - **Syscall 层是最可靠的拦截点**——函数指针替换(`fp_hook_syscalln`)比代码修改(`hook_wrap`)更安全 - **kfunc 符号不可加 `extern`**——必须让编译器分配 .bss 空间 - **性能即正确性**——回调在每次 write 时触发,必须在第一个 `if` 过滤掉绝大多数调用 - **日志也会被拦截**——fd≤2 过滤不仅是性能优化,更是功能正确性保障 ### 加载 ```bash cd kpms/kpm_selinux_filter && make adb push kpm_selinux_filter.kpm /data/local/tmp/ # 通过 APatch App: KPM 模块管理 → 加载 ``` --- ## 参考 - <a href="elink@9bfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1L8h3q4^5x3e0t1I4i4K6u0r3d9$3g2J5L8X3g2D9f1r3q4@1j5$3R3`.">KernelPatch</a> — ARM64 Linux 内核热补丁框架 - <a href="elink@34eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6x3f1#2m8G2M7$3g2V1i4K6u0r3c8r3W2J5N6s2W2e0k6i4m8G2L8r3W2U0P5b7`.`.">DirtySepolicy</a> — LSPosed 团队的 App Zygote SELinux 检测方案 - <a href="elink@5c7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6W2L8s2c8S2N6X3W2F1k6g2)9J5c8V1c8#2j5$3E0Q4x3X3c8p5k6i4c8W2j5%4c8G2M7W2)9J5k6q4u0W2k6X3q4U0N6r3!0J5K9h3&6Y4i4K6u0r3M7s2g2D9L8q4)9J5c8U0t1J5">Duck Detector PR#22</a> — eltavine 的 SELinux context 有效性检测 - [看雪论坛:Android Root 环境隐藏:SELinux 查询探测与对抗](https://bbs.kanxue.com/thread-291232-1.htm) — selinux-query-guard KPM 方案参考
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
12
点赞
・
9
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
benbens
感谢你的贡献,论坛因你而更加精彩!
2026-8-21 16:37
trackL
期待更多优质内容的分享,论坛有你更精彩!
2026-8-13 22:28
mb_liydalll
+1
你的分享对大家帮助很大,非常感谢!
2026-8-10 00:22
mb_imrvxzwd
+1
为你点赞!
2026-7-31 04:41
东方玻璃
为你点赞!
2026-7-23 14:47
Cr0ssx2
感谢你的积极参与,期待更多精彩内容!
2026-7-14 22:25
bluegatar
为你点赞!
2026-6-28 09:49
wx_funcrever
你的分享对大家帮助很大,非常感谢!
2026-6-22 10:40
lingyi223
谢谢你的细致分析,受益匪浅!
2026-6-21 10:33
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
6
)
温泉划水鱼
雪 币:
84
能力值:
( LV1,RANK:0 )
在线值:
发帖
6
回帖
221
粉丝
27
关注
私信
温泉划水鱼
2
楼
build下最新版apatch就可以了,新版权限管理更严格
2026-6-17 21:35
0
blackr0ck
雪 币:
2455
活跃值:
(670)
能力值:
( LV6,RANK:90 )
在线值:
发帖
8
回帖
16
粉丝
51
关注
私信
blackr0ck
2
3
楼
11213是还没修复
2026-6-18 10:20
0
安卓逆向test
雪 币:
499
能力值:
( LV1,RANK:0 )
在线值:
发帖
9
回帖
47
粉丝
65
关注
私信
安卓逆向test
4
楼
难怪我之前写模块的时候发现一些函数hook了没反应,原来是这个原因
2026-6-18 12:51
0
Imxz
雪 币:
112
活跃值:
(9395)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
749
粉丝
9
关注
私信
Imxz
5
楼
t'q;
2026-6-21 09:34
0
mb_khbfrikh
雪 币:
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
mb_khbfrikh
6
楼
学习了
2026-7-23 14:33
0
mb_khbfrikh
雪 币:
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
mb_khbfrikh
7
楼
这个模块开源了吗?在哪里可以下载到?
2026-7-23 14:34
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
blackr0ck
2
8
发帖
16
回帖
90
RANK
关注
私信
他的文章
[原创]一加ACE5至尊版 kernel 6.6.89尝试利用GhostLock / IonStack 进行root提权
48477
[原创]领域特定虚拟机DSVM指令完整还原
3804
[原创]APatch KPM 开发:绕过 DirtySepolicy / Duck Detector 的 SELinux Root 检测
3052
[原创]ARM64 页表断点——基于 PTE 缺页异常的透明拦截、上下文捕获与寄存器实时修改
15254
[原创]内核驱动级内存读写的用户层检测与绕过:一次攻防对抗实践
42601
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部