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

漏洞利用

为了成功利用此漏洞,我们需要解决以下两点:

  1. 使TempSegCs为我们可以控制的值。
  2. 触发漏洞,使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项,但是受到操作系统的限制:
参考

  1. Base+limit 被限制在用户地址空间。
  2. 创建的段能被用户模式访问(DPL=3)。
  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.

参考

j00ru