前言
所有保护模式索引链接:保护模式笔记一 保护模式介绍
在先前的保护模式笔记七 CALL 长调用与短调用中没有具体分析长调用的实例,接下来补充其需要的相关知识:调用门
调用门
调用门的作用
调用门可以用来提权,通过提权可以实现访问高地址空间等RING0(内核)层才能进行的操作
调用门和长调用关系
回顾先前的笔记可知:
长调用CALL FAR CS:EIP指令要调用的地址是由CS段选择子查GDT得到的调用门 段描述符得来的,后面的EIP不发挥作用
调用门执行流程
- 根据CS的值 查GDT,找到对应的段描述符 这个描述符是一个调用门
- 在调用门描述符中存储另一个代码段的选择子
- 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址
调用门描述符
对比段描述符
调用门描述符结构
当一个段描述符是一个调用门描述符时,有以下特征:
- S位为0,表示该段描述符为系统段描述符(调用门描述符属于系统段描述符)
- Type域为1100,表示该段描述符为32位调用门
- 低16位到31位由原本的基地址变为存储一个段选择子,该段选择子才和代码真正要调用的地址相关
- 真正要调用的地址 = 段选择子所指向的段.Base + 32位的段中偏移 (段中偏移分为两部分:高位31-16位和低位15-0位)
- 段.Base默认为0,故真正要调用的地址 = 32位的段中偏移
给出段描述符和调用门描述符各部分的对比(上半部分为段描述符,下半部分为调用门描述符):
数据位 |
31-24 |
23 |
22 |
21 |
20 |
19-16 |
15 |
14-13 |
12 |
11-8 |
7-0 |
|
含义 |
Base |
G |
D/B |
0 |
AVL |
Seg.Limit |
P |
DPL |
S |
Type |
Base |
|
解释 |
基地址 |
粒度 |
默认操作大小 |
固定为0 |
用于系统软件使用 |
段大小限制 |
有效位 |
特权等级 |
描述符类型 |
段类型 |
基地址 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
数据位 |
31-16 |
|
|
|
|
|
15 |
14-13 |
12 |
11-8 |
7-5 |
4-0 |
含义 |
offset |
|
|
|
|
|
P |
DPL |
S |
Type |
无 |
param.count |
解释 |
段中偏移 |
|
|
|
|
|
有效位 |
特权等级 |
值为0 |
值为1100 |
值为000 |
参数计数 |
数据位 |
31-16 |
15-0 |
含义 |
Base Adress |
Segment Limit |
解释 |
基地址 |
段大小限制 |
|
|
|
数据位 |
31-16 |
15-0 |
含义 |
selector |
offset |
解释 |
段选择子 |
段中偏移 |
构造无参调用门描述符
了解了调用门描述符的结构后,尝试自己构造一个无参的调用门描述符,如下:
数据位 |
31-16 |
|
|
|
|
|
15 |
14-13 |
12 |
11-8 |
7-5 |
4-0 |
含义 |
offset |
|
|
|
|
|
P |
DPL |
S |
Type |
无 |
param |
解释 |
段中偏移 |
|
|
|
|
|
有效位 |
特权等级 |
值为0 |
值为1100 |
值为000 |
参数 |
值(二进制) |
0 |
|
|
|
|
|
1 |
11 |
0 |
1100 |
000 |
0000 |
数据位 |
31-16 |
15-0 |
含义 |
selector |
offset |
解释 |
段选择子 |
段中偏移 |
值(十六进制) |
0x0008 |
0 |
得到调用门描述符为:0000EC00`00080000
段中偏移暂时不明确要调用的代码段,先置0
示例代码
接下来给出一段演示代码:
#include <Windows.h>
#include <stdio.h>
__declspec(naked) void callGate(){
_asm{
int 3 //软中断
retf //注意这里长调用对应长返回
}
}
int main(){
char buff[6];
//*(DWORD*)&buff[0]= 0x12345678; //低地址32位为0x12345678,EIP已废弃,故随便填即可
//*(DWORD*)&buff[4]=0x48; //高地址16位为0x48,段选择子
//也可以换作这种写法
_asm{
mov dword ptr ds:[buff],0x12345678 //低32位赋值废弃EIP
lea eax,dword ptr ds:[buff] //将buff地址给EAX
add eax,4 //地址+4,即得到高地址
mov word ptr ds:[eax],0x48 //高16位赋值段选择子selector
}
//使用 调用门
_asm{
call fword ptr ds:[buff] //fword 数据宽度为6字节
}
return 0;
}
代码说明
代码十分简单,主要分为两部分:
- callGate:调用门真正要调用的函数,先软中断,然后长返回
- main:先构造一个CS:EIP,这里为0x48:0x12345678,然后使用调用门
关于构造CS:EIP,可以观察到赋值后buff在内存中的存储情况:
可以看到CS:EIP在内存中由高地址到低地址存储,为0x004812345678
将门描述符写入GDT
在代码中,CS的也就是段选择子的值为0x48,该选择子指向的GDT的地址为要写入的地址
关于Selector和GDT地址的对应关系在保护模式笔记三 段描述符和段选择子中已经说明过了,这里不再赘述
确定门描述符
在写入GDT前,还需要确定要写入的值,前面已经构造好了的门描述符为:0x0000EC00`00080000
但其段中偏移还未确定,于是使用VC++ 6.0查看要调用的代码的地址:
进入debug模式,中断后,选中callGate函数,然后右键→Go to Disassembly(查看反汇编)
可以得到要调用的函数的地址为0x00401020
将得到的要调用的函数地址填入门描述符中对应的offset得到:
- 原:0000EC00`00080000
- 现:0040EC00`00081020
于是得到确定的门描述符为0040EC00`00081020
写入门描述符
确定完门描述符和要写入的地址后,就可以将其写入GDT了,操作如下:
指令如下:
r gdtr 查看gdtr
dq 0x8003f000 以qword查看地址,这里的地址为上面得到的gdtr地址
eq 8003f048 0040EC00`00081020 写入门描述符
dq 0x8003f000 查看是否写入成功
过程图如下:
执行代码
执行代码结果如下:
Windbg获取到了代码中的int 3断点
可以看到此时中断的地址正是门描述符中的偏移地址(要调用的地址 = 段.Base+Offset,Base默认为0,故要调用的地址就直接等于门描述符中的offset)
原本的Ring3(应用)层的int 3断点不会被Windbg所捕获,但这里通过门描述符提权后变为了Ring0(内核层)权限,故会引起Windbg的捕获
可以查看此时的寄存器情况:
r
此时的CS正是前面构造的门描述符中的selector(选择子)
接下来继续单步执行
t
可以看到int 3的下一行代码位retf,也就是callGate函数里的代码中的下一行,由此可以确定调用成功
对比执行前后寄存器
前面只提到了CS段寄存器的变化,现在来总览对比执行前后寄存器的变化:
执行前寄存器情况
在使用调用门的汇编语句处下断点,断下后得到:
得到此时的寄存器情况:
寄存器 |
值 |
ESP |
12FF2C |
EBP |
12FF80 |
CS |
1B |
DS |
23 |
ES |
23 |
SS |
23 |
FS |
3B |
GS |
0 |
执行后寄存器情况
得到此时的寄存器情况:
寄存器 |
值 |
ESP |
B1026DD0 |
EBP |
12FF80 |
CS |
08 |
DS |
23 |
ES |
23 |
SS |
10 |
FS |
30 |
GS |
0 |
变化对比
寄存器 |
执行前值 |
执行后值 |
是否变化 |
ESP |
12FF2C |
B1026DD0 |
√ |
EBP |
12FF80 |
12FF80 |
× |
CS |
1B |
08 |
√ |
DS |
23 |
23 |
× |
ES |
23 |
23 |
× |
SS |
23 |
10 |
√ |
FS |
3B |
30 |
√ |
GS |
0 |
0 |
× |
可以得出变化的寄存器有:ESP、CS、SS、FS
通过调用门提权后,前后寄存器的变化涉及到TSS,这里先记录下变化,具体细节留作之后
构造有参调用门描述符
示例代码
#include <Windows.h>
#include <stdio.h>
int a,b,c;
__declspec(naked) void callGate(){
_asm{
pushad //将所有32位通用寄存器压入堆栈
pushfd //将32位标志寄存器EFLFAGS压入堆栈
//关于为何是通过ESP+XXX寻址详见后续的堆栈情况说明
mov eax,[esp+0x24+0x8+0x8] //从堆栈中取出第一个参数
mov dword ptr ds:[a],eax //将取出的参数赋值给全局变量a
mov eax,[esp+0x24+0x8+0x4] //从堆栈中取出第二个参数
mov dword ptr ds:[b],eax //将取出的参数赋值给全局变量b
mov eax,[esp+0x24+8+0] //从堆栈中取出第三个参数
mov dword ptr ds:[c],eax //将取出的参数赋值给全局变量c
popfd //将所有32位通用寄存器出栈
popad //将所有32位标志寄存器EFLFAGS出栈
retf 0xC//注意这里长调用对应长返回,堆栈平衡 0xC=12=3*4=参数个数*参数的数据宽度(单位字节)
}
}
int main(){
char buff[6];
//*(DWORD*)&buff[0]= 0x12345678; //低地址32位为0x12345678,EIP已废弃,故随便填即可
//*(DWORD*)&buff[4]=0x48; //高地址16位为0x48,段选择子
//也可以换作这种写法
_asm{
mov dword ptr ds:[buff],0x12345678 //低32位赋值废弃EIP
lea eax,dword ptr ds:[buff] //将buff地址给EAX
add eax,4 //地址+4,即得到高地址
mov word ptr ds:[eax],0x48 //高16位赋值段选择子selector
}
//使用 调用门
_asm{
push 1
push 2
push 3
call fword ptr ds:[buff] //fword 数据宽度为6字节
}
printf("%X\t%X\t%X\n",a,b,c);
return 0;
}
代码说明
与构造无参调用门描述符相比,主要变化为:
- 在使用调用门前压入了三个参数:1、2、3
- 调用代码作用为:①保护现场(压入所有通用寄存器和标志寄存器);②从堆栈中取出对应的参数;③将取出的参数赋值给对应的全局变量
- 调用代码最后要平衡堆栈,ret 0xC 0xC=12=3*4=参数个数*参数的数据宽度(单位字节)
- 在调用结束后,输出调用后被赋值的全局变量,验证参数是否成功传递
堆栈情况说明
堆栈调用情况按执行流程顺序依次说明:
执行前(压入参数后)
记录下此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
0012FF20 |
ESP |
3 |
压入的第三个参数 |
0012FF24 |
ESP+4 |
2 |
压入的第二个参数 |
0012FF28 |
ESP+8 |
1 |
压入的第一个参数 |
切换到调用代码后
记录下此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
B9CAFDC4 |
ESP |
0040D4E8 |
执行后要返回的地址 |
B9CAFDC8 |
ESP+0x4 |
1B |
执行后要恢复的段选择子:CS |
B9CAFDCC |
ESP+0x8 |
3 |
压入的第三个参数 |
B9CAFDD0 |
ESP+0xC |
2 |
压入的第二个参数 |
B9CAFDD4 |
ESP+0x10 |
1 |
压入的第一个参数 |
B9CAFDD8 |
ESP+0x14 |
0012FF20 |
执行后要恢复的堆栈寄存器:ESP |
B9CAFDDC |
ESP+0x18 |
23 |
执行后要恢复的段选择子:SS |
保存通用寄存器组后
记录下此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
B9CAFDA4~B9CAFDC0 |
ESP~ESP+0x1C |
略 |
通用寄存器组 |
B9CAFDC4 |
ESP+0x20 |
0040D4E8 |
执行后要返回的地址 |
B9CAFDC8 |
ESP+0x20+0x4 |
1B |
执行后要恢复的段选择子:CS |
B9CAFDCC |
ESP+0x20+0x8 |
3 |
压入的第三个参数 |
B9CAFDD0 |
ESP+0x20+0xC |
2 |
压入的第二个参数 |
B9CAFDD4 |
ESP+0x20+0x10 |
1 |
压入的第一个参数 |
B9CAFDD8 |
ESP+0x20+0x14 |
0012FF20 |
执行后要恢复的堆栈寄存器:ESP |
B9CAFDDC |
ESP+0x20+0x18 |
23 |
执行后要恢复的段选择子:SS |
保存标志寄存器后
记录下此时的堆栈情况:
地址 |
相对栈顶地址 |
值 |
说明 |
B9CAFDA0 |
ESP |
0x202 |
标志寄存器 |
B9CAFDA4~B9CAFDC0 |
ESP+0x4~ESP+0x4+0x1C |
略 |
通用寄存器组 |
B9CAFDC4 |
ESP+0x4+0x20 |
0040D4E8 |
执行后要返回的地址 |
B9CAFDC8 |
ESP+0x4+0x20+0x4 |
1B |
执行后要恢复的段选择子:CS |
B9CAFDCC |
ESP+0x4+0x20+0x8 |
3 |
压入的第三个参数 |
B9CAFDD0 |
ESP+0x4+0x20+0xC |
2 |
压入的第二个参数 |
B9CAFDD4 |
ESP+0x4+0x20+0x10 |
1 |
压入的第一个参数 |
B9CAFDD8 |
ESP+0x4+0x20+0x14 |
0012FF20 |
执行后要恢复的堆栈寄存器:ESP |
B9CAFDDC |
ESP+0x4+0x20+0x18 |
23 |
执行后要恢复的段选择子:SS |
将门描述符写入GDT
确定门描述符
先确定段中偏移:进入debug模式,中断后,选中callGate函数,然后右键→Go to Disassembly(查看反汇编)
可以得到要调用的函数的地址为0x0040D480
将得到的要调用的函数地址填入门描述符中对应的offset得到:
- 原:0000EC00`00080000
- 现:0040EC00`0008D480
因为此次调用门描述符需要传递三个参数,故修改为:
0040EC03`0008D480
PS:修改了门描述符结构中的param.count,如不熟悉可回顾上面的 调用门描述符结构
于是得到确定的门描述符为0040EC03`0008D480
写入门描述符
确定完门描述符和要写入的地址后,就可以将其写入GDT了,操作如下:
指令如下:
r gdtr 查看gdtr
dq 8003f000 以qword查看地址,这里的地址为上面得到的gdtr地址
eq 8003f048 0040EC03`0008D480 写入门描述符
dq 8003f000 查看是否写入成功
过程图如下:
执行代码
执行代码结果如下:
代码执行后,能够正确地输出三个参数,构造有参调用门描述符成功
总结
- 当通过门,权限不变的时候,只会PUSH两个值:①CS(新的CS的值由调用门决定) ;②返回地址
- 当通过门,权限改变的时候,会PUSH四个值:①SS;② ESP;③ CS ;④ 返回地址 (新的CS的值由调用门决定 新的SS和ESP由TSS提供)
- 通过门调用时,要执行代码的地址由调用门中的选择子决定;使用RETF返回时,由堆栈中压入的返回地址决定
关于TSS的内容留作之后的笔记(* ̄3 ̄)╭
PS:写得比较匆忙,可能会有谬误之处,欢迎指出