https://en.wikipedia.org/wiki/Buffer_overflow_protection#Clang.2FLLVM
来自Wikipedia,免费的百科全书
缓冲溢出保护是在软件开发期间,通过检测栈上分配变量的缓冲溢出,并防止它们导致程序行为异常,或成为严重安全漏洞,来增加可执行程序安全性的各种技术。栈缓冲溢出发生在程序写入该程序调用栈上预期数据结构之外的一个内存地址时,这个预期数据结构通常是一个固定长度的缓冲。当程序向一个栈上缓冲写入的数据超过该缓冲实际分配的内存,栈缓冲溢出故障就发生了。这几乎总是导致栈上临近数据的毁坏,这可以引致程序崩溃,不正确的操作,或安全问题。
通常,缓冲溢出保护修改栈分配数据的组织,包括入一个探测值(canary value),在被栈缓冲溢出破坏时,指示内存中它之前的数据已经溢出。通过校验探测值,可以终止受影响程序的执行,防止它行为异常或使攻击者获取控制权。其他缓冲溢出保护技术包括,边界检查(bounds checking),它检查内存每个分配块的访问,使得它们不会超出实际分配的空间;标记(tagging),它确保为保存数据分配的内存不会包含可执行代码。
相比溢出一个堆上分配的缓冲,溢出栈上分配的一个缓冲更有可能影响程序的执行,因为栈包含了所有活动函数调用的返回地址。不过,类似实现细节的保护也存在于基于堆的溢出。
缓冲溢出保护有几个实现,包括GCC,LLVM,MicrosoftVisual Studio,以及其他编译器。
概览 栈缓冲溢出发生在程序写入该程序调用栈上预期数据结构之外的一个内存地址时,这个预期数据结构通常是一个固定长度的缓冲。当程序向一个栈上缓冲写入的数据超过该缓冲实际分配的内存,栈缓冲溢出故障就发生了。这几乎总是导致栈上临近数据的毁坏,在溢出是由错误触发的情形下,通常将导致程序崩溃或操作不正确。栈缓冲溢出是更宽泛的称为缓冲溢出(或缓冲侵占)的编程失误的一种类型。相比溢出一个堆上分配的缓冲,溢出栈上分配的一个缓冲更有可能使程序跑飞,因为栈包含了所有活动函数调用的返回地址 [1] 。 作为称为栈粉碎(stack smashing)的攻击的一部分,栈缓冲溢出可以被精心达成。如果受影响的程序以特权运行,或者它接受来自不受信网络主机的数据(比如一个公共web服务器),那么这个缺陷是一个允许攻击者向正在运行程序注入可执行代码,并控制该进程的潜在安全漏洞。这是攻击者获取计算机非授权访问的最古老且更可靠的方法之一 [2] 。 通常,缓冲溢出保护修改函数调用栈帧中数据的组织来包含一个“检测”值,它在被破坏时,指示内存中它之前的缓冲已经溢出了。这提供了防止一整类攻击的好处。根据某些研究者 [3] ,这些技术的性能影响是可忽略的。栈粉碎保护不能防止某种形式的攻击。例如,它不能保护堆上的缓冲溢出。没有健全的方法改变结构体数据的布局;结构体被期望在模块间是相同的,特别是共享库。跟在一个缓冲后的一个结构体中的数据不可能使用检测值继续保护;因此,程序员必须在如何组织变量以及使用结构体方面非常小心。
检测值检测值或检测字是放置在栈上一个缓冲以及控制数据之间监控缓冲溢出的已知值。在缓存溢出时,第一个被破坏的数据通常是检测值,因此检测值数据的校验失败是溢出的一个警告,然后这警告可以得到处理,比如使被破坏数据无效。
这个术语参考了在煤矿中使用金丝雀的历史实践,因为金丝雀比矿工更容易受毒气的影响,因此这提供了一个生物警告系统。检测值也被称作cookie,它意在唤起被破坏值的一个“坏的曲奇饼”形象。
有三种检测值在使用:terminator,random,以及randomXOR。当前版本的StackGuard支持所有三者,而ProPolice支持terminator与random检测值。
终结符检测值
终结符检测值基于大多数缓冲溢出攻击以某些在字符串终结符结束的字符串操作为基础的观察。对这个观察的响应是由null终结符,CR,LF以及-1构建的检测值。结果,攻击者必须在写入返回地址前写入一个空字符以避免改变检测值。这阻止了攻击者使用strcpy及其他依据拷贝一个空字符返回的方法,与此同时不期望的结果是检测值是已知的。即使使用保护,攻击者有可能使用已知值改写这个检测值,使用不匹配的值改写控制信息,因此通过检测值检查代码,而后者在处理器的调用返回指令前将得到执行。
随机检测值
随机检测值是随机生成的,通常来自一个收集熵的守护进程,防止攻击者知道它们的值。通常,读检测值没有逻辑上的可能性或可行性;检测值是一个仅对需要知道它的对象已知的安全值――在这里是缓冲溢出保护代码。
正常地,一个随机检测值在程序初始化时生成,并保存在一个全局变量中。这个变量通常由未映射内存页填充,因此任何利用缺陷从RAM读取的伎俩都会导致段错误,从而终结程序。如果攻击者知道它在哪里,或者可以让程序从栈读取。这个检测值还是可能读到的。
随机XOR检测值
随机XOR检测值是与所有或部分控制数据XOR过的随机检测值。这样,一旦检测值或控制数据被破坏了,检测值就不对了。
随机XOR检测值具有与随机检测值一样的弱点,除了“从栈读”得到检测值的方法更复杂一些。 攻击者必须获得检测值,算法,以及控制数据来生成原始的检测值,重新编码为要欺骗保护的检测值。
另外,随机XOR检测值可以保护某种攻击,它涉及溢出一个结构体中的缓冲到一个指针,修改这个指针指向一块控制数据。因为XOR编码,如果控制数据或返回值改变了,检测值将是错的。因为这个指针,可以改变控制数据或返回值,而无需溢出覆盖检测值。
虽然这些检测值保护控制数据免遭受攻击指针的修改,但它们自己不能保护其他数据或指针。这里函数指针特别成问题,因为它们可以被溢出改写,在调用时将执行shellcode。
边界检查 边界检查是一个基于编译器的技术,它对每个分配内存块添加了运行时边界信息,在运行时对照检查所有的指针。对于C及C++,边界检查可以在指针计算时刻 [4] 或解引用时刻执行 [5] [6] [7] 。 这个方法的实现要么使用一个描述了每个分配内存块的中央数据块456,要么使用包含了指针以及描述它们指向区域的额外数据的胖指针 [8] 。 标记 标记 [9] 是一个基于编译器或基于硬件(要求一个 tagged archtecture )的技术,标记内存中数据片段的类型,主要用于类型检查。通过将内存的某些区域标记为不可执行,它有效地防止分配来保存数据的内存包含可执行代码。同样,内存的某些区域可以标记为不可分配,防止缓冲溢出。 得到操作系统合适的支持,标记还可以用于检测缓冲溢出 [10] 。一个例子是Intel,AMD以及ARM处理器支持的NX比特硬件特性。 实现GNU编译器套件(GCC)
栈粉碎保护在1997年首先由StackGuard实现,并在1998年的 USENIX安全研讨会 上发布 [11] 。StackGuard作为一组补丁引入GCC-2.7的Intelx86后端。从1998到2003年,linux的发布 Immunix 维护了StackGuard,并扩展了终结符、随机及随机XOR检测值的实现。在GCC2003 SummitProceedings 里, StackGuard被建议包括进GCC-3.x [12] ,不过这从未实现。 从2001到2005年,IBM开发了称为ProPolice的 [13] ,栈粉碎保护的GCC补丁。它通过在栈帧内的局部指针及函数实参后放置缓冲改进StackGuard的思想。这有助于避免指针毁坏,防止访问任意内存位置。 RedHat工程师确定了ProPolice的问题,在2005年为GCC-4.1重新实现了栈粉碎保护 [14] [15] 。这个工作引入了-fstack-protector选项,它仅保护某些易受攻击的函数,以及-fstack-protector-all选项,它保护所有的函数,不管它们需不需要 [16] 。 在2012年,Google的工程师实现了-fstack-protector-strong选项,以达到安全与性能更好的平衡 [17] 。这个选项比-fstack-protector保护更多类型的易受攻击函数,但不是每个函数,提供了比-fstack-protector-all更好的性能。这个选项在4.9版本后的GCC中可用 [18] 。 自Fedora Core 5起,所有的Fedora包都使用-fstack-protector编译,而自Fedora20起使用-fstack-protector-strong [19] [20] 。自6.10起,Ubuntu的大部分包使用-fstack-protector编译 [21] 。自2011年起,每个ArchLinux包使用-fstack-protector编译 [22] 。自2014年5月4日起,所有ArchLinux包使用-fstack-protector-strong编译 [23] 。在Debian中栈保护仅用于某些包 [24] ,自8.0起仅用于FreeBSD的基本系统 [25] 。栈保护在某些操作系统中已是标准,包括OpenBSD [26] ,HardenGentoo [27] ,以及DragonFlyBSD。 StackGuard与ProPolice不能保护在自动分配结构体中覆盖函数指针的溢出。ProPolice至少会重排分配使得这样的结构体分配在函数指针的前面。在PointGuard里提出了指针保护的另一个机制 [28] ,它在Microsoftwindows中可用 [29] 。Microsoft Visual Studio
Microsoft的编译器套件自2003版本起实现了通过/GS命令行选项的缓冲溢出保护,自2005版本起它默认激活 [30] 。使用/GS-禁止该保护。IBM编译器
可以通过编译器选项-qstackprotect打开栈粉碎保护 [31]Clang/LLVM
Clang支持三个缓冲溢出检测器,即 AddressSanitizer (-fsanitize=address) 6 , -fstanitize=bounds [32] ,以及 SafeCode [33] 。这些系统在性能损失,内存开销及检测缺陷类型方面各有取舍。Intel编译器
Intel 的 C 与 C++ 编译器使用类似于 GCC 以及 MicrosoftVisual Studio 提供的选项支持栈粉碎保护 [34] 。Fail-Safe C
Fail-Safe C 7 是一个开源的、内存安全的 ANSI C 编译器,它执行基于胖指针以及面向对象内存访问的边界检查 [35] 。StackGhost(基于硬件)
由 Mike Franttzen 发明, StackGhost 是对寄存器窗口溅出 / 填充例程的一个简单调整,它使得利用缓冲溢出变得困难得多。它使用 SunMicrosystems SPARC 架构的一个独特的硬件特性(延迟栈上帧内寄存器窗口溅出 / 填充)透明地检测返回指针的修改(劫持执行路径的一个常用方式),自动地保护所有的应用程序,而无需要求二进制或源代码修改。性能影响可以忽略不计,少于 1% 。所导致的 gdb 问题由 Mark Ketternis 在两年后解决,允许启动这个特性。之后, StackGhost 代码被整合(及优化)入 OpenBSD/SPARC 。
一个检测值例子X86 架构以及其他类似架构的正常缓冲分配显示在缓冲溢出项。这里,我们展示在附属到 StackGuard 时,被修改的进程。
在调用一个函数时,一个栈帧被创建。从内存末尾向内存开头构建栈帧;每个栈帧被放置在栈顶,最靠近内存的开头。因此,跑出一个栈帧数据片段的末尾改变了之前进入该栈帧的数据;跑出一个栈帧的末尾可以在之前的栈帧放入数据。通常一个栈帧看起来如下,首先是一个返回地址( RETA ),后跟其他控制信息( CTLI )。
(CTLI)(RETA)
在 C 中,函数可能包含许多不同的每调用数据结构。对调用创建的每块数据被依次放置在栈帧中,因此从内存末尾向开头排列。下面是一个假想的函数及其栈帧。
int foo () {
int a; /*integer*/
int * b; /*pointerto integer*/
char c[ 10 ]; /*character array*/ char d[ 3 ];b = & a; /*initialize b to point to location of a*/
strcpy(c,get_c()); /*get ''c'' from somewhere, write it to ''c''*/
* b = 5 ; /*the data at the point in memory ''b''indicates is set to 5*/
strcpy(d,get_d());
return * b; /*read from ''b'' and pass it to thecaller*/
}
(d..)(c.........)(b...)(a...)(CTLI)(RETA)
在这个假想的情形里,如果向数组 c 写入超过 10 个字节,或者向字符数组 d 写入超过 3 个字节,超出部分将溢出到整数指针 b ,然后进入整数 a ,然后进入控制信息,最后返回地址。通过溢出 b ,指针可以引用内存的任何位置,导致从任意地址的一个读操作。通过溢出 RETA 。可以使得函数执行其他代码(在它尝试返回时),可以是现存的函数( ret2libc ),或者在溢出时写入栈的代码。
简而言之, c 与 d 糟糕的处理,比如上面没有边界的 strcpy 调用,可能允许攻击者通过直接影响分配给 c 与 d 的值,控制一个程序。缓冲溢出保护的目的是以侵入性尽可能小的方式检测这个问题。这通过消除处在危险境地中的对象,并在缓冲后放置某种绊网( tripwire ),或检测值。
缓冲溢出保护实现为编译器的一个改进。比如,保护改变栈帧上数据结构的可能的。这就是比如 ProPolice 系统的情形。上面函数的自动变量被更安全地重排:数组 c 与 d 首先在栈帧上分配,整数 a 与整数指针 b 在内存中在它们之前。因此栈帧变成:
(b...)(a...)(d..)(c.........)(CTLI)(RETA)
因为不破坏生成的代码,是不可能移动 CTLI 或 RETA 。另一个策略得到应用,一个称为“检测值( CNRY )”的额外信息片段,被放置在栈帧中缓冲之后。在缓冲溢出发生时,这个检测值被改变。因此,要有效地攻击这个程序,攻击者必然留下他攻击的迹象。这样栈帧是:
(b...)(a...)(d..)(c.........)(CNRY)(CTLI)(RETA)
在每个函数的末尾有一条指令从由 RETA 指示的内存地址继续执行。在这条指令执行之前, CNRY 的一个检查确保它没有被改变。如果 CNRY 的值不能通过检测,程序的执行立即终止。基本上