23、Binder漏洞静态分析:CodeQL与Binder驱动、污点分析、路径可达性分析
好,咱们进入第二十三讲。前面聊了很多Binder的运行时攻击和防御,今天换个角度——静态分析。说白了,就是不动手跑代码,光靠“看”就能找出漏洞。你可能会问:这靠谱吗?我个人的经验是,静态分析在Binder漏洞挖掘里,效率往往比动态调试还高。尤其是面对AOSP这种千万级代码量的项目,你不可能每个路径都去插桩跑一遍。
今天的主角是CodeQL。嗯,这工具其实不算新了,但用在Binder驱动分析上,确实有它独到的地方。我会从三个维度展开:CodeQL怎么建模Binder驱动、污点分析怎么追踪用户态数据、以及路径可达性分析怎么帮你找到真正的可利用路径。
23.1 CodeQL与Binder驱动的“对话”
先说说CodeQL是什么。它本质上是一个“把代码当成数据库来查询”的工具。你写查询语句,它帮你找出代码中符合特定模式的位置。对于Binder驱动,我们关心的无非是:数据从哪里来(用户态传入)、经过哪些处理、最终写到哪里去(内核态内存)。
我记得第一次用CodeQL分析Binder驱动时,最头疼的是怎么描述“用户态数据”。Binder驱动里,数据是通过copy_from_user从用户空间拷贝进来的。那我们就得告诉CodeQL:所有经过copy_from_user的数据,都是“污点”的源头。
来看一个简单的CodeQL查询模板:
import cpp
class BinderIoctlFunction extends Function {
BinderIoctlFunction() {
this.getName().matches("binder_ioctl%")
}
}
class UserDataSink extends Expr {
UserDataSink() {
this.(FunctionCall).getTarget().getName() = "copy_from_user"
}
}
from FunctionCall call, BinderIoctlFunction f
where call.getEnclosingFunction() = f
and call.getTarget().getName() = "copy_from_user"
select call, "用户态数据进入Binder驱动"
这个查询干了什么事?它找出了所有在binder_ioctl系列函数里调用了copy_from_user的地方。你想想看,这些点就是用户态数据进入内核的“入口”。一旦我们标记了这些入口,后续的污点传播就有据可依了。
核心思路:CodeQL分析Binder驱动的关键,是把“用户态数据流入”建模成污点源,把“内核态内存写入”建模成污点汇。中间的数据流就是我们要分析的路径。
23.2 污点分析:追踪“脏数据”的旅程
污点分析,说白了就是追踪“脏数据”的流动。在Binder场景下,用户态传入的数据就是“脏”的——你永远不知道攻击者会塞什么恶意内容进来。
我遇到过最典型的案例是:某个Binder驱动函数从用户态接收了一个缓冲区大小参数,然后直接用这个大小去分配内核内存。如果攻击者传一个超大值,分配就会失败,但驱动没有检查返回值——嗯,这就是个典型的空指针解引用漏洞。
用CodeQL做污点分析,我们需要定义三样东西:
- 污点源(Source):
copy_from_user、get_user、strncpy_from_user等函数 - 污点汇(Sink):
kmalloc、memcpy、__put_user等可能造成破坏的函数 - 污点传播规则(TaintPropagation):数据经过赋值、运算、类型转换后,污点如何传递
下面是一个更完整的污点查询示例:
import cpp
import semmle.code.cpp.dataflow.TaintTracking
class BinderTaintConfig extends TaintTracking::Configuration {
BinderTaintConfig() { this = "BinderTaintConfig" }
override predicate isSource(DataFlow::Node source) {
exists(FunctionCall fc |
fc.getTarget().getName() = "copy_from_user" and
source.asExpr() = fc.getArgument(1)
)
}
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall fc |
fc.getTarget().getName() = "kmalloc" and
sink.asExpr() = fc.getArgument(0)
)
}
}
from DataFlow::Node source, DataFlow::Node sink, BinderTaintConfig config
where config.hasFlow(source, sink)
select source, sink, "发现污点从用户态流入kmalloc参数"
这个查询会找出所有从copy_from_user的第二个参数(用户态缓冲区)流向kmalloc的第一个参数(大小)的路径。说白了,就是检查有没有“用户控制的大小直接用于内存分配”的情况。
避坑指南:我曾经在分析某个厂商的Binder驱动时,发现CodeQL报了很多假阳性。后来排查发现,是因为驱动里用了自定义的拷贝函数,没有直接用copy_from_user。所以,做污点分析前,一定要先摸清楚目标驱动用了哪些“类用户态拷贝”函数,把它们都加到Source里。
23.3 路径可达性分析:从“可能”到“一定”
污点分析找到了“可能”有问题的路径,但还不够。你想想看,一个路径虽然存在,但如果它被某个条件守卫挡住了——比如必须满足某个特定的cmd值才能进入——那这个路径在实际攻击中可能根本走不通。
路径可达性分析,就是用来解决这个问题的。它会检查:从污点源到污点汇的路径上,有没有不可达的分支?有没有需要满足的约束条件?
我举个例子。Binder驱动里常见的结构:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case BINDER_WRITE_READ:
// 处理读写请求
copy_from_user(&bwr, (void __user *)arg, sizeof(bwr));
// ... 后续处理
break;
case BINDER_SET_CONTEXT_MGR:
// 设置上下文管理器
break;
default:
return -EINVAL;
}
}
如果污点分析发现copy_from_user的数据流向了某个危险函数,但这条路径只在BINDER_WRITE_READ分支里存在,那攻击者就必须通过这个cmd值才能触发。路径可达性分析会帮我们确认:这个cmd值是否可以被普通应用调用?
CodeQL里做路径可达性,通常需要结合数据流分析和控制流分析。我习惯的做法是:
- 先用污点分析找出所有候选路径
- 对每条路径,提取路径上的条件表达式
- 用约束求解器(比如Z3)检查这些条件是否可满足
不过CodeQL本身不内置约束求解器,所以实际中我常用的是“半自动”方式:让CodeQL输出路径和条件,然后手动分析或者用脚本去验证。
注意:路径可达性分析不是万能的。有些条件涉及运行时状态(比如某个全局变量的值),静态分析很难精确判断。这时候就需要结合动态调试来验证。我一般把静态分析当作“筛子”,先筛出可疑路径,再用动态分析去确认。
23.4 知识体系总览
下面这张图,是我自己总结的Binder漏洞静态分析的核心逻辑。你可以把它当作一个“作战地图”:
这张图展示了整个分析流程:从用户态数据输入开始,经过CodeQL建模、污点分析,再到路径可达性和约束验证,最终输出可利用的漏洞路径。每一步都有它存在的意义,缺一不可。
23.5 实战中的一些体会
最后聊点实战中的体会。静态分析Binder驱动,有几个坑是绕不开的:
- 驱动版本差异:不同Android版本的Binder驱动实现有差异。比如Android 10之后引入了
binderfs,一些ioctl命令变了。我建议针对具体版本单独建CodeQL数据库。 - 厂商定制:很多手机厂商会修改Binder驱动。我遇到过某厂商在驱动里加了一个自定义的ioctl,用来做快速IPC——结果那个ioctl的实现里有个明显的越界写漏洞。CodeQL默认的规则是查不到的,得自己写查询。
- 假阳性处理:静态分析难免有假阳性。我的经验是:先跑一遍全量查询,然后根据
copy_from_user的参数类型做一次过滤。比如,如果参数是一个固定的结构体指针,那风险就低很多;如果参数是一个可变长度的缓冲区,那就要重点看。
总结一下:CodeQL做Binder驱动静态分析,核心就三步——建模、污点追踪、路径可达性验证。这三步走完,你基本能把驱动里90%的“用户态可控数据导致内核异常”的漏洞筛出来。剩下的10%,嗯,那就得靠动态调试和运气了。
好,这一讲就到这里。记住,工具只是辅助,真正值钱的是你对Binder驱动内部机制的理解。CodeQL查出来的每一条路径,你都得能讲清楚“为什么这里会有问题”、“攻击者怎么利用”。这才是硬功夫。