跳到内容

基于C的工具链强化备忘单

简介

基于C的工具链强化是对项目设置的一种处理,旨在帮助您在使用C、C++和Objective C语言的多种开发环境中交付可靠且安全的代码。本文将探讨针对C、C++和Objective C语言的Microsoft和GCC工具链。它将引导您完成创建具有更强防御姿态并与可用平台安全性更高集成的可执行文件所需的步骤。有效地配置工具链还意味着您的项目在开发过程中将享有多项优势,包括增强的警告和静态分析,以及自调试代码。

强化工具链时,需要检查四个方面:配置、预处理器、编译器和链接器。在设置项目时,几乎所有这些方面都被忽视或忽略。这种忽视似乎普遍存在,并且适用于几乎所有项目,包括自动配置的项目、基于Makefile、基于Eclipse、基于Visual Studio和基于Xcode的项目。在配置和构建时解决这些问题至关重要,因为在某些平台上,事后对已分发的可执行文件添加强化几乎不可能。

这是一篇规范性文章,它不会争论语义或推测行为。某些信息,例如C/C++委员会对程序诊断NDEBUGassertabort()的动机和出处,似乎像《指环王》中的故事一样失传了。因此,本文将明确语义(例如,“调试”和“发布”构建配置的理念)、指定行为(例如,断言在“调试”和“发布”构建配置中应做什么),并提出一个立场。如果您觉得这种姿态过于激进,那么您可以根据需要进行调整以适应您的偏好。

安全工具链并非万能药。它是工程过程中整体策略的一部分,旨在帮助确保成功。它将补充现有的流程,例如静态分析、动态分析、安全编码、负面测试套件等。Valgrind和Helgrind等工具仍然是必需的,项目仍然需要扎实的设计和架构。

OWASP ESAPI C++ 项目身体力行。您将在本文中看到的大多数示例都直接来自ESAPI C++项目。

最后,对于那些希望简洁处理材料的人,我们提供了一份备忘单。请访问基于C的工具链强化备忘单获取缩略版本。

准则

代码**必须**正确。它**应该**安全。它**可以**高效。

Jon Bentley博士“如果不需要正确,我可以让它运行得和你希望的一样快”

Gary McGraw博士“构建安全软件时,不应仅仅依赖安全特性和功能,因为安全性是整个系统的涌现属性,因此依赖于正确构建和集成所有部分”

配置

配置是您首次有机会配置项目以取得成功。您不仅要配置项目以满足可靠性和安全性目标,还必须正确配置集成库。通常有三种选择。首先,如果您在Linux或Unix上,可以使用自动配置工具。其次,您可以手动编写Makefile。这在Linux、macOS和Unix上很常见,但也适用于Windows。最后,您可以使用集成开发环境(IDE)。

构建配置

在此阶段,您应专注于配置两种构建:调试版和发布版。调试版将用于开发,并包含完整的检测。发布版将配置用于生产。两者设置之间的差异通常是优化级别调试级别。第三种构建配置是测试版,通常是发布版的一个特例。

对于调试版和发布版,设置通常截然相反。调试配置不进行优化并包含完整的调试信息,而发布构建则进行优化并包含最少到中等的调试信息。此外,调试代码包含完整的断言和附加库集成,例如mudflaps和dmalloc等内存分配防护。

测试配置通常是发布配置的一种,它将所有内容公开用于测试并构建一个测试工具。例如,所有公共成员函数(C++类)和所有接口(库或共享对象)都应可用于测试。许多面向对象纯粹主义者反对测试私有接口,但这与面向对象无关。这(参见)是为了构建可靠和安全的软件。

GCC 4.8引入了-Og优化。请注意,它仅是优化,并且仍然需要通过-g实现常规调试级别。

调试构建

调试构建是开发人员在审查问题时花费大部分时间的地方,因此此构建应集中力量和工具,或成为“力量倍增器”。尽管许多人没有意识到,调试代码比发布代码更有价值,因为它附加了额外的检测。调试检测将使程序几乎“自调试”,并帮助您捕获诸如错误参数、API调用失败和内存问题等错误。

自调试代码可减少您在故障排除和调试上的时间。减少在调试器下的时间意味着您有更多时间进行开发和处理功能请求。如果代码在未进行调试检测的情况下提交,则应通过添加检测来修复或拒绝。

对于GCC,优化和调试符号化通过两个开关控制:-O-g。您应该将以下内容作为CFLAGSCXXFLAGS的一部分用于最小调试会话

-O0 -g3 -ggdb

-O0关闭优化,-g3确保提供最大化的调试信息。您可能需要使用-O1以进行一些分析。否则,您的调试构建将缺少发布构建中不存在的许多警告。-g3确保为调试会话提供最大化的调试信息,包括符号常量和#defines-ggdb包含有助于在GDB下进行调试会话的扩展。完整起见,Jan Krachtovil在私人电子邮件中表示-ggdb目前没有效果。

发布构建还应考虑-mfunction-return=thunk-mindirect-branch=thunk的配置对。这些是“Reptoline”修复,它是一种间接分支,用于阻止诸如Spectre和Meltdown等推测执行CPU漏洞。CPU无法判断要推测执行哪段代码,因为它是一个间接(而非直接)分支。这是一个额外的间接层,就像通过指针调用指针一样。

调试构建还应定义DEBUG,并确保NDEBUG未定义。NDEBUG会移除“程序诊断”并具有不期望的行为和副作用,这些将在下面更详细地讨论。这些定义应存在于所有代码中,而不仅仅是程序。您将它用于所有代码(您的程序和包含的库),因为您也需要知道它们是如何失败的(记住,您负责错误报告——而不是第三方库)。

