软件作为信息系统的关键组成和代码重用攻击的防护

软件作为信息系统的关键组成, 控制着计算机系统设备的工作方式, 其安全性关系到整个信息系统的安全、可靠和稳定.而软件作为人类智力活动的表达和产物, 在其规模和复杂程度迅速增长的同时, 就不可避免地存在漏洞(和缺陷)[1-3].在当前严峻的网络空间(cyberspace)安全形势下, 软件漏洞以其高威胁、难防御、普遍存在等特点, 被作为一种战略资源, 广泛用于攻防博弈中[4-6].而本文则是在当前系统架构和软件生态环境下, 从攻击和防护两个角度对软件漏洞利用中的二进制代码重用关键技术展开探讨和研究.

自1988年11月第1个缓冲区溢出漏洞攻击案例——莫里斯蠕虫(Morris worm)之后, 针对软件漏洞的攻击与防护技术就已被广泛研究[7].回顾过去30年的发展历程, 研究人员通过向编译器、操作系统和处理器这3个层面引入新安全特性, 实现在源代码安全检查、二进制代码生成和软件运行过程中对软件进行保护, 提高软件的安全性, 试图消除或降低攻击者利用软件漏洞对信息系统进行攻击的可能性.

然而, 攻防技术相生相长.虽然在硬件、操作系统和软件不同维度的防护下, 传统的完全依赖于代码注入(code injection)的控制流劫持类漏洞的攻击方式已能被较好地防御, 但攻击者仍可利用跳转指令将内存空间中已有的、分散的代码片段(code blocks)链接, 构造出其所期望的、具有攻击目的的功能逻辑, 最终实现在不引入外部代码的情况下, 实施控制流劫持后的恶意攻击[8].这种利用内存中已经存在的代码片段实现新的功能逻辑的程序执行方式, 就是二进制代码重用技术.当前, 内存不可执行(non-executable memory, 简称NX)或数据执行保护(data execution prevention, 简称DEP)、动态代码签名(dynamic code-signing)等安全特性被广泛部署于主流操作系统中, 使得漏洞攻击的难度显著提高.在这种情况下, 代码重用技术的“无需注入具有可执行能力攻击代码”的特性使其能够有效绕过这些新型安全机制, 其在漏洞利用中的关键性作用更加凸显.从已公开发布和披露的案例来看, 代码重用技术已成为控制流劫持类漏洞利用中的一个必要环节.

最近10年, 网络空间安全形势的愈加严峻, 学术界、工业界都对二进制代码重用进行了广泛且深入的研究.这些研究涵盖基于二进制代码重用的攻击思路、相应的防护方法与检测机制等方面.本文从3个方面进行分析和探讨:(1) 二进制代码重用技术的基本原理、本质思想, 不同平台架构、操作系统环境下的应用以及在攻防博弈下的进化; (2) 针对二进制代码重用攻击方法的防护和缓解机制的研究, 涵盖从软件本身、程序运行环境(如操作系统)等角度进行的安全性防护以及针对攻击行为的异常检测方法; (3) 工业界对重要学术成果的转化和应用情况, 如微软发布的CFG(control flow guard)和Intel拟推出的处理器级别的代码重用防护技术——CET (control-flow enforcement technology).最后, 本文对二进制代码重用未来的发展方向从攻击、防护和代码逻辑混淆等角度进行展望和探讨.

1 二进制代码重用关键技术1.1 代码重用基本原理

在缓冲区溢出、UAF(use-after-free)等常见漏洞利用中, 攻击者往往能够成功地控制线程调用栈中的返回地址或保存程序跳转目标(代码指针)的寄存器, 形成控制流劫持.通过将控制流重定向到其所期望的任何有效代码区域, 即可造成任意代码执行的高风险威胁.在早期的漏洞利用中, 攻击者直接将所劫持的控制流重定向到已精心布局(注入)在栈或堆中的外部代码(shellcode), 进行后续执行.但随着不可执行栈、NX(no-execute)、DEP等内存不可执行技术的引入, 使得攻击者直接注入到内存中的shellcode不具备执行权限, 从而攻击失效.而Apple在iOS系统中引入的代码签名机制则比DEP等更为严格, 只有包含已签名代码的内存页才具有执行权限, 这也同样限制了外部注入的代码执行[910].

