@[TOC]

中断现场

1.esp在进入内核前后的变化
修改代码保存int 0x20前后两个esp的值,可以发现3环是一个小地址,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
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
DWORD g_esp[2];

void __declspec(naked) IdtEntry() {
__asm {
mov [g_esp+4],esp
iretd

}
}
void go() {
__asm {
mov [g_esp],esp
int 0x20;
}
}

// eq 8003f500 0040ee00`00081040
int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
printf("%p\n", g_esp[0]);
printf("%p\n", g_esp[1]);
system("pause");
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
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
DWORD g_eax[2], g_ecx[2], g_edx[2], g_ebx[2];
DWORD g_esp[2], g_ebp[2], g_esi[2], g_edi[2];
WORD g_cs[2], g_ds[2], g_ss[2], g_es[2], g_fs[2],g_gs[2];
void __declspec(naked) IdtEntry() {
__asm {
mov [g_eax+4], eax
mov[g_ecx + 4], ecx
mov[g_edx + 4], edx
mov[g_ebx + 4], ebx
mov[g_esp + 4], esp
mov[g_ebp + 4], ebp
mov[g_esi + 4], esi
mov[g_edi + 4], edi

push eax

mov ax,cs
mov [g_cs+2],ax
mov ax, ds
mov[g_ds + 2], ax
mov ax, ss
mov[g_ss + 2], ax
mov ax, es
mov[g_es + 2], ax
mov ax, fs
mov[g_fs + 2], ax
mov ax, gs
mov[g_gs + 2], ax

pop eax
iretd

}
}
void go() {
__asm {
mov[g_eax ], eax
mov[g_ecx ], ecx
mov[g_edx ], edx
mov[g_ebx ], ebx
mov[g_esp ], esp
mov[g_ebp ], ebp
mov[g_esi ], esi
mov[g_edi ], edi
push eax

mov ax, cs
mov[g_cs], ax
mov ax, ds
mov[g_ds ], ax
mov ax, ss
mov[g_ss ], ax
mov ax, es
mov[g_es ], ax
mov ax, fs
mov[g_fs ], ax
mov ax, gs
mov[g_gs ], ax

pop eax
int 0x20
}
}

// eq 8003f500 0040ee00`00081040
int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
printf("%p %p %p %p %p %p %p %p\n", g_eax[0], g_ecx[0], g_edx[0], g_ebx[0], g_esp[0], g_ebp[0], g_esi[0], g_edi[0]);
printf("%p %p %p %p %p %p %p %p\n", g_eax[1], g_ecx[1], g_edx[1], g_ebx[1], g_esp[1], g_ebp[1], g_esi[1], g_edi[1]);
printf("%p %p %p %p %p %p\n", g_cs[0], g_ds[0], g_ss[0], g_es[0], g_fs[0], g_gs[0]);
printf("%p %p %p %p %p %p\n", g_cs[1], g_ds[1], g_ss[1], g_es[1], g_fs[1], g_gs[1]);
system("pause");
return 0;
}

改变的有 cs,ss,esp
在这里插入图片描述

描述符基础

实模式和保护模式

先说实模式,实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线,8个16位的通用寄存器,以及4个16位的段寄存器。
访问内存的格式为  (段基址:段偏移量)
就是物理地址 = 段基址<<4 + 段内偏移
保护模式:CPU的地址线的个数也从原来的20根变为现在的32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位。所以实模式下的内存地址计算方式就已经不再适合了。所以就引入了现在的保护模式,实现更大空间的,更灵活也更安全的内存访问。
我们的偏移值和实模式下是一样的,就是变成了32位而已,而段值仍旧是存放在原来16位的段寄存器中,但是这些段寄存器存放的却不再是段基址了,毕竟之前说过实模式下寻址方式不安全,我们在保护模式下需要加一些限制,而这些限制可不是一个寄存器能够容纳的,于是我们把这些关于内存段的限制信息放在一个叫做全局描述符表(GDT)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符。而段寄存器在保护模式下存放的便是相当于一个数组索引的东西,通过这个索引,可以找到对应的表项。段描述符存放了段基址、段界限、内存段类型属性(比如是数据段还是代码段,注意一个段描述符只能用来定义一个内存段)
也就是说,我们寻址的方式还是段基址+偏移地址。不过段基址不直接放在段寄存器了,而是开一个表,叫 GDT,即全局描述表,把段的基址、还有这个段的其它一些信息,比如权限什么的。
全局描述符表位于内存中,需要用专门的寄存器指向它后, CPU 才知道它在哪里。这个专门的寄存器便是GDTR(一个48位的寄存器),专门用来存储 GDT 的内存地址及大小
而在保护模式下要生成最终的地址,显然就变成了先到 GDT 里拿段基址,再和偏移地址组合起来。而 GDT 由于存了很多段,所以就需要有个指针指向哪个段,这个指针就是段选择子,平时放在段寄存器里。

GDTR GDT LDTR LDT

