栈溢出原理与实践_3

0day软件漏洞分析技术 专栏收录该内容
3 篇文章 0 订阅

栈溢出原理与实践

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. 结果:运行程序,弹窗。

  • 0
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值