摘了两篇介绍Heap Spray的文章

  下面是以前摘的两篇介绍Heap Spray的文章。

了解Heap Spray

Heap Spray:高危漏洞的垫脚石

Heap Spray原理浅析

Heap Spray:高危漏洞的垫脚石

来源:本站转载 作者:佚名 时间:2011-04-10 http://www.myhack58.com/Article/60/76/2011/30041_3.htm

网络木马已经成为当今网络世界里最大的危害,而它们的散播源,则来自各个知名或不知名的网站,经由网页浏览器破窗而入,悄悄地在受害者的系统里扎根潜伏,最终给受害者带来一定的损失,这是一种几乎无法完全防御的瘟疫。

装上金山网盾以后,小茹突然发现网络世界是多么的危险。仅在短短十分钟的时间里,金山网盾就已经弹出了三十几次网页木马报警窗口。最令她奇怪的是,她刚才只不过是查了一些有关会计的数据资料而已,而且那些网站看起来也都很正常。
因此,小茹也没有过于在意,将报警窗口关闭后就继续做自己的事情了。
几天过后,小茹已经对时不时弹出来的木马拦截警告框见怪不怪了。但是一个频繁出现的词语却还是在她心里留下了深刻印象。在木马拦截警告里经常会出现“发现iexplore.exe试图触发Heap Spray漏洞,已被成功阻止!”的字样。
什么是“Heap Spray”?小茹查了很多网站,却只找到一些已经被公开了技术细节的过时网页木马源代码,一番搜索下来,这个词组反而越发神秘了……我们可以通过词典查到“Heap”是“堆”的意思。那么,“堆”是什么呢?“Heap Spray”又到底是什么呢?
“堆” (Heap)经常与“栈”(Stack)同时出现,很多技术人员也都习惯于将这两者连起来读作“堆栈”,实际上它们却是两种不同的概念。在信息安全和编程 方面,除非特别指明是代表“数据结构”,否则大家提到的“堆”和“栈”,实际上都是指操作系统对内存管理实现的两种不同分配形式。

