[二进制漏洞]栈(Stack)溢出漏洞 Linux篇

[二进制漏洞]栈(Stack)溢出漏洞 Linux篇

前言

我们在学习栈溢出漏洞之前,最好都要懂一些开发,还有一些汇编知识,因为不管是安全还是逆向,这些都是基于开发的,有了开发扎实的基础在后续中才会突破瓶颈。

堆栈

推荐大家可以先去看看《王爽汇编》,或者直接看Bilibili的堆栈是个啥?

image-20220617134927429

堆栈(Stack)概念

首先来了解下什么是堆栈?我们得从CPU开始说起,CPU中有个模块叫ALU,专门用来处理数据运算。

学过汇编的小伙伴们都知道,CPU中有多个寄存器,不过是固定的,比如eax、ebx、ecx、edx、ebp、esp、edi、esi、eip等,当处理的数据过多或者过大时候,寄存器都不够用了,这时候怎么办?

增加CPU的寄存器吗?不行那样成本太大了,所以就需要找另外的地方存数据,那么硬件中读取速度除了CPU,也就内存条速度最快了。

所以CPU招募了内存条,来用来存储数据,在内存条中还专门找了个区域用来存数据即:堆栈(stack),说白了堆栈就是一块内存

堆栈数据存储方式

我简单的画了一个堆栈示意图,堆栈是一个自高地址向下增长的内存空间,从图中可以看到我们的高地址,也就是栈底,而低地址大概4个空格的位置是栈顶。

也就是记住地址越低是栈顶,而且堆栈中要添加数据,地址要往跟低的地址移动。

image-20220617143333004

接下来我们继续来看看,如何在堆栈中存取和读取数据,他既然是块内存,那么我们关注的肯定是存取和读取,首先堆栈中存入数据叫push,读取数据叫pop

堆栈管理数据的方式是先进后出,即存取进去的数据,会在堆底,最后存取进去的数据会在栈顶,所以最先拿出来的数据也是最后放进去的即栈顶。

image-20220617144702914

这里有个需要注意的地方,就是很多人以为pop数据后,堆栈里面的数据就清空了,其实并不是。

之前说过堆栈其实就是一块内存,当我们pop后,其实知识把栈顶往下移了而已,内存里面的数据还是在的,并没有被清除掉,只是对于堆栈而言,那数据被弹出。

当要push新数据,push很多个数据,或者pop很多个数据,都按照图示以此类推。

image-20220617145414921

函数调用

函数调用C语言代码

学习堆栈最重要的应该就是函数调用了,当我们调用完一个函数后,代码都会往下继续执行下一句代码,那么这一步在底层是如何实现的呢?CPU怎么知道接下来要执行你函数调用完后的下一句代码?

这一部分其实稍微学过汇编的都应该知道。

当我们调用个函数的时候,在汇编层是叫Call myfunction,而调用函数的时候就会用到堆栈,传入的参数即:push

#include <stdio.h>

/*自己的函数*/
void myfunction(int a,int b)
{
    int c = a+b;
    printf("%d\n",c);
}

int main()
{
    myfunction(1,2);
    printf("函数调用完毕!");
    return 0;
}

函数调用过程GDB调试

接着我们将上面代码编译出来,并且关闭stack保护,编译成32位,命令gcc test.c -m32 -fno-stack-protector -o test

接下来用pwndbg进行调试,详细的看下,函数调用与堆栈中的关系。gdb test , b main ,r

image-20220617154234257

断点断到如上的位置,然后再单步n执行到call myfunction处,此时注意观察堆栈,可以看到堆栈中压入了数据2,1

而我们代码是 myfunction(1,2);第一个参数是1,第而个参数是2,因为堆栈的先进后出的特性,所以先把最后的数据入栈。

image-20220617154431218

函数Call返回原理

接着最重要的一步,需要注意!

目前我们处在call myfunction函数上,我们先记一下call myfunction的下一句汇编地址是多少,我这里是0x565555ac,然后接着我们输入si,单步步入进行调试,跳转到myfunction函数的内部,然后此时注意观察你的堆栈有什么变化!

此时我们观察堆栈发现,之前我们call的下一句地址0x565555ac被压栈了。

image-20220617155152479

当我们一直单步步过myfunction函数中的汇编代码,直到他的最后一句这里,发现汇编代码是一句ret,ret的汇编代码其实就是pop eip

也就是将堆栈中的数据弹出到eip,eip我们都知道是汇编中的PC指针,修改eip,那么当前CPU就会指向那地方开始执行代码。

而当前的堆栈数据就是我们调用myfunction函数时压入的下一条指令的地址,所以将其弹到eip,CPU就会指向那地方执行代码。

image-20220617155447844

所以底层利用这种call 函数时将下一条指令地址压栈的方式,然后执行完函数后再弹栈到eip的方式跳过到调用完函数后的下一条代码。

