检测潜在的缓冲区溢出¶
您可以使用 CodeQL 通过检查 C 和 C++ 中与 strlen
相等的分配来检测潜在的缓冲区溢出。本主题介绍了如何开发用于检测潜在缓冲区溢出的 C/C++ 查询。
问题 - 检测省略空终止符空间的内存分配¶
此查询的目标是检测 C/C++ 代码,这些代码分配的内存量等于空终止字符串的长度,但未添加 +1 以腾出空终止符的空间。例如,以下代码演示了此错误,并导致缓冲区溢出
void processString(const char *input)
{
char *buffer = malloc(strlen(input));
strcpy(buffer, input);
...
}
基本查询¶
在编写查询之前,您需要确定要搜索的实体,然后定义如何识别它们。
定义感兴趣的实体¶
您可以通过以下两种方法来解决此问题:搜索类似于第 3 行调用 malloc
或第 5 行调用 strcpy
的代码(请参阅上面的示例)。对于我们的基本查询,我们从一个简单的假设开始:任何仅使用 strlen
来定义内存大小的 malloc
调用很可能在填充内存时会导致错误。
可以使用库 StrlenCall 类识别对 strlen
的调用,但我们需要定义一个新类来识别对 malloc
的调用。库类和新类都需要扩展标准类 FunctionCall
,并添加对其适用的函数名的限制
import cpp
class MallocCall extends FunctionCall
{
MallocCall() { this.getTarget().hasGlobalName("malloc") }
}
注意
您可以轻松地扩展此类以包括类似的函数,例如
realloc
或您自己的自定义分配器。只需稍加努力,它们甚至可以包含 C++new
表达式(为此,MallocCall
需要扩展FunctionCall
和NewExpr
的通用超类,例如Expr
)。
查找 strlen(string)
模式¶
在我们开始编写查询之前,还有一个任务需要完成。我们需要修改新的 MallocCall
类,以便它返回分配大小的表达式。当前,这将是 malloc
调用的第一个参数 FunctionCall.getArgument(0)
,但将其转换为谓词使其在将来的细化中更灵活。
class MallocCall extends FunctionCall
{
MallocCall() { this.getTarget().hasGlobalName("malloc") }
Expr getAllocatedSize() {
result = this.getArgument(0)
}
}
定义基本查询¶
现在,我们可以使用这些类编写一个查询
import cpp
class MallocCall extends FunctionCall
{
MallocCall() { this.getTarget().hasGlobalName("malloc") }
Expr getAllocatedSize() {
result = this.getArgument(0)
}
}
from MallocCall malloc
where malloc.getAllocatedSize() instanceof StrlenCall
select malloc, "This allocation does not include space to null-terminate the string."
请注意,无需检查是否在 strlen
表达式中添加了任何内容,因为这将在更正后的 C 代码 malloc(strlen(string) + 1)
中出现。这是因为更正后的代码实际上将是一个包含 StrlenCall
的 AddExpr
,而不是 StrlenCall
本身。这种方法的一个副作用是,我们省略了某些不太可能的模式,例如 malloc(strlen(string) + 0
。在实践中,如果这是一个问题,我们始终可以返回并扩展查询以涵盖此模式。
提示
对于某些项目,此查询可能不会返回任何结果。您查询的项目可能没有任何此类问题,但也务必确保查询本身正常运行。一种解决方案是设置一个测试项目,其中包含正确和不正确的代码示例以供查询运行(此页面最顶部的 C 代码是一个很好的起点)。另一种方法是单独测试查询的每个部分,以确保所有内容都正常运行。
定义好基本查询后,您可以细化查询以包含更多编码模式或排除误报
使用“SSA”库改进查询¶
SSA
库以静态单赋值 (SSA) 形式表示变量。在此形式中,每个变量恰好分配一次,并且每个变量都在使用之前定义。SSA 变量的使用简化了查询,因为大多数局部数据流分析已为我们完成。有关详细信息,请参阅维基百科上的 静态单赋值。
包含字符串大小在使用前存储的示例¶
上面的查询适用于简单情况,但不会识别一个常见的编码模式,其中 strlen(string)
在传递给 malloc
之前存储在变量中,如以下示例所示
int len = strlen(input);
buffer = malloc(len);
为了识别这种情况,我们可以使用标准库 SSA.qll
(作为 semmle.code.cpp.controlflow.SSA
导入)。
此库帮助我们识别分配给局部变量的值随后可能在何处使用。
例如,请考虑以下代码
void myFunction(bool condition)
{
const char* x = "alpha"; // definition #1 of x
printf("x = %s\n", x); // use #1 of x
if (condition)
{
x = "beta"; // definition #2 of x
} else {
x = "gamma"; // definition #3 of x
}
printf("x = %s\n", x); // use #2 of x
}
如果我们在代码上运行以下查询,我们将获得三个结果
import cpp
import semmle.code.cpp.controlflow.SSA
from Variable var, Expr defExpr, Expr use
where exists(SsaDefinition ssaDef |
defExpr = ssaDef.getAnUltimateDefiningValue(var)
and use = ssaDef.getAUse(var))
select var, defExpr.getLocation().getStartLine() as dline, use.getLocation().getStartLine() as uline
结果
var |
dline |
uline |
---|---|---|
x |
3 | 5 |
x |
9 | 14 |
x |
11 | 14 |
通常,如果存在定义表达式 defExpr
,则显示它也很有用。例如,我们可以调整上面的查询,如下所示
import cpp
import semmle.code.cpp.controlflow.SSA
from Variable var, Expr defExpr, Expr use
where exists(SsaDefinition ssaDef |
defExpr = ssaDef.getAnUltimateDefiningValue(var)
and use = ssaDef.getAUse(var))
select var, defExpr.getLocation().getStartLine() as dline, use.getLocation().getStartLine() as uline, defExpr
现在,我们可以在结果中看到分配的表达式
var |
dline |
uline |
defExpr |
---|---|---|---|
x |
3 | 5 | alpha |
x |
9 | 14 | beta |
x |
11 | 14 | gamma |
扩展查询以包含通过变量传递的分配¶
使用上面的实验,我们可以扩展 MallocCall.getAllocatedSize()
的简单实现。通过以下细化,如果参数是访问变量,则 getAllocatedSize()
返回分配给该变量的值,而不是变量访问本身
Expr getAllocatedSize() {
if this.getArgument(0) instanceof VariableAccess then
exists(LocalScopeVariable v, SsaDefinition ssaDef |
result = ssaDef.getAnUltimateDefiningValue(v)
and this.getArgument(0) = ssaDef.getAUse(v))
else
result = this.getArgument(0)
}
完成的查询现在将识别 strlen
的结果在传递给 malloc
之前存储在局部变量中的情况。以下是查询的完整内容
import cpp
import semmle.code.cpp.controlflow.SSA
class MallocCall extends FunctionCall
{
MallocCall() { this.getTarget().hasGlobalName("malloc") }
Expr getAllocatedSize() {
if this.getArgument(0) instanceof VariableAccess then
exists(LocalScopeVariable v, SsaDefinition ssaDef |
result = ssaDef.getAnUltimateDefiningValue(v)
and this.getArgument(0) = ssaDef.getAUse(v))
else
result = this.getArgument(0)
}
}
from MallocCall malloc
where malloc.getAllocatedSize() instanceof StrlenCall
select malloc, "This allocation does not include space to null-terminate the string."