24、Binder漏洞动态分析:KASAN与Binder、KCOV覆盖率收集、动态插桩技术
各位同学,今天我们来聊聊Binder漏洞的动态分析。说实话,静态分析能发现的问题其实有限,很多真正致命的漏洞——比如UAF(Use-After-Free)、越界读写——只有在运行时才会露出马脚。我个人习惯把动态分析比作“现场勘查”,你得在案发时盯着内存和指令流,才能抓到真凶。
这一章我会重点讲三个工具:KASAN、KCOV,以及动态插桩技术。它们分别解决什么问题?说白了就是:KASAN抓内存错误,KCOV看代码覆盖率,动态插桩做精准监控。三者配合,基本能覆盖Binder漏洞分析的大部分场景。
24.1 KASAN:内核内存错误检测器
KASAN(Kernel Address SANitizer)是Google贡献给Linux内核的运行时内存检测工具。它的核心思想很简单:在每次内存访问前后插入检查代码,一旦发现越界或UAF,立刻panic并打印调用栈。
我在项目中遇到过好几次,Binder驱动里因为并发操作导致的对象释放后又被访问,静态代码审查根本看不出来。但KASAN一开,跑一轮测试就抓到了。
24.1.1 KASAN的工作原理
KASAN使用了一种叫“影子内存”(shadow memory)的技术。内核每分配8字节内存,KASAN就在影子区用1字节记录它的状态。这1字节的值表示:
| 影子值 | 含义 |
|---|---|
| 0x00 | 所有8字节均可访问 |
| 0x01 ~ 0x07 | 前N字节可访问,剩余不可访问(用于检测越界) |
| 0xF1 ~ 0xFF | 已释放(UAF检测) |
| 0xFA | 栈变量(用于栈缓冲区溢出检测) |
当代码访问某个内存地址时,KASAN会计算对应的影子地址,检查影子值。如果影子值显示该地址不可访问,就触发报错。
关键点:KASAN对性能有影响,通常会使系统变慢2-3倍。生产环境不建议开启,但调试阶段必须开。
24.1.2 在Binder分析中启用KASAN
要分析Binder漏洞,我们需要编译一个带KASAN的内核。以Android通用内核为例:
# 在defconfig中添加
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
CONFIG_KASAN_OUTLINE=n
CONFIG_KASAN_GENERIC=y
# 编译内核
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- -j8
启动后,你可以通过dmesg查看KASAN的检测结果。我记得有一次,我在测试一个Binder transaction时,KASAN直接报出了binder_thread结构体的UAF:
[ 123.456] ==================================================================
[ 123.456] BUG: KASAN: use-after-free in binder_ioctl_write_read+0x1a4/0x8c0
[ 123.456] Read of size 8 at addr ffffff800a1b2c00 by task Binder:1234_5
[ 123.456]
[ 123.456] Call trace:
[ 123.456] binder_ioctl_write_read+0x1a4/0x8c0
[ 123.456] binder_ioctl+0x88/0x120
[ 123.456] do_vfs_ioctl+0x88/0x8c0
[ 123.456] __arm64_sys_ioctl+0x28/0x38
[ 123.456] el0_svc_common+0x88/0x120
[ 123.456]
[ 123.456] Allocated by task 1234:
[ 123.456] binder_thread_new+0x40/0x100
[ 123.456] binder_get_thread+0x88/0x120
[ 123.456]
[ 123.456] Freed by task 5678:
[ 123.456] binder_thread_release+0x60/0x200
[ 123.456] binder_deferred_release+0x28/0x80
[ 123.456] ==================================================================
看到没?KASAN不仅告诉你发生了UAF,还给出了分配和释放的调用栈。这对定位漏洞根因帮助极大。
我的经验:KASAN报错后,别急着修。先分析分配和释放的时序,看看是不是并发导致的。Binder驱动里很多UAF都是因为线程A释放了对象,线程B还在用。
24.2 KCOV:代码覆盖率收集
KCOV(Kernel Code Coverage)是另一个神器。它用于收集内核代码的覆盖率信息,特别适合fuzzing场景。你想想看,如果你在fuzz Binder,怎么知道哪些代码路径被覆盖了?KCOV就是干这个的。
24.2.1 KCOV的工作原理
KCOV在内核编译时插入桩代码,记录每个基本块(basic block)是否被执行。用户态程序可以通过一个特殊的伪文件/sys/kernel/debug/kcov来获取覆盖率数据。
基本流程是这样的:
- 用户态程序打开
/sys/kernel/debug/kcov - 通过ioctl设置覆盖模式(如KCOV_MODE_TRACE_PC)
- mmap映射一块内存用于接收覆盖率数据
- 执行目标系统调用(如Binder ioctl)
- 读取mmap区域,获取覆盖的PC地址
24.2.2 在Binder fuzzing中使用KCOV
我建议你在写Binder fuzzer时,一定要集成KCOV。下面是一个简化的示例:
#include <linux/kcov.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int setup_kcov() {
int fd = open("/sys/kernel/debug/kcov", O_RDWR);
if (fd == -1) return -1;
// 设置覆盖模式
if (ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE) == -1) {
close(fd);
return -1;
}
// 映射缓冲区
void *cover = mmap(NULL, COVER_SIZE * sizeof(unsigned long),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (cover == MAP_FAILED) {
close(fd);
return -1;
}
// 启用覆盖
if (ioctl(fd, KCOV_ENABLE, KCOV_MODE_TRACE_PC) == -1) {
munmap(cover, COVER_SIZE * sizeof(unsigned long));
close(fd);
return -1;
}
return fd;
}
void run_binder_ioctl(int kcov_fd) {
// 重置覆盖计数
unsigned long *cover = ...; // 从mmap获取
__atomic_store_n(&cover[0], 0, __ATOMIC_RELAXED);
// 执行Binder操作
int binder_fd = open("/dev/binder", O_RDWR);
struct binder_write_read bwr = { ... };
ioctl(binder_fd, BINDER_WRITE_READ, &bwr);
// 读取覆盖数据
unsigned long new_edges = cover[0];
printf("Covered %lu new edges\n", new_edges);
}
注意:KCOV收集的是PC地址,你需要用/proc/kallsyms或vmlinux符号表来解析对应的函数名。我一般写个脚本自动解析,省得手动查。
24.3 动态插桩技术
动态插桩,说白了就是在不修改内核源码的情况下,在运行时插入监控代码。KASAN和KCOV其实也算插桩的一种,但这里我重点讲的是更灵活的插桩方式——比如Ftrace、kprobe,以及SystemTap。
24.3.1 Ftrace:内核函数追踪器
Ftrace是内核自带的追踪工具,可以动态开启/关闭对特定函数的追踪。我在分析Binder transaction流程时,经常用Ftrace来观察函数调用链:
# 开启Binder相关函数的追踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo binder_* > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 执行Binder操作后,查看追踪结果
cat /sys/kernel/debug/tracing/trace
# 输出示例:
# binder_ioctl-1234 [001] .... 123.456: binder_ioctl: dev=0, cmd=0xc0186201
# binder_ioctl_write_read-1234 [001] .... 123.457: binder_ioctl_write_read: ...
# binder_transaction-1234 [001] .... 123.458: binder_transaction: ...
24.3.2 kprobe:动态探测点
如果Ftrace不够灵活,可以用kprobe。它允许你在任意内核指令地址插入探测点。我曾经用kprobe监控binder_thread结构体的生命周期:
# 在binder_thread_new入口插入探测点
echo 'p:binder_new binder_thread_new $arg1' > /sys/kernel/debug/tracing/kprobe_events
echo 'r:binder_new_ret binder_thread_new $retval' >> /sys/kernel/debug/tracing/kprobe_events
# 在binder_thread_release入口插入探测点
echo 'p:binder_release binder_thread_release $arg1' >> /sys/kernel/debug/tracing/kprobe_events
# 开启追踪
echo 1 > /sys/kernel/debug/tracing/events/kprobes/binder_new/enable
echo 1 > /sys/kernel/debug/tracing/events/kprobes/binder_new_ret/enable
echo 1 > /sys/kernel/debug/tracing/events/kprobes/binder_release/enable
这样,每次创建和释放binder_thread时,我都能看到对应的地址和调用栈。配合KASAN的UAF报告,就能精确还原漏洞触发过程。
警告:kprobe插入过多会影响性能,而且如果探测点在内核热路径上,可能导致系统响应变慢。我建议只针对怀疑的函数插入探测点,不要全开。
24.4 三件套的配合使用
在实际的Binder漏洞分析中,我通常这样组合使用这三个工具:
- 第一步:用KCOV指导fuzzing。先跑一轮fuzz,收集覆盖率数据,找出哪些Binder代码路径没被覆盖到,然后调整fuzz输入。
- 第二步:用KASAN抓漏洞。在fuzzing过程中开启KASAN,一旦触发UAF或越界,立刻记录现场。
- 第三步:用动态插桩复现。根据KASAN的报错信息,用Ftrace或kprobe在关键函数插入探测点,复现漏洞并观察完整流程。
下面这张图展示了三者的协作关系:
我的建议:别指望一次就能跑通。动态分析是个迭代过程——先跑fuzz,看覆盖率,调整输入,再跑,直到触发漏洞。KASAN报错后,用插桩工具精确定位,然后修复。这个过程我重复过无数次,每次都能发现新东西。
24.5 实战中的避坑指南
最后,分享几个我在实战中踩过的坑:
- KASAN误报:有时候KASAN会报假阳性,特别是当内核使用了非标准的内存分配方式时。遇到这种情况,先确认是不是真的UAF,别急着改代码。
- KCOV覆盖不全:KCOV默认只收集用户态触发的内核路径。如果你要分析内核线程的Binder操作,需要额外配置。我曾经因为这个漏掉了一个关键路径,浪费了两天时间。
- 插桩影响时序:动态插桩会改变代码执行时序,有些竞态条件可能因此被掩盖或触发。我建议在插桩时尽量轻量,只记录必要信息。
好了,这一章的内容就到这里。KASAN、KCOV和动态插桩是Binder漏洞分析的三大法宝,熟练掌握它们,你就能在漏洞挖掘中事半功倍。
公众号:蓝海资料掘金营,微信deep3321