Windows中断机制

PS:老文章,一年多前写的。最近在复习一些内核的概念,拿出来重发一下

Windows中断机制

很久没搞过Windows内核了,很多概念和技术都忘得七七八八了,因为后面想要研究IntelVT,所以这里就以Windows中断进制为切入点,重新回顾下Windows内核的相关概念,顺便解决下一直依赖搞得都不是很明白的IntelVT技术。

双机调试环境搭建

最开始研究Windows内核的时候搭建的XP的环境,网上能找到的关于XP的资料也是最全的,最保底的有毛德操老师的<<Windows内核情景分析分析>>一书,但是2021年了,XP还是作为参考的好,主要的研究对象应该还是要放在Win10/Win7 X64上面。

另外微软官方已经不在Symbol Server上提供XP的Symbols了,这会使得调试XP非常麻烦,网上是有大佬提供备份的XP符号的,但是由于我不想再那么麻烦了,因此就不再考虑XP的调试了。

环境搭建可以参考这篇文章
https://bianchengnan.gitee.io/articles/vmware-virtualkd-windbg-win10-kernel-debug-setup-step-by-step/

配置虚拟机

首先应该去下面这个网站上下载Win10和Win7的镜像
https://msdn.itellyou.cn/

然后用Vmware把他们都给装上,这里有个问题是,如果你和我一样使用Vmware16的话,会发现16不能在Win7上正常安装VmTools,解决方案呢参考这里
https://blog.csdn.net/teisite/article/details/117675403

由于微软更新了驱动程序签名算法,2019年开始弃用SHA1,改用SHA2。猜测VMware Tools驱动程序使用SHA2,而Windows7只支持SHA1,需要下载安装补丁kb4474419来支持SHA2算法。下载地址:https://www.catalog.update.microsoft.com/Search.aspx?q=kb4474419

打上补丁之后重新让Vmware把Tool的光盘载入进来安装就行

Q:没Tools怎么把补丁拖进Vmware里面
A:这里我的方法是用U盘

Windbg

https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk

WDK (Windows Driver Kit) 是Windows驱动开发工具包,目前最新版本是WDK11,不过安装这个之前你得有VS2019,并且其配套的SDK是
https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/

Windbg是在SDK里面的,如果只需要Windbg的话可以只装一个SDK,但是后面跑不掉的得用驱动开发,因此我这里全部装上。

配置Symbol Server

这里配置Symbol Server有两种方式,一种是在Windbg里面配(Symbol File Path),另一种就直接添加这样一个环境变量即可
两个**之间填的是符号文件保存的本地路径,后面网址不用动


后面还得配置下Image File Path,要不然启动的时候会卡一下,这个里面填个目录就行

VirtualKD

最原始的配置方法就是手动给虚拟机设置个串口,然后手动给虚拟机加个启动项,比较麻烦,
这里已经有大佬写的一键配置的工具了,官方的不是很稳,用这个项目就很稳
https://github.com/4d61726b/VirtualKD-Redux

我们需要把target64这个文件夹拖到虚拟机里面,直接点击vminstall安装

后面每次启动虚拟机前,我们先打开vmmon64.exe,这个会监测我们正在运行的被调试虚拟机,然后自动启动windbg挂载上去。

这里winDbg Path也要配置一下

注意由于X64是有驱动签名保护的,所以每次我们到启动项页面的时候要按F8,然后选择禁用强制驱动签名,这样才可以正常调试

基础概念

IDT

IDT表是由IDT表项构成的,一个表项代表一个中断,x64的IDT表项如下图,当中断发生时,中断控制器会向CPU发送一个interrupt vector,也就是IDT表项的索引,CPU会根据整个索引去找到响应的表项,再根据表项里的值进行地址跳转。

R idtr读取idtr的值
然后dq 读取idt表

!irq 命令可以直接dump出当前所有中断

以第一个IDT表项为例,按照格式,我们可以拼出其地址为fffff800 0398 ec00是对的

这个地址一般是ISR或者ISR的前奏,ISR是指中断处理例程,由其对具体的中断进行处理,
这里要提到的是每个处理器都有一个IDT表,因此不同的处理器可以运行不同的ISR,典型的就是每个处理器都可以收到时钟中断,但只需要一个处理器来更新系统时钟。

中断控制器

最开始,传统的X86系统依赖于i8259A PIC,但是这个Controller只能工作在单处理器系统上,并且只有8条中断线,因此后来IBM又网上加了一个从控制器,使得整个i8259A系统支持15条中断线,但是由于还是有很多瓶颈和问题,因此逐渐被淘汰了
在现在的主板上,使用的是APIC,APIC由LAPIC和IOAPIC两个设备组成,外部设备的外部中断会先到达IOAPIC,IOAPIC会轮流向处理器的LAPIC传递中断,最后由LAPIC中断CPU。

当处理器被中断后,会向Controller询问GSIV global system interrupt vector
有时也被称为IRQ,这是一个数字,表示是发生了什么中断,中断控制器会把GSIV转换成process interrupt vector,这个才是IDT表的索引。APIC上的这个对应关系是可以编程的。

