文章总结: 文档介绍LSPosed团队开源的DirtySepolicy检测方案,该方案通过AppZygote机制在Android环境中查询SELinux策略以检测Root环境。文章详细分析了检测原理,包括利用isolatedProcess触发私有Zygote执行敏感策略查询,并提出了对抗方案selinux-query-guard内核模块,通过hookSELinux相关函数过滤敏感查询结果。内容涵盖技术实现细节、内核hook方法和版本兼容性处理。 综合评分: 85 文章分类: 移动安全,二进制安全,内核安全,逆向分析,安全工具
Android Root 环境隐藏:SELinux 查询探测与对抗
用户名null 用户名null
看雪学苑
2026年6月1日 17:59 上海
在小说阅读器读本章
去阅读
LSPosed 团队公布的一个新的 root 检测方案,把查询SELinux policy这种检测方式进行了开源。
如果系统里加载过额外 sepolicy,可能会多出一些 domain、type 或 allow 规则。普通应用虽然不能直接读很多 root 相关文件,但它可以通过系统接口查询某些 SELinux 结果。只要返回值和正常环境不一样,就能变成检测点。
DirtySepolicy 的实现可以简单理解为:用 isolatedProcess 配合 useAppZygote 触发应用私有 Zygote,再通过 zygotePreloadName 把检测代码放到 app_zygote 上下文里执行。只要这个载体能调用 SELinux.checkSELinuxAccess() 向内核查询当前策略,额外 sepolicy 规则就会变成普通应用能读到的信号。
1. DirtySepolicy介绍
DirtySepolicy 通过 AppZygote 这条链路检测。
配置上大概是这样:
<application
android:zygotePreloadName="org.lsposed.dirtysepolicy.AppZygote">
<service
android:name=".DirtySepolicyService"
android:isolatedProcess="true"
android:useAppZygote="true" />
</application>
isolatedProcess 配合 useAppZygote 会触发应用私有 Zygote,zygotePreloadName 指向的类会在 preload 阶段执行。DirtySepolicy 在正式查询前还会做一些自检,比如确认 UID 没被换掉、当前 context 是 app_zygote,并比对 getContext()、getPidContext() 和 /proc/self 文件上下文。
自检通过以后,检测点主要落在两个地方。
1.1 SELinux.checkSELinuxAccess
Java 层可以通过类似下面的接口查询 SELinux 访问结果:
SELinux.checkSELinuxAccess(source, target, clazz, perm)
DirtySepolicy 里查询面比较宽,类似这些:
u:r:system_server:s0 -> u:r:system_server:s0 process execmem
u:r:fsck_untrusted:s0 -> u:r:fsck_untrusted:s0 capability sys_admin
u:r:shell:s0 -> u:r:su:s0 process transition
u:r:adbd:s0 -> u:r:adbroot:s0 binder call
u:r:untrusted_app:s0 -> u:r:magisk:s0 binder call
u:r:untrusted_app:s0 -> u:object_r:ksu_file:s0 file read
u:r:untrusted_app:s0 -> u:object_r:lsposed_file:s0 file read
u:r:untrusted_app:s0 -> u:object_r:xposed_data:s0 file read
u:r:zygote:s0 -> u:object_r:adb_data_file:s0 dir search
如果这些查询返回 true,应用就能判断 live sepolicy 里可能存在额外规则。
底层会走到 SELinux 的 context 转换和 AV 计算:
context string -> sid
sid + class + perm -> av_decision
对应到内核函数,大概就是:
security_context_to_sid
security_context_str_to_sid
security_compute_av_user
1.2 /proc/self/attr/current
另一种方式是写:
/proc/self/attr/current
写入一个目标 context 后,内核会走 SELinux procattr 和 transition 检查。如果返回值不是预期的 EINVAL,也可能被拿来做判断。
对应路径大概是:
selinux_setprocattr
security_bounded_transition
security_validate_transition
#
2. selinux-query-guard
检测出来了,那就产生了对抗,最近写了对抗的KPM 模块: selinux-query-guard,用来处理这类 SELinux 查询探测。
做法是在内核 SELinux 相关函数上挂 hook,应用 UID 走到查询入口时,对一些敏感结果做过滤。实现还比较粗,更多是一次针对 DirtySepolicy 思路的 KPM 侧尝试。
selinux-query-guard 加载后会直接安装 hook。
入口:
KPM_INIT(selinux_query_guard_init);
KPM_CTL0(selinux_query_guard_control0);
KPM_EXIT(selinux_query_guard_exit);
初始化时调用:
return install_hooks();
install_hooks() 里通过 kallsyms_lookup_name() 找符号,再用 KernelPatch 的 hook_wrap() 挂回调。
3. hook的函数说明
按用途分几类。
3.1 context 转 SID
security_context_to_sid
security_context_str_to_sid
security_context_to_sid_force
security_secctx_to_secid
security_secid_to_secctx
这里主要用来观察应用在查什么 context。
如果普通 app 或 isolated app 解析了下面这类 context,就先把这个来源记下来:
u:r:magisk:s0
u:r:ksu:s0
u:object_r:ksu_file:s0
u:object_r:lsposed_file:s0
u:object_r:xposed_data:s0
3.2 AV 查询
security_compute_av_user
security_compute_av
SELinux.checkSELinuxAccess() 最后关心的是 av_decision.allowed。
命中敏感来源后,模块会把:
avd->allowed
改成:
0
这样应用看到的就是 deny。
这里没有把所有查询都改掉。代码里保留了一个基础查询能力:
if (avd->allowed == SELINUX_ACCESS_QUERY_ALLOWED) {
p->reason = "preserve-selinux-access-query-capability";
return;
}
如果把 SELinux 查询能力本身破坏掉,检测样本可能直接报环境异常,所以这块不能粗暴处理。
3.3 procattr
security_setprocattr
selinux_setprocattr
这里处理 /proc/self/attr/current。
before hook 里记录写入的 name 和 value。
如果是 current,并且 value 是敏感 context,after hook 里再判断是否需要改返回值。
命中后返回:
-EINVAL
3.4 transition
security_bounded_transition
security_validate_transition
security_validate_transition_user
写 attr current 后,有些内核会继续走 transition 检查。这里也做了返回值过滤,避免从另一条路径露出差异。
4. 如何过滤敏感查询
模块里放了一个列表:
static const char *interesting_contexts[] = {
"u:r:app_zygote:s0",
"u:r:isolated_app:s0",
"u:r:ksu:s0",
"u:r:ksu_file:s0",
"u:r:magisk:s0",
"u:r:magisk_file:s0",
"u:r:lsposed_file:s0",
"u:r:xposed_data:s0",
"u:r:system_server:s0",
"u:r:fsck_untrusted:s0",
"u:r:shell:s0",
"u:r:su:s0",
"u:r:adbd:s0",
"u:r:adbroot:s0",
"u:r:untrusted_app:s0",
"u:r:zygote:s0",
"u:object_r:ksu_file:s0",
"u:object_r:lsposed_file:s0",
"u:object_r:xposed_data:s0",
"u:object_r:adb_data_file:s0",
};
另外还做了前缀判断:
u:r:ksu*
u:r:magisk*
u:r:lsposed*
u:r:xposed*
u:object_r:ksu*
u:object_r:magisk*
u:object_r:lsposed*
u:object_r:xposed*
这块现在写得比较直接。好处是简单,坏处是后续要维护。后面可以考虑把模块加载时机再提前,在 SELinux 初始化完成但脏规则加载之前,先抓一份干净 policy,后续把这份干净策略当白名单来判断查询结果。
5. 怎么区分是否需要hook
不能只看context进行区分。因为系统进程、shell、root 管理器自己也可能查这些东西。直接拦会误伤。
所以模块会记录当前 task 的信息:
struct source_info
{
pid_t pid;
pid_t tgid;
uid_t uid;
uid_t euid;
uid_t fsuid;
const char *comm;
const char *source_class;
bool is_app_uid;
bool is_isolated_uid;
bool is_privileged_uid;
bool is_root_manager_comm;
bool is_sniffer;
};
分类时先放行 root manager 和 system 这类来源:
if (src->is_root_manager_comm) return "root-manager";
if (src->is_privileged_uid) return "privileged/system";
然后再判断是否是已经记录过的探测来源:
if (source_matches_learned_probe(src)) {
src->is_sniffer = true;
return "sensitive-selinux-prober";
}
如果普通 app 或 isolated uid 命中了敏感 context,就记录 UID 和 TGID:
if (sensitive_probe && (src->is_app_uid || src->is_isolated_uid)) {
if (src->is_isolated_uid) learned_probe_isolated_uid = src->uid;
if (src->is_app_uid) learned_probe_uid = src->uid;
learned_probe_tgid = src->tgid;
src->is_sniffer = true;
return "sensitive-selinux-prober";
}
这样做是为了兼容 AppZygote 和 isolatedProcess。检测逻辑可能不在主进程里跑,前一次 context 解析和后一次 AV 查询也不一定完全在同一个进程状态里。
6. 实际修改位置
实际修改只有两处。
第一处是返回值:
p->proposed_ret = -ERRNO_EINVAL;
args->ret = (uint64_t)(int64_t)p->proposed_ret;
用于 procattr 和 transition。
第二处是 AV decision:
p->proposed_allowed = 0;
avd->allowed = p->proposed_allowed;
用于 security_compute_av_user。
这两个修改都要先判断来源。不是 app UID,或者没有命中过敏感 context,不会触发改动。
7. 内核版本处理
不同内核版本里,SELinux 函数参数位置不一样。这里用了一个简单判断:
static bool need_selinux_compat_signature(void)
{
return kver >= VERSION(4, 17, 0) && kver < VERSION(6, 4, 0);
}
如果命中这个区间,就走 *_compat 回调。主要是取参数的位置不同:
context 可能是 arg0,也可能是 arg1
avd 可能是 arg3,也可能是 arg4
oldsid/newsid 的位置也可能不同
#
8. 编译
目录:
KernelPatch/kpms/selinux-query-guard
编译:
make
产物:
selinux_query_guard.kpm
如果工具链路径不同,可以自己传:
make KP_DIR=/path/to/KernelPatch TARGET_COMPILE=/path/to/aarch64-none-elf-
#
9. 不足
这个版本能用,但是没有完全自适应root的selinux policy
目前的问题:
敏感 context 是硬编码
UID/TGID 没有过期机制
isolated UID 有复用风险
没有读取 current task 的 SELinux context
AV 查询里没有还原完整 source/target/class/perm
不同设备上符号可能不全
如果继续做,比较值得改的是白名单来源。现在是看到敏感 context 就学习来源,后面可以在 SELinux 初始化前后找一个更早的时机,保存一份干净 policy。应用查询时,优先用这份干净策略判断:干净策略里没有的 context 或 allow,就按普通失败处理。再加上 UID/TGID 过期机制,误伤会少一些。
特别感谢 LSPosed 和 DirtySepolicy 项目的开源分享。
#
看雪ID:用户名null
https://bbs.kanxue.com/user-home-807294.htm
*本文为看雪论坛优秀文章,由 用户名null 原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿
# 往期推荐
安卓逆向基础知识之frida Hook
2025 强网杯和强网拟态部分题解
在逆向分析方面-unidbg真的适合 MCP 吗?
AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链
某安全so库深度解析
球分享
球点赞
球在看
戳“阅读原文”一起来充电吧!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 用户名null 用户名null《Android Root 环境隐藏:SELinux 查询探测与对抗》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论