苹果安全编码引导-Avoiding Buffer Overflows and Underflows

972 阅读28分钟

避免缓冲区溢出和下溢 Avoiding Buffer Overflows and Underflows

前言

栈和堆上的缓冲区溢出是C,Objective-C和C++代码中安全漏洞的主要来源。本章讨论了避免缓冲区溢出和下溢问题的编码实践,列出了可用于检测缓冲区溢出的工具,并提供了说明安全代码的示例。

每当您的程序请求输入时(无论是来自用户,来自文件,通过网络还是通过其他某种方式),都有可能接收不适当的数据。例如,输入数据可能比内存中保留的空间更长。

当输入数据的长度超出保留空间的容量时,如果不截断输入数据,该数据将覆盖内存中的其他数据。发生这种情况时,称为缓冲区溢出。如果覆盖的内存包含了程序运行必不可少的数据,则此溢出会导致错误,该错误是间歇性的,可能很难复现。如果覆盖的数据包括要执行的其他代码的地址,并且用户故意这样做,则用户可以指向程序将执行的恶意代码。

类似地,当输入数据小于或小于保留空间时(由于错误的假设,错误的长度值或将原始数据复制为C字符串),这称为缓冲区下溢。从不正确的行为到泄漏当前栈或堆中的数据,这可能会导致许多问题。

尽管大多数编程语言都会根据存储检查输入以防止缓冲区溢出和下溢,但是C,Objective-C和C++不会。因为许多程序都链接到C库,所以即使在以“安全”语言编写的程序中,标准库中的漏洞也可能导致漏洞。因此,即使您确信自己的代码没有缓冲区溢出问题,也应该通过尽可能少的特权运行来减少漏洞。

栈溢出 Stack Overflows

在大多数操作系统中,每个应用程序都有一个栈(而多线程应用程序每个线程有一个栈)。该栈包含用于本地范围的数据的存储。

栈分为称为栈帧的单元。每个栈帧都包含特定于对特定函数的特定调用的所有数据。这些数据通常包括函数的参数,该函数内的完整局部变量集以及链接信息(即函数调用本身的地址,函数返回时将在此继续执行)。根据编译器标志的不同,它可能还包含下一个栈帧顶部的地址。栈上数据的确切内容和顺序取决于操作系统和CPU体系结构。

每次调用函数时,都会在栈顶部添加一个新的栈帧。每次函数返回时,顶部栈帧都会被删除。在执行的人格时刻,应用程序只能直接访问最顶层堆栈帧中的数据。 (指针可以解决这个问题,但是这样做通常不是一个好主意。)这种设计使递归成为可能,因为对函数的每个嵌套调用都会获得自己的局部变量和参数。

image

通常,应用程序应检查所有输入数据以确保它适合于预期的目的(例如,确保文件名具有合法长度且不包含非法字符)。不幸的是,在很多情况下,程序不会出问题是因为用户不会做任何不合理的事情。

当应用程序将该数据存储到固定大小的缓冲区中时,这将成为一个严重的问题。如果用户是恶意用户(或打开一个包含由恶意用户创建的数据的文件),则他们所提供的数据可能长于缓冲区的大小。因为该函数在栈上为该数据保留有限的空间,所以该数据将覆盖栈上的其他数据。

如图下图2所示,聪明的攻击者可以使用此技术覆盖该函数使用的返回地址,用其他代码的地址代替。然后,当函数C完成执行时,而不是返回到函数B,它跳转到攻击者的代码。

由于该应用程序执行攻击者的代码,因此攻击者的代码继承了用户的权限。如果用户以管理员身份登录,则攻击者可以完全控制计算机,从磁盘读取数据,发送电子邮件等。 (请注意,受沙盒管理的应用程序,包括iOS中的任何应用程序,以及您在macOS中采用App Sandbox的应用程序 都无法完全控制设备,尽管攻击者仍可以通过这种方式访问应用程序的数据)

image

除了攻击链接信息外,攻击者还可以通过修改栈上的本地数据和功能参数来更改程序操作。 例如,攻击者可以修改数据结构,而不是连接到所需的主机,以使您的应用程序连接到其他(恶意)主机。

堆溢出 Heap Overflows

