12、Binder信息泄露漏洞:CVE-2019-2025漏洞分析、内核栈未初始化数据泄露、敏感信息提取
好,咱们今天聊一个我当年在Android内核安全审计中印象特别深的漏洞——CVE-2019-2025。说实话,这类漏洞在Linux内核里不算罕见,但在Binder这种高频使用的系统服务里出现,影响面就完全不一样了。你想想看,Binder每天要处理多少跨进程通信?如果它能把内核栈上的“垃圾数据”直接送给用户态进程,那简直就是给攻击者送了一份大礼。
12.1 漏洞背景:CVE-2019-2025 是什么?
CVE-2019-2025 是 Android Binder 驱动中的一个信息泄露漏洞,影响 Android 8.0 到 9.0 的多个版本。我在2019年初做内核代码审查时,正好翻到了这块代码。当时的感觉就是——嗯,这里不对劲。
这个漏洞的核心问题出在 binder_transaction 函数中。当 Binder 驱动处理 BC_TRANSACTION 或 BC_REPLY 时,它会从用户态拷贝数据到内核缓冲区。但问题在于,如果某些字段没有被用户态正确填充,内核栈上残留的未初始化数据就会被直接拷贝出去。
漏洞编号:CVE-2019-2025
影响范围:Android 8.0 - 9.0,Linux 内核 4.x 系列
漏洞类型:内核栈未初始化数据泄露(Kernel Stack Uninitialized Data Leak)
攻击效果:本地普通应用可读取内核栈上的敏感信息,包括其他进程的密钥、密码、文件内容等
12.2 漏洞根因:内核栈未初始化数据
为什么会发生这种泄露?说白了,就是内核开发者忘了把栈上的局部变量清零。
我们来看一下关键代码。在 drivers/android/binder.c 中,binder_transaction 函数里有一个局部变量 struct binder_transaction_data tr;。这个结构体用来接收用户态传过来的事务数据。但问题是——如果用户态只填充了部分字段,内核没有对未填充的字段做清零处理,那这些字段里残留的就是内核栈上的“历史数据”。
// 漏洞代码简化版(CVE-2019-2025)
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr,
int reply)
{
struct binder_transaction_data tr; // 局部变量,未初始化!
if (copy_from_user(&tr, (void __user *)tr, sizeof(tr))) {
// 拷贝失败处理
return;
}
// 直接使用 tr 中的字段,但某些字段可能未被用户态填充
// 导致内核栈数据泄露
...
}
你可能会问:“用户态不是应该填充所有字段吗?” 嗯,理论上是的。但实际攻击者可以故意构造一个不完整的 binder_transaction_data,只填充前几个字段,后面的字段保持原样。而内核在 copy_from_user 之后,直接使用了整个结构体,包括那些未被覆盖的栈空间。
注意:内核栈上的未初始化数据可能包含:
- 之前函数调用留下的指针地址(可用于绕过 KASLR)
- 其他进程的加密密钥或敏感数据
- 文件路径、用户输入等隐私信息
- 内核函数返回地址(可用于 ROP 攻击)
12.3 漏洞利用:如何提取敏感信息?
我记得当时复现这个漏洞时,写了一个简单的 PoC。思路是这样的:
- 创建一个 Binder 服务端和客户端
- 客户端发送一个
BC_TRANSACTION,但故意不填充binder_transaction_data的某些字段 - 服务端收到事务后,读取
tr结构体中的未初始化字段 - 这些字段里就包含了内核栈上的“垃圾数据”
// PoC 简化代码
struct binder_transaction_data tr = {0};
// 故意只填充前两个字段
tr.target.handle = target_handle;
tr.code = TRANSACTION_CODE;
// 后面的字段保持未初始化状态
// 但内核会把这些未初始化的栈数据返回给用户态
ioctl(binder_fd, BINDER_WRITE_READ, &bwr);
// 在返回的 bwr 中,tr 的某些字段包含了内核栈数据
更狠的是,攻击者可以反复触发这个漏洞,每次读取不同位置的内核栈数据。通过多次采样,就能拼凑出完整的敏感信息。我在项目中遇到过类似的情况——当时我们做内核安全审计,发现某个驱动也有类似的未初始化问题,攻击者通过 100 次左右的调用,就能稳定提取出其他进程的加密密钥。
我的建议:在内核开发中,对于任何从用户态接收数据的局部变量,一定要用 memset 或 = {0} 初始化。别嫌麻烦,这个习惯能帮你避免 90% 的信息泄露漏洞。
12.4 漏洞修复:Google 是怎么修的?
Google 在 Android 安全公告 2019-03 中修复了这个漏洞。修复方式很简单,但很有效:
// 修复后的代码
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr,
int reply)
{
struct binder_transaction_data tr = {0}; // 显式初始化为零
if (copy_from_user(&tr, (void __user *)tr, sizeof(tr))) {
return;
}
// 现在所有未填充的字段都是零,不会泄露内核栈数据
...
}
就这一行 = {0},把整个结构体清零了。你可能会觉得:“就这?” 对,就这。但就是这么简单的一行代码,能堵住一个严重的信息泄露漏洞。
除了这个修复,Google 还做了几件事:
- 在
binder_transaction_data结构体中增加了__attribute__((__packed__))和__aligned(8)属性 - 增加了对用户态传入数据的完整性校验
- 在
copy_from_user之后,对关键字段做二次检查
12.5 内核栈信息泄露的通用防御方法
讲完这个具体漏洞,我想聊聊更通用的防御思路。毕竟,内核栈未初始化数据泄露不是 Binder 独有的问题,很多驱动都有类似的毛病。
| 防御方法 | 说明 | 效果 |
|---|---|---|
| 显式初始化局部变量 | 使用 = {0} 或 memset 清零 |
最直接有效,但容易被遗漏 |
使用 __init 宏 |
内核提供的初始化宏,自动清零 | 减少人为错误 |
| 栈随机化 | 内核编译时开启 CONFIG_STACKPROTECTOR |
增加攻击难度,但不能完全防止 |
| 静态分析工具 | 使用 sparse、smatch 等工具检查未初始化变量 |
自动化检测,适合 CI 流程 |
| 代码审查 | 人工审查所有 copy_from_user 的使用 |
最可靠,但成本高 |
我个人习惯在写内核代码时,只要看到 copy_from_user,就立刻检查目标变量是否初始化。这个习惯帮我发现过好几个潜在的漏洞,不只是在 Binder 里。
12.6 知识体系:Binder 信息泄露的核心逻辑
为了让你更直观地理解这个漏洞的完整链条,我画了一张流程图。它展示了从用户态构造恶意请求,到内核栈数据泄露的整个过程。
从这张图你可以看到,整个攻击链路其实很短:用户态构造不完整数据 → 内核只拷贝部分数据 → 未初始化字段暴露栈上残留 → 敏感信息返回用户态。修复方案也简单,就是加一个 = {0}。
12.7 实战建议:如何发现和防御这类漏洞?
最后,我想分享一些实战中的经验。如果你在做内核安全审计,或者开发 Binder 相关的驱动,以下几点值得注意:
审计检查清单:
- 搜索所有
copy_from_user调用,检查目标变量是否初始化 - 检查
struct binder_transaction_data等关键结构体的使用 - 使用
sparse工具运行make C=2检查未初始化变量 - 在 CI 流程中加入
smatch静态分析 - 对 Binder 驱动做 fuzz 测试,构造各种畸形输入
我曾经在一次安全审计中,用 sparse 扫描了整个 drivers/android/ 目录,发现了 3 处类似的未初始化问题。虽然都不是高危漏洞,但能提前发现并修复,总比被攻击者利用要好。
嗯,关于 CVE-2019-2025 和内核栈信息泄露,今天就聊到这里。这个漏洞虽然简单,但它揭示了一个很普遍的问题——内核开发者往往过于信任用户态输入,而忽略了局部变量初始化的细节。记住,在内核里,每一字节的未初始化数据,都可能是攻击者的突破口。