安全特性缺失的拼写错误及其测试方法

本文探讨了在C/C++项目中因拼写错误导致_FORTIFY_SOURCE宏未启用,从而无法检测内存破坏漏洞的问题,并提供了测试和修复方法,涉及多个实际案例和工具使用。

安全特性缺失的拼写错误及其测试方法

在安全审计期间,我发现了一个容易忽略的拼写错误,无意中未能启用_FORTIFY_SOURCE,这有助于检测错误使用的C函数中的内存破坏漏洞。我们在GitHub上搜索、发现并修复了20个具有相同模式的C和C++错误。以下是与该拼写错误相关的一些示例:

  • microsoft/binskim#777
  • PowerShell/PowerShell-Native#88
  • apple-open-source/macos#3:尽管这是一个非官方分支,我已在Apple的Feedback Assistant中进一步报告了此问题
  • trailofbits/cb-multios#96(是的,我们也有这个问题!)
  • lavabit/libdime#49
  • lavabit/magma#155
  • Jackysi/advancedtomato#454
  • adaptivecomputing/torque#474
  • gstrauss/mcdb#14
  • Homegear/Homegear#364
  • sergey-dryabzhinsky/dedupsqlfs#235
  • randlabs/algorand-windows-node#5
  • rpodgorny/unionfs-fuse#131
  • cgaebel/pipe#15
  • jkrh/kvms#48
  • angaza/nexus-embedded#8
  • hashbang/book#24

我们将向您展示如何测试代码以避免此问题,该问题可能使漏洞利用更容易。

源加固的工作原理

源加固是一种安全缓解措施,它用执行额外运行时或编译时检查的更安全包装器替换某些函数调用。

通过定义一个特殊宏“_FORTIFY_SOURCE=”,值为1、2或3,并使用优化编译程序来启用源加固。值越高,加固的函数越多或执行的检查越多。此外,libc库和编译器必须支持源加固选项,glibc、Apple Libc、gcc和LLVM/Clang支持,但musl libc和uClibc-ng不支持。实现细节也可能有所不同。例如,级别值3最近才在glibc 2.34中添加,但似乎Apple Libc中不可用。

以下示例显示了源加固的实际作用。无论我们是否启用缓解措施,生成的二进制文件都将调用strcpy函数或其__strcpy_chk包装器:

图1:编译器生成的汇编比较(通过Compiler Explorer创建)。

在这种情况下,__strcpy_chk包装器函数由glibc实现(源代码):

1
2
3
4
5
6
7
/* 复制SRC到DEST并检查DEST缓冲区溢出*/
char * __strcpy_chk (char *dest, const char *src, size_t destlen) {
    size_t len = strlen (src);
    if (len >= destlen)
    __chk_fail ();
    return memcpy (dest, src, len + 1);
}

图2:glibc中的__strcpy_chk函数

如我们所见,包装器多接受一个参数——目标缓冲区大小——然后检查源的长度是否大于目标。如果是,包装器调用__chk_fail函数,该函数中止进程。图1显示编译代码在mov edx, 10指令中传递了dest目标缓冲区的正确长度。

拼写错误很常见

由于预处理器宏决定了源加固,宏拼写中的拼写错误会有效地禁用它,而libc和编译器都不会捕获此问题,这与使用编译器标志而非宏启用的其他安全加固选项中的拼写错误不同。

实际上,如果您向编译器传递“-DFORTIFY_SOURCE=2 -O2”而不是“-D_FORTIFY_SOURCE=2 -O2”,源加固将不会被启用,包装器函数也不会被使用:

图3:在_FORTIFY_SOURCE宏中拼写错误时的汇编(通过Compiler Explorer创建)

我使用grep.app、sourcegraph.com和cs.github.com工具搜索了此错误模式和类似错误模式,并发送了20个拉取请求。我的三个拉取请求与本文开头的列表略有不同。

  • kometchtech/docker-build#50使用了“-FORTIFY_SOURCE=2 -O2”。这不会被检测为编译器错误,因为它是一个“-F”标志,用于设置“框架包含文件的搜索路径”。
  • ned14/quickcpplib#37在“-fsanitize=safe-stack”编译器标志中有拼写错误。尽管编译器检测到此类拼写错误,但该标志在CMake脚本中用于确定编译器是否支持安全堆栈缓解。由于此拼写错误,CMake脚本从未启用此缓解措施。我感谢我的同事Paweł Płatek发现了这个案例,他建议检查编译器是否检测到安全相关标志中的拼写错误。尽管它们确实检测,但标志拼写错误仍可能在编译器功能检测期间导致问题。
  • OpenImageIO/oiio#3729是一个无效的报告/PR,因为“-DFORTIFY_SOURCE=2”选项为CMake变量提供了一个值,最终导致设置了正确的_FORTIFY_SOURCE宏。(然而,这仍然是一个不幸的CMake变量名称。)

