一个故意存在漏洞的、约 170 行的 C 程序,用作 CodeQL 静态分析目标。它在一个与 pppd 不共享任何函数名、调用深度或分发结构的程序中,复现了 CVE-2020-8597 —— pppd EAP rhostname 栈缓冲区溢出(CWE-120)—— 的缺陷类别。
目标是进行一次通用性测试:为捕获 pppd 缺陷而编写的 CodeQL 查询,必须在不经修改的情况下也能在这个程序上触发。如果能够触发,说明该查询表达的是缺陷类别,而不是原始代码的形态。
此程序故意不安全,仅用于分析。请勿部署。它所模仿的缺陷是公开的(CVE-2020-8597,2020 年披露)。
一个由攻击者控制的长度被复制到固定大小的缓冲区中,且没有任何防护将该长度与缓冲区大小关联起来。
在每种情况下都存在边界检查 —— 只是它未能将两个关键量关联起来。恰好有两种出错方式,而该程序中各包含一种:
sizeof(dest)。这防止了越界读取,但对越界写入毫无作用。(handle_hello)sizeof(dest) —— 看起来完全像是缓冲区边界 —— 但它约束的是另一个变量,而不是用作复制长度的那个变量。(handle_stat)第二种更难,而这正是 pppd 中那个失效的 vallen >= len + sizeof(rhostname) 检查的本质:一个提及目标缓冲区大小、却约束了并非复制长度的东西的比较。一个只问*“这里是否有某个比较提及了 sizeof(dest)?”*的查询会被它蒙蔽。
两者都保留了使其成为数据流(而非 grep)的那一个特性:在来源与 sink 之间存在一次通过函数指针表的间接调用。
handle_stat 是区分性案例。它的检查中写的是 sizeof(report),因此一个接受任何提及目标缓冲区大小的比较的查询会将其视为已防护,从而漏掉该缺陷。要捕获它,需要将被检查的值与用作复制长度的值进行比较 —— 全局值编号。从查询中删除这一点,该处理函数就会变成假阴性,而其他所有位置都保持其判定。
一个 UDP 数据报 = 一个帧:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello,0x02 → echo,0x03 → stat。一个声明长度在 65 到约 2045 之间的 hello 帧会溢出 name[64]。一个 stat 帧则携带两个单字节长度 —— 一个头部长度和一个主体长度 —— 无论头部长度如何,任何大于 32 的主体长度都会溢出 report[32]。
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX(BSD sockets)。构建干净,无警告。
CodeQL 会跟踪真实的编译过程,因此请从干净状态构建:
make clean
codeql database create db --language=cpp --command="make"
# or, without the clean step:
codeql database create db --language=cpp --command="make -B"
然后对 db 运行 Part 3 查询;它应当报告 handle_hello 中的 memcpy 和 handle_stat 中的那个,并对 handle_echo 保持静默。该查询及其运行说明位于 codeql/。
每当添加处理函数时源代码都会变化,因此请重建数据库 —— CodeQL 在 database create 时对代码进行快照,现有的 db/ 不会看到新代码。
| pppd / CVE-2020-8597 | 本项目 |
|---|
| 来源 | 对 PPP fd 的 read() | 对 UDP socket 的 recvfrom() |
| 分发 | 全局 struct protent *protocols[],按协议号线性匹配 | 文件内 const struct frame_op ops[],按 1 字节标签线性匹配 |
| 到 sink 的深度 | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| 目标 | char rhostname[256] | char name[64] |
| 错误的防护 | vallen 受数据包 len 约束 | vlen 受帧 plen 约束 |
| 处理函数 | 行号 | 存在的检查 | 判定 |
|---|
handle_hello() | sink 位于 :80 | vlen > plen - 2 —— 值正确,边界错误 | 必须触发 |
handle_echo() | 复制位于 :106 | vlen >= sizeof(buf) —— 两者都正确 | 必须保持静默 —— 阴性对照 |
handle_stat() | sink 位于 :145 | hlen >= sizeof(report) —— 边界正确,值错误 | 必须触发 |