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_userget_userstrncpy_from_user 等函数
  • 污点汇(Sink)kmallocmemcpy__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里做路径可达性,通常需要结合数据流分析和控制流分析。我习惯的做法是:

  1. 先用污点分析找出所有候选路径
  2. 对每条路径,提取路径上的条件表达式
  3. 用约束求解器(比如Z3)检查这些条件是否可满足

不过CodeQL本身不内置约束求解器,所以实际中我常用的是“半自动”方式:让CodeQL输出路径和条件,然后手动分析或者用脚本去验证。

注意:路径可达性分析不是万能的。有些条件涉及运行时状态(比如某个全局变量的值),静态分析很难精确判断。这时候就需要结合动态调试来验证。我一般把静态分析当作“筛子”,先筛出可疑路径,再用动态分析去确认。

23.4 知识体系总览

下面这张图,是我自己总结的Binder漏洞静态分析的核心逻辑。你可以把它当作一个“作战地图”:

Binder漏洞静态分析知识体系 用户态数据输入 CodeQL建模:标记Source/Sink 污点分析:追踪数据流路径 路径可达性分析 约束求解与条件验证 输出:可利用漏洞路径

这张图展示了整个分析流程:从用户态数据输入开始,经过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查出来的每一条路径,你都得能讲清楚“为什么这里会有问题”、“攻击者怎么利用”。这才是硬功夫。