image-20220617160107736

函数栈帧

函数栈帧描述

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构

当一个函数在运行时,需要为它在堆栈中创建一个栈帧(stack frame)用来记录运行时产生的相关信息,因此每个函数在执行前都会创建一个栈帧,在它返回时会销毁该栈帧。

所以说函数栈帧就是一种数据结构,也是块内存里的数据。

函数栈帧调试

我们继续用之前的代码做例子,然后用pwndbg调试来详细的分析函数栈帧。

image-20220617164713531

如上图,当我们准备调用call myfunction的时候,其实在C语言中是当我们执行myfunction(1,2)的时候就会生成一个栈帧,那么在汇编层具体是什么时候创建呢?

然后当我们进入到myfunction函数内部,然后看到第一条汇编语句是push ebp,将ebp寄存器压入堆栈。

EBP寄存器又被称为帧指针(Frame Pointer) 【指向当前栈帧的底部】
ESP寄存器又被称为栈指针(Stack Pointer) 【永远指向栈帧的顶部】

然后接着的一句汇编代码是mov ebp,esp这一句汇编指向完后,才开始真正的创建栈帧。

此时栈帧的数据结构差不多是这样: (现在我们就可以用ebp来进行寻址了,当我们要用到第一个参数那么用ebp+8即可,第二个参数ebp+0xC)

[ebp+0]  -----> 栈帧底 ,也是当前的栈顶  【ebp】【esp】
[ebp+4]     --> 调用完Call函数后下一条指令地址
[ebp+8]     --> 1(参数1)
[ebp+0xC]   --> 2(参数2)

image-20220617181933836

在我们代码中myfunction里面还有计算a+b的值赋值给c的代码,我们继续调试看汇编且关注栈帧中对数据的处理。

当执行完栈帧创建后的汇编代码后,第一句的汇编代码是sub esp,0x10,我们之前讲过esp永远为栈顶,当esp-16代表的是,esp要向上移动16字节,用来存放数据。

一般来说这种sub esp,xxx或者add esp,-xxx,都是用来创建临时变量 ,存放临时变量数据的。我们这里的临时变量就一个那就是int c,那么int占用4个字节,这里开辟了

16字节空间,可能是gcc的优化为了对齐什么的吧,Windows的话多少个临时变量空间就开辟多少空间。

image-20220617182016232

那么此时的栈帧结构如下所示:

[ebp-0x10]  栈顶 [esp]
[ebp-0xC]
[ebp-8]
[ebp-4]
[ebp+0]     -----> ebp 栈帧底 ,之前栈顶
[ebp+4]     --> 调用完Call函数后下一条指令地址
[ebp+8]     --> 1(参数1)
[ebp+0xC]   --> 2(参数2)
    
【可以看到我们可以利用ebp这种方式来进行对临时变量的一个定位,因为ebp永远是栈底,所以可以用来寻找不同的数据,当ebp-代表的是临时变量,ebp+代表的是函数参数】

当我们继续单步执行代码,执行到如下图所示的地方,可以看到果然是利用【ebp+偏移】进行函数参数的定位,然后利用【ebp-偏移】进行临时变量的定位。

image-20220617183223102

OK很好,到这里我们基本已经了解了函数调用栈帧的一个详细原理了,这里再考考大家,那我在这个myfunction函数里要怎么知道返回后下一条代码的地址呢?

这个在之前说过了,当执行到ret汇编代码的时候,会把堆栈里面数据弹给eip。
那么现在我们用了函数调用帧的概念,是不是就很好懂了,当我们执行到ret的时候,这时候栈帧也就全部结束了,所以此时堆栈中的数据就是返回地址了。
也可以用[ebp+4]来代表返回地址。

最后从其他文章里面偷来的图片,方便理解函数栈帧概念。

img

栈溢出漏洞实战

要求实现栈溢出来执行没有被调用的hack函数。

要求:不允许使用pwntools工具

#include <stdio.h>

void hack()
{
    printf("Hack Success!!!!\n");
}

int main()
{
    printf("Hello,Please Start Hack!\n");
    char buf[20];
    scanf("%s",buf);
    return 0;
}

首先我们执行程序,然后输入>=20字节,程序会崩溃(缓冲区溢出)!

image-20220617190111564

pwndbg调试

接下来老规矩,pwndbg开始调试。

首先来找到返回地址,正常情况下[ebp+4]就是ret的返回地址,但是main函数可能不太一样。

image-20220617193144547

调试下来发现,[ebp+20]才是返回地址,这个实际情况还是以ret语句时候堆栈里面的数据为准。

image-20220617204342983

在这里我们可以手动用命令set *地址=值来把return地址改成其他的,这里我们改成hack函数。

image-20220617204941416

image-20220617205103779

