文章总结: 本文分析了Duck-Detector检测APatch的侧信道原理:通过比较128字节key与空key的系统调用延迟差(超过3微秒则标记),采用ARMv8硬件计时器和大样本统计。APatch官方通过重构内核补丁(KernelPatch)分离可信调用与认证状态,优化执行路径,将时间差从3.821微秒降至0.336微秒以绕过检测。结论指出该检测仅对旧版本有效,新版已免疫。 综合评分: 87 文章分类: 漏洞分析,移动安全,二进制安全,内核安全,逆向分析
APatch最新版检测与过检测原理分析
原创
非虫 非虫
软件安全与逆向分析
2026年5月6日 10:32 湖北
在小说阅读器读本章
去阅读
APatch最新版检测与过检测原理分析
本文主要讲解
Duck-Detector最新版本引入的对APatch的检测原理的分析,以及APatch官方的过检测思路。作者:非虫([email protected])
Duck-Detector的检测逻辑
APatch目前只支持ARM64架构,那检测与反检测都是针对安卓这一架构进行。这个检测思路的关键点不在于检测框架调用了什么系统功能或有什么文件特征,而是“同一个系统调用在两种输入下的时间差有多大”。
核心检测逻辑大致如下:
1. 读取两组输入,一组是128字节的key字符串,一组是仅包含`\0`的key字符串。
2. 用ARMv8硬件计时器测两组输入的平均延迟。
3. 计算两者差值。
4. 如果差值超过3.0微秒,就同时标记`kernel_su`和`apatch`。
相关更新的检测代码如下:
staticinlineuint64_tget_cntfrq(){
// 读取cntfrq_el0,得到计数器频率
uint64_t val;
asmvolatile("mrs %0, cntfrq_el0" : "=r" (val));
return val;
}
staticinlineuint64_tget_cntvct(){
// 读取cntvct_el0,得到当前计数值
uint64_t val;
asmvolatile("isb; mrs %0, cntvct_el0; isb" : "=r" (val));
return val;
}
doublemeasure_latency(constchar *buffer){
uint64_t start, end;
uint64_t total_ticks = 0;
constuint64_t freq = get_cntfrq();
if (freq == 0) return0.0;
syscall(kSupercallNr, buffer, kSupercallHello);
for (int i = 0; i < kIterations; i++) {
start = get_cntvct();
syscall(kSupercallNr, buffer, kSupercallHello);
end = get_cntvct();
total_ticks += (end - start);
}
return (static_cast<double>(total_ticks) * 1000000.0) /
(static_cast<double>(freq) * kIterations);
}
if (diff > 3.0) {
result.flags.kernel_su = true;
result.flags.apatch = true;
}
值得注意的地方有三处。
第一,计时方式从clock_gettime换成cntvct_el0和cntfrq_el0。cntvct_el0是AArch64里的虚拟计数寄存器,读取到的是当前计数值,适合做两次调用之间的间隔测量;cntfrq_el0则保存这个计数器的频率,用来把“tick差值”换算成微秒。也就是说,检测器没有把系统调用计时交给通用时间接口,而是直接读取ARMv8虚拟计数器。这样做的目的很明确,就是压低测量噪声,让亚微秒级的差异更稳定。clock_gettime的执行本身也会占用一些时间窗口,这可能会带来一些假阳性的情况发生。
第二,样本数被提高到100000次。对这种侧信道型检测来说,单次差异可能很小,必须靠大样本平均来把抖动洗掉。先做一次预热调用,再反复测量,最后取平均值,这就是这条探针的统计基础。
第三,判定条件非常直接:128字节key字符串与仅包含\0的key字符串之间,只要平均延迟差超过3.0微秒,就同时打上kernel_su和apatch标记。换句话说,它不是在找“有没有APatch字样”,而是在找“认证前拷贝与认证路径是否留下了足够大的时间指纹”。
代码里还有一个细节也很关键:如果__NR_supercall被Seccomp拦掉,检测器会把这次情况单独记为SECCOMP,而不是简单当成未检测到。这个设计说明作者清楚地区分了“没有命中”和“系统调用根本没机会执行”。
官方补丁的过检测思路
需要说明的是,先有APatch的内核补丁工具仓库KernelPatch的补丁提交,才有检测器的检测点更新。这也与很多的漏洞分析一样,攻击者往往在目标程序更新漏洞后,通过分析补丁内容还原Poc。KernelPatch的思路是改写路径顺序:尽量让不可信调用在进入敏感逻辑之前就结束,避免每次都先把用户态缓冲区拷进来,再去做认证判断。
从公开diff能直接确认的变化,主要集中在predata.c、sucompat.c、supercall.c和头文件声明上。大致逻辑概括如下:
1. 先判断当前UID是不是排除对象,避免无关路径继续执行。
2. 如果`has_preset_superkey()`分支成立,就再去读取用户缓冲区并做认证。
3. 把trusted caller和authed拆成两个状态。
4. 不是trusted caller就提前返回,不进入后续拷贝和命令处理。
5. 只有通过前置判定后,才跳过原始处理器并进入supercall。
对应的过检测代码如下:
inthas_preset_superkey()
{
return start_preset.superkey[0] == '\0';
}
staticvoidhandle_before_execve(char **__user u_filename_p, char **__user uargv, void *udata)
{
// 先排除不需要进入处理链的UID
uid_t uid = current_uid();
if (!is_su_allow_uid(uid)) return;
char __user *ufilename = *u_filename_p;
char filename[SU_PATH_MAX_LEN];
// 先把用户态路径拷进内核缓冲区,再继续后续逻辑
int flen = compat_strncpy_from_user(filename, ufilename, sizeof(filename));
if (flen < 0) return;
}
staticvoidbefore(hook_fargs6_t *args, void *udata)
{
// 先看当前UID是不是排除对象
uid_t uid = current_uid();
if (get_ap_mod_exclude(uid)) return;
// 把“是否可信”和“是否已认证”拆成两个状态
int is_trusted_caller = 0;
int is_authed = 0;
// 如果存在预置superkey,先从用户态拷贝key,再做认证
if (has_preset_superkey()) {
constchar *__user key_user = (constchar *__user)syscall_argn(args, 0);
char key[MAX_KEY_LEN];
long len = compat_strncpy_from_user(key, key_user, MAX_KEY_LEN);
if (len <= 0) return;
is_authed = !auth_superkey(key);
is_trusted_caller = is_authed;
}
// 管理员UID直接进入可信路径
if (is_trusted_manager_uid(uid)) {
is_trusted_caller = 1;
is_authed = 1;
} elseif (is_su_allow_uid(uid)) {
is_trusted_caller = 1;
}
// 非可信调用直接结束,不进入后续拷贝和命令处理
if (!is_trusted_caller) return;
long ver_xx_cmd = (long)syscall_argn(args, 1);
long cmd = ver_xx_cmd & 0xFFFF;
if (cmd < SUPERCALL_HELLO || cmd > SUPERCALL_MAX) return;
// 读取后续参数,交给补丁后的supercall统一处理
long a1 = (long)syscall_argn(args, 2);
long a2 = (long)syscall_argn(args, 3);
long a3 = (long)syscall_argn(args, 4);
long a4 = (long)syscall_argn(args, 5);
// 跳过原始实现,避免再次走旧的认证分支
args->skip_origin = 1;
args->ret = supercall(is_authed, cmd, a1, a2, a3, a4);
}
staticlongsupercall(int is_authed, long cmd, long arg1, long arg2, long arg3, long arg4)
{
// 未通过认证时,直接拒绝敏感命令
if (!is_authed) return -EPERM;
switch (cmd) {
// 这里省略其它命令分支
default:
break;
}
...
return -ENOSYS;
}
这里的“认证前拷贝”,对应的是compat_strncpy_from_user(key, key_user, MAX_KEY_LEN)这一段。它先把用户态传入的key复制到内核缓冲区,再交给auth_superkey(key)判断是否通过认证。也就是说,认证判断本身并不是直接读取用户态指针,而是建立在一次内核态拷贝之后完成的。
真正的认证路径则体现在supercall(is_authed, cmd, a1, a2, a3, a4)这一步。前置钩子先把is_trusted_caller和is_authed分开处理,只有可信调用才会继续往下走;而在supercall内部,if (!is_authed) return -EPERM;又把后续敏感命令重新卡住。于是,认证前拷贝负责把输入带进来,认证路径负责决定这些输入能不能进入真正的命令分支。
这段改动的意义在于,把“是否可信”和“是否已认证”拆成两个状态。
is_trusted_caller决定这次调用能不能继续往下走,is_authed决定后面的supercall里哪些命令可以真正执行。这样一来,很多不该进入copy_from_user的场景,会在更早的分支里结束掉。
supercall本体也从is_key_auth改成了is_authed。表面上只是变量名变化,实质上是把认证状态的语义整理得更清楚,让前置钩子和实际命令执行之间的关系更直接。
另一个补丁点在sucompat.c。这里先判断is_su_allow_uid(uid),再去做compat_strncpy_from_user。这一步很小,但很关键,因为它减少了不可信应用触发用户态拷贝的机会。对于Duck-Detector这种专门盯时间差的工具来说,只要路径里多了一次不必要的拷贝,就可能把侧信道放大。
predata.c里新增的has_preset_superkey(),以及predata.h和userd.h里的接口补位,说明这次修补不是单点热修,而是在把启动预置状态、认证状态和UID信任判断拆开整理。提交说明里提到的“mark as umount直接跳过所有supercall”,从思路上看也是同一类动作:把本来可能暴露时间差的路径尽量前移或直接裁掉。
过检测效果分析
这个补丁的效果,可以直接从提交里的基准数据看出来。
补丁前,128字节key字符串的平均延迟是4.281微秒,'\0'key字符串是0.460微秒,差值大约3.821微秒,远高于Duck-Detector的3.0微秒阈值。补丁后,两者分别变成0.943微秒和0.607微秒,差值缩小到0.336微秒,已经和普通未打补丁系统的0.792微秒对0.446微秒非常接近。
这说明补丁把时间差压回了正常系统“认为”的自然抖动范围。对于这类检测器来说,真正重要的不是绝对值,而是异常差值是否足够大到越过阈值。KernelPatch这次做对的事情,就是让这个差值不再像补丁前那样显著。
但这不等于检测逻辑失效。它只是说明这条侧信道当前被压住了。只要未来检测器改用更高分辨率的计时方式,或者把多个信号叠加起来,仍然可能重新把差异拉出来。
结论
这次对抗的本质很清楚:Duck-Detector在KernelPatch官方最新提交的过检测补丁的启发下,找到了一种通过侧信道稳定检测老版本APatch的方法。显然,这个检测只对当前设备是2026年五一之前APatch框架的版本有效,升级到最新版本后可以无视这个检测。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:软件安全与逆向分析 非虫 非虫《APatch最新版检测与过检测原理分析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论