20、Binder与内核防护机制:KASLR与Binder、CFI与Binder函数指针、PAN与内核内存保护

聊到Binder安全,很多人第一反应是“权限检查够不够严格”。但说实话,真正的高手过招,往往是在内核层面。你想想看,Binder作为Android系统的“血管”,每天流过多少敏感数据?如果内核本身不设防,那上层权限检查做得再漂亮,也挡不住直接攻击内核的恶意程序。

这一章,我带你看看内核是怎么给Binder“穿盔甲”的。KASLR、CFI、PAN,这三个缩写你可能听过,但它们在Binder场景下具体怎么发挥作用?咱们一个一个拆开讲。

20.1 KASLR:让攻击者找不到Binder的内核地址

KASLR(Kernel Address Space Layout Randomization),说白了就是让内核的基地址每次启动都不一样。我刚开始做内核安全时,觉得这玩意儿挺简单的——不就是随机化一下地址嘛。但后来发现,它对Binder的影响远比想象中复杂。

为什么?因为Binder驱动在内核中暴露了大量函数接口。攻击者如果知道某个关键结构体的地址,比如struct binder_proc,就能直接构造恶意数据包,绕过上层检查。

核心机制:KASLR将内核代码段、数据段的起始地址随机化。Binder驱动作为内核模块,其函数指针、全局变量、关键结构体的地址也随之随机化。

我在项目中遇到过一个问题:某个定制ROM的KASLR没有正确覆盖Binder驱动模块。结果攻击者通过一个信息泄露漏洞,直接拿到了binder_thread_read函数的地址,然后利用这个地址计算出其他关键函数的偏移量。嗯,这其实是个典型的“部分随机化”陷阱。

避坑指南:我曾经见过一些厂商为了性能,把Binder驱动编译进内核而不是作为模块。这本身没问题,但要注意——如果内核开启了KASLR,Binder驱动的地址必须跟着随机化。否则,攻击者只要知道内核版本,就能预测Binder的地址布局。

KASLR覆盖范围对Binder的影响
内核代码段binder_open/binder_ioctl等函数地址随机化
内核数据段binder_proc、binder_node等结构体地址随机化
模块地址如果Binder是模块,其地址独立随机化
堆栈地址Binder调用栈的返回地址随机化

但KASLR不是万能的。它防的是“远程”攻击,也就是攻击者无法直接读取内核内存的情况。如果攻击者已经拿到了一个任意内核读取漏洞,KASLR就形同虚设了。所以,KASLR只是第一道防线。

20.2 CFI:保护Binder的函数指针不被篡改

CFI(Control Flow Integrity),控制流完整性。这名字听着挺学术,其实核心思想很简单:确保程序执行的控制流,必须符合预先定义的合法路径。

Binder驱动里到处都是函数指针。比如binder_ioctl会根据不同的命令码调用不同的处理函数。攻击者最喜欢干的事,就是篡改这些函数指针,让它们指向恶意代码。

我的经验:在Android 10之后,Google强制要求内核开启CFI。我参与过一个项目,当时为了兼容旧硬件,尝试关闭CFI来提升性能。结果跑了一遍CTS安全测试,Binder相关的测试项全挂了。从那以后,我再也不敢动CFI了。

CFI在Binder场景下的具体工作方式是这样的:

// 伪代码:CFI检查前后的函数调用
// 原始调用
binder_ioctl(file, cmd, arg);

// CFI检查后的调用
__cfi_check(binder_ioctl, (uintptr_t)binder_ioctl);
binder_ioctl(file, cmd, arg);

每次调用函数指针前,CFI会检查目标地址是否在合法的函数列表中。如果不在,内核直接panic。攻击者就算篡改了函数指针,也跳不到他想要的地址。

你可能会问:那攻击者能不能篡改CFI的检查表本身?嗯,这是个好问题。CFI的检查表是只读的,存放在内核的只读数据段。攻击者要改它,得先过MMU的权限检查——这就涉及到下一个机制了。

20.3 PAN:让内核不能随便访问用户空间

PAN(Privileged Access Never),特权访问永不。这个机制在ARMv8.1之后引入,它的作用很简单:内核在运行内核代码时,不能直接访问用户空间的内存。