堆用于应用程序中所有动态分配的内存。 当使用malloc,C++ new运算符或等效函数来分配内存块或实例化对象时,支持这些指针的内存将分配在堆上。

由于堆用于存储数据,但不用于存储函数和方法的返回地址值,并且由于堆上的数据在程序运行时以不明显的方式发生更改,因此可供攻击者利用堆上的缓冲区溢出漏洞并不明显。 在某种程度上,正是这种非显而易见性使堆溢出成为有吸引力的目标,与栈溢出相比,程序员花费较少的精力去担心并且修复他们。

image

通常,利用堆上的缓冲区溢出比利用栈上的溢出更具挑战性。然而许多成功的漏洞都涉及堆溢出。利用堆溢出的方法有两种:修改数据和修改对象。

攻击者可以通过覆盖关键数据来利用堆上的缓冲区溢出,从而导致程序崩溃或更改以后可以利用的值(例如,覆盖存储的用户ID以获取其他访问权限)。修改此数据称为非控制数据攻击。堆上的许多数据是由程序内部生成的,而不是从用户输入中复制的;此类数据可以在内存中相对一致的位置,具体取决于应用程序分配的方式和时间。

攻击者还可以通过覆盖指针来利用堆上的缓冲区溢出。在许多语言中,例如C++和Objective-C,分配在堆上的对象都包含函数表和数据指针。通过利用缓冲区溢出来更改此类指针,攻击者可以潜在地替换不同的数据,甚至替换类对象中的实例方法。

利用堆上的缓冲区溢出可能是一个复杂而又难以解决的问题,但是一些恶意的黑客却在这种挑战中成长。例如:

  1. 用于解码位图图像的代码堆溢出使远程攻击者可以执行任意代码。

  2. 网络服务器中的堆溢出漏洞使攻击者可以通过发送带有负“ Content-Length”标头的HTTP POST请求来执行任意代码。

字符串处理 String Handling

字符串是输入的一种常见形式。 由于许多字符串处理函数没有内置的字符串长度检查功能,因此字符串经常是可利用的缓冲区溢出的来源。 图2-4说明了三个字符串复制函数处理同一条超长字符串的不同方式。

image

如你所见,strcpy函数仅将整个字符串写入内存,并覆盖之后的所有内容。

strncpy函数将字符串截断为正确的长度,但没有终止的空字符。 读取此字符串后,它之后的内存中的所有字节(直到下一个空字符)都可能被读取为该字符串的一部分。 尽管可以安全地使用此功能,但它经常引起程序员错误,因此被视为中度不安全。 为了安全地使用strncpy,您必须在调用strncpy之后将缓冲区的最后一个字节显式清零或将缓冲区预清零,然后传入最大长度,该长度比缓冲区大小小一个字节。

只有strlcpy函数才是完全安全的,它将字符串截断为小于缓冲区大小的一个字节,并添加终止的空字符

你还可以通过使用更高级别的接口来避免字符串处理缓冲区溢出。

  1. 如果你使用的是C++,则ANSI C ++字符串类可避免缓冲区溢出,尽管它不能处理非ASCII编码(例如UTF-8)。

  2. 如果要在Objective-C中编写代码,请使用NSString类。请注意,必须将NSString对象转换为C字符串,才能传递给C例程,例如POSIX函数。

  3. 如果使用C语言编写代码,则可以使用Core Foundation中的字符串表示形式(CFString)以及CFString API中的字符串操作函数。

Core Foundation CFString与Cocoa Foundation NSString是“toll-free bridged”。这意味着Core Foundation类型可以在函数或方法调用中与其等效的Foundation对象互换。因此,在看到NSString *参数的方法中,可以传入CFStringRef类型的值,而在看到CFStringRef参数的函数中,可以传入NSString实例。这也适用于NSString的子类。

计算缓存区大小 Calculating Buffer Sizes

使用固定长度的缓冲区时,应始终使用sizeof来计算缓冲区的大小,然后确保不要将过多的数据放入缓冲区。 即使你最初为缓冲区分配了静态大小,您或将来维护代码的其他人也可能会更改缓冲区大小,但无法更改每次写入缓冲区的情况。

下表显示了两种分配字符缓冲区长度为1024字节,检查输入字符串的长度并将其复制到缓冲区的方式。