因为主板上可以有多个IOAPIC,所以主板上还有个core logic再IOAPIC和LAPIC之间,其作用是中断路由,使得整个系统更高效,在大部分情况下,Windows会重新编程IOAPIC 以使其支持自己的特性 例如 interrupt sterring

IRQL

IRQL(Interrupt Request Level)是Windows使用的一种中断请求优先级机制,这个曾是我很迷的一个概念, IRQL字面意思中断请求等级,其在具体语境下应该有两个意思:
1.当前CPU所处的中断优先级等级
2.各个中断对应的中断请求等级

X32 的IRQL有32个等级,X64的IRQL分为16个等级
中断优先级就是字面意思,位于高IRQL的中断可以”中断”位于低IRQL的中断,同时CPU会提高到这个高IRQL的IRQL。
IRQL表示当前CPU所处的中断优先级等级就是一种处理器状态,KPCR中是有个成员表示其值.

以前觉得IRQL是Windows实现的优先级机制,但其实其仍然是借助APIC实现的,在APIC中有个TPR (Task Priority Register)寄存器

和PPR(Processor-Priority)寄存器

Intel手册中描述的APIC的结构

在这张图的右上角可以看到PPR的值是由TPR决定的,由PPR决定中断是否发送到CPU,而X64架构下APIC的TPR寄存器现在直接绑定到了处理器的CR8上,也就是直接mov CR8就可以修改TPR的值。

每个中断向量是8bit的值,其前4bit表示interrupt-priority,总计16个值,就代表了16个IRQL。

回头看下TPR寄存器,bits7:4决定了task-priority class,在PPR中,bits7:4决定了Processor-priority class,这个值是当前正在运行的优先级,sub-class是不参与决定中断是否该被传递给CPU的。

PPR的值是基于TPR和ISRV的,不过这个ISRV应该会被设置成0:

PPR[7:4] (the processor-priority class) the maximum of TPR[7:4] (the task- priority class) and ISRV[7:4] (the priority of the highest priority interrupt in service).

最终当PPR被设置后,只有interrupt-priority大于processor-priority的中断向量才会被传递给CPU。

代码分析

Windows系统在boot阶段,kernel会把IDT表项初始化成针对各种异常的专用的例程,或者是一个thunk — KilsrThunk,第三方设备驱动可以注册自己的ISR到这个Thunk来使用这个中断。

IDT的前32个表项是Windows预留的的专用例程

在ntoskrnk中是搜不到KiLsrThunk这个函数的,毕竟只是个Thunk。
以0x40号中断来看

可以看到Thunk都是一个样,先压入中断号,再压入RBP,最后跳转到KiIstLinkage
KiIstLinkage直接搜是搜不到的,在IDA要搜KxIsrLinkage
直接用!idt命令可以查到时钟中断0xd1的ISR函数是HalpHpetClockInterrupt
我们在上面下断点bp HalpHpetClockInterrupt

断下之后可以用kv查看堆栈

可以看到在调用到HalpHpetClockInterrupt函数之前还经历了KiInterruptDispatchNoLock

这里就要讲解下驱动程序怎么插入ISR到中断里面了

驱动ISR注册流程

这段参考文章
https://bbs.pediy.com/thread-270388.htm#msg_header_h2_1

1
2
3
4
5
6
7
8
9
10
11
12
13
NTSTATUS IoConnectInterrupt(
[out] PKINTERRUPT *InterruptObject,
[in] PKSERVICE_ROUTINE ServiceRoutine,
[in, optional] PVOID ServiceContext,
[in, optional] PKSPIN_LOCK SpinLock,
[in] ULONG Vector,
[in] KIRQL Irql,
[in] KIRQL SynchronizeIrql,
[in] KINTERRUPT_MODE InterruptMode,
[in] BOOLEAN ShareVector,
[in] KAFFINITY ProcessorEnableMask,
[in] BOOLEAN FloatingSave
);

驱动程序通过IoConnectInterrupt注册一个ISR到中断里,这里面参数ServiceRoutine是真正的ISR函数,ServiceContext是传入ISR的参数,Vector是中断向量号,Irql是中断等级,这个函数最终会构造一个KINTERRUPT对象插入到KPCR中的InterruptObject数组里面,中断号是索引

这个函数在X64下面变得还是很复杂的,整体的部分我还不是很明白,所以只分析下其中比较关键的两个函数
KeInitializeInterrupt和KiConnectInterrupt(PKINTERRUPT interrupt)

KeInitialize是初始化KINTRRUPT结构体

可以看到其逻辑就是根据传进来的参数初始化KInterrupt对象,
紧接着在KiInitializeInterrupt中

可以看到我们前面的KiInterruptDispatchNoLock函数,其会被设置到DispatchAddress里面(这个函数里面具体的这些分支我还没有搞明白)

