首页
社区
课程
招聘
数据代码隔离绕过crc
发表于: 2019-11-9 14:36 6594

数据代码隔离绕过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期)

最后于 2019-11-9 14:37 被ZwCopyAll编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//