只要不更改缓冲区大小的原始声明,左侧的两个代码段都是安全的。 但是,如果在程序的更高版本中更改了缓冲区大小而不更改测试,则将导致缓冲区溢出。

右侧的两个代码段显示了此代码的安全版本。 在第一个版本中,缓冲区大小是使用在别处设置的常量设置的,并且检查使用相同的常量。 在第二个版本中,缓冲区设置为1024字节,但是检查会计算缓冲区的实际大小。 在这两案例中,更改缓冲区的原始大小不会使检查无效

下表显示了一个追加.ext后缀的方法

两种版本都使用文件的最大路径长度作为缓冲区大小。 左列中的不安全版本假定文件名不超过此限制,并在不检查字符串长度的情况下附加后缀。 右栏中的较安全版本使用strlcat函数,如果该字符串超过缓冲区的大小,该函数将截断该字符串。

重要说明:在计算缓冲区的大小和进入缓冲区的数据时,应始终使用无符号变量(例如size_t)。 由于负数存储为大正数,因此,如果使用带符号的变量,则攻击者可能会通过向程序中写入大量数字来导致错误计算缓冲区或数据的大小。

避免整型溢出和下溢 Avoiding Integer Overflows and Underflows

如果使用用户提供的数据来计算缓冲区的大小,则恶意用户有可能输入对于整型而言太大的数字,这可能导致程序崩溃和其他问题。

在二进制补码算法(大多数现代CPU用于有符号整数算法)中,负数是通过将二进制数的所有位取反并加1来表示的。最高有效位中的1表示负数。 因此,对于4字节有符号整数,0x7fffffff = 2147483647,但0x80000000 = -2147483648

int 2147483647 + 1 = - 2147483648

如果恶意用户在程序只希望使用无符号数字的情况下指定了负数,则程序可能会将其解释为非常大的数字。根据该数字的用途,您的程序可能会尝试分配该大小的缓冲区,从而导致内存分配失败,或者如果分配成功,则会导致堆溢出。例如,在流行的Web浏览器的早期版本中,将对象存储到以负大小分配的JavaScript数组中可能会覆盖内存。

在其他情况下,如果使用带符号的值来计算缓冲区大小并进行测试以确保该数据对于缓冲区而言不是太大,则足够大的数据块将显示为负大小,因此将通过缓冲区溢出。

根据缓冲区大小的计算方式,指定负数可能会导致缓冲区对于其预期用途而言过小。例如,如果您的程序希望最小缓冲区大小为1024字节并添加用户指定的数字,那么攻击者可能会通过指定一个较大的正数来使您分配小于最小大小的缓冲区,如下所示:

1024 + 4294966784 = 512
0x400 + 0xFFFFFE00 = 0x200

同样,任何溢出超过整数变量的长度(有符号或无符号)的位都将被丢弃。 例如,当以32位整数存储时,2 ** 32 ==0。因为拥有大小为0的缓冲区不是非法的,并且因为malloc(0)返回指向小块的指针,所以 如果攻击者指定一个导致您的缓冲区大小计算为2 ** 32的倍数的值,则代码可能没有错误地运行。 换句话说,对于n和m的任何值,其中(n * m)mod 2 ** 32 == 0,分配大小为n * m的缓冲区会导致指向一些非常小的缓冲区( 取决于架构)大小。 在这种情况下,可以确保缓冲区溢出。

为避免此类问题,在执行缓冲区数学运算时,应始终包括范围检查,以确保不会发生整数溢出。

执行这些测试时,常见的错误是检查可能溢出的乘法或其他运算的结果:

size_t bytes = n * m;
if (bytes < n || bytes < m) { /* BAD BAD BAD */
 ... /* allocate “bytes” space */
}</pre>

不幸的是,如果m和n带符号,则C语言规范允许编译器优化此类测试。 即使没有符号,在某些情况下测试仍会失败。 例如,在64位计算机上,如果m和n都声明为size_t,并且都设置为0x180000000,则将它们相乘的结果为0x24000000000000000,但是字节将以模2 ** 64或0x4000000000000000的形式包含该结果。 尽管确实发生了溢出,但这仍通过了测试(结果大于任一输入)。