此外,您还应使用其他相关标志,例如-fno-omit-frame-pointer。确保存在帧指针可使解码堆栈跟踪更容易。由于调试构建不会发布,因此将符号保留在可执行文件中是可以的。包含调试信息的程序不会遭受性能损失。例如,请参阅gcc -g选项如何影响性能?

最后,您应确保您的项目包含附加诊断库,例如dmalloc地址消毒器(Address Sanitizer)。一些内存检查工具的比较可以在内存工具比较中找到。如果您在调试构建中不包含附加诊断,那么您应该开始使用它们,因为发现您没有寻找的错误是可以的。

发布构建

发布构建是您的客户收到的内容。它们旨在在生产硬件和服务器上运行,并且应可靠、安全且高效。稳定的发布构建是开发过程中辛勤工作和努力的成果。

对于发布构建,您应将以下内容作为发布构建的CFLAGSCXXFLAGS的一部分

-On -g2

-On设置速度或大小的优化(例如,-Os-O2),而-g2确保创建调试信息。

调试信息应被剥离并保留,以备从现场获得的崩溃报告进行符号化。虽然不建议,但调试信息可以保留,而不会带来性能损失。有关详细信息,请参阅gcc -g选项如何影响性能?

发布构建还应定义NDEBUG,并确保DEBUG未定义。调试和诊断的时间已经结束,因此用户获得具有完整优化、无“编程诊断”和其他效率的生产代码。如果您无法优化或正在进行过多的日志记录,通常意味着程序尚未准备好投入生产。

如果您一直依赖assert然后随后的abort(),那么您一直在滥用“程序诊断”,因为它在生产代码中没有位置。如果您想要内存转储,请自行创建一个,这样用户就不必担心秘密和其他敏感信息以明文形式写入文件系统并被电子邮件发送。

对于Windows,调试构建使用/Od,发布构建使用/Ox/O2/Os。有关详细信息,请参阅Microsoft的/O选项(优化代码)

测试构建

测试构建用于通过正面和负面测试套件提供启发式验证。在测试配置下,所有接口都经过测试,以确保它们按照规范和满意度执行。“满意度”是主观的,但即使面对负面测试,也应包括不崩溃且不破坏内存区域。

由于所有接口都经过测试(而不仅仅是公共接口),因此您的CFLAGSCXXFLAGS应包含

-Dprotected=public -Dprivate=public

您还应将__attribute__ ((visibility ("hidden")))更改为__attribute__ ((visibility ("default")))

几乎每个人都能正确进行正面测试,因此无需多言。负面自测试更有趣,您应该专注于尝试让您的程序失败,以便您可以验证它是否优雅地失败。记住,恶意行为者在试图使您的程序失败时不会礼貌行事,而您的项目将因错误报告或在Full DisclosureBugtraq上的客串而丢脸——而不是您包含的<某个库>

Autotools

自动化配置工具在许多基于Linux和Unix的系统上很流行,这些工具包括AutoconfAutomakeconfigConfigure。这些工具协同工作,从脚本和模板文件生成项目文件。流程完成后,您的项目应该已设置好,并可以使用make进行构建。

使用自动配置工具时,有一些值得一提的有趣文件。这些文件是Autotools链的一部分,包括m4以及各种*.in*.ac (Autoconf)和*.am (Automake)文件。有时,您将不得不打开它们或生成的Makefile,以调整“默认”配置。

工具链中的命令行配置工具有三个缺点:(1)它们经常忽略用户请求,(2)它们无法创建配置,以及(3)安全通常不是目标。

为了说明第一个问题,请使用以下命令配置您的项目:configure CFLAGS="-Wall -fPIE" CXXFLAGS="-Wall -fPIE" LDFLAGS="-pie"。您可能会发现Autotools忽略了您的请求,这意味着下面的命令不会产生预期的结果。作为变通方法,您将不得不打开一个m4脚本、makefile.inmakefile.am,并修复配置。

$ configure CFLAGS="-Wall -Wextra -Wconversion -fPIE -Wno-unused-parameter
    -Wformat=2 -Wformat-security -fstack-protector-all -Wstrict-overflow"
    LDFLAGS="-pie -z,noexecstack -z,noexecheap -z,relro -z,now"

对于第二点,您可能会失望地了解到Automake不支持配置概念。这并非完全是Autoconf或Automake的错——Make及其无法检测变更才是根本问题。具体来说,Make检查先决条件和目标的修改时间,而不检查诸如CFLAGSCXXFLAGS之类的内容。最终结果是,当您执行make debug,然后执行make testmake release时,您将无法获得预期结果。

最后,您可能会失望地了解到诸如Autoconf和Automake之类的工具错失了许多与安全相关的机会,并且默认不安全地发布。有许多编译器开关和链接器标志可以改进程序的防御姿态,但它们默认不是“开启”的。像Autoconf这样的工具——本应处理这种情况——通常提供服务于最低共同点的设置。

Automake邮件列表上最近的一次讨论阐明了这个问题:启用编译器警告标志。改进默认配置的尝试遭到了抵制,并且没有采取任何行动。这种抵制通常表现为“<某些有用的警告>也会产生误报”或“<某些不常见的平台>不支持<既定的安全功能>”。值得注意的是,《Linux和Unix安全编程指南》的作者David Wheeler是试图改善这种态势的人之一。

Makefile

