Windows内核实验 ——— 再次开中断

前言

还记得我们之前的那个中断提权吗?我们修改了 idtr 表来把我们自己的中断函数进行执行。再此之前我们提到了在中断函数处理时我们无法再中断,但是在内核中存在有如下两个指令,可以让硬件中断变得可以执行或者是禁止执行

1
2
CLI ;禁止中断发生
STI ;允许中断发生

需要注意的是,当我们在内核模式下执行的时候应该尽快的恢复中断,如果长时间禁止中断时,我们可能会影响操作系统中其他动作的执行(如鼠标移动等等),此时的操作系统便会变得不稳定。

长时间的禁止硬件中断会影响内部时钟混乱

在此次实验中,我们将允许中断,进行观察对应的现象

实验过程

在实验开始之前,我们需要将虚拟机中的ntkrnlpa.exe提取出来,我们可以利用PCHunter来对其进行定位。

我们找到对应的程序后,将其拖入IDA进行反编译分析

我们选择 _KiFastCallEntry函数进行分析,此函数通过中断门进入 Ring 0,同时将 Ring 3 的 esp、eip、ss、cs、eflags 进行保存

大致可以得到以下信息:

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
.text:80542520                               _KiFastCallEntry proc near    ; DATA XREF: KiLoadFastSyscallMachineSpecificRegisters(x)+24↑o
.text:80542520 ; _KiTrap01+74↓o
.text:80542520
.text:80542520 var_B= byte ptr -0Bh
.text:80542520 anonymous_0= dword ptr -8
.text:80542520 anonymous_1= dword ptr -4
.text:80542520
.text:80542520 ; FUNCTION CHUNK AT .text:805424ED SIZE 00000026 BYTES
.text:80542520 ; FUNCTION CHUNK AT .text:805427C0 SIZE 00000014 BYTES
.text:80542520
.text:80542520 B9 23 00 00 00 mov ecx, 23h ; '#'
.text:80542525 6A 30 push 30h ; '0'
.text:80542527 0F A1 pop fs ; fs设置为0x30 为0环数据段
.text:80542529 8E D9 mov ds, ecx ; ds设置为0x23 为3环数据段
.text:8054252B 8E C1 mov es, ecx ; es = 0x23
.text:8054252D 64 8B 0D 40 00 00 00 mov ecx, large fs:40h ; 进入0环,fs:[0]指向KPCR表 _KPCR.TSS
.text:80542534 8B 61 04 mov esp, [ecx+4] ; TSS:esp0 即切换到0环堆栈
.text:80542537 6A 23 push 23h ; '#' ; 3环的 ss 压入
.text:80542539 52 push edx ; 3环esp
.text:8054253A 9C pushf ; 将三环的eflags压入栈
.text:8054253A
.text:8054253B
.text:8054253B loc_8054253B: ; CODE XREF: _KiFastCallEntry2+23↑j
.text:8054253B 6A 02 push 2
.text:8054253D 83 C2 08 add edx, 8 ; Add
.text:80542540 9D popf ; 设立新eflags = 0x2 清空0环标志位
.text:80542541 80 4C 24 01 02 or byte ptr [esp+1], 2 ; 设置IF=1,进行屏蔽中断
.text:80542546 6A 1B push 1Bh
.text:80542548 FF 35 04 03 DF FF push dword ptr ds:0FFDF0304h ; _KUSER_SHARED_DATA.SystemCallReturn
.text:8054254E 6A 00 push 0
.text:80542550 55 push ebp
.text:80542551 53 push ebx
.text:80542552 56 push esi
.text:80542553 57 push edi
.text:80542554 64 8B 1D 1C 00 00 00 mov ebx, large fs:1Ch ; ebx 指向 _KPCR
.text:8054255B 6A 3B push 3Bh ; ';'
.text:8054255D 8B B3 24 01 00 00 mov esi, [ebx+124h] ; KPCR.KPRCB.CurrentThread 获取当前线程
.text:80542563 FF 33 push dword ptr [ebx] ; _KPCR.NtTib.ExceptionList
.text:80542565 C7 03 FF FF FF FF mov dword ptr [ebx], -1 ; _KPCR.NtTib.ExceptionList=-1
.text:8054256B 8B 6E 18 mov ebp, [esi+18h] ; _KPCR._KPRCB.CurrentThread.InitialStack
.text:8054256E 6A 01 push 1 ; _KTRAP_FRAME.PreviousPreviousMode = 1,表示从3环来
.text:80542570 83 EC 48 sub esp, 48h ; esp 指向 _KTRAP_FRAME
.text:80542573 81 ED 9C 02 00 00 sub ebp, 29Ch ; Integer Subtraction
.text:80542579 C6 86 40 01 00 00 01 mov byte ptr [esi+140h], 1 ; CurrentThread.PreviousMode = 1,表示从3环调用来
.text:80542580 3B EC cmp ebp, esp ; 判断ebp与esp是否都指向_KTRP_FRAME
.text:80542582 75 8D jnz short loc_80542511 ; Jump if Not Zero (ZF=0)
.text:80542582
.text:80542584 83 65 2C 00 and dword ptr [ebp+2Ch], 0 ; _KTRAP_FRAME.Dr7 = 0
.text:80542588 F6 46 2C FF test byte ptr [esi+2Ch], 0FFh ; Logical Compare
.text:8054258C 89 AE 34 01 00 00 mov [esi+134h], ebp ; CurrentThread.TrapFrame = ebp,即指向当前 _KTRAP_FRAME
.text:80542592 0F 85 38 FE FF FF jnz Dr_FastCallDrSave ; 如果DebugActive == 1(被调试),那么跳转到 Dr_FastCallDrSave
.text:80542592 ; Dr_FastCallDrSave 的功能是保存调试寄存器
.text:80542592
.text:80542598
.text:80542598 loc_80542598: ; CODE XREF: Dr_FastCallDrSave+10↑j
.text:80542598 ; Dr_FastCallDrSave+7C↑j
.text:80542598 8B 5D 60 mov ebx, [ebp+60h]
.text:8054259B 8B 7D 68 mov edi, [ebp+68h]
.text:8054259E 89 55 0C mov [ebp+0Ch], edx ; _KTRAP_FRAME.DbgArgPointer = edx, 保存3环参数指针
.text:805425A1 C7 45 08 00 0D DB BA mov dword ptr [ebp+8], 0BADB0D00h
.text:805425A8 89 5D 00 mov [ebp+0], ebx ; _KTRAP_FRAME.DbgEbp = _KTRAP_FRAME.Ebp
.text:805425AB 89 7D 04 mov [ebp+4], edi ; _KTRAP_FRAME.DbgEip = _KTRAP_FRAME.Eip
.text:805425AE FB sti ; Set Interrupt Flag

