与缓冲区溢出漏洞相比,Format String漏洞的历史就要短得多,而且实际的例子也少很多。比较著名的象Linux上的rpc.statd,还有wu-ftpd版本2.x,前一段时间我还想找这个2.x版本的wu-ftpd源程序来Exploit一下,找来找去没找到,估计作者把它藏起来了。
不过网上倒是有几篇介绍Format String漏洞的文章值得一读,象Team teso的"Exploiting Format String Vulerabilities",还有Kalou/Pascal Bouchareine的"Format string vulnerability",这些文章的引用率应该很高,你们到Google上保证找得到。当然,我觉得我的这篇文章也不错,欢迎大家来看啦,只是有史以来很多人都喜欢读原著,象我大学时教政治的老师,他就经常说要读马列原著才行,翻译的都不灵光。
这一章我将在Caldera Linux V2.2上演示Format String的原理以及Exploit例子,不过我想其它Flavor的Linux也能试验这一章用到的程序。演示用的机器是claton----这是我美国梦开始的地方。
针对Linux操作系统或者Linux应用程序的Exploit多如牛毛,毕竟Linux是Open Source的,各方豪杰都要围着它们显显身手、切磋武功----我也花了不少时间在Linux上操练(不好意思,自己称自己为豪杰)。
Anyway,新时代的口号: Long Live OpenSource!!!!
关于Format String漏洞的背景介绍之一:
按老规矩,在作Exploit之前,我们要先介绍一下Format String的背景知识。请看下面的程序fordemo.c
<======================fordemo.c=========================>
main()
pb
char buf[512]="";
char tmp[512]="hello world ";
memset(buf, " ", 512);
read(0, buf, 512);
printf(tmp);
printf("%s %s", buf,tmp);
printf("%x==%x==%x==%x ");
}
<========================================================>
fordemo连续三次调用格式输出函数printf:第一次与第二次都属正常,大家在编写程序时也经常如此这般地调用printf;但第三次调用却有些奇怪,为什么只有格式化符号%x?根据printf的语法要求,这次调用还需要另外四个输入参数,类似于下面的形式才对:
printf("%x==%x==%x==%x ",forx1,forx2,forx3,forx4);
那么,当程序fordemo调用第三个printf时,它会如何反应呢?让我们深入到fordemo的汇编码中看看。
先编译程序fordemo.c,编译器并不认为第三个printf调用有语法错误。
[moda@claton format]$ gcc fordemo.c -o fordemo -g
用gdb运行fordemo:
[moda@claton format]$ gdb fordemo
GNU gdb 4.17.0.11 with Linux/x86 hardware watchpoint and FPU support
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-COL-Linux"...
/*
先在函数printf入口设置断点
*/
(gdb) b printf
Breakpoint 1 at 0x80484f4
(gdb) r
Starting program: /home/moda/format/fordemo
Breakpoint 1 at 0x40066c1c: file printf.c, line 30.
hello babby
/*
这里通过函数read输入字符串"hello babby"给缓冲区buf
*/
Breakpoint 1, printf (format=0xbffff8b8 "hello world ") at printf.c:30
/*
已经进入第一次printf函数调用----"printf(tmp)",然后在断点处停下。我们看看进程当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0 printf (format=0xbffff8b8 "hello world ") at printf.c:30
#1 0x80486a9 in main () at fordemo.c:10
#2 0x4003286f in __libc_start_main (main=0x8048608 , argc=1,
argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
at ../sysdeps/generic/libc-start.c:92
(gdb) x/20x $esp
0xbffff8a8: 0x40102e9c 0xbffffcb8 0x080486a9 0xbffff8b8
0xbffff8b8: 0x6c6c6568 0x6f77206f 0x0a646c72 0x00000000
0xbffff8c8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff8d8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff8e8: 0x00000000 0x00000000 0x00000000 0x00000000
/*
观察一下call stack可以知道,0x080486a9是函数printf返回main的地址,它前面的0xbffffcb8应该是main的堆栈栈底($EBP)地址,而它后面的0xbffff8b8就是函数printf的输入参数,也就是指向缓冲区tmp的起始地址的指针。我们可以核实一下:
*/
(gdb) x/s 0xbffff8b8
0xbffff8b8: "hello world "
/*
上面这个"printf(tmp)"调用是最简单的情况,只需要一个字符串指针0xbffff8b8作为输入参数,而且这个指针参数是紧接在printf返回地址后面。
我顺便提醒一下(因为可能有人在打瞌睡):指针0xbffff8b8所指向的缓冲区tmp就紧跟在这个指针的后面。
下面继续执行,我们要看一下第二个printf调用时内存的内容。
*/
(gdb) c
Continuing.
hello world
Breakpoint 1, printf (format=0x8049562 "%s %s") at printf.c:30
/*
程序已经进入"printf("%s %s", buf,tmp);",现在停在断点处。我们看看进程当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0 printf (format=0x8049562 "%s %s") at printf.c:30
#1 0x80486c4 in main () at fordemo.c:11
#2 0x4003286f in __libc_start_main (main=0x8048608 , argc=1,
argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
at ../sysdeps/generic/libc-start.c:92
(gdb) x/20x $esp
0xbffff8a0: 0x40102e9c 0xbffffcb8 0x080486c4 0x08049562
0xbffff8b0: 0xbffffab8 0xbffff8b8 0x6c6c6568 0x6f77206f
0xbffff8c0: 0x0a646c72 0x00000000 0x00000000 0x00000000
0xbffff8d0: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff8e0: 0x00000000 0x00000000 0x00000000 0x00000000
/*
0xbffffcb8是函数main的堆栈栈底($EBP)地址,0x080486c4是printf返回main的地址,而紧跟在0x080486c4后面的应该是printf函数的输入参数。源程序中的printf有三个输入参数:"%s %s"、buf、tmp,它们分别对应着0x080486c4后面的0x08049562、0xbffffab8、0xbffff8b8。在printf最后作格式化输出时,0xbffffab8、0xbffff8b8所指向的字符串将替换格式化字符串"%s %s"中的两个%s。
*/
(gdb) x/s 0x08049562
0x8049562 <_IO_stdin_used+18>: "%s %s"
(gdb) x/s 0xbffffab8
0xbffffab8: "hello babby "
(gdb) x/s 0xbffff8b8
0xbffff8b8: "hello world "
/*
从上面的内存分配情况可以看出,当printf的输入参数中有格式化符号时,比如说"%s %s",系统会把实现(或者说替换)这些格式化符号的"真实的"参数或参数指针分配在格式化符号串的后面。这一点对于我们理解Format String的漏洞很重要!的确很重要!
如果你还不理解这一点的重要性,请想象一下如果我们忘记提供替换"%s %s"的两个字符串(不管是有意还是无意),printf会这么处理??
这就是我们第三个printf调用"printf("%x==%x==%x==%x ");"所要说明的!
*/
(gdb) c
Continuing.
hello babby
hello world
Breakpoint 1, printf (format=0x8049568 "%x==%x==%x==%x ") at printf.c:30
/*
现在程序已经进入" printf("%x==%x==%x==%x ")"。我们看看程序当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0 printf (format=0x8049568 "%x==%x==%x==%x ") at printf.c:30
#1 0x80486d1 in main () at fordemo.c:12
#2 0x4003286f in __libc_start_main (main=0x8048608 , argc=1,
argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
at ../sysdeps/generic/libc-start.c:92
(gdb) x/20x $esp
0xbffff8a8: 0x40102e9c 0xbffffcb8 0x080486d1 0x08049568
0xbffff8b8: 0x6c6c6568 0x6f77206f 0x0a646c72 0x00000000
0xbffff8c8: 0x00000000 0x00000000