攻防对抗形势下代码重用技术的演进

,基于代码重用的程序执行方式被广泛用于漏洞攻击中, 用来绕过代码不可执行、动态代码签名等安全机制.图 2从时间维度给出了代码重用攻击的演变历程.

Fig. 2 Evolution of binary code reuse-based attacks图 2 二进制代码重用攻击的演变历程

整个历程分为4个阶段.

●  阶段1, 以Ret2Libc为核心思想的函数级别代码重用, 跳转目标一般为函数开始位置[26-29].

●  阶段2, 以具备控制流转移能力的代码块级别的代码重用为核心.证明了基于代码重用执行方式的图灵完备性; 经历从返回导向编程(ROP)到直接跳转导向编程(jump without return, 简称JOP)的发展; 应用场景也扩展到ARM, SPARC等架构上[13222430-32].

●  阶段3, 以Just-in-time code reuse为代表的交互式环境下, 具备对抗随机化能力的代码重用攻击[33-35].

●  阶段4, 动态生成所需的指令片段来进行代码重用攻击[36].

上述演变历程中, 一方面将代码重用扩展到不同架构、平台上, 例如, 从早期X86架构扩展到当前广泛用于移动互联网设备的ARM架构上, 但更重要的是另一方面——对代码重用攻击方式的进化.代码重用攻击涉及两个关键过程:

(1) 利用gadget的控制流转移特性来拼接具备不同功能的gadget;

(2) 在内存代码页中定位并获取所需的gadget.

在攻防对抗形式下, 代码重用攻击方式主要是从这两个角度不断进化的, 即扩展控制流转移方式和拓宽gadget获取方式.本节将主要从这两个方面详细分析, 最后也介绍了在对抗代码重用攻击检测方面的进展.

2.1 控制流转移方式

根据第1.1节中对gadget的定义——gadget必须具备控制流转移能力, 而这种控制流转移能力是基于不同的程序分支指令实现的.

2.1.1 基于函数返回指令的控制流转移

利用函数末尾的返回指令实现控制流转移的方式在代码重用攻击中使用最早且最广泛, 典型技术有ret2libc和ROP等.Ret2libc是代码重用攻击思想, 最早由Solar于1997年在Bugtraq邮件列表上提出来:介绍了通过覆盖函数返回地址将控制流重定向到libc库中的目标函数(如setuid, system等)的方法, 从而解决栈上代码不可执行的问题[26].在该方法以及后续研究中, 分别针对x86/x64, SPARC提出了更加完善的基于ret2libc的攻击利用方法, 将函数粒度的gadget调用转变为短指令序列.例如, 使用包含函数返回指令的短指令序列“pop reg; ret”对寄存器的赋值操作, 使用ret n指令实现对函数的栈上参数的布局[2728].2005年, Krahmer在文献[29]中实现的工具, 能够通过register-pop代码序列构建任何的形式的参数.实际上, 这与ROP已经非常接近了.

ROP技术的出现, 为基于代码重用的攻击提供了完备的理论支持.2007年, Shacham提出了Return-to-libc without Function Calls(on X86) 的方法, 该方法与此前的基于return-into-lib(c)方法相比的最大进步就是无需调用函数即可实现相应的功能[13].传统的方法使用“pop-ret”等短指令来连接不同的函数; Shacham则证明了可以使用类似的短指令实现任何所需的功能, 即证明了短指令的图灵完备性.

接下来, 基于Shcham的理论, ROP的代码重用方法被广泛推广, 被逐步证明并应用到更多的指令集、系统架构等上面, 如ARM, SPARC等[21303137].但对于不同的架构, 对应的函数返回指令不尽相同.例如在ARM架构上, 程序主要通过pop {…, pc}和pop {…, lr}; bx lr的方式进行函数返回, 但其功能类似于ret指令, 可以作为代码重用攻击中的控制流转移指令.

2.1.2 基于函数调用或跳转指令的控制流转移