栈:后进先出无 论是数据结构领域还是内存分配方面,对于“栈”(Stack)的结构描述和用法都是很类似的。它是一种只能在某一端插入和删除数据的特殊线性表。你可以把 栈想象成筒装“品客”薯片,这个长筒一端封口一端开口,当这个长筒里没有数据时,我们称之为“空栈”。长筒在流水线上被一片片的薯片填满的过程称为“进 栈”(PUSH),在操作系统里,表示将一定的数据填进栈结构里。从这样的结构里我们不难看出,当你要从栈里取数据时,你是只能从最后一片薯片开始取的, 而不能一开始就指定了要取位于筒底的第一片薯片,你也不能任意打乱它们的排列顺序,而只能老老实实的从最后一片开始取到最早放进去的那一片,这就是“后进 先出”,而取数据的过程,被称为“退栈”(POP)。
栈在系统里通常由操作系统自己分配,用于储存局部变量、函数所需的参数等。在Windows系统里,最常见的栈操作就是各种系统API函数的调用。
堆:按需索取堆在数据结构和内存分配方面的工作方式并不相同,在数据结构中堆是一种特殊的二叉树,而我们平时提及的堆是一种内存分配的类型,它是一种类似于链表的结构。
操作系统自身定义了许多链表,其中一个链表专门用于记录当前尚未使用的内存地址,当一个程序申请一块空闲内存的时候,它就是在请求一个堆。系统在空闲内存地址链表中寻找一个足够大小的内存空间并将它标注出来,之后再进行分配。
堆 被用于存储动态数据变量,你也许会想,前面提到的栈结构不也是用于存储数据吗?它们的确都是用于数据存储,但是,栈是由系统分配的、预定义了大小的内存区 块,系统是在严格的控制下对栈进行管理的。栈的大小由操作系统决定,在Windows系统下这个大小为1MB或2MB,这样的设置对于存放更新频繁的临时 变量十分合适,但是它不能对程序在运行中另外需要分配的未知数据进行预设,这时候就要用到堆了。
因此,堆一般被用于存放事先不知道数量和大小的数 据变量,以及占用内存空间超过栈大小的大尺寸对象等。操作系统默认在每个程序执行时都预先给它分配一个堆用于存放进程初始数据,程序在运行时还可以根据需 要另行申请别的堆。堆在使用结束后系统并不会自动清空,只能由开发人员自行调用指令删除使用过的堆。如果开发人员没有对被用过的堆进行处理,它就会一直驻 留在内存空间浪费资源。当内存里能用的空间都被消耗殆尽时,程序乃至系统自身都会发生异常甚至系统崩溃。但是,如果程序被关闭,系统将会自动释放所有与这 个进程有关的内存。
栈溢出和堆溢出一个被申请了的指定大小的内存空间,无论是堆还是栈,都被统称为“缓冲区”(Buf fer)。因为系统要求这个空间必须大于实际数据需要的大小,也就是一个贴着“最大可装100片”的筒装薯片里往往不可能真的装满100片薯片。但是如果 有人刻意往标注了“最大可装100片”的筒子里塞了200片薯片呢?毫无疑问,多出来的100片将会在流水线上散落得到处都是,甚至还会掉到其他的薯片筒 里去,这个现象就是“溢出”。
 在 内存空间里发生的溢出往往会让情况变得很复杂,因为内存里各个指令和数据的地址是相当紧凑的,并没有多少空闲内存刚好可以接纳这些超额的数据,那么数据会 往哪里去呢?它可能会恰好覆盖到另一个API的栈数据,从而成为别人的参数,也可能覆盖到自己的下一个指令,导致CPU执行一个无法执行的指令,最终造成 内存异常。在早期的Win98系统里,这个问题将会直接导致蓝屏出现。而在内存管理机制相当完善和严谨的NT架构系统里,这种内存异常只要不是发生在内核 领域,就能顺利触发系统的异常处理机制,令系统强行中止发生异常的程序,于是我们能看到由溢出引发的程序崩溃。而这个运行机制就引出了我们今天的主角—— Heap Spray技术。