Make是可追溯到20世纪70年代的最早的构建工具之一。它在Linux、macOS和Unix上都可用,因此您会经常遇到使用它的项目。不幸的是,Make有许多缺点(递归Make被认为有害GNU make有什么问题?),并且可能造成一些不适。尽管Make存在问题,ESAPI C++主要出于三个原因使用Make:第一,它无处不在;第二,它比Autotools系列更容易管理;第三,libtool根本不可能。

考虑一下当您执行以下操作时会发生什么:(1)输入make debug,然后输入make release。由于优化和调试支持级别不同,每次构建都需要不同的CFLAGS。在您的Makefile中,您将提取相关目标并设置类似于以下的CFLAGSCXXFLAGS(取自ESAPI C++ Makefile

## makefile
DEBUG_GOALS = $(filter $(MAKECMDGOALS), debug)
ifneq ($(DEBUG_GOALS),)
    WANT_DEBUG := 1
    WANT_TEST := 0
    WANT_RELEASE := 0
endif
…

ifeq ($(WANT_DEBUG),1)
    ESAPI_CFLAGS += -DDEBUG=1 -UNDEBUG -g3 -ggdb -O0
    ESAPI_CXXFLAGS += -DDEBUG=1 -UNDEBUG -g3 -ggdb -O0
endif

ifeq ($(WANT_RELEASE),1)
    ESAPI_CFLAGS += -DNDEBUG=1 -UDEBUG -g -O2
    ESAPI_CXXFLAGS += -DNDEBUG=1 -UDEBUG -g -O2
endif

ifeq ($(WANT_TEST),1)
    ESAPI_CFLAGS += -DESAPI_NO_ASSERT=1 -g2 -ggdb -O2 -Dprivate=public
                                                      -Dprotected=public
    ESAPI_CXXFLAGS += -DESAPI_NO_ASSERT=1 -g2 -ggdb -O2 -Dprivate=public
                                                        -Dprotected=public
endif
…

## Merge ESAPI flags with user supplied flags. We perform the extra step to ensure
## user options follow our options, which should give user option's a preference.
override CFLAGS := $(ESAPI_CFLAGS) $(CFLAGS)
override CXXFLAGS := $(ESAPI_CXXFLAGS) $(CXXFLAGS)
override LDFLAGS := $(ESAPI_LDFLAGS) $(LDFLAGS)
…

Make将首先使用类似于以下规则在调试配置中构建程序,以便在调试器下进行会话

%.cpp:%.o:
        $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@

当您想要发布构建时,Make将不做任何事情,因为它认为所有内容都是最新的,尽管CFLAGSCXXFLAGS已经更改。因此,您的程序实际上将处于调试配置中,并可能在运行时面临SIGABRT的风险,因为存在调试检测(回想一下当NDEBUG**未**定义时,assert会调用abort())。本质上,您由于make而对自己进行了DoS攻击。

此外,许多项目不遵守用户的命令行。ESAPI C++尽力通过上述的override确保用户的标志得到尊重,但其他项目则不然。例如,考虑一个应该启用位置无关可执行文件(PIE或ASLR)和数据执行保护(DEP)的项目。忽略用户设置加上默认不安全的设置(并且在自动设置或自动配置期间没有拾取它们)意味着使用以下方式构建的程序可能不具有这两种防御

make CFLAGS="-fPIE" CXXFLAGS="-fPIE" LDFLAGS="-pie -z,noexecstack, -z,noexecheap"

ASLR和DEP等防御措施在Linux上尤为重要,因为数据执行(而非数据执行保护)是常态

集成

项目级集成提供了利用特定领域知识强化程序或库的机会。例如,如果平台支持位置无关可执行文件(PIE或ASLR)和数据执行保护(DEP),那么您应该与其集成。不这样做可能导致漏洞利用。举例来说,请参见KingCope在2012年12月针对MySQL的0day漏洞(CVE-2012-5579和CVE-2012-5612等)。与平台安全性的集成将削弱许多0day漏洞。

您还有机会包含业务逻辑支持不需要的有用库。例如,如果您正在使用DMalloc地址消毒器(Address Sanitizer)的平台,您应该在调试构建中可能使用它。对于Ubuntu,DMalloc可从包管理器获取,并可通过sudo apt install libdmalloc5安装。对于Apple平台,它可作为方案选项。地址消毒器在GCC 4.8及更高版本中适用于许多平台。

此外,项目级集成是强化您选择包含的第三方库的机会。因为您选择包含它们,所以您和您的用户对它们负责。如果您或您的用户经受SP800-53审计,第三方库将纳入范围,因为供应链已包含在内(具体而言,项目SA-12,供应链保护)。审计不限于美国联邦领域——金融机构也进行审查。违反此指导的完美例子是CVE-2012-1525,其原因是Adobe包含了一个有缺陷的Sablotron库

另一个例子是包含OpenSSL。您知道(1)SSLv2不安全,(2)SSLv3不安全,以及(3)压缩不安全(等等)。此外,假设您不使用硬件和引擎,并且只允许静态链接。鉴于这些知识和规范,您将按如下方式配置OpenSSL库

$ Configure darwin64-x86_64-cc -no-hw -no-engine -no-comp -no-shared
    -no-dso -no-ssl2 -no-ssl3 --openssldir=

请注意:您可能需要引擎,尤其是在Ivy Bridge微架构(第三代英特尔酷睿i5和i7处理器)上。要让OpenSSL使用处理器的随机数生成器(通过rdrand指令),您需要调用OpenSSL的ENGINE_load_rdrand()函数,然后使用ENGINE_METHOD_RAND调用ENGINE_set_default。有关详细信息,请参阅OpenSSL的随机数

如果您不使用这些开关进行配置,那么您很可能拥有易受攻击的代码/库,并有审计失败的风险。如果程序是远程服务器,那么以下命令将揭示通道上是否启用了压缩

echo "GET / HTTP1.0" | openssl s_client -connect <nowiki>example.com:443</nowiki>

nmopenssl s_client将显示客户端中启用了压缩。实际上,OPENSSL_NO_COMP预处理器宏中的任何符号都将证实这一点,因为-no-comp被转换为CFLAGS定义。

$ nm /usr/local/ssl/iphoneos/lib/libcrypto.a 2>/dev/null | egrep -i "(COMP_CTX_new|COMP_CTX_free)"
0000000000000110 T COMP_CTX_free
0000000000000000 T COMP_CTX_new

更令人发指的是,当审计师专门询问配置和协议时,给出的答案是:“我们不使用弱/有缺陷/已损坏的密码”或“我们遵循最佳实践”。使用压缩告诉审计师您正在不安全的配置中使用有缺陷的协议,并且您不遵循最佳实践。这很可能会敲响警钟,并确保审计师更深入地审查更多项目。

预处理器

预处理器对于项目的成功设置至关重要。C委员会提供了一个宏——NDEBUG——该宏可用于派生多种配置并推动工程流程。不幸的是,委员会还将许多相关项留给了偶然,这导致程序员滥用内置功能。本节将帮助您设置项目,使其与其他项目良好集成并确保可靠性和安全性。

强化预处理器时,需要讨论三个主题。第一个是产生明确行为的定义明确的配置,第二个是断言的有用行为,第三个是在集成供应商代码和第三方库时宏的正确使用。

配置

为了消除歧义,您应该识别两种配置:发布版和调试版。发布版用于生产服务器上的生产代码,其行为通过C/C++ NDEBUG宏请求。它也是C和C++委员会以及Posix观察到的唯一宏。与发布版截然相反的是调试版。虽然针对!defined(NDEBUG)有令人信服的论证,但您应该为该配置提供一个显式宏,并且该宏应该是DEBUG。这是因为供应商和外部库使用DEBUG(或类似)宏进行配置。例如,卡内基梅隆大学的Mach内核使用DEBUG,微软的CRT使用_DEBUG,而Wind River Workbench使用DEBUG_MODE

除了NDEBUG(发布版)和DEBUG(调试版)之外,您还有两个额外的交叉乘积:两者都已定义或两者都未定义。同时定义两者应视为错误,而两者都不定义应默认为发布配置。下面内容来自ESAPI C++ EsapiCommon.h,它是所有源文件使用的配置文件

// Only one or the other, but not both
##if (defined(DEBUG) || defined(_DEBUG)) && (defined(NDEBUG)
                                           || defined(_NDEBUG))
