Windows內存隱藏技術初探
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
NetRoc
最早看到Shadow Walker這種隱藏內存數據的技術的時間忘了,呵呵。大約是一年多或者兩年以前吧。當時還只是提出了理論性的東西,沒有人實現出來。前段時間搞NP玩的時候,對Themida和虛擬機深惡痛絕,遂想,乾脆把內存數據隱藏得了,省得掛個鉤子還得縮手縮腳,找個一勞永逸的辦法。於是google了一把網上的代碼,自己也花了幾天時間實現了一下。雖然不太完美,不支持PAE,目前也只能隱藏ring3的代碼,不過對這項技術的原理和實現也算是摸清楚了。由於對現在的安全軟件具有極度破壞性的殺傷力,開始沒想寫這篇文章,不過最近確實比較無聊,NMAKE的文章又比較難寫,so還是寫寫玩吧,權當練文筆了,呵呵。
關於內存隱藏的概念。有時候我們會面臨這麼一個問題,如果我們有一段內存中的代碼,不想被別人發現,但是又要它能確確實實的執行起來起作用,怎麼辦?或許有很多辦法,比如自我變形之類的。但是,如果可以完完全全把這段代碼的痕跡從內存中“抹消”,豈不是很舒坦的事情?hoho。簡單來說,就是讓內存中的一段數據,執行的時候是一個樣子,但是read/write的時候又是另外一個樣子。是不是有點玄妙了?呵呵。
一切都要從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
Hook 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 )