APP下载

二进制代码级函数指针攻击机理与检测研究

2019-01-24李昆仑巩春景李尚然张德智

小型微型计算机系统 2018年12期
关键词:攻击者指令程序

李昆仑,巩春景,李尚然,王 琳,张德智

(河北大学 电子信息工程学院,河北 保定 071000)

1 引 言

随着互联网的普及,各种攻击方式也趋于多样化,最常见的攻击方式是利用缓冲区溢出将控制流导向将要执行的恶意代码.在这种类型的攻击中,第一步需要找到一种方法覆盖内存中的指针,比较常见的方法是利用缓冲区溢出和格式化字符串漏洞.攻击者一旦获取了程序的控制流,下一步就是控制其去执行一些恶意程序.比如注入攻击、return-into-libc攻击、ROP攻击和JIT-ROP攻击等.其中return-into-libc是一种不依赖注入代码并且可以绕过内存不可执行技术的攻击方式,攻击者在栈中合适位置预先填入库函数入口的地址,将控制流导向库函数的入口实施攻击[1].比如libc库中提供了大量的API函数,攻击者便可通过执行若干库函数来进行实质性的攻击.ROP攻击与return-into-libc相比,其复用的代码不再是整个函数,而是代码库中某些以ret结尾的短小指令.JIT-ROP攻击动态获取进程中的资源构造gadgets实施攻击[2].

随后也出现了一些保护机制,比如控制流完整性(Control-Flow Integrity,CFI) 通过阻止原计划中没有的控制流,有效的阻止了代码复用攻击[3]; 地址空间随机化方法 (Address space layout randomization,ASLR)通过将库函数的代码段和数据段的起始地址进行随机化处理,使得攻击者不能得到正确的地址而无法进行攻击[4];但是通过暴力破解的方法可以得到真正的地址,又可以通过内存泄漏绕过传统的地址空间随机化方法[5].随后出现的细粒度随机化方法,进一步增加了程序的多样性,并限制内存泄露的有效性[6,7].Pappas 等人提出in-place 代码随机化技术,通过指令重排列、等价指令替换和寄存器重赋值阻止 ROP 攻击.然而,最近的研究表明,系统中存在的漏洞使得攻击者可以在任何地方读取内存并绕过细粒度的随机化方法[8].另外还有DEP技术和W⊕X技术(W的含义是writable,X的含义是executable,W⊕X代表可写区域不可执行,可执行区域不可写入)等.

2007年,Shacham将return-into-libc这种攻击方式进一步演化为ROP[9-11](Return-Oriented Programming).ROP攻击的出现表明了攻击者无需再向程序中注入任何新的代码便可实现图灵机完全性.

ROP攻击的思想很简单,与return-into-libc相比,ROP攻击的跳转位置不再是libc库中的API函数,而是函数中的某些指令,而且这些指令都是以ret结尾的且指令最多不超过6条的短指令序列,一般将其称之为gadget.ROP攻击可以绕过Windows的数据执行保护技术DEP(Data Execution Prevention).针对栈溢出的ROP攻击是当前研究的热点,通过修改函数指针可以很容易发起ROP攻击,本文主要研究这种攻击的机理与的检测方法.

PointGuard是针对函数指针攻击检测方面一种有效的检测方法,将函数指针经过加密之后保存在内存中,当进行函数调用时再进行动态解密[12].控制流安全 (Control Flow Integrity,CFI)也是一种经典的基于控制流的保护思想[13].它们所用到的关键技术是静态分析,对于间接跳转不起作用.对于目前发起的ROP攻击大多数是利用缓冲区溢出后覆盖函数的返回地址执行gadget,而对于通过覆盖函数指针发起ROP攻击的研究很少.本文深入分析了函数指针攻击形成机理,实现了这种攻击,并且提出一种动静结合的方法来检测通过修改函数指针发起的ROP攻击,核心思想是检查某些间接跳转指令是否连续执行有效的gadget.将fpDetect检测方法应用于正常程序与含有ROP攻击的程序中,实验表明这种检测方法的误报率低.可以同时应用在Linux与Windows操作系统中.

2 ROP攻击的形成机理

2.1 ROP原理剖析

对于具有可变大小指令的x86系统来说,攻击者可以在已有程序或者动态库中找到gadgets.具体的说,这些gadgets是从一个多字节指令当中的某个字节开始进行编译的指令.例如代码字节0b 00 00 83 c4 2c c3 66从第一个字节开始反汇编得到以下指令序列:

oreax,dwordptr[eax]

addbyteptr[ebx+0x66c32cc4],al

如果从第二个字节开始反汇编则得到以下指令序列:

addbyteptr[eax],al

addesp,0x2c

ret

图1 unintended指令序列Fig.1 Unintended instruction sequence

