保护模式笔记九 中断门和IDT(中断描述符表)
https://www.52pojie.cn/thread-1455684-1-1.html
(出处: 吾爱破解论坛)
前言
所有保护模式索引链接:保护模式笔记一 保护模式介绍
前面学习了调用门之后继续学习中断门
中断门
中断门的作用
先前学习的调用门在实际的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位的段中偏移
给出调用门描述符和中断门描述符各部分的对比(上半部分为调用门描述符,下半部分为中断门描述符):
可以发现中断门描述符和调用门描述符的结构基本一致,只在Type域和参数计数处不同(Type域是描述符的类型标识;中断门不允许传参)
构造中断门描述符
了解了中断门描述符的结构后,尝试自己构造一个无参的中断门描述符,如下:
得到调用门描述符为:0000EE00`00080000
段中偏移暂时不明确要调用的代码段,先置0
示例代码
接下来给出一段演示代码:
复制代码 隐藏代码
#include <Windows.h>
#include <stdio.h>
int value;__declspec(naked) void INTGate(){_asm{pushadpushfd mov value,0x610popfdpopadiretd}}int main(){//使用 中断门_asm{int 0x20}printf("%X\n",value);return 0;
}
代码说明
代码十分简单,主要分为两部分:
INTGate:中断门真正要调用的函数,给全局变量赋值,之后中断返回
main:通过中断进入中断门,最后输出全局变量观察是否通过中断门被修改
将门描述符写入IDT
中断索引和IDT地址的对应关系
在代码中,索引的值为0x20,其对应的IDT中的地址为:8003f500
关于索引值和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
执行代码
执行结果如下:
全局变量能够被修改,说明中断门能够正常执行
对比执行前后寄存器和堆栈
执行前寄存器情况
在使用中断门语句处下断点,断下后得到:
得到此时的寄存器情况:
寄存器 说明 值
有关段寄存器的详解可回顾:保护模式笔记二 段寄存器
关于标志寄存器的详解可回顾:逆向基础笔记五 标志寄存器
这里简单拆解一下标志寄存器:
先将值转换为二进制得到 0x202→ 0000 0000 0000 0000 0000 0010 0000 0010
按对应的结构填入得到:
此时IF标志位为1表示当前CPU允许响应INTR可屏蔽中断请求
若IF标志位为0则表示CPU不会响应可屏蔽中断请求
执行前堆栈情况
记录下此时的堆栈情况:
执行后寄存器情况
为了查看执行后寄存器的情况,在INTGate函数中加入了INT 3引发软中断,但在中断门调用的代码中再引发软中断会引发错误,这里仅作演示观察使用。修改后的INTGate函数如下:
__declspec(naked) void INTGate(){_asm{int 3 //中断pushadpushfd mov value,0x610popfdpopadiretd}}
之后INT3中断后查看寄存器情况如下:
得到此时的寄存器情况:
执行后堆栈情况
通过内存窗口观察此时的堆栈情况:
得到此时的堆栈情况:
对比执行前后寄存器
执行前后寄存器情况如下:
主要关注到执行前后标志寄存器的变化:
将执行后的EFL按对应的结构拆解得到:
对比发现,中断门调用后将标志寄存器的IF标志位置为0,表明当前正在处理中断请求,不再响应其它可屏蔽中断
对比执行前后堆栈
执行前后堆栈情况如下:
不难发现中断门执行后,向堆栈中压入了5个值:SS、ESP、EFL、CS、返回地址
IRETD指令
为了研究IRETD指令干了什么,观察IRETD执行前后堆栈和寄存器的变化情况
IRETD执行前
通过内存窗口观察执行前的堆栈情况:
得到此时的堆栈情况:
再观察此时的寄存器情况:
IRETD执行后
通过内存窗口观察执行后的堆栈情况:
查看寄存器情况:
IRETD执行前后对比
堆栈对比
寄存器对比
IRETD返回的时候比RETF多了一个EFL的恢复,关于RETF的内容可回顾:保护模式笔记八 调用门提权(无参+有参)
中断门使用RETF返回
了解了IRETD的原理后,就可以尝试使用RETF来返回
示例代码
示例代码如下:
__declspec(naked) void INTGate(){_asm{pushadpushfd //中断门会修改eflags的IF位为0 所以需要保存标志寄存器mov eax,[esp+0x24] //retmov ebx,[esp+0x28] //cs//中间少了个esp+0x2c 为EFLmov ecx,[esp+0x30] //espmov edx,[esp+0x34] //ssmov [esp+0x24+4],eaxmov [esp+0x28+4],ebxmov [esp+0x2c+4],ecxmov [esp+0x30+4],edxmov value,0x610popfdpopadadd esp,4retf}}
执行结果
依旧可以正常返回,并且执行正常
代码说明
代码也比较简短简单,可以分为七个部分:
保护现场:pushad、pushfd
将堆栈中的数据取出存到寄存器
将取出来的数据覆盖到堆栈中
全局变量赋值
恢复现场:popfd、popad
堆栈平衡:add esp,4
返回:retf
要理解堆栈数据的覆盖和平衡首先要了解IRETD和RETF的区别
IRETD 中断返回需要堆栈中按顺序存储:返回地址、CS、EFL、ESP、SS 共5个数据
RETF返回需要堆栈中按顺序存储:返回地址、CS、ESP、SS 共4个数据
因此将堆栈中的数据由原本的5个数据替换成4个数据即可
因此通过对堆栈中数据进行覆盖,即可实现在中断门中使用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并没有使用调用门,但有使用中断门