使用 Semmle QL 进行漏洞搜索 Part 2
字数 820 2025-08-26 22:11:40
Semmle QL 漏洞挖掘技术详解(第二部分)
1. 概述
本文是Semmle QL漏洞挖掘系列的第二部分,重点介绍如何使用Semmle QL进行实际的漏洞挖掘工作。在第一部分介绍了Semmle QL基础概念后,本文将通过Azure固件组件的安全审核案例,详细演示如何利用Semmle QL进行静态代码分析。
2. 目标分析环境
2.1 审查目标
- 基于Linux的嵌入式设备
- 连接服务后端和管理后端
- 在两个接口之间传递操作数据
- 主要攻击面:两个接口使用的管理协议
2.2 协议特点
- 基于消息的管理协议
- 超过400种不同的消息类型
- 每种类型有独立的处理函数
- 手动审计难度大,易出错
3. 攻击定义与建模
3.1 消息路由机制
协议在请求-响应基础上工作,消息类型由类别和命令编号标识:
MessageCategoryTable g_MessageCategoryTable[] = {
{ CMD_CATEGORY_BASE, g_CommandHandlers_Base },
{ CMD_CATEGORY_APP0, g_CommandHandlers_App0 },
...
{ NULL, NULL }
};
CommandHandlerTable g_CommandHandlers_Base[] = {
{ CMD_GET_COMPONENT_VER, sizeof(ComponentVerReq), GetComponentVer },
{ CMD_GET_GLOBAL_CONFIG, -1, GetGlobalConfig },
...
};
3.2 QL建模 - 命令处理程序表
class CommandHandlerTable extends Variable {
CommandHandlerTable() {
exists(Variable v |
v.hasName("g_MessageCategoryTable") and
this.getAnAccess() = v.getInitializer().getExpr().getAChild().getChild(1)
)
}
}
3.3 QL建模 - 消息处理函数
class MessageHandlerFunction extends Function {
Expr tableEntry;
MessageHandlerFunction() {
exists(CommandHandlerTable table |
tableEntry = table.getInitializer().getExpr().getAChild()
) and
this = tableEntry.getChild(2).(FunctionAccess).getTarget()
}
int getExpectedRequestLength() {
result = tableEntry.getChild(1).getValue().toInt()
}
Parameter getRequestDataPointer() {
result = this.getParameter(0)
}
Parameter getRequestLength() {
result = this.getParameter(1)
}
}
4. 数据流分析技术
4.1 数据流库导入
import semmle.code.cpp.dataflow.DataFlow
4.2 基本数据流查询结构
class RequestDataFlowConfiguration extends DataFlow::Configuration {
RequestDataFlowConfiguration() { this = "RequestDataFlowConfiguration" }
override predicate isSource(DataFlow::Node source) { ... }
override predicate isSink(DataFlow::Node sink) { ... }
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) { ... }
override predicate isBarrier(DataFlow::Node node) { ... }
}
from DataFlow::Node source, DataFlow::Node sink
where any(RequestDataFlowConfiguration c).hasFlow(source, sink)
select "Data flow from $@ to $@", source, sink
5. 内存安全漏洞检测
5.1 数组越界访问检测
override predicate isSource(DataFlow::Node source) {
any(MessageHandlerFunction mhf).getRequestDataPointer() = source.asParameter()
}
override predicate isSink(DataFlow::Node sink) {
exists(ArrayExpr ae | ae.getArrayOffset() = sink.asExpr())
}
override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
exists(Expr e, FieldAccess fa |
node1.asExpr() = e and
node2.asExpr() = fa and
fa.getQualifier*() = e and
not (fa.getParent() instanceof FieldAccess)
)
}
override predicate isBarrier(DataFlow::Node node) {
exists(ConditionalStmt condstmt |
node.asExpr().(VariableAccess).getTarget().getAnAccess() =
condstmt.getControllingExpr().getAChild*() and
condstmt.getASuccessor+() = node.asExpr() and
not (node.asExpr() = cs.getControllingExpr().getAChild*())
)
}
5.2 危险函数调用检测
class ArgumentMustBeCheckedFunctionCall extends FunctionCall {
int argToCheck;
ArgumentMustBeCheckedFunctionCall() {
(this.getTarget().hasName("memcpy") and argToCheck = 2) or
(this.getTarget().hasName("_fmemcpy") and argToCheck = 2) or
(this.getTarget().hasName("CalculateChecksum") and argToCheck = 1)
}
Expr getArgumentToCheck() {
result = this.getArgument(argToCheck)
}
}
override predicate isSink(DataFlow::Node sink) {
exists(ArgumentMustBeCheckedFunctionCall fc |
fc.getArgumentToCheck() = sink.asExpr()
)
}
6. 污点跟踪技术
6.1 污点跟踪库导入
import semmle.code.cpp.dataflow.TaintTracking
6.2 污点传播规则
predicate isTaintedString(Expr expSrc, Expr expDest) {
exists(FunctionCall fc, Function f |
expSrc = fc.getArgument(1) and
expDest = fc.getArgument(0) and
f = fc.getTarget() and
(f.hasName("memcpy") or f.hasName("_fmemcpy") or
f.hasName("memmove") or f.hasName("strcpy") or
f.hasName("strncpy") or f.hasName("strcat") or
f.hasName("strncat"))
) or
exists(FunctionCall fc, Function f, int n |
expSrc = fc.getArgument(n) and
expDest = fc.getArgument(0) and
f = fc.getTarget() and
((f.hasName("sprintf") and n >= 1) or
(f.hasName("snprintf") and n >= 2))
)
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isTaintedString(node1.asExpr(), node2.asExpr())
}
7. 路径遍历漏洞检测
7.1 文件操作函数定义
class FileOpenFunction extends Function {
FileOpenFunction() {
this.hasName("fopen") or this.hasName("open")
}
int getPathParameter() {
result = 0 // filename parameter index
}
}
override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall fc, FileOpenFunction fof |
fc.getTarget() = fof and
fc.getArgument(fof.getPathParameter()) = sink.asExpr()
)
}
7.2 函数调用图分析
predicate mayCallFunction(Function caller, FunctionCall fc) {
fc.getEnclosingFunction() = caller or
mayCallFunction(fc.getTarget(), fc)
}
from MessageHandlerFunction mhf, FunctionCall fc, FileOpenFunction fof
where mayCallFunction(mhf, fc) and
fc.getTarget() = fof and
not fc.getArgument(fof.getPathParameter()).isConstant()
select mhf, "$@ may have a path to $@", mhf, mhf.toString(), fc, fc.toString()
8. 实际成果
通过上述技术,在目标代码库中发现了:
- 15个消息处理函数中的18个越界读写漏洞
- 17个未验证参数的函数调用漏洞(多数为越界读取)
- 2个消息处理函数中的3个整数下溢漏洞
- 5个潜在路径遍历漏洞(确认2个)
总计发现33个易受攻击的消息处理函数。
9. 总结
Semmle QL提供了强大的静态分析能力:
- 可以自动化执行重复性代码审查任务
- 能够发现传统手动审计容易遗漏的漏洞
- 支持复杂的数据流和污点跟踪分析
- 可针对特定代码模式定制查询
- 在深度防御策略中发挥重要作用
通过结合数据流分析和污点跟踪技术,Semmle QL能够有效地识别各种类型的安全漏洞,包括内存安全问题和逻辑漏洞。