22、Binder模糊测试:syzkaller与Binder fuzzing、binder_ioctl命令字fuzz、崩溃分析与复现

说到Binder安全,我个人的习惯是,光靠静态代码审查远远不够。你想想看,Binder驱动在内核里,每天要处理成千上万的IPC请求,光靠人眼去翻代码找漏洞,效率太低了。这时候,模糊测试(Fuzzing)就派上用场了。

说白了,模糊测试就是让机器替你“乱搞”Binder的接口,看看会不会搞出什么幺蛾子。今天我们就聊聊怎么用syzkaller这个神器,对Binder驱动做系统性的模糊测试。

22.1 syzkaller与Binder Fuzzing

syzkaller是Google开源的内核模糊测试工具。它厉害的地方在于,能自动生成系统调用序列,并且能感知代码覆盖率。我当年第一次用它测Binder时,心里也没底,结果跑了不到半小时,就崩了一次——嗯,那种感觉,又兴奋又紧张。

syzkaller对Binder的fuzzing,主要分两块:

  • 系统调用级别的fuzzing:通过生成open、ioctl、mmap等系统调用的组合,来测试Binder驱动的边界情况。
  • 命令字级别的fuzzing:专门针对binder_ioctl的各个命令字,构造畸形参数。

syzkaller的配置里,需要描述Binder设备节点的路径和权限。我记得第一次配的时候,忘了给/dev/binder设置正确的权限,结果fuzzer一直报权限错误,白白浪费了半天时间。

小提示: 在syzkaller的配置文件里,记得把/dev/binder、/dev/hwbinder、/dev/vndbinder都加进去。不同Android版本,这些节点的权限策略不一样,建议用adb shell ls -l先确认一下。

22.2 binder_ioctl命令字fuzz

Binder驱动的核心入口就是binder_ioctl。这个函数接收的命令字大概有十几个,每个命令字对应的数据结构都不一样。fuzz的时候,我们需要针对每个命令字,构造各种畸形数据。

常见的命令字包括:

命令字 功能 常见fuzz点
BINDER_WRITE_READ 读写Binder事务 buffer大小、偏移量、句柄值
BINDER_SET_CONTEXT_MGR 设置上下文管理器 节点重复注册、权限绕过
BINDER_THREAD_EXIT 线程退出通知 空指针、重复退出
BINDER_VERSION 获取版本号 输出缓冲区溢出

我个人习惯,在fuzz BINDER_WRITE_READ时,会特别关注binder_transaction_data结构体中的偏移量字段。为什么?因为很多历史漏洞,都是因为偏移量计算错误导致的越界读写。

下面是一个简单的fuzz用例片段,用C语言模拟构造畸形命令字:

// 构造一个畸形的BINDER_WRITE_READ请求
struct binder_write_read bwr = {0};
bwr.write_size = 0xFFFFFFFF;  // 故意填一个超大值
bwr.write_consumed = 0;
bwr.write_buffer = (unsigned long)malloc(0x1000);

// 发送ioctl
int fd = open("/dev/binder", O_RDWR);
int ret = ioctl(fd, BINDER_WRITE_READ, &bwr);
// 如果内核没有做好校验,这里就可能触发崩溃
警告: 在真机上跑fuzz时,一定要确保设备已经解锁了bootloader,并且刷入了可调试的内核。我曾经在同事的工程机上跑fuzz,结果把系统搞挂了,重启后数据全丢了——从那以后,我都是用专门的测试机。

22.3 崩溃分析与复现

syzkaller跑出崩溃后,会生成一个日志文件和一个C语言的reproducer程序。这个reproducer可以直接编译运行,复现崩溃。

分析崩溃时,我一般按这个步骤来:

  1. 看dmesg:先看内核日志,找到Oops信息,定位崩溃的指令地址和函数名。
  2. 看syzkaller日志:找到触发崩溃的系统调用序列,理解上下文。
  3. 编译reproducer:在本地环境编译运行,确认崩溃可复现。
  4. 反汇编分析:用objdump或GDB反汇编崩溃点的代码,找到根本原因。

举个例子,有一次syzkaller报了一个Binder的崩溃,日志里显示在binder_thread_read函数中发生了空指针解引用。我一看reproducer,发现它先调用了BINDER_THREAD_EXIT,然后又发了一个BINDER_WRITE_READ请求。问题就出在这里:线程退出后,binder_thread结构体被释放了,但后续的读操作还在引用它。

复现这个漏洞的代码大概长这样:

int fd = open("/dev/binder", O_RDWR);
// 先让线程退出
ioctl(fd, BINDER_THREAD_EXIT, NULL);
// 再尝试读写,此时binder_thread已经没了
struct binder_write_read bwr = {0};
ioctl(fd, BINDER_WRITE_READ, &bwr);  // 这里会崩溃

这个漏洞的本质,就是缺少对线程生命周期的正确管理。修复方案也很直接:在binder_thread_read入口处,检查当前线程是否已经标记为退出状态。

核心要点: Binder模糊测试的价值,在于能发现那些人工审查容易遗漏的边界条件。尤其是并发场景下的竞态条件,靠人眼很难看出来,但fuzzer能通过随机组合系统调用,把隐藏的bug炸出来。

22.4 知识体系总览

下面这张图,是我自己总结的Binder模糊测试知识体系。你可以把它当作一个检查清单,看看自己覆盖了哪些点:

Binder模糊测试 syzkaller配置 设备节点权限配置 系统调用序列生成 覆盖率反馈驱动 binder_ioctl命令字 BINDER_WRITE_READ BINDER_SET_CONTEXT_MGR BINDER_THREAD_EXIT 崩溃分析与复现 dmesg日志分析 reproducer编译运行 反汇编定位根因 核心目标:发现边界条件与竞态漏洞 提升Binder驱动的安全性与稳定性

这张图把Binder模糊测试分成了三条主线:syzkaller配置、命令字fuzz、崩溃分析。我个人建议,初学者先从命令字fuzz入手,因为这一块最容易上手,也最容易出成果。等你跑通了基本的fuzz流程,再深入去研究syzkaller的配置优化和覆盖率提升。

好了,关于Binder模糊测试的内容就聊到这里。记住,fuzzing不是万能的,但它绝对是你发现Binder漏洞的一把利器。用好它,能帮你省下大量手动审计的时间。


公众号:蓝海资料掘金营,微信deep3321