随着对基于ROP的代码重用的研究的深入, 用于ROP中的短跳转指令也不仅仅局限于“pop-ret”, 而是扩展到各种直接跳转指令和间接跳转指令上.Checkoway等人在2010年提出了ROP without return的思路[15].通过分析典型ROP中return指令所承担两个角色:① 实现控制流跳转; ② 更新寄存器状态, Checkoway等人证明了使用pop+jump的指令组合成功实施ROP攻击的可行性; 同时, 他们也证明了ROP without return的图灵完备性.同时, Bletsch等人和Chen等人提出了类似的不使用return指令的代码重用方法Jump-Oriented Programing (JOP)[1419].具体来说, Bletsch等人首先指出, 由于编译器优化的原因, 使得Checkoway的方案[15]中所使用的pop+jmp类型的gadget数量非常少, 很难实际应用; 其次, pop+jmp的方案依旧严重依赖于栈操作指令pop, 根据栈指针sp的变化来加载数据.而Bletsch提出的Jump-oriented Programing则使用了两类以jmp或call间接跳转指令结尾的gadget实现代码重用攻击, 结合图 3中JOP代码重用攻击的示例来看,

Fig. 3 code reuse example based on JOP图 3 基于JOP的代码重用示例

●  一类是作为跳转枢纽的dispatcher类指令, 例如add ebx, 4; jmp [edx], 用于从位于攻击者可控内存区域的dispatch table中获取跳转目标;

●  另一类是具备功能作用且能够跳转回dispatcher指令的功能性gadget, 例如add eax, [ebx]; jmp [edi], 是一个用于进行加法运算的功能性gadget.

相对于pop+jmp的代码重用方法, JOP方案有更多满足条件的gadget, 且不依赖于栈进行控制流跳转.此外, 国内学者茅兵、刑骁等人也提出了基于分支指令的ROP方案BIOP(branch instruction-oriented programming).该方案是对JOP方案的改进和完善, 使用jmp指令或call指令结尾的短指令序列作为gadget构造ROP攻击.该方法由于不引入ret指令, 能够躲避一些检测方法[32].无论是pop+jmp还是JOP, 这种ROP without Return的思路不仅可以用于X86, 而且也可以应用于ARM等架构平台上[20].

2.2 Gadget获取方式

随着攻防形式的演变, 攻击者为了绕过针对代码重用的各种防护策略, gadget的获取方式也从传统的二进制离线分析扩展到内存在线分析和动态构建、生成gadget的方式.其中, “二进制离线分析”基于程序静态分析方法完成, 而“内存在线分析”和“动态构建、生成gadget”则依赖于程序运行时的动态分析.

(1) 二进制离线分析

一般情况下, 二进制代码重用所需的gadget都是通过逆向分析将被漏洞程序加载到内存中的模块, 从模块的代码段中搜索获得.基于二进制离线分析的方式获取gadget的典型工具有ROPgadget, ROPshell等.搜索工具通过这种方式获得的是gadget的模块内偏移地址, 在最终组装gadget链时还需要加上实际的模块基地址.

(2) 内存在线分析

相对于传统的ROP, Ret2Libc等基于静态分析来构造代码重用的方法, Snow等人在2013年提出了Just-in-time code reuse的动态代码重用方法[3334].这种代码重用思路的重点是:在浏览器、文档阅读器等支持脚本运行环境的软件中, 通过脚本与软件的实时交互获取内存分布情况, 进而基于内存中的指令分布来动态地构造ROP链.在Blind ROP研究中, 斯坦福大学的Bittau等人则是利用Liunx中一个服务崩溃后会自动重启, 且服务重启后内存布局不发生变化的特性, 实现了通过多次触发漏洞、动态调整基于代码重用的payload, 最终实现在不直接获悉内存地址的情况下, 利用ROP成功攻击的案例[35].

(3) 动态构建、生成gadget

动态构建、生成gadget的方式和内存在线分析的方法都是动态代码重用, 前者侧重于动态获悉已加载模块的内存分布, 后者侧重于动态生成所需的代码片段, 取消了对程序内存中其他模块的依赖性.具体来说, 在动态构建、生成所需gadget方面, 2015年, Athanasakis等人提出通过定义特定的常量, 使得浏览器中的JIT引擎动态生成所需的代码片段.此类代码片段具有较易定位的特点, 并且能够绕过当前绝大多数防御机制, 因此, 可以更加有效地用于代码重用攻击[36].

2.3 代码重用攻击检测的绕过

除了从代码重用本身出发开展相关研究之外, 研究人员也从对抗代码重用攻击检测的角度提出了一些专门针对检测技术(详见第3.2节)进行绕过的代码重用变形方案[38-41].

在当前针对软件系统漏洞攻击与防护的严峻对抗形势下, 研究人员已从代码重用过程中控制流转移方式和gadget获取方式等增强和拓宽了代码重用技术的灵活性和可靠性, 但在后续研究中, 无论是出于学术研究还是黑客获取利益等目的, 代码重用作为一种十分有效的漏洞攻击方法仍会被广泛关注, 并促进其从不同维度去发展和进化.

