Windows内核实验 ——— ShadowWalker

前言

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
// DataSeg.h
#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]; // 创建一个新的页 0x41b000

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(); // 0x411000

void Crash() {
__asm {
int 0x20
}
}

// eq 8003f500 0040ee00`00081040
int main() {
__asm {
jmp L
ret ; 0x0411003
L:
}
if ((DWORD)IdtEntry != 0x00401040) {
printf("IdtEntry : %p\n", IdtEntry);
return 0;
}

FakePage[0] = 0xdeadbeef; // 将假页面挂载,解决CPU延时分配内存
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"



// gdt addr 8003f120
void GlobalFunc();
char* p;
void __declspec(naked) IdtEntry() {
p = (char*)0x8003f120; // gdt 中内存
for (int i = 0; i < 256; i++) {
*p = ((char*)GlobalFunc)[i];
p++;
}
// hook part
__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
}
}


// eq 8003f500 0040ee00`00081000
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回调函数,我们可以尝试将原页面进行恢复,进而在释放内存时避免出现重复释放内存的问题。


Windows内核实验 ——— ShadowWalker
https://equinox-shame.github.io/2023/07/07/Windows内核实验 — ShadowWalker/
作者
梓曰
发布于
2023年7月7日
许可协议