Heap Spray:溢出漏洞的垫脚石虽然前面将溢出解释得十分简单,但是实际环境里要实现真正 有用的溢出并不容易。不过我们可以用下面这个简单的实验来再现溢出。在Windows XP中选择“开始 | 运行”,输入“cmd”后回车打开命令行输入窗口,在里边输入“dir \ \?\AAAAA……”(超过四行的A),由于命令行输入窗口里用于复制字符串的API忽略了长度检查,就导致了栈溢出,溢出的字符串数据在内存中编码为 “00 41”(A的Unicode编码),因为“41”是没有对应任何汇编指令的,所以溢出发生后很多进程的内存空间受到波及,最直接的一个影响就是将CPU下 一步执行的指令覆盖成了不可被解析执行的“41 00”。这会触发了程序异常处理机制SEH(Structured Exception Handling,结构化异常处理),而由于SHE运行需要的内存空间也已经被同样的“41 00”覆盖了,最终预置的异常处理机制都无法执行了出现崩溃退出的情况。
如果将溢出用的“A”换作“q”,这个过程又会不一样了,因为“q”的 Unicode编码是“00 71”,在发生溢出后,CPU执行的指令将会是“7 1”,这对应着汇编指令“JNO”(不溢出跳转)。这个溢出的内容被执行后,紧随在“71”之后的“00”会被视为跳转地址,于是这个指令最终的意思是 “NOP”(空指令,不跳转到任何地方)。此时,因为剩下来执行到的指令全都是“空”,CPU不会遇到无法执行指令而退出的情况,因此用户很难发现异常。 但是如果这个溢出是由黑客运行的,执行的就可能是一个攻击指令了。
溢出的难点在于寻找它的溢出根源、溢出类型以及相关地址,并不是随便在某个程序 界面里胡乱输入一些字符就能实现有效溢出的。而“程序异常处理机制”(SEH)的存在更是为溢出增加了一定难度,有时候虽然攻击者构造的恶意执行指令 (Shellcode)已经成功触发了溢出,但是随之被触发的SHE会将程序跳转到SEH指定的异常处理过程中去,这会使攻击者构造的指令只能引发一个错 误提示或程序崩溃的结果。
因此,攻击者就必须想办法在溢出发生时把SHE跳转用另外一个地址给覆盖掉,这样一来就控制了程序在遭遇异常后执行的代码流向,一旦CPU被成功带到攻击者放置了攻击代码的位置,攻击者的意图就达到了。
为 了实现有效的溢出攻击,攻击者使用特定大小的Shellcode内容来重复申请堆,并将Shellcode数据填充进去,这段数据块一定要足够大才能确保 溢出后能实现它真正的功能。由于目前Shellcode在网页木马中使用的机会较多,它们必须依赖于JavaScript执行,而如果用户当前打开的网页 较多的话JavaScript申请到的内存就不一定还能出现在预期的范围内,所以攻击者通过重复填充一块又一块内存空间的手段来提高命中率。通常申请堆的 步骤在触发溢出之前就已经做好,只要成功触发了溢出通常就能使SHE失效并实现恶意代码执行。这个手段的统称,就是“Heap Spray”。
如 果用户对网络安全有兴趣并学习了一些溢出教程,会发现这些教程大部分会告诉用户要设法将恶意指令写到“0x0C0C0C0C”这个内存位置上,这是由 JavaScript自身特性决定的。JavaScript解释器在请求堆的时候是从“0x00000000”的位置开始申请内存块的,如果攻击者只请求 了一个堆,就无法让溢出后的程序跳转到指定指令所在的内存位置。因此攻击者需要构造至少几百个堆,每个堆里的内容都是一样的,最终会有一个堆刚好分配到 “0x0C0C0C0C”内存位置上,这个过程就是“Heap Spraying”。
攻击:访问网页即中招下面我们以今年七月份爆发的最新0day漏洞“MPEG-2 ActiveX Remote Buf fer Over f low Exploi t”为例,介绍一下Head Spray是怎么让恶意代码被成功执行的。
这个漏洞的根源在于MPEG2视频组件的msvidctl.dll模块里有个方法调用的系统函数ReadFile被传递了错误的参数类型。在大家都是“乖宝宝”的情况下,这个隐患不会被暴露出来,然而,又有人不按规矩出牌了。
首 先,攻击者编写一段Shellcode(恶意指令),这段代码要小于溢出时能构造的有效溢出代码的空间。然后使用总共占据0x30000个字节的空指令 NOP加上末尾的Shellcode代码,最终构造出0x040404大小的数据块。通过反复申请300个堆将这数据块内容填充进去,这样就能保证至少最 后一块内存位置是0x0C0C0C0C。
如果有人观察过多种漏洞溢出利用代码的示范样本就会发现,它们几乎都是以大量的空指令打头阵的,为什么不 直接干脆用Shel lcode自身来反复填充内存位置呢?这是因为JavaScript解释器可能会由于用户浏览了多个网页而不能从最低地址开始申请堆,所以它申请到的内存 位置可能会与攻击者设想的起始处有偏差。这样会导致Shellcode到0x0C0C0C0C位置的时候只有部分指令被溢出,也就是JavaScript 申请到的堆并没有按照攻击者设想的那样刚好把代码开始的第一个字节填充到0x0C0C0C0C这个位置上,这样一来当溢出发生时CPU执行到这里的代码就 会由于缺少了一部分指令而导致溢出失败。而如果攻击者在Shellcode代码的前面以大量空指令填充呢?由于代码前部是空指令,它们被截断成多少份也不 会影响到后面实际代码的执行。即使一段恶意指令被覆盖到0x0C0C0C0C位置时前面少了很多空指令,它仍然能被执行。
当然,有时候即使用了大量空指令做铺垫,也会出现溢出失败的情况。
那是因为攻击者的代码构造不合理或Shellcode只有一部分进入了0x0C0C0C0C位置。因此要想让溢出成功几率更大些,计算一个合适大小的Shellcode数据块是相当重要的。
回 到前边提到的MPEG-2漏洞,当攻击者以适当长度的空指令和Shel lcode代码反复填充了多个内存地址后,0x0C0C0C0C位置也被这段代码所占据,然后溢出代码调用MPEG-2组件加载一个精心构造了错误内容的 GIF文件,这个文件第14个字节开始的内容导致了ReadFile函数发生溢出。溢出数据里的0xFFFFFFFF和0x0C0C0C0C刚好将SEH 地址覆盖,于是SHE的跳转地址变为0x0C0C0C0C。这时候又由于前边发生了溢出而启动了异常处理机制,程序代码会执行SHE。在正常情况下,这一 步后应该会导致程序崩溃或执行自身预置的异常处理过程,例如弹出错误消息提示等。但是现在,跳转地址被改写为0x0C0C0C0C,于是CPU就跳转到 0x0C0C0C0C位置去执行攻击者预先编写的Shellcode了。

