-
-
数据代码隔离绕过crc
-
发表于: 2019-11-9 14:36 6594
-
转发来自原文
随着Intel Nehalem引入sTLB,TLB隔离(曾经是一种可靠的技术)已成为历史。那些不得不HOOK用户模式的人开始研究虚拟机管理程序。特别是EPT违规。但是,实现虚拟机监控程序意味着实现庞大的,依赖于平台的代码,这并不是您尝试发布软件时的最佳方法-尤其是在您试图隐身的情况下,因为虚拟化很容易检测并且很难隐藏。
这就是分段发挥作用的地方。尽管我们长期以来一直使用CS == SS == DS模型,但自1978年以来,分段一直处于无效状态,但是可以起作用。CS的值指示如何执行指令,DS的值指示如何读取内存。实际上,我们希望从TLB拆分中获得什么。
尽管我们必须禁用Patchguard才能使用此技术(相对简单),但该技术将使我们做很多有趣的事情,例如欺骗返回指针,挂钩函数,而无需更改.text和大量的call指令。
我们将必须钩住一堆内核函数并创建其他的段。但是在此之前,让我们先讨论一下其基本工作方式:
这项技术基本上是通过创建镜像模块(而不是原始模块)来工作的。我们分配一个与原始模块大小相等的内存,然后按1:1复制其内容。尽管代码数据位于不同的内存地址中,但由于代码引用的IP不会有所不同,因此我们无需进行任何重定位。然后,我们将克隆原始CS的GDT条目(无论是0x23还是0x33),并将GDT的Base设置为新分配的内存减去原始模块基地址(进程内各种dll模块 每个模块都有自己的数据段 代码段 我们镜像了整个dll 镜像dll的数据段减去原始dll的数据段的值正好是镜像dll和原始dll的基地址偏移),这很简单:
typedefstruct _KGDTENTRY { uint8_t Limit0; uint8_t Limit1; uint8_t Base0; uint8_t Base1; uint8_t Base2; uint8_t Access; uint8_t Limit2 : 4; uint8_t Unk : 1; uint8_t L : 1; uint8_t Db : 1; uint8_t Granularity : 1; uint8_t Base3; } KGDTENTRY; typedefstruct _SET_ENTRY_DPC_ARGS { uint16_t EntryId; uint64_t Entry; NTSTATUS Status; uint64_t Error_Trgt; uint64_t Error_Base; uint64_t Error_Lmt; } SET_ENTRY_DPC_ARGS; staticvoidGdt_SetEntryDpc( KDPC *Dpc, SET_ENTRY_DPC_ARGS* Args, PVOID SystemArgument1, PVOID SystemArgument2 ) { uint64_t Backup = DisableWP(); GDTR Gdtr; _sgdt( &Gdtr ); uint64_t* Limit = Gdtr.Base + Gdtr.Limit + 1 - 8; uint64_t* Target = Gdtr.Base + Args->EntryId * 8; if( Target > Limit ) { Args->Error_Trgt = Target; Args->Error_Base = Gdtr.Base; Args->Error_Lmt = Gdtr.Limit; Args->Status = GDT_SEG_NOT_PRES; Log("Target (%x) > Limit (%x) [%d]\n", Target, Limit, KeGetCurrentProcessorNumber()); } else { *Target = Args->Entry; Log("Target (%x) <= Limit (%x) [%d]\n", Target, Limit, KeGetCurrentProcessorNumber()); } KeSignalCallDpcSynchronize( SystemArgument2 ); ResetWP( Backup ); KeSignalCallDpcDone( SystemArgument1 ); } static NTSTATUS Gdt_SetEntry( uint16_t EntryId, uint64_t Entry ) { static SET_ENTRY_DPC_ARGS Args; Args.EntryId = EntryId; Args.Entry = Entry; Args.Status = STATUS_SUCCESS; KeGenericCallDpc( Gdt_SetEntryDpc, &Args ); return Args.Status; } static NTSTATUS Gdt_SetupSeg( uint32_t Seg, uint8_t Wow64, uint32_t Base, uint32_t Limit ) { BOOLEAN Granularity = Limit > 0xFFFFF; if( Granularity ) Limit /= 0x1000; // 4 kb if( Limit > 0xFFFFF) return GDT_LIM_TOO_BIG; uint64_t SegBaseVal = Wow64 ? Gdt_GetEntry(GDT_ENTRY(0x23)) : Gdt_GetEntry(GDT_ENTRY(0x33)); KGDTENTRY* SegBase = &SegBaseVal; SegBase->Base0 = ( Base >> 8 * 0) & 0xFF; SegBase->Base1 = ( Base >> 8 * 1) & 0xFF; SegBase->Base2 = ( Base >> 8 * 2) & 0xFF; SegBase->Base3 = ( Base >> 8 * 3) & 0xFF; SegBase->Limit0 = ( Limit >> 8 * 0) & 0xFF; SegBase->Limit1 = ( Limit >> 8 * 1) & 0xFF; SegBase->Limit2 = ( Limit >> 8 * 2) & 0xF; SegBase->Granularity = Granularity; returnGdt_SetEntry(GDT_ENTRY( Seg ), SegBaseVal ); }
只需复制原始段的值,相应地设置Base,Limit和granularity ,然后创建一个DPC以使用sgdt获取每个处理器的GDT 基地址 ,然后写入指定的索引。 (您可能会注意到我没有分配新的GDT,这是因为在替换GDT指针后,我的用户很少会遇到奇怪的系统冻结)
现在我们已经成功设置了新的段,这是我们遇到的第一个问题,即IP范围之外的问题。
让我们看下面的例子:
Module base (0x400000) /---------------------\ /---------------------\ (CS Base + 0x400000) 我们的镜像模块基地址 | ... | | ... | | CALL 0x600214 | | CALL 0x600214 | | MOV [0x312321], EAX | | MOV [0x312321], EAX | | CALL 0x333333 | | CALL 0x333333 | | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | Module end (0x500000) \---------------------/ \---------------------/ (CS Base + 0x500000) 镜像模块结束地址
当该目标IP比模块基地址小(低级CALL 0x333333 ),或者当目标IP比模块基座更高(CALL 0x600214 ),这回产生问题,因为他们将分别执行CS 基地址+ 0x333333和 CS 基地址+ 0x600214,不在我们的镜像模块范围 可能是其它模块。
这第一个问题很容易处理。只需将GDT条目的Limit设置为模块大小,并在发生GPF(页面异常)时恢复CS为原始的cs 并设置IP = IP + Shadow Base-Real Base(此时ip指向镜像模块当前执行的地方 继续执行 并冒着泄漏shadow base的风险,因为需要push指向镜像模块的返回指针到堆栈,然后回到0x23:镜像模块处继续执行 执行完毕返回的时候 会回到镜像模块继续执行 执行的时候 一旦遇到call jmp等 目标地址不在镜像段范围内的 又会异常 此时我们恢复cs为镜像cs 继续在镜像原始地址下一条执行),或者自己解决调用,如下所示:
static BOOLEAN ResolveCall( ITRAP_FRAME* Frame, UCHAR* Instruction, uint32_t* Target, uint8_t* InstructionSize ) { if( Instruction[0] != 0xE8 && Instruction[0] != 0xFF) return FALSE; hde32s s = {0}; *InstructionSize = hde32_disasm( Instruction, &s ); if( Instruction[0] == 0xFF && s.modrm_reg == 2) { if( s.sib) { if( s.modrm_mod == 0) *Target = *( uint32_t* )(ResolveRegisterById( Frame, s.sib_index) * (1 << s.sib_scale) + s.disp.disp32); elseif( s.modrm_mod == 1) *Target = *( uint32_t* )(ResolveRegisterById( Frame, s.sib_base) + s.disp.disp32); else *Target = *( uint32_t* )(ResolveRegisterById( Frame, s.sib_base) + ResolveRegisterById( Frame, s.sib_index) * (1 << s.sib_scale) + s.disp.disp32); } else { if( s.modrm_mod == 0) *Target = *( uint32_t* )( s.disp.disp32); elseif( s.modrm_mod == 3) *Target = ResolveRegisterById( Frame, s.modrm_rm); elseif( s.modrm_mod == 2 || s.modrm_mod == 1) *Target = *( uint32_t* )(ResolveRegisterById( Frame, s.modrm_rm) + s.disp.disp32); } return TRUE; } elseif( Instruction[0] == 0xE8) { *Target = Frame->Rip + s.imm.imm32 + 5; return TRUE; } return FALSE; } hook处理GPF页面异常的中断 static BOOLEAN NTAPI HkOnGpf( ITRAP_FRAME* TrapFrame ) { if( TrapFrame->Cs == SHADOW_HOOK_SEG ) { // CALL | JMP | RET 目标地址不在本段范围内? __swapgs();//切换gs到内核环境 SHADOW_MODULE_ENTRY Sme = GetShadowModuleFromRip(PsGetCurrentProcessId(), TrapFrame->Rip );//通过异常ip获取在镜像段的相关信息结构 if( Sme.ModuleReal)//原始模块基地址 { uint64_t RspBackup = TrapFrame->Rsp;//异常时候的堆栈 _enable(); //Log( "Handling call to the outside of shadow module @ %llx\n", TrapFrame->Rip ); __try { uint32_t Destination = 0; uint8_t InstructionSize = 0; //解析 获取要跳转的目标地址 if(ResolveCall( TrapFrame, TrapFrame->Rip - Sme.ModuleReal + Sme.ModuleShadow, &Destination, &InstructionSize )) { uint32_t IsPageMapped = FALSE; KIRQL Irql = RsAcquireSpinLockRaiseToDpc( &Rs_ProcessRecordSpinLock ); DWORD Pid = PsGetCurrentProcessId(); if( Rs_ProcessRecordsMaxPid > Pid && Rs_ProcessRecords[ Pid ]) { for(int i = 0; i < ARRAYSIZE( Rs_ProcessRecords[ Pid ]->SpoofedProtect ); i++ ) { if( !Rs_ProcessRecords[ Pid ]->SpoofedProtect[ i ].PageBase) break; if(( Rs_ProcessRecords[ Pid ]->SpoofedProtect[ i ].PageBase & ( ~0xFFF)) == ( TrapFrame->Rip & ( ~0xFFF))) { IsPageMapped = TRUE;//已经存在镜像 break; } } } RsReleaseSpinLock( &Rs_ProcessRecordSpinLock, Irql ); TrapFrame->Rsp -= 0x4; *( uint32_t* ) TrapFrame->Rsp = IsPageMapped ? ( TrapFrame->Rip + InstructionSize - Sme.ModuleReal + Sme.ModuleShadow)//如果目标内存已存在镜像 直接堆栈塞入在镜像的 指令返回地址 比如第一个CALL 0x600214 : ( TrapFrame->Rip + InstructionSize );//否则塞入原始模块的地址 指令返回地址 TrapFrame->Rip = Destination;//塞入目标地址 TrapFrame->Cs = WOW64_SEG;//指向原始的cs //Log( " --> %llx (%d)\n", TrapFrame->Rip, IsPageMapped ); _disable(); __swapgs(); return TRUE; } } __except(1) { } _disable(); __swapgs(); TrapFrame->Rsp = RspBackup;//解析目标地址失败 可能是数据的访问 就直接恢复堆栈 比如第二个MOV [0x312321], EAX TrapFrame->Rip -= Sme.ModuleReal;//#1但是第三条指令是 CALL 0x333333 那么执行完第二条后 因为用的cs是原始的 原始limit是整个进程范围 那么0x333333也可以执行 而不是镜像的 关键call的目标地址和下一条指令地址+偏移有关系 这里的0x333333只是打比喻 真实情况下 镜像的0x333333代表一个只有镜像才存在的目标指令地址 但是当cs是原始cs的时候 这个0x333333可能是另外一个地址 那里没有指令 可能全是nop 或者c3 导致直接异常 TrapFrame->Rip += Sme.ModuleShadow;//设置镜像模块的地址 TrapFrame->Cs = WOW64_SEG;//原始模块的cs return TRUE; } else { // 从原始模块执行完毕 返回到镜像模块 发生异常的时候 切换cs到镜像cs } __swapgs(); } return FALSE; }
现在,我们有了第二个问题: 第三个 CALL 0x333333 。这个问题是,无论我们做什么,这都是一个完全有效的操作,因为从技术上讲,它是在CS边界内(作为模块基数= 0x0),而不是在镜像模块边界;因此处理程序不会在这里帮助我们。参考##1
为了解决这个问题,我们可以像下面这样钩住PsMapSystemDlls ,在加载目标所需的DLL之前,只需在模块下边界下方保留虚拟内存 :
static NTSTATUS NTAPI HkPsMapSystemDlls( PEPROCESS Process, BOOLEAN UseLargePages ) { USING_SYMBOL( ZwAllocateVirtualMemory ); fnNtAllocateVirtualMemory ZwAllocateVirtualMemory = GET_SYMBOL( ZwAllocateVirtualMemory ); KAPC_STATE Apc; KeStackAttachProcess( Process, &Apc ); MEMORY_BASIC_INFORMATION Mbi = {0}; while(1) { NTSTATUS Status = VvQueryVirtualMemory ( NtCurrentProcess(), ( PUCHAR ) Mbi.BaseAddress + Mbi.RegionSize, MemoryBasicInformation, &Mbi, sizeof( Mbi ), 0 ); if( Status ) { break; } else { struct { UNICODE_STRING Str; wchar_t Buffer[1024]; } Buffer; RtlZeroMemory( &Buffer, sizeof( Buffer )); VvQueryVirtualMemory(NtCurrentProcess(), Mbi.BaseAddress, 2ull, &Buffer, sizeof( Buffer ), 0ull ); if( Buffer.Str.Buffer) { if(wcsstr( Buffer.Str.Buffer, L"<process name for simplicity>")) { Log("<process name> found @ %llx [0x%x bytes] (%ls)\n", Mbi.BaseAddress, Mbi.RegionSize, Buffer.Str.Buffer); Log("Wasting %d MB...!\n", (( uint64_t ) Mbi.BaseAddress) / 1024 / 1024); for( PUCHAR Page = 0x10000; Page < ((PUCHAR)Mbi.BaseAddress - 0x20000); Page += 0x1000) { PVOID Base = Page; SIZE_T Size = ( uint64_t ) Mbi.BaseAddress - ( uint64_t ) Page - 0x20000; if(ZwAllocateVirtualMemory(NtCurrentProcess(), &Base, 0ull, &Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE ) == 0) {//在目标模块附近从0x10000一直分配到目标模块基地址处 实在想不通 搞这干啥 因为我们镜像模块 需要这快内存作为跳板?也许吧 发生异常 //他会吧目标指令写入到这里 恢复执行 作为跳板 跳到原始的地址去执行 总之作者文章写的模模糊糊 也没留代码 只能遐想了 估计是怕大手子 //拿去抄 所以只给思路与poc 剩下自己去实现 去调试 Log("Allocated %llx -> %llx\n", Page, Page + Size ); break; } } break; } } } } KeUnstackDetachProcess( &Apc ); return(( fnPsMapSystemDlls ) PsMapSystemDllsHook->OriginalBytes )( Process, UseLargePages ); }
在此阶段没有设置EPROCESS,并且我们无法打开该进程的句柄,因此我们必须使用KeStackAttachProcess和NtCurrentProcess()。只需使用NtQueryVirtualMemory和MemorySectionName搜索进程的目标库,然后尝试在其下分配内存。
瞧,问题解决了!尽管这听起来像是我们将浪费大量内存,但我还没有看到Windows将30MB以上内存映射到进程。
现在我们已经设置了镜像模块,剩下的就是在需要时将控制流重定向到镜像模块,这可以通过在原始页面上设置xd标志(页面不可执行 或调用NtProtectVirtualMemory并从NtQueryVirtualMemory欺骗保护)来实现。 。我们将需要自己(在执行时)处理分页的内存,但是除此之外,我们的页面错误挂钩将相对简单:
static BOOLEAN NTAPI HkOnPageFault( ITRAP_FRAME* TrapFrame ) { uint64_t FaultyPtr = __readcr2(); if( TrapFrame->Cs == WOW64_SEG ) // 原始cs { if( TrapFrame->ExceptionCode & (1 << 4) && // Caused by instruction fetch TrapFrame->ExceptionCode & (1 << 0)) // Page is present { if( TrapFrame->Rip == FaultyPtr )//发生不可执行异常 { __swapgs(); SHADOW_MODULE_ENTRY Sme = GetShadowModuleFromRip(PsGetCurrentProcessId(), TrapFrame->Rip );//获取原始ip对应的镜像ip等信息 //所有原始指令执行流程都劫持到镜像去 if( Sme.ModuleReal) { Log("Handling call to remapped page of module @ %llx\n", TrapFrame->Rip ); TrapFrame->Cs = SHADOW_HOOK_SEG;// 切换到镜像cs __swapgs(); return TRUE; } __swapgs(); } elseif( FaultyPtr - TrapFrame->Rip < 15) { //#2 __swapgs(); SHADOW_MODULE_ENTRY Sme = GetShadowModuleFromRip(PsGetCurrentProcessId(), TrapFrame->Rip ); if( Sme.ModuleReal) { Log("Fixed half instruction failure! %llx %llx\n", FaultyPtr, TrapFrame->Rip ); PUCHAR From = TrapFrame->Rip; PUCHAR To = TrapFrame->Rip - Sme.ModuleReal + Sme.ModuleShadow;//在镜像的地址 之前我们已经在目标模块下面分配了一大块内存 用来做跳板 SIZE_T Size = FaultyPtr - TrapFrame->Rip; _enable();//欢迎加入miao1yan.top群号835875625和755836982 memcpy( To, From, Size );//把原始目标rip的指令拷贝到跳板 跳板 _disable(); TrapFrame->Cs = SHADOW_HOOK_SEG;//切换cs到镜像cs 继续执行 __swapgs(); return TRUE; } __swapgs(); } } } elseif( TrapFrame->Cs == SHADOW_HOOK_SEG )//如果是镜像cs执行时候发生异常 { if( TrapFrame->ExceptionCode & (1 << 4) && // Caused by instruction fetch !( TrapFrame->ExceptionCode & (1 << 0))) // Page is not present { // Page is not present __swapgs(); _enable(); SHADOW_MODULE_ENTRY Sme = GetShadowModuleFromRip(PsGetCurrentProcessId(), FaultyPtr );//获取镜像rip相关信息 if( Sme.ModuleReal)//如果发生异常地址在镜像模块内 { //Log( "Handling paged out memory (%llx)\n", FaultyPtr ); PUCHAR FaultyAdr = FaultyPtr - Sme.ModuleReal + Sme.ModuleShadow; __try { volatile uint64_t volatile PageIn[1]; memcpy( PageIn, (volatile UCHAR volatile * ) FaultyAdr, 8);//换页了?那就尝试访问一下 刷新TLB 下回直接就访问TLB了 } __except(1) { } _disable(); __swapgs(); return TRUE; } _disable(); __swapgs(); return FALSE; } else { //目标地址 不在本模块GDT limit范围内 因为镜像cs基地址是之上的范围不能访问 一旦call访问那里 就会异常 //这里作者流了很多坑 也没有写清楚 我估计是 镜像的一些超出范围的指令被他 写成了jmp 23:跳板 因为跳板 //处的指令还没有 所以会发生异常 进入上面的分支处理了 见#2 TrapFrame->Cs = WOW64_SEG; // Windows doesnt handle it otherwise... } } return FALSE; }
现在,您可以使用jmp 0x23 :Trampoline HOOK 镜像模块, 并使用jmp 0xAA :RetPtr 返回, 或者根据需要更改指令。除了您需要使原始页面不执行之外,没有其他区别。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)