相反,测试乘法期间整数溢出的正确方法是在乘法之前进行测试。 特别是,将最大允许结果除以乘数,然后将结果与被乘数进行比较,反之亦然。 如果结果小于被乘数,则这两个值的乘积将导致整数溢出。 不过,正确地做到这一点可能很棘手。 例如,选择错误的最大允许整数常量(例如SIZE_MAX或INT_MAX?)会产生错误的结果。

因此,对未知输入执行乘法的最安全方法是使用clang检查的算法内置函数。 例如:

size_t bytes;
if (__builtin_umull_overflow(m, n, &bytes)) {
 /* Overflow occured.  Handle appropriately. */
} else {
 /* Allocate "bytes" space. */
}</pre>

在这种情况下,__ builtin_umull_overflow将执行无符号乘法(根据根据给定的m和n),并将(可能溢出的)结果存储为字节,但还返回一个布尔值,指示由于该操作而发生溢出。

使用macOS 10.12或iOS 10 SDK或更高版本进行构建时,可以使用内置包装器(在os / overflow.h头文件中定义)来增强此代码:

#include <os/overflow.h>

if (os_mul_overflow(m, n, &bytes)) {
 /* Overflow occured.  Handle appropriately. */
} else {
 /* Allocate "bytes" space. */
}</pre>

os_mul_overflow宏(如其兄弟方法os_add_overflow和os_sub_overflow)包装了一个新的clang内置函数,即使对于混合类型整数运算,它也可以正确检测溢出。 这样就无需将参数和结果转换为通用类型,从而消除了另一个溢出错误源。 如果您不使用返回值,则包装程序还会生成一个编译时警告,有助于确保您的代码实际上根据报告的溢出情况采取了某些措施。

检测缓冲区溢出 Detecting Buffer Overflows

要测试缓冲区溢出,您应该尝试输入比程序接受输入的任何地方都要多的数据。 另外,如果您的程序接受标准格式的数据(例如图形或音频数据),则应尝试传递格式错误的数据。 此过程称为模糊测试-Fuzzing。

如果程序中有缓冲区溢出,它将最终崩溃。 (不幸的是,它可能要到一段时间后尝试使用被覆盖的数据而崩溃。)崩溃日志可能会提供一些线索,说明崩溃的原因是缓冲区溢出。 例如,如果您连续多次输入包含大写字母“ A”的字符串,那么您可能会在崩溃日志中找到一个数据块,该数据块重复数字“ A”的ASCII码41(请参见下图)。 如果程序尝试跳转到实际上是ASCII字符串的位置,则可以肯定地表明缓冲区溢出是导致崩溃的原因。

image

如果程序中有任何缓冲区溢出,则应始终假定它们是可利用的并进行修复。 要证明缓冲区溢出是不可利用的,要比仅仅修复错误要难得多。 还要注意,尽管您可以测试缓冲区溢出,但不能测试缓冲区溢出是否存在; 因此,有必要仔细检查代码中的每个输入和每个缓冲区大小的计算、

避免缓冲区溢出 Avoiding Buffer Underflows

从根本上说,当代码的两个部分对缓冲区的大小或该缓冲区中的数据有不同情况时,就会发生缓冲区下溢。例如,固定长度的C字符串变量可能有256个字节的空间,但可能包含只有12个字节长的字符串。

缓冲区下溢条件并不总是危险的;当正确的操作取决于代码的两个部分以相同的方式处理数据时,它们将变得很危险。当您读取缓冲区以将其复制到另一个内存块,通过网络连接发送缓冲区等时,通常会发生这种情况。

缓冲区下溢漏洞有两大类:短写入和短读取。

短写入缓冲区无法完全填充缓冲区时,会发生短写入漏洞。发生这种情况时,写入后仍然存在缓冲区中先前的某些数据。如果应用程序稍后在整个缓冲区上执行操作(例如,将其写入磁盘或通过网络发送),则现有数据会随手携带。数据可能是随机垃圾数据,但是如果数据碰巧很有趣,则说明信息泄漏。

此外,当发生此类下溢时,如果这些位置中的值影响程序流,则该下溢可能会导致不正确的行为,甚至包括允许您通过将现有授权数据从堆栈中保留下来而跳过身份验证或授权步骤。另一个用户,应用程序或其他实体的先前调用。

