- 静态分析
- SAST
- 应用安全
- 漏洞扫描
- 代码质量
【免费下载链接】codeql
CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security
导读
本文围绕 CodeQL 仓库中 C++ 查询包 0.6.3 版本发布说明(cpp/ql/src/change-notes/released/0.6.3.md)引入的新查询cpp/overrun-write展开,深入解析它如何检测 C 风格缓冲区操作函数中的缓冲区溢出(buffer overflow)。读完本文,你将掌握该查询的触发原理、与cpp/overrunning-write系列查询的分工差异、底层依赖的 ProductFlow 与范围分析(Range Analysis)实现,以及如何结合仓库内的示例与测试用例验证查询行为。
一、发布说明中的新查询
在 cpp/ql/src/change-notes/released/0.6.3.md 中,0.6.3 版本的发布说明只有一条核心变更:
Added a new query,
cpp/overrun-write, to detect buffer overflows in C-style functions that manipulate buffers.
即:新增了cpp/overrun-write查询,用于检测“在操作缓冲区的 C 风格函数”中发生的缓冲区溢出。与之配套的正式 CHANGELOG 记录见 cpp/ql/src/CHANGELOG.md。
这里有两个值得关注的关键词:
- C 风格函数(C-style functions):指
strcpy、strncpy、memset、sprintf、memcpy等以裸指针/字符数组方式操作缓冲区的 C 库函数,而非 C++ 的std::vector、std::string等自带长度管理的容器。 - 缓冲区溢出(buffer overflow):写入或访问超出分配范围之外的内存,轻则导致段错误(segmentation fault),重则成为可利用的安全漏洞。
二、查询定位:cpp/overrun-write是什么
2.1 查询元数据
该查询的实现文件为 cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.ql,其头部元数据完整定义了查询的身份信息:
| 元数据项 | 值 | 含义 |
|---|---|---|
@name | Overrunning write | 查询名称 |
@description | Exceeding the size of a static array during write or access operations may result in a buffer overflow | 问题描述 |
@kind | path-problem | 输出带数据流路径的问题 |
@problem.severity | error | 问题严重级别 |
@security-severity | 9.3 | 安全严重级别(CVSS 风格评分) |
@precision | medium | 检出精度(中等,意味着存在一定误报/漏报空间) |
@id | cpp/overrun-write | 查询唯一标识 |
@tags | reliability、security、external/cwe/cwe-119、external/cwe/cwe-131 | 关联 CWE-119(缓冲区操作中的内存破坏)与 CWE-131(缓冲区大小计算错误) |
与常见的problem类查询不同,cpp/overrun-write是path-problem查询:它不仅报告“哪里溢出”,还会呈现从分配源头(source)到写入点(sink)的完整数据流路径,便于开发者定位根因。
2.2 查询套件归属
从集成测试的套件清单(cpp/ql/integration-tests/query-suite/cpp-security-and-quality.qls.expected 与 cpp/ql/integration-tests/query-suite/cpp-security-extended.qls.expected)可以看到,OverrunWriteProductFlow.ql同时被cpp-security-and-quality与cpp-security-extended两个套件收录,即它默认参与 C/C++ 代码扫描的安全质量检查。
三、工作原理:从源码看检测机制
3.1 核心依赖
OverrunWriteProductFlow.ql顶部导入了以下关键库:
import cpp import semmle.code.cpp.ir.dataflow.internal.ProductFlow import semmle.code.cpp.ir.IR import semmle.code.cpp.models.interfaces.Allocation import semmle.code.cpp.models.interfaces.ArrayFunction import semmle.code.cpp.rangeanalysis.new.internal.semantic.analysis.RangeAnalysis import semmle.code.cpp.rangeanalysis.new.internal.semantic.SemanticExprSpecific import semmle.code.cpp.security.ProductFlowUtils.ProductFlowUtils import semmle.code.cpp.rangeanalysis.new.RangeAnalysisUtil import StringSizeFlow::PathGraph1 import codeql.util.Unit这些依赖揭示了查询的三层机理:
- IR 层:基于 CodeQL 的中间表示(IR)进行分析,
CallInstruction、LoadInstruction等指令级元素是分析的原子单位。 - ProductFlow 库:这是一个“双投影”数据流框架,同时追踪缓冲区指针(第一投影)与缓冲区大小(第二投影)两条独立的数据流,并将二者关联成“缓冲区 + 大小”的配对约束。
- Range Analysis(范围分析):新式范围分析(
semmle.code.cpp.rangeanalysis.new)用于推导分配大小的数值上下界,判断写入长度是否会越界。
3.2 源点:提取分配大小表达式
查询通过hasSize谓词从分配表达式中提取“大小变量”与“偏移量 delta”:
predicate hasSize(HeuristicAllocationExpr alloc, DataFlow::Node n, int state) { exists(VariableAccess va, Expr size, int delta | size = alloc.getSizeExpr() and // Get the unique variable in a size expression like `x` in `malloc(x + 1)`. va = unique( | | getAVariableAccess(size)) and // Compute `delta` as the constant difference between `x` and `x + 1`. bounded(any(Instruction instr | instr.getUnconvertedResultExpression() = size), any(LoadInstruction load | load.getUnconvertedResultExpression() = va), delta) and n.asExpr() = va and state = delta ) }例如对malloc(x + 1),查询提取出变量x,并计算出常量偏移delta = 1,作为数据流的状态值(state)在后续传播中携带。这保证了malloc(size + 1)这类“多分配一个元素”的常见防御性写法不会被误判为越界。
3.3 汇点:识别带大小参数的缓冲区函数
isSinkPairImpl0谓词通过ArrayFunction模型识别“缓冲区 + 大小”成对出现的调用:
predicate isSinkPairImpl0( CallInstruction c, DataFlow::Node bufSink, DataFlow::Node sizeSink, int delta, Expr eBuf, Instruction sizeBound, Instruction sizeInstr ) { exists(int bufIndex, int sizeIndex, Instruction bufInstr, ArrayFunction func | bufInstr = bufSink.asInstruction() and c.getArgument(bufIndex) = bufInstr and sizeBound = sizeSink.asInstruction() and c.getArgument(sizeIndex) = sizeInstr and c.getStaticCallTarget() = func and pragmaonly_bind_into .hasArrayWithVariableSize(pragmaonly_bind_into, pragmaonly_bind_into) and bounded(sizeInstr, sizeBound, delta) and eBuf = bufInstr.getUnconvertedResultExpression() ) }ArrayFunction是 CodeQL 中为 C 库缓冲区操作函数建立的模型接口(定义于 cpp/ql/lib 下的models/interfaces/ArrayFunction.qll),它声明了哪些参数是缓冲区、哪些参数是其大小。strncpy(dst, src, n)中dst与n、memset(p, c, n)中p与n均属于这类配对。
3.4 状态传播与越界判定
查询的核心在于StringSizeConfig这一ProductFlow::StateConfigSig实现。它把“大小”维度设计为一个整数状态FlowState2:
- 源端:
hasSize(bufSource.asExpr(), sizeSource, state2)记录分配时的偏移量。 - 汇点:
isSinkPair中比较delta > state2——即调用处实际传入的大小参数超出分配时携带的大小状态偏移时,判定越界。 - 屏障:
SizeBarrier与isBarrierOut2用于阻断回边(back edge),避免循环导致的无限数据流与误报。
最终的getOverflow谓词计算“越过缓冲区边界访问的元素个数”,并输出形如以下的消息:
This write may overflow
$@by N elements.
从select子句看,查询以path-problem形式报告CallInstruction对应的表达式、双源点、双汇点及越界元素数,完整呈现从malloc到strncpy/memset的整条路径。
四、典型漏洞示例
仓库为查询配套了示例文件 cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.cpp:
int f(char * s, unsigned size) { char* buf = (char*)malloc(size); strncpy(buf, s, size + 1); // wrong: copy may exceed size of buf for (int i = 0; i <= size; i++) { // wrong: upper limit that is higher than size of buf cout << buf[i]; } }两处问题一目了然:
strncpy(buf, s, size + 1):buf只分配了size字节,却可能写入size + 1字节,越界 1 字节。for (int i = 0; i <= size; i++)循环:访问buf[0..size]共size + 1个元素,下标i == size时越界。
官方 qhelp 文档(OverrunWriteProductFlow.qhelp)给出的修复建议是:检查高亮操作中使用的偏移量与大小,确保不会发生缓冲区溢出。安全做法是改为strncpy(buf, s, size)并将循环上界改为i < size。
五、测试用例验证:查询行为边界
查询的自动化测试位于 cpp/ql/test/query-tests/Security/CWE/CWE-119/SAMATE/(对应.qlref为 OverrunWriteProductFlow.qlref),测试源码 test.cpp 用注释标注了预期结果($ Alert[cpp/overrun-write]/$ Source[cpp/overrun-write]/GOOD/MISSING),是理解查询能力边界的绝佳素材:
- 跨函数传递:
mk_string_t分配缓冲区并保存size到结构体,strncpy(str->string, buf, str->size)被判定 GOOD,而str->size + 1触发 Alert,证明查询能跟踪“分配→结构体保存→读取使用”的跨函数流。 - 包装函数:
strncpy_wrapper场景(test2)验证流经中间调用层后大小关系仍被保留。 - 条件约束:
if(anotherSize < str->size)保护下的调用是 GOOD,if(anotherSize <= str->size + 1)则是 BAD——查询对<=/<与+1偏移的组合语义判断细致。 size与str->size的差异:当结构体保存的大小与局部变量size不同源时(test3/test4),查询对“直接使用局部变量大小”的某些越界(如size + 1)标注MISSING: Alert [NOT DETECTED],如实反映medium精度的漏报边界。- 分配大小减一:test5 中
malloc(size - 1)配合str->size = size - 1,随后strncpy(..., str->size + 1)被检出,而strncpy(..., str->size)安全。 memset与分配语义:memset(p, 0, size + 1)对malloc(size)的缓冲越界被检出(test_flow_through_setter、foo);repeated_alerts中循环内++size导致的大小变化场景标注NOT DETECTED,体现对“大小在循环中可变”这一复杂情况的处理限度。- 误报抑制:test7 中
malloc(++n)后的memset(p, 0, n)被标注为SPURIOUS: Alert ... GOOD [FALSE POSITIVE],说明当前实现对该场景仍存在已知误报。
这些标注让查询的“已知能力”与“已知局限”都以可验证的方式固化在仓库中,是评估该查询精度时最重要的第一手资料。
六、与同族查询的分工:overrun-write 在 CWE-119/CWE-120 家族中的位置
cpp/overrun-write并非孤立存在,它与 CWE-120(缓冲区复制不当)下的多个查询构成互补的检测家族:
| 查询 ID | 实现文件 | 特点 |
|---|---|---|
cpp/overrun-write | Security/CWE/CWE-119/OverrunWriteProductFlow.ql | path-problem、ProductFlow 双投影、追踪“分配大小↔写入大小”跨函数流 |
cpp/overrunning-write | Security/CWE/CWE-120/OverrunWrite.ql | problem 类,基于BufferWrite模型,要求getMaxDataLimited估计值超过目标大小 |
cpp/very-likely-overrunning-write | Security/CWE/CWE-120/VeryLikelyOverrunWrite.ql | problem 类、precision high,仅采用ValueFlowAnalysis得出的高置信估计 |
cpp/overrunning-write-with-float | Security/CWE/CWE-120/OverrunWriteFloat.ql | 专门覆盖%f等浮点格式化导致的极端长度写入 |
从 OverrunWrite.ql 的源码可见,cpp/overrunning-write明确排除ValueFlowAnalysis原因的估计(“we exclude ValueFlowAnalysis as it is reported in cpp/very-likely-overrunning-write”),而 VeryLikelyOverrunWrite.ql 恰恰只接收ValueFlowAnalysis原因——两者互补去重。历史发布说明 cpp/ql/src/change-notes/released/0.0.8.md 也记载了cpp/very-likely-overrunning-write加入默认套件后接管了cpp/overrunning-write的部分结果。
相比之下,cpp/overrun-write走的是另一条技术路线:不依赖BufferWrite模型的“估计字节数”,而是通过 ProductFlow 将分配点的大小表达式与调用点的大小参数做符号级关联,因此在“结构体保存分配大小、经包装函数间接使用”这类场景下具备更强的跨函数追踪能力。
七、如何运行与验证该查询
在 CodeQL CLI 环境中,可使用查询套件或直接指定查询文件运行:
# 方式一:通过安全套件运行(该查询已收录) codeql database analyze <database> codeql/cpp-queries --format=sarif-latest --output=result.sarif # 方式二:直接指定查询文件 codeql database analyze <database> \ cpp/ql/src/Security/CWE/CWE-119/OverrunWriteProductFlow.ql \ --format=sarif-latest --output=result.sarif仓库内验证该查询的推荐方式是 CodeQL 查询测试框架:以 OverrunWriteProductFlow.qlref 指向查询文件,配合 test.cpp 中的GOOD/BAD标注,运行codeql test run即可复现全部预期结果。
八、小结
cpp/overrun-write是 C++ 查询包 0.6.3 引入的缓冲区溢出检测利器,其独特价值在于:
- path-problem 输出:给出从分配到写入的完整路径,便于定位根因;
- ProductFlow 双投影:同时追踪缓冲区指针与大小值,处理
malloc(x + 1)等带偏移分配、跨结构体/包装函数的大小传播; - 与既有查询家族互补:与
cpp/overrunning-write、cpp/very-likely-overrunning-write、cpp/overrunning-write-with-float按估计来源与置信度分工,降低重复告警。
值得注意的边界:该查询精度为medium,仓库测试明确标注了对“局部变量大小直接 +1”与“循环内大小递增”等场景的漏报,以及对malloc(++n)场景的已知误报。实际部署时,建议将其与同族查询(尤其是 precision 更高的cpp/very-likely-overrunning-write)组合使用,并依据测试用例理解告警含义,再结合代码评审确认修复方案。
- 静态分析
- SAST
- 应用安全
- 漏洞扫描
- 代码质量
【免费下载链接】codeql
CodeQL: the libraries and queries that power security researchers around the world, as well as code scanning in GitHub Advanced Security
相关推荐
CodeQL C/C++ 静态缓冲区溢出检测:从 `cpp/static-buffer-overflow` 的精度升级看安全查询工程化
CodeQL C/C++ 静态缓冲区溢出检测:从 cpp/static buffer overflow 的精度升级看安全查询工程化 本篇文章围绕 CodeQL
静态分析SAST应用安全漏洞扫描代码质量终极窗口管理技巧:5分钟掌握跨平台自动化
你是否曾经在多个应用窗口间频繁切换,却总是找不到想要的那个?或者想要自动化管理窗口布局,却苦于没有合适的工具?今天我要向你介绍一款真正的窗口管理工具——get
静态分析SAST应用安全漏洞扫描代码质量CodeQL C++ 查询 cpp/bad-strncpy-size 0.3.2 更新:strxfrm、wcsxfrm、stpncpy 等字符串复制函数纳入缓冲区大小误用检测
CodeQL C++ 查询 cpp/bad strncpy size 0.3.2 更新:strxfrm、wcsxfrm、stpncpy 等字符串复制函数纳入缓冲
静态分析SAST应用安全漏洞扫描代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考