3 二进制代码重用攻击的对抗策略与应用

二进制代码重用作为一种攻击技术, 在漏洞利用中被广泛应用的同时, 自然也催生了对其相应对抗策略的研究.本节一方面从代码重用攻击的防护和检测两个角度出发介绍了学术界近年来的研究; 另一方面, 对CFG (control flow guard)和CET(control flow enforcement)这两项近期被工业界实践和应用的防护方案进行了剖析和探讨.

3.1 代码重用攻击的防护

基于代码重用的漏洞利用攻击方式, 通常需要两个前置条件[42-45].

(1) 内存中存在足够的、合适的能够完成功能需求的代码块(gadget), 且通过在该代码块间进行控制流转移和控制流传递(保持)能够动态地引入新的功能性控制流;

(2) 能够准确定位到程序运行期间所需的gadget, 即, 需要获悉gadget在程序运行期间的内存地址, 该地址将作为控制流的跳转目标.

根据代码重用攻击所需前置条件, 结合代码重用攻击对控制流跳转的影响以及对特殊指令和指令地址高度依赖的特性, 当前针对代码重用攻击的防护思路和方法如图 4所示.

Fig. 4 Defense strategies for code reuse attacks图 4 代码重用攻击的防护思路

●  一方面, 以阻止非预期的控制流跳转为出发点, 构建控制流完整性保护体系(control flow integrity enforcement);

●  另一方面, 以降低攻击者对内存布局的知悉情况为出发点, 引入增强型随机化策略(randomization)[44].

接下来, 本文将对这两种防护思路进行详细分析.

3.1.1 基于控制流完整性的防护方法

代码重用攻击方法是在劫持程序原有控制流之后, 通过连续地执行一个个分散的gadget形成新的控制流、功能逻辑、实现所期望的功能, 达到预期攻击目的.对于代码重用,

(1) 从宏观上来看, 攻击者在原有程序上构造的控制流跳转和动态引入的功能逻辑都不是程序开发者预先设计和期望的.这种非预期的程序行为破坏了原有程序的控制流完整性, 导致了程序行为异常.

(2) 从微观上来看, 代码重用攻击所使用的大部分代码片段均是函数末尾指令或其他跳转指令跟随的代码块, 执行的入口点也并非相应代码块所预期的一个跳转来源, 即对一个函数或者原始代码块而言, 代码重用的执行方式破坏了其正常的控制流转移和执行流程.

基于控制流完整性(control flow integrity, 简称CFI)的防护思路通过限制程序运行时的控制流转移, 使应用程序的所有控制流转移均处于事先定义的预期控制流图(control flow graph, 简称CFG)内[46-49].具体来说, 通过编译时的源码分析或基于二进制代码的静态分析, 或运行时profiling来获取程序的预期控制流图; 程序运行时, 在控制流转移的指令附近进行额外的跳转目标验证, 保证当前跳转目标处于预期控制流图内[40464750].因此, 通过保证控制流的完整性, 可有效防御代码重用攻击.

控制流完整性(CFI)在2005年由Abadi等人在文献[4649]中提出.文献[49]为CFI提供了理论基础, 使用程序语言理论分析和定义修改后程序的执行行为.文献[46]给出了基于二进制文件静态分析和二进制重写的一个CFI实现.具体来说, 对应用程序中函数调用的目的地址、函数返回的目的地址进行编号、生成标识符ID, 在控制流转移前对函数调用的目的地址、函数返回的目的地址的ID进行校验, 看是否属于合法跳转集合, 如果不属于CFG, 则报错或抛出异常.

严格意义上的CFI需要对每个控制流转移的目标地址进行检查, 保证所有的跳转都始终维持在相应的合法集合中.此外, 如果要提供更好的防御效果, 则还需从程序行为的角度, 结合程序上下文进行分析.但这种方法粒度较细, 且额外引入的指令较多, 对系统性能造成了明显的影响[48].所以, 后续的研究主要侧重于研究更实用的粗粒度的控制流完整性方案——在尽量不损失安全性的前提下提高效率, 同时增强多架构多系统下的适用性.