短写示例(系统调用):例如,考虑需要命令数据结构并在该数据结构中包含授权令牌的UNIX系统调用。假定数据结构有多个版本,且具有不同的长度,因此系统调用同时采用结构和长度。假设授权令牌在结构中相当低。

​ 假设恶意应用程序传递了命令结构,并且传递了一个包含数据的大小,该大小一直到但不包括授权令牌。内核的系统调用处理程序调用copyin,后者将应用程序中的一定数量的字节复制到内核地址空间中的数据结构中。如果内核没有对该数据结构进行零填充,并且内核没有检查该大小是否有效,则极有可能堆栈仍然在内核内存中的相同地址上包含先前调用者的授权令牌。因此,攻击者能够执行应被禁止的操作。

当从缓冲区读取无法读取缓冲区的完整内容时,将发生短读取漏洞。如果程序随后基于该短读做出决策,则可能导致许多错误行为。当使用C字符串函数从实际上不包含有效C字符串的缓冲区中读取数据时,通常会发生这种情况。

C字符串定义为包含以空终止符结尾的一系列字节的字符串。根据定义,它不能在字符串末尾包含任何空字节。结果,基于C字符串的函数(例如strlenstrlcpystrdup)将字符串复制到第一个空终止符,并且不知道原始源缓冲区的大小。

相比之下,其他格式的字符串(例如CFStringRef对象,Pascal字符串或CFDataRef blob)具有显式长度,并且可以在数据中的任意位置包含空字节。如果将这样的字符串转换为C字符串然后求值该C字符串,则会得到错误的行为,因为生成的C字符串实际上在第一个空字节处结束。

短读示例(SSL验证):几年前,许多SSL堆栈中都出现了一个短读漏洞示例。通过为您拥有的域的精心设计的子域申请SSL证书,您可以有效地创建对任意域有效的证书。

考虑格式为targetdomain.tld [null_byte] .yourdomain.tld的子域。

因为证书签名请求包含Pascal字符串,所以假设证书颁发机构正确地解释了它,则证书颁发机构将与yourdomain.tld的所有者联系,并会请求提供证书的许可。因为您拥有该域,所以您将同意该域。然后,您将获得一个有效的证书,该证书对于所讨论的看起来很奇怪的子域是有效的。

但是,在检查证书的有效性时,许多SSL堆栈将Pascal字符串错误地转换为C字符串,而没有进行任何有效性检查。发生这种情况时,所得的C字符串仅包含targetdomain.tld部分。然后,SSL堆栈将该截短的版本与用户请求的域进行比较,并将证书解释为对目标域有效。

在某些情况下,甚至有可能构建对此类浏览器中每个可能域都有效的通配符证书(例如,*。com [null] .yourdomain.tld将与每个.com地址匹配)。

如果遵守以下规则,则应该能够避免大多数下溢攻击:

  • 使用前将所有缓冲区归零。仅包含零的缓冲区不能包含过时的敏感信息。

  • 始终检查返回值,并适当地失败。

  • 如果对分配或初始化函数的调用失败(例如,AuthorizationCopyRights),则不要评估结果数据,因为它可能是过时的。

  • 使用读取的系统调用和其他类似调用返回的值来确定实际读取了多少数据。然后:

    • 使用该结果来确定存在多少数据,而不是使用预定义的常量或

    • 如果函数未返回预期的数据量,则失败.

  • 如果写调用,printf调用或其他输出调用返回而未写所有数据,则显示错误并失败,尤其是如果您稍后可能回读该数据时。

  • 使用包含长度信息的数据结构时,请始终验证数据是否符合您的预期大小。

  • 尽可能避免将非C字符串(CFStringRef对象,NSString对象,CFDataRef对象,Pascal字符串等)转换为C字符串。而是使用原始格式的字符串。 如果无法做到这一点,请始终对所得的C字符串执行长度检查,或检查源数据中的空字节.

  • 避免混合使用缓冲区操作和字符串操作。如果无法做到这一点,请始终对所得的C字符串执行长度检查,或检查源数据中的空字节。

  • 以防止恶意篡改或截断的方式保存文件。 (有关更多信息,请参见竞争条件和安全文件操作。)