我们将之前实验一的中断处理函数修改为:

1
2
3
4
5
6
7
8
9
10
void __declspec(naked) IdtEntry() { // 裸函数,不会产生对应的开辟栈帧过程
__asm {
push 0x30
pop fs ;仿照 _KiFastCallEntry 修改fs
sti ;开启中断
L:
jmp L
iretd ; 中断返回
}
}

我们编译运行,可以发现程序运行但是占用了100%的CPU

image-20230402125420975.png

此处但是虚拟机并不同于我们之前设置的循环就直接卡死,可以看到虚拟机可以甚至可以调出任务管理器,我们而也可以通过 WinDbg 对其进行断下,而不是直接的 Busy 状态。同时此处产生了一个问题,我们无法关闭这个测试程序。无论我们是直接关闭或者是通过任务管理器,都是关不掉对应程序的,我们都可以看到在后台会存在着对应的进程。

不会卡死是因为我们开起了中断,在 Ring 0 中允许中断是十分危险的。

为什么会无法对其进行关闭呢?在操作系统中存在有个APC队列的东西,我们可以通过WinDbg观察其结构

image-20230402142426424.png

同时需要注意的是线程是不能被“杀掉”、“挂起”、“恢复”的,线程在执行的时候自己占据着CPU,别人不能控制它,所以说线程如果想“死”,一定是自己执行代码把自己杀死,不存在“他杀”的情况。如果我们需要改变一共线程的行为,我们需要给他一个函数,让其进行调用。

在正常情况下,我们掉用关闭的线程是上述代码从 Ring 0 到 Ring 3 进行返回的时候调用。 对于上述的例子,我们设置了一个循环,将其进行了卡死,使其无法回到 Ring 3,进而造成了伪卡死状态(CPU占用100%,但是其他进程仍然有机会得到指令)。

此时如果关机或者重启可能会造成卡死现象

那么我们有什么方式可以将其进行处理一下?答案是有的,因为我们之前开启了中断,意味着我们可以使用 WinDbg 在 Ring 0 进行中断,我们之前的函数处理的地址为:0x00401040,我们对该地址处的东西进行反汇编,可以看到如下界面

image-20230402143818534.png

我们通过 WinDbg 将 jmp 处的汇编指令进行修改,将其改为 nop,那么便解除了对应的卡死状态,使其返回 Ring 3 时,可以调用相应的线程结束函数,使其”自我结束“。

image-20230402144120879.png

因为 ebfe 为双字,所以我们此时使用 ew 进行修改

此时我们便在任务管理器中看不到其身影了,已经成功的对其进行关闭。

总结

在此次实验中我们在 Ring 0 开中断, 允许线程调度, 制造了一个在 Ring 0 的死循环, 不能被杀死的进程。同时简单的了解了一下进程结束的相关东西以及Ring 3 到 Ring 0 的大致切换过程。


Windows内核实验 ——— 再次开中断
https://equinox-shame.github.io/2023/04/05/Widnows 内核实验 — 再次开中断/
作者
梓曰
发布于
2023年4月5日
许可协议