HTB Void writeup
Challenge
Intended way
ret2dlresolve. It can be solved using pwntools in no time. Writeup (video): https://ctftime.org/writeup/36434
My way
Analysis
Okay, so let’s do something different. We know which version of GLIBC is running on the remote server because it is provided to us: GLIC 2.31. The binary has Partial RelRO (obviously so because it was supposed to be solved using ret2dlresolve). So we can overwrite got. The binary calls read() to get up to 0xc8 bytes from stdin into a buffer on the stack in the function vuln(), which overflows and allows us to take control of RIP. No more libc6 functions are called, so this is what the got table looks like:
[0x404018] read@GLIBC_2.2.5 → 0x7ffff7ee1780
Overwriting read
We can use my script genlist.py to see if we can overwrite read() with just one byte without leaks:
python3 genlist.py glibc/libc.so.6 > libc.so.txt
This time, write() and read() are further apart, so we cannot just overwrite read() with write() with one byte:
Functions grouped by 0xec700
0x000ec750:48:sym.__openat64_2
0x000ec780:139:sym.__read
unctions grouped by 0xec800
0x000ec820:147:sym.__write
We still need to find a way to overwrite read(). However, we need to overwrite more than one byte, and for that we cannot just use read itself, because we do not know its address yet. We are only certain that the last byte for write() is 0x20 and for read() is 0x80. Nothing else. But how about adding instead of overwriting? If we can find a ROP gadget that adds a value we control to a memory address we control, then we’re on!
Adding instead of overwriting
Using r2, we found the following ROP-gadget:
[0x00401122]> /R add
0x00401108 015dc3 add dword [rbp - 0x3d], ebx
0x0040110b 0f1f440000 nop dword [rax + rax]
0x00401110 c3 ret
Okay, we control RBX and RBP thanks to the ret2csu technique (this binary is built with __lib_csu_init() because it’s using GLIBC 2.31). So we need to add a value to read@got. Can we, say, transform read() into system right away? Let’s see how far they are:
gef➤ p read - system
$1 = 0xa6930
So read() is after system. But we can only add, we cannot subtract … or can we? Yeah: if we cannot subtract 0xa6930 from read, we can always add -0xa6930 to it! So:
gef➤ p/x -(read-system)
$3 = 0xfffffffffff596d0
Because our ROP-gadget allows us to add a double word (32 bits only) and not 64 bits, we can, in fact, use the value:
0xfff596d0
And that’s because the offset between read and write, expressed in negative form, fits in 32 bits:
gef➤ p read - system
$1 = 0xa6930
gef➤ p/x -0xa6930
$2 = 0xfff596d0
Exploit
And that’s all! We do not need anything else to spawn a shell. We can write our command into the .bss section first, then add the value 0xfffffffffff596d0 to read@got, pop the address of our command into RDI and call read@plt. Game over. Without leaks, without using the one gadget technique described here: https://github.com/sbencoding/htb_ca2023_writeups/tree/master/pwn/void. Just one simple addition!:
1payload = padding \
2 + p64(RBP) \
3 + ropAdd(binary.symbols["got.read"],system_offt) \
4 + ropShell()
Warning
I revisited this challenge in order to provide another cool example for this project, but I have to confess that at the time, when the competition was on, I wrote an exploit using ret2dlresolve. Yeah, I admit it: I was lazy. But I love revisiting old challenges afterwards, because it’s then when you can come up with something a bit different. This way you learn a ton!