格式字符串漏洞 - printf

时间:2011-04-15 06:09:45

标签: c string security format

为什么打印内存地址的值为0x08480110?我不确定为什么有5%的08x参数 - 这会把你带到哪里?

address = 0x08480110
address (encoded as 32 bit le string): "\x10\x01\x48\x08"
printf ("\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|");

此示例取自本文第11页http://crypto.stanford.edu/cs155/papers/formatstring-1.2.pdf

4 个答案:

答案 0 :(得分:21)

我认为本文以一种有点令人困惑的方式提供了它的printf()示例,因为这些示例使用字符串文字来表示格式字符串,而那些通常不允许描述的漏洞类型。此处描述的格式字符串漏洞取决于用户输入提供的格式字符串。

所以示例:

printf ("\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|");

最好表现为:

/* 
 * in a real program, some user input source would be copied 
 * into the `outstring` buffer 
 */
char outstring[80] = "\x10\x01\x48\x08_%08x.%08x.%08x.%08x.%08x|%s|";

printf(outstring);

由于outstring数组是自动的,编译器可能会把它放在堆栈上。将用户输入复制到outstring数组后,它在堆栈上看起来像下面的'words'(假设小端):

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"

编译器会在其认为合适的情况下将其他项放在堆栈上(其他局部变量,保存的寄存器,等等)。

即将进行printf()调用时,堆栈可能如下所示:

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"
var1
var2
saved ECX
saved EDI

请注意,我正在完成这些条目 - 每个编译器将以不同的方式使用堆栈(因此格式字符串漏洞必须针对特定的确切方案自定义。换句话说,您不会总是使用这个示例中有5个虚拟格式说明符 - 作为攻击者,您需要确定特定漏洞需要多少个虚拟对象。

现在调用printf(),参数(outstring的地址)被推送到堆栈并调用printf(),因此堆栈的参数区域如下所示: / p>

outstring[0c]               // etc...
outstring[08] 0x30252e78    // from "x.%0"
outstring[04] 0x3830255f    // from "_%08"
outstring[00] 0x08480110    // from the ""\x10\x01\x48\x08"
var1
var2
var3
saved ECX
saved EDI
&outstring   // the one real argument to `printf()`

但是,printf并不真正知道有多少参数放在堆栈上 - 它依赖于它在格式字符串中找到的格式说明符(它确定要获得的一个参数)。所以printf()获取格式字符串参数并开始处理它。当它到达与我的示例中的“已保存的EDI”相对应的第一个“%08x”时,则下一个“%08x”将打印出 保存了ECX'等等。因此,“%08x”格式说明符只是在堆栈中占用数据,直到它返回到攻击者能够输入的字符串。确定需要多少这些是攻击者通过一种试验和错误(可能通过具有大量“%08x”格式的测试运行来做的事情,直到他能够“看到”格式字符串的开始位置)。

无论如何,当printf()处理“%s”格式说明符时,它已经消耗了outstring缓冲区所在的所有堆栈条目。 “%s”说明符将其堆栈条目视为指针,并且用户放入该缓冲区的字符串经过精心设计,具有0x08480110的二进制表示形式,因此printf()将打印出来无论在该地址作为ASCIIZ字符串。

答案 1 :(得分:8)

您有6个格式说明符(5个%08x和1个%s),但您没有为这些格式说明符提供值。你会立即陷入未定义行为的境界 - 任何事情都可能发生,并且没有错误的答案。

但是,在正常的事件过程中,传递给printf()的值将存储在堆栈中,因此printf()中的代码会读取堆栈中的值,就好像额外的值已经过了通过。函数返回地址也在堆栈中。无法保证我可以看到实际生成的值为0x08480110。这种攻击很大程度上取决于具体的程序和错误的函数调用,你可能会得到一个非常不同的值。示例代码很可能是在假设32位Intel(小端)CPU而不是64位或大端CPU的情况下编写的。


调整代码片段,将其编译成完整的程序,忽略编译警告,使用MacOS X 10.6.7上的32位编译和GCC 4.2.1(XCode 3),代码如下:

#include <stdio.h>

static void somefunc(void)
{
    printf("AAAAAAAAAAAAAAAA.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.|%s|\n");
}

int main(void)
{
    char buffer[160] =
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz01234";
    somefunc();
    return 0;
}

产生以下结果:

 AAAAAAAAAAAAAAAA.0x000000A0.0xBFFFF11C.0x00001EC4.0x00000000.0x00001E22.0xBFFFF1C8.0x00001E5A.|abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz012345abcdefghijklmnopqrstuvwxyz01234|

正如您所看到的,我最终从printf()语句中找到了主程序中的字符串。当我在64位模式下编译它时,我得到了一个核心转储。两种结果都完全正确;程序调用未定义的行为,因此程序执行的任何操作都是有效的。如果您感到好奇,请搜索“鼻子恶魔”以获取有关未定义行为的更多信息。

并习惯于尝试这些问题。


另一种变化

#include <stdio.h>

static void somefunc(void)
{
    char format[] =
        "AAAAAAAAAAAAAAAA.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n"
        ".0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n"
        ".0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X.0x%08X\n";
    printf(format, 1);
}

int main(void)
{
    char buffer[160] =
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz012345"
        "abcdefghijklmnopqrstuvwxyz01234";
    somefunc();
    return 0;
}

这会产生:

AAAAAAAAAAAAAAAA.0x00000001.0x00000099.0x8FE467B4.0x41000024.0x41414141
.0x41414141.0x41414141.0x2E414141.0x30257830.0x302E5838.0x38302578.0x78302E58
.0x58383025.0x2578302E.0x2E583830.0x30257830.0x2E0A5838.0x30257830.0x302E5838

您可能会识别十六进制输出中的格式字符串 - 例如,0x41是大写字母A.

该代码的64位输出相似且不同:

AAAAAAAAAAAAAAAA.0x00000001.0x00000000.0x00000000.0xFFE0082C.0x00000000
.0x41414141.0x41414141.0x2578302E.0x30257830.0x38302578.0x58383025.0x0A583830
.0x2E583830.0x302E5838.0x78302E58.0x2578302E.0x30257830.0x38302578.0x38302578

答案 2 :(得分:1)

你误解了这篇论文。

您链接的文字是假设堆栈上的当前位置是0x08480110(查看周围的文字)。 printf()将从您正好在堆栈的任何位置转储数据。

格式字符串开头的\x10\x01\x48\x08仅用于将(假定的)地址打印到转储数据前面的stdout。这些数字决不会修改转储数据的地址。

答案 3 :(得分:0)

关于“把你拉上筹码”你是对的,但只是勉强;它依赖于假设参数在堆栈上传递,而不是在寄存器中传递。 (对于variadic function来说,这可能是一个安全的假设,但仍然是关于实现细节的假设。)

每个%08x要求以“十六进制”打印“下一个unsigned int参数”;在“下一个参数”位置实际发生的是体系结构和编译器相关的。如果您将此过程中使用/proc/self/maps得到的值进行比较,则可能会缩小某些数字的含义。