提供帮助的安全功能 Security Features that Can Help

每次运行软件时,如有可能,最新版本的macOS和iOS将为堆栈,堆,库,框架和可执行代码选择不同的位置。 这使得成功利用缓冲区溢出变得更加困难,因为不再可能知道缓冲区在内存中的位置,也无法知道库和其他代码的位置。

地址空间布局随机化 Address Space Layout Randomization

地址空间布局随机化需要编译器提供一些帮助。特别是,它需要与位置无关的代码。

  • 如果要编译针对macOS 10.7和更高版本或iOS 4.3和更高版本的可执行文件,则默认情况下会启用必要的标志。 您可以根据需要使用-no_pie标志禁用此功能,但是为了最大程度的安全,您不应这样做。

  • 如果要编译针对较早操作系统的可执行文件,则必须通过添加-pie标志来显式启用与位置无关的可执行文件支持

Non-Executable Stack and Heap

最近的处理器支持一种称为NX位的功能,该功能使操作系统可以将内存的某些部分标记为不可执行。 如果处理器试图在任何标记为不可执行的内存页面中执行代码,则相关程序将崩溃。

macOS和iOS通过将堆栈和堆标记为不可执行来利用此功能。 这会使缓冲区溢出攻击更加困难,因为任何将可执行代码放置在堆栈或堆上然后尝试运行该代码的攻击都将失败。

注意:对于32位macOS应用程序,如果您的应用程序允许在10.7之前的OS X上执行,则仅将堆栈标记为不可执行,而不是标记为堆。

大多数情况下,这是您想要的行为。 但是,在极少数情况下(例如编写即时编译器),可能有必要修改该行为。

有两种使堆栈和堆可执行的方法:

  • -allow_stack_execute标志传递给编译器。 这使堆栈(而不是堆)可执行。

  • 使用mprotect系统调用将特定的内存页标记为可执行。

Debugging Heap Corruption Bugs

为了帮助您调试堆破坏错误,可以使用libgmalloc库。 它通过使用保护页和其他技术来提供对溢出的其他检测。 要启用此库,请在终端中键入以下命令:

export DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib

然后从终端运行软件(通过运行可执行文件本身或使用open命令)。 有关更多信息,请参见libgmalloc的手册页。

Other Compiler Flags That Affect Security

除了-pie-allow_stack_execute之外,以下标志还对安全性有影响:

  • -fstack-protector-fstack-protector-all-启用堆栈canaries (特殊值,如果修改这些值,则意味着相邻字符串溢出其边界),并更改堆栈中项的顺序,以最大程度地减少损坏的风险。当您的函数调用可能会溢出的其他函数时,编译器随后将插入其他代码,以验证canaries 值是否已被修改。 -fstack-protector标志仅对包含超过8个字节的缓冲区(例如,堆栈上的字符串)的函数启用堆栈Canaries,并且在为macOS 10.6及更高版本进行编译时默认情况下启用。

  • -fstack-protector-all标志为所有功能启用堆栈canaries 。

  • -D_FORTIFY_SOURCE-向通常不提供任何功能的许多功能(sprintfvsprintfsnprintfvsnprintfmemcpymempcpymemmovememsetstrcpystpcpystrncpystrcatstrncat)添加附加的静态和动态边界检查。如果设置为1级(-D_FORTIFY_SOURCE = 1),则仅执行编译时检查。针对macOS 10.6及更高版本进行编译时,默认情况下启用1级。在级别2,执行其他运行时检查。

  • MallocCorruptionAbort-一个环境变量,告诉32位应用程序由于堆结构损坏而导致malloc调用失败而中止。将为64位应用程序自动启用中止堆损坏。

追加内容

这个篇幅是下一篇文章 《Validating Input and Interprocess Communication》 中的,个人感觉也是不错的案例,分享一下。

格式化字符串攻击 Format String Attacks

如果要从用户或其他不可信来源获取输入并进行显示,则需要注意,your display routines 不会处理从不可信来源收到的格式字符串。 例如,在以下代码中,syslog标准C库函数用于将收到的HTTP请求写入系统日志。 因为syslog函数处理格式字符串,所以它将处理输入数据包中包含的所有格式字符串:、