## error Both DEBUG and NDEBUG are defined.
##endif

// The only time we switch to debug is when asked.
// NDEBUG or {nothing} results
// in release build (fewer surprises at runtime).
##if defined(DEBUG) || defined(_DEBUG)
## define ESAPI_BUILD_DEBUG 1
##else
## define ESAPI_BUILD_RELEASE 1
##endif

DEBUG生效时,您的代码应接收完整的调试检测,包括断言的全部作用。

断言

断言将帮助您通过快速轻松地找到首次失败点来创建自调试代码。断言应在您的程序中广泛使用,包括参数验证、返回值检查和程序状态。assert将通过其生命周期默默地守护您的代码。它将始终存在,即使在不调试模块的特定组件时也是如此。如果您有彻底的代码覆盖率,您将花费更少的时间调试,更多的时间进行开发,因为程序将自行调试。

要有效使用断言,您应该断言所有内容。这包括进入函数时的参数、函数调用返回的值以及任何程序状态。在您放置if语句进行验证或检查的任何地方,都应该有一个断言。在您放置断言进行验证或检查的任何地方,都应该有一个if语句。它们是相辅相成的。

如果您仍在使用printf,那么您有机会进行改进。在您编写printfNSLog语句所需的时间内,您可能已经编写了一个assert。与通常在不再需要时被移除的printfNSLog不同,assert将永久保持活动状态。记住,这一切都是为了快速找到首次失败点,以便您可以将时间花在其他事情上。

使用断言有一个问题——Posix规定,如果NDEBUG**未**定义,assert应调用abort()。调试时,NDEBUG永远不会被定义,因为您需要“程序诊断”(Posix描述中的引用)。这种行为使得assert及其伴随的abort()在开发中完全无用。由于标准C/C++行为导致“程序诊断”调用abort()的结果是弃用——开发人员根本不使用它们。这对开发社区来说非常糟糕,因为自调试程序可以帮助消除如此多的稳定性问题。

由于自调试程序如此强大,您将不得不提供您自己的断言和具有改进行为的信号处理程序。您的断言将自动中止行为替换为自动调试行为。自动调试功能将确保在检测到问题时调试器中断,您将快速轻松地找到首次失败点。

ESAPI C++提供其自己的断言,其行为如上所述。在下面的代码中,ASSERT生效时引发SIGTRAP,否则在其他情况下评估为void

// A debug assert which should be sprinkled liberally.
// This assert fires and then continues rather
// than calling abort(). Useful when examining negative
// test cases from the command-line.
##if (defined(ESAPI_BUILD_DEBUG) && defined(ESAPI_OS_STARNIX))
##  define ESAPI_ASSERT1(exp) {                                    \
    if(!(exp)) {                                                  \
        std::ostringstream oss;                                     \
        oss << "Assertion failed: " << (char*)(__FILE__) << "("     \
            << (int)__LINE__ << "): " << (char*)(__func__)          \
            << std::endl;                                           \
        std::cerr << oss.str();                                     \
        raise(SIGTRAP);                                             \
    }                                                             \
    }
