前言
ShadowWalker
是一种隐藏内存的技术,利用 TLB 缓存线性地址到物理地址映射的特性,当CRC线程校验检查某段代码是,它使用的线性地址会缓存到 TLB 的数据页表缓存中(Data-TLB)中,而当 EIP 运行到此段代码时,又会将代码的线性地址缓存到 TLB 的指令页表缓存(Instruction-TLB)中。ShadowWalker
技术核心就在于修改指令页表缓存中的物理地址,让 CRC 线程读取原来的代码,而程序真正执行的时候跳转到其他代码
实验过程
在上次实验代码基础上,此次实验中我们将读写异常与执行异常进行分离,对于原来的页面的异常我们遇到执行异常时需要将原来的页面进行挂上,之后继续执行我们的相应地址空间内的程序,之后再将原来的假页面挂载达到保护原页面的作用。而遇到读或写异常时,我们任然保持原来的假页面,通过自己内部实现读写来将对应信息挂载到数据TLB中。具体构建的异常信息处理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| void __declspec(naked) GlobalFunc() { __asm { pushad mov eax, cr3 cmp eax, ds : [Addr_CR3] jne _PASS
mov eax, cr2 shr eax, 12 cmp eax, 0x411; 判断是否为main页地址异常 jne _PASS
mov eax, [esp + 0x20]; 拿到error code test eax, 0x10; 判断是否为写异常 jne _EXECUTE ; 执行异常 jmp _WRITE_READ ; 读异常
_EXECUTE : } PTE(0x411000)[0] = *(DWORD *)K_REAL_PTE0; PTE(0x411000)[1] = *(DWORD *)K_REAL_PTE1;
__asm { mov eax, 0x411003 ; 真实程序块的代码地址 call eax }
PTE(0x411000)[0] = *(DWORD *)K_FAKE_PTE0; PTE(0x411000)[1] = *(DWORD *)K_FAKE_PTE1;
__asm { popad add esp, 4 ;去除error code iretd
_WRITE_READ: }
PTE(0x411000)[0] = *(DWORD*)K_FAKE_PTE0; PTE(0x411000)[1] = *(DWORD*)K_FAKE_PTE1;
__asm { mov eax, ds: [0x401100] ; 读取假页面,使得数据TLB中存在相应信息 }
PTE(0x411000)[0] = 0; PTE(0x411000)[1] = 0;
__asm{ popad mov word ptr[esp + 2], 0 iretd _PASS : popad mov word ptr[esp + 2], 0 push 0x805454af; 目标跳转地址 ret } }
|
而同时我们原页面则需要将真实页信息进行保存到GDT中,为我们HOOK
后获取并处理做准备,并让原真实页面失效,进而被我们HOOK
的异常函数处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void __declspec(naked) IdtEntry() { *(DWORD *)K_REAL_PTE0 = PTE(0x411000)[0]; *(DWORD *)K_REAL_PTE1 = PTE(0x411000)[1]; *(DWORD *)K_FAKE_PTE0 = PTE(0x41b000)[0]; *(DWORD *)K_FAKE_PTE1 = PTE(0x41b000)[1]; PTE(0x411000)[0] = 0; PTE(0x411000)[1] = 0;
__asm { mov eax, cr3 mov ds : [Addr_CR3] , eax
iretd } }
|
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #pragma once
#define K_ESP 0x8003f3f4 #define K_ESP_4 0x8003f3f0 #define Addr_CR3 0x8003f3ec #define Addr_CR2 0x8003f3e8
#define K_REAL_PTE0 0x8003f3e4 #define K_REAL_PTE1 0x8003f3e0 #define K_FAKE_PTE0 0x8003f3dc #define K_FAKE_PTE1 0x8003f3d8
#define PTE(addr) ((DWORD *)(((addr >> 12) << 3) + 0xC0000000))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include <stdio.h> #include <Windows.h> #include "DataSeg.h"
#pragma section(".mySeg",read,write) __declspec(allocate(".mySeg")) volatile DWORD FakePage[1024];
void __declspec(naked) IdtEntry() { *(DWORD *)K_REAL_PTE0 = PTE(0x411000)[0]; *(DWORD *)K_REAL_PTE1 = PTE(0x411000)[1]; *(DWORD *)K_FAKE_PTE0 = PTE(0x41b000)[0]; *(DWORD *)K_FAKE_PTE1 = PTE(0x41b000)[1]; PTE(0x411000)[0] = 0; PTE(0x411000)[1] = 0;
__asm { mov eax, cr3 mov ds : [Addr_CR3] , eax
iretd } }
#pragma code_seg(".mycode") __declspec(allocate(".mycode")) void Crash(); #pragma code_seg(".mycode") __declspec(allocate(".mycode")) void main();
void Crash() { __asm { int 0x20 } }
int main() { __asm { jmp L ret ; 0x0411003 L: } if ((DWORD)IdtEntry != 0x00401040) { printf("IdtEntry : %p\n", IdtEntry); return 0; }
FakePage[0] = 0xdeadbeef; Crash();
while (1) { printf("running\n"); Sleep(1000); }
return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| #include <stdio.h> #include <Windows.h> #include "DataSeg.h"
void GlobalFunc(); char* p; void __declspec(naked) IdtEntry() { p = (char*)0x8003f120; for (int i = 0; i < 256; i++) { *p = ((char*)GlobalFunc)[i]; p++; } __asm { mov eax, 0xffffffff mov ds : [Addr_CR3] , eax; 全局变量Cr3初始化为 0xffffffff
; 关闭写保护 mov eax, cr0 and eax, ~0x10000 mov cr0, eax
; Hook main mov eax, 0x03f12068 mov ds : [0x805454A8] , eax mov ax, 0xC380 mov ds : [0x805454AC] , ax
xor eax, eax mov ds : [Addr_CR2] , eax mov ds : [K_ESP] , eax mov ds : [K_ESP_4] , eax
; 开启写保护 mov eax, cr0 or eax, 0x10000 mov cr0, eax } __asm { iretd } }
void __declspec(naked) GlobalFunc() { __asm { pushad mov eax, cr3 cmp eax, ds : [Addr_CR3] jne _PASS
mov eax, cr2 shr eax, 12 cmp eax, 0x411; 判断是否为main页地址异常 jne _PASS
mov eax, [esp + 0x20]; 拿到error code test eax, 0x10; 判断是否为写异常 jne _EXECUTE ; 执行异常 jmp _WRITE_READ ; 读异常
_EXECUTE : } PTE(0x411000)[0] = *(DWORD *)K_REAL_PTE0; PTE(0x411000)[1] = *(DWORD *)K_REAL_PTE1;
__asm { mov eax, 0x411003 ; 真实程序块的代码地址 call eax }
PTE(0x411000)[0] = *(DWORD *)K_FAKE_PTE0; PTE(0x411000)[1] = *(DWORD *)K_FAKE_PTE1;
__asm { popad add esp, 4 ;去除error code iretd
_WRITE_READ: }
PTE(0x411000)[0] = *(DWORD*)K_FAKE_PTE0; PTE(0x411000)[1] = *(DWORD*)K_FAKE_PTE1;
__asm { mov eax, ds: [0x401100] ; 读取假页面,使得数据TLB中存在相应信息 }
PTE(0x411000)[0] = 0; PTE(0x411000)[1] = 0;
__asm{ popad mov word ptr[esp + 2], 0 iretd _PASS : popad mov word ptr[esp + 2], 0 push 0x805454af; 目标跳转地址 ret } }
void Crash() { __asm { int 0x20 } }
int main() { if ((DWORD)IdtEntry != 0x00401040) { printf("IdtEntry : %p\n", IdtEntry); return 0; } Crash(); return 0; }
|
我们先执行对应的HOOK程序,执行后在来执行相应的核心程序,我们可以看到其一直在打印running
,此时我们可以尝试使用CE
来读取对应页面的内存信息
我们尝试读取main
函数对应的地址空间,可以看到没有数据,只有我们写入的0xDEADBEEF
,可以看到外面成功的将main函数的信息进行了隐藏处理达到了隐藏页信息的效果。
同样的我们在创建的伪代码地址也无法看到对应的页面信息
同时我们如果使用OD来进行查看对应地址处的数据我们同样无法观测到对应的代码信息,证明我们成功的将代码进行了隐藏
总结
需要注意的是上述代码并不是完美代码,我们可以发现在关闭程序的时候会引发一个异常信息,我们使用Windbg
的栈回溯来观察可以发现是我们对应的页地址崩溃,因为我们之前对页面进行了修改,而关闭程序时我们原来构建的虚假物理页面释放时会出现重复释放的问题,进而引发异常
对于这个问题的解决方式我们可以手动实现一个退出时的HOOK
,将原来的页面信息进行处理恢复到原来状态,之后再进行退出
在点击窗口的关闭按钮(就是那个“叉”)的时候,会产生HandlerRoutine
回调函数,我们可以尝试将原页面进行恢复,进而在释放内存时避免出现重复释放内存的问题。