Zhang等人在2013年提出了将CFI与随机化结合的实现方法CCFIR(compact control flow integrity and randomization)[51].该方法将间接调用指令和函数的返回指令的跳转目标进行区分, 阻止非预期的跳转; 同时, 将所有的跳转指令的检查代码统一放在一个特定随机化过的内存区域, 使得攻击者无法继续正常跳转或者绕过随机化, 有效避免了大量插桩引发的性能损耗.此外, CCFIR的实现方案支持增量部署, 使其更具实用性.

Zhang等人提出了一种针对COTS(commercial off the shelf)程序的方法binCFI.该方法相对于Abadi在文献[46]中给出的CFI方案、Zhang的binCFI方案[51], 不需要依赖于编译器, 也不依赖于二进制程序中的重定位信息(relocation)、符号信息(symbol)和调试信息(debug)等, 却能够应用更多复杂的、较为底层的二进制文件, 提供更加全面的CFI应用方法, 能够有效地保护包括二进制程序、共享库和加载器(loader)在内的模块内控制流转移以及模块间的控制流转移[52].具体来说, 在细致地静态分析过程中, 识别出作为间接跳转(indirect control flow transfer, 简称ICF transfer)目标的代码指针, 并将其分类为编译时已经确定的常量指针、运行时动态计算出来的可计算型指针、指向异常处理代码指针、指向导出表的指针以及返回地址, 然后分别对这几类进行细化分析, 对不同类型的间接控制转移指令收集其合法跳转目标地址集合, 设计相应的间接跳转替换, 减少可被用于代码重用的gadget.binCFI通过反汇编、代码修改、再编译成目标代码(code in object file)、最后作为新的节放到原始二进制文件中作为代码节的方式与指令重写的方式相比, 不改变原有代码, 实现更便捷, 对原代码透明.优点就是可以在不损失安全性、性能损耗很小的情况下很好地应用于系统程序和第三方程序.

binCFI和CCFIR只是将间接调用指令和间接跳转指令的跳转目标进行验证.但在文献[53]中, Göktas通过使用两种特殊的gadget, 即entry point (EP) gadget和call site (CS) gadget, 实现对binCFI和CCFIR等CFI的绕过, 成功实施代码重用.对此, Mashtizadeh等人提出了CCFI(cryptographic CFI)方案.CCFI一方面对跳转目标进行更细粒度的分类, 即函数指针、函数返回地址、方法指针、虚表指针; 另一方面, 在程序运行期间保存代码指针时, 对运行上下文信息加密保存, 在程序返回时, 则解密上下文信息并对比, 进而判断跳转的合法性.额外的上下文信息比对, 增强了CFI在对抗代码重用攻击上的安全性和可靠性[48].

Niu等人在2014年提出了模块化CFI(modular CFI, 简称MCFI).MCFI允许模块在加载和链接时利用模块中的辅助信息生成新的CFG, 或者对CFG进行更新, 以保证模块间的调用仍然遵循CFI[54].MCFI使CFI的实用性得到了增强.在RockJIT中, Niu等人将MCFI的思想运用在浏览器中JIT引擎的安全性增强上[55].Mohan等人的O-CFI则是将随机化与CFI相结合, 使得攻击者无法获悉存在可被劫持的CFG中的边, 实现了对代码重用攻击的抑制[56].在多架构、多系统应用方面, Davi等人则将CFI应用在ARM架构上[57], Pewny等人则是将其应用于iOS[58].

除了上述通过验证跳转目标地址的CFI的实现方法之外, 还有一类则是基于函数调用栈的特性, 通过shadow stack实现对call和return的地址进行验证, 从而阻止防护代码重用攻击[424659-62].Lucas等人在2011年实现了工具ROPdefender, 通过将函数调用的返回地址单独保存在一个称为shadow stack的内存空间, 在函数返回时进行验证[60].ROPdefender通过Pin[63]进行运行时的指令监控, 当调用call时, 除了进行call指令本身的操作之外, 同时将返回地址入栈到shadow stack.当执行ret返回指令时, 则验证当前的目标返回地址与shadow stack中栈顶的地址是否一致:如果不一致, 则说明存在对函数的不完整调用——ROP攻击一般所用的gadget代码块仅仅是函数的末尾部分.考虑到基于Pin的实现方法在一般场景中很难应用, 同时, Davi Lucas只对返回型gadget有效防护, 但对于其他面向call的代码重用无效, 因此, Qiao等人在2015年提出了兼容性更好、防护更加全面的方案, 并且将原来的shadow stack进化为RCAP-stack(return capability stack).文中对ret和call的目标地址进一步细致分析, 对用于代码重定位的call和用于跳转的ret指令进行单独处理, 同时采用了应用性更好的基于二进制重写的实现方案[42].总的来说, 该方案是对Shadow-stack思路的一个精细化研究和完善, 提升了防御ROP攻击的能力.

