几种经典的ROP手段简述
ROP
ROP一般指的是借助于栈缓冲区溢出手段,修改调函数栈帧中的返回地址。在程序进行返回的时候,会被转移到攻击者指定的地址,执行指定地址处的函数代码。从而形成控制流劫持。而Gadget,指的是能够用来起到攻击作用的,且程序中原有的代码片段。
比如,攻击前的栈结构如下
高地址
return address
...
低地址
现在,...中的某一个缓冲区发生溢出,将栈修改成了
高地址
gadget3 address
gadget2 address
gadget1 address
...
低地址
其中,gadget1、gadget2、gadget3都是具有下面格式的代码片段:
# do something
ret
下面模拟函数的执行过程。在函数体执行完毕后,栈中只剩下三个gadget address。ret指令的作用是:
- 从栈中弹出一个地址
- 跳转到这个地址(通过修改rip)
于是,gadget1 address被弹出。进入到“do something"部分。然而,”do something“这里又有一个ret,于是又会返回到gadget2 address,最终会跳转到gadget3 address。我们发现,gadget1、gadget2、gadget3对应的代码片段都可以得到执行。于是,这些gadget在一起构成了一个执行序列,我们称为ROP Chain。
实际上,只要我们找到种类足够多的ROP gadgets,就可以构成图灵完备的原子指令,然后用这些原子指令组成ROP Chain,在目标机器上执行任意代码(听起来就可怕)。
BROP
上面的ROP需要攻击者对程序的内存分布有所了解才能执行。这意味着程序的源代码或者二进制代码需要被攻击者知道(常见于开源软件)。对于一些代码没有公开的程序(如一些网站服务器程序),如何使用ROP攻击,这是BROP(Blind ROP)研究的内容。BROP论文原文很容易找到,非常精彩。BROP非常有趣。我后面会更新复现利用BROP攻击旧版NGINX服务器程序。
BROP攻击的最终目标是dump出.text段。这可以通过write系统调用来实现。一旦dump出了.text段,那就可以转换为普通的ROP来攻击了。所以最终目标是执行一个系统调用
sys_write(socket, buffer, length)
我们需要通过弹栈来实现寄存器修改,以传递参数,所以需要这样调用:
pop rdi
pop rsi
pop rdx
pop rax
syscall
实际上最后两个指令可以替换成call write一条。所以又可以写成
pop rdi
pop rsi
pop rdx
call write
然后任务就是为这四条指令分别找到gadget即可。
读栈(Stack reading)
BROP攻击可以完美对抗栈金丝雀保护(Stack canary)以及栈随机化(ASLR,地址空间随机化)。这得有益于BROP提出的Stack reading方法。
首先需要读取到缓冲区溢出的长度。常见的带有缓冲区溢出漏洞的程序(比如Nginx),的栈布局如下。
高地址
return address
frame pointer
canary(8 bytes)
buffer
低地址
我们只需要不断填缓冲区,当破坏掉canary的时候就会crash掉。于是我们可以知道缓冲区的大小。然后我们开始枚举canary的第一个字节,1到256。然后将这一个字节覆盖(后面的字节不动,这里很巧妙,避免了穷举)。如果这个结果不是canary的正确值,那么程序依然崩溃。如果是,那么说明我们知道了canary的第一个字节。然后开始枚举第二个字节。同样,后面的字节不动,覆盖掉第二个字节。以此类推。这样我们可以爆破出整个canary。这样做速度远远大于穷举手段。穷举手段需要穷举\(256^8\)个数字,而这样最多只需要进行\(256\times8\)种情况。
同样的手段,我们也可以爆破出frame pointer和return address。不过一般用不着。
获取stop gadget
stop gadget实际上指的是,攻击者可以知道已经进入了stop gadget的gadget。比如什么呢?如果在一个特定的函数中,有一个特定的输出。攻击者可以将stop gadget选为这个输出函数。一旦攻击者看到了那个输出,就知道代码进入了stop gadget。stop gadget主要辅助后面的攻击流程。直接扫描一编整个.text段即可知道stop gadget的位置。
前两条gadget
对于rdi和rsi的gadget很容易找。方法是借助于下面这样的gadget(被称为BROP Gadget):
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
这个gadget只要稍微处理一下,就可以被“分裂”成设置rsi、设置rdi的那两个Gadget。从0X7偏移开始读取这段汇编对应的机器代码,将会对应
pop rsi
pop r15
ret
从0X9汇编开始读取,就会对应
pop rdi
ret
这样,前两个gadget都有了。
下面问题来了,BROP Gadget怎么找?这个Gadget有一个特殊之处,它弹出六个寄存器。如果栈是这么分布的
高地址
... 这里也要放点0
0
0
0
stop_gadget
0
0
0
0
0
0
低地址
因为pop指令一般都是集中在程序的尾部。后面紧跟ret。如果从这六条中的第一条指令(pop rbx)的起始地址开始执行,那么将会顺利进入stop_gadget。而如果从第二条指令起始位置开始执行,以及更往后的指令,那么一定都是进程崩溃(因为访问了不可访问的0地址)。可以通过这个明显的特征来辨别出BROP Gadget。
找rdx
pop rdx的gadget怎么找呢?论文中提出可以通过strcmp来找。strcmp接受两个字符串地址参数。在strcmp内部实现中,会把字符串长度存放在rdx寄存器中。所以,strcmp函数,其实可以操纵rdx寄存器。而且strcmp接受两个地址的特征,可以让我们很好地将它区别出来(下面会介绍strcmp的特殊之处)。那怎么找strcmp的gadget呢?论文想法非常巧妙,从PLT入手。首先找到PLT,因为PLT实际上是一传指令序列,每一个PLT项都有一个重要特征——地址16字节对齐,且前6字节、后10字节都可以正常运行。所以我们只要扫描所有16字节对齐的地址A,如果发现从A开始连续的一长段(每次+16字节)都满足从addr、addr+6开始均不会crash,那么A就作为是PLT的入口。
然后从PLT中遍历找出strcmp。strcmp满足这样的特性(bad表示非法内存地址,readable表示可读的地址):
- strcmp(bad, bad): crash
- strcmp(bad, readable): crash
- strcmp(readable, bad): crash
- strcmp(readable, readable): no crash
这是很特殊的一个特征。如果找到满足这个条件的,那就是strcmp了。rdi、rsi我们已经控制了。所以构造一个ROP Chain,调用strcmp,即可控制rdx寄存器。至此,我们可以控制三个参数寄存器了。
然后需要构造一定长度的字符串。这里我们无法人为构造了,因为没法爆破出当前栈的位置(只能爆破上一级的FP)。所以需要尝试让strcmp的字符串地址参数指向不同的readable的地址,比如.text段的一些地址(因为总能碰上连续很多非0,然后跟着一个0的那种),然后调用write看看返回多少字节的数据。找一个返回字节数比较大的“字符串地址”即可。
找call write
这个仍然是在PLT当中找。找write比较清晰,只要从PLT里面一条一条试,如果调用完后,攻击者接收到了数据,那说明是调用了write。而socket的fd可以通过暴力穷举得到。因为一个进程最多只能有1024个fd。
完全控制
现在我们可以完全控制write的调用以及所有参数的传递。至此,通过调用write,我们可以分块地逐步导出整个text段。二进制源码在手,任务就转换普通的ROP攻击了。
SROP
Sigreturn ROP攻击也是一种很棒的攻击手段。Linux内核的Signal机制网上有很多资料,可以了解。此外我的博客提供了Linux内核Signal机制源码分析。
Sigreturn ROP主要借助于Linux的系统调用rt_sigreturn来进行。在Linux内核中有一个rt_sigframe结构体。信号处理程序被触发之前,Kernel会先将陷入内核态前用户态寄存器值、返回地址等信息放入Sigframe。这个返回地址,实际上是rt_sigreturn系统调用。然后信号处理程序触发之后,会返回到rt_sigreturn中,rt_sigreturn会从Sigframe中恢复寄存器值。
换句话说,rt_sigreturn实际上会从用户态栈上取值放入寄存器中。这对攻击者简直是极大的便利!我们正愁寄存器难以完全控制呢。而Sigframe能够控制所有寄存器(包括rip之类的,这意味着同时可以劫持控制流)。而rt_sigreturn是一个系统调用。我们没有必要真的整出信号,只需要劫持控制流到rt_sigreturn系统调用即可。
SROP的方法也非常简单。首先在栈上伪造一个Fake Sigframe。然后利用缓冲区溢出等方式构造一个ROP chain,里面包含一个设置rax的gadget,和一个syscall gadget。振奋人心的是这个syscall后面不需要接着ret,因为控制流会被rt_sigreturn所更改,再也不会回来了。syscall这个gadget其实随处可见。而设置rax的gadget略微复杂一些,但是rax是返回值寄存器!我们可以想办法通过控制函数的返回值(比如找个strlen的gadget),构造一个长度为15的字符串,即可将rax设置为15。然后紧接着来一个syscall即可。