前言
所有保护模式索引链接:保护模式笔记一 保护模式介绍
前面学习了调用门之后继续学习中断门
中断门
中断门的作用
先前学习的调用门在实际的Windows中并没有被使用,只是操作系统提供了调用门描述符给开发人员使用。相比之下,Windows使用了中断门,用于:
- 系统调用(老的CPU通过中断门进入RING(内核)0层;新的CPU使用快速调用)
- 调试(常见的INT3 对应硬编码为0xCC)
中断门执行流程
- 根据INT XXX的值 查IDT(中断描述符表),找到对应的段描述符 这个描述符是一个中断门描述符
- 在中断门描述符中存储另一个代码段的选择子
- 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址
IDT
IDT全称Interrupt Descriptor Table(中断描述符表),和GDT相似,IDT也是由一系列描述符组成的。
- IDT中存储的段描述符都是系统段描述符
- IDT中的第一个元素不是NULL(不为空)
- IDT可以包含三种门描述符:①任务门描述符;②中断门描述符;③陷阱门描述符
使用windbg查看IDT的地址和长度:
查看地址:
r idtr
查看长度:
r idtl
中断门描述符
对比调用门描述符
中断门描述符结构
当一个段描述符是一个调用门描述符时,有以下特征:
- S位为0,表示该段描述符为系统段描述符(中断门描述符属于系统段描述符)
- Type域为1110,表示该段描述符为32位中断门
- 低16位到31位存储一个段选择子,该段选择子才和代码真正要调用的地址相关
- 真正要调用的地址 = 段选择子所指向的段.Base + 32位的段中偏移 (段中偏移分为两部分:高位31-16位和低位15-0位)
- 段.Base默认为0,故真正要调用的地址 = 32位的段中偏移
给出调用门描述符和中断门描述符各部分的对比(上半部分为调用门描述符,下半部分为中断门描述符):
数据位 |
31-16 |
15 |
14-13 |
12 |
11-8 |
7-5 |
4-0 |
含义 |
offset |
P |
DPL |
S |
Type |
无 |
param.count |
调用门 |
偏移 |
有效位 |
特权等级 |
值为0 |
值为1100 |
值为000 |
可以传递参数 |
中断门 |
同上 |
同上 |
同上 |
同上 |
值为1110 |
值为000 |
不允许传参,固定为0000 |
可以发现中断门描述符和调用门描述符的结构基本一致,只在Type域和参数计数处不同(Type域是描述符的类型标识;中断门不允许传参)
构造中断门描述符
了解了中断门描述符的结构后,尝试自己构造一个无参的中断门描述符,如下:
数据位 |
31-16 |
|
|
|
|
|
15 |
14-13 |
12 |
11-8 |
7-5 |
4-0 |
含义 |
offset |
|
|
|
|
|
P |
DPL |
S |
Type |
无 |
param |
解释 |
段中偏移 |
|
|
|
|
|
有效位 |
特权等级 |
值为0 |
值为1110 |
值为000 |
不允许传参 |
值(二进制) |
0 |
|
|
|
|
|
1 |
11 |
0 |
1110 |
000 |
0000 |
数据位 |
31-16 |
15-0 |
含义 |
selector |
offset |
解释 |
段选择子 |
段中偏移 |
值(十六进制) |
0x0008 |
0 |
得到调用门描述符为:0000EE00`00080000
段中偏移暂时不明确要调用的代码段,先置0
示例代码
接下来给出一段演示代码:
#include <Windows.h>
#include <stdio.h>
int value;
__declspec(naked) void INTGate(){
_asm{
pushad
pushfd
mov value,0x610
popfd
popad
iretd
}
}
int main(){
//使用 中断门
_asm{
int 0x20
}
printf("%X\n",value);
return 0;
}
代码说明
代码十分简单,主要分为两部分:
- INTGate:中断门真正要调用的函数,给全局变量赋值,之后中断返回
- main:通过中断进入中断门,最后输出全局变量观察是否通过中断门被修改
将门描述符写入IDT
中断索引和IDT地址的对应关系
在代码中,索引的值为0x20,其对应的IDT中的地址为:8003fc00
关于索引值和IDT地址的对应关系为:
IDT地址 = 索引值 × 8 + IDT首地址
代入当前的值即为:IDT地址 = 0x20 × 8 + 0x8003f400 = 0x100 + 0x8003f400 = 0x8003f500
确定门描述符
在写入GDT前,还需要确定要写入的值,前面已经构造好了的门描述符为:0x0000EE00`00080000
但其段中偏移还未确定,于是使用VC++ 6.0查看要调用的代码的地址:
进入debug模式,中断后,选中INTGate函数,然后右键→Go to Disassembly(查看反汇编)
可以得到要调用的函数的地址为0x00401020
将得到的要调用的函数地址填入门描述符中对应的offset得到:
- 原:0000EE00`00080000
- 现:0040EE00`00081020
于是得到确定的门描述符为0040EE00`00081020
确定中断索引并写入门描述符
确定中断索引其实就是确定要写入中断描述符的地址,根据前面中断索引和IDT地址的对应关系,不难倒推出:
中断索引 = (要写入中断描述符的地址 - IDT首地址)÷ 8
因此问题又转换为了确定要写入的中断描述符地址
流程如下图所示:
用到的指令如下:
1.查看IDT首地址:
r idtr
2.使用指令查看IDT内容:
dq 8003f400 L30
这里的L30代表要查看的长度为 0x30 个qword长度的数据,即0x30个段描述符
3.找到要写入的地址后,将构造好的中断门描述符写入:
eq 8003f500 0040EE00`00081020
同时在确定了要写入的地址后,就可以根据计算出中断索引:
中断索引 = (要写入中断描述符的地址 - IDT首地址)÷ 8 = (8003f500 - 8003f400) ÷ 8 = 0x100 ÷ 8 = 0x20
4.最后再查看写入的地址,确保已正确写入:
dq 8003f500
执行代码
执行结果如下:
全局变量能够被修改,说明中断门能够正常执行
对比执行前后寄存器和堆栈
执行前寄存器情况
在使用中断门语句处下断点,断下后得到:
得到此时的寄存器情况:
寄存器 |
说明 |
值 |
ESP |
栈顶寄存器 |
12FF34 |
EBP |
栈底寄存器 |
12FF80 |
CS |
代码段寄存器 |
1B |
DS |
数据段寄存器 |
23 |
ES |
附加段寄存器 |
23 |
SS |
堆栈段寄存器 |
23 |
FS |
附加段寄存器 |
3B |
GS |
附加段寄存器 |
0 |
EFL |
标志寄存器 |
202 |
有关段寄存器的详解可回顾:保护模式笔记二 段寄存器
关于标志寄存器的详解可回顾:逆向基础笔记五 标志寄存器
这里简单拆解一下标志寄存器:
先将值转换为二进制得到 0x202→ 0000 0000 0000 0000 0000 0010 0000 0010
按对应的结构填入得到:
数据位 |
31-12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
标志位 |
0 |
OF |
DF |
IF |
TF |
SF |
ZF |
0 |
AF |
0 |
PF |
1 |
CF |
含义 |
0 |
溢出标志 |
方向标志 |
中断使能标志 |
单步标志 |
符号标志 |
零标志 |
0 |
辅助进位标志 |
0 |
奇偶标志 |
1 |
进位标志 |
值 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
- 此时IF标志位为1表示当前CPU允许响应INTR可屏蔽中断请求
- 若IF标志位为0则表示CPU不会响应可屏蔽中断请求
执行前堆栈情况
记录下此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
0012FF34 |
ESP |
0 |
无 |
0012FF38 |
ESP+4 |
0 |
无 |
0012FF3C |
ESP+8 |
0x7FFDF000 |
无 |
0012FF40 |
ESP+12 |
0xCCCCCCCC |
INT3的硬编码 |
执行后寄存器情况
为了查看执行后寄存器的情况,在INTGate函数中加入了INT 3引发软中断,但在中断门调用的代码中再引发软中断会引发错误,这里仅作演示观察使用。修改后的INTGate函数如下:
__declspec(naked) void INTGate(){
_asm{
int 3 //中断
pushad
pushfd
mov value,0x610
popfd
popad
iretd
}
}
之后INT3中断后查看寄存器情况如下:
得到此时的寄存器情况:
寄存器 |
说明 |
值 |
ESP |
栈顶寄存器 |
B9CAFDCC |
EBP |
栈底寄存器 |
12FF80 |
CS |
代码段寄存器 |
08 |
DS |
数据段寄存器 |
23 |
ES |
附加段寄存器 |
23 |
SS |
堆栈段寄存器 |
10 |
FS |
附加段寄存器 |
30 |
GS |
附加段寄存器 |
0 |
EFL |
标志寄存器 |
2 |
执行后堆栈情况
通过内存窗口观察此时的堆栈情况:
得到此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
B9CAFDCC |
ESP |
0040105A |
执行后要返回的地址 |
B9CAFDD0 |
ESP+0x4 |
1B |
执行后要恢复的段选择子:CS |
B9CAFDD4 |
ESP+0x8 |
302 |
EFL标志寄存器 |
B9CAFDD8 |
ESP+0xC |
0012FF34 |
执行后要恢复的堆栈寄存器:ESP |
B9CAFDDC |
ESP+0x10 |
23 |
执行后要恢复的段选择子:SS |
对比执行前后寄存器
执行前后寄存器情况如下:
寄存器 |
说明 |
执行前的值 |
执行后的值 |
是否变化 |
ESP |
栈顶寄存器 |
12FF34 |
B9CAFDCC |
√ |
EBP |
栈底寄存器 |
12FF80 |
12FF80 |
× |
CS |
代码段寄存器 |
1B |
08 |
√ |
DS |
数据段寄存器 |
23 |
23 |
× |
ES |
附加段寄存器 |
23 |
23 |
× |
SS |
堆栈段寄存器 |
23 |
10 |
√ |
FS |
附加段寄存器 |
3B |
30 |
√ |
GS |
附加段寄存器 |
0 |
0 |
× |
EFL |
标志寄存器 |
202 |
2 |
√ |
主要关注到执行前后标志寄存器的变化:
将执行后的EFL按对应的结构拆解得到:
数据位 |
31-12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
标志位 |
0 |
OF |
DF |
IF |
TF |
SF |
ZF |
0 |
AF |
0 |
PF |
1 |
CF |
含义 |
0 |
溢出标志 |
方向标志 |
中断使能标志 |
单步标志 |
符号标志 |
零标志 |
0 |
辅助进位标志 |
0 |
奇偶标志 |
1 |
进位标志 |
值 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
对比发现,中断门调用后将标志寄存器的IF标志位置为0,表明当前正在处理中断请求,不再响应其它可屏蔽中断
对比执行前后堆栈
执行前后堆栈情况如下:
|
ESP |
ESP+0x4 |
ESP+0x8 |
ESP+0xC |
ESP+0x10 |
执行前 |
0 |
0 |
7FFDF000 |
CCCCCCCC |
CCCCCCCC |
执行前数据说明 |
无 |
无 |
无 |
INT3硬编码 |
INT3硬编码 |
执行后 |
0040105A |
1B |
302 |
0012FF34 |
23 |
执行后数据说明 |
返回地址(EIP) |
CS |
EFL |
ESP |
SS |
不难发现中断门执行后,向堆栈中压入了5个值:SS、ESP、EFL、CS、返回地址
IRETD指令
为了研究IRETD指令干了什么,观察IRETD执行前后堆栈和寄存器的变化情况
IRETD执行前
通过内存窗口观察执行前的堆栈情况:
得到此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
B9CAFDCC |
ESP |
0040105A |
执行后要返回的地址 |
B9CAFDD0 |
ESP+0x4 |
1B |
执行后要恢复的段选择子:CS |
B9CAFDD4 |
ESP+0x8 |
302 |
EFL标志寄存器 |
B9CAFDD8 |
ESP+0xC |
0012FF34 |
执行后要恢复的堆栈寄存器:ESP |
B9CAFDDC |
ESP+0x10 |
23 |
执行后要恢复的段选择子:SS |
再观察此时的寄存器情况:
寄存器 |
说明 |
值 |
ESP |
栈顶寄存器 |
B9CAFDCC |
EBP |
栈底寄存器 |
12FF80 |
CS |
代码段寄存器 |
08 |
DS |
数据段寄存器 |
23 |
ES |
附加段寄存器 |
23 |
SS |
堆栈段寄存器 |
10 |
FS |
附加段寄存器 |
30 |
GS |
附加段寄存器 |
0 |
EFL |
标志寄存器 |
2 |
IRETD执行后
通过内存窗口观察执行后的堆栈情况:
地址 |
相对栈顶地址 |
值 |
12FF34 |
ESP |
0 |
12FF38 |
ESP+0x4 |
0 |
12FF3C |
ESP+0x8 |
7FFD5000 |
12FF40 |
ESP+0xC |
CCCCCCCC |
12FF44 |
ESP+0x10 |
CCCCCCCC |
查看寄存器情况:
寄存器 |
说明 |
值 |
ESP |
栈顶寄存器 |
12FF34 |
EBP |
栈底寄存器 |
12FF80 |
CS |
代码段寄存器 |
1B |
DS |
数据段寄存器 |
23 |
ES |
附加段寄存器 |
23 |
SS |
堆栈段寄存器 |
23 |
FS |
附加段寄存器 |
3B |
GS |
附加段寄存器 |
0 |
EFL |
标志寄存器 |
202 |
IRETD执行前后对比
堆栈对比
|
ESP |
ESP+0x4 |
ESP+0x8 |
ESP+0xC |
ESP+0x10 |
执行前 |
0040105A |
1B |
302 |
0012FF34 |
23 |
执行前数据说明 |
返回地址(EIP) |
CS |
EFL |
ESP |
SS |
执行后 |
0 |
0 |
7FFD5000 |
CCCCCCCC |
CCCCCCCC |
寄存器对比
寄存器 |
说明 |
执行前的值 |
执行后的值 |
是否变化 |
ESP |
栈顶寄存器 |
B9CAFDCC |
12FF34 |
√ |
EBP |
栈底寄存器 |
12FF80 |
12FF80 |
× |
CS |
代码段寄存器 |
08 |
1B |
√ |
DS |
数据段寄存器 |
23 |
23 |
× |
ES |
附加段寄存器 |
23 |
23 |
× |
SS |
堆栈段寄存器 |
10 |
23 |
√ |
FS |
附加段寄存器 |
30 |
0 |
√ |
GS |
附加段寄存器 |
0 |
0 |
× |
EFL |
标志寄存器 |
2 |
202 |
√ |
IRETD返回的时候比RETF多了一个EFL的恢复,关于RETF的内容可回顾:保护模式笔记八 调用门提权(无参+有参)
中断门使用RETF返回
了解了IRETD的原理后,就可以尝试使用RETF来返回
示例代码
示例代码如下:
__declspec(naked) void INTGate(){
_asm{
pushad
pushfd //中断门会修改eflags的IF位为0 所以需要保存标志寄存器
mov eax,[esp+0x24] //ret
mov ebx,[esp+0x28] //cs
//中间少了个esp+0x2c 为EFL
mov ecx,[esp+0x30] //esp
mov edx,[esp+0x34] //ss
mov [esp+0x24+4],eax
mov [esp+0x28+4],ebx
mov [esp+0x2c+4],ecx
mov [esp+0x30+4],edx
mov value,0x610
popfd
popad
add esp,4
retf
}
}
执行结果
依旧可以正常返回,并且执行正常
代码说明
代码也比较简短简单,可以分为七个部分:
- 保护现场:pushad、pushfd
- 将堆栈中的数据取出存到寄存器
- 将取出来的数据覆盖到堆栈中
- 全局变量赋值
- 恢复现场:popfd、popad
- 堆栈平衡:add esp,4
- 返回:retf
要理解堆栈数据的覆盖和平衡首先要了解IRETD和RETF的区别
IRETD 中断返回需要堆栈中按顺序存储:返回地址、CS、EFL、ESP、SS 共5个数据
RETF返回需要堆栈中按顺序存储:返回地址、CS、ESP、SS 共4个数据
因此将堆栈中的数据由原本的5个数据替换成4个数据即可
|
ESP |
ESP+0x4 |
ESP+0x8 |
ESP+0xC |
ESP+0x10 |
取出数据 |
返回地址(EIP) |
CS |
EFL |
ESP |
SS |
取出到的寄存器 |
EAX |
EBX |
无 |
ECX |
EDX |
覆盖数据 |
|
返回地址(EIP) |
CS |
ESP |
SS |
|
ESP |
ESP+0x4 |
ESP+0x8 |
ESP+0xC |
ESP+0x10 |
ESP+4前 |
|
返回地址(EIP) |
CS |
ESP |
SS |
ESP+4后 |
返回地址(EIP) |
CS |
ESP |
SS |
|
因此通过对堆栈中数据进行覆盖,即可实现在中断门中使用RETF返回
总结
- 中断门执行后会将EFL(标志位寄存器)中的IF标志位 置0,使CPU不再响应可屏蔽中断
- 执行中断门时,分为两种情况:
- 在没有权限切换时,只向堆栈中压入3个值:①CS;②EFL;③返回地址
- 在涉及权限切换时,会向堆栈中压入5个值:①SS;②ESP;③EFL;④CS;⑤返回地址
- 中断门不允许传递参数,调用门允许传递参数
- 中断门通过INT N(索引)执行,调用门通过远调用 CALL FAR CS:EIP执行
- 中断门一般使用IRET(16位)/IRETD(32位)返回,调用门一般使用RETF返回
- Windows并没有使用调用门,但有使用中断门