前言
在之前的实验过程中都需要我们使用WinDBG
来中断内核,在系统中断表中添加int 20
,将我们自己构造的内核代码进行运行,而在此处实验中我们将构建一个处于三环的程序,通过系统调用来完成对应的功能。
实验过程
因为在程序中ntdll
都会被程序所调用对此我们哦那个给模拟该方式来实现从三环到零环的权限。
我们此处尝试实现两个函数ReadMem
、AllocMem
。我们同样需要两个程序来构建,第一个用于构建内核,注册对应的中断服务,同时将函数实现进行拷贝到GDT
表项中(不再额外分配内存),另外一个程序则是使用其注册的表项通过中断来跳转到执行我们的在三环完成对零环权限的执行。
此处借用一下周壑师傅的图:
我们根据这张表的地址来进行更改GDT
表中的空闲内存空间,在SystemCallEntry
我们需要实现一个山寨版本的入口KiFastCallEntry
,然后再实现下面的两个函数的功能。
内核构建程序
当我们从三环进入到零环时对应会将eip、cs、eflags、esp、ss进行压入栈中来保存当前三环环境,随后进入到零环中,其栈结构大致如下:
我们需要取出三环的ESP
,因为三环的栈顶指向的是返回地址。又因为我们构建的都是裸函数,相当于没有push esp
和mov ebp,esp
的操作来开辟栈帧,因此我们直接使用零环的ESP
来控制获取三环的ESP
,然后我们再通过三环的ESP
来获取参数,根据这个参数来获取我们对应执行的函数
1 2 3 4
| mov ebx, ss:[esp + 0xc]; 获取三环ESP mov ecx, ds:[ebx + 0x4]; 获取三环参数 mov ebx, 0x8003f3c0; 获取SysCallTable call dword ptr[ebx + ecx * 4]; 调用SysCallTable中的函数
|
当我们获取到需要执行的函数时,我们需要在GDT中提前写好上面两个函数(ReadMem、AllocMem)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void __declspec(naked) IdtEntry() { p = (char*)Target[0]; for (i = 0; i < 128; i++) { *p = ((char*)SysCallEntry)[i]; p++; } p = (char*)Target[1]; for (i = 0; i < 64; i++) { *p = ((char*)ReadMem)[i]; p++; } p = (char*)Target[2]; for (i = 0; i < 64; i++) { *p = ((char*)AllocMem)[i]; p++; } SysCallTable[0] = 0x8003f1c0; SysCallTable[1] = 0x8003f200; __asm { iretd } }
|
当我们利用该程序构建内核时,便可以将需要执行的函数写入以及获取我们在三环的参数,通过该参数来实现对应偏移的调用服务程序。
所有代码如下:
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
| #include <stdio.h> #include <stdlib.h> #include <Windows.h>
void SysCallEntry(); void ReadMem(); void AllocMem();
int i; char* p; DWORD* SysCallTable = (DWORD*)0x8003f3c0; DWORD Target[3] = { 0x8003f120, 0x8003f1c0, 0x8003f200 };
void __declspec(naked) IdtEntry() { p = (char*)Target[0]; for (i = 0; i < 128; i++) { *p = ((char*)SysCallEntry)[i]; p++; } p = (char*)Target[1]; for (i = 0; i < 64; i++) { *p = ((char*)ReadMem)[i]; p++; } p = (char*)Target[2]; for (i = 0; i < 64; i++) { *p = ((char*)AllocMem)[i]; p++; } SysCallTable[0] = 0x8003f1c0; SysCallTable[1] = 0x8003f200; __asm { iretd } } void __declspec(naked) SysCallEntry() { __asm { push 0x30 pop fs sti; 开中断
mov ebx, ss: [esp + 0xc] ; 获取三环ESP mov ecx, ds: [ebx + 0x4] ; 获取三环参数 mov ebx, 0x8003f3c0; 获取SysCallTable, 同时将其设置为地址无关量 mov edx, dword ptr[ebx + eax * 4]; 调用SysCallTable中的函数 call edx
cli; 关中断 push 0x3b pop fs iretd } }
void __declspec(naked) ReadMem() { __asm { mov eax, ds: [ecx] ; 直接获取ecx参数里的值后返回 ret } }
void __declspec(naked) AllocMem() { __asm { push ecx; Size push 0; 非分页内存 mov eax, 0x80537FF8 call eax; 调用ExAllocatePool ret } }
void Crash() { __asm { int 0x20 } }
int main() { if ((DWORD)IdtEntry != 0x00401040) { printf("Wrong addr : %p \n", IdtEntry); } Crash(); system("pause"); return 0; }
|
其中我们可以在里面顺便把0x21的注册表项进行注册为我们构造的伪入口切换程序,通过加入以下代码即可
1 2 3 4 5 6
| ; target addr 8003f120 ; eq 8003f508 8003ee00`0008f120 mov eax, 0x0008f120 mov ds:[0x8003f508], eax mov eax, 0x8003ee00 mov ds:[0x8003f50c], eax
|
执行程序
执行程序来说就相对比较简单,我们将需要调用的参数通过eax保存,随后通过中断0x21来跳转到我们内核构建程序中。由于我们在内核构建程序中获取了其参数,通过该方法来获取eax的值,随后计算偏移量来跳转。代码如下:
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
| #include <stdio.h> #include <cstdlib> #include <Windows.h>
DWORD ReadMem(DWORD addr); DWORD AllocMem(DWORD size);
DWORD __declspec(naked)ReadMem(DWORD addr) { __asm { mov eax,0 int 0x21 ret } }
DWORD __declspec(naked)AllocMem(DWORD size) { __asm { mov eax, 1 int 0x21 ret } }
int main() { printf("%p\n", ReadMem(0x8003f3c0)); system("pause"); return 0; }
|
代码测试
我们将程序拷贝到XP虚拟机中,先使用WinDbg
进行中断将int 0x20
进行设置,然后恢复继续。
我们可以成功的看到对应代码已经成功写入了对应位置
此时我们再次运行相应的函数调用的三环程序,尝试从三环中获取零环的信息,过程如下
可以看到我们已经成功的将零环的地址内容进行输出,我们可以在WinDbg上进行验证
总结
我们成功的在三环中调用了我们在内核中编写的代码,通过一个简单的程序构建代码于内核环境中,我们可以尝试通过自己编写一些相应的代码来实现更加高级的功能,该提不提的是当初写地址表写成了char*
,导致没能成功的将构建的函数写上去。对此需要更加注意下相应写入地址时采用的格式,否则容易导致内核崩溃,进而蓝屏。