##  define ESAPI_ASSERT2(exp, msg) {                               \
    if(!(exp)) {                                                  \
        std::ostringstream oss;                                     \
        oss << "Assertion failed: " << (char*)(__FILE__) << "("     \
            << (int)__LINE__ << "): " << (char*)(__func__)          \
            << ": \"" << (msg) << "\"" << std::endl;                \
        std::cerr << oss.str();                                     \
        raise(SIGTRAP);                                             \
    }                                                             \
    }
##elif (defined(ESAPI_BUILD_DEBUG) && defined(ESAPI_OS_WINDOWS))
##  define ESAPI_ASSERT1(exp)      assert(exp)
##  define ESAPI_ASSERT2(exp, msg) assert(exp)
##else
##  define ESAPI_ASSERT1(exp)      ((void)(exp))
##  define ESAPI_ASSERT2(exp, msg) ((void)(exp))
##endif

##if !defined(ASSERT)
##  define ASSERT(exp)     ESAPI_ASSERT1(exp)
##endif

在程序启动时,如果其他组件未提供SIGTRAP处理程序,将安装一个。

    struct DebugTrapHandler
    {
      DebugTrapHandler()
      {
        struct sigaction new_handler, old_handler;

        do
          {
            int ret = 0;

            ret = sigaction (SIGTRAP, NULL, &old_handler);
            if (ret != 0) break; // Failed

            // Don't step on another's handler
            if (old_handler.sa_handler != NULL) break;

            new_handler.sa_handler = &DebugTrapHandler::NullHandler;
            new_handler.sa_flags = 0;

            ret = sigemptyset (&new_handler.sa_mask);
            if (ret != 0) break; // Failed

            ret = sigaction (SIGTRAP, &new_handler, NULL);
            if (ret != 0) break; // Failed

          } while(0);
      }

      static void NullHandler(int /*unused*/) { }

    };

    // We specify a relatively low priority, to make sure we run before other CTORs
    // http://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes
    static const DebugTrapHandler g_dummyHandler __attribute__ ((init_priority (110)));

在Windows平台上,您将调用_set_invalid_parameter_handler(并可能调用set_unexpectedset_terminate)来安装新的处理程序。

运行生产代码的活动主机应始终定义NDEBUG(即发布配置),这意味着它们不进行断言或自动中止。自动中止是不可接受的行为,任何要求此行为的人都在完全滥用“程序诊断”的功能。如果程序想要核心转储,那么它应该创建转储而不是崩溃。

有关有效断言的更多阅读,请参阅John Robbin的著作,例如调试应用程序。John是Windows圈子里传奇的Bug杀手,他将向您展示如何完成几乎所有事情,从调试简单程序到多线程程序中的Bug清除。

附加宏

附加宏包括为正确安全地集成所需的任何宏。它包括将程序与平台(例如MFC或Cocoa/CocoaTouch)和库(例如Crypto++或OpenSSL)集成。这可能是一个挑战,因为您必须精通您的平台以及所有包含的库和框架。下面的列表说明了集成时您将需要的详细程度。

尽管Boost不在列表中,但它似乎缺乏建议、附加调试诊断和强化指南。有关详细信息,请参阅BOOST强化指南(预处理器宏)。此外,Tim Day指出[boost.build]我们是否不应为所有msvc工具集默认定义_SECURE_SCL=0以进行近期与强化(或缺乏)相关的讨论。

除了您应该定义的宏之外,定义某些宏和取消定义其他宏应触发与安全相关的缺陷。例如,Linux上的-U_FORTIFY_SOURCES和Windows上的_CRT_SECURE_NO_WARNINGS=1_SCL_SECURE_NO_WARNINGS_ATL_SECURE_NO_WARNINGSSTRSAFE_NO_DEPRECATE

AdditionalPlatformLibraryMacrosTable

a) 使用发行版中的预编译库(如Boost)时,请小心_GLIBCXX_DEBUG。存在ABI不兼容性,结果很可能是崩溃。您必须使用_GLIBCXX_DEBUG编译Boost或省略_GLIBCXX_DEBUG

b) 有关详细信息,请参阅libstdc++手册的第5章“诊断”

c) SQLite安全删除在销毁时将内存归零。按要求定义,并在美国联邦政府中始终定义,因为FIPS 140-2一级要求归零。

d) N默认是0644,这意味着每个人都有一些访问权限。

e) 强制将临时表存入内存(没有未加密数据到磁盘)。

编译器和链接器

编译器编写者在编译过程中通过代码分析提供了丰富的警告集。GCC和Visual Studio都具有静态分析功能,有助于在开发过程早期发现错误。GCC和Visual Studio的内置静态分析功能通常足以确保正确的API使用并捕获诸如使用未初始化变量或比较负有符号整型和正无符号整型等错误。

作为一个具体示例(对于不熟悉C/C++类型提升规则的人),如果有符号整数被提升为无符号整数然后进行比较,将发出警告,因为提升后的副作用是-1 > 1!GCC和Visual Studio目前无法捕获例如SQL注入和其他污染数据的使用。为此,您将需要一个旨在执行数据流分析或污点分析的工具。