结合上面的知识,再总结一下GDTR GDT LDTR LDT
GDTR是一个长度为48bit的寄存器,内容为一个32位的基地址和一个16位的段限。其中32位的基址是指GDT在内存中的地址。
GDT是全局描述附表,主要存放操作系统和各任务公用的描述符,如公用的数据和代码段描述符、各任务的TSS描述符和LDT描述符。
LDTR是局部描述符寄存器,由一个可见的16位寄存器(段选择子)和一个不可见的描述符寄存器组成(描述符寄存器实际上是一个不可见的高速缓冲区)。
LDT是局部描述符表,主要存放各个任务的私有描述符,如本任务的代码段描述符和数据段描述符等。
这里再说一下啊LDT 和LDTR,局部描述符表(LDT)可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table,局部描述符表),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
LDT只是一个可选的数据结构,你完全可以不用它。使用它或许可以带来一些方便性,但同时也带来复杂性,如果你想让你的OS内核保持简洁性,以及可移植性,则最好不要使用它。

实验结果

所以cs的改变其实就是段选择子的改变,而ss,esp就是因为堆栈的切换而变化,特权切换必须伴随着堆栈的切换(TSS)
(windbg调试信息不一定准确)

再次开中断

之前做的开中断实验,不加入开中断指令,就会在0环产生一个死循环,而加上一个开中断指令,由于线程切换,会导致蓝屏。这是因为虽然我们进行了提权,但是还没有权限去使用0环的api函数,那如何保证在线程切换的条件下,不会使虚拟机蓝屏呢?
需要增加两行汇编代码环境

1
2
push 0x30
pop fs
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
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
DWORD g_eflags;

void __declspec(naked) IdtEntry() {
__asm {
push 0x30
pop fs
sti
Label :
jmp Label
iretd

}
}
void go() {
__asm {
int 0x20;
}
}


int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
printf("%p\n", g_eflags);
system("pause");
return 0;
}

fs在3环指向teb,可以保证结构化异常处理,而在0环指向kpcr 才能保证线程调度,所以我们应该把fs指向kpcr即可
实验结果:发现结束不了进程,卡死但是不会发生蓝屏,结束不了进程的原因就行只有0环返回3环时才可以杀死进程,而我们一直在0环里循环操作,无法返回3环,进而不能结束进程,不过我们可以用windbg命令直接nop掉循环代码。

内核API调用

1当我们从内核返回的时候,还是需要把fs改成0x3b,这样可以保证在3环运行不会出错

1
2
3
4
5
6
7
8
9
10
push 0x30
pop fs
sti
//

//
cli
push 0x3b
popfs
iretd

利用内核的api函数尝试申请一个地址,也可以用dbgprint尝试输出

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
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
typedef DWORD(__stdcall *EX_ALLOCATE)(DWORD PoolType, DWORD NUmberOfBytes);
EX_ALLOCATE ExAllocatePool = (EX_ALLOCATE)0x80537FF8;
DWORD g_pool;
//typedef DWORD(*DBG_PRINT)(char* Format, ...);
//DBG_PRINT DbgPrint = (DBG_PRINT)0x8052c862;
//char str[] = "Hello Driver";

void __declspec(naked) IdtEntry() {
__asm {
push 0x30
pop fs
sti
}
g_pool = ExAllocatePool(0, 4096);

//DbgPrint(str);
__asm {
cli
push 0x3b
pop fs
iretd

}



}
void go() {
__asm {
int 0x20;
}
}


int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
printf("%p\n", g_pool);
system("pause");
return 0;
}

成功申请到一个地址
在这里插入图片描述

_InlineHook

3环程序在低2g的地址是不一样的(私有地址空间),在高2g的地址是一样的(共有地址空间),我们尝试在ntkrnlpa.exe的3环到0环的入口地址做一个hook,这个需要两个程序:
在这里插入图片描述

第一个是patch ntkrnlpa.exe的kifastcallentry函数,让他跳到我们后面写的代码里

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
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
//jmp:8003f120 >80542525
void __declspec(naked) IdtEntry() {
__asm {
//可写权限
mov eax,cr0
and eax ,not 10000h
mov cr0,eax


//修改函数的起始位置为跳转
mov al,0xe9//jmp
mov ds:[0x80542520],al
mov eax,0xffafcbfb// 80542520>8003f120 ;0xffafcbfb=8003f120-80542520-5
mov ds:[0x80542521],eax

//还原之前的不可写环境
mov eax,cr0
or eax,10000h
mov cr0,eax
iretd
}



}
void go() {
__asm {
int 0x20;
}
}


int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
//printf("%p\n", g_pool);
system("pause");
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

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
//jmp:8003f120 >80542525
void JmpTarget();
int i;
char* p;
void __declspec(naked) IdtEntry() {
p = (char*)0x8003f120;//目标跳转地址
for (i = 0; i < 64; i++) {
*p = ((char*)JmpTarget)[i];
p++;
}
__asm {
iretd
}


}
void __declspec(naked) JmpTarget() {
__asm {
mov ecx,0x23
push 0x30
pop fs
mov ds,cx
mov es,cx
mov ecx,0x8054252d
jmp ecx
}



}
void go() {
__asm {
int 0x20;
}
}


int main() {
if ((DWORD)IdtEntry != 0x401040) {
printf("wrong addr: %p", IdtEntry);
exit(-1);
}
go();
system("pause");
return 0;
}

注意要先运行2再运行1,否则会发生蓝屏,用windbg反汇编,查看结果
在这里插入图片描述
现在hook函数已经写好了,现在准备填入自己的内容,可以用来测试函数被调用的次数

1
2
3
4
5
6
pushad
pushfd
mov eax,ds:[0x8003f3f0]
inc eax
mov ds:[0x8003f3f0],eax
popfd