吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5872|回复: 24
收起左侧

[系统底层] 保护模式笔记九 中断门和IDT(中断描述符表)

[复制链接]
lyl610abc 发表于 2021-6-8 16:18
本帖最后由 lyl610abc 于 2021-6-8 16:19 编辑

前言

所有保护模式索引链接:保护模式笔记一 保护模式介绍

前面学习了调用门之后继续学习中断门


中断门

中断门的作用

先前学习的调用门在实际的Windows中并没有被使用,只是操作系统提供了调用门描述符给开发人员使用。相比之下,Windows使用了中断门,用于:

  1. 系统调用(老的CPU通过中断门进入RING(内核)0层;新的CPU使用快速调用)
  2. 调试(常见的INT3 对应硬编码为0xCC)

中断门执行流程

  1. 根据INT XXX的值 查IDT(中断描述符表),找到对应的段描述符 这个描述符是一个中断门描述符
  2. 在中断门描述符中存储另一个代码段的选择子
  3. 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址

IDT

IDT全称Interrupt Descriptor Table(中断描述符表),和GDT相似,IDT也是由一系列描述符组成的。

  • IDT中存储的段描述符都是系统段描述符
  • IDT中的第一个元素不是NULL(不为空)
  • IDT可以包含三种门描述符:①任务门描述符;②中断门描述符;③陷阱门描述符

使用windbg查看IDT的地址和长度:

查看地址:

r idtr

查看长度:

r idtl

image-20210607201242023


中断门描述符

对比调用门描述符

image-20210605100534276


中断门描述符结构

image-20210607203904224


当一个段描述符是一个调用门描述符时,有以下特征:

  • 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(查看反汇编)

image-20210608001212251


image-20210608001253807

可以得到要调用的函数的地址为0x00401020


将得到的要调用的函数地址填入门描述符中对应的offset得到:

  • 原:0000EE00`00080000
  • 现:0040EE00`00081020

于是得到确定的门描述符为0040EE00`00081020


确定中断索引并写入门描述符

确定中断索引其实就是确定要写入中断描述符的地址,根据前面中断索引和IDT地址的对应关系,不难倒推出:

中断索引 =  (要写入中断描述符的地址 - IDT首地址)÷ 8

因此问题又转换为了确定要写入的中断描述符地址


流程如下图所示:

image-20210608002633819


用到的指令如下:

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

执行代码

执行结果如下:

image-20210608002829685

全局变量能够被修改,说明中断门能够正常执行


对比执行前后寄存器和堆栈

执行前寄存器情况

在使用中断门语句处下断点,断下后得到:

image-20210608134938201


得到此时的寄存器情况:

寄存器 说明
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中断后查看寄存器情况如下:

image-20210608141920136


得到此时的寄存器情况:

寄存器 说明
ESP 栈顶寄存器 B9CAFDCC
EBP 栈底寄存器 12FF80
CS 代码段寄存器 08
DS 数据段寄存器 23
ES 附加段寄存器 23
SS 堆栈段寄存器 10
FS 附加段寄存器 30
GS 附加段寄存器 0
EFL 标志寄存器 2

执行后堆栈情况

通过内存窗口观察此时的堆栈情况:

image-20210608142711851


得到此时的堆栈情况:

地址 相对栈顶地址 说明
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执行前

image-20210608145457816

通过内存窗口观察执行前的堆栈情况:

image-20210608145323930


得到此时的堆栈情况:

地址 相对栈顶地址 说明
B9CAFDCC ESP 0040105A 执行后要返回的地址
B9CAFDD0 ESP+0x4 1B 执行后要恢复的段选择子:CS
B9CAFDD4 ESP+0x8 302 EFL标志寄存器
B9CAFDD8 ESP+0xC 0012FF34 执行后要恢复的堆栈寄存器:ESP
B9CAFDDC ESP+0x10 23 执行后要恢复的段选择子:SS

再观察此时的寄存器情况:

image-20210608145835213


寄存器 说明
ESP 栈顶寄存器 B9CAFDCC
EBP 栈底寄存器 12FF80
CS 代码段寄存器 08
DS 数据段寄存器 23
ES 附加段寄存器 23
SS 堆栈段寄存器 10
FS 附加段寄存器 30
GS 附加段寄存器 0
EFL 标志寄存器 2

IRETD执行后

通过内存窗口观察执行后的堆栈情况:

image-20210608151938831


地址 相对栈顶地址
12FF34 ESP 0
12FF38 ESP+0x4 0
12FF3C ESP+0x8 7FFD5000
12FF40 ESP+0xC CCCCCCCC
12FF44 ESP+0x10 CCCCCCCC

查看寄存器情况:

image-20210608152126564


寄存器 说明
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
         }
 }

执行结果

image-20210608155316526

依旧可以正常返回,并且执行正常


代码说明

代码也比较简短简单,可以分为七个部分:

  1. 保护现场:pushad、pushfd
  2. 将堆栈中的数据取出存到寄存器
  3. 将取出来的数据覆盖到堆栈中
  4. 全局变量赋值
  5. 恢复现场:popfd、popad
  6. 堆栈平衡:add esp,4
  7. 返回: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不再响应可屏蔽中断
  • 执行中断门时,分为两种情况:
    1. 在没有权限切换时,只向堆栈中压入3个值:①CS;②EFL;③返回地址
    2. 在涉及权限切换时,会向堆栈中压入5个值:①SS;②ESP;③EFL;④CS;⑤返回地址
  • 中断门不允许传递参数,调用门允许传递参数
  • 中断门通过INT N(索引)执行,调用门通过远调用 CALL FAR CS:EIP执行
  • 中断门一般使用IRET(16位)/IRETD(32位)返回,调用门一般使用RETF返回
  • Windows并没有使用调用门,但有使用中断门

免费评分

参与人数 15吾爱币 +19 热心值 +15 收起 理由
junjia215 + 1 + 1 谢谢@Thanks!
jomme012 + 1 + 1 我很赞同!
Chenda1 + 1 + 1 我很赞同!
renminbi + 1 + 1 我很赞同!
鱼无论次 + 2 + 1 写得太好了
爱你小吉君 + 1 热心回复!
努力加载中 + 1 + 1 谢谢@Thanks!
天山雪 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
石碎大胸口 + 1 + 1 用心讨论,共获提升!
debug_cat + 1 + 1 用心讨论,共获提升!
sam喵喵 + 1 谢谢@Thanks!
qianshang666 + 2 + 1 用心讨论,共获提升!
xzl9552547 + 1 我很赞同!
TR小米哥 + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
千百度° + 5 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

qianshang666 发表于 2021-6-8 18:06
大哥又更新了,我立马来支持一下
TR小米哥 发表于 2021-6-8 16:36
bigdawn 发表于 2021-6-8 20:01
泰达王 发表于 2021-6-8 20:24
复杂啊谢谢分享
是荞麦呀 发表于 2021-6-8 21:07
感谢大佬分享
debug_cat 发表于 2021-6-8 21:33
十年老粉不请自来,咦更新了。
hubohang 发表于 2021-6-8 22:46
666 大佬 学习了
胡种花 发表于 2021-6-8 22:50
666 大佬 学习了
天山雪 发表于 2021-6-9 00:20
支持一下。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-3-29 01:39

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表