最终在KiConnectInterrupt中,这个新建的KINTERRUPT对象会被插入到KPCR的interruptobject数组里面

这里首先会看到检查IRQL是不是设置对的,和前面讲的一样,这个值必须等于vector的前四位

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
  if ( !interrupt->Connected )
{
v9 = (PKINTERRUPT)0x140000000i64;
v11 = (char *)KiIsrThunkShadow + 8 * vector;
if ( !KiKvaShadow )
v11 = (char *)KxUnexpectedInterrupt0 + 8 * vector;
if ( v15 == v11 )
{
v9 = (PKINTERRUPT)*KiGetInterruptObjectAddress(vector);
if ( v9 ) // 如果已经有interrupt存在KPCR上
{
if ( real_vector >= 0x30 )
{
v5 = 1;
if ( interrupt->Mode == v9->Mode
&& interrupt->ShareVector
&& v9->ShareVector
&& (__int64 (__fastcall *)(__int64, __int64, __int64, __int64, char))interrupt->DispatchAddress == KiInterruptDispatch
&& ((__int64 (__fastcall *)(__int64, __int64, __int64, __int64, char))v9->DispatchAddress == KiInterruptDispatch
|| (__int64 (__fastcall *)(__int64, __int64, __int64, __int64, char))v9->DispatchAddress == KiChainedDispatch) )
{
v6 = 1;
interrupt->Connected = 1;
if ( (__int64 (__fastcall *)(__int64, __int64, __int64, __int64, char))v9->DispatchAddress != KiChainedDispatch )
{
v9->InterruptListEntry.Blink = &v9->InterruptListEntry;
v9->InterruptListEntry.Flink = &v9->InterruptListEntry;
v9->DispatchAddress = (void (__fastcall *)())KiChainedDispatch;// 把DispatchAddress修改成ChainedDispatch
}
v12 = v9->InterruptListEntry.Blink;
v9 = (PKINTERRUPT)((char *)v9 + 8);
interrupt->InterruptListEntry.Blink = v12;
interrupt->InterruptListEntry.Flink = (_LIST_ENTRY *)v9;
v12->Flink = &interrupt->InterruptListEntry;
v9->InterruptListEntry.Flink = &interrupt->InterruptListEntry;
}
}
}
else
{
if ( !interrupt->SynchronizeIrql )
{
interrupt->InterruptListEntry.Blink = &interrupt->InterruptListEntry;
interrupt->InterruptListEntry.Flink = &interrupt->InterruptListEntry;
interrupt->DispatchAddress = (void (__fastcall *)())KiChainedDispatch;
}
v6 = 1;
interrupt->Connected = 1;
*KiGetInterruptObjectAddress(vector) = interrupt;
}
}
}
LOBYTE(v9) = v14[0];
KiReleaseInterruptConnectLock(v9, v16);
if ( v6 )
result = v5 != 0 ? 0x127 : 0;
else
LABEL_25:
result = 0xC00000EFi64;
return result;
}

这个函数后面的逻辑就是判断interruptvector数组里面 以参数vector索引的地方是不是已经有对象了,如果有的话会检查一下是不是开了shareVector也就是共享向量,如果开了会把这两个KINTERRUPT链在一起,并且将链头的DispatchAddress改成KiChainedDispatch,也就链式模式,如果还没对象,就简单将其新构建的对象放进去。

至此,ISR就注册到了对应的中断向量上了。

ISR的执行流程

整体的执行流程如下:
KiIsrThunk -> KxIsrLinkage(KiIsrLinkage) -> KiInterruptDispatchNoLock -> ourISR

KiIsrLinkage中这一块代码就是按向量号从InterruptObject中取出DispatchAddress来执行

KiInterruptDispatchNoLock的主要逻辑就是,
1.设置IRQL

2.执行驱动注册的ISR函数

3.恢复IRQL

4.iretq
iret返回,中断处理结束

上面是单个interrupt的模式,如果是链式就是会依次执行链上的每个ISR,具体的可以看前面那篇文章。

附:windbg调试技巧

基础命令
https://bbs.pediy.com/thread-250670.htm Windbg新手入坑指南
https://bbs.pediy.com/thread-270324.htm Windbg使用详解
http://boxcounter.com/technique/2012-02-08-teb-kpcr-gs-in-x64/ 快速查找当前CPU的KPCR

首先需要说明的是,在x64中,gs的base内容已经挪到MSR(Model-Specific Registers) (boxcounter: 可参考注1),需要使用如下方法:

1: kd> rdmsr 0xC0000101
msr[c0000101] = fffff880`009f2000

!irql可以快速查看当前的IRQL

!idt查看IDT表

kv查看CallStack

verTarget可以快速查看当前的kernelbase

总结

这篇文章其实也不过是简单的梳理了一些中断的一些基本逻辑,但是中断毕竟涉及到很多硬件方面的逻辑,所以可能会有很多谬误,我只能尽量使得一些关键逻辑能够自洽,所有大佬勿喷,也希望大佬能来一起交流,ORZ