开发社区中的一些人抵制静态分析或反驳其结果。例如,当静态分析警告Linux内核的sys_prctl正在将无符号值与小于零的值进行比较时,Jesper Juhl提供了一个补丁来清理代码。Linus Torvalds大声叫道:“不,你不要这样做……GCC就是垃圾”(指的是用警告编译)。有关完整讨论,请参阅来自Linux内核邮件列表的[PATCH] Don't compare unsigned variable for <0 in sys_prctl()

以下部分将详细说明三个平台的步骤。第一个是提供GCC和Binutils的典型GNU/Linux发行版,第二个是Clang和Xcode,第三个是现代Windows平台。

发行版强化

在讨论GCC和Binutils之前,最好指出下面讨论的一些防御措施已经存在于发行版中。不幸的是,这是委员会设计,所以存在的内容通常只是可用内容的轻微变体(这样,每个人都轻微冒犯)。对于那些纯粹担心性能的人,您可能会惊讶地发现,您甚至在不知情的情况下已经采取了小小的性能提示。

Linux和BSD发行版通常通过GCC Spec文件在没有干预的情况下应用一些强化措施。如果您使用的是Debian、Ubuntu、Linux Mint及其家族,请参阅Debian强化。对于Red Hat和Fedora系统,请参阅F16中即将推出新的强化构建支持。Gentoo用户应访问Hardened Gentoo

您可以通过gcc -dumpspecs查看发行版使用的设置。从下面的Linux Mint 12中可以看出,默认使用了-fstack-protector(但没有-fstack-protector-all)。

$ gcc -dumpspecs
…
*link_ssp: %{fstack-protector:}

*ssp_default: %{!fno-stack-protector:%{!fstack-protector-all:
              %{!ffreestanding:%{!nostdlib:-fstack-protector}}}}

上面的“SSP”代表堆栈溢出保护器(Stack Smashing Protector)。SSP是Hiroaki Etoh在IBM Pro Police堆栈检测器上工作的重新实现。有关详细信息,请参阅Hiroaki Etoh的补丁gcc堆栈溢出保护器和IBM的用于保护应用程序免受堆栈溢出攻击的GCC扩展

GCC/Binutils

GCC(编译器集合)和Binutils(汇编器、链接器和其他工具)是独立的工程,它们协同工作以生成最终可执行文件。编译器和链接器都提供了选项,可帮助您编写更安全的代码。链接器将生成利用内核和PaX提供的平台安全功能(例如不可执行堆栈和堆(NX)以及位置无关可执行文件(PIE))的代码。

下表提供了一组用于构建程序的编译器选项。静态分析警告有助于早期发现错误,而链接器选项则在运行时强化可执行文件。在下表中,“GCC”应大致理解为“非旧版发行版”。尽管GCC团队认为4.2是旧版,但由于2007年左右GPL许可的变更,您仍然会在Apple和BSD平台上遇到它。有关使用详情,请参阅GCC选项摘要请求或抑制警告的选项Binutils (LD) 命令行选项

值得特别一提的是-fno-strict-overflow-fwrapvₐ。这些标志确保编译器不会删除导致溢出或包装的语句。如果您的程序只有在使用这些标志时才能正确运行,则很可能违反了C/C++关于溢出的规则且是非法的。如果程序由于溢出或包装检查而非法,您应该考虑在C中使用safe-iop或在C++中使用David LeBlanc的SafeInt

对于使用强化设置编译和链接的项目,可以通过Tobias Klein编写的Checksec工具验证这些设置。checksec.sh脚本旨在测试应用程序正在使用的标准Linux操作系统和PaX安全功能。有关详细信息,请参阅Trapkit网页。

GCC C警告选项表

GCCCWarningOptionsTable

a) 与Clang和-Weverything不同,GCC不提供真正启用所有警告的开关。b) -fstack-protector保护带有高风险对象(如C字符串)的函数,而-fstack-protector-all保护所有对象。

可以使用的其他C++警告包括表3中的内容。有关其他选项和详细信息,请参阅GCC控制C++方言的选项

GCC C++警告选项表

GCCCPPWarningOptionsTable

《Effective C++(第二版)》.

以及其他有用的Objective C警告包括以下内容。有关其他选项和详细信息,请参阅控制Objective-C和Objective-C++方言的选项

GCC Objective C警告选项表

GCCObjectiveCWarningOptionsTable

使用激进的警告会产生虚假噪音。噪音是一种权衡——您可以通过筛选一些无用信息来了解潜在问题。以下内容将有助于减少警告系统中的虚假噪音

  • -Wno-unused-parameter (GCC)
  • -Wno-type-limits (GCC 4.3)
  • -Wno-tautological-compare (Clang)

最后,下面显示了一个简单的基于版本的Makefile示例。这与Autotools生成的基于功能的Makefile(它将测试特定功能,然后定义符号或配置模板文件)不同。并非所有平台都使用所有选项和标志。为了解决这个问题,您可以采取两种策略之一。首先,您可以通过满足最低共同点来发布具有弱化姿态的版本;其次,您可以发布所有功能都已启用的版本。在后一种情况下,那些没有可用功能的用户将编辑Makefile以适应他们的安装。

CXX=g++
EGREP = egrep
…

GCC_COMPILER = $(shell $(CXX) -v 2>&1 | $(EGREP) -i -c '^gcc version')
GCC41_OR_LATER = $(shell $(CXX) -v 2>&1 | $(EGREP) -i -c '^gcc version (4\.[1-9]|[5-9])')GNU_LD210_OR_LATER = $(shell $(LD) -v 2>&1 | $(EGREP) -i -c '^gnu ld .* (2\.1[0-9]|2\.[2-9])')
GNU_LD214_OR_LATER = $(shell $(LD) -v 2>&1 | $(EGREP) -i -c '^gnu ld .* (2\.1[4-9]|2\.[2-9])')
…

