六、特殊案例
译者:飞龙
日期:2001.9.1
版本:v1.2
有一些可以利用的特定场景,不需要了解所有偏移,或者你可以使利用更加简单,直接,最重要的是:可靠。这里我列出了一些利用格式化字符串漏洞的常见方法。
6.1 替代目标
受基于栈的缓冲区溢出的较长历史的影响,很多人认为,覆盖栈上的返回地址是控制进程的唯一方式。但是如果我们利用格式化字符串漏洞,我们不能准确知道我们的缓冲区在哪里,并且我们可以覆盖另外一些东西。常见的基于栈的缓冲区溢出只能覆盖返回地址,因为它们也存储在栈上。但是使用格式化函数,我们可以覆盖内存中的任意地址,让我们能够修改整个可写入的进程空间。
因此,检验其它部分或完全控制被利用程序的方式,就很有意思了。在特定场景下,这可以产生一种更简单的利用方式 -- 我们之前看到 -- 或者可以用于绕过特定的保护。
我会在这里简单讨论一下替代的地址,并给出更深入的文章的引用。
6.1.1 GOT 覆盖
任何 ELF 二进制 [12] 的进程空间都包含一个特殊区段,叫做“全局偏移表”(GOT)。每个程序使用的库函数都在这里拥有一个条目,它包含一个真实函数的地址。这样是为了允许库在进程内存中简单地重定向,而不是使用硬编码的地址。在程序首次使用函数之前,条目包含运行时链接器(RTL)的地址。如果函数被程序调用,控制流就传递给了 RTL,并且函数的真实地址被解析并插入到 GOT。该函数的每个调用都将控制流直接传递给它自己,RTL 不再为该函数调用了。对于 GOT 利用的更加全面的概览,请参考 Lam3rZ 兄弟的不错的文章 [19]。
通过覆盖程序随后使用的函数的 GOT 条目,我们就可以利用格式化字符串漏洞,获取控制权,并跳到任何可执行的地址。不幸的是,这意味着任何基于栈的保护都会失效,它们检查了返回地址。
我们从覆盖 GOT 条目中获得的巨大优势,就是它独立于环境变量(例如栈),以及动态内存分配(堆)。GOT 条目的地址在每个二进制中是固定的,所以如果两个系统运行了相同的二进制,GOT 条目始终是同一地址。
你可以通过执行这个命令,看到 GOT 条目位于函数的哪里:
objdump --dynamic-reloc binary
真实函数(或者 RTL 链接函数)的地址直接就是打印出的地址。
另一个非常重要的因素,为什么使用 GOT 条目来获取控制权,而不是返回地址,是代码的形式(在一些“安全”指纹守护程序中发现):
syslog (LOG_NOTICE, user);
exit (EXIT_FAILURE);
这里你不能通过覆盖返回地址,来可靠地获取控制权。你可以尝试覆盖syslog
自己的返回地址,但是更加可靠的方式就是覆盖exit
函数的 GOT 条目,它会将执行流传递给你指定的地址,只要exit
被调用。
译者注:动态链接时,程序会调用
libc
中的系统调用的封装。其它系统调用同理。
但是 GOT 技巧的最实用的优点,就是它易于使用,你只需要运行objdump
,就能得到要覆盖的地址(retloc
)。黑客们都懒得打字(除了粗心)。
6.1.2 DTORS
实用 GCC 编译的二进制包含一个特殊的析构器表区段,叫做DTORS
。在真实的exit
系统调用触发之前,在所有的常见清理操作完成之后,这里列出的析构器会调用。DTORS
区段为以下格式:
DTORS: 0xffffffff 0x00000000 ...
其中第一项是一个计数器,它保存了下面函数指针的数量,如果列表为空则为负一(就像这里)。在所有 DTORS 区段的实现中,这个字段都是被忽略的。之后,在相对偏移+4
的位置,就是清理函数的地址,以 NULL 地址终止。你可以仅仅将这个 NULL 指针覆盖为你的 shellcode 指针,并且你的 shellcode 就会在程序退出时执行。这一技巧更加复杂的介绍可以在 [17] 找到。
6.1.3 C 标准库的钩子
几个月之前,Splar Designer 介绍了一种新的技巧来利用malloc
分配的内存中基于堆的溢出。它提倡覆盖 GNU C 库以及其他库中的钩子。通常,这个钩子有内存调试和性能工具使用,在应用使用malloc
接口分配或释放内存时获取通知。有一些钩子,但是最常见的是__malloc_hook
、__realloc_hook
和__free_hook
。通常它们设为 NULL,但是只要你使用指向你代码的指针覆盖了它,你的代码就会在malloc
、realloc
和free
执行时调用。由于钩子通常用作调试工具,它们在真实函数执行之前调用。
关于malloc
覆盖技巧的讨论在 Solar Designer 关于 Netspace JPEG 解码器漏洞的报告中提供。
6.1.4 __atexit
结构
几个月之前,Kalou 介绍了一种利用 Linux 下静态链接二进制的方式,它利用了叫做__atexit
的通用处理器,只要你的程序调用了exit
,它就会执行。这允许程序建立很多处理器,它们会在退出时调用来释放资源。__atexit
结构上的攻击的详细讨论,可以在 Pascal Bouchareines 的文章 [16] 中找到。
6.1.5 函数指针
如果漏洞应用使用了函数指针,我们就有机会覆盖它们。为了充分利用它们,你需要覆盖它并且之后触发它们。一些守护程序使用函数指针表来处理命令,例如 QPOP。同时,函数指针也通常用于模拟类似__atexit
的处理器,例如 SSHD。
6.1.6 jmpbuf
首先,jmpbuf
覆盖技巧用于堆缓冲区的利用。使用格式化字符串,jmpbuf
的行为就像函数指针,因为我们可以覆盖内存的任意地方,不仅限于jmpbuf
到我们的缓冲区的相对位置。深入讨论可以在 Shok 关于堆溢出的文章中发现。
6.2 Return-to-libc
你可以使用常见的 Return-to-libc 技巧,同样是由 Solar Designer 提出的 [14]。但是有时会有捷径,它会产生更简单的利用。
FILE * f;
char foobuf[512];
snprintf (foobuf, sizeof (foobuf), user);
foobuf[sizeof (foobuf) - 1] = ’\0’;
f = fopen (foobuf, "r");
你可以将fopen
的 GOT 地址替换为system
的函数地址。之后使用这样的格式化字符串:
"cd /tmp;cp /bin/sh .;chmod 4777 sh;exit;" "addresses|stackpop|write"
其中addresses
、stackpop
和write
是常见的格式化字符串利用㤡。它们用于架构fopen
GOT 条目覆盖为system
的地址。fopen
调用的时候,字符串转递给了system
函数。或者你可以使用常用的旧方法,向上面描述的那样。
6.3 多重打印
如果你可以在相同进程中多次触发格式化字符串漏洞(就像 wu-ftpd 那样),你就可以不仅仅覆盖返回地址。例如,你可以将整个 shellcode 储存在堆上来绕过任何不可执行的栈保护。和其它这里解释的技巧一起使用,你就可以绕过下面的保护措施(显然是不完全的):
- StackGuard
- StackShield
- Openwall 内核补丁(由 Solar Designer)
- libsafe
在 2000 年十月中旬,一群人发布了一系列 Linux 内核补丁,叫做 PaX [11],能够高效实现可读可写但不可执行的页面。由于它不焖好后在 x86 CPU 系列上这么做,这个补丁用了一些技巧,它们被 Plex CPU 模拟器项目所发明。在运行这个补丁的系统中,几乎不可能执行引入该进程的任意 shellcode。但是多数情况下,在进程空间中已经有了实用的代码。我们可以执行这个代码来做通常在 shellcode 中所做的事情。
使用通常的 Return-to-libc 技巧 [14],你可以绕过这个保护。最简单的案例就是返回到system
库函数,使用格式化字符串作为参数。
通过稍微优化字符串,你可以将需要了解的强制性偏移减为一个:system
函数地址。为了调用程序,你可以在格式化字符串的尾部使用这个序列:
";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;id > /tmp/owned;exit;"
任何指向;
字符的地址,传递给system
函数时,都会执行该命令,因为;
字符在 shell 的命令中是 NOP。
6.4 堆中的格式化字符串
到现在为止,我们假设格式化字符串始终在栈上。但是,有些情况下,它储存在堆上。如果栈上有另一个我们可以影响的缓冲区,我们就可以使用它来提供要写入的地址,但是如果没有这种缓冲区,我们有几种替代方案。
如果目标缓冲区在栈上,我们首先可以打印它,之后使用那里的地址,来使用%n
参数写入:
void func (char *user_at_heap) {
char outbuf[512];
snprintf (outbut, sizeof (outbuf), user_at_heap);
outbuf[sizeof (outbuf) - 1] = ’\0’;
return;
}
这里我们使用了一个格式化字符串,它包含我们想要写入的地址,像通常一样。但是它特别的是,我们不能从格式化字符串本身来访问这些地址,而是通过目标缓冲区。为此我们首先需要在栈上储存地址,通过简单打印它们。因此写入的序列需要在格式化字符串的地址后面。
如果两个缓冲区都不在栈上,问题就来了:
void func (char *user_at_heap) {
char * outbuf = calloc (1, 512);
snprintf (outbut, 512, user_at_heap);
outbuf[511] = ’\0’;
return;
}
现在它取决于我们是否可能在栈上提供数据。例如,一些 wu-ftpd 的利用使用密码字段来储存数据(shellcode,并不是地址 -- 这些利用程序不能利用非匿名的账户)。
每个漏洞和利用都是不同的,在说它不可利用之前,你应该花费几个小时来学习漏洞,并且你有可能是错的,因为这里展示的不仅仅是格式化字符串漏洞的历史。(你好,OpenBSD 团队!)
6.5 特殊的考虑
除了利用自身,也有一些需要考虑的东西。如果格式化字符串含有 shellcode,它不能包含\x25
(%
)或者空字节。但是由于没有重要的操作码是0x25
或者0x00
,你在构造 shellcode 时不会有什么麻烦。如果地址储存在格式化字符串中,是一样的。如果你想要写入的地址包含空字符,你可以将其替换为某个奇数地址的短整形写入,它位于你想要写入的地址下方。虽然它在所有架构上都是不可能的。同样,你也可以使用两个单独的格式化字符串。第一个在内存中,整个字符串的后面创建你打算写入的地址。第二个使用这个地址来写入它。
这可能变得有些复杂,但是可以可靠地利用,并且有时值得花费精力。