虽然堆是由程序员自己控制的,但是这并不意味着我们可以随心所欲地指定程 序在某个内存位置去新建一个堆,但是我们能控制每个堆的大小以及数量,而且堆的初始位置是可以被预测的。因此利用大量重复数据生成堆的Heap Spray技术成了控制溢出代码位置的最流行手段。虽然实际上我们仍然无法真正控制代码被填充到指定内存位置,但是通过大量的循环,总能有一段代码被成功 填充到指定位置上,这样就能被视为控制成功了。
“Heap Spray”还具备跨平台跨浏览器的能力,特别是在基于JavaScript的攻击方面,无论用户是使用漏洞很多的IE浏览器还是宣称安全性较强的Firefox,攻击者总能找出一个适合它们的攻击手法,再结合Heap Spray溢出攻击总有机会实现。
因此,“Heap Spray”并不是指某一种漏洞或木马手段,它自身是一种合理存在的技术手段罢了,只不过被大量用于溢出Shellcode,成为恶意木马得以顺利执行的垫脚石。

防范:如何阻止Heap Spray
在 这个网页木马满天飞的时代里,“裸奔”(不在电脑里安装任何信息安全工具)已经成了疯狂的代名词,你永远无法保证今天还正常浏览的网站是否明天就会被黑客 入侵并放上了一个网页木马。而也许这个木马恰巧可以攻击你电脑上尚未修补的漏洞。因此在电脑中安装杀毒软件是非常必要的。但是这样还不够,杀毒软件通常只 能在病毒木马已经被下载或执行时进行补救,它并不能预防或阻止溢出的发生,而这个触发的溢出很可能存在针对杀毒软件攻击代码,这些代码可以关掉杀毒软件, 此时系统也就毫无安全性可言了。因此仅仅安装杀毒软件并不能百分之百地防范针对漏洞的攻击,用户还必须选择一款网络安全防护工具。
目前许多厂商都推出了能够检测浏览器和网络行为的监控工具,如金山网盾、趋势云安全、超级巡警等。
那 么监控工具是如何发现Heap Spray嫌疑代码的呢?首先,它将自身监控模块注入浏览器进程,或者直接将自身作为浏览器的一个BHO对象让浏览器主动加载。随后,当浏览器的脚本解释 器开始重复申请堆的时候,监控模块记录堆的大小、内容和数量,当判断这些重复的堆请求到达了一个阀值或覆盖了指定的地址时,监控模块会阻止这个脚本执行过 程并弹出警告,由于脚本执行被中断,后面的溢出也就无法实现了。于是一般情况下浏览器不会发生异常,就像用户刚才根本没有被恶意网页纠缠过。
然 而,监控工具也仍然可能会存在问题,例如一种新的针对监控工具的溢出代码,就有可能会令它们的模块失效,最终用户还是被木马渗透。虽然当前还没有发生这种 攻击,但是当攻击者们将监控工具研究透以后,他们总能找到绕过的方法。因此,有经验的用户最好能在使用监控工具的基础上继续安装一款主动防御类工具,如 SSM等。

 

