文章总结: 本文分析安卓三大Root框架(Magisk、KernelSU、APatch)的通杀检测与反检测方法。Duck-Detector通过appzygote进程查询SELinux策略,检测Root相关上下文存在性和访问权限。DirtySepolicy提供精简版检测PoC,直接调用FrameworkAPI查询脏策略规则。KernelSU采用selinuxhide机制,为应用UID呈现备份策略以实现隐藏。文章详细解析了检测原理、代码实现及反制方案。 综合评分: 85 文章分类: 移动安全,漏洞分析,安全工具,逆向分析,恶意软件
三大Root框架通杀检测与反检测方法分析
原创
非虫 非虫
软件安全与逆向分析
2026年5月12日 12:21 湖北
在小说阅读器读本章
去阅读
最近比较流行通杀!前面讲过两篇Linux的通杀漏洞,
这次轮到安卓了。这一次又是
Duck-Detector在搞事,老版本这个检测器C语言编写的,现在新版本使用C++重构开源了,挺值得学习的。Duck这次公开了一个检测方法。可用于全版本
Magisk检测,实际它也可以用于对KernelSU与APatch的检测。随后LSPosed沿用同一思路,整理出更精简的DirtySepolicy检测器。与此同时,
KernelSU与APatch社区也亮出了反检测的思路与代码。接下来,本篇主要介绍一下这个通杀检测的原理,以及三种过这个检测的方法。文章作者:非虫([email protected])
Duck-Detector检测原理
这次检测的核心思路,是把探针下沉到app_zygote进程,借助它对SELinux策略的查询能力,跳过传统的包名、进程名与文件路径扫描,直接向内核发问两件事:目标上下文是否存在、目标访问是否被放行。前者通过写入/sys/fs/selinux/context完成,后者则依赖android.os.SELinux.checkSELinuxAccess接口。
预加载类是整条检测链路的起点。当currentUid与appInfo.uid不一致,或Native库未能加载时,探针会直接产出一份可解析的失败快照,把”载体失效”与”发现Root痕迹”严格区分开来,避免环境异常被误判为命中。
classAppZygotePreload : ZygotePreload {
overridefundoPreload(appInfo: ApplicationInfo) {
val result = runCatching {
val currentUid = Os.getuid()
if (currentUid != appInfo.uid) {
fallbackPayload("UID mismatch: $currentUid != app uid ${appInfo.uid}.")
} elseif (!SelinuxContextValidityBridge.isNativeLibraryLoaded) {
fallbackPayload("SELinux native library unavailable.")
} else {
SelinuxContextValidityBridge.nativeCollectContextValiditySnapshot()
.ifBlank { fallbackPayload("SELinux native snapshot payload was empty.") }
}
}.getOrElse { throwable ->
fallbackPayload(throwable.message ?: "SELinux app zygote preload failed.")
}
SelinuxContextValidityBridge.setPreloadedRawData(result)
}
}
Native层首先声明被检测的上下文与控制组。其中u:r:ksu:s0、u:object_r:ksu_file:s0、u:object_r:magisk_file:s0是Root相关类型,剩余四个上下文则用于自检oracle的可信度。
constexprconstchar *kSelinuxContextPath = "/sys/fs/selinux/context";
constexprconstchar *kExpectedCarrierType = "app_zygote";
constexprconstchar *kKsuContext = "u:r:ksu:s0";
constexprconstchar *kKsuFileContext = "u:object_r:ksu_file:s0";
constexprconstchar *magiskFileContext = "u:object_r:magisk_file:s0";
constexprconstchar *kNegativeControlContext = "u:r:duckdetector_context_oracle_sentinel:s0";
constexprconstchar *kStockFileControlContext = "u:object_r:system_data_file:s0";
constexprconstchar *kNegativeFileControlContext =
"u:object_r:duckdetector_context_oracle_sentinel_file:s0";
/sys/fs/selinux/context的职责是将输入上下文转换为内核sid。只要当前策略认可目标type,write()便会返回非负值,这正是判定”上下文存在”的唯一信号。
ContextCheckResult check_context_validity(constchar *context){
ContextCheckResult result;
int fd = open(kSelinuxContextPath, O_RDWR | O_CLOEXEC);
if (fd < 0) {
result.valid = false;
result.note = std::string("Unavailable: ") + context + " errno=" + strerror(errno);
return result;
}
constssize_t written = write(fd, context, std::strlen(context) + 1);
constint error = errno;
close(fd);
if (written >= 0) {
result.valid = true;
return result;
}
result.valid = false;
result.note = std::string("Unavailable: ") + context + " errno=" + strerror(error);
return result;
}
为防止偶然误判,检测链路对每个Root相关上下文都重复查询两次。只要两次结果不一致即进入UNSTABLE_RESULTS状态,这样即便Root管理器只拦截一次、或在不同入口给出不同答案,依然能够被识别。控制组中”正常上下文应通过、哨兵上下文应失败”四组判定全部符合预期之后,Root相关结果才会被采信;否则视为自检失败,所有KSU结论降级为附加信息。
checkSELinuxAccess走的则是访问向量计算的路径。Duck-Detector以app_zygote的身份充当查询者,覆盖以下四组访问关系:
| 检测点 | source | target | class | perm | 严重级 | | — | — | — | — | — | — | | 通用脏策略 | system_server | system_server | process | execmem | WARNING | | Magisk binder | untrusted_app | magisk | binder | call | WARNING | | KernelSU binder | untrusted_app | ksu | binder | call | WARNING | | LSPosed file read | untrusted_app | lsposed_file | file | read | DANGER |
正向控制(app_zygote到isolated_app的process dyntransition)与反向控制(到哨兵文件类型)同样会做两次稳定性确认。其中Magisk与KernelSU的binder call属于辅助证据,而lsposed_file的读权限才是LSPosed残留的直接证据。Native快照只负责生产数据,展示层不再重新查询策略。
DirtySepolicy独立PoC
DirtySepolicy将同一检测入口浓缩为一个最小化的App。它舍弃了Duck的完整展示层与Native快照包装,重点是证明app_zygote可以直接调用SELinux.checkSELinuxAccess查询全局策略。配置层面,isolatedProcess配合useAppZygote会触发应用私有的Zygote,再由zygotePreloadName指定预加载类,载体便稳稳落在app_zygote之上。
<application
android:zygotePreloadName="org.lsposed.dirtysepolicy.AppZygote">
<service
android:name=".DirtySepolicyService"
android:isolatedProcess="true"
android:useAppZygote="true" />
</application>
与Duck一样,PoC也把载体可信性的校验置于检测之前:先确认UID未被换掉、当前上下文确实是app_zygote,并验证getContext()、getPidContext()与/proc/self三处文件上下文完全一致。自检通过之后,再直接调用Framework API完成查询。
if (SELinux.checkSELinuxAccess("u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) {
sb.append("system_server can execmem; ");
}
if (SELinux.checkSELinuxAccess("u:r:fsck_untrusted:s0", "u:r:fsck_untrusted:s0", "capability", "sys_admin")) {
sb.append("neverallow violated; ");
}
if (Build.TYPE.equals("user") && SELinux.checkSELinuxAccess("u:r:shell:s0", "u:r:su:s0", "process", "transition")) {
sb.append("found AOSP su in user build; ");
}
if (SELinux.checkSELinuxAccess("u:r:adbd:s0", "u:r:adbroot:s0", "binder", "call")) {
sb.append("found adb_root; ");
}
if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", "u:r:magisk:s0", "binder", "call")) {
sb.append("found Magisk; ");
}
if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", "u:object_r:ksu_file:s0", "file", "read")) {
sb.append("found KernelSU; ");
}
if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", "u:object_r:lsposed_file:s0", "file", "read")) {
sb.append("found LSPosed; ");
}
if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", "u:object_r:xposed_data:s0", "file", "read")) {
sb.append("found Xposed; ");
}
if (SELinux.checkSELinuxAccess("u:r:zygote:s0", "u:object_r:adb_data_file:s0", "dir", "search")) {
sb.append("found ZygiskNext; ");
}
相比Duck,PoC的查询面更宽,涵盖了fsck_untrusted的sys_admin、shell到su的迁移、adbd到adbroot的binder call,以及Xposed与ZygiskNext的残留特征。它真正的价值并不在UI,而是揭示了一个事实:只要app_zygote能向内核问到当前策略,脏规则便会化作可读信号,用户态进程根本绕不开这道查询口。
KernelSU运行时过检测
KernelSU上游新增selinux_hide之后,过检测的思路便从”清掉策略”演变为”给应用UID呈现另一份备份策略”。当前策略仍然保留Root所需的全部规则,但应用态的查询入口被悄悄转向注入之前的策略副本。
backup_sepolicy在KernelSU追加自身域与文件类型之前便已生成。如此一来,后续隐藏逻辑无需伪造任何字符串,只需拿一张真实的旧策略表重新计算答案。
系统UID仍然走原始的context写入口,应用UID则改走backup_sepolicy。当检测器提交u:r:ksu:s0或u:object_r:ksu_file:s0时,旧策略里并不存在对应类型,转换随即失败,检测侧看到的便是”上下文不存在”。
staticssize_tmy_write_context(struct file *file, char *buf, size_t size)
{
if (likely(current_uid().val < 10000)) {
return orig_context_write(file, buf, size);
}
char *canon = NULL;
u32 sid, len;
ssize_t length;
length = security_context_to_sid_with_policy(backup_sepolicy, buf, size, &sid, SECSID_NULL, GFP_KERNEL);
if (length)
goto out;
length = security_sid_to_context_with_policy(backup_sepolicy, sid, &canon, &len);
if (length)
goto out;
memcpy(buf, canon, len);
length = len;
out:
kfree(canon);
return length;
}
checkSELinuxAccess最终触发的是访问向量计算。应用UID的输入被引导至backup_sepolicy重新计算,因此untrusted_app到ksu的binder call,以及untrusted_app到ksu_file的file read,不再按当前策略给出结果。
staticssize_tmy_write_access(struct file *file, char *buf, size_t size)
{
if (likely(current_uid().val < 10000)) {
return orig_access_write(file, buf, size);
}
/* 省略参数解析与错误处理 */
length = security_context_to_sid_with_policy(backup_sepolicy, scon, strlen(scon),
&ssid, SECSID_NULL, GFP_KERNEL);
if (length) goto out;
length = security_context_to_sid_with_policy(backup_sepolicy, tcon, strlen(tcon),
&tsid, SECSID_NULL, GFP_KERNEL);
if (length) goto out;
security_compute_av_user_with_policy(backup_sepolicy, ssid, tsid, tclass, &avd);
length = scnprintf(buf, SIMPLE_TRANSACTION_LIMIT, "%x %x %x %x %u %x",
avd.allowed, 0xffffffff, avd.auditallow,
avd.auditdeny, avd.seqno, avd.flags);
return length;
}
除此之外,setprocattr current入口同样被纳入隐藏面,目标上下文统一交由backup_sepolicy解析,以防检测器从/proc/self/attr/current一侧绕开selinuxfs查询。
运行时方案的优势在于不必修改设备内核源码。具体做法是:通过kallsyms_lookup_name("write_op")定位selinuxfs的写操作表,再用ksu_patch_text将SEL_CONTEXT与SEL_ACCESS两个槽位替换为自定义函数,同时挂上selinux_setprocattr的LSM hook。
selinux_write_op = kallsyms_lookup_name("write_op");
context_write = &selinux_write_op[SEL_CONTEXT];
orig_context_write = *context_write;
ksu_patch_text(context_write, &my_write_context, sizeof(my_write_context),
KSU_PATCH_TEXT_FLUSH_DCACHE);
access_write = &selinux_write_op[SEL_ACCESS];
orig_access_write = *access_write;
ksu_patch_text(access_write, &my_write_access, sizeof(my_write_access),
KSU_PATCH_TEXT_FLUSH_DCACHE);
ksu_lsm_hook(&selinux_setprocattr_hook);
管理器侧的”隐藏SELinux修改”开关最终对应到KSU_FEATURE_SELINUX_HIDE这一特性位。开启之后,应用UID所见即为备份策略的答案;关闭之后,selinuxfs入口回归原始实现。截图中Duck的Danger显示为0,恰好与该功能的工作面相互印证。
内核源码级过检测补丁
Andrea仓库里的补丁直接动了security/selinux/ss/services.c。如果说运行时方案改的是selinuxfs入口,那么源码级方案改的便是SELinux服务函数本身:上下文解析与访问向量计算,统统在内核源码层被过滤。
补丁优先使用KernelSU上游已经暴露的backup_sepolicy;若该符号不存在,则回退到静态规则表与隐藏type表。静态表所覆盖的,恰好正是DirtySepolicy与Duck所查询的访问关系。
externstructselinux_policy *backup_sepolicy __attribute__((weak));
staticconststructselinux_hidden_av_ruleselinux_hidden_av_rules[] = {
{ "system_server", "system_server", "process", "execmem" },
{ "fsck_untrusted", "fsck_untrusted", "capability", "sys_admin" },
{ "shell", "su", "process", "transition" },
{ "adbd", "adbroot", "binder", "call" },
{ "untrusted_app", "magisk", "binder", "call" },
{ "untrusted_app", "ksu", "binder", "call" },
{ "untrusted_app", "ksu_file", "file", "read" },
{ "untrusted_app", "lsposed_file", "file", "read" },
{ "untrusted_app", "xposed_data", "file", "read" },
{ "zygote", "adb_data_file", "dir", "search" },
};
staticconstchar * const selinux_hidden_context_types[] = {
"su", "adbroot", "ksu", "ksu_file",
"magisk", "magisk_file", "lsposed_file", "xposed_data",
};
过滤逻辑只对app_zygote生效。系统服务与内核内部的正常计算依旧沿用原策略,从而将对系统运行的影响降到最低;检测器恰好倚仗app_zygote,因此正好落入过滤分支。
staticboolselinux_hidden_av_query_caller(struct policydb *policydb,
struct sidtab *sidtab)
{
structcontext *ccontext;
constchar *ctype;
ccontext = sidtab_search(sidtab, current_sid());
if (!ccontext)
returnfalse;
ctype = sym_name(policydb, SYM_TYPES, ccontext->type - 1);
return ctype && !strcmp(ctype, "app_zygote");
}
当app_zygote提交ksu、ksu_file、magisk_file等隐藏type时,补丁会让security_context_to_sid_core提前返回-EINVAL,Duck的/sys/fs/selinux/context检测因此只能得到”上下文无效”的回应。
staticboolselinux_hide_context_validity_query(struct policydb *policydb,
struct sidtab *sidtab,
constchar *scontext,
u32 scontext_len)
{
unsignedint i;
if (!selinux_hidden_av_query_caller(policydb, sidtab))
returnfalse;
if (selinux_hide_context_query_with_backup(scontext, scontext_len))
returntrue;
for (i = 0; i < ARRAY_SIZE(selinux_hidden_context_types); i++) {
if (selinux_hidden_context_has_type(scontext, scontext_len,
selinux_hidden_context_types[i]))
returntrue;
}
returnfalse;
}
至于访问向量计算,则视backup_sepolicy是否可用分作两条路径:可用时将源、目标上下文映射到备份策略,重新计算av_decision;不可用时则按静态规则表清掉命中的permission位,核心逻辑仅有寥寥数行。
for (i = 0; i < ARRAY_SIZE(selinux_hidden_av_rules); i++) {
conststructselinux_hidden_av_rule *rule = &selinux_hidden_av_rules[i];
if (strcmp(rule->stype, stype) || strcmp(rule->ttype, ttype) ||
strcmp(rule->tclass, tclass_name))
continue;
perm = selinux_perm_to_av(tclass_datum, rule->perm);
if (!perm)
continue;
avd->allowed &= ~perm;
avd->auditallow &= ~perm;
avd->auditdeny |= perm;
}
这段代码被插入到security_compute_av_user()之中。checkSELinuxAccess底层一旦计算完成,结果会被立刻过滤,应用态最终拿到的,只能是处理之后的av_decision。该路径与KernelSU运行时方案如出一辙:不删当前策略,只把查询结果改写成旧策略本应给出的答案。
KPM运行时补丁方案
APatch社区给出的反检测方案,是一份名为selinux_magisk_access_filter的KPM(kernel patch module)。它与KernelSU上游selinux_hide同属”运行时挂钩”路线,但走的是KPM框架的hook_wrap,无需重编内核,也无需依赖backup_sepolicy这一上游符号。
整体思路可以浓缩为一句话:先在SELinux就绪时悄悄抓取一份干净策略,再在应用UID走到查询入口时,让它看到那份干净策略的回答。
KPM在init阶段一次性挂上10个selinuxfs与SELinux服务函数的钩子,覆盖检测器可能用到的所有入口。各目标的角色如下:
| 目标符号 | 作用 |
| — | — |
| security_read_policy | 给应用UID返回干净policy副本,绕开当前dirty策略 |
| security_load_policy | 标记”外部load”以识别脏策略加载 |
| sel_write_load | 计数/sys/fs/selinux/load写入次数 |
| sel_write_access | 拦截/sys/fs/selinux/access,命中Root类型即返回-EINVAL |
| sel_write_context | 拦截/sys/fs/selinux/context,未在干净策略中出现即返回-EINVAL |
| security_setprocattr 或selinux_setprocattr | 过滤procattr current一侧,防止从/proc/self/attr/current绕开 |
| context_struct_compute_av | 把应用UID的policydb参数替换成干净副本 |
| string_to_context_struct | 同上,用于上下文字符串到context_struct的转换 |
| selinux_complete_init | SELinux首次就绪时触发干净策略快照 |
| selinux_policy_commit | 每次新策略生效后维护first_policy并刷新快照 |
辅助符号则按需通过kallsyms_lookup_name解析,包括vmalloc/vmalloc_noprof、vfree、copy_from_kernel_nofault(旧内核退化为probe_kernel_read)、security_context_to_sid、selinux_policy_cancel、sidtab_cancel_convert。任一关键符号缺失时,对应的过检测路径会安全降级而非崩溃。
干净策略的采集时机是selinux_complete_init或selinux_policy_commit的after回调,对应SELinux首次就绪和后续每次新策略落地。snapshot_clean_policy会调用security_read_policy复制出一份原始blob,并在blob中扫描MAGISK字样以标记g_clean_policy_has_magisk。
rc = g_security_read_policy(&blob, &len);
if (rc || !blob || !len)
return;
for (i = 0; i + 6 <= len; i++) {
if ((p[i] & 0xDF) == 'M' && (p[i + 1] & 0xDF) == 'A' &&
(p[i + 2] & 0xDF) == 'G' && (p[i + 3] & 0xDF) == 'I' &&
(p[i + 4] & 0xDF) == 'S' && (p[i + 5] & 0xDF) == 'K') {
magisk_seen = true;
break;
}
}
g_clean_policy_blob = blob;
g_clean_policy_len = len;
随后用security_load_policy把这份blob反序列化成内部load_state,作为应用态查询的备用引擎。需要注意的是,这次内部load是合法的”副本load”,因此KPM用g_internal_policy_load_depth计数把它与外部的脏策略load区分开来——只有计数为0的security_load_policy调用,才会被g_dirty_policy_seen标记。
sel_write_context钩子的核心,是用前面采集的干净blob反向校验当前查询样本是否合法。判定规则极简:把上下文按冒号切片,取最后一段type名,到干净blob里做字节比对。
if (!g_clean_load_state_ready || !g_clean_policy_blob ||
clean_context_exists(sample)) {
args->local.data0 = 3; /* 放行 */
return;
}
args->skip_origin = 1;
args->local.data0 = 2; /* 拦截 */
args->ret = (u64)(-EINVAL);
sel_write_access的处理类似,只是要把”scontext tcontext tclass”中的前两段都校验一遍,任一段在干净策略中找不到,便直接-EINVAL返回。context_struct_compute_av与string_to_context_struct两个钩子则更进一步:它们把传入的policydb第一参数替换为g_clean_policydb,这样即便检测器绕过了selinuxfs字符接口直接走Framework调用,最终的av_decision仍然由干净策略计算得出。
本方案与KernelSU方案的对照:
| 维度 | KernelSUselinux_hide | APatch KPM |
| — | — | — |
| 入口替换方式 | kallsyms_lookup_name("write_op") +ksu_patch_text | KPM框架的hook_wrap |
| 备份策略来源 | 内核内部backup_sepolicy,构造时机早于Root域追加 | 用security_read_policy+security_load_policy运行时重建 |
| 覆盖入口 | SEL_CONTEXT 、SEL_ACCESS、setprocattr current | 同上,并额外覆盖compute_av、string_to_context等更深层入口 |
| 适用平台 | 与KernelSU一同合入 | 任何能加载KPM的内核,无需源码改动 |
总体上,APatch的KPM思路与KernelSU运行时方案同源——都是”让应用UID看到一份不含Root类型的旧策略”。只是它把这件事完全放到模块层来做,连backup_sepolicy这一上游符号都可以舍弃,靠运行时再生成一份等价的”干净策略”来支撑后续所有查询路径。
KPM方案的限制
把上述思路真正落到产品级使用时,KPM方案有几条无法回避的限制需要正视。
限制一:加载时机必须早于Root域追加。
KPM的干净策略来自security_read_policy运行时读取,而不是像KernelSU那样在编译期就备份。一旦KPM加载晚于Root域注入,snapshot_clean_policy抓到的blob里就已经包含ksu、magisk_file等Root相关type,clean_context_exists("u:r:ksu:s0")随即返回true,KPM不仅不会拦截,反而会主动放行,检测器写入成功后直接判定ksu_domain_valid=true命中。因此该KPM不适合热加载,必须由APatch的bootkernel加载链在SELinux初始化之前就将其驻留到内核。
限制二:compute_av深层入口实际并未做策略重定向。
虽然KPM挂上了context_struct_compute_av与string_to_context_struct两个钩子,但其before_policydb_arg0回调中真正替换policydb参数的条件是g_clean_eval_depth > 0:
if (g_clean_eval_depth && clean_db && incoming) {
args->arg0 = (u64)(uintptr_t)clean_db;
return;
}
g_clean_eval_depth只有在KPM自己调用g_security_context_to_sid做内部清洁评估时才会临时大于零;对外部直接调用这两个函数的情况,本钩子退化为观察者,只负责检测脏策略并触发快照,并不实际改写计算输入。这与KernelSU的my_write_access把应用UID的av_decision计算彻底引导到backup_sepolicy不同。
实战中这个不等效之所以没有暴露,是因为Duck与DirtySepolicy走的都是/sys/fs/selinux/access这一用户态写入路径,被前面的sel_write_access钩子提前拦截了;一旦未来出现绕过/sys/fs/selinux/access、直接经compute_av系内核内部入口探测的检测器,本KPM将失守,而KernelSU仍可拦截。
限制三:clean_context_token_exists基于字节子串匹配。
KernelSU走的是security_context_to_sid_with_policy(backup_sepolicy, ...),由内核策略引擎给出标准答案;而本KPM则是在blob里做ASCII子串扫描,匹配的是”:后到行尾的最后一段type名”。这意味着若干净策略里恰好出现某Root相关字符串作为其他type名的子串,存在理论上的误判风险。实践中Root相关type名(ksu、magisk、magisk_file、ksu_file、lsposed_file、xposed_data)通常是独立token,干净策略里不存在与之构成部分匹配的合法type,故误判概率极低,但仍是潜在脆弱点。
限制四:security_read_policy双ABI不兼容。
本KPM对security_read_policy只按”无state参数”的2参形态调用:
typedefint(*security_read_policy_t)(void **data, size_t *len);
而Android 13/14对应的5.15内核里,该函数仍是int security_read_policy(struct selinux_state *, void **, size_t *)三参形态。在那类内核上直接以2参签名调用会因参数错位而security_read_policy提前失败,整个干净策略快照流程便走不下去,KPM也就退化为纯观察者。如要兼容旧内核,需要额外按kver分支选择正确的调用约定。
限制五:依赖kallsyms_lookup_name解析符号。
KPM挂钩的10个目标和7个辅助函数全部依赖kallsyms_lookup_name返回有效地址。在/proc/kallsyms被禁用或符号被剥离的发行版内核上,security_read_policy、sidtab_cancel_convert、copy_from_kernel_nofault等任一关键符号缺失,对应钩子就会被跳过,相关检测路径不再被拦截。KPM加载日志中需要逐项确认这些符号是否解析成功。
限制六:策略热更新场景下的快照陈旧问题。
若运行期间策略以外部方式被合法更新(例如系统OTA后init重新加载policy),before_security_load_policy会把这次load标记为g_dirty_policy_seen=true,从此snapshot_clean_policy不再刷新副本。这虽然能避免把脏策略当成干净副本,但也意味着干净blob会一直停留在最早那次快照的版本,长期运行后可能与系统真实策略产生越来越大的偏差,进而拦截到一些本应放行的合法上下文。
总之一句话,目前kpm的实现还有一些提升的空间,期待后续的完善。
最后,希望APatch快快提供代码方式来补丁,这样就不用折腾插件了。
总结
Duck与DirtySepolicy的检测点都依赖app_zygote所拥有的SELinux查询能力。一边用/sys/fs/selinux/context询问上下文是否存在,另一边用checkSELinuxAccess询问访问是否放行,两者皆从策略层一举绕开包名隐藏、进程名隐藏与文件路径伪装。
KernelSU上游的selinux_hide,处理面已覆盖SEL_CONTEXT、SEL_ACCESS与setprocattr current,对当前Duck和DirtySepolicy的检测路径而言已经具备过检测能力。Andrea的内核源码补丁则把同一思路落到services.c之中,对定制内核而言更为直接,无需依赖运行时对write_op的patch。
至于Magisk,目前在其官方公开代码中尚未见到同级别的app_zygote策略查询隐藏层。只要目标设备仍然把magisk或magisk_file暴露在当前策略之中,Duck的上下文检测以及DirtySepolicy式的访问检测,仍会轻易命中。
若未启用任何隐藏措施,ksu、ksu_file、magisk、magisk_file、lsposed_file这些类型都会被策略oracle一一读出。一旦开启KernelSU的selinux_hide,或将源码级隐藏补丁合入内核,检测器虽然仍在调用同一份API,但返回值已经接近一份干净策略,命中率自然随之下降。
参考资料
-
Duck-DetectorPR
#22:https://github.com/eltavine/Duck-Detector-Refactoring/pull/22 -
Duck-Detector最新实现:
app/src/main/cpp/selinux/context_validity_probe.cpp -
Duck-Detector预加载:
app/src/main/java/com/eltavine/duckdetector/features/selinux/data/service/AppZygotePreload.kt -
Duck-DetectorLSPosed信号:
app/src/main/java/com/eltavine/duckdetector/features/lsposed/data/probes/LSPosedDirtyPolicyProbe.kt -
DirtySepolicy:https://github.com/LSPosed/DirtySepolicy
-
DirtySepolicy核心实现:
app/src/main/java/org/lsposed/dirtysepolicy/AppZygote.java -
AOSP
app_zygote.te:https://android.googlesource.com/platform/system/sepolicy/+/master/private/app_zygote.te -
KernelSU
selinux_hide:kernel/feature/selinux_hide.c -
KernelSU
backup_sepolicy:kernel/selinux/rules.c -
KernelSU功能开关:
uapi/feature.h、manager/app/src/main/cpp/ksu.cc -
Andrea内核源码级补丁:https://github.com/Andrea-lyz/oppo_oplus_realme_sm8750/commit/db4883d7d243a9fc6ed8ea4071acce2e0be7b460
-
Andrea补丁文件:
other_patch/70_selinux_hide_policy_query.patch -
APatch KPM样例还原:
kpms/selinux_hook/selinux_hook.c(基于selinux_magisk_access_filter v1.0.4反编译还原) -
KPM框架
hook_wrap接口:kpms/kpm/kernel/include/hook.h -
Magisk
magiskpolicy说明:https://github.com/topjohnwu/Magisk/blob/master/docs/tools.md
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:软件安全与逆向分析 非虫 非虫《三大Root框架通杀检测与反检测方法分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论