ifeq ($(GCC_COMPILER),1)
    MY_CC_FLAGS += -Wall -Wextra -Wconversion
    MY_CC_FLAGS += -Wformat=2 -Wformat-security
    MY_CC_FLAGS += -Wno-unused-parameter
endif

ifeq ($(GCC41_OR_LATER),1)
    MY_CC_FLAGS += -fstack-protector-all
endif

ifeq ($(GCC42_OR_LATER),1)
    MY_CC_FLAGS += -Wstrict-overflow
endif

ifeq ($(GCC43_OR_LATER),1)
    MY_CC_FLAGS += -Wtrampolines
endif

ifeq ($(GNU_LD210_OR_LATER),1)
    MY_LD_FLAGS += -z,nodlopen -z,nodump
endif

ifeq ($(GNU_LD214_OR_LATER),1)
    MY_LD_FLAGS += -z,noexecstack -z,noexecheap
endif

ifeq ($(GNU_LD215_OR_LATER),1)
    MY_LD_FLAGS += -z,relro -z,now
endif

ifeq ($(GNU_LD216_OR_LATER),1)
    MY_CC_FLAGS += -fPIE
    MY_LD_FLAGS += -pie
endif

## Use 'override' to honor the user's command line
override CFLAGS := $(MY_CC_FLAGS) $(CFLAGS)
override CXXFLAGS := $(MY_CC_FLAGS) $(CXXFLAGS)
override LDFLAGS := $(MY_LD_FLAGS) $(LDFLAGS)

Clang/Xcode

ClangLLVM自苹果在2007年因Tivoization(导致GPLv3)失去其GPL编译器以来,一直被积极开发。自那时起,许多开发人员和Google加入了这项工作。虽然Clang将消耗大部分(所有?)GCC/Binutil标志和开关,但该项目支持许多自己的选项,包括一个静态分析器。此外,Clang相对容易构建附加诊断功能,例如John Regher博士和Peng Li的整数溢出检查器(IOC)

IOC非常有用,并且已经在许多项目中发现了错误,包括Linux内核(include/linux/bitops.h,仍未修复)、SQLite、PHP、Firefox(许多仍未修复)、LLVM和Python。未来版本的Clang(Clang 3.3及更高版本)将允许您直接通过-fsanitize=integer-fsanitize=shift启用这些检查。

Clang选项可以在Clang编译器用户手册中找到。Clang确实包含一个开启所有警告的选项——-Weverything。谨慎使用它,但要经常使用,因为你会得到很多噪音和遗漏的问题。例如,为生产构建添加-Weverything,并将非虚假问题作为质量关卡。在Xcode下,只需将-Weverything添加到CFLAGSCXXFLAGS

除了编译器警告之外,还可以执行静态分析和额外的安全检查。有关Clang静态分析功能的阅读资料可以在Clang静态分析器中找到。下面的图1显示了Xcode使用的一些安全检查。

XCode1

Visual Studio

Visual Studio提供了一个便捷的集成开发环境(IDE),用于管理解决方案及其设置。“Visual Studio选项”一节讨论了应与Visual Studio一起使用的选项,“项目属性”一节演示了如何将这些选项纳入解决方案的项目中。

下表列出了在Visual Studio下应使用的编译器和链接器开关。有关详细讨论,请参阅Howard和LeBlanc的《编写安全代码》(微软出版社)或Michael Howard在《安全简报》中撰写的《使用Visual C++防御保护您的代码》。在下表中,“Visual Studio”指的是几乎所有版本的开发环境,包括Visual Studio 5.0和6.0。

对于使用强化设置编译和链接的项目,可以通过BinScope验证这些设置。BinScope是微软提供的一款验证工具,它分析二进制文件,以确保它们在构建时符合微软安全开发生命周期(SDLC)的要求和建议。有关详细信息,请参阅BinScope二进制分析器下载页面。

VStudioWarningOptionsTable

a) 有关Jon Sturgeon对该开关的讨论,请参阅Visual C++中默认关闭的编译器警告

a) 使用/GS时,有多种情况会影响安全cookie的包含。例如,如果堆栈帧中没有缓冲区、优化被禁用,或者函数被声明为裸函数或包含内联汇编,则不使用保护。

b) #pragma strict_gs_check(on)应谨慎使用,但在高风险情况下建议使用,例如当源文件解析来自互联网的输入时。

警告抑制

从上表中可以看出,已经启用了大量警告,以帮助检测可能的编程错误。潜在的错误通过编译器检测,该编译器在代码分析阶段携带大量上下文信息。有时,您会收到虚假警告,因为编译器没有那么智能。这是可以理解的,甚至是好事(你会希望因为一个程序能够自己编写程序而失业吗?)。有时您将不得不学习如何使用编译器的警告系统来抑制警告。请注意未提及的是:关闭警告。

抑制警告可以平息编译器的虚假噪音,从而让您关注重要问题(您正在去伪存真)。本节将提供一些提示并指出一些潜在的雷区。首先是未使用的参数(例如,argcargv)。抑制未使用参数警告对于C++和接口编程尤其有用,因为这些场景下参数通常未使用。对于此警告,只需定义一个“UNUSED”宏并包装参数

##define UNUSED_PARAMETER(x) ((void)x)


int main(int argc, char* argv[])
{
    UNUSED_PARAMETER(argc);
    UNUSED_PARAMETER(argv);
    
}