我使用的三个代码搜索工具可以找到更多类似案例,但我没有向所有项目发送PR,例如当项目似乎被放弃时。

测试_FORTIFY_SOURCE缓解

除了在持续集成期间测试代码外,开发人员还应测试构建系统的结果以及他们选择启用的选项。除了帮助检测回归外,这还可以帮助理解选项的真正作用,例如当优化禁用时源加固被禁用。

那么,如何查看是否正确启用了源加固?您可以扫描二进制文件使用的符号,并确保您期望使用的加固源函数确实被使用。如下所示的简单Bash脚本可以实现这一点:

1
2
3
4
5
if readelf --symbols /bin/ls | grep -q ' __snprintf_chk@'; then
    echo "snprintf is fortified";
else
    echo "snprintf is not fortified";
fi

图3:检查加固符号的简单Bash脚本

然而,在实践中,您应该使用二进制分析工具扫描二进制文件的安全缓解措施,例如checksec.rs、BinSkim Binary Analyzer、Pwntools’ checksec、checksec.sh或winchecksec(Trail of Bits为Windows上的checksec创建的工具)。

在使用工具之前,最好仔细检查它是否正常工作。如上文错误列表中所引用,BinSkim在其建议文本中有拼写错误。另一个错误,这次在checksec.sh中,导致“2020年家庭路由器安全报告”中的结果不正确。checksec.sh中的错误原因是什么?如果扫描的二进制文件使用了堆栈金丝雀,“__stack_chk_fail”符号(用于在金丝雀损坏时中止程序)错误地计入了源加固。这是因为checksec.sh在readelf –symbols命令的输出中查找“_chk”字符串,而不是期望符号名称后缀匹配“_chk”字符串。在slimm609/checksec.sh#103和slimm609/checksec.sh#130中报告的问题解决后,此错误似乎已修复。

还值得注意的是,BinSkim和checksec.sh都可以告诉您二进制文件中有多少可加固函数与已加固函数。它们是如何做到的?BinSkim保留了一个从glibc推导出的可加固函数名称的硬编码列表,而checksec.sh扫描您自己的glibc以确定这些名称。尽管这可以防止一些误报,但这些解决方案仍然不完美。如果您的二进制文件链接到不同的libc,或者在BinSkim的情况下,如果glibc添加了新的可加固函数,该怎么办?最后但并非最不重要的是,没有工具检测实际使用的加固级别,但这可能只影响可加固函数的数量。我不确定。

有趣的事实:Nginx中的拼写错误

在这项研究期间,我还发现Debian的Nginx包过去有过这种拼写错误。目前,Nginx包使用dpkg-buildflags工具提供正确的宏标志:

1
2
3
4
5
$ dpkg-buildflags --get CPPFLAGS
-Wdate-time -D_FORTIFY_SOURCE=2

$ dpkg-buildflags --get CFLAGS
-g -O2 -fdebug-prefix-map=/tmp/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security

奇怪的是,源加固和优化标志被分成CFLAGS和CPPFLAGS。难道一些项目使用一个而不使用另一个,从而错过一些选项吗?我没有检查过。

一些愿望

在理想的世界中,编译器会自动在生成的二进制文件中包含所有必要的安全缓解措施和加固选项的信息。然而,我们受限于必须处理的不完整信息。

在测试构建系统时,似乎没有银弹,尤其是因为并非所有安全缓解措施都易于检查,有些可能需要分析生成的汇编。我们没有详尽分析工具,但可能建议在Linux上使用checksec.rs或BinSkim,在Windows上使用winchecksec。我们还计划扩展Blight,我们的构建检测工具,以在构建时发现本文中描述的错误。即便如此,扫描生成的二进制文件以确认编译器和链接器的操作可能仍然有意义。

最后,如果您觉得这项研究有趣,并希望进一步保护您的软件,请联系我们,因为我们喜欢解决困难的安全问题。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计