CodeQL 文档

检测潜在的缓冲区溢出

您可以使用 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 需要扩展 FunctionCallNewExpr 的通用超类,例如 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) 中出现。这是因为更正后的代码实际上将是一个包含 StrlenCallAddExpr,而不是 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."
  • ©GitHub, Inc.
  • 条款
  • 隐私