srop

Sigreturn Oriented Programming,依然是一种很好用的rop技术啦!

srop

只需要一个syscall指令地址,一个足够长的buffer overflow与可控的eax(eax是函数的返回值,所以控制起来比较容易),就可以达到控制任意寄存器的目的!

srop原理

当内核将signal dispatch给user mode,会将所有寄存器入栈,在执行sigreturn system call时会恢复寄存器:

恢复时,即返回地址为rt_sigreturn,它内部再调用sigreturn,后者会将存储在栈上的寄存器值填回寄存器,此时栈上的结构如下:

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
struct sigcontext_32 {
__u16 gs, __gsh;
__u16 fs, __fsh;
__u16 es, __esh;
__u16 ds, __dsh;
__u32 di;
__u32 si;
__u32 bp;
__u32 sp;
__u32 bx;
__u32 dx;
__u32 cx;
__u32 ax;
__u32 trapno;
__u32 err;
__u32 ip;
__u16 cs, __csh;
__u32 flags;
__u32 sp_at_signal;
__u16 ss, __ssh;
__u32 fpstate; /* Zero when no FPU/extended context */
__u32 oldmask;
__u32 cr2;
};

/*
* The 64-bit signal frame:
*/
struct sigcontext_64 {
__u64 r8;
__u64 r9;
__u64 r10;
__u64 r11;
__u64 r12;
__u64 r13;
__u64 r14;
__u64 r15;
__u64 di;
__u64 si;
__u64 bp;
__u64 bx;
__u64 dx;
__u64 ax;
__u64 cx;
__u64 sp;
__u64 ip;
__u64 flags;
__u16 cs;
__u16 gs;
__u16 fs;
__u16 ss;
__u64 err;
__u64 trapno;
__u64 oldmask;
__u64 cr2;
__u64 fpstate; /* Zero when no FPU/extended context */
__u64 reserved1[8];
};

于是可以构造sigcontext结构,ax,bx等存参数,pc存系统调用指令地址,于是就可以控制全部寄存器并进行系统调用了~

vdso

上面已经提了原理,现在实现起来还要做些事–找到syscall gadget,动态链接时syscall一般不会出现在给的binary中,libc里面的一般位置都是不固定的,于是就需要这个知识点了:

这个图中是sigreturn这个gadget的位置,可以看到在32位下是vdso,64位下只有libc了,但是其实sigreturn本来就是一个系统调用,而且没有参数,于是只需要在32位下将eax设置为119,64位下将rax设置为15,再使用syscall指令即可,于是须要的就是syscall,下图就是比较通用的情况了:

在低版本的64位Linux中,vsyscall这片区域的位置是固定的,当然是最好用的了,而高版本的就是位置不固定的vdso了。vdso和vsyscall都是用来加速一些并不需要太高权限的系统调用的,他们最大的区别就是前者的位置是不固定的,于是前者更加安全,可以使用gdb的dumpmem vd vdsodumpmem vs vsyscall将其dump出来观察。
现在主要的问题就是vdso位置不固定了,但是幸运的是,vdso的伪随机是很比较弱的:


x86: 只有⼀個 byte 是 random 的,所以有 1/256 之⼀的機率猜
x64:在有 pie 的情況下,只有 11 bit 是 random 的
CVE-2014-9585
linux kernel 3.18.2 後已修正到 18 bit

于是爆破总是最粗暴的方法!当然另外的方法就是通过泄露 __libc_stack_end得到栈起始地址再得到auxv再得到vdso的地址。

pwntools工具

emmm,比直接构造要简单一点点,只需要注意下部分寄存器的值不能随便改动即可:

1
2
3
4
5
## 在有明显控制eax,有int80时优先使用srop
context.kernel = 'amd64'
rop = ROP(binary)
binsh = binary.symbols['binsh']
rop.execve(binsh, 0, 0)

也可以手动来:

1
2
3
4
5
6
7
8
9
## 这里的寄存器不能随便填,emmm,没填完
frame = SigreturnFrame(kernel='amd64')
frame.eax = constants.SYS_write
frame.ebx = constants.STDOUT_FILENO
frame.ecx = binary.symbols['message']
frame.edx = len(message)
frame.esp = 0xdeadbeef
frame.eip = binary.symbols['syscall']
## 使用str(frame)可以转换成字符串形式

