前言:本篇记录本次栈溢出题目解题过程中遇到的问题排查与踩坑经历,梳理解题思路的调整过程,完整记录从碰壁到最终完成利用的全过程。
0x01 什么是栈迁移
我们知道,平常我们利用栈溢出攻击获取shell时,需要把整套攻击数据全部写到栈上,这往往需要足够的栈内存空间。然而,设想一下,如果题目严格限制栈缓冲区的尺寸,留给我们能够自由写入数据的空间十分狭小,而我们需要构造的一整套攻击数据体量较大,现有空间根本存放不下该怎么办呢?
面对这种棘手的情况,就需要用到栈迁移。所谓栈迁移,也就是借助手里仅有的溢出能力更换程序运行栈的位置,把栈切换到一块空间充裕的区域,在这块区域存入我们的完整攻击数据,拿下shell。
0x02 如何实现栈迁移
整体的操作手法其实十分简单,借助栈溢出,我们将原本储存EBP的内容覆盖成我们打算迁移过去的目标地址往前偏移4字节的位置,再将原本的返回地址覆盖成leave;ret;这一条指令的地址。

那么,我们为什么要这么写呢?这套布置背后的运行逻辑,我们结合栈内数据变化一步步来看:
- 首先,我们先回顾leave和ret指令:leave = mov esp,ebp; pop ebp, ret = pop eip。
- 接着, 我们思考函数执行结束时,会进行leave和ret指令。CPU执行leave时,先将ebp的值赋给esp,此时esp指向ebp的位置,再pop ebp,这是会将我们填充的new_addr-8的值赋给ebp,此时ebp指向new_addr-4的位置。
- 然后,CPU执行ret,也就是将我们填充的leave;ret;弹入eip。
- 最后,CPU执行eip的内容(leave;ret;),在执行leave时,esp指向ebp的位置(new_addr-4),在执行ret时,由于pop eip,所以esp+8,此时,esp便指向了我们迁移到的新地址new_addr。
这样,我们就完成了一次栈迁移。结合图片或许可以更好的理解。

0x03 栈迁移实战
IDA分析构思整体思路
首先,借助IDA对本题进行逆向分析。

这里有一个vul_function(),我们点进去发现:

这里存在溢出点:read读到buf这个地方,距离ebp只有0x18字节,但我们可以填入0x20字节,所以存在栈溢出,也就是说可以覆盖ebp和return_address。
但是,我们又会发现,0x20-0x18=8字节,也就是说,我们只能够覆盖ebp和返回地址而写不下完整的ROP链,因此需要栈迁移。
最初,我准备直接把栈迁移到buf缓冲区上,然而运行失败了,调试过程中发现buf的内存地址每次运行都会发生改变。这是程序开启了地址空间随机化ASLR保护,栈地址每次启动随机重定向,没办法固定定位,这套思路无法稳定利用,因此需要更换迁移地点。


那么,接下来需要思考的就是该迁到哪里,我们希望迁入的地址时可读可写的,地址是固定不变的,并且整片内存时空置空闲状态的。
在IDA中我们发现,程序中存在BSS段,BSS段默认RW,并且有充足的空闲内存空间。同时,程序没有开启PIE保护,所以BSS的地址从程序编译就写死了,完美符合我们的要求,因此,我们现在就可以迁移至BSS段。

解决了迁到什么地方,接下来就该解决往BSS段写什么的问题。我们发现,IDA左侧fucntion列表里加载了_libc_start_main,这也就代表程序运行时已经正常载入了libc动态链接库,自然而然我们就可以选用经典的ret2libc思路完成后续攻击。

下面要做的就是泄露地址,再结合漏洞函数内部的代码来看,程序中原生自带了write函数,write(文件描述符,要打印的内容,长度),我们可以借助此函数泄露地址。
现在的整体思路是:栈迁移至空间充足的BSS段后,依托BSS空间布置ROP链,调用write函数打印地址,拿到泄露地址后换算libc基地址,借此定位system函数和/bin/sh字符串地址。
Payload1:泄露地址


那么,我们先来布置第一轮的payload
- 首先用垃圾字节填满缓冲区,偏移可以在IDA中直接找到,或者借助pwntools中的cyclic工具生成一段具有特征的规律的测试字符串,将字符串发送至存在漏洞的程序,程序发生崩溃后查看此时寄存器EIP被覆盖到的四字节数值,即可精准计算出从输入开头到栈上返回地址需要填充的字节。
- 然后写入bss-4的地址,地址在上文中的图片里已标出。
- 最后填入leave; ret;的指令。
现在,我们来布置ROP链:填入write函数入口地址,让程序跳转执行write,执行完write函数后会回到返回地址,我们将返回地址填入main函数,以便第二轮利用。
接下来三个位置便是write的三个参数的位置,第一个参数fd文件描述符为标准输出1,第二个参数buf便是我们要打印的地址,我们让他指向write的GOT表,GOT表里存放着运行时libc中write函数真实的内存地址,第三个参数count代表读取输出的4字节数据,在32位环境下一个指针刚好4字节,正好完整取出这一条函数地址。

