Windows保护模式学习笔记(三)—— 长调用/短调用/调用门
前言
一、学习自
滴水编程达人
中级班课程,官网:https://bcdaren.com
二、海东老师牛逼! 三、本文转自https://blog.youkuaiyun.com/qq_41988448/article/details/102592866
要点回顾
通过JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用
CALL FAR 比 JMP FAR 要复杂,JMP并不影响堆栈,但CALL指令会影响
长调用与短调用
一、短调用
指令格式:
CALL 立即数 / 寄存器 / 内存
发生改变的寄存器:ESP EIP
二、长调用(跨段不提权)
指令格式:
CALL CS:EIP(EIP是废弃的)
发生改变的寄存器:ESP EIP CS
三、长调用(跨段并提权)
指令格式:
CALL CS:EIP(EIP是废弃的)
长调用执行时:
发生改变的寄存器:ESP EIP CS SS
注意:长调用执行后,堆栈已经不是原来的堆栈,而是0环的堆栈(ESP0)
执行返回(RETF)时:
发生改变的寄存器:ESP EIP CS SS
总结
-
跨段调用时,一旦有权限切换,就会切换堆栈
-
CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样
-
JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限
SS与ESP从哪里来?参见TSS段
调用门
指令格式:
CALL CS:EIP(EIP是废弃的)
执行步骤:
- 根据CS的值查GDT表,找到对应的段描述符 这个描述符是一个调用门
- 在调用门描述符中存储另一个代码段的段选择子
- 段选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址
门描述符
结构图:
注意:
- S位(第12位)必须为0,只有当S位为0时,段描述符才是系统段描述符;此时,当Type域为1100时,该描述符是门描述符
- 低四字节的16~31位是决定 调用的代码存在于哪个段 的 段选择子
- 当长调用执行时:
真正要执行的代码地址
=门描述符中段选择子所指向的代码段的Base
+门描述符高四字节的16~31位
+门描述符低四字节的0~15位
调用门(无参)
实验:构造一个无参调用门
第一步:初步构造参数
Offset in Segment 31:16 = 0x0000 //暂定
P = 1
DPL = 二进制:11
Param.Count = 二进制:00000
Segment Selector = 0x0008
Offset in Segment 15:00 = 0x0000 //暂定
由上述参数构造出的门描述符为:0000EC00`00080000
第二步:确定 Offset in Segment
在VC6中执行如下代码并中断
#include "stdafx.h"
#include <windows.h>
void __declspec(naked) GetRegister()
{
__asm
{
int 3
retf // 注意返回,不能是ret
}
}
int main(int argc, char* argv[])
{
char buff[6];
(DWORD)&buff[0] = 0x12345678; // 在这行设置断点
(WORD)&buff[4] = 0x48; // 段选择子所在偏移
__asm
{
call fword ptr[buff] // 长调用
}
getchar();
return 0;
}
右键进入反汇编窗口,查看GetRegister函数起始地址,我这里是00401010
至此,门描述符的最终确定为:0040EC00`00081010
第三步:将门描述符写入GDT表
第四步:继续执行第二步代码
运行后,虚拟机成功中断至WinDbg
第五步:再次运行程序,观察寄存器与堆栈的变化
长调用执行前:
长调用执行后:
寄存器:
堆栈:
结果和需求完全一致,调用门执行成功!
第三步:修改代码
将代码修改为如下:
#include <windows.h>
BYTE GDT[6] = {0};
DWORD dwH2GValue;
void __declspec(naked) GetRegister()
{
__asm
{
pushad
pushfd
mov eax,0x8003f00c // 读取高2G内存
mov ebx,[eax]
mov dwH2GValue,ebx
sgdt GDT; // 读取GDT
popfd
popad
retf
}
}
void PrintRegister()
{
DWORD GDT_ADDR = *(PDWORD)(&GDT[2]);
WORD GDT_LIMIT = *(PWORD)(&GDT[0]);
printf("%x %x %x \n", dwH2GValue, GDT_ADDR, GDT_LIMIT);
}
int main(int argc, char* argv[])
{
__asm
{
mov ebx,ebx
mov ebx,ebx
}
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48; // segment select
__asm
{
call fword ptr[buff]
}
PrintRegister();
getchar();
return 0;
}
注意:若GetRegister函数起始地址发生改变,则需要修调相应的门描述符
执行结果:
成功读取了GDT,提权成功!
修正
:sgdt命令实际上并不需要0环权限【大写尴尬】
调用门(有参)
实验:构造一个带参数的调用门
第一步:初步构造参数
Offset in Segment 31:16 = 0x0000 // 暂定
P = 1
DPL = 二进制:11
Param.Count = 二进制:00011 // 注意变化!
Segment Selector = 0x0008
Offset in Segment 15:00 = 0x0000 // 暂定
- 1
- 2
- 3
- 4
- 5
- 6
第二步:执行代码
代码如下:
#include <windows.h>
DWORD x;
DWORD y;
DWORD z;
void __declspec(naked) CateProc()
{
__asm
{
pushad
pushfd
mov eax,[esp+0x24+8+8]
mov dword ptr ds:[x],eax
mov eax,[esp+0x24+8+4]
mov dword ptr ds:[y],eax
mov eax,[esp+0x24+8+0]
mov dword ptr ds:[z],eax
popfd
popad
retf 0xC // 注意堆栈平衡 写错蓝屏
}
}
void PrintRegister()
{
printf("%x %x %x \n", x, y, z);
}
int main(int argc, char* argv[])
{
char buff[6];
(DWORD)&buff[0] = 0x12345678;
(WORD)&buff[4] = 0x48;
__asm
{
push 1 // 参数1
push 2 // 参数2
push 3 // 参数3
call fword ptr[buff]
}
PrintRegister();
getchar();
return 0;
}
注意:需要将门描述符的段偏移修改为CateProc函数的起始地址
执行结果:
传入的参数被成功输出,有参调用门构造成功!
思考:pushad、pushfd、popfd、popad 这几条指令有什么意义?是必须的吗?
总结
- 当通过门,权限不变的时候,只会PUSH两个值:CS 和 返回地址,新的CS的值由调用门决定
- 当通过门,权限改变的时候,会PUSH四个值:SS、ESP、CS、返回地址,新的CS的值由调用门决定,新的SS和ESP由TSS提供
- 通过门调用时,要执行哪行代码由调用门决定;但使用RETF返回时,由堆栈中压入的值决定;这就是说,进门时只能按指定路线走,出门时可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)
- 问:可不可以再建个门出去呢?也就是再用CALL出去
答:当然可以了,前"门"进,后"门"出