如图1所示攻击者可以利用x86指令密集型的特点在库中寻找大量以ret结尾的gadget来实施攻击.

图2描述本文所做实验的缓冲区中布局情况,图的左边是缓冲区中填入的数据(每条数据对应着地址),右边是每条地址所对应的指令.当执行第一条指令时,首先会将地址0x0806ea3a出栈执行pop edx,将数据0x080ea060出栈并存储到edx中(0x080ea060为data段首的地址).然后在执行ret时,0x080bb7f6的地址出栈,并执行pop eax指令,此时会将字符串“/bin”的ASCII码存入到eax中.接下来将地址0x0809a79d出栈并执行mov dword ptr [edx],eax,这条指令的执行会将字符串“/bin”存放到data段中.最后将执行execve函数.只要库足够大就可以找到各种类型的指令,完成各种功能,实现图灵机完全性.图灵机完全性是指能够实现数据的移动存储、算数加减、逻辑运算操作、控制流操作、函数调用、系统调用操作的具备通用图灵机计算能力的gadget集合[14].

图2 ROP攻击指令执行顺序Fig.2 ROP attack instruction execution order

指令密集型的特点使得攻击者更容易寻找gadget,利用其指令字节数不规整的特点,当我们在一个多字节指令中的某个字节处开始进行反汇编时会得到意想不到的结果.

2.2 ROP gadget的组成

gadget由两部分组成:功能指令和控制转移指令.在ROP中控制转移指令采用ret,目的是改变控制流跳转到下一个gadget.功能指令可以实现的功能可概括为以下:

1)加载常数到寄存器:将堆栈中的常量加载到指定的寄存器中,如pop ecx; ret.执行此语句后,存储在堆栈中的值将加载到ecx寄存器中.

2)从内存中加载数据:如mov ecx,[eax]; ret指令.执行这条指令后会将eax中的地址指向的值加载到ecx中.

3)将寄存器中的值写到内存中:如mov [eax],ecx; ret指令.执行这条指令后会将寄存器ecx中存放的值写入到eax地址指向的内存区域中.

4)算数运算:包括加、减、乘、除、异或等.

5)实现内核中断:int 0x80; ret和call gs:[0x10];ret.

6)尽量避免使用的gadgets:不要使用以leave结尾的gadgets,会污染栈帧;不要使用包含pop ebp的gadget,同样也会污染栈帧.

2.3 函数信息获取

在windows操作系统中本文采取IDA Pro反汇编可执行二进制文件,编写idc脚本对可执行二进制文件进行分析.IDA Pro是一种交互式、可编程的、可扩展的、多处理器的交叉Windows或Linux WinCE MacOS平台主机分析程序反汇编工具.是一个常用的静态反编译软件,对于0day世界的许多成员和shellcode安全分析师来说是不可或缺的工具.

在Linux操作系统中采用pin动态二进制插桩工具来获取函数信息进行分析.pin是一种动态二进制检测框架,适用于x86、x64构架,一般用于程序动态分析,支持windows、Linux以及OSX.在计算机安全领域有重要的应用.

2.4 基于函数指针进行的攻击

近些年来已经有成熟的保护机制来预防通过覆盖函数的返回地址进行的攻击.Cowan提出的金丝雀方法(canary),通过在栈中插入关键字canary,当函数返回时动态检测canary的值是否被修改,以此来检测函数的返回地址是否被修改.文献[15]以软件和硬件的方式建立影子栈来存储函数的返回地址,并在函数返回时检测返回地址的值是否被篡改,以此来检测覆盖函数返回地址的攻击.

C语言是计算机编程语言,C语言的学习与研究对于操作系统的安全至关重要[16].由于针对函数的返回地址进行攻击的保护机制日趋完善,所以攻击者把目光投向函数指针.在c/c++中被定义的函数指针是一个指向被调用函数的地址,攻击者可以重写函数指针指向代码段中的任意地址来执行,进而控制程序的执行流.如图3所示,是一段代码以及栈空间中的示意图.

图3 函数代码段及栈中布局Fig.3 Function code segment and layout in stack

由图3中的代码可知,程序在栈空间中开辟了156个字节的缓冲区,当func中传递的参数p的字节数大于156个字节时将会覆盖函数指针fp,但不一定会覆盖函数的返地址ret.攻击者可以精心构造参数p的值使其发生缓冲区溢出.使覆盖函数指针fp的地址为代码段的地址(return-into-libc和ROP),因为代码段为可执行权限,而DEP无法防御这种攻击.

3 基于fpDetect的解决方案

fpDetect检测方案虽然实现简单,但效率高,检测的误报漏报率低.

3.1 基本检测思想