完成这些后,我们就可以发送rop和payload,利用sendafter函数,在接受”What is your name?”后将ROP传到BSS段,在接受”What do you want to say?”将payload送到漏洞处的缓冲区。
这样,我们就完成了第一轮的攻击流程:借助精心构造的 payload 触发栈溢出,依靠leave; ret指令完成栈迁移,将运行栈切换至空间充足的BSS段,随后执行布置好的ROP调用write函数,把GOT表中write的运行时真实地址回传到本地终端,顺利拿到了计算libc基址所必需的关键地址数据,第一轮地址泄露工作就此结束。此时程序已经返回main函数,整体程序状态回到最开始的状态,我们可以开始第二轮攻击。
Payload2:尝试写入system(/bin/sh)惨遭碰壁

在第二轮攻击开始前,我们可以先算出Libc的基址,用接受到的write函数的真实地址减去write函数在Libc表中的偏移,便可以求出libc的基址,方便后续使用。
下面,我们来布置第二轮攻击。理想情况下,因为我们已经找到Libc的基址,可以直接部署system函数调用并且传参/bin/sh字符串,拿到shell。

但是,在运行过程中我们发现,此路行不通。经过调试发现,问题的根源在于:BSS段执行system函数过程中,栈指针ESP会蔓延至.dynamic段,而dynamic段只读,不可写,导致触发写入报错。
因此,我们当下需要解决的问题是:更换一处能够正常承接 system 内部栈读写操作的合法原生栈区域。
那么,迁到哪呢?我们当然希望这块空间是可读可写的,地址已知的,生命周期稳定的合法内存空,最好的就是进程原生栈空间。恰好,libc中存在全局变量environ,该变量内部保存着栈上环境变量对应的内存地址,栈本身与生俱来具备可读、可写的权限,运行期间地址稳定不会被随意销毁覆盖,所以,我们将ROP写入这里最好不过了。

解决了写到哪的问题,那么,通过什么函数写呢?回看IDA中的程序逻辑,我们发现,程序内置了可用的read函数,read(文件描述符, 目标地址, 长度),该函数能够从标准输入接收我们传输的数据并写入指定内存地址,完全契合我们当下的需求。
至此,我们的思路是:再次调用write函数读出environ内部保存的栈地址,将该地址作为read函数的写入目的地址,再通过read把我们发送的ROP写入这片栈内存。这样,我们的问题就解决了。
重构Payload2: 获取_environ地址
我们来布置第二轮payload

第二轮攻击是为了打印_environ内部存储的原生栈地址,与第一轮打印write函数GOT表地址的思路相似度极高,只需要将write的第二个参数改成_environ 对应的内存地址即可,最后接受打印出的地址。
Payload3: 向environ写入执行载荷
我们来布置第三轮payload


第三轮Payload是为了把构造好的ROP攻击载荷写入获取到的栈地址之中,通过执行read函数达到我们想要的效果。
将read函数的目标地址写成stack_addr地址,然后将构造好的payload发送,这样payload就写到stack_addr上了。
在执行完 read 函数之后记得加上 sleep(1),留出时间让程序完整接收我们发送的数据,否则发包速度过快容易造成数据接收不完整,导致后续利用流程异常出错。
至此我们已经在原生栈空间内布置好了调用system的ROP,同时配置好了执行所需的参数,后续只需将程序执行流跳转至该地址,运行对应的代码,就能够成功拉起交互式Shell。
Payload4:执行利用链
我们来布置第四轮payload

第四轮Payload是为了调度执行原生栈中提前布置好的攻击,因此只需将栈迁移目标地址改为stack_address – 4即可。此时,ROP的内容已经不重要,只需要发送过去一个ROP而已。
将这些整理拼凑,就可以得到完整的EXP。


写到这里,就能成功获取程序的交互式shell啦!

小结
- 发现漏洞,确认采用栈迁移
- 计划迁移至buf,因其地址不固定作废
- 迁移至BSS段,调用write泄露地址,计算libc基址
- 计划在BSS段部署载荷,运行时栈侵入dynamic段崩溃,作废
- 更换思路,决定迁至_environ对应的原生栈区域
- 再次调用write,获取_environ原生栈地址
- 调用read向_environ写入载荷
- 重新栈迁移至目标原生栈,获取shell
评论(0)
暂无评论