Windows内存隐藏技术初探(转)
一切都要从Petium架构CPU提供的TLBS说起。TLBS的全称是Translation Lookaside Buffers。为了加速处理器内存在分页模式下的访问速度,从P6 family的cpu开始就支持这样一种特性。处理器会将最近访问过的页目录(page-directory)和页表(page-table)存储在芯片内部称为translation lookaside buffers的缓存中。P6 family处理器分别为4K页面和4M页面分别保留不同的TLBs。绝大多数对分页的访问,都可以通过TLBs的内容完成,只有在缓存中找不到所访问的页面的信息时,才会去访问实际的页目录和页表。Ring0的代码通过重新装载CR3或者使用INVLPG指令,可以将TLB里面某些页表的入口无效化。而CPU分别为内存的执行和读写保存了不同的TLB,即DTLB和ITLB。对于DTLB,在执行数据访问指令的时候,会更新DTLB中被访问页面的入口;而对于ITLB,在执行某个页面代码的时候,会更新ITLB内的入口。通常情况下,DTLB和ITLB的内容是同步的,但是我们可以通过操作这两种TLB,实现对某段内存的读写/执行控制。
如何才能在某个内存地址被读写或者访问的时候获得控制呢?很明显,当内存访问出错的时候,系统会触发页面异常,即Trap0E。通过钩挂Trap0E,并且将我们要控制的内存页面PTE标记为不存在,并且通过INVLPG指令清空TLB中该页的入口,这样,当对这个页面进行读写/执行访问的时候,TLB中不存在该页的入口信息,并且页面不存在,能够触发Trap0E,使得我们可以获得系统的控制权。
接下来,我们需要区分触发异常是由于读写访问还是执行访问。通过比较发生异常的页面地址,和出现异常的代码地址,我们可以区分异常类型。如果出现异常的地址就是发生异常时正执行的代码地址,那么说明是一次执行访问;反之则是读写访问。在捕获到异常并区分出异常类型之后,就可以通过分别手动装载DTLB和ITLB使得读写/执行数据的过滤。TLB被装载之后,只要没有被清除出去,对这些页面的访问就通过TLB里面的内容实现。这样使得Hook页面之后的访问速度,比未Hook之前并不会有明显的损失。
基本的思想就是这样了,后面贴出来一些我的代码实现,其中很多直接“参考”了OllyBone和网上其他公布出来的代码。
由于目的是想做对目标进程的Hook,并且隐藏被修改的代码,仅需要支持对ring3代码的隐藏即可。我的流程是在驱动中提供了一个接口,将应用层传进去的一段代码复制到要hook的地址,并保存原始内容。通过内存伪装,使得该地址执行的是新代码,而读写操作得到的是旧代码。为了在自己Trap0E里面区分是哪个进程触发的异常,采用了比较奇怪的办法,即通过对比CR3寄存器的值来判断,至于为什么这么做,忘了……。另外,为了装载ITLB,需要调用一下伪装页面内的代码,所以我们在被Hook的页面内需要找到一条retn指令,并把这个地址传递给驱动,在装载ITLB的时候,驱动程序直接call这句retn。
下面的代码还有一些问题,只是当时写的实验性的代码,所以也比较零乱。不过大致的流程是清楚的。
这段是IoCtrl里面的
case IOCTL_SHADOWHOOK:
//Shadow Hook!!
g_ulHookPid = (ULONG)PsGetCurrentProcessId();
g_ulHookProcessCr3 = GetCR3();
if( g_pbyCode == NULL)
{
g_pbyCode = ExAllocatePoolWithTag( PagedPool, 4*1024, 'SWHK');
g_pMdl = IoAllocateMdl( g_pbyCode, 4*1024, FALSE, FALSE, NULL);
if ( g_pMdl == NULL)
{
ExFreePool( g_pbyCode);
g_pbyCode = NULL;
break;
}
else
{
__try
{
MmProbeAndLockPages( g_pMdl, KernelMode, IoReadAccess);
g_blIsLocked = TRUE;
}
__except( EXCEPTION_EXECUTE_HANDLER)
{
IoFreeMdl(g_pMdl);
g_pMdl = NULL;
ExFreePool( g_pbyCode);
g_pbyCode = NULL;
g_blIsLocked = FALSE;
}
}
}
RtlZeroMemory( g_pbyCode, 4*1024);
pstShadowHookInfo = (PSHADOW_HOOK_INFO)Irp->AssociatedIrp.SystemBuffer;
RtlCopyMemory( g_pbyCode, (PVOID)(pstShadowHookInfo->ulStartAddr & 0xFFFFF000), 4*1024);
SetCopyOnWrite( (PVOID)pstShadowHookInfo->ulStartAddr);
g_ulHookCodeLen = pstShadowHookInfo->ulLength;
_asm
{//关闭内存写保护
cli;
mov eax,cr0;
and eax,0fffeffffh;
mov cr0,eax;
}
RtlCopyMemory( (PVOID)pstShadowHookInfo->ulStartAddr, (PVOID)pstShadowHookInfo->pbyHookCode, pstShadowHookInfo->ulLength);
_asm
{//重新打开内存写保护
mov eax,cr0;
or eax,0x10000;
mov cr0, eax;
sti;
}
CpuCount = *KeNumberProcessors;
while( CpuCount > 0)
{
KeSetAffinityThread( KeGetCurrentThread(), CpuCount);//绑定CPU
HookMemoryPage( (PVOID)pstShadowHookInfo->ulStartAddr, g_pbyCode, (PVOID)pstShadowHookInfo->pfnNullSub);
CpuCount--;
}
g_blIsHookedPage = TRUE;
ntStatus = HookTrap0E();
if( !NT_SUCCESS(ntStatus))
{
DbgPrint( "HookTrap0E fail.\r\n");
}
break;
下面是钩挂Trap0E
NTSTATUS HookTrap0E()
{
CCHAR CpuCount = 0;
PIDTENTRY IdtEntry = NULL;
IDTR stIdtr = {0};
if( g_blIsHookTrapE0)
return STATUS_UNSUCCESSFUL;
CpuCount = *KeNumberProcessors;
while( CpuCount > 0)
{
KeSetAffinityThread( KeGetCurrentThread(), CpuCount);//绑定CPU
//得到 IDTR 中得段界限与基地址
_asm sidt stIdtr;
IdtEntry = (PIDTENTRY)stIdtr.Base;
//保存原有得 IDT
RtlCopyMemory(&g_IdtEntryOld, &IdtEntry[0x0E], sizeof(g_IdtEntryOld));
_asm cli;//禁止中断
g_ulOldTrap0E = (ULONG)IdtEntry[0x0E].OffsetLow | ((ULONG)IdtEntry[0x0E].OffsetHigh<<16);
IdtEntry[0x0E].OffsetLow = (unsigned short)NewTrap0E;
IdtEntry[0x0E].OffsetHigh = (unsigned short)((unsigned int)NewTrap0E>>16);
_asm sti;//开中断
CpuCount--;
}
g_blIsHookTrapE0 = TRUE;
return STATUS_SUCCESS;
}
Hook一个页面
void HookMemoryPage( PVOID pExecutePage, PVOID pReadWritePage,
PVOID pfnCallIntoHookedPage)
{
PPTE ExecutePte;
g_pExecutePage = pExecutePage;
g_pReadWritePage = pReadWritePage;
g_pfnCallIntoHookedPage = pfnCallIntoHookedPage;
__asm cli; //关中断
ExecutePte = GetPteAddress( pExecutePage);
g_pReadWritePTE = GetPteAddress( pReadWritePage);
g_ExecutePTE = ExecutePte;
//这里因为我们是Hook ring3页面,所以不用EnableGlobalPageFeature
//EnableGlobalPageFeature( ExecutePte);
//标记页面为不存在
MarkPageNotPresent( ExecutePte);
//清空TLB入口
__asm invlpg pExecutePage
__asm sti //reenable interrupts
}//end HookMemoryPage
钩挂的Trap0E的代码了。关键就在这里。
#pragma optimize( "", off )
void __declspec (naked) NewTrap0E(void)
{
__asm
{
pushad
mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode
test edx, 1 //不是缺页错误
jne PassDown
//通过CR3判断当前进程
mov eax, cr3
cmp eax, g_ulHookProcessCr3
jnz PassDown
mov eax,cr2 //faulting virtual address
////////////////////////////////////////
//判断是否是Hook掉的page
/////////////////////////////////////////
mov ebx, g_pExecutePage
and ebx, 0xFFFFF000
and eax, 0xFFFFF000
cmp eax, ebx
mov eax, cr2
jnz PassDown //不是,传下去
///////////////////////////////////////
//下面处理Hook掉的页面了
/////////////////////////////////////
mov eax, cr2
push eax
push eax
call GetPteAddress
mov ebx, eax //ebx = pPte
pop eax
cmp [esp+0x24], eax //判断是执行出错还是读写时出错?
je LoadITLB
//判断是否是Hook的那些字节
cmp eax, g_pExecutePage
jb LoadDTLB
sub eax, g_pExecutePage
cmp eax, g_ulHookCodeLen
jg LoadDTLB
jmp LoadFakeFrame
LoadITLB:
////////////////////////////////
//是一次执行操作,所以Load ITLB
///////////////////////////////
cli
or dword ptr [ebx], 0x01 //标志页面为存在
call g_pfnCallIntoHookedPage //通过调用一下那个页面内的代码,装载ITLB
and dword ptr [ebx], 0xFFFFFFFE //重新标记为不存在
//sti
jmp ReturnWithoutPassdown
////////////////////////////////
// 这是读写造成的异常,并且不在我们要隐藏的代码范围内,Load DTLB
///////////////////////////////
LoadDTLB:
cli
mov eax,cr2
or dword ptr [ebx], 0x01 //mark the page present
mov eax, dword ptr [eax] //load the DTLB
and dword ptr [ebx], 0xFFFFFFFE //mark page not present
//sti
jmp ReturnWithoutPassdown
/////////////////////////////////
//需要隐藏这段代码,所以Load伪装的页面
/////////////////////////////////
LoadFakeFrame:
mov eax, cr2
mov esi, g_pReadWritePTE
mov ecx, dword ptr [esi] //ecx = PTE of the
//read / write page
//把页面替换为假的
mov edi, [ebx]
and edi, 0x00000FFF //preserve the lower 12 bits of the
//faulting page's PTE
and ecx, 0xFFFFF000 //isolate the physical address in
//the "fake" page's PTE
or ecx, edi
mov edx, [ebx] //save the old PTE so we can replace it
cli
mov [ebx], ecx //replace the faulting page's phys frame
//address w/ the fake one
//load DTLB
or dword ptr [ebx], 0x01 //标志为存在
mov eax, cr2 //faulting virtual address
mov eax, dword ptr[eax] //访问一次页面的数据,Load DTLB
and dword ptr [ebx], 0xFFFFFFFE //重新标志为不存在
//Finally, restore the original PTE
mov [ebx], edx
//sti
ReturnWithoutPassDown:
popad
add esp,4
sti
iretd
PassDown:
popad
jmp g_ulOldTrap0E
}//end asm
}
#pragma optimize( "", on )
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现