应用程序中只包含两种函数,本地函数与动态库函数(简称库函数),本地函数调用本地函数时使用直接跳转指令如call xxx(xxx为本地函数的地址),因为本地函数的地址固定,而库函数的地址采取运行时重定位机制,只有在运行时库函数的地址才可以确定,故在调用库函数时采取间接跳转指令,如call eax(其中eax为调用的库函数的地址).另外,在通过使用函数指针调用函数时也是间接调用的,例如可以使用call eax来调用函数指针,eax中地址便为被调用的本地函数的入口点地址.

本文提出的检测方法fpDetect是一种动静结合的方法,首先使用IDA Pro对二进制文件进行反汇编,编写idc脚本取得程序中除调用交叉引用函数以外的间接跳转指令的地址,然后分析这些间接跳转指令将要执行的指令,若将要执行的指令能够形成连续的gadgets,则判断为函数指针攻击.

核心思想就是检查部分间接跳转的目标地址中所执行的指令是否为连续的gadgets.因而这是一种动静结合的技术.

图4 fpDetect检测流程Fig.4 fpDetect detection process

3.2 gadget需要满足的条件

gadget需要满足下面的几个条件:1、指令数不超过6;2、组成gadget的指令都为副作用无关指令.副作用指令是在指令运行时修改EFLAGS寄存器的指令.为使得gadget的执行更具有灵活性,一般组成gadget的指令数目为2到3条.由于正常程序执行时存在大量的副作用指令,故判断执行的指令中是否全为副作用无关指令也是判断其是否为gadget的一个重要条件.

4 实 验

本文所做基于函数指针的ROP攻击是在Linux操作系统下进行的.gadget 1覆盖原函数指针fp,gadget 2~gadget n依次存放于buf中,此时的gadget 1相当于一个跳板将执行流引入到gadget 2上,然后依次执行gadgets.

DBI的检测思想:如果函数调用不是从库函数或者本地函数的入口点调用,则将其判断为非法调用,即检测到ROP攻击[17].而这种检测方法忽略了正常函数调用库函数时,也会存在非入口点跳转这种情况.正常的非入口点跳转是由编译器决定的,非入口点跳转的存在可以使程序的执行变得更加简便.非正常的非入口点跳转发生时,当距离ret较近时便有可能形成gadgets.

DROR的检测思想:判断程序运行时连续使用的ret指令的数量是否超出检测阈值,若超出这个检测阈值则判断其为ROP攻击[18].同样这种检测方法也忽略了正常的程序执行时也会有连续执行ret指令的情况,会增加了检测的误报与漏报率,针对这种情况,本文提出了fpDetect检测方法,来提高检测的准确性.

4.1 实验环境

本文所做实验均在操作系统Linux ubuntu 14.04.5 32/64位、windows 10和windows XP中进行.fpDetect检测方法在Linux与windows中同样适用.

4.2 注入攻击与检测

在Linux操作系统下本文实验中注入攻击部分所使用的弱点程序为图3中所示的程序,被注入的shellcode为shellcode database中真实的shellcode,如下所示:

execve(“/bin/sh”)

xorecx%,ecx%

mulecx%

pushecx%

push0x68732f2f

push0x6e69622f

movesp%,ebx%

moval%,11

int0x80

其对应的硬编码如下:

shellcode="/xeb/x0b/x5b/x31/xc0/x31/xc9/x31/xd2"

shellcode+=/xb0/x0b/xcd/x80/xe8/xf0/xff/xff/xff"

shellcode+="/x2f/x62/x69/x6e/x2f/x73/x68"

最终攻击程序构造的payload如下:

payload=shellcode+'A'*(128-len(shellcode))

+p32(shellcodeaddr).

当执行了read函数之后,fp位置的地址被覆盖为一个作为跳板的指令的地址,使得程序的执行流去执行shellcode.最终攻击成功之后的截图如图5所示.

图5 攻击成功之后的截图Fig.5 A screenshot after a successful attack

在windows操作系统下本文实验中的注入攻击是借助user32.dll来完成的,所使用的弱点程序与在Linux操作系统中使用的程序一样.首先编写shellcode(本文所编写的shellcode是调用MessageBox函数),然后在user32.dll中寻找跳板指令push ebp;ret.注入攻击成功之后将弹出图6所示的窗口.

图6 攻击成功Fig.6 Attack success

在Linux中通过使用pin对二进制文件插桩间接跳转指令,去除调用库函数的间接跳转指令的地址,调试剩余的间接跳转指令发现当函数调用函数指针时控制流跳转去执行栈空间中的shellcode,由此判断为注入攻击.

4.3 ROP攻击及其检测

4.3.1 ROP攻击的实现