/* receiving http packet */
int size = recv(fd, pktBuf, sizeof(pktBuf), 0);
if (size) {
    syslog(LOG_INFO, "Received new HTTP request!");
    syslog(LOG_INFO, pktBuf);
}

许多格式字符串可能会导致应用程序出现问题。 例如,假设攻击者在输入数据包中传递了以下字符串

"AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%n"

该字符串从堆栈中检索八个item。 假设格式字符串本身存储在堆栈中,具体取决于堆栈的结构,这可能有效地将堆栈指针移回格式字符串的开头。 然后,%n token将导致打印功能获取到目前为止已写入的字节数,并将该值写入存储在下一个参数(恰好是格式字符串)中的内存地址中。 因此,假设为32位体系结构,格式字符串本身中的AAAA将被视为指针值0x41414141,并且该地址处的值将被数字76覆盖。

这样做通常会在下次系统必须访问该内存位置时导致崩溃,通过使用为特定设备和操作系统精心设计的字符串,攻击者可以将任意数据写入任何位置。 有关格式字符串语法的完整说明,请参见printf的手册页。

为防止格式字符串攻击,请确保没有任何输入数据作为格式字符串的一部分传递。 要解决此问题,只需在每个此类函数调用中包含您自己的格式字符串。 例如,这样的调用printf(buffer) 可能会受到攻击,但 printf(“%s”, buffer) 不会。在第二种情况下,将打印出buffer参数中的所有字符(包括百分号(%)),而不是将其解释为格式化标记。

当一个字符串被意外格式化超过一次时,这种情况会变得更加复杂。 以下示例错误地将调用结果传递给NSString方法stringWithFormat:作为NSAlert方法alertWithMessageText:defaultButton:alternateButton:otherButton:informativeTextWithFormat:的notifyativeTextWithFormat参数的值。 结果,该字符串被格式化两次,并且来自导入证书的数据被用作NSAlert方法的格式字符串的一部分。

alert = [NSAlert alertWithMessageText:"Certificate Import Succeeded"
    defaultButton:"OK"
    alternateButton:nil
    otherButton:nil
    informativeTextWithFormat:[NSString stringWithFormat: /* BAD! BAD! BAD! */
       @"The imported certificate \"%@\" has been selected in the certificate pop-up.",
       [selectedCert identifier]]];

[alert setAlertStyle:NSInformationalAlertStyle];
[alert runModal];

正确的做法:

alert = [NSAlert alertWithMessageText:"Certificate Import Succeeded"
    defaultButton:"OK"
    alternateButton:nil
    otherButton:nil
    informativeTextWithFormat:@"The imported certificate \"%@\" has been selected in the certificate pop-up.",
       [selectedCert identifier]];

以下常用功能和方法会遭到格式字符串攻击:

  • Standard C

    • printf and other functions listed on the printf(3) manual page

    • sscanf and other functions listed on the scanf(3) manual page

    • syslog and vsyslog

  • Carbon

    • AEBuildDesc and vAEBuildDesc

    • AEBuildParameters and vAEBuildParameters

    • AEBuildAppleEvent and vAEBuildAppleEvent

  • Core Foundation

    • CFStringCreateWithFormat

    • CFStringCreateWithFormatAndArguments

    • CFStringAppendFormat

    • CFStringAppendFormatAndArguments

  • Cocoa

    • stringWithFormat:, initWithFormat:, and other NSString methods that take formatted strings as arguments

    • appendFormat: in the NSMutableString class

    • alertWithMessageText:defaultButton:alternateButton:otherButton:informativeTextWithFormat: in NSAlert

    • predicateWithFormat:, predicateWithFormat:arguments:, and predicateWithFormat:argumentArray: in NSPredicate

    • raise:format: and raise:format:arguments: in NSException

    • NSRunAlertPanel and other AppKit functions that create or return panels or sheets

感悟

这个文章让我对底层的堆栈溢出有了更好的认识,这篇文章在解决崩溃问题中也是极有帮助的,特此记录一下以便以后回看。几乎是用Google翻译的,稍微改了下措辞,有不对的地方请大家斧正。

原地址:developer.apple.com/library/arc…