Heap Spray原理浅析

Magictong 2012/03

 http://blog.csdn.net/magictong/article/details/7391397

摘要:本文主要介绍Heap Spray的基本原理和特点、以及防范技术。

关键词:Heap Spray、溢出攻击、漏洞利用、堆溢出

 

Heap Spray定义基本描述

Heap Spray并没有一个官方的正式定义,毕竟这是漏洞攻击技术的一部分。但是我们可以根据它的特点自己来简单总结一下。Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。

传统slide code(滑板指令)一般是NOP指令,但是随着一些新的攻击技术的出现,逐渐开始使用更多的类NOP指令,譬如0x0C(0x0C0C代表的x86指令是OR AL 0x0C),0x0D等等,不管是NOP还是0C,他们的共同特点就是不会影响shellcode的执行。使用slide code的原因下面还会详细讲到。

Heap Spray只是一种辅助技术,需要结合其他的栈溢出或堆溢出等等各种溢出技术才能发挥作用。

Heap Spray第一次被用于漏洞利用至少是在2001年,但是广泛被使用则应该是2005年,因为这一年在IE上面发现了很多的漏洞,造成了这种利用技术的爆发,而且在浏览器利用中是比较有效的。另一个原因是这种利用技术学习成本低,通用性高并且方便使用,初学者可以快速掌握。

因此,Heap Spray是一种通过(比较巧妙的方式)控制堆上数据,继而把程序控制流导向ShellCode的古老艺术。

 

为什么需要Heap Spray

首先看看现有的攻击技术:

对于已有的栈溢出堆溢出攻击方法,大部分已经很难利用了

目的

保护技术

覆盖返回地址

通过GS保护

覆盖SEH链

通过SafeSEH、SEHOP保护

覆盖本地变量

可能被VC编译器经过重新整理和优化

覆盖空闲堆双向链表

通过safe unlinking保护

覆盖堆块头

XP下使用8位的HeaderCookie进行保护,VISTA之后使用XOR HeaderData

覆盖lookaside linked list

Vista之后被移除

 

攻击者怎么办?

别急!还可以覆盖的函数指针,对象指针,但是覆盖这些指针之后怎么利用呢?强大的Heap Spray来帮你。

 

Heap Spray原理

1、应用程序内存模型。

要理解堆喷的原理,首先需要弄清楚一个应用程序在系统里面运行之后,它的虚拟内存结构是什么样的。从windows95开始,windows操作系统就开始构建在平坦内存模型上面,该模型在32位系统上总共提供了4GB的可寻址空间。一般来讲,顶层的一半(0x8000 0000~0xFFFF FFFF)被保留用做内核空间(NT系统),这种划分是可以修改的。而低地址的0x0000 0000~0x7FFF FFFF里面,前64K与后64K都是不可以使用的(前64K估计是为了兼容DOS程序或者设置NULL指针而保留,严禁进程访问,后64K用于隔离进程的用户和内核空间),因此进程可用的区域就为0x0001 0000~0x7FFE FFFF。为了进一步简化程序员的编程工作,Windows操作系统通过虚拟寻址来管理内存。从本质上来说,虚拟内存模型为每个运行中的进程提供了它自己的4GB虚拟地址空间。这项工作是通过一个从虚拟地址到物理地址的转换来完成的,并且是在一个内存管理单元(Memory Management Unit,MMU)的帮助下完成的。

 

用户空间中,可以再次进行内存模型的细化,我们可以写一个如下的简单程序进行基本的探测:

