警惕安全特性遗漏的拼写错误及其测试方法

本文探讨了在C/C++项目中因拼写错误导致_FORTIFY_SOURCE宏未启用而引发的安全风险,详细介绍了源加固机制的工作原理、常见错误模式及测试方法,并提供了实用的二进制分析工具推荐。

警惕安全特性遗漏的拼写错误及其测试方法

源加固机制的工作原理

源加固是一种安全缓解措施,通过将特定函数调用替换为执行额外运行时或编译时检查的更安全包装函数来实现。通过定义特殊宏“_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目标缓冲区的正确长度。

拼写错误难以避免

由于源加固由预处理器宏决定,宏拼写错误会 effectively 禁用它,而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 设计