开始Hack

OK上面我没通过调试器修改数值,直接将堆栈的值改成了hack函数的地址,让他在return的时候直接返回到hack函数,从而成功输出Hack Success!!!!

接下来我们用溢出来构造流程,让程序执行hack函数。

思路:
    char buf[20]; 是20个字节的空间,因为他是个临时变量,所以他应该是用ebp-xxx来定位。
    假设 [ebp-xxx] = buf地址
    那么我们需要覆盖到的是返回地址,一般是在[ebp+4]
    而这里strcpy允许我们任意的输入任何长度的字符串(造成漏洞的原因)
    我们这里只要把[ebp+4]给覆盖了就行,所以我们在输入20个字符串后,再继续输入4个字符串会把[ebp+0]覆盖掉,因为溢出。
    接着继续输入4个字符串,(28个字符串),就会把[ebp+4]也给覆盖掉,就覆盖到返回地址了。
    程序ret的时候,就能跳到我们28个字符串中最后4个字符串构造的地址中去了。

因为我们这里调试出来是[ebp+20]才是返回地址,而且这里buf是[ebp-0x1c],0x1c=28,所以28字节刚好覆盖到ebp,那么再加20就覆盖到返回地址,所以长度是28+20=48

覆盖前

image-20220619220038503

溢出覆盖后,溢出字符串1111111111111111111111111111111111111111111111112222

image-20220619220553737

哈哈哈,一开始我还以为开心的结束了能hack到了,结果狗日的...有坑啊这玩意。

image-20220619220910799

;这里把[ebp=8]地址设为栈顶,调试发现[ebp-8],刚好是char [20]字节后的数据,也就是溢出后的第一个字节地址。
0x565555e2 <main+74>                  lea    esp, [ebp - 8]
;然后这里把栈顶弹给ecx寄存器
0x565555e5 <main+77>                  pop    ecx
0x565555e6 <main+78>                  pop    ebx
0x565555e7 <main+79>                  pop    ebp
;这里又把[ecx-4],也就是[ebp-8]栈顶-4位置堆栈里面的 值 ,设置为新的esp,然后ret返回。
0x565555e8 <main+80>                  lea    esp, [ecx - 4]
0x565555eb <main+83>                  ret
所以这里的思路是,我们可以来控制ecx寄存器,因为ecx寄存器是由[ebp-8]地址的值赋值过去的,这里刚好是我们溢出覆盖到的最开始4个字节,所以我们可以控制这个地址,然后让这个地址指向偏移-4位置,然后这位置里面的值是hack函数地址,即可hack成功!

哈哈,因为我自己出的题目,要求不能用pwntools工具,所以只能用ASCII码来构造,构造来构造去发现ecx的堆栈地址是0xFF这种开头的,这种ASCII码对不上,超过能显示正常字符的ASCII码了,所以最后放弃了,我重新把题目代码改了下,改成了下面的样子。

题目要求:不能使用pwntools,让程序执行hack函数。

#include <stdio.h>
int _a = 1;
int _b = 2;
int _c = 3;
int _d = 4;
int _e = 5;
int _f = 6;
int _g = 7;
int _h = 0x5655556d;
int _i = 8;
int _j = 9;

void hack()
{
    asm("mov esp,0xffffd57c\n");
    printf("Hack Success!!!!\n");
    asm("mov ebx,0\n");
    asm("mov eax,1\n");
    asm("int 0x80\n");
}

int main()
{
    printf("Hello,Please Start Hack!\n");
    char buf[20];
    scanf("%s",buf);
    return 0;
}

解题思路:

这题目不同电脑可能运行效果不一样,因为我把地址写死了,我这里把hack函数地址写到了全局变量,而且故意是第8个全局变量,因为这位置刚好是 .data段中地址是 可以用ASCII码来显示的,然后我在hack函数开头用了一个汇编设置了栈顶,因为不设置的话调用printf函数会失败,最后用汇编调用int 80(中断),功能号1 exit来强制退出程序,让其能显示出Hack Suucess字符串。

image-20220619223032330

因为构造中是要[ecx-4]才是返回地址,所以我们要填入的地址是0x56557028,字符串是VUp(

image-20220619223204751

因为内存中是大端存储,我们要反过来,改成(pUV

最后加上20个字符串用来做溢出,payload如下。

Payload:

11111111111111111111(pUV

image-20220619223529706

调试图:

image-20220619223628759

image-20220619223727379

image-20220619223818300

Pwn菜鸡小分队

最后感谢大家的阅读,本菜鸡也是刚学,文章中如有错误请及时指出。

大家也可以来群里骂我哈哈哈,群里有PWN、RE、WEB大佬,欢迎交流

img

posted @ 2022-06-19 22:43  VxerLee昵称已被使用  阅读(1053)  评论(6编辑  收藏  举报