如果攻击者将控制流的跳转目标指向内存中某函数的入口处, 并在栈上布局了该函数的调用参数, 则可以实现对该函数的调用.若被调用的是类似system()或execv()等安全敏感的函数, 则可在不注入代码的情况下造成有效攻击.但在实际攻击过程中, 对单个函数的调用往往是不够的, 攻击者通过使用位于函数末尾的类ret的指令片段, 让控制流跳转的目标地址始终来自于其布局在栈上的数据, 从而实现内存中分散指令片段的连续调用, 形成具有特定功能的大规模代码片段重用攻击.

图 1是一个二进制代码重用示例, 其功能是通过代码重用的方式实现了两个值的加法运算, 并将结果写入指定内存地址0x400000.通过在调用栈上精心布局指向内存中已有指令片段的指针和指令运行期间需要引用的数据, 使得每个指令片段在完成其功能执行后能够通过ret指令“返回”到栈上预置数据指向的代码片段, 即下一个指令片段, 从而实现在具备不同功能的指令片段上的连续运行, 最终实现特定的完整功能逻辑.

Fig. 1 An example of binary code reuse and connected code blocks图 1 二进制代码重用示例及指令片段连续调用

结合图 1所示的例子, 在典型的基于代码重用的执行方式中涉及如下4个要素.

(1) 程序调用栈:由攻击者控制, 用于提供指令片段的指针和所需数据, 是链接不同指令片段、确保连续执行的枢纽.

(2) 指令片段地址:被精确布局到调用栈中, 使控制流正确跳转到目标指令片段.

(3) 指令片段:代码重用中的基本功能单位——指令, 一般称为gadget.根据Schwartz在文献[11]中的描述, gadget需具备如下特性:

① 控制流保持:具备控制流转移能力, 且当执行完该gadget中的指令序列后, 控制流需跳转到一个可控的地址, 以继续执行gadget-chain中的后续gadget.

② 功能性:通过执行gadget中的指令序列能够实现特定操作, 如从内存读数据或进行加法运算等.

③ 移动栈指针:为了保证控制流的持续可控, 通常执行gadget的过程中需要移动栈指针esp, 使得gadget能够继续引用布局在栈上的其他数据, 尤其后续跳转地址.

④ 不改变程序状态:gadget在执行过程中不会产生一些不确定的结果, 使得程序执行状态发生意外改变.

(4) 新引入的控制流:由代码片段的连续执行形成的新的功能逻辑.

基于代码重用技术的攻防对抗, 正是以上述4个因素为关键点进行相应研究的展开, 本文将在第3节、第4节中详述.

1.2 代码重用的图灵完备性

代码重用的概念在早期被称为返回导向式编程(return-oriented programing, 简称ROP)方法, 说明其具备程序语言一样的能力[1213].为了说明特定的代码重用方式在图灵机模型下能够进行任何操作, 通常需要证明该代码重用方式的图灵完备性(turing completeness).一旦证明该代码重用攻击方法是图灵完备的, 则从理论上说明攻击者基于此种代码重用攻击模型能够实现任意攻击目的的程序行为[13-15].

Shacham在2007年首次证明了基于ROP的代码重用方式是图灵完备的[13].证明代码重用方式具有图灵完备性, 需证明该重用方法下的gadget能够实现如下功能:内存读/写、数据处理、控制流跳转、系统调用和函数调用.下面结合kernel32.dll(Windows 32, X86) 中的gadget(ROP gadgets search: Free Online ROP Gadgets Search)分别进行描述.

(1) 内存读/写

