作者:ze0r @360A-TEAM
公眾號:360安全監測與響應中心
相關閱讀:[下篇]從補丁diff到EXP--CVE-2018-8453漏洞分析與利用
在本篇文章中,我們將對CVE-2018-8453(Windows win32kfull.sys內核提權漏洞)進行深入分析。
因國內外各大安全公司和平臺主觀和客觀上的各種原因,該漏洞的技術分析一直模糊不清,甚至帶有故意的錯誤,為還原真實,我們以漏洞為主,卡巴斯基的分析文章為輔進行分析。現將分析過程和利用對外分享發布,以供學習參考。
前言
CVE-2018-8453漏洞是一個Windows內核提權漏洞,由卡巴斯基官方于野外發現用于APT中攻擊中東地區國家。在微軟發布了更新補丁后,卡巴斯基也于第二天發布了關于這個漏洞的更加詳細的分析,但仍然諱莫如深,以及多個故意錯誤(可能是因為卡巴斯基擔心該漏洞可能被作為Nday利用)。此外,國內兩大安全平臺關于該漏洞的描述文章,也為谷歌直接翻譯。故意錯誤加上翻譯錯誤,讓人無法清楚的知道該漏洞的原理和利用。
為了還原真實,我們以直面漏洞為主、卡巴分析文章為輔的方式對該漏洞進行分析和學習。
相關鏈接:
微軟官方的補丁和漏洞簡介可以看鏈接:https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2018-8453
卡巴斯基的分析文章鏈接:https://securelist.com/cve-2018-8453-used-in-targeted-attacks/88151/
正文內容
下載該漏洞的單獨補丁用bindiff與歷史補丁進行查看,可發現主要變化如下(新舊變化主要用上一個補丁日的win32k.sys來比較,下同):

在底部,看到顏色差異比較大的就是一個叫NtUserSetWindowFNID的函數,比較一下:

可見判斷流程中,多了一個IsWindowBeingDestroyed函數調用:

也就是說,主要在設置改變窗口的一個成員時,多了一個檢查。那么就意味著,本漏洞的原因是設置成員時,沒有判斷某成員造成,從名字上看,這個成員為FNID。
那么,問題在于,FNID這個成員沒有檢查又會造成什么影響呢?我們查看一下這個成員的作用,在win2000的部分源代碼中,我們可以搜索FNID來探明FNID是什么意義。

這個FNID成員是用來標識本窗口是一個什么樣的窗口,比如是一個按鈕還是一個編輯框,這一點從文章里也可以印證。而從補丁修改后新加的函數名IsWindowBeingDestroyed來看,這里是要判斷本窗口是否已經準備刪除了。從文章中說的查看ReactOS代碼,可知道準備刪除標記就是添加上FNID_FREE(0x8000)的標記。關鍵在于,不檢查FREE之后的窗口是如何觸發漏洞呢?
通過卡巴的文章,我們整理出來大致利用思路:代碼先HOOK KernelCallbackTable->產生一個主窗口->在USER32!fnINLPCREATESTRUCT回調中去查找并取消掉sysShadow窗口->以主窗口作為父窗口產生一個滾動條窗口SrollBar->發送WM_LBUTTONDOWN消息->系統處理消息時會發生USER32!fnDWORD回調,在USER32!fnDWORD回調中銷毀主窗口->這將導致主窗口銷毀從而產生USER32!fnNCDESTROY回調->USER32!__fnNCDESTROY回調中調用NtUserSetWindowFNID更改掉FNID->至此文章中開始語焉不詳,文章中說重用了sysShadow,但我們根本理解不了如何發生得重用。所以需要我們自己來動手實現。
首先我們來實現漏洞函數得調用,仔細觀察:

可以看到,要想成功更改FNID,需要滿足幾個條件,我們不可能只設置為0x4000(這個只是打個標記,不產生實際作用)。至于新FNID得值,我們可以按照文章中說的直接設置為0x2a1即可。而對于后面的條件,要求我們要設置的窗口原來不能有FNID(除0x4000和0x8000外,但這兩個標記我們打了沒用)。這里經過多次測試,發現三種情況會時FNID為空:一種是在任意類型窗口剛建立時,這時系統在用戶態主動調用NtUserSetWindowFNID來設置FNID(user32.dll中自動實現),而此時,如果沒有設置完FNID,則窗口還沒有設置消息處理函數,也就沒有處理消息的能力。而文章中提到了WM_LBUTTONDOWN消息,則可以肯定是在Scrollbar窗口完全創建之后。故此種情況不行。二種是用戶注冊的窗口類所產生的窗口,此窗口一直到銷毀,都沒有設置FNID。第三種就是文章中所說的sysShadow窗口,此窗口的作用只是產生陰影效果,但是確實FNID為空。也正是由于這個特性,本人被文章誤導很長時間。后來請教leeqwind才知道,根本不是重用的sysShadow,而是SBTrack結構。另外也可以看文章截圖:

由于本人注意力全放在了文章觸發中,還未關注利用,沒注意后面的內容,其實這里已經泄露了真相(深刻檢討反思!)。從截圖的紅框中可看到,標記是Usst,分配者又是win32k!xxxSBTrackInit。所以很明顯可知要被重用的是SBTrack。
文章中說明了需要在FNID設置為0x8000之后,再調用漏洞函數更改FNID。我們知道,一個窗口銷毀的用戶態接受到的最后消息是WM_NCDESTORY,在win32k中,這是在xxxFreeWindow函數中發送給窗口的:

可以看到在106行發送了0x82(WM_NCDESTORY)號消息,所以我們需要在106行之后想辦法回到用戶態。但同時有另外一個問題,就是注意第134行,這行把FNID打上0x4000的標記,而文章中完全是0x8000直接變成了0x82a1,沒有0x4000的標記,所以我們如果再WM_NCDESTORY消息中去更改FNID,那么確實可以馬上更改掉FNID,但是這時窗口還并沒有打上0x8000的標記(到136行中才被標記),這與文章明顯不符。所以文中所說的在USER32!__fnNCDESTROY中去調用NtUserSetWindowFNID更改FNID的做法為故意錯誤。

經過本人用pykd動態測試發現,窗口在426行的調用后,窗口句柄將不存在,NtUserSetWindowFNID函數的ValidateHwnd函數將返回0從而直接跳過FNID設置。也就是我們想要實現文章中直接將0x8000設置成0x82a1的效果,需要我們在134行到426行之間,找到一個回到用戶態的調用。
這里插播一點題外話。本人一開始是在WINDOWS7的win32k.sys做分析,結果搜索很長時間未能成功找到,直到某天twitter上有人提到該漏洞在win8.1之后可利用,在看了win10的win32kfull.sys后恍然大悟,教訓深刻!
回到正題,之后我們可以看到這里:

上面這張截圖為win10下的win32kfull.sys的IDA分析結果。我們可以看到,在256行改為了0x8000后,在266行有一個xxxClientFreeWindowClassExtraBytes函數調用:

該函數中很直接的調用了KeUserModeCallback!毫無條件的直接回到了用戶態。所以我們只要符合進入到xxxClientFreeWindowClassExtraBytes函數的條件即可。仔細查看代碼,發現258行的判斷,主要是判斷窗口是否具有擴展字節(正如xxxClientFreeWindowClassExtraBytes函數名字所暗示的那樣),有的話則調用xxxClientFreeWindowClassExtraBytes函數釋放掉,由于擴展字節是分配在用戶空間中的,所以該函數返回到用戶態讓用戶態代碼去釋放掉(至少要通知)。所以只要我們在注冊窗口類的時候,cbWndExtra成員不為0即可。在窗口銷毀時,就會在設置了0x8000之后,又回到了用戶態。當窗口以0x8000回到用戶態后,我們更改FNID為0x82a1,返回到內核態后,xxxFreeWindow繼續往后執行。
回到xxxFreeWindow函數:

