内存错误的过去现在与将来 (2012)
30 Aug 2013 | categories notes很多攻击都始于内存破坏(memory corruption),并以此作为后续攻击的立足点。
当访问对象使用的指针表达式和预期不一致时就称作发生内存错误。内存错误分为空间错误和时间错误。
空间错误包括
- 未初始化指针的解引用
- 非指针数据的解引用
- 有效指针用于非法指针运算,如缓冲区溢出
时间错误包括
- 悬空指针(dangling pointer)
- 两次释放(double free)
按照内存特性可以分为6类:基于栈的、基于堆的、整数问题、空指针解引用、格式串错误和其他。其中:
- 格式串漏洞几乎很难再被发现了
- 整数漏洞兴起于2002年,到现在仍很有生命力
- 空指针解引用曾经被大面积发现,但利用它很困难,通常需要特定应用的攻击手段
- 基于堆、栈的漏洞仍是内存错误的主体
内存错误始现于1972年,到了1995年,几名著名黑客先后发表了利用内存错误的方法,包括最经典的基于栈的缓冲区溢出,使得对于内存错误的关注激增。
- 1997年,提出非执行(non-executable,NX)栈,是针对利用栈溢出进行代码注入攻击的首个对策。
- 1998年,提出基于canary的保护。
- 1999年,出现了基于堆的溢出攻击和格式串攻击。
- 1999年,出现了return-to-libc攻击,可绕过非可执行段,直接在共享库段找执行代码,但会被随机化防住,因为攻击需要预先知道执行代码的地址
- 2001年7月,地址空间随机化(ASLR)由PaX小组提出。一开始只对栈地址实施随机化。后来推广到堆、共享库等内存区域。
- 2001年5月,出现了空指针引用攻击。
- 2003年,出现了SEH(Structured Exception Handler)覆盖攻击
- 2004年,出现了针对ASLR的堆喷射攻击(2007年,堆风水)
- 2006年,微软提出了SafeSEH以抵抗SEH覆盖攻击。
- 2008年,出现了空指针引用被用于任意代码注入。
- 2009年,提出了堆喷射的对抗技术;微软提出了SEHOP以抵抗SEH覆盖攻击。
- 2010年,出现了return-oriented-programming,可绕过非可执行段,直接在可执行代码段里找代码片段,也能避开随机化
最后,保护移动应用的内存很具有挑战性。
攻击方式介绍
return-to-libc
libc提供了类UNIX操作系统中的C运行时支持,它总是会被链接到程序中,并且它提供了对攻击者而言一些相当有用的函数(如system()
调用可以只附加一个参数即执行外部程序)。
return-to-libc攻击利用缓冲区溢出将栈中的返回地址修改为现有libc函数(如system()
)的入口地址,并通过栈传递所利用的libc函数的参数(如“bin/sh”)。这样无需注入恶意代码到程序中,且只用到了标记为可执行的代码(libc中的函数均为可执行)。当然,攻击者也只能够调用已存在的函数(即上例中的system()
),如果对于内核代码,或是检查调用来源的库,return-to-libc就不那么给力了。。
栈粉碎保护(Stack-Smashing Protection,SSP)(stack smashing就是就是栈溢出)防护能够阻止这种溢出,因为它可以检测到损坏的堆栈并且有可能移除被攻击的节(segment)。
地址空间布局随机化 (ASLR)使这种攻击在64位平台上变得几乎不可能成功,因为所有函数的内存地址都是随机的。在32位系统中,ASLR能够提供部分防护,因为只有16位地址可供用于随机化,这可以用暴力攻击在很少的几分钟内破解。
return oriented programming(ROP)
简单的说就是把原来已经存在的代码块拼接起来,,拼成一段恶意的代码。拼接的方式是通过一个预先准备好的特殊的返回栈,里面包含了各条指令结束后下一条指令的地址。
例如现在函数A里面有这么一段指令 instrA ret,函数B里面有另外一段: instrB ret。它们在正常的运行情况下没有任何关系,但是把A和B拼起来就能达到我想要的结果。于是,构造一个包含有A和B的地址的栈,先通过ret指令返回到instrA处,之后再执行ret指令时,由于栈是精心构造的,因此接下来会执行到instrB,这样就得到我想要的结果了。只要这个ret前的指令库足够大,就能实现几乎所有的程序。
heap spraying
Heap Spray只是漏洞攻击技术的一部分,需要结合其他的栈溢出或堆溢出等等各种溢出技术才能发挥作用。它在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。
传统slide code(滑板指令)一般是NOP指令,但是随着一些新的攻击技术的出现,逐渐开始使用更多的类NOP指令,譬如0x0C(0x0C0C代表的x86指令是OR AL 0x0C),0x0D等等,不管是NOP还是0C,他们的共同特点就是不会影响shellcode的执行。
利用slide code填充堆,可以覆盖堆上的某些函数指针,这样调用这个函数的时候就转到slide code代表的地址去执行了,如0x0C0C0C0C。因此slidecode+shellcode一定要超过这个地址。现在为了绕过操作系统的一些安全保护,使用较多的攻击技术是覆盖虚函数指针(这是一个多级指针),这种情况下,slidecode选取要防止程序流程转入内核空间。
一开始该技术仅应用于浏览器,后来扩展到pdf阅读器和falsh播放器,这些应用的共同点是都可以在程序内执行脚本,申请大量的堆内存。
2009~2010年各浏览器(IE、Firefox等)分别出了针对堆喷射的保护措施,但在2012年,出现了针对HTML5的堆喷射技术,可通杀各类浏览器。
SEH攻击
SEH,Structured Exception Handler,是微软提供的异常处理机制。通过SEH注册的异常处理函数是以指针方式存在于程序栈中。当程序发生异常时候。会自动调用离出现异常的地址最近(即调用链上最近)的SEH指针。
SEH攻击将SEH指针覆盖为ShellCode起始位置,然后将最近的一个指针继续覆盖为0。当程序引用这个0指针时会发生异常,于是调用SEH指针指向的函数做异常处理。
非执行栈
非执行栈保证栈上的代码不可执行。但在该措施提出不久,他的提出者就指出了return-to-libc的攻击方式。
return-to-libc后来成为运行非执行段上的注入代码的前奏。通过返回到Unix世界的mprotect
系统调用或Windows世界的VirtualProtect
API,攻击者可以显式将注入代码所在的数据区域设置为可执行。
PaX项目除了采用非执行栈,还将数据段设置为可写但不可执行,代码段可执行但不可写。此外,它还对mmap
的基地址进行了随机化,使得进程栈和第一个载入的库函数的地址是随机的。
该思路在OpenBSD和ReadHat Enterprise Linux以及Windows XP SP2中分别得到了体现和强化。OpenBSD v3.3实施了4项改进以对抗缓冲区溢出,其中就包括了著名的W^X原则:
W^X原则:内存不能同时可写和可执行。
RH Linux则提出基于内核的ExecShield方案,它设置包括栈在内的一大片内存不可执行,此外还对栈、共享库、堆、代码区域的地址进行了随机化处理。Windows则提供了DEP保护,以防代码从程序内存中执行。
为了绕过这些保护措施,2005年提出了面向返回的编程:利用代码片段组成的链完成攻击。这种方法另一个优势是可以在64bit机器上实现,64位架构下函数参数是用寄存器传递的,而return-to-libc需要参数通过栈传递。
ROP的保护措施目前还没有在主流OS里部署,未来可能的方案包括低负载的边界检查和实际可用的污点跟踪技术等。
基于canary的保护
基本思想是当进入函数时在栈上的函数返回值附近放置一个难以预测的值,当函数返回时,检查这个值是否被改变。这种保护显然会被直接改写函数返回值的方法给绕过。
一种典型的绕过方法是破坏特定的异常处理回调指针(如Windows的Structured Exception Handling,SEH机制)。2006年微软引入了SafeSEH技术以应对这种方法。
基于canary的保护措施派生出许多新的方法,比如VS的/GS选项,又比如ProfPolice,也叫做栈粉碎保护(SSP)。SSP成为了GCC 3.x的低负载的补丁,并包含在了BSD和Unubtu中。
堆攻击
堆攻击的描述最早出现在1998年,2000年的时候出现了更为高级的利用方式,2002年蔓延到了Windows平台。2003年出现了在ASLR有效时利用堆错误获得任意写(write-anything-anywhere)和信息泄漏功能的方法。
Windows XP SP2采用了一系列技术保护堆,包括禁止堆可执行、并引入canary保护堆等,但很快就被攻破了,等到2007年Windows Vista发布,微软对堆攻击的保护又进行了加强。2010年,有报告讨论了ASLR和非执行堆对堆攻击的保护效果,很遗憾地,这些现有保护均不能抵抗return-into-libc的攻击方法,此外,堆喷射(heap spraying)和堆风水(heap FengShui)技术也被引入以对抗ASLR。
堆攻击可分为三代:
- 经典的溢出,以破坏相邻的内存
- 破坏堆管理的元数据,实现“任意写”,执行任意代码
- 堆喷射和特定应用的攻击,可以绕过堆保护机制。
前两代可以通过加固动态内存分配器的方式化解,所以目前主要的攻击都是基于堆喷射的,它可以对付堆随机技术。
格式串攻击
主要影响printf
一族的函数。其原因在于这一族函数接受的参数中包含的某些格式控制符(如%x, %s, %n
等)可能会泄漏或影响参数所在栈的信息。这种漏洞可能会造成任意写或任意读取进程的地址空间。
这类错误2000年左右被发现,在兴盛几年之后,由于微软在CRT(C/C++运行库)中禁用了%n
,GNU也在C库里提供了FORTIFY_SOURCE
选项对格式串进行保护,现在已经很难利用了。
地址空间布局随机化
ASLR最初由PaX小组提出,在一开始对mmap
的基地址进行随机化,这样动态链接代码的位置在程序运行时是随机映射的。这就可以提高return-to-libc攻击的难度。后来又加入了栈随机化和PIE随机化,从而降低了ROP的成功率。随着内核栈溢出的流行,内核栈和堆的随机化也被提出来。
PaX为这些技术向Linux Kernel提交了一系列的补丁,但OpenBSD也在几乎同时研发了类似的措施,只是没有支持内核栈的随机化,因为这会破坏POSIX标准。于是双方为谁是第一个发明人打了很久的嘴仗。
Linux官方陆续在内核中支持了栈、mmap
基地址、堆和PIE的随机化,虽然没有内核栈随机化,但号称支持全部的ASLR。
Windows在Vista中加入了栈、堆、库的ASLR,但该技术是否管用又引起了一番争论。该版本中ASLR的缺陷是否在后续的Windows 7中得到修补不得而知,但2010年的一份报告中指出,第三方软件对Windows的ASLR支持非常缓慢,截至2010年6月,只有Goole Chrome和Adobe Flash Player使用了全功能ASLR,甚至诸如Adobe Reader, Mozilla Firefox 和 Apple iTunes这样的常用软件都没有支持全功能ASLR。
Mac OS X 10.5实现了库的随机化,并支持栈和堆的非可执行,显然,这不是全功能的ASLR。
现有的ASLR只能被称为粗粒度的随机化,因为仅仅随机处理了某个数据区域的基地址,其中的对象见的相对偏移则是固定的。攻击者只需猜到区域的基地址就可以畅通无阻了。于是有人提出了细粒度的、基于对象的随机化ASR,他们或者利用二进制重写技术加入随机化方案,或者利用源代码转化的方式获得自随机化的类似PIE的二进制代码。
攻击ASLR
第一个攻击ASLR的技术称为return-to-plt。它可以直接调用动态连接器的符号解析过程,获得感兴趣的符号地址。但PIE可以搞定这种攻击。
2002年,人们发现某种缓冲区溢出漏洞可以转化成格式串bug,从而泄漏关于进程地址空间的信息。于是,这成了攻击ASLR的标准过程。
2009年提出的ROP可以绕过现有的W^X和ASLR。它通过从全局偏移表拷贝数据的方法,将原始可执行程序中的代码片段串起来,从而计算出动态链接共享库的基地址。再用基地址构建传统的return-to-libc攻击。攻击成功率超过了95%,这是由于现代系统很少使用PIE的缘故。
另一种攻击方式叫堆喷射,它通过包含攻击者提供的代码的对象大面积地分配在进程的堆中,以此提高引用/执行这些代码的成功率。它通常用来攻击跨平台的浏览器,因为他们允许在内部执行JavaScript和ActionScript脚本。
堆喷射又衍生出指针推理和JIT喷射技术。前者用于在受ASLR保护的ActionScript解释器里找到shellcode的地址,后者利用ActionScript JIT编译器的可预测行为绕开DEP,将shellcode写到可执行区域。动态代码生成(dynamic code generation)也有可能受喷射的影响,因为这些代码所在页面必须同时可写和可执行,它的防护措施于2010年提出。
ASLR的效果
总的说来,ASLR的效果不错,而且可以防止很多内存错误,不仅是缓冲区溢出。然而它的问题在于它的随机性,架构限制了它的随机程度,尤其是32bit机器。对它的攻击也由此分为两类:暴力破解和(基地址的)信息泄漏。
如果采用64bit机器、实施全功能ASLR,支持PIE的话,ROP攻击很难奏效。然而,目前PIE的程序很少。
空指针解引用
一般说来,这种漏洞很难利用。然而,一份水平顶尖的报告显示,通过利用Adobe Flash Player的一个空指针漏洞,可以借由ActionScript虚拟机实现跨平台的攻击。之后,人们又发现Linux上存在一个空指针漏洞,可以绕开LSM的权限检查。RedHat Linux也被发现有个类似的漏洞,使得SELinux域可以映射到第0页上。这个漏洞向人们揭示了编译器优化可能会引出WYSINWYX的问题。
相关问题
libsafe
假设本地缓冲区不会超过当前的栈帧,于是作为动态链接库链入程序在运行时检查边界,实际上是将与之相关的库函数替代为自己的函数。
指令集随机化(ISR)用于抵御代码注入攻击,主要思想是对程序指令用每个进程唯一的密钥加密,在调入cpu执行前再解密。它不能抵抗return-to-libc和ROP。
整数溢出
整数溢出本身不是内存错误,但会导致内存错误,如缓冲区溢出或任意写。目前的方法有:静态类型检查、符号执行、整数安全运算库等。目前Linux Kernel也引入了整数溢出的保护措施,但是,仍然该漏洞仍可用。
统计分析
从1998年到2007年,内存错误数量几乎成线性增长。其中,2003年到2007年正是网络漏洞涌现的时期。这阶段正值网络发展高峰,各类新特性未经严格测试就匆忙上线,因此漏洞频现。
2007年到2010年,各类漏洞总数呈线性下降,内存错误也是如此。考虑到这四年间,代码的产出并没有减少,而有关软件可靠性的研究指出:平均每1000行可执行代码中包含75个bug,所以这种下降趋势不太可能都是由错误修复技术导致的。一个可能的原因是网络开发公司们开始意识到SQL注入或XSS攻击是多么的容易,并开始重视他们的编程质量了。此外,2007年之后,网络开发由手工作坊式的开发过渡到基于某些成熟的开发框架进行开发的阶段,这也提高了软件质量。
另外,报告的错误数量少并不意味着发现的错误少,可能由于某些原因,错误发现者们不愿意去报告自己的发现。比如,
- 软件公司不再为自己的安全专家们支付发现漏洞的高额奖金
- 软件公司将第三方发现的漏洞买下,而不是公布出来
- 由于保护措施的不断更新,发现利用漏洞的难度变大了
- 网络交易的盛行使得漏洞在网络经济犯罪中地位更重,相比公开发现,贩卖漏洞更加吸引人