// VirtualMemoryLayout.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <stdio.h>
#include <malloc.h>
int g_i = 100;
int g_j = 200;
int g_k, g_h;
int _tmain(int argc, _TCHAR* argv[])
{
    const int MAXN = 100;
    int *p = (int*)malloc(MAXN * sizeof(int));
    static int s_i = 5;
    static int s_j = 10;
    static int s_k;
    static int s_h;
    int i = 5;
    int j = 10;
    int k = 20;
    int f, h;
    char *pstr1 = "magictong Hello World";
    char *pstr2 = "magictong Hello World";
    char *pstr3 = "Hello World";
    printf("堆中数据地址:x%08x\n", p);
    putchar('\n');
    printf("栈中数据地址(有初值):x%08x = %d\n", &i, i);
    printf("栈中数据地址(有初值):x%08x = %d\n", &j, j);
    printf("栈中数据地址(有初值):x%08x = %d\n", &k, k);
    printf("栈中数据地址(无初值):x%08x = %d\n", &f, f);
    printf("栈中数据地址(无初值):x%08x = %d\n", &h, h);
    putchar('\n');
    printf("静态数据地址(有初值):x%08x = %d\n", &s_i, s_i);
    printf("静态数据地址(有初值):x%08x = %d\n", &s_j, s_j);
    printf("静态数据地址(无初值):x%08x = %d\n", &s_k, s_k);
    printf("静态数据地址(无初值):x%08x = %d\n", &s_h, s_h);
    putchar('\n');
    printf("全局数据地址(有初值):x%08x = %d\n", &g_i, g_i);
    printf("全局数据地址(有初值):x%08x = %d\n", &g_j, g_j);
    printf("全局数据地址(无初值):x%08x = %d\n", &g_k, g_k);
    printf("全局数据地址(无初值):x%08x = %d\n", &g_h, g_h);
    putchar('\n'); 
    printf("字符串常量数据地址:x%08x 指向0x%08x 内容为-%s\n", &pstr1, pstr1, pstr1);
    printf("字符串常量数据地址:x%08x 指向0x%08x 内容为-%s\n", &pstr2, pstr2, pstr2);
    printf("字符串常量数据地址:x%08x 指向0x%08x 内容为-%s\n", &pstr3, pstr3, pstr3);
    free(p);
    return 0;
}

 

(运行结果如下环境:XP sp3系统使用VS2005编译 Release版本)

 

从这里可以看出,在用户空间,各个类型数据在内存地址的分布大概为:栈 - 堆 – 全局静态数据 & 常量数据(低地址到高地址),其中全局静态数据和常量数量都是在操作系统加载应用程序时直接映射到内存的,一般映射的起始地址是0x 0040 0000,而应用程序依赖的DLL一般都映射在这个地址之后(注:当然这些不是绝对的,DLL可以映射在应用程序本身的前面,应用程序自身也可以通过修改编译选项映射到其他地址,至于堆的区域,则很可能分布在虚拟地址空间的很多地方,但是这些特殊情况并不影响今天讨论的话题)。从上面的分析可知,一个进程的内存空间在逻辑上可以分为3个部分:代码区,静态(全局)数据区和动态数据区。而动态数据区又有“堆”和“栈”两种动态数据。

从上面的探测结果可知,堆的起始分配地址是很低的。

 

2、当申请大量的内存到时候,堆很有可能覆盖到的地址是0x0A0A0A0A(160M),0x0C0C0C0C(192M),0x0D0D0D0D(208M)等等几个地址,可以参考下面的简图说明。这也是为什么一般的网马里面进行堆喷时,申请的内存大小一般都是200M的原因,主要是为了保证能覆盖到0x0C0C0C0C地址。

3、覆盖的问题解决后,下面再来看看slidecode(滑板指令)的选取标准问题。

首先需要明确的是为什么需要在shellcode前面加上slidecode呢?而不是整个都填充为shellcode呢?那样不是更容易命中shellcode吗?这个问题其实很好解释,如果要想shellcode执行成功,必须要准确命中shellcode的第一条指令,如果整个进程空间都是shellcode,反而精确命中shellcode的概率大大降低了(概率接近0%),加上slidecode之后,这一切都改观了,现在只要命中slidecode就可以保证shellcode执行成功了,一般shellcode的指令的总长度在50个字节左右,而slidecode的长度则大约是100万字节(按每块分配1M计算),那么现在命中的概率就接近99.99%了。因为现在命中的是slidecode,那么执行slidecode的结果也不能影响和干扰shellcode。因此以前的做法是使用NOP(0x90)指令来填充,譬如可以把函数指针地址覆盖为0x0C0C0C0C,这样调用这个函数的时候就转到shellcode去执行了。不过现在为了绕过操作系统的一些安全保护,使用较多的攻击技术是覆盖虚函数指针(这是一个多级指针),这种情况下,slidecode选取就比较讲究了,如果你依然使用0x90来做slidecode,而用0x0C0C0C0C去覆盖虚函数指针,那么现在的虚表(假虚表)里面全是0x90909090,程序跑到0x90909090(内核空间)去执行,直接就crash了。根据这个流程,你可以猜到,我们的slidecode也选取0x0C0C0C0C就可以了嘛,可以参考下面的图解。

 

