围绕内存漏洞的攻防博弈已经持续了近三十年,控制流劫持攻击也越来越多的成为攻击者的常用手段。随着防护技术的发展,针对控制流的攻击变得愈发困难。而不通过劫持控制流,而是针对数据流来进行攻击的方式,如Non-control data(非控制数据)攻击虽然显示出了其潜在的危害性,但目前对针对数据流的攻击还知之甚少,长久以来该攻击手段可实现的攻击目标一直被认为是有限的。实际上,非控制数据攻击可以是图灵完备的,这就是我将为大家介绍的攻击DOP攻击。
背景知识:Non-control data(非控制数据)攻击
控制流保护机制旨在保护目标程序控制流不被篡改,但是控制流转移指令(如 ret , jmp)往往不直接使用内存变量,这就意味着控制流保护机制(如CFI)往往无法将内存变量纳入保护。非控制数据攻击就利用了这一点,攻击者直接控制数据流来实施相应攻击。比如鼎鼎大名的的心脏出血(heartbleed)漏洞就是典型的非控制数据攻击。又比如下图代码:
如果攻击者能够控制pw_uid变量,就能够实现提权操作。
DOP相关概念
DOP全称为 Data Oriented Programming,是新加坡国立大学的胡宏等人于2016年提出来的一种针对数据流的非控制数据攻击方式,核心思想是用各种各样的内存行为来模拟相应操作。
正如前述所提到的那样,非控制数据攻击往往只能用来实现信息泄露(心脏出血)或者是提权,其本身不能实现复杂的操作。但DOP打破了这一成见,证明了非控制数据攻击本身就能实现图灵完备的攻击。
类似于ROP,DOP攻击的实现也依赖于gadgets。但二者有以下两点不同:
- DOP的gadgets只能使用内存来传递操作的结果,而ROP的gadgets可以使用寄存器。
- DOP的gadgets必须符合控制流图(CFG),不能发生非法的控制流转移,而且无需一个接一个的执行。而ROP的gadgets必须成链,顺序执行。
为了更好的说明DOP的gadgets特性,胡宏等人定义了如下语言MINDOP:
其展示了DOP的gadgets如何在受限制的语义下实现算术、逻辑、赋值、加载、存储、跳转、条件跳转等操作。正如前述所提到的,DOP的gadgets不能使用寄存器,所以DOP转而用内存来模拟寄存器(也就是上图中的*p等就对应了一个虚拟的寄存器)及各种操作。接下来将结合ProFTPD(一个开源的FTP服务器软件,也是之后实际进行攻击时的目标程序)源码来进行说明。
1.算术运算
1)加减法
用gadgets来模拟加减法是比较容易的,因为目标程序中存在大量的目标代码片段。比如如下代码:
通过该代码就能完成一次加法,减法同理。
2)乘法
乘法的模拟较为复杂,但是如果存在条件跳转,则乘法的模拟也可以完成。比如要完成a×b,可以将b进行比特位的分解,根据当前比特位来进行相应的跳转,同时在每一步都进行a的自加(可用左移位来实现)即可。简单来说就是用加法来实现乘法。
2.赋值
DOP中的赋值操作相当于是从内存某地值读取数据存储到另一地址中,比如如下代码:
就完成了一次赋值操作。
3.加载和存储
由于DOP不能使用寄存器,所以加载和存储操作都是通过指针解引用来模拟的。如以下代码:
其实现了加载操作。
又比如以下代码,其实现了存储操作:
4.跳转操作
跳转操作的实现需要依赖于一个内存错误(如栈溢出)的发生。关键就是要通过某处内存来模拟处一个虚拟的指令寄存器(pc),使得通过该内存来进行“跳转(并非真实跳转,只是转而执行某处代码)”。比如以下代码:
pubf -> current指向了恶意输入的缓冲区。在每一次循环迭代中,代码从该缓冲区读取一行,然后在循环体中处理它,因此这个指针可以用来模拟虚拟PC指针。如果pbuf->current被控制,精心构造相应的值,那么buf处就会发生相应的改变,进而影响其相邻位置,最终使得函数参数cmd被控制,执行相应操作。
同ROP类似,DOP也需要有一个gadgets调度器(dispatcher)。DOP中的调度器指的是能够让攻击者重复的调用gadgets,并且让攻击者选择具体的调用哪一个gadget的指令序列。比较常见的就是带有一个选择器(selecor, 让攻击者选择具体的调用哪一个gadget,通常是一处内存错误发生点)。每次迭代将会使用前一次的迭代中使用的gadget输出,并且将本轮迭代使用的gadget的输出输入到下次迭代,同时选择器改变下次迭代的加载地址为本次迭代的存储地址。选择器由攻击者通过内存错误控制。如上图中第2-7行,它将循环的处理cmd请求,通过构造相应的cmd,就能引入相应的gadgets。
接下来以如下代码片段对各个模拟出的操作效果进行说明:
可以看到,该代码片段没有调用任何敏感的代码指针。其CFG图如下:
6、7行即为调度器,其中第7行为选择器。即使通过第7行的栈溢出进行覆盖,看起来似乎也并没有什么威胁。但是再考虑如下代码:
buf处的内存排布如图:
通过栈溢出可以将其变为:
假如p是list地址,q是addend地址,n是srv地址的话,就会执行如下操作:
而进行如下覆盖;
令m是STREAM字符串地址的话,就等效于:
如果再循环体中各自进行一次,最终效果等效于:
while(condition)
{
if(list==NULL) break:
srv=list;
list->prop+=addend;
list=list->next;
}
实际上就实现了通过控制数据流来“调用”updateList函数,但实际的控制流并未发生转移,因为并没有一个真正的函数被调用了,控制流仍处在循环体内。
至此,DOP的模型已经明确了,如下图所示:
内存错误将会激活调度器,在循环loop中通过控制选择器来选择执行相应的gadgets。
DOP的准备工作
经过前述的说明,DOP的基本概念已经清晰,接下来要解决的就是以下几个问题:
- 如何定位gadgets
- 如何寻找调度器
- gadgets的拼接
对于前两点来说,一个静态分析工具的实现是可能的。因为DOP的gadgets的特征是非常明确的,其必须符合MINDOP语义,而且必须是如下模式:
加载-其他操作-存储
而调度器也有明显特征。
为了避免对源码进行分析,可以把目标程序编译成LLVM IR形式,然后通过胡宏等人提供的工具DOP-StaticAssist(https://github.com/melynx/DOP-StaticAssist)进行gadget(包括调度器)的定位。
对于最后一点来说,暂时只能依靠直觉(胡宏等人的原话,瞬间玄学)来进行不断的尝试。
DOP攻击的构造
DOP的准备工作已经完成,接下来就是具体攻击的构造。由以下几个步骤组成:
- 从目标程序中定位发生内存错误的函数,然后寻找包含该函数的调度器,收集用于攻击的gadgets。
- 以预期的恶意MINDOP操作作为模板,每个MINDOP操作可以通过相应的功能类别的任何gadgets来实现。可以根据优先级来进行选择。
- 一旦我们得到了实现所需的功能的gadgets,接下来要做的就是验证每个拼接。构造输入到程序,触发内存错误,激活gadgets。如果攻击不成功,我们回滚到步骤2,选择不同的gadgets,并尝试再次拼接
攻击ProFTPD
为了直观的感受DOP攻击能够实现的目标,接下来将会对ProFTPD进行两种不同的攻击。该程序存在栈溢出漏洞(cve 2006-5815)。攻击时使用metasploit来进行,脚本可以在作者提供的虚拟机中查看(https://drive.google.com/file/d/0B_6p5h2gdgmoTV9ON0xYZWpMRTg/view)。要自己解决的问题只有一个:选择一个当前进程空间中的以0xb开头的可写地址,且内容不能为\0。所有攻击均在ASLR和DEP开启的情况下进行。
泄露密钥
该程序使用一个ssh密钥,DOP攻击可以泄露这个密钥。
开启ProFTPD服务:
可以看到,地址随机化已经开启。
进入msfconsole,输入如下命令:
use exploit/windows/ftp/proftp_sreplace_dop
set ftpuser ftptest
set ftppass ftptest
set rhost 127.0.0.1
然后进行如下操作:
此处0xb77430001即为上述所需地址。
密钥成功泄露:
实际密钥可以通过以下命令查看(字节逆序):
可以看到,泄露出的确实是真实密钥。
修改代码段内容为int 3
启动目标程序
地址随机化已经开启,LD_PRELOAD用于给gdb传递信息。
进入msfconsole,输入如下命令:
use exploit/windows/ftp/proftp_sreplace_dlopen
set ftpuser ftptest
set ftppass ftptest
set rhost 127.0.0.1
将gdb附着(attach)到正在运行的进程:
设置follow-fork-mode为child(调试子进程,父进程不受影响):
然后进行以下操作:
这里选用了另外一个地址0xb76a6001。
攻击完成:
查看当前代码段位置,已经被修改为int 3:
而目标程序相应位置处应为ret:
对目标程序代码段(不可写)的修改成功。