11、Binder类型混淆漏洞:CVE-2017-13212漏洞分析
好,咱们今天聊一个硬核的漏洞——CVE-2017-13212。这个漏洞当年在Android安全圈里引起了不小的震动。为什么?因为它涉及Binder机制里一个非常隐蔽的类型混淆问题,最终能导致任意地址读写。说白了,攻击者可以利用这个漏洞,在内核空间里想读哪里读哪里,想写哪里写哪里。嗯,这权限提升的力度,你想想看有多可怕。
11.1 漏洞背景:binder_node与binder_ref的“身份错乱”
先说说Binder里两个核心数据结构:binder_node 和 binder_ref。前者代表一个Binder实体(服务端),后者代表一个Binder引用(客户端持有)。正常情况下,它们井水不犯河水。但CVE-2017-13212的问题出在哪?出在Binder驱动在处理某些特定ioctl命令时,没有严格校验传入对象的实际类型。
我记得当时分析这个漏洞时,第一反应是:“这也能混淆?” 后来仔细看代码才发现,问题出在 binder_ioctl 函数里对 BINDER_WRITE_READ 命令的处理路径上。驱动错误地将一个 binder_ref 对象当成了 binder_node 来操作。
核心问题: 驱动在处理 BC_INCREFS 和 BC_ACQUIRE 等命令时,没有区分传入的句柄(handle)指向的是 binder_ref 还是 binder_node。攻击者可以构造一个特殊的Binder事务,让驱动误以为一个 binder_ref 是 binder_node,从而触发类型混淆。
11.2 漏洞触发路径:一步步走进陷阱
咱们来梳理一下攻击者是怎么一步步触发这个漏洞的。整个过程其实挺巧妙的,我尽量用通俗的语言讲清楚。
- 第一步:创建恶意Binder实体
攻击者先创建一个Binder服务,并获取它的句柄。这个句柄在客户端看来就是一个整数。 - 第二步:构造特殊事务
攻击者构造一个Binder事务,其中包含一个特殊的flat_binder_object。这个对象的type字段被设置为BINDER_TYPE_BINDER,但handle字段指向一个已经释放的binder_ref对象。 - 第三步:触发UAF(Use-After-Free)
驱动在处理这个事务时,会尝试通过handle查找对应的binder_node。但由于类型混淆,它实际上拿到了一个binder_ref的指针。更糟的是,这个binder_ref可能已经被释放了——这就成了UAF。 - 第四步:利用UAF实现任意读写
攻击者通过堆喷(heap spray)技术,在binder_ref被释放后,用自己控制的数据重新占位。这样,驱动后续对这个“假binder_node”的操作,实际上就是在操作攻击者控制的内存。
注意: 这个漏洞的利用条件比较苛刻,需要攻击者能够精确控制堆内存布局。但一旦成功,效果是毁灭性的——攻击者可以直接读写内核任意地址,进而获取root权限。
11.3 代码层面的类型混淆细节
咱们来看一段简化后的漏洞代码逻辑。这是从Android内核源码中提取的关键路径:
// 漏洞代码简化版
static int binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr) {
struct binder_node *node;
struct binder_ref *ref;
// 问题出在这里:没有检查传入对象的实际类型
node = binder_get_node(proc, tr->target.handle);
// 如果 target.handle 实际上指向一个已释放的 binder_ref,
// 那么 node 就是一个被攻击者控制的内存区域
if (node == NULL) {
// 错误处理
return -EINVAL;
}
// 后续操作 node 的成员,实际上是在操作攻击者控制的数据
node->ptr = ...; // 任意地址写!
node->cookie = ...; // 任意地址读!
}
你看,关键就在 binder_get_node 这个函数。它没有区分传入的句柄是 binder_node 还是 binder_ref。我当年在分析这个漏洞时,第一件事就是去查 binder_get_node 的实现,结果发现它只是简单地从红黑树里根据ID查找,根本没有类型校验。
11.4 漏洞利用:从类型混淆到任意地址读写
攻击者拿到类型混淆的“假 binder_node”后,怎么实现任意地址读写呢?这里有个关键点:binder_node 结构体里有两个非常重要的字段——ptr 和 cookie。
| 字段 | 正常用途 | 被攻击者控制后 |
|---|---|---|
ptr |
指向Binder服务端对象的指针 | 攻击者可以修改它,实现任意地址写 |
cookie |
服务端自定义数据 | 攻击者可以读取它,实现任意地址读 |
具体来说,攻击者通过以下步骤实现任意读写:
- 任意地址读: 攻击者将
cookie字段设置为目标地址,然后触发Binder驱动读取cookie的操作。驱动会把这个“假cookie”返回给攻击者,攻击者就拿到了目标地址的内容。 - 任意地址写: 攻击者将
ptr字段设置为目标地址,然后触发Binder驱动向ptr写入数据的操作。驱动会把攻击者提供的数据写入目标地址。
避坑指南: 我曾经在分析类似漏洞时犯过一个错误——以为只要校验了句柄的合法性就安全了。其实不然,类型混淆漏洞的核心在于“类型”而不是“值”。即使句柄的值是合法的,但如果它指向的对象类型不对,一样会出问题。所以,内核开发者在处理Binder相关操作时,一定要做类型检查,不能只看ID。
11.5 漏洞修复:Google是怎么补的
Google在2017年12月的安全公告中修复了这个漏洞。修复方式其实很直接——在 binder_get_node 函数里增加了类型检查。具体来说,他们引入了一个新的标志位 BINDER_NODE_FLAG,用来区分 binder_node 和 binder_ref。
// 修复后的代码
static struct binder_node *binder_get_node(struct binder_proc *proc,
uint32_t handle) {
struct rb_node *n = proc->nodes.rb_node;
while (n) {
struct binder_node *node = rb_entry(n, struct binder_node, rb_node);
// 新增的类型检查
if (node->flags & BINDER_NODE_FLAG_IS_REF) {
// 这是一个 binder_ref,不是 binder_node
return NULL;
}
if (handle < node->debug_id)
n = n->rb_left;
else if (handle > node->debug_id)
n = n->rb_right;
else
return node;
}
return NULL;
}
这个修复看起来简单,但效果很好。它从根本上杜绝了类型混淆的可能性——每个对象在创建时就打上了类型标签,后续操作时先检查标签,再决定怎么处理。
11.6 知识体系总结
为了让你更直观地理解这个漏洞的整个知识体系,我画了一张图:
这张图把整个漏洞的链条串起来了。从根源的类型混淆,到触发条件,再到利用步骤和最终效果,最后是修复方案。你想想看,如果任何一个环节被阻断,这个漏洞就利用不了。所以,安全防御要做的是“纵深防御”——每个环节都设卡。
11.7 我的个人经验与思考
最后,我想分享一点个人经验。我在做Android内核安全审计时,发现类型混淆漏洞其实比想象中更常见。很多开发者习惯用“ID查找”的方式来获取对象,但忽略了“类型校验”。这就像你去酒店,前台只问“你房间号多少?”而不看你的身份证——随便一个人报个房间号就能拿到房卡。
我曾经在一个项目中,就因为类似的问题差点翻车。当时我在写一个自定义的Binder驱动,也是用句柄来查找对象。幸好我在代码审查时多留了个心眼,加了个类型检查,不然可能就埋下一个CVE了。嗯,从那以后,我养成了一个习惯——凡是涉及对象查找的代码,一定要做类型校验,哪怕看起来多此一举。
好了,关于CVE-2017-13212的分析就到这里。这个漏洞虽然已经修复了,但它背后的类型混淆思想,至今仍在各种安全漏洞中反复出现。理解它,对你分析其他Binder漏洞会有很大帮助。