4、精确申请的问题。

这里以javascript的字符串为例子简单说明一下,在javascript中,字符串“ABCD”是以下面这种方式存储的:

大小

数据

结尾0字符

4字节

string的长度 * 2 字节

2字节

08 00 00 00

41 00 42 00 43 00 44 00

00 00

 

其次是堆块头的大小问题,一般来讲每个堆块除了用户可访问部分之外还有一个前置元数据和后置元数据部分。前置元数据里面8字节堆块描述信息(包含块大小,堆段索引,标志等等信息)是肯定有的,前置数据里面可能还有一些字节的填充数据用于检测堆溢出,后置元数据里面主要是后置字节数和填充区域以及堆额外数据,这些额外数据(指非用户可以访问部分)加起来的大小在32字节左右(这些额外数据,像填充数据等是可选的,而且调试模式下堆分配时和普通运行模式下还有区别,因此一般计算堆的额外数据数据时以32字节这样一个概数计算,参考《windows高级调试》)。

 

5、其它问题。

主要是绕过Javascript的内存池(在oleaut32中)和绕过系统的堆空闲链表问题,其实在这里因为在heap spray时分配的内存足够大,暂时可以忽略这系列问题。因此本文不讨论这个问题,感兴趣的可以自己查阅资料。

 

6、实例分析:一段Heap spray代码分析(查看代码注释即可)。

<script>

try{

// 设定溢出参数,返回方式等,这里是栈溢出,使用nops + ret(0x0c0c0c0c)即可

var nops = "";

var nops_size = 216;

for(var i = 0;i < nops_size;i ++) nops += "A";

var ret = "\x0c\x0c\x0c\x0c";

var payload = nops + ret; // 构造payload

 

// =============================================

//放置shellcode并进行双字节逆序的unicode转换

var shellcode = "\x33\xc9\x51\x68\x20\x79\x6f\x75";

    shellcode +="\x68\x66\x75\x63\x6b\x8d\x14\x24";

    shellcode +="\x51\x52\x52\x51\xb9\x98\x80\xe1";

    shellcode +="\x77\xff\xd1\xb9\x1a\xe0\xe6\x77";

    shellcode +="\x50\xff\xd1";

var shellcode_tmp = "";

if(shellcode.length % 2 == 1) shellcode += "\x90"; //奇数个shellcode进行补全

for(i = 0;i < shellcode.length;i = i + 2)

{

    shellcode_tmp += "%u" + shellcode.charCodeAt(i+1).toString(16) + shellcode.charCodeAt(i).toString(16);

}

shellcode = unescape(shellcode_tmp);

// 上面都是构造shellcode,暂时可以不用关注,可以直接使用%u格式来构造

// =============================================

 

// 开始构造block

var heap_size = 0xFF000;                        //JS为变量分配的堆大小 0xFF000 进过win2k扩展0x1000后正好为0x100000大小的块

var header_size = 32;                           // 堆块首大小为32 bytes

var string_header = 4;                          // 字符串长度头

var null_byte = 2;                              // 字符串结束2bytes null(unicode)

var nop_length = heap_size/2 - header_size/2 - string_header/2 - shellcode.length - null_byte/2;  // 计算nop的长度 (这里计算的是unicode字符串长度,要注意)

 

var big_block = unescape("%u9090%u9090%u9090%u9090");

while(big_block.length <= (heap_size / 2))

big_block += big_block;

var block = big_block.substring(0, nop_length); // 从空间中剪取该大小的块备用

 

// 将上面的目的地址转换成整数

var ret_parseint = 0;

for(i = 0;i < 4;i ++)

ret_parseint += ret.charCodeAt(i) * Math.pow(0x10, i*2);

ret_parseint = ret_parseint - 0x01000000; // 分配堆栈的大概基地址0x01000000

 

var block_cnt = parseInt(ret_parseint / 0x100000) + 1; // 计算需要的块数量(向上取整),以0为开始分配的地址,实际上不可能,所以一定能覆盖到

 

// 开始申请很多block这样的块

var slide = new Array();

for(i = 0;i < block_cnt;i ++)

slide[i] = block + shellcode;

 

// 溢出漏洞函数,这里仅仅是一个例子

var vuln = new ActiveXObject("Vuln.server.1");

vuln.Method1(payload);

 

}catch(e){alert(e.message);}

