开源安全基金会(OpenSSF)成立于2020年8月3日,是开源社区中专门关注软件代码和系统安全的组织。今天我们要介绍的文章 Compiler Options Hardening Guide for C and C++ 就来自OpenSSF,详细介绍了在编译C和C++代码的过程中,通过编译器选项的特定设定带来的安全加固优势:
在日常开发过程中,很多人可能都知道,为了保证代码的质量,在编译的过程中可以考虑使用-Wall
选项,让编译器尽可能告警,然后消除可能的不安全代码风格。但是除了-Wall
选项,你还知道多少其他的可以使用的选项呢?
在这篇技术博客的第三章,详细介绍了两大类安全加固的编译选项。第一类安全加固编译选项是编译期安全检查,主要是在编译过程中开展各种严格的静态分析检查,让潜在的问题代码在编译期就直接被检测出来;第二类安全加固编译选项是运行时安全检查,主要是让编译器给binary code加入更多的运行期检查插桩代码。接下来我们逐一看看各个具体的选项(注意,这里面只涉及到GCC编译器的选项,以及LLVM为了兼容GCC基本上也包括了同类的选项;M$家的VC编译器的选项目前还没包含进来)。
首先是最标准的两个参数-Wall
和-Wextra
,这个基本上严格一点的开发团队都会要求:
其次是-Wformat
选项,主要是为了检查printf
和scanf
的格式化参数是否存在类型误用。注意还有一个-Wformat=2
选项,能够检查更多的printf
和scanf
格式化参数问题,可以参考原文。
接下来是-Wconversion
和-Wsign-conversion
选项,顾名思义是对数值类型转换的严格检查:
大家对-Wtrampolines
这个选项是否有点眼熟?可以去看看我们去年的文章《G.O.S.S.I.P 阅读推荐 2024-10-24 To Write & To Execute》了解具体细节:
对于switch
结构中某些case中(可能无意)缺少break的检查,可以用-Wimplicit-fallthrough
这个选项来进行严格的检查:
针对最近几年出现的Trojan source code,最新的编译器(可以从下图中看到,GCC从12.0以后才开始支持)对一些人类可能会误读(例如从右往左字符集合)的代码可以开启-Wbidi-chars=
检查(默认不允许这些情况):
接下来,有一系列-Werror=
的检查选项,会把一些默认作为warning的代码问题都视为代码错误(并且阻止编译器生成binary),例如下面的代码写法就不能通过编译:
printf(fmt);
printf(gettext("Hello Worldn"));
fprintf(stderr, fmt);
上面介绍的都是静态编译期的检查,接下来的是选项是对生成的二进制代码加入安全检查插桩代码的选项:
首先是FORTIFY_SOURCE
选项,这个选项能够对GNU C library(就是我们常说的glibc)加入针对缓冲区溢出攻击的运行时检查,主要针对如下这些函数:
memcpy, mempcpy, memmove, memset, strcpy, stpcpy, strncpy, strcat, strncat, sprintf, vsprintf, snprintf, vsnprintf, gets
接下来是-D_GLIBCXX_ASSERTIONS
选项,这个主要是针对C标准库(libstdc)中的容器和字符串的越界等问题的动态检查:
下面这个-fstrict-flex-arrays
选项大家可能有点陌生,加入编译器的时间也比较晚,不过下面的代码例子大家可能知道,就是在C语言的结构体里面,如果一个数组放在最后(叫做trailing arrays),那么编译器是不会管它声明时候定义的size,而是允许在运行时才初始化这个指针(也就是说这个数组的长度范围不确定),这样虽然有历史原因,但是开发者可以要求编译器严格执行声明,杜绝对这种灵活性。
structtrailing_array {
int a;
int b;
int c[4];
};
structtrailing_array *trailing;
-fstack-clash-protection
和-param stack-clash-protection-guard-size
这两个选项,主要是针对所谓的“stack clash”问题的防护。Stack clash问题是一种奇怪的stack越界,它并不是说函数对于局部变量的越界读写,而是指整个stack在不断增长的过程中,入侵到了某个其他内存区域,然后攻击者可以藉由栈指针来访问某些并不是stack的内存区域:
而针对stack overflow的检查,当然有专门的参数-fstack-protector-*
来对应:
对应我们大家都很关心的控制流劫持问题,现代编译器提供了一系列的-fcf-protection
予以回应:
在移动平台上,对于运行时代码加载有比较严格的限制,而在桌面平台这方面的限制好像没那么严格,不过现在可以使用-Wl,-z,nodlopen
选项来对dlopen
进行一定的限制:
下面这个-Wl,-z,noexecstack
选项好像有点啰嗦,目前应该所有的操作系统都默认执行了stack的可写不可执行策略了吧?
然后是一些对relocation table的防护,不给一些针对此类table改写攻击可乘之机:
对于PIE和PIC代码生成的选项,好像又是有点啰嗦,2025年应该新代码都是按照这些选项编译了吧?
这个-fno-delete-null-pointer-checks
看起来非常简单:
对于算术运算中的有符号数的overflow,有一系列选项可以开启运行时检查:
-fno-strict-aliasing
是一个很有意思的选项,在很早期版本的GCC就支持了,但是LLVM/Clang直到18.0之后才开始支持。不过这里其实似乎是放松了对安全的检查?
-ftrivial-auto-var-init
这个选项很好理解:
-fexceptions
是一个非常古老的选项了,主要是对多线程代码的安全加固:
-fhardened
和-Whardened
是将一些预设的安全加固规则的整体应用:
最后是一系列链接器的安全加固选项:
文章的第五章是对于sanitizer的参数建议:
而第七章和第八章很有趣,是假定你在编译一个编译器/链接器时应该遵循的选项:
总之,对于编译器的使用绝对是一门手艺,大家平时如果只会用IDE去编译,可能就会越来越难以理解底层的知识,随着人工智能时代的来临,多学一点冷门的底层细节永远不会有错!
原文:https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html
原文始发于微信公众号(安全研究GoSSIP):G.O.S.S.I.P 阅读推荐 2025-04-09 编译器的安全之道
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论