StackOverflow
前段时间给CWL写了一个简单推文,存一下。
Stack Overflow—栈溢出
你听说过栈溢出吗?如果没有,或许你在编程入门时一定遇到过段错误(Segment Fault),栈溢出是程序漏洞的入门级漏洞,编程初学者很容易在使用c、c++误用带有栈溢出漏洞的函数、进而造成错误的内存访问,这是出现段错误的其中一个原因。
栈是一种先进后出(LIFO)的数据结构,其在程序函数调用过程和程序运行过程中广泛应用,每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。下面将以linux下x86系统为例,分析栈溢出的成因。
在学习栈溢出前,我们需要了解一下函数调用栈的相关知识。
在C语言程序中,函数调用经常是嵌套的,但机器寄存器的数量是有限的,假设我们在函数A中调用函数B,我们需要将A的参数信息保存下来,从而使程序执行函数B的代码,这个保存的方式就是栈帧:在同一个时间,每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame),栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
当程序调用一个新函数时,会建立一个栈帧,依次进行以下步骤:将寄存器数据压入栈中→将函数参数压入栈中→将该函数结束的下一条语句的地址压入栈中(返回地址)→将原函数的栈帧基址压入栈中→进行函数本身的栈利用操作
当该新函数运行结束时,会依次弹出该栈帧的数据,通过EBP将栈基址恢复为上一层函数的栈基址,程序再跳到返回地址指向的代码处继续运行。
在此过程中,最关键的部分在于【返回地址】,如果我们使用某些操作将返回地址修改掉,那么在函数结束返回原函数时会失败,进而跳转到被修改的地址上,这就属于其中一种最入门的栈溢出漏洞。
那么栈溢出漏洞利用的原理就很明显了:利用栈中数据的溢出篡改程序进程,进而劫持程序或破坏程序。
接下来将以一个基础例子(Buuctf—jarvisoj_level0)来描述利用read函数的劫持过程。
read函数说明:read(int fd, void * buf, size_t count)会把参数fd 所指的文件传送count 个字节到buf 指针所指的内存中. 若参数count 为0, 则read()不会有作用并返回0.
若该程序定义的输入字节数大于buf指向的内存空间,程序也将照样执行,我们将可以借此修改栈帧中的返回地址。
使用反汇编程序ida查看题目所给的可执行文件
main函数:
进入vulnerable_function()
发现存在read函数,buf长度128,却能读入0x200个字节,我们回顾一下栈帧的压入过程
由于栈是由高地址向低地址生长,当往buf中存入0x200字节内容时,溢出的内容会覆盖至后面的返回地址上,因此我们可以借此修改返回地址,实现改变程序结束该函数后运行的代码地址。
继续查看反汇编代码,发现有一个后门函数(可直接劫持程序的函数,往往叫systemcall,后门函数常出现于安全类赛事中,用于程序漏洞利用的考察)
查看该后门函数的地址并记录
因此,我们现在需要做的事情就非常明确了:输入大于128字节的数据,覆盖栈中存储返回地址的空间,修改为我们需要程序运行的函数的地址,此处为0x400596,使程序在read函数结束后跳转到后门函数中,实现程序劫持。
使用python写一个小的脚本实现
1 |
|
这只是体现栈溢出原理的一个很基础的例子,常用的存在该类型溢出的函数还有gets()等,实际利用中,常常搭配ROP链(利用程序中已有的汇编代码片段进行组合成为攻击代码链,以此劫持程序)、shellocode(往可执行栈堆中注入攻击代码,利用栈溢出跳转执行攻击代码实现劫持)等实现程序劫持,若想更深入地了解,可以访问ctf wiki进行进一步的学习。
关于栈溢出的防范,还有一些应对栈溢出的方法,如NX栈堆不可执行保护(防范shellcode)、canary(在栈中使用标志位,通过标记数字是否被篡改判断是否产生栈溢出)、ASLR地址随机化(使攻击者无法直接注入静态地址),但这些方式都可以通过一定的方式进行绕过,因此最有效的方法即慎用甚至不用危险函数。