在基于CFI的代码重用攻击对抗策略的学术研究基础上, 近期, 工业界基于CFI和shadow stack实现了安全机制CFG和CET, 试图构建从程序到运行环境的完整生态系统, 以对抗代码重用攻击.具体细节将在第3.3节中分析.

3.1.2 基于随机化的防护方法

基于随机化的防护方法的出发点在于降低攻击者对内存信息的知悉情况, 使得代码重用等严重依赖内存布局的攻击方法失效.通过对内存中模块、数据对象、代码指令等的地址和布局进行随机化, 攻击者难以定位内存中关键对象、无法构造出可以有效执行的代码重用链(gadget chain), 即打破了攻击者必须获悉所需gadget地址这一代码重用的必要条件, 进而达到代码重用防护效果.在攻防对抗的实践过程中, 基于随机化的防护方法的研究主要从两个方面展开:一方面是尽可能提升随机化能力本身; 另一方面, 则是在随机化的基础上防止和减少内存信息泄漏.本节将分别论述.

(1) 随机化能力提升

对随机化能力的提升, 主要包括地址空间布局随机化(address space layout randomization, 简称ASLR)和指令随机化两个方面.地址空间分布随机化最初是由PaX Team于2001年提出, 后来逐步被现在的绝大多数操作系统所广泛应用, 如Linux, Windows 7/8/10, iOS, Android( > 4.0)[964-67].在应用程序启动时、重新加载模块时, ASLR机制将会以一定的熵为应用程序随机确定程序加载的基地址、模块基地址、堆和栈的基地址, 使得加载到内存中的模块内指令和堆栈上的数据地址无法预测.由于ASLR的随机化粒度为模块级别, 所以一旦模块内的某个地址Addrleaked被泄露, 则由该地址的固定偏移Offsetleaked可计算得出当前模块的基地址, 进而模块内指令或者堆栈上数据的实际地址Addrtarget可通过固定偏移Offsettarget计算得出, 使得现有广泛使用的ASLR存在单指针泄露威胁(single pointer leakage threat).计算过程为Addrtarget=Addrleaked-Offsetleaked+Offsettarget.

此外, 出于性能优化等目的, 在部分系统上, 同一模块在不同进程中的基地址是相同的.例如, Android上的应用程序进程均是由Zyogte进程fork出来的, 使得libc等众多系统库的基地址是相同的, 攻击者可以通过协同攻击实现地址信息泄露和代码重用攻击的组合攻击[68].即通过应用程序A获得所需模块的基地址, 通过其他方法将该地址泄露给应用程序B, 应用程序B则可以基于此地址定位到所需gadget的真实地址, 构造出有效的代码重用攻击载荷.类似的问题也存在于Linux系统上的部分重要服务上, 如ngix和mysql等, 这些服务的主程序崩溃重启后, 仍然使用相同地址空间映射, 使得攻击者可以通过暴力尝试方式实现地址推测, 获悉模块基地址, 进而实施代码重用攻击[35].因此, 在内存信息泄露漏洞的辅助下, ASLR可以被轻易地绕过, 使其防护代码重用攻击的能力弱化.

基于上述因素, 自2012年起, 针对代码重用攻击防御的研究开始转向如何通过实施细粒度的指令随机化和杜绝指令信息泄露来实现对代码重用攻击的防护.ILR[69], Binary Stirring[70], Smashing the gadgets[71]等研究通过采用重新排列函数顺序、指令顺序重排、等效指令替换、寄存器重新分配等方法, 在不改变原有程序语义的前提下, 使得内存中gadget的语义发生变化、位置发生变化等, 从而实现降低攻击者对程序运行时内存的知悉程度[3334].图 5给出了部分指令随机化策略的示例.Snow等人在Just-in-time Code Reuse一文提出的动态生成ROP gadget链的方法有效地绕过了上述细粒度随机化, 但考虑到该方法要通过在线读取内存中大量的可执行代码来实现代码重用攻击, 所以攻击场景具有一定的局限性.梁玉等人则利用ARM指令特性, 通过二进制重写技术(binary rewriting), 在函数首尾的栈操作指令中随机地插入成对的空闲寄存器来实现栈帧布局随机化[4567].例如, 指令“push {r5, r6, lr}…pop {r5, r6, pc}”则可通过随机插入r2和r7变为“push {r2, r5, r6, r7, lr}…pop {r2, r5, r6, r7, pc}”.在程序运行时, 随机化后的代码能够向栈帧中引入随机大小的padding(r2和r7中的数据)来改变栈帧中数据的相对偏移, 从而使得布局在栈帧中的地址无法准确还原到寄存器pc中, 进而使得代码重用攻击失败.