</script>

 

 

注意:像上面这种计算方式是比较精确的一种Heap Spray,譬如对block_cnt的计算,实际操作时不需要这么精确,像block_cnt一般都是使用>=200的数字,已经足够覆盖0x0C0C0C0C等地址了。

 

Heap Spray的优缺点

优点:

a、增加缓冲区溢出攻击的成功率;

b、覆盖地址变得更加简单了,可以简单使用类NOP指令来进行覆盖;

c、它也可以用于堆栈溢出攻击,用slidecode覆盖堆栈返回地址即可;

 

缺点:

a、会导致被攻击进程的内存占用暴增,容易被察觉;

b、不能用于主动攻击,一般是通过栈溢出利用或者其他漏洞来进行协同攻击;

c、上面说了,如果目的地址被shellcode覆盖,则shellcode执行会失败,因此不能保证100%成功

 

Heap Spray的防范和检测

1、一般来讲,应用程序的堆分配是很平滑的,分配模式也应该是随机的,或者从理论上来说随机性非常明显,不应该出现内存暴增现象,从已有的一些Heap Spray的代码来看,都会瞬间申请大量内存,从这个特点出发,我们可以设计这样一种方案,如果发现应用程序的内存大量增加(设置阈值),立即检测堆上的数据,看是否包含大量的slidecode,如果满足条件则告警提示用户“受到Heap Spray攻击”或者帮助用户结束相关进程。不过这种方式有一个缺点是无法确定攻击源,而优点则是能够检测未知漏洞攻击。

2、针对特殊的浏览器,将自身监控模块注入浏览器进程中,或者通过BHO让浏览器(IE)主动加载。当浏览器的脚本解释器开始重复申请堆的时候,监控模块可以记录堆的大小、内容和数量,如果这些重复的堆请求到达了一个阀值或者覆盖了指定的地址(譬如几个敏感地址0x0C0C0C0C,0x0D0D0D0D等等),监控模块立即阻止这个脚本执行过程并弹出警告,由于脚本执行被中断,后面的溢出也就无法实现了。这种检测方法非常安静,帮助用户拦截之后也不影响用户继续浏览网页,就好像用户从来没有遇到过此类恶意网页。

3、对于一些利用脚本(Javascript Vbscript Actionscript)的进行Heap Spray攻击的情况,可以通过hook脚本引擎,分析脚本代码,根据一些Heap Spray常见特征,检测是否受到Heap Spray攻击,如果条件满足,则立即结束脚本解析。现在QQ电脑管家里面正在使用这一技术。

4、比较好的系统级别防范办法应该是开启DEP,即使被绕过,被利用的概率也大大降低了。

 

参考文献

[1] Heap spraying

http://en.wikipedia.org/wiki/Heap_spraying

 

[2] HeapSpray Feng Shui

 

[3] windows核心编程

 

[4] windows高级调试

posted @ 2014-10-28 23:16  山岚的一缺  阅读(788)  评论(0编辑  收藏  举报
喜欢
评论
收藏
顶部