small-bin

emmmmm,名字是乱取的,因为这不是某道题[4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;;;安全:ASLR+NX
section .text

global _start
jmp _start
vuln:
sub rsp, 8
mov rax, 0 ; sys_read ;Addr1
mov rdi, 0 ;Addr2
mov rsi, rsp
mov rdx, 1024
syscall ;Addr3
add rsp, 8
ret

_start:
call vuln
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall

观察发现功能很简单的,缓冲8bytes,最大读入1024bytes,明显的溢出但是利用起来似乎有些困难,思路是:

  1. 第一次输入sh\0,并且溢出ROP为:Addr1 + Addr2 + Addr1 这样可以再次输入并且返回Addr2
  2. 第二次输入一个字符,此时rax置为1,返回到Addr2
  3. Addr2时参数是syscall(rax=1,rdi=0,rsi=rsp,rdx=1024),即从当前栈输出1024字节,这里有个知识点就是stdin与stdout指向同一个字符设备:
  4. 这样就能够读到sh\0的地址啦,因为栈上有一些指向栈的地址
  5. 此时再次到Addr1,可以输入一个sigFrame到栈上,它的作用是执行execv,即rip=syscallAddr,rax=constants.SYS_execve,rdi=shAddr,rsi=0,rdx=0,这次输入的返回地址写为Addr1+Addr3
  6. 到这里,再次再次输入15个字符,返回到Addr3,此时rip=syscallAddr,rax=15即调用sigreturn,调用时将之前的fake sigFrame放入寄存器,并且执行rip就可以拿到shell啦。

这就是总体思路,但是有一些细节需要注意,就是写入的时候可能会覆写掉之前的数据,这里要么尽量靠下写要么尽量靠上写即可。
另外,这里坐着的另一种思路也是很巧妙的,答题读写思路一样,但是他直接读到了auxv,从那里面找到了栈地址与vdso的地址,有了vdso就可以利用里面的代码构造gadgets啦,由此来构造普通的rop。

defcon-2015-fuckup

defcon的题就是直接,明说了漏洞在那里,限制是静态编译,NX保护,ASLR,代码与数据的地址是伪随机的,但是可以知道上一次的地址,经过分析可以直接带text里面找到execv的gadget,但是不知道它的地址,于是首先需要得到text的地址,就是第一种解法,使用z3约束求解,然后对于aslr的另一种通用解法是部分覆盖,在这里也能用:

1
2
3
4
5
6
7
do
{
hasRead = readin(0, addr, total - count); //一直从一个位置开始写
if ( hasRead != -1 )
count += hasRead;
}
while ( count < total );

现在记录第三种解法,即srop,这里最关键的就是需要知道vdso的地址,不过还有个问题就是溢出位数不够:

1
2
3
4
5
6
7
8
9
unsigned int bufOverFlow()
{
char addr; // [esp+0h] [ebp-12h]
char var_4; // [ebp-4h]
write(1, "Input buffer is 10 bytes in size. Accepting 100 bytes of data.\n", 0x3Fu);
write(1, "This will crash however the location of the stack and binary are unknown to stop code execution\n", 0x60u);
sub_80481A6();
return myRead(&addr, 100);
}

于是溢出的第16h字节才是返回地址,再之上能用的空间不足以容纳一个sigcontext(80bytes),于是通用的方法是先调用read,将后续的写入一个位置可知的地方,再迁移栈到那一个位置继续rop,

参考

[1]http://ytliu.info/blog/2015/12/02/sigreturn-oriented-programming-srop-attack-gong-ji-yuan-li/
[2]https://tc.gtisc.gatech.edu/bss/2014/r/srop-slides.pdf
[3]http://www.newsmth.net/bbsanc.php?path=%2Fgroups%2Fcomp.faq%2FKernelTech%2Finnovate%2Fsolofox%2FM.1222336489.G0
[4]http://v0ids3curity.blogspot.jp/2014/12/return-to-vdso-using-elf-auxiliary.html