Fig. 5 Examples of various instruction randomization strategies图 5 部分指令随机化策略示例

除了指令的细粒度随机化外, 研究人员也从随机化方式和随机化频率两个角度进行随机化能力提升[72]. Oxymoron利用X86的段特性实现了对库基地址的随机化, 分页内存管理模式使得Just-in-time Code Reuse攻击中的页代码泄露失效[73]; Isomeron则是将程序在内存中布局两份, 在函数返回跳转时, 随机确定跳转目标的所在的程序布局[74]; Lu等人则实现了在程序每次fork子进程时都进行一次随机化, 从而解决了fork的子进程与其父进程内存布局一致的缺陷[75].

(2) 缓解内存信息泄漏

此外, 研究人员从防止和减少内存信息泄漏的角度, 提出了新的防止可执行内存中的代码被读取的解决方案.防止可执行内存被任意读取的研究有XnR[76], Readactor[77]等, 这些方法或通过软件模拟MMU(memory management unit, 内存管理单元, 负责虚拟地址映射为物理地址以及提供硬件机制的内存访问授权), 或通过硬件与虚拟化(基于虚拟化的MMU)相结合的方法实现了代码内存页的可执行不可读的特性, 使得即使软件中存在单指针内存泄露, 攻击者也无法通过该内存泄漏实现整个代码页、代码节的指令泄露, 进而攻击者也就无法构造有效的攻击.值得关注的是, Readactor是一个相对比较全面的防御机制, 能够有效防止JIT-Code Reuse中提到的直接内存泄漏, 也能防止Isomeron[74]中提到的间接内存泄漏, 进而结合细粒度随机化, 可有效防止静态、动态的代码重用攻击.

3.2 代码重用攻击的检测

针对代码重用攻击的检测方法则是根据控制流跳转规则建立行为异常模型, 实施检测.根据检测异常的探测点的不同, 本节从控制流异常和栈异常两个方面进行论述.

3.2.1 控制流异常

在典型代码重用中, 引入新的控制流并对短指令块的返回式调用、函数的不完整调用等程序执行方式属于控制流异常.据此提出的实用性较强的代表研究有CFIMon[38]、kBouncer[78]和ROPecker[41]等方案.这3种方案均是利用系统的硬件设备进行辅助, 在降低性能消耗的同时, 从代码重用攻击跳转频率较高、函数的不完整调用特性出发实现代码重用攻击检测的.这种代码重用检测思路是基于启发式的, 需要设定每个gadget的指令数阈值和gadget数阈值, 如图 6所示.CFIMon利用了性能计数器(performance counter)捕获程序控制流, 进行合法性判定; kBouncer和ROPecker则是利用了LBR(last branch record)对最近捕获的16次跳转进行分析和合法性判定.相比之下, ROPecker提供了更加综合、更加完善的代码重用检测方案, 除了针对跳转频率和gadget长度的启发式检测之外, 还对触发检测的时机进行了有效选择——基于敏感系统调用触发异常检测, 从而很好地控制了性能开销.但这些代码重用攻击检测方案仅针对大多数通用的攻击有效, 对于针对性攻击, 基于启发式的检测方法还是存在被绕过的可能, 例如, Isomeron, Size Does Matter等研究中均给出了对此类启发式检测方法的bypass实例[7479].

Fig. 6 A heuristic detection strategy for code reuse attacks图 6 基于启发式的代码重用攻击检测方法


3.2.2 栈异常

线程栈在程序函数调用过程中至关重要——保存了函数调用返回地址.利用线程栈的栈式结构, 可以很容易地在栈上布局数据和程序跳转目标, 实现gadget和函数的连续调用.为了方便布局攻击数据、尽可能地减小栈的破坏程度, 攻击者通常会使用stack pivot指令将栈帧切换到一个位于堆上的伪造栈(forked stack)上.根据代码重用攻击过程中对栈或伪造栈的严重依赖性以及栈帧的异常切换, 梁玉等人提出的S-Tracker[80]和Prakash[81]等人从栈完整性异常的角度对代码重用攻击进行检测.S-Tracker对栈关键要素esp, ebp等进行了完整性定义, 限制esp等指针的范围, 在敏感行为发生时, 触发对栈的检测; Prakash等则是从代码重用中类似于stack pivot的关键gadget入手, 使用instrument的方式对其修改, 使得这类指令被调用时触发检测机制, 通过判断栈指针完整性, 实现对代码重用攻击的检测.

