CVE-2011-2018 未初始化TrapFrame.TempSegCs引发的漏洞。
漏洞描述
从ring0返回到ring3时, 正常情况下iret/iretd会以_KTRAP_FRAME.SegCs和Eip为参数返回, 并使用_KTRAP_FRAME.HardwareEsp做为ring3的栈指针。 当由Ring0返回到Ring3时,会检查SegCs的高13位,如果为0,说明被编辑过,则iret会将_KTRAP_FRAME.TempSegCs做为参数返回,并使用_KTRAP_FRAME.TempEsp做为ring3的栈指针。 在包含此漏洞的情况下,由于TempSegCs和TempEsp没有被初始化,如果这两个域被我们控制,将会造成以cpl==0权限限执行任意代码漏洞。具体见wrk中Kimacro.inc中EXIT_ALL宏的实现。
如果未初始化的变量可以被我们控制,且此时代码引用了该变量,则可形成漏洞被我们利用,如果是call或jmp之类的引用可造成任意代码执行,或者对变量进行了其它操作,这种情况可在我们控制变量指向的内存处伪造相应的结构体,造成任意地址写任意值或固定值,要视具体情况而定。
漏洞分析
TRAP_FRAME如下:
kd> dt _KTRap_frame
nt!_KTRAP_FRAME
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint4B
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
kd> u KiFastcallEntry L100
nt!KiFastCallEntry:
804df6f0 b923000000 mov ecx,23h
804df6f5 6a30 push 30h
804df6f7 0fa1 pop fs ; fs = 30h
804df6f9 8ed9 mov ds,cx ; ds = 23h
804df6fb 8ec1 mov es,cx ; es = 23h
804df6fd 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h]
804df703 8b6104 mov esp,dword ptr [ecx+4] ; esp = KPCR->KTSS->Esp0, ring0的esp
; 开始构造KTRAP_FRAME
804df706 6a23 push 23h ; _KTRAP_FRAME.HardwareSegSs, ring3 ss
804df708 52 push edx ; _KTRAP_FRAME.HardwareEsp, edx = ring3 esp
804df709 9c pushfd ; _KTRAP_FRAME.EFags, eflags
804df70a 6a02 push 2
804df70c 83c208 add edx,8
804df70f 9d popfd ; EFlags = 2
804df710 804c240102 or byte ptr [esp+1],2
804df715 6a1b push 1Bh ; _KTRAP_FRAME.SegCs
804df717 ff350403dfff push dword ptr ds:[0FFDF0304h] ; _KTRAP_FRAME.Eip, ds:[0FFDF0304h] 是SystemCallReturn(_KUser_Shared_Data +304h)
804df71d 6a00 push 0 ; _KTRAP_FRAME.ErrorCode , int 2eh中之前的 _KTRAP_FRAME由cpu填充
804df71f 55 push ebp ; _KTRAP_FRAME.Ebp
804df720 53 push ebx ; _KTRAP_FRAME.Ebx
804df721 56 push esi ; _KTRAP_FRAME.Esi
804df722 57 push edi ; _KTRAP_FRAME.Edi
804df723 8b1d1cf0dfff mov ebx,dword ptr ds:[0FFDFF01Ch] ; ebx == _KPCR.SelfPcr
804df729 6a3b push 3Bh ; _KTRAP_FRAME.SegFs
804df72b 8bb324010000 mov esi,dword ptr [ebx+124h] ; _KPCR.PrcbData.CurrentThread, esi指向当前线程
804df731 ff33 push dword ptr [ebx] ; _KTRAP_FRAME.ExceptionList
804df733 c703ffffffff mov dword ptr [ebx],0FFFFFFFFh
804df739 8b6e18 mov ebp,dword ptr [esi+18h] ; ebp = KPCR.PrcbData.CurrentThread. InitialStack
804df73c 6a01 push 1 ; _KTRAP_FRAME.PreviousPreviousMode = 1
804df73e 83ec48 sub esp,48h ; esp = _KTRAP_FRAME 建立完成KTRAP_FRAME
804df741 81ed9c020000 sub ebp,29Ch ; ebp = _KTRAP_FRAME
由以上代码可知 , TrapFrame的TempSegCs并没有进行初始化操作。
而在返回到ring3 的EXIT_ALL宏中:
;
; 这里测试TrapFrame的TsSegCs 是否被编辑过,如果被编辑过,则跳到b
; 主要测试cs的高13位是否为0,如果为0则说明被编辑过
; 通常情况下cs的高13位是不会为0的,如果被编辑过,则使用trapFrame的
; TempSegCs和TempEsp返回。
;
test word ptr [esp]+TsSegCs,FRAME_EDITED
jz b ; Edited frame pop out.
......
b: mov ebx,[esp]+TsTempSegCs
mov [esp]+TsSegCs,ebx
;
; There is no instruction that will load esp with an arbitrary value
; (i.e. one out of a frame) and do a return, if no privledge transition
; is occuring. Therefore, if we are returning to kernel mode, and
; esp has been edited, we must "emulate" a kind of iretd.
;
; We do this by logically pushing the eip,cs,eflags onto the new
; logical stack, loading that stack, and doing an iretd. This
; requires that the new logical stack is at least 1 dword higher
; than the unedited esp would have been. (i.e. It is not legal
; to edit esp to have a new value < the old value.)
;
; KeContextToKframes enforces this rule.
;
;
; Compute new logical stack address
;
mov ebx,[esp]+TsTempEsp
sub ebx,12
mov [esp]+TsErrCode,ebx
;
; Copy eip,cs,eflags to new stack. note we do this high to low
;
mov esi,[esp]+TsEflags
mov [ebx+8],esi
mov esi,[esp]+TsSegCs
mov [ebx+4],esi
mov esi,[esp]+TsEip
mov [ebx],esi
漏洞利用
为了成功利用此漏洞,我们需要解决以下两点:
- 使TempSegCs为我们可以控制的值。
- 触发漏洞,使iret/iretd以TempSegCs为选择子返回到Ring3。
对于1,可以利用NtMapUserPhysicalPages函数在内核栈中留下 被我们控制的值,原理见WRK NtMapUserPhysicalPages实现。
由于trap frame总是位于内核栈的同一个地方(见intel手册, 发生特权级切换时,CPU会从TSS中取得ring0的ss和esp 并加载到ss和esp,达到堆栈切换,当堆栈发生切换时, 内层的ss和esp就是从这里取得的,比如,我们当前所在的是ring3,当转移至ring0时,堆栈将被自动切换到由ss0和esp0指定的位置。 由于只是在由外层转移到内层(低特权级到高特权级)切换时新堆栈才会从TSS中取得,所以TSS中没有位于最外层的ring3的堆栈信息 ), 这样可以通过将 内核栈指针 向上移动,以便TempSepCs、TempEsp位于我们可以控制的区域(上次函数调用时局部变量区域)。
XP/2003下通过KeUserModeCallBack向用户模式发起一个回调,这样我们可以对用户模式代码进行HOOK, 然后在我们挂钩的函数中再进行一次系统调用 ,就可以实现将内核栈指针向上移动的目的。
KeUserModeCallback可以参考这里。
KeUserModeCallback –> KiCallUserMode ( 会调整Tss.esp0),所以当在用户层的hook函数中进行系统服务调用时,会在之前TrapFrame之上建立TrapFrame。
对于2, 查看wrk /EXIT_ALL宏 可知,当TrapFrame.SegCs的高13位为0时,会使用TempSegCs代替SegCs返回。
Intel 手册规定GDT的第一项(index=0)是保留的,被称为空(NULL)选择子,当用空选择子进行存储器访问时会引起异常。当TI=1时,Index为0的选择子不是空选择子,它指定了当前任务局部描述符表LDT的第0个描述符。 因此 我们可以在LDT的第0项建立一个可执行的代码段,然后跳到此代码段执行,并触发一个软件中断,当由内核返回时就会触发漏洞。
因为GDT和LDT位于内核地址空间, 不能被应用层程序修改。
我们可以通过NtSetInformationProcess添加的LDT项,但是受到操作系统的限制:
参考
- Base+limit 被限制在用户地址空间。
- 创建的段能被用户模式访问(DPL=3)。
- 只能创建代码段和数据段,不能为中断门或调用门。
由于只有TempSegCs和TempEsp可以控制,使TempSegCs的为8即可达到以特权级0执行任意代码。但是由于 KeSystemCallExit2 执行时的IRQL为 DISPATCH_LEVEL,因此TempSep必须指向一个非分页内存, 并且为一段可写内存(因为iret返回会需要对esp指向的内存进行操作),这里选择了ntoskrnl.exe里的HalDispatchTable.
Vista/Window 7不再为多次回调使用单独的栈,而是每次用户模式回调时将创建一个新的内核栈, 此操作由KiMigrateToNewKernelStack完成。
win7上利用此漏洞只有一个方法:移动栈基址来达到 映射到与默认地址不同的TRAP_FRAME。
这里利用段更新失败异常来达到上面的目的:无论用户模式或内核模式代码尝试去修改六个段寄存器之一时,CPU会做基本的校验来确保操作合理(例如目标选择子必须指向一个有效的GDT/LDT项)。失败时将会加载一个新的段选择子到寄存器,CPU产生中断11(Segment Not Present #NP)。
实际上,内核经常会为用户模式代码加载 CS、DS和其它段寄存器, 这里有三种情况:
- SetThreadContext 会导致CONTEXT结构体域会拷贝到远程线程的TRAP_FRAME并且加载到相应的寄存器。
- 未文档化函数NtContinue有相同的作用,但是它只能影响当前的线程。
- Windows VDM(virtual dos machine),为了在控制的环境中执行任意16位代码,需要在NTVDM.exe子系统进程中调用NtVdmControl ( VdmStartExecution)服务。 此服务会导致使CPU环境更新为PEB中预定 义的值。
因为KiSystemCallExit2函数在使用SegCs, SegDs, SegSs域时没有对它们做深入检查,所以我们可以利用上面的方法提供给内核一个伪造的选择子,这样在内核返回ring3执行iretd时会发生#NP 异常。
在KiTrap0B(#NP异常处理函数)中会将当前整个trap frame(的内容)向下移动12个字节,并调整ebp和esp。新建的TrapFrame的TempSegCs和 TempEsp ,对应于发生#NP异常前的Trap frame的DbgEip和DbgArgMark,且 异常处理函数并没有对这两个域进行初始化,所以此时两个域的值分别为上一次TrapFrame的DbgEip和DbgArgMark 的值,所以只要控制发生#NP异常前TrapFrame的DbgEip和DbgArgMark的值,即可控制TempSegCs和TempEsp的值。
DbgEip包含了 用户模式返回地址,可以被我们完全控制, DbgArgMark作为陷阱帧的标志,它被设置成0xBADB0D00: mov [ebp]+TsDbgArkMark, 0BADB0D00h.