为什么这对Binder重要?因为Binder驱动经常需要从用户空间拷贝数据。比如binder_transaction函数,需要读取用户空间传入的struct binder_transaction_data。如果没有PAN,攻击者可以构造一个用户空间地址,让内核去读写,从而绕过内核的地址校验。

注意:PAN不是万能的。它只防止内核“主动”访问用户空间。如果攻击者通过DMA或其他硬件机制访问内存,PAN管不了。但至少,它堵死了最常用的攻击路径。

我记得有一次调试一个Binder死锁问题,发现是PAN导致的。某个内核驱动在中断上下文中调用了copy_from_user,而PAN在中断上下文中是不允许这种操作的。嗯,这其实是个设计缺陷——Binder驱动本身不会犯这种错,但第三方驱动可能会。

PAN与Binder的交互流程大致如下:

// 用户空间调用Binder
ioctl(fd, BINDER_WRITE_READ, &bwr);

// 内核空间处理
binder_ioctl() {
    // PAN开启:不能直接访问用户空间
    // 必须使用copy_from_user/copy_to_user
    copy_from_user(&bwr, arg, sizeof(bwr));
    
    // 处理Binder事务
    binder_transaction() {
        // 再次使用copy_from_user读取数据
        copy_from_user(&tr, bwr.ptr, sizeof(tr));
    }
    
    // 返回结果
    copy_to_user(arg, &bwr, sizeof(bwr));
}

如果没有PAN,攻击者可以传入一个精心构造的arg指针,让内核直接解引用。有了PAN,内核必须通过copy_from_user来访问,而这个函数内部会做地址校验和权限检查。

20.4 三者协同:Binder内核防护的纵深防御

KASLR、CFI、PAN,这三个机制不是孤立的。它们构成了Binder内核防护的纵深防御体系:

  • KASLR:让攻击者找不到目标地址
  • CFI:让攻击者跳不到非法地址
  • PAN:让攻击者读不到用户数据

我画了一张图,帮你理解这三者的关系:

Binder内核防护纵深防御体系 攻击者 第一层:KASLR 地址随机化,攻击者找不到目标 第二层:CFI 控制流完整性,函数指针不可篡改 第三层:PAN 内核不能直接访问用户空间 防护效果 • KASLR:防信息泄露 • CFI:防控制流劫持 • PAN:防权限提升 三者缺一不可 攻击者必须 同时突破三层 才能成功攻击Binder

从图中你可以看到,攻击者要成功攻击Binder,必须同时突破三层防护。缺一层都不行。我见过一些安全分析报告,只关注KASLR而忽略了CFI和PAN,这其实是很危险的——攻击者可能通过其他漏洞绕过KASLR,然后直接攻击CFI或PAN的薄弱点。

20.5 实战中的注意事项

说了这么多理论,最后给你几个实战中的建议:

我的建议:

  • 开启KASLR时,务必确认Binder驱动被正确随机化。可以用/proc/kallsyms检查binder_开头的符号地址是否每次启动都不同。
  • CFI的误报率很低,但如果遇到莫名其妙的panic,先检查是不是CFI触发的。日志里会有CFI failure关键字。
  • PAN对性能有轻微影响,但可以忽略不计。不要为了那1%的性能提升关闭PAN——我曾经见过一个团队这么干,结果被攻击者用ret2user技术轻松提权。

嗯,这一章的内容就到这里。Binder的内核防护机制,说白了就是让攻击者“找不到、跳不了、读不到”。KASLR、CFI、PAN这三个机制,每一个单独拿出来都有弱点,但组合在一起,就构成了一个很难攻破的防线。

你想想看,攻击者要同时绕过这三个机制,得需要多少个漏洞?一个信息泄露漏洞绕过KASLR,一个任意写漏洞绕过CFI,还得有一个用户空间访问漏洞绕过PAN。这种组合漏洞在现实中极其罕见。所以,做好这三件事,Binder的安全就稳了一大半。

核心总结:KASLR随机化地址、CFI保护控制流、PAN隔离用户空间。三者协同,构成Binder内核防护的纵深防御体系。任何一环缺失,都会给攻击者可乘之机。


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