3.3 典型防护技术的实践与应用

对于防护方案, 从理论、原型的提出到最终实际部署、应用是一个相对漫长的过程.近两年来, 以CFG和CET为代表的基于控制流完整性的平台级防护方案在工业界得到应用和实践.

3.3.1 CFG

CFG(control flow guard)是微软对控制流完整性的一个平台级实现, 通过代码编译和程序运行时(runtime)相结合的方式实现对call的间接跳转目标的限制, 从而有效缓解包括代码重用在内的攻击方式.具体来说, 微软在Visual Studio 2015中引入了CFG, 并通过代码生成选项“/guard:cf”开启对间接跳转指令的插桩.图 7给出了插桩前后的对比.自Windows 10开始, 微软正式引入了操作系统级的CFG支持.图 7(a)中额外调用的_guard_check_ icall在支持CFG的操作系统上, 会通过调用ntdll!LdrpVaildateUserCallTarget对跳转目标进行验证; 在不支持CFG的系统中是一个空调用, 不引发额外的操作.操作系统通过位图CFGBitmap存储了当前调用的有效跳转目标集合, 在ntdll!LdrpValidateUserCallTarget当中首先获取CFGBitmap对象, 然后索引到对应的值, 从而判断当前跳转是否有效:如果无效, 则终止进程[8283].

Fig. 7 Comparison of code blocks generated by Visual Studio with CFG on and off图 7 Visual Studio开启CFG前后生成的代码对比


3.3.2 CET

2016年6月, Intel发布了称为Control-flow Enforcement Technology(CET)的技术预览白皮书, 正式公开了其准备推出的主要用于防护ROP, COP/JOP(call/jump oriented programming)的芯片级解决方案.该方案的核心思想源自学术界早先提出的shadow stack, 即在系统中的特定内存区域构造一个专门用来存储间接跳转数据的栈[4784].具体来说, 当一个子函数被调用时, 像传统方式一样, 其返回地址将会被保存到线程栈上; 同时也会被保存到shadow stack上; 在程序后续执行中若遇到返回指令, 则处理器需要保证线程栈上的返回地址与影子栈中的地址相匹配.如果二者不匹配, 处理器则会抛出异常, 使得操作系统能够捕获该异常并停止程序执行.

在CET技术中, Intel从CPU级别对shadow stack提供了安全特性支持.Shadow stack仅用于控制流转移操作, 其中保存了函数返回地址等, 与一般的数据栈相对独立.为了防止shadow stack被篡改, Intel通过CPU的内存管理单元MMU在页表保护中提供了新的扩展属性, 使得页内存除了具备读、写、执行保护外, 还可被标记为shadow stack页.被标记为shadow stack的内存页, 普通软件和指令无法直接对其进行写操作, 只有通过特定的控制流转移指令和Intel提供的专门的shadow stack操作指令, 才能实现对shadow stack的写操作, 从而从硬件体系结构提供了shadow stack这一安全特性.

此外, Intel还提供了一个新寄存器SSP(shadow stack pointer).该寄存器始终指向shadow stack栈顶.当CET功能打开时, 执行函数返回指令时, CPU将自动地从SSP寄存器指向的shadow stack栈顶处取保存的返回地址, 并与普通栈中的返回地址进行对比:二者如果不一致, CPU则抛出异常, 操作系统捕获该异常后进行后续处理.

和CFG一样, CET技术也需要通过平台支撑才能完全实现防护效果.Intel为CET技术提供了新指令ENDBRANCH, 该指令可用于标记合法的间接跳转目标.只有开发者使用支持CET技术的编译器来生成目标程序, 才会使得目标程序中的返回地址等合法间接跳转目标被ENDBRANCH标记.在程序运行时, CPU内部构建了一个状态机来跟踪call/jmp等间接跳转指令:当执行到call/jmp时, 状态机由IDLE进入WAIT_FOR_ ENDBRANCH状态.该状态下, 其下一条指令必须是ENDBRANCH:如果不是ENDBRANCH, 则说明跳转目标是非法的, 触发CET保护, 抛出异常; 如果是ENDBRANCH, 则状态机再次进入IDLE状态.通过上述方式, 实现对基于COP/JOP等代码重用方式的防护.

