# 8.5 Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks [paper](https://www.comp.nus.edu.sg/~shweta24/publications/dop_oakland16.pdf) ## 简介 * 上一代的攻击方式是通过代码注入来劫持程序控制流 * ROP和DOP都是图灵完整的 * 在程序的控制执行中执行一些简短的指令序列,使得模拟图灵机的具体操作成为可能,如赋值、运算等 ### 非控制数据攻击(Non-control Data Attacks) * 在一些环境上(如现代浏览器),仅仅是几个内存字节的破坏就可能导致远程攻击者的图灵完全攻击 * `printf`这样的格式化字符串函数,接受并解释格式化参数的这类函数,对格式化字符串“语言”来说,实际上也是图灵完整的解释器 * 如果非控制数据攻击可以允许攻击者完全控制格式字符串参数,那么攻击者可以构造有效的payload ### 数据导向编程的例子 Code1, 带有DOP Gadgets的FTP Server代码: ```c 1 struct server{ int *cur_max, total, typ;} *srv; 2 int connect_limit = MAXCONN; int *size, *type; 3 char buf[MAXLEN]; 4 size = &buf[8]; type = &buf[12]; 5 ... 6 while(connect_limit--) { 7 readData(sockfd, buf); // stack bof 8 if(*type == NONE ) break; 9 if(*type == STREAM) // condition 10 *size = *(srv->cur_max); // dereference 11 else { 12 srv->typ = *type; // assignment 13 srv->total += *size; // addition 14 } ... (following code skipped) ... 15 } ``` 上述代码不会在良性控制流中调用任何涉及安全的关键功能,因此漏洞仅破坏局部变量。 Code2, 函数将链接列表的整数字段递增给定值: ```c 1 struct Obj{struct Obj *next; unsigned int prop;} 2 void updateList(struct Obj *list, int addend) { 3 for(; list != NULL; list = list->next) 4 list->prop += addend; 5 } ``` #### MinDOP 最小DOP语言: | 语义 | C语言 | DOP Gadgets | | ------------- | ------------- | -------------------- | | 算术/逻辑运算 | `a op b` | `*p op *q` | | 赋值 | `a = b` | `*p = *q` | | 加载 | `a = *b` | `*p = **q` | | 储存 | `*a = b` | `**p = *q` | | 跳转 | `goto L` | `vpc = &input` | | 条件跳转 | `if a goto L` | `vpc = &input if *p` | 注:`p - &a; q - &b; op - 算术/逻辑运算; vpc - virtual input pointer` 在MinDOP中,实现了经精心选择的储存单元(不是硬件寄存器)的虚拟寄存器,在Gadgets的控制下下使用。 在概念上,数据导向Gadgets模拟了三种逻辑微操作,一是加载微操作,二是预期的虚拟操作语义,三是储存微操作;加载微操作模拟从储存器中读取虚拟寄存器操作数,储存微操作将计算结果写回虚拟寄存器。 每一个Gadgets的语义都和彼此不同,许多不同的x86指令序列足以模拟虚拟操作,由于x86指令集支持好几中寻址模式,只要微操作的顺序是正确的,不同的序列也可以正常工作,如指令`add %eax, (%ecx)` ,这一条指令就执行了加载、算术和存储三个微操作。 | C | `srv->total += *size;` | | :--- | :--------------------------------- | | ASM | `1 mov (%esi), %ebx //load` | | | `2 mov 0x4(%edi), eax //load` | | | `3 add %ebx, %eax //addition` | | | `4 mov %eax, 0x4(%edi) //store` | 数据导向Gadgets和代码导向中的Gadgets有两点不同,一是数据导向Gadgets需要使用内存传递操作结果,而代码导向Gadgets既可以使用内存,也可以使用寄存器;二是,数据导向Gadgets必须在一个合法的控制流中执行,且没有必要立即执行。 * 模拟算术运算: 如果语言支持条件跳转指令,那么可以更有效的模拟乘法运算(?)。MinDOP支持条件跳转,那么就可以做到检查一个值大于或小于一个常数。 * 模拟赋值运算: 在MinDOP中,赋值Gadgets从一个储存单元读取数据并直接存储到另一个储存单元,在这种情况下,我们可以直接跳过立即数的加载操作。 | C | `srv->typ = *type;`| |:---|:----------| |ASM|`1 mov (%esi), %ebx // load`| ||`2 mov %ebx, %eax // move`| ||`3 mov %eax, 0x8(%edi) // store`| * 模拟加载/存储操作: 加载和存储在C语言中需要内存的反引用,将一个寄存器作为地址并访问内存位置进行读取或写入。由于在DOP中,寄存器是内存模拟的,因此反引用操作通过两个内存反引用来模拟:第一个内存反引用模拟寄存器的访问,第二个内存反引用第一个反引用的结果(寄存器值)作为地址。 | C | `1 LOAD1: *size = *(srv->cur_max);` | | ------------ | ------------------------------------- | | | `2 LOAD2: memcpy(dst, *src_p, size);` | | | `3 STORE: memcpy(*dst_p, src, size);` | | ASM(`LOAD1`) | `1 mov (%esi), %ebx // load` | | | `2 mov (%ebx), %eax // load` | | | `3 mov %eax, (%edi) //store` | * Gadgets的调度程序 Gadgets的调度程序也是x86的指令序列,攻击者可以重复调用Gadgets,可以模拟Gadgets调度程序的x86指令的一个常见序列是一个循环,它对模拟小工具的计算进行迭代,并且有一个选择器。每次迭代使用前一次迭代中Gadgets的输出执行Gadgets的子集,为了将迭代i中的Gadgets的输出引导至第i+1中的Gadgets的输入,选择器将第i+1的加载地址更改为第i次迭代的存储地址。 *选择器的行为由攻击者通过内存错误来控制(?)* * 另一类DOP攻击是非交互的。 攻击者提供整个恶意输入,作为一个单个的数据传输。 * 模拟跳转操作: ```c 1 void cmd_loop(server_rec *server, conn_t *c) { 2 while (TRUE) { 3 pr_netio_telnet_gets(buf, ..); 4 cmd = make_ftp_cmd(buf, ...); 5 pr_cmd_dispatch(cmd); // dispatcher 6 } 7 } 8 char *pr_netio_telnet_gets(char * buf, ...) { 9 while(*pbuf->current!=’\n’ && toread>0) 10 *buf++ = *pbuf->current++; 11 } ``` 这里的关键是找到一个合适的变量,可以在每次循环迭代中被破坏的虚拟PC指针,如上述代码,有一个内存指针`pubf -> current`,指向了恶意网络输入的缓冲区。在每一次循环迭代中,代码从该缓冲区读取一行,然后在循环体中处理它,因此这个指针可以用来模拟虚拟PC指针。对于模拟非条件跳转,攻击者只需要配置好内存,来触发另一个操作Gadgets(如加法、赋值)来改变虚拟PC指针的值。 有两种方式模拟条件跳转,一种情况是使用虚拟PC指针读取内存配置是有条件的,攻击者只需使用操作k将合适的变量设置为读取条件;另一种情况是操作k的执行条件依赖于数据变量。 与ROP不同,DOP受限于应用的源控制流。 ### Gadgets的定义 ```text Input: G:- the vulnerable program Output: S:- data-oriented gadget set 1 S = ;; 2 FuncSet = getFuncSet(G) 3 foreach f 2 FuncSet do 4 cfg = getCFG(f) 5 for instr = getNextInstr(cfg) do 6 if isMemStore(instr) then 7 gadget = getBackwardSlice(instr, f) 8 input = getInput(gadget) 9 if isMemLoad(input) then 10 S = S [ fgadgetg ``` LLVM IR 提供了比二进制程序更多的语义,也避免了解析程序源代码,它还允许以任何具有LLVM前端的语言编写的源代码的语言不可知分析。 相同语义的Gadgets功能上等同于同一个MinDOP操作,赋值Gadgets可以用来准备其他Gadgets的立即数,有条件的Gadgets有助于简单Gadgets实现高级计算。因为不改变控制流,所以DOP中没有函数调用Gadgets。 我们将Gadgets分为三类:一类是全局的,一类是函数参数,另外一类是局部Gadgets。全局Gadgets操作全局变量,内存错误可以在任意地址改变这些变量;函数参数Gadgets操作被传递给函数的参数,内存错误可以控制函数的参数;局部Gadgets在局部变量中产生,在函数内部出现的内存错误可以激发他们。 ### Gadgets调度程序的定义 ```text Input: G:- the vulnerable program Output: D:- gadget dispatcher set 1 D = ;; 2 FuncSet = getFuncSet(G) 3 foreach f 2 FuncSet do 4 foreach loop = getLoop(f) do 5 loop.gadgets = ; 6 foreach instr = getNextInstr(loop) do 7 if isMemStore(instr) then 8 loop.gadgets [= getGadget(instr) 9 else if isCall(instr) then 10 target = getTarget(instr) 11 loop.gadgets [= getGadget(target) 12 if loop.gadgets != ; then 13 D = D [ floopg ``` ### 攻击的构造 1) 准备Gadgets(自动) 发现一个内存错误,从程序代码中定位到该函数,然后,我们确定是否包含易受攻击代码,并收集数据导向Gadgets的Gadgets调度程序。 2) 构造攻击链 ​ 我们将预期的恶意MinDOP程序为输入,每一个MinDOP操作由相同功能的数据导向Gadgets实现,并根据优先级选择合适的Gadgets。 3) 可协作的验证 一旦我们获得一系列数据导向Gadgets来完成我们想要的功能,我们将验证每一个围绕它们的调度程序完成拼接是否可能。向程序提供构造好的输入来触发内存错误,来连接相应的Gadgets,如果攻击不起作用,回滚步骤2来选择不同的Gadgets并再次尝试。 ​ ## DOP的潜在防御 ### 内存安全 内存安全首先通过检测恶意内存损坏来防止出现内存错误。DOP利用大量的内存错误来粘合各种数据导向Gadgets,因此,内存安全执行将防止所有可能的漏洞攻击,包括DOP。但是,为了达到内存安全需要大量的开销。 见参考文献: ```text L. Szekeres, M. Payer, T. Wei, and D. Song, “SoK: Eternal War in Memory,” in Proceedings of the 34th IEEE Symposium on Security and Privacy, 2013. ``` ### 数据流完整性(DFI) 在程序执行之前,DFI生成数据流图(DFG),DFG是关于定义-使用关系的数据库,DFI在程序的检测之前检查每个存储单元是否有合法的指令定义。通过这种方式,DFI可以防止破坏程序内存的恶意行为。然而完整的DFI保护依然需要很大的开销。 参考文献使用DFI保护内核安全数据: ```text C. Song, B. Lee, K. Lu, W. R. Harris, T. Kim, and W. Lee, “Enforcing Kernel Security Invariants with Data Flow Integrity,” in Proceedings of the 23th Annual Network and Distributed System Security Symposium, 2016. ``` ### 细粒度的数据面随机化 细粒度的数据面随机化可以缓解DOP攻击,因为DOP仍然需要获取某些非控制数据指针的地址。然而,数据面上的细粒度随机化可能会导致高性能开销,因为所有数据(包括控制数据和非控制数据)应该常被随机化。 高性能和强安全性保证的数据面随机化仍然是一个悬而未决的问题。 ### 硬件错误和软件错误隔离 内存隔离被广泛用于防止未经授权访问高权限资源,只有合法的代码区域才能访问特定的资源,这样可以防止一些直接的数据破坏攻击,但是,DOP不依赖于安全关键数据的可用性 - 它可能会损坏指针,只能针对数据导向Gadgets。为了防止这种攻击,内存隔离必须保护所有指针不受纯数据影响。 然而,精确识别二进制代码中的指针是一个挑战,此外,一个程序中有成千上万的指针,保护所有的指针将带来很大的开销,因此,当程序被指针隔离正确保护时,隔离只能防止部分DOP攻击。