一般情况下, 内存读写主要有3种形式:加载常量到寄存器(load cons)、从指定内存加载数据到寄存器(load mem)、向指定内存写入数据(write mem).故要证明具备内存读/写能力, 需存在上述3种类型的gadget:通过类似于pop REG的gadget即可实现从栈到寄存器的常量加载; 在X86下, 通过mov指令可实现从内存加载数据到寄存器, 如“mov eax, [ebp+0x10]; pop ebp; ret 0xc”; 同理, 内存写入gadget形如“mov [ebx+2], eax; ret”.

(2) 数据处理

若要具备完整的数据处理能力, 则需要具有算术运算和逻辑运算的gadget.其中, 算术运算gadget需要包括用于进行算数加法、减法、乘法和除法的4类gadget, 而逻辑运算包括能够进行逻辑与、或、非和异或这4类gadget.表 1给出了相应的gadget示例.

Table 1 Gadgets for data processing (arithmetic operation and logic operation)表 1 数据处理gadgets示例(算数运算和逻辑运算)

(3) 控制流转移

控制流转移分为直接跳转(unconditional-jump)和条件跳转(conditional-jump).对于前者, 使用函数末尾类似于“ret”的返回指令进行任意目标的无条件跳转.对于条件跳转则较为复杂, Shacham在文献[13]中给出了间接实现条件跳转的一种思路.

a) 使用指令neg对特定值进行求补码运算, 操作数是否为0可以通过进位标志CF(carry flag)体现出来.由neg指令特性可知:如果操作数为0, 则CF为0;否则, 操作数为非0, CF被置为有效, 即CF值为1.

b) CF位反映了条件判定结果, 通过使用移位指令或者带进位的加法即可取出CF值.一种简单的办法是使用类似于xor ecx, ecx; adc cl, cl; ret的指令片段将CF的值放入寄存器ecx.

c) 将ecx向左移位2n位, ecx的值则为0或4n, 其中, 4n是两个分支目标之间的offset.只需要通过gadget序列实现功能mov eax, offset; add esp, eax; ret, 则实现由步骤a)中的特定值来决定最终的跳转目标的功能, 即条件跳转.

上述步骤中的后两步可能需要其他多个基础gadget的操作来完成, 但该思路最终能够实现条件跳转.

(4) 函数调用、系统调用

对于X86和ARM等架构, 其函数调用过程本身就是借助于栈实现的, 其位于栈上的参数、返回地址等是攻击者可控的, 故攻击者很容易地通过控制流转移gadget实现函数调用.早期的代码重用方式ret2libc就已实现函数调用.在系统环境下, 直接或间接地进行系统调用是实现复杂功能的必要条件.在系统中, 系统调用一般都是通过在特定寄存器中设置系统调用号, 然后执行系统调用指令来实现系统调用的.例如, 在Windows系统中, 调用过程封装在函数ntdlll!KiFastSystemCall中, 其内部实现为mov edx, esp; sysenter; retn, 由上使用sysenter进入内核.对于特定功能的系统调用, 通常则是封装在系统调用对应的功能函数中, 例如ntdll!NtReadFile, 一般形如mov eax, SYSCALL_NO; mov edx, offset SharedUserData!SystemCallStub; call dword ptr [edx]; ret 24h.

所以, 当需要进行特定系统调用时, 使用ret2libc的方式调用相应的库函数; 亦可在eax中设置系统调用号后, 调用KiFastSystemCall实现.故系统调用的本质也是函数调用.

(5) Stack Pivot

在攻击过程中, 如果将所有的用于组织代码重用攻击的数据都保存到栈上, 则一方面有可能触发操作系统对栈的保护机制; 另一方面也有可能导致栈上关键数据被覆盖, 使得结束攻击者的执行流之后无法继续执行原有程序流程.因此, 攻击者通过修改栈指针esp的值, 使其指向攻击者完全可控的堆空间.esp指向的堆上内存区域则称为伪造栈(forged stack), 而用于改变esp值、进行栈切换的指令被称为stack pivot指令.例如, 使用“xchg eax, esp; ret”的gadget-chain作为第1个gadget, 实现将栈切换到eax所指向的内存区域.Stack

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值