完善的防护方案需要构建从整个生态系统出发, 尽可能地减少攻击面, 提升安全性.CFG和CET的设计和实施过程中, 都是从底层硬件支撑、系统层程序执行环境保障、编译器层程序代码生成等环节进行了安全性和兼容性考虑, 使其可快速地、最大化地被实际部署.但是, 考虑到提供CET的Intel芯片目前尚未发布, 且攻防对抗是一个持续、演进的过程, 新的攻击方法存在绕过CET, CFG这类防护策略的可能性, 所以二进制代码重用技术仍然是软件安全领域所面临的一个重要威胁.

4 总结与展望

代码重用作为一种程序执行方式, 以其灵活的代码组织方式、功能逻辑动态生成的特点, 在过去被广泛用于漏洞攻击当中.严峻的网络空间安全形势使得代码重用技术的研究价值凸显, 学术界、工业界的研究人员在近10年中, 从攻击和防护两个方面开展了深入的研究.本文前面部分重点对已有研究的分析和总结, 一方面探讨了代码重用技术在攻击过程中的关键要素、逻辑组织方式及其攻防博弈下的演变历程; 另一方面, 从防护和缓解攻击的角度分析了已有防御技术的思路, 如基于控制流完整性的防护思路和基于随机化的防护思路.此外, 文中也对最新的来自工业界防护方法CFG, CET进行了分析, 为研究实用性更强的防护技术提供了启发.

代码重用技术除了用于攻击之外, 这种动态生成程序逻辑的程序执行方式亦可用于代码混淆, 尤其是对关键功能、关键逻辑的代码混淆和隐藏[8586].例如, 在程序运行过程中, 通过服务器推送的特定数据输入, 动态生成所需要的功能逻辑, 实现关键功能执行.这种方式的一个关键是在正常程序中构建一个风险可控的“漏洞”, 使得程序控制流有机会被隐藏的功能逻辑使用.因此, 研究如何利用基于二进制代码重用的程序执行方式进行代码逻辑混淆、代码隐写等应用, 是一项可探索的工作.

随着防御技术的不断演进, 代码重用攻击不局限于编译、发布的程序, 可能更加倾向于动态生成代码的重用.目前, 针对JIT等方式动态生成的二进制代码的防护相对薄弱, 这将成为攻击者的新的攻击面.因此, 针对动态生成代码的攻击防护, 也就是代码重用攻击对抗中的一个新的战场.

此外, 从控制流完整性和随机化的角度研究代码重用攻击防御方法仍将继续.在控制流完整性防护方面, 一方面是完善已有的防御方法, 如CET, CFG, 使其更加完备; 另一方面, 继续探索如何高效地实现更细粒度的CFI.对于随机化能力增强的研究可从3个方面展开:(1) 引入新的随机化机制, 弥补现有随机化机制中的不足; (2) 对内存中不同的数据对象进行不同程度的随机化, 除了使其具备防范代码重用攻击之外, 还可提升其整体安全性; (3) 提高随机化的熵, 增强随机化的效果.此外, 邬江兴院士提出的拟态计算机理论则是从整个计算机体系结构出发, 提高运行环境或执行机构的不确定性[87].对于该理论, 从攻击者的角度来看, 一类与运行环境、执行程序实体关系紧密的攻击方式将可能失效.因此, 研究新的计算机体系、从根源上解决代码重用等众多内存漏洞攻击, 也是可供探索的方向.

典型代码重用攻击需要配合内存信息泄漏, 而这其中的信息泄露既包括程序漏洞引发的内存任意读缺陷, 也包括由软件、硬件设计缺陷导致的侧信道信息泄露.因此, 如何从硬件、系统、软件等层面有效防止程序运行信息泄漏、防止内存中敏感数据泄漏, 是近期研究的一个方向.在数据中心、云环境中, 针对硬件特性引发的侧信道信息泄露的研究也值得关注.

二进制代码重用在当前的计算机系统体系结构和软件生态环境下暂时无法消除, 基于代码重用的攻防博弈仍将继续.因此, 无论是为了在网络空间安全博弈中占据主动权, 还是出于防护的目的, 都有必要对二进制代码重用技术做进一步的研究.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值