文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/202988
安全客 – 有思想的安全新媒体
0x01 前言
由于关于Kernel安全的文章实在过于繁杂,本文有部分内容大篇幅或全文引用了参考文献,若出现此情况的,将在相关内容的开头予以说明,部分引用参考文献的将在文件结尾的参考链接中注明。
Kernel的相关知识以及一些实例在Kernel中的利用已经在Kernel Pwn 学习之路(一)(二)给予了说明
Kernel中内存管理的相关知识已经在Kernel Pwn 学习之路(三)给予了说明
本文以及接下来的几篇文章将主要以系统调用为例介绍内核中的中断处理机制。本文涉及到的所有Linux Kernel
相关代码均基于5.6.2
版本。
限于篇幅的原因,本文仅介绍了IDT
的初始化,下一篇文章将更多的涉及中断服务函数的内容~
【传送门】:Kernel Pwn 学习之路(一)
【传送门】:Kernel Pwn 学习之路(二)
【传送门】:Kernel Pwn 学习之路(三)
0x02 中断的概述
什么是中断
中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
这里我们可以举一个比较实际的例子🌰:
比如说我正在厨房用煤气烧一壶水,这样就只能守在厨房里,苦苦等着水开——如果水溢出来浇灭了煤气,有可能就要发生一场灾难了。等啊等啊,外边突然传来了惊奇的叫声“怎么不关水龙头?”,于是我惭愧的发现,刚才接水之后只顾着抱怨这份无聊的差事,居然忘了这事,于是慌慌张张的冲向水管,三下两下关了龙头,声音又传到耳边,“怎么干什么都是这么马虎?”。伸伸舌头,这件小事就这么过去了,我落寞的眼神又落在了水壶上。
门外忽然又传来了铿锵有力的歌声,我最喜欢的古装剧要开演了,真想夺门而出,然而,听着水壶发出“咕嘟咕嘟”的声音,我清楚:除非等到水开,否则没有我享受人生的时候。在这个场景中,我是唯一具有处理能力的主体,不管是烧水、关水龙头还是看电视,同一个时间点上我只能干一件事情。但是,在我专心致志干一件事情时,总有许多或紧迫或不紧迫的事情突然出现在面前,都需要去关注,有些还需要我停下手头的工作马上去处理。只有在处理完之后,方能回头完成先前的任务,“把一壶水彻底烧开!”
中断机制不仅赋予了我处理意外情况的能力,如果我能充分发挥这个机制的妙用,就可以“同时”完成多个任务了。回到烧水的例子,实际上,无论我在不在厨房,煤气灶总是会把水烧开的,我要做的,只不过是及时关掉煤气灶而已,为了这么一个一秒钟就能完成的动作,却让我死死地守候在厨房里,在10分钟的时间里不停地看壶嘴是不是冒蒸气,怎么说都不划算。我决定安下心来看电视。当然,在有生之年,我都不希望让厨房成为火海,于是我上了闹钟,10分钟以后它会发出“尖叫”,提醒我炉子上的水烧开了,那时我再去关煤气也完全来得及。我用一个中断信号——闹铃——换来了10分钟的欢乐时光,心里不禁由衷地感叹:中断机制真是个好东西。
正是由于中断机制,我才能有条不紊地“同时”完成多个任务,中断机制实质上帮助我提高了并发“处理”能力。它也能给计算机系统带来同样的好处:如果在键盘按下的时候会得到一个中断信号,CPU就不必死守着等待键盘输入了;如果硬盘读写完成后发送一个中断信号,CPU就可以腾出手来集中精力“服务大众”了——无论是人类敲打键盘的指尖还是来回读写介质的磁头,跟CPU的处理速度相比,都太慢了。没有中断机制,就像我们苦守厨房一样,计算机谈不上有什么并行处理能力。
跟人相似,CPU也一样要面对纷繁芜杂的局面——现实中的意外是无处不在的——有可能是用户等得不耐烦,猛敲键盘;有可能是运算中碰到了0除数;还有可能网卡突然接收到了一个新的数据包。这些都需要CPU具体情况具体分析,要么马上处理,要么暂缓响应,要么置之不理。无论如何应对,都需要CPU暂停“手头”的工作,拿出一种对策,只有在响应之后,方能回头完成先前的使命,“把一壶水彻底烧开!”
中断的类型
概括地说,可以将中断分为两个主要类别:
- 外部或硬件产生的中断(异步中断)
- 软件生成的中断(同步中断)
异步中断是通过由 Local APIC
或者与 Local APIC
连接的处理器针脚接收。
同步中断是由处理器自身的特殊情况引起(有时使用特殊架构的指令)。一个常见的例子是是division by zero
(除零错误),另一个示例是使用syscall
指令退出程序。
如前所述,中断可以在任何时间因为超出代码和 CPU 控制的原因而发生。对于同步中断,还可以分为三类:
Faults
(故障)—— 这是在执行“不完善的”指令之前报告的异常,中断服务程序运行结束后允许恢复被中断的程序。Traps
(陷门)—— 这是在执行trap
指令之后即刻报告的异常,中断服务程序运行结束后允许恢复被中断的程序。Aborts
(终止)—— 这种异常从不报告引起异常的精确指令,中断服务程序运行结束不允许恢复被中断的程序。
另外,中断又可分为可屏蔽中断(Maskable interrupt
)和非屏蔽中断(Nomaskable interrupt
)。
对于可屏蔽中断,在x86_64
架构中,可以使用cli
命令阻止中断信号的发送。
/* In /source/arch/x86/include/asm/irqflags.h#L47 */
static inline void native_irq_disable(void)
{
asm volatile("cli": : :"memory");
}
static inline void native_irq_enable(void)
{
asm volatile("sti": : :"memory");
}
可屏蔽中断能否发送取决于中断寄存器中的IF
标志位。
cli
命令会将在这个标志位清除,而sti
命令会将这个标志位置位。
非屏蔽中断将会始终进行报告,通常,硬件产生的任何错误都将作为非屏蔽中断进行报告!
中断的产生
简化起见,假定每一个物理硬件都有一根连接 CPU 的中断线。设备可以使用它向CPU发出中断信号。但是,这个中断信号并不会直接发送给CPU。在老旧的机器中,有一个PIC芯片,负责顺序处理来自各种设备的各种中断请求。在新机器中,有一个通常被称为 APIC
的高级可编程中断控制器。一个APIC
由两个互相独立的设备组成:
Local APIC
(本地控制器)I/O APIC
(IO控制器)Local APIC
位于每个CPU核心中,它负责处理特定于 CPU 的中断配置。
Local APIC
常被用于管理来自APIC
时钟(APIC-timer
)、热敏元件和其他与I/O
设备连接的设备的中断。
I/O APIC
提供多核处理器的中断管理,它被用来在所有的 CPU 核心中分发外部中断。
中断可以随时发生。发生中断时,操作系统必须立即处理它。处理逻辑的概述如下:
- 内核必须暂停执行当前进程。(抢占当前任务)
- 内核必须搜索中断处理程序并且转交控制权(执行中断处理程序)
- 中断处理程序执行结束后,被中断的进程可以恢复执行。(交还控制流,解除抢占)
当然,在处理中断的过程中涉及许多复杂问题。但是以上三个步骤构成了该过程的基本框架。
每个中断处理程序的地址都被保存在一个特殊的位置,这个位置被称为IDT(Interrupt Descriptor Table,中断描述符表)
。
如果同时发生多个异常或中断,则处理器将按照其预定义的优先级顺序对其进行处理。优先级如下所示:
- 硬件重置或机器检查(
Hardware Reset and Machine Checks
) - 任务调度时触发陷门(
Trap on Task Switch
) ——TSS
中的T
标志位被置位时发生 - 外部硬件干预(External Hardware Interventions) —— 发生下列指令之一时报告
FLUSH
—— 刷新STOPCLK
—— 时钟发出终止信号SMI
—— 系统管理中断(System Management Interrupt
)INIT
—— 初始化
- 指令陷门(
Traps on the Previous Instruction
) —— 常见于断点(BreakPoint
)和调试异常(Debug Trap Exceptions
) - 非屏蔽中断(
Nonmaskable Interrupts
) - 可屏蔽的硬件中断(
Maskable Hardware Interrupts
) - 代码断点错误(
Code Breakpoint Fault
) - 以下三种异常或中断均属于第八优先级
- 获取下一条指令时出错(
Faults from Fetching Next Instruction
) - 违反代码段限制(
Code-Segment Limit Violation
) - 代码页错误(
Code Page Fault
)
- 获取下一条指令时出错(
- 以下四种异常或中断均属于第九优先级
- 对下一条指令解码时出错(
Faults from Decoding the Next Instruction
) - 指令长度大于16个字节(
Instruction length > 15 bytes
) OP Code
不合法(Invalid Opcode
)- 协处理器不可用(
Coprocessor Not Available
)
- 对下一条指令解码时出错(
- 以下几种异常或中断均属于第十优先级
- 运行指令时出错(
Faults on Executing an Instruction
) - 溢出(
Instruction length > 15 bytes
) - 绑定错误(
Bound error
) - 任务状态段不合法(
Invalid TSS(Task State Segment)
) - 段不存在(
Segment Not Present
) - 堆栈错误(
Stack fault
) - 一般保护(
General Protection
) - 数据页错误(
Data Page Fault
) - 对齐验证(
Alignment Check
) - x87 FPU浮点异常(
x87 FPU Floating-point exception
) - SIMD FPU浮点异常(
SIMD floating-point exception
) - 虚拟化异常(
Virtualization exception
)
- 运行指令时出错(
中断号与中断向量
处理器使用唯一的编号来识别中断或异常的类型,这个编号被称为中断号( vector number
)。它将作为IDT(Interrupt Descriptor Table,中断描述符表)
的索引值,中断号的取值范围是从0
到255
。在Linux Kernel
中关于中断设置的地方可以找到这样的检查:
/* In /source/arch/x86/kernel/idt.c#L230 */
static void set_intr_gate(unsigned int n, const void *addr)
{
struct idt_data data;
BUG_ON(n > 0xFF);
memset(&data, 0, sizeof(data));
data.vector = n;
data.addr = addr;
data.segment = __KERNEL_CS;
data.bits.type = GATE_INTERRUPT;
data.bits.p = 1;
idt_setup_from_table(idt_table, &data, 1, false);
}
从0
到31
的前32个中断号由处理器保留,用于处理体系结构定义的异常和中断。
Vector | Mnemonic | Description | Type | Error Code | Source |
---|---|---|---|---|---|
0 | #DE | Divide Error | Fault | NO | DIV and IDIV |
1 | #DB | Reserved | F/T | NO | |
2 | — | NMI | INT | NO | external NMI |
3 | #BP | Breakpoint | Trap | NO | INT 3 |
4 | #OF | Overflow | Trap | NO | INTO instruction |
5 | #BR | Bound Range Exceeded | Fault | NO | BOUND instruction |
6 | #UD | Invalid Opcode | Fault | NO | UD2 instruction |
7 | #NM | Device Not Available | Fault | NO | Floating point or [F]WAIT |
8 | #DF | Double Fault | Abort | YES | An instruction which can generate NMI |
9 | — | Reserved | Fault | NO | |
10 | #TS | Invalid TSS | Fault | YES | Task switch or TSS access |
11 | #NP | Segment Not Present | Fault | NO | Accessing segment register |
12 | #SS | Stack-Segment Fault | Fault | YES | Stack operations |
13 | #GP | General Protection | Fault | YES | Memory reference |
14 | #PF | Page fault | Fault | YES | Memory reference |
15 | — | Reserved | NO | ||
16 | #MF | x87 FPU fp error | Fault | NO | Floating point or [F]Wait |
17 | #AC | Alignment Check | Fault | YES | Data reference |
18 | #MC | Machine Check | Abort | NO | |
19 | #XM | SIMD fp exception | Fault | NO | SSE[2,3] instructions |
20 | #VE | Virtualization exc. | Fault | NO | EPT violations |
21-31 | — | Reserved | INT | NO | External interrupts |
从 32
到 255
的中断标识码设计为用户定义中断并且不被系统保留。这些中断通常分配给外部I/O
设备,使这些设备可以发送中断给处理器。
如前所述,IDT
存储中断和异常处理程序的入口点,其结构与Global Descriptor Table
结构类似。IDT
的表项被称为门(gates
)的成员,它可以是以下类型之一:
- Interrupt gates(中断门)
- Task gates(任务门)
- Trap gates(陷阱门)
在x86
架构下,仅能使用长模式下的Interrupt gates
或Trap gates
能在x86_64
中被引用。就像 GDT
(全局描述符表),IDT
在 x86
上是一个 8 字节数组门,而在 x86_64
上是一个 16 字节数组门。
IDT
可以在线性地址空间和基址的任何地方被加载。同时,它需要在 x86
上以 8 字节对齐,在 x86_64
上以 16 字节对齐。IDT
的基址存储在一个特殊的寄存器——IDTR
中。
在 x86
上有两个指令LIDT(Load Interrupt Descriptor Table
)、SIDT(Store Interrupt Descriptor Table)
来修改 IDTR
寄存器的值。
指令 LIDT
用来加载 IDT
的基址,即将指定操作数存在 IDTR
中。
指令 SIDT
用来在读取 IDTR
的内容并将其存储在指定操作数中。
在 x86
上 IDTR
寄存器是 48 位,包含了下面的信息:
47 16 15 0
+-----------------------------------+----------------------+
| Base address of the IDT | Limit of the IDT |
+-----------------------------------+----------------------+
0x03 IDT 的初始化
IDT
由setup_idt
函数进行建立及初始化操作
处理器准备进入保护模式(go_to_protected_mode
函数分析)
对IDT的配置在go_to_protected_mode
函数中完成,该函数首先调用了 setup_idt
函数配置了IDT,然后将处理器的工作模式从实模式环境中脱离进入保护模式。保护模式(Protected Mode
,或有时简写为 pmode
)是一种80286
系列和之后的x86
兼容CPU
操作模式。保护模式有一些新的特色,设计用来增强多功能和系统稳定度,像是内存保护,分页系统,以及硬件支援的虚拟内存。大部分的现今x86
操作系统都在保护模式下运行,包含Linux
、FreeBSD
、以及微软 Windows 2.0
和之后版本。
setup_idt
函数在go_to_protected_mode
函数中调用,go_to_protected_mode
函数在/source/arch/x86/boot/pm.c#L102
中实现:
/*
* Actual invocation sequence
*/
void go_to_protected_mode(void)
{
/* Hook before leaving real mode, also disables interrupts */
// 首先进行Hook操作进而从实模式中脱离,禁用中断
realmode_switch_hook();
/* Enable the A20 gate */
// 启动 A20 门
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* Reset coprocessor (IGNNE#) */
// 重置协处理器
reset_coprocessor();
/* Mask all interrupts in the PIC */
// 在 PIC 中标记所有的中断
mask_all_interrupts();
/* Actual transition to protected mode... */
// 开始过渡到保护模式
setup_idt();
setup_gdt();
// 正式进入保护模式
protected_mode_jump(boot_params.hdr.code32_start,(u32)&boot_params + (ds() << 4));
}
初始化IDTR
寄存器(setup_idt
函数分析)
setup_idt
在/source/arch/x86/boot/pm.c#L93
中实现
go_to_protected_mode
将仅加载一个NULL表项在IDT
中
/*
* Set up the IDT
*/
static void setup_idt(void)
{
// 准备一个 null_idt
static const struct gdt_ptr null_idt = {0, 0};
// 使用 lidt 指令把它加载到 IDTR 寄存器
asm volatile("lidtl %0" : : "m" (null_idt));
}
gdt_ptr
类型表示了一个48-bit的特殊功能寄存器 GDTR
,其包含了全局描述符表 Global Descriptor Table
的基地址,其在/source/arch/x86/boot/pm.c#L59
中定义:
/*
* Set up the GDT
*/
struct gdt_ptr {
u16 len;
u32 ptr;
} __attribute__((packed));
这就是 IDTR
结构的定义,就像我们在之前的示意图中看到的一样,由 2 字节和 4 字节(共 48 位)的两个域组成。显然,在此处的 gdt_prt
不是代表 GDTR
寄存器而是代表 IDTR
寄存器,因为我们将其设置到了中断描述符表中。之所以在Linux
内核代码中没有idt_ptr
结构体,是因为其与gdt_prt
具有相同的结构而仅仅是名字不同,因此没必要定义两个重复的数据结构。可以看到,内核在此处并没有填充Interrupt Descriptor Table
,这是因为此刻处理任何中断或异常还为时尚早,因此我们仅仅以NULL
来填充IDT
。
0 条评论