We covered another case of a binary vulnerable to buffer overflow but has some protections enabled such as NX and PIE. To get around these protections, we leaked a binary address and subtracted the address from a specific offset found by subtracting a start of the user input in memory from the start of the stack. Then we build the ROP chain consisting of GOT, PLT, setvbuf, system and /bin/sh offsets so that these gadgets will execute in the memory stack and return shell.. This was part of HackTheBox Pwnshop Intro to Binary Exploitation track.
Initial Analysis
I start by downloading the challenge files and turning on the instance to interact with the binary. I use the file
command to determine the nature of the pawnshop
file. It’s a 64-bit executable and is stripped (meaning debugging symbols are removed). I check the security protections. NX (Non-eXecutable stack) is enabled, meaning the stack cannot contain executable code. PIE (Position Independent Executable) is also enabled, meaning the binary and its dependencies are loaded into random memory locations each time, making exploitation harder.
Understanding the Binary with Ghidra
I open the binary in Ghidra to analyze its functions. The buy
function has a buffer overflow vulnerability. It allocates 72 bytes for a variable but reads about 80-82 bytes from the user, allowing for an overflow. I note the offset of the buy
function (132a). The sell
function has a potential memory leak. It uses the read
function, which doesn’t add a null byte to the end of the string. If used with %s
(to read a string), this can lead to leaking memory addresses. When I interact with the sell
function and provide input, I observe unrecognizable strings, which are likely leaked binary addresses.
Exploitation – Leaking Addresses and Bypassing PIE
My exploit code first interacts with the sell
function to trigger the memory leak. I capture the output after the prompt and encode it to hex to reveal the leaked address. To bypass PIE, I need to calculate the base address of the binary. I use GDB (with the Jeff plugin) for this. In GDB, I find the memory mapping and identify an offset by subtracting the start of the program’s memory from an address where my input (“test”) is stored. The base address is then calculated by subtracting this offset from the leaked address obtained earlier.
Crafting the ROP Chain
With PIE bypassed, I start building a ROP (Return-Oriented Programming) chain. ROP chains use “gadgets” (small sequences of instructions ending in ret
) to execute code. I use ropgadget
to find useful gadgets in the pawnshop
binary. I look for a gadget that subtracts from RSP (Stack Pointer) to ensure enough space for other gadgets (e.g., sub rsp, 0x28; ret
). I note its offset (1219). I find the offset for pop rdi; ret
(13c3), which is crucial for ROP chains to control function arguments. I also need offsets for PLT (Procedure Linkage Table) entries like puts@plt
and GOT (Global Offset Table) entries. I use objdump
to find the offset for setvbuf@plt
(1030) and puts@plt
. The offset of the vulnerable buy
function (132a) is also included.
Finding Libc Offsets
To achieve ret2libc
, I need the addresses of functions like system
and strings like /bin/sh
within libc. Initially, I find these offsets locally on my machine using commands like readelf
and strings
on my system’s libc. However, these local offsets won’t work on the remote challenge server. I use an online libc database (like libc.blukat.me) to find the correct offsets for the target architecture by providing a known libc function (e.g., setvbuf
) and its leaked address. I find the offsets for system
, setvbuf
, and /bin/sh
from this database.
Final Payload and Execution
The libc base address is calculated by subtracting the offset of a known libc function (e.g., setvbuf
) from its leaked runtime address. I construct the final payload by combining:
- Padding to overflow the buffer.
- The addresses of the ROP gadgets (base address + gadget offset).
- The address of
puts@plt
to leak a libc address. - The address of the
buy
function to return to for another overflow. - Then, in the second stage of the overflow:
- The address of
pop rdi; ret
. - The address of
/bin/sh
in libc. - The address of
system
in libc.
- The address of
I define the target IP and port and execute the Python script. The exploit successfully gives me a shell on the remote server, and I can then read the flag.
Technical Commands
file pawnshop
gdb pawnshop
vmmap
(GDB command, often used with Jeff plugin)grep test
(GDB command, often used with Jeff plugin)p/x <address1> - <address2>
(GDB command)ropgadget --binary pawnshop | grep rsp
ropgadget --binary pawnshop
ropgadget --binary pawnshop | grep "pop rdi"
objdump -M intel -d pawnshop | grep setvbuf
objdump -M intel -d pawnshop | grep "puts@plt"
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep setvbuf
python exploit.py
ls
cat flag.txt
Flag
HTB{th1s_is_wh@t_I_c@ll_a_g00d_d3a1!}
Video Walkthrough