潜在的雷区在于“比较无符号和有符号”值附近,而-Wconversion会为您捕获它。这是因为C/C++类型提升规则规定,有符号值将被提升为无符号值,然后进行比较。这意味着提升后-1 > 1!要修复此问题,您不能盲目地进行类型转换——您必须首先对值进行范围测试

int x = GetX();
unsigned int y = GetY();

ASSERT(x >= 0);
if(!(x >= 0))
    throw runtime_error("WTF??? X is negative.");

if(static_cast<unsigned int>(x) > y)
    cout << "x is greater than y" << endl;
else
    cout << "x is not greater than y" << endl;

请注意,上面的代码将自行调试——您不需要设置断点来查看x是否存在问题。只需运行程序,等待它告诉您问题所在。如果存在问题,程序将中断调试器(更重要的是,不会像Posix指定的那样调用无用的abort())。这比不再需要时被删除或污染输出的printf语句要好得多。

您将遇到的另一个转换问题是类型之间的转换,并且-Wconversion也会为您捕获它。以下内容将始终有机会失败,并且应该像圣诞树一样亮起来

struct sockaddr_in addr;


addr.sin_port = htons(atoi(argv[2]));

以下内容可能更适合您。请注意,不使用atoi及其相关函数,因为它们可能静默失败。此外,代码经过检测,因此您无需浪费大量时间调试潜在问题

const char* cstr = GetPortString();

ASSERT(cstr != NULL);
if(!(cstr != NULL))
    throw runtime_error("WTF??? Port string is not valid.");

istringstream iss(cstr);
long long t = 0;
iss >> t;

ASSERT(!(iss.fail()));
if(iss.fail())
    throw runtime_error("WTF??? Failed to read port.");

// Should this be a port above the reserved range ([0-1024] on Unix)?
ASSERT(t > 0);
if(!(t > 0))
    throw runtime_error("WTF??? Port is too small");

ASSERT(t < static_cast<long long>(numeric_limits<unsigned int>::max()));
if(!(t < static_cast<long long>(numeric_limits<unsigned int>::max())))
    throw runtime_error("WTF??? Port is too large");

// OK to use port
unsigned short port = static_cast<unsigned short>(t);

再次请注意,上面的代码将自行调试——您不需要设置断点来查看port是否存在问题。这段代码将继续检查条件,即使在进行检测多年后(假设在项目早期编写读取配置文件的代码)也是如此。无需像printf那样移除ASSERT,因为它们是默默的守护者。

另一个有用的抑制技巧是避免忽略返回值。它不仅有助于抑制警告,而且是正确代码所必需的。例如,snprintf会通过其返回值提醒您截断。您不应通过忽略警告或转换为void来使其成为静默截断

char path[PATH_MAX];


int ret = snprintf(path, sizeof(path), "%s/%s", GetDirectory(), GetObjectName());
ASSERT(ret != -1);
ASSERT(!(ret >= sizeof(path)));

if(ret == -1 || ret >= sizeof(path))
    throw runtime_error("WTF??? Unable to build full object name");

// OK to use path

这个问题普遍存在,不仅仅是枯燥的用户空间程序。提供高完整性代码的项目,例如SELinux,也会遭受静默截断。以下内容来自一个已批准的SELinux补丁,尽管有评论指出其compute_create.c文件中的security_compute_create_name函数存在静默截断问题。

12  int security_compute_create_raw(security_context_t scon,
13                                  security_context_t tcon,
14                                  security_class_t   tclass,
15                                  security_context_t * newcon)
16  {
17    char path[PATH_MAX];
18    char *buf;
19    size_t size;
20    int fd, ret;
21
22    if (!selinux_mnt) {
23      errno = ENOENT;
24      return -1;
25    }
26
27    snprintf(path, sizeof path, "%s/create", selinux_mnt);
28    fd = open(path, O_RDWR);

与之前的示例不同,上述代码不会自行调试,您必须设置断点并跟踪调用以确定首次失败点。(而且上述代码盲目执行open,赌截断的文件不存在或不在攻击者的控制之下。)

运行时

前几节重点讨论了如何为项目成功进行设置。本节将检查更多关于运行增强诊断和防御的提示。并非所有平台都生而平等——GNU/Linux在编译和静态链接后很难甚至不可能向程序添加强化,而Windows允许通过下载进行构建后强化。记住,目标是快速找到首次失败点,从而提高代码的可靠性和安全性。

Xcode

Xcode提供额外的代码诊断功能,有助于发现内存错误和对象使用问题。方案可以通过“产品”菜单项、“方案”子菜单项,然后是“编辑”来管理。在编辑器中,导航到“诊断”选项卡。在下图中,调试周期中启用了四个附加工具:Scribble guards、Edge guards、Malloc guards和Zombies。

XCode2

使用某些保护措施时有一个注意事项:苹果只为模拟器提供这些功能,而不为设备提供。过去,这些保护措施对设备和模拟器都可用。

Windows

Visual Studio提供了多种调试辅助工具,可在开发期间使用。这些辅助工具称为托管调试助手(MDAs)。您可以在“调试”菜单,然后是“异常”子菜单中找到MDAs。MDAs允许您调整调试体验,例如,过滤调试器应捕获的异常。有关详细信息,请参阅Stephen Toub的让CLR通过托管调试助手为您查找错误

Windows1

最后,对于运行时强化,微软提供了**Windows Defender Exploit Guard**和**Process Mitigation Management Tool**。
Windows Defender Exploit Guard取代了EMET,并提供高级漏洞防护功能。

此外,Process Mitigation Management ToolProcessMitigations模块)允许管理员通过PowerShell和组策略配置漏洞缓解策略。

Windows2