其中這里,可以看到代碼判斷了FNID的值,從而決定要不要調用USER32!fnDWORD。我們知道,當一個窗口被多個其他窗口、結構引用時,即時這個窗口已經被用戶調用DestoryWindow銷毀掉了,窗口對象也要在內存中繼續存在,以等待所有引用它的地方不再引用它才真正釋放本對象內存。那么,如果我們在銷毀了一個窗口后,它的最后一個引用也釋放的時候,調用xxxFreeWindow時,我們就可以用FNID來控制流程是否要回到用戶態的USER32!fnDWORD調用。所以攻擊鏈也就此完整。
結合上面提到的,文章中提到了使用xxxSBTrackInit。該函數主要用來實現滾動條按鈕的跟隨鼠標滾動,當用戶在一個滾動條上按下左鍵,表示用戶想要拖動滾動條,此時需要開始處理鼠標的移動,讓滾動條也跟著相應動起來,在系統中,產生SBTrack結構來標記用戶鼠標的當前位置,最后當用戶放開鼠標左鍵時,表示用戶已經拖動完成,需要釋放相應SBTrack結構。
在windows 2000的源代碼中,xxxSBTrackInit部分代碼如下:

大致流程就是在調用UserAllocPoolWithQuota申請了內存后,初始化SBtrack,會將滾動條窗口以及通知窗口的指針放在本結構中,然后在2425行將當前窗口設置為捕獲窗口。之后就調用xxxSBTrackLoop開始循環來處理用戶的鼠標消息:

可以看到,xxxSBTrackLoop循環獲取消息、判斷消息、分發消息。當用戶放開鼠標時,應當停止跟蹤處理消息,退出xxxSBTrackLoop后回到xxxSBTrackInit之后,釋放SBTrack占用的內存:

而往上兩行,可以看到在釋放SBTrack之前,會解除一次spwndSBNotify窗口的引用。結合上面的分析,我們可以讓這次解除引用時,回到用戶態。如果在用戶態釋放掉SBTrack,則流程再次回到內核時,緊接著后面的UserFreePool即造成重復釋放的問題。
那么我們在用戶態如何釋放SBTrack呢?分析發現,導致釋放SBTrack一種是用戶正常放開了鼠標左鍵,還有一種就是xxxEndScroll函數:
void xxxEndScroll( PWND pwnd, BOOL fCancel){ UINT oldcmd; PSBTRACK pSBTrack; CheckLock(pwnd); UserAssert(!IsWinEventNotifyDeferred());
pSBTrack = PWNDTOPSBTRACK(pwnd);if (pSBTrack && PtiCurrent()->pq->spwndCapture == pwnd && pSBTrack->xxxpfnSB != NULL) {……..省略部分代碼…….
pSBTrack->xxxpfnSB = NULL;
/* * Unlock structure members so they are no longer holding down windows. */ Unlock(&pSBTrack->spwndSB); Unlock(&pSBTrack->spwndSBNotify); Unlock(&pSBTrack->spwndTrack); UserFreePool(pSBTrack); PWNDTOPSBTRACK(pwnd) = NULL; }}
xxxEndScroll函數判斷了主要根據窗口的線程信息中存放的SBTrack和pq->sqpwndCapture()。

而我們的程序是單線程,由于每個線程信息是屬于線程的,所以線程創建的所有窗口也都指向同一線程信息結構。所以,即使SBTrack所屬于的Scrollbar窗口已經釋放了,只要還是同一線程創建的新窗口,pSBTrack也還是原來的。而qp->spwndCapture==pwnd如何繞過呢?我們如果創建新的窗口,給這個新窗口發送的消息和操作,pwnd則為新窗口,這顯然不會等于在xxxSBTrackInit中設置的捕獲窗口----舊窗口。
通過測試發現,這個Capture窗口的設置,只要簡單的在用戶態調用SetCapture API即可直接設置。所以我們只要直接調用API即可讓xxxEndScroll中的判斷完全通過。
在搜索之后,發現可以通過如下路徑調用xxxEndScroll函數:

向一個窗口發送WM_CANCELMODE-> xxxDefWindowProc判斷消息->調用xxxDWP_DoCancelMode-> xxxDWP_DoCancelMode 判斷當前線程信息中pSBTrack-> xxxEndScroll。而上面我們知道,所有的窗口都在同一線程中創建,所以這里的判斷也可以通過!
整理一下流程:
HOOK KernelCallbackTable->注冊窗口類,WNDCLASSEXW.cbWndExtra設置為4->產生主窗口 ->以主窗口作為父窗口產生一個滾動條窗口SrollBar->發送WM_LBUTTONDOWN消息->系統處理消息初始化SBTrack結構并開始循環->發生fnDWORD回調,fnDWORD回調中銷毀主窗口->銷毀主窗口,釋放擴展字節xxxClientFreeWindowClassExtraBytes->xxxClientFreeWindowClassExtraBytes系統調用回調fnClientFreeWindowClassExtraBytesCallBack->fnClientFreeWindowClassExtraBytesCallBack HOOK中調用NtUserSetWindowFNID更改掉窗口FNID->創建新窗口并調用SetCapture設置新窗口為捕獲窗口->xxxSBLoop返回后解除主窗口引用->由于這是主窗口唯一的一個引用,這次解除導致徹底釋放主窗口對象,xxxFreeWindow函數執行->由于主窗口對象的FNID已經被更改,xxxFreeWindow函數執行過程中將再一次回到用戶態->用戶態向新窗口發送WM_CANCELMODE消息->系統處理WM_CANCELMODE消息,釋放了SBTrack->流程返回到內核繼續執行xxxSBTrackInit函數最后的釋放SBTrack->重復釋放SBTrack!
值得說明的一點是:在上面這個流程中,完全跟sysShadow窗口沒有關系,自然也跟本不需要HOOK __fnINLPCREATESTRUCT回調。
下面看一下具體代碼實現:
首先,我們設置一下回調HOOK,這里就直接用fs來獲取PEB了:

創建主窗口及ScrollBar:
WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = DefWindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 4; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CVE8453)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = L"WNDCLASSMAIN"; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
RegisterClassExW(&wcex); hMainWND = CreateWindowW(L"WNDCLASSMAIN", L"CVE", WS_DISABLED , 2, 2, 4, 3,nullptr, nullptr, hInstance, nullptr);
hSBWND = CreateWindowEx(0, L"ScrollBar", L"SB", WS_CHILD | WS_VISIBLE | SBS_HORZ, 0, 0, 3, 3, hMainWND, NULL, hInstance, NULL); 之后發送WM_LBUTTONDOWN消息: bMSGSENT = TRUE; SendMessage(hSBWND, WM_LBUTTONDOWN, 0, 0x00020002);
這將導致系統初始化SBTrack并開始循環。這導致系統回調fnDWORD:
void fnDWORDCallBack(PDWORD msg) { if (*msg) { HWND hCurrentDestroyWND = (HWND)*((DWORD*)(*msg)); memset(ClassName, 0, 0x10); GetClassNameA(hCurrentDestroyWND, ClassName, 0xF); if (!strcmp(ClassName, "ScrollBar")) { if (bMSGSENT) { bMSGSENT = FALSE; DestroyWindow(hMainWND); } } } fnDWORD(msg);}
由于在運行過程中,DWORD回調會執行很多次,所以我們加一個全局變量bMSGSENT來控制。在系統執行DestroyWindow時,由于已經預留了擴展字節,所以會回調到用戶HOOK:
void fnClientFreeWindowClassExtraBytesCallBack(PDWORD msg) {
if ((HWND)*(msg + 3) == hMainWND) { hSBWNDnew = CreateWindowEx(0, L"ScrollBar", L"SB", SB_HORZ, 0,0, 0, 0, NULL, NULL, NULL, NULL); SetWindowFNID(hMainWND, 0x2A1); SetCapture(hSBWNDnew); } fnClientFreeWindowClassExtraBytes(msg);}
我們在fnClientFreeWindowClassExtraBytes回調中,直接設置FNID。由于后面還有捕獲窗口的檢查,所以我們一并創建窗口并且設置為捕獲窗口。當流程回到系統后,發現捕獲窗口已經改變,退出了xxxSBTrackLoop函數并開始釋放SBTrack內存空間,在解除對主窗口的引用時,會導致調用xxxFreeWindow釋放主窗口內存對象,由于我們已經改變了FNID,所以再次回到用戶態。此時消息為0x70:

所以在fnDWORD中,判斷消息:
if ((*(msg + 1) == 0x70) && (hCurrentDestroyWND == hMainWND)) { SendMessage(hSBWNDnew, WM_CANCELMODE, 0, 0); }}
WM_CANCELMODE將導致SBTrack被釋放,從用戶態返回后,xxxSBTrack繼續釋放SBTrack將導致重復釋放!

最后:非常感謝leeqwind的幫助!在分析過程中給了很大的幫助!再次感謝!極力推薦他的博客:https://xiaodaozhi.com/
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/784/
暫無評論