在linux操作系统中构造ROPchain时,如果是简单的gadgets,可以通过objdump来查找.但当我们寻找一些复杂的gadgets的时候,可以借助一些查找gadgets的工具,例如Ropme、Ropper和ROPgadget等.但这些工具多数只能应用于linux操作系统中,所以本文函数指针ROP攻击实验在linux32位与linux64位操作系统中进行.

ROP攻击所使用的ROPchain是从shellcode database中的shellcode经过改写之后在库中找到的gadgets构造的ROPchain.在shellcode database中随机选取了十个简短的shellcode构造了ROPchain,用于进行本次的实验.

为了使得本文更具有说服性,本文采用Linux操作系统中自动构造工具ROPgadget构造的ROPchain举例说明基于函数指针ROP攻击的实现.由于fpDetect检测方法对于如何覆盖函数指针没有要求,所以实验中选择了图3所示的弱点程序进行实验.除read函数外还有大量的危险库函数,比如strcpy、memcpy和malloc等.本文以read函数为例,通过发送测试代码实施ROP攻击.当执行了read函数之后的栈空间图如图7所示.

图7 read函数执行前后的栈空间Fig.7 Stack space of the read function before and after execution

在linux32位操作系统中发送测试代码,当read函数执行之后缓冲区被填充上gadgets的地址,其中函数指针fp的位置被替换为gadget1的地址.这个地址很关键,因为它的作用是将程序的执行流引到gadget2上去执行.本文通过精心构造及寻找,最终选定了add 0x2c ,esp%;ret作为gadget1处地址所存储的指令.在这里值得一提的是,因为库中不一定有我们想要的那条指令,所以我们应该在库中寻找与我们需指令执行效果最为相近的指令,然后再调整gadgets在缓冲区中的具体位置,以此思想来构造真正的攻击.图8为实施ROP攻击成功之后的截图.

图8 32位操作系统中攻击成功截图Fig.8 A screenshot of the attack in the 32-bit operating system

在linux 64 位操作系统中基于函数指针的ROP攻击的实施与在32位操作系统中有所不同,其内存地址的范围由32位变成了64位.但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常.其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9中,如果还有更多的参数的话才会保存在栈上,所以我们需要寻找一些类似于pop rdi;ret的这种gadget.图9为实施ROP攻击成功之后的截图.其中amd64-64-little代表由AMD公司开发的64位元的处理器架构.

图9 64位操作系统中攻击成功截图Fig.9 A screenshot of the attack in the 64-bit operating system

4.3.2 函数指针ROP攻击检测

针对函数指针ROP攻击检测本文分别在windows10操作系统与linux 64位操作系统中进行.在windows 10中,使用IDA Pro对二进制文件进行静态分析,编写idc脚本获取程序中的除调用库函数的间接跳转指令的地址,然后对其进行动态分析;在linux操作系统中使用pin动态插装,取得函数信息并进行gdb调试.如表1所示,fpDetect检测全部检测成功.与DBI的检测方法相比较,fpDetect检测方法考虑了函数的非入口点跳转,提高了检测的准确率.与DROP相比,DROP只考虑到了栈溢出ROP攻击,而没有考虑到覆盖函数指针的攻击,对于本文中构造的攻击DROP检测方法失效.

表1 性能测试表Table 1 Performance test table

本文采用的是动静结合的检测思想,所以检测的准确性会大大提高.本文通过实验对计算机中的程序进行真实检测,结果如表2可知,没有误报产生,这也进一步验证了本文方法的有效性.

表2 误报测试结果表Table 2 Misreport test result table

5 总 结

对于目前发起的ROP攻击大多数是利用缓冲区溢出后覆盖函数的返回地址后执行gadget,而对于通过覆盖函数指针发起ROP攻击的研究很少.本文通过实验证明了这种攻击是存在的.因为函数指针在c/c++中普遍存在,所以很容易被攻击者所利用.本文提出的fpDetect检测方法能够检测基于函数指针发起的ROP攻击,同时也提高了检测的准确率.但是除了检测攻击,还要把更多的精力放到防御上.

由于近年来大多数研究集中在应用层代码重用攻击上,而面向内核级的代码复用攻击将会给计算机带来更加实质性的破坏[19],所以接下来作者将要研究内核级代码复用攻击.并且会把更多的研究重点放到防御上.

猜你喜欢

攻击者指令程序
基于贝叶斯博弈的防御资源调配模型研究
基于 Verilog HDL 的多周期 CPU 设计与实现
《单一形状固定循环指令G90车外圆仿真》教案设计
给Windows添加程序快速切换栏
试论我国未决羁押程序的立法完善
关于ARM+FPGA组建PLC高速指令控制器的研究
正面迎接批判
正面迎接批判
“程序猿”的生活什么样
英国与欧盟正式启动“离婚”程序程序