前言
说是笔记,实际上还是课后习题的内容,我不擅长写笔记,就不误导别人了
常见问题
这个写在前面是便于错误速查,若是有同样的问题可以直接在这里速查,免得去翻后面的内容
这里对于错误的定义是不符合视频内容的东西,比如说软件问题,命令报错等等
所用镜像和软件版本
- WDK7600
- WinXP SP3
- zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74070.iso
- SHA1:D142469D0C3953D8E4A6A490A58052EF52837F0F
- 601.04MB
- 2008-05-02
- 下载地址:ed2k://|file|zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74070.iso|630237184|EC51916C9D9B8B931195EE0D6EE9B40E|/
- 序列号: MRX3F-47B9T-2487J-KWKMF-RPWBY
VC6
-
编译前要清理
如果遇到以下错误,试试先清理项目再重新编译,构造调用门
Access violation - code c0000005 (!!! second chance !!!) nt!NtWaitForDebugEvent+0x122: 8063a3aa f6412c05 test byte ptr [ecx+2Ch],5
Windbg
- !process 0 0符号错误
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
NT symbols are incorrect, please fix symbols
解决方法:
.sympath SRV*C:\Symbols*https://msdl.microsoft.com/download/symbols ;表示把本地缓存放在 C:\Symbols,并从微软符号服务器下载
.symfix ;(可选)自动设置到 MS 符号服务器
!sym noisy ;开启符号加载的详细信息(便于排错)
.reload /f ;强制重载全部模块符号
.sympath ;查看当前符号路径
lmv m nt ;或 lmv m ntkrnl* 查看内核模块及其符号加载状态
!process 0 0 ;重试
调用门实验蓝屏,C05等错误
-
参考资料 进入0环蓝屏问题(幽灵熔断)
-
具体错误
我并没有遇到幽灵熔断导致的蓝屏问题,但我遇到了C05的错误,我的代码如下
#include <windows.h>
#include <stdio.h>
DWORD x = 0;
DWORD y = 0;
DWORD z = 0;
void __declspec(naked) Func()
{
__asm
{
int 3;
pushad;
pushfd;
mov eax,[esp+0x24+0x8+0x8];
mov dword ptr ds:[x],eax;
mov eax,[esp+0x24+0x8+0x4];
mov dword ptr ds:[y],eax;
mov eax,[esp+0x24+0x8];
mov dword ptr ds:[z],eax;
}
__asm
{
popfd;
popad;
retf 0xc; // push了3个参数,需要手动平衡堆栈
}
}
void PrintRegisetr()
{
printf("%x %x %x",x, y, z);
}
int main()
{
char buf[6];
*(DWORD*) &buf[0] = 0x12345678;
*(WORD*) &buf[4] = 0x48;
__asm
{
push 3;
push 2;
push 1;
call fword ptr [buf];
}
PrintRegisetr();
return 0;
}
在阅读上面的资料后,所有线索指向内核断点会修改FS


-
解决办法
- 不在内核产生断点
- 使用
push fs;pop fs(失败) 这个方法我没有成功,在内核产生断点依旧会修改FS的值为30,并且出内核后FS的值会恢复为0
段描述符与段选择子
笔记
- GDT(全局描述符表) LDT(局部描述符表)
当我们执行类似
MOV DS,AX指令时,CPU会查表,根据AX的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据.
也就是说,在执行MOV DS,AX时AX的值不能是随便的
可以用Windbg的
r gdtr(r是查看寄存器,gdtr是48位的,不是96位的)来查找GDT表的地址
r gdtl来查看GDT表的长度
dd 8003f000查看地址为8003f000的数据,以四字节分组显示(dq则是8字节分组显示)
dq 8003f000 L40显示40组数据,每组8字节,从8003f000开始

8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00000000`00000000
8003f050 80008954`af000068 80008954`af680068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
以8字节显示是因为GDT表的每个项占8字节,而GDT表的起始地址是0x00000000,接下来分析每个项的含义
- 段选择子
段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符

RPL:请求特权级别
TI:
TI=0 查GDT表 TI=1 查LDT表
Index:
处理器将索引值乘以8 在加上GDT或者LDT的 基地址,就是要加载的 段描述符
例如拆分1B
1B
0000 0000 0001 1011
0000000000011 0 11
TI=0,查GDT表
index=3,查GDT的第三项(00cffb00`0000ffff)
- 段描述符

-
加载段描述符至段寄存器
除了MOV指令,我们还可以使用LES、LSS、LDS、LFS、LGS指令修改寄存器. CS不能通过上述的指令进行修改,CS为代码段,CS的改变会导致EIP的改变,要改CS,必须要保证CS与EIP一起改,后面会讲.
char buffer[6];
__asm
{
les ecx,fword ptr ds:[buffer] //高2个字节给es,低四个字节给ecx
}
注意:RPL<=DPL(在数值上)
课后练习
1、记住段描述符与段选择子的结构
2、使用LES、LDS等指令修改段寄存器
思考题:
段描述符共有64位,但需要填充的是80位,怎么填?
段描述符属性
笔记
| 域简称 | 全称 | 含义 |
|---|---|---|
| S | 系统(System) | S=0代表该描述符是一个系统段,S=1代表该描述符是代码段,数据段或者堆栈段 |
| P | 存在(Present) | 指示该段是否存在于内存中。如果P=0,则当将指向段描述符的段选择器加载到段寄存器中时,处理器将生成段不存在异常。内存管理软件可以使用此标志来控制哪些段实际加载到物理内存中。它除了提供分页外,还提供了一个管理虚拟内存的控件。 |
| DPL | 描述符特权级(Descriptor Privilege Level) | 这两位定义了该段的特权级别(0~3),简单来说,仅当要访问该段的程序的特权级别(CPL)等于或高于这个段的级时CPU才允许其访问,否则便会抛出保护性异常(GPF) |
| D/B | Default/Big | 对于代码段,该位表示的是这个代码段默认的位数(Default Bit)。 D=0:16位代码段 D=1:32位代码段 对于栈段,该位称为B(Big)标志。B=1:使用32位堆栈指针(保存在ESP中) B=0:使用16位堆栈指针(保存在SP中) 对于数据段,该位称为B(Big)标志,指定了段的上界。B=1:段的上界是0xFFFFFFFF(4GB) B=0:段的上界是0xFFFF(64KB) |
| G | Granularity | 该位用于描述段界限的粒度。G=1:段界限是4K字节对齐的。G=0:段界限是字节对齐的。 |
| Type | 段类型 | 由S位来决定段类型,具体内容见下面的分析 |
| L | 64-bit代码段 | 用于描述IA-32e模式下的代码段。L=1:代码段包含64位代码 L=0:代码段包含兼容模型的代码 |
| AVL | Availible and reserved bits | 供系统软件(操作系统)使用 |
-
P位
P位在段描述符的高4字节第15位,用于描述段是否有效
P=1 段描述符有效
P=0 段描述符无效
当我们通过指令往段寄存器加载段描述符时,CPU首先检查P位,若P位为0,则不做后续检查
-
G位
G位在段描述符的高4字节第23位
-
段描述符与段寄存器的关系
段寄存器的结构如下
WORD Selector; //16位 WORD Atrribute; //16位 DWORD Base; //32位 DWORD Limit; //32位按照段描述符来拆分段寄存器
-
Atrribute
段描述符高4字节第8位至24位
-
Base
Base被拆分为多块,来自段描述符的
- (Base 31:24)高4字节24位到31位
- (Base 23:16)高4字节0位到7位
- (Base 15:0)低4字节16位到31位
-
Limit
Limit被拆分为多块,来自段描述符的
- (Limit 19:16)高4字节16位到19位
- (Limit 15:0)低4字节0位到15位
但是这样加起来只有20位(FFFFF),剩下的12位在哪?
若G=1,则Limit为4K字节对齐的段界限(FFFFFFFF),若G=0,则Limit为字节对齐的段界限(000FFFFF)
-
S位
S = 1 代码段或者数据段描述符
S = 0 系统段描述符
S位,DPL,P位构成一个字,其中在windows操作系统中DPL只有
00或11两种可能所以当我们想分析代码段或数据段TYPE域属性,S位一定是1,P位一定是1,则只需要注意段描述符高4字节的第5个字若为
0x9或0xF,则是代码段或数据段
若要再细分是代码段还是数据段,则需要分析TYPE域
-
-
TYPE域
- S位为1

TYPE域位于段描述符高4字节第6字,如上图所示,若大于等于0x8,则是代码段,否则数据段
-
数据段
A 访问位,表示该位最后一次被操作系统清零后,该段是否被访问过.每当处理器将该段选择符置入某个段寄存器时,就将该位置1.
W 是否可写
E 扩展方向(为1则向下拓展)
如下图,红色部分是有效的,图一是向上拓展,图二向下拓展

-
代码段
A 访问位
R 可读位
C 一致位
C = 1 一致代码段
C = 0 非一致代码段
- S位为0
当S=0时,该段描述符为系统描述符,系统描述符有分为以下类型

-
D/B位
位于段描述符的第22位
情况一:对CS段的影响
D = 1 采用32位寻址方式 D = 0 采用16位寻址方式 前缀67 改变寻址方式情况二:对SS段的影响
D = 1 隐式堆栈访问指令(如:PUSH POP CALL) 使用32位堆栈指针寄存器ESP D = 0 隐式堆栈访问指令(如:PUSH POP CALL) 使用16位堆栈指针寄存器SP情况三:向下拓展的数据段
D = 1 段上线为4GB D = 0 段上线为64KB
-
段权限检查
-
CPU分级

ring0权限最高,ring3权限最低
-
CPL(Current Privilege Level):当前特权级
CPL用来查看当前程序属于几环
CS和SS中存储的段选择子后2位就是CPL
例如: CS: 001B = 0000 0000 0001 1011 最后两位加起来为3,则当前程序处于3环(ring3)
-
DPL(Descriptor Privilege Level):描述符特权级别
DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么. 通俗的理解: 如果你想访问我,那么你应该具备什么特权
举例说明:
mov DS,AX如果AX指向的段
DPL = 0但当前程序的CPL = 3这行指令是不会成功的! -
RPL(Request Privilege Level):请求特权级别
RPL是针对段选择子而言的,每个段的选择子都有自己的RPL。

举例说明:
Mov ax,0008 与 Mov ax,000B //段选择子 Mov ds,ax Mov ds,ax //将段描述0008 = 1 0 00
000B = 1 0 11
指向的是同一个段描述符,但RPL是不一样的.
-
数据段的权限检查
参考如下代码:
比如当前程序处于0环,也就是说CPL=0
Mov ax,000B //1011 RPL = 3 Mov ds,ax //ax指向的段描述符的DPL = 0数据段的权限检查:
CPL <= DPL 并且 RPL <= DPL(数值上的比较)注意:
代码段和系统段描述符中的检查方式并不一样
-
总结:
CPL:CPU当前的权限级别
DPL:如果你想访问我,你应该具备什么样的权限
RPL:用什么权限去访问一个段
为啥要有RPL?
我们本可以用"读 写"的权限去打开一个文件,但为了避免出错,有些时候我们使用"只读"的权限去打开。
课后练习
-
分析段选择子为0x1B、0x23对应的段描述符,并将内容填写到段寄存器结构体中
- 0x1B
1B 0000 0000 0001 1011 0000000000011 0 11index=3,查GDT的第三项(00cffb00`0000ffff)
00cffb00`0000ffff 0000 0000 1100 1111 1111 1011 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 Selector = 0x1B Atrribute = 1100 1111 1111 1011 = 0xCFFB Base = 0000 0000 0000 0000 0000 0000 0000 0000 = 0x00000000 Limit = 1111 1111 1111 1111 = 0xFFFFFFFF(G=1)- 0x23
23 0000 0000 0010 0011 0000000000100 0 11index=4,查GDT的第五项(00cff300`0000ffff)
00cff300`0000ffff 0000 0000 1100 1111 1111 0011 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 Selector = 0x23 Atrribute = 1100 1111 1111 0011 = 0xCFF3 Base = 0000 0000 0000 0000 0000 0000 0000 0000 = 0x00000000 Limit = 1111 1111 1111 1111 = 0xFFFFFFFF(G=1)
代码间的跳转
笔记
-
段间跳转
JMP 0x20:0x004183D7 ;CPU如何执行这行代码?(1) 段选择子拆分
0x20 对应二进制形式 0000 0000 0010 0000 RPL = 00 TI = 0 Index = 4(2) 查表得到段描述符
TI = 0 所以查GDT表 Index = 4 找到对应的段描述符 四种情况可以跳转:代码段、调用门、TSS任务段、任务门(3) 权限检查
如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL 如果是一致代码段,要求:CPL >= DPL(4) 加载段描述符
通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中.(5) 代码执行
CPU将 CS.Base + Offset 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束.
课后练习
- 实现段间跳转
长调用与短调用
笔记
-
短调用
CALL 立即数/寄存器/内存发生改变的寄存器:ESP EIP

-
长调用(跨段不提权)
CALL CS:EIP(EIP是废弃的)发生改变的寄存器:ESP EIP CS

-
长调用(跨段提权)
CALL CS:EIP(EIP是废弃的)发生改变的寄存器:ESP EIP CS SS

-
总结
-
跨段调用时,一旦有权限切换,就会切换堆栈.
-
CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样.
-
JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门 提权,提升CPL的权限.
SS与ESP从哪里来?参见TSS段.
-
调用门
笔记
-
调用门执行流程
指令格式:CALL CS:EIP(EIP是废弃的,跳转地址由调用门描述符决定)
执行步骤:
-
根据CS的值 查GDT表,找到对应的段描述符 这个描述符是一个调用门.
-
在调用门描述符中存储另一个代码段段的选择子.
-
选择子指向的段的 段.Base + 偏移地址 就是真正要执行的地址.
-
-
门描述符

调用门描述符的P位,DPL都要为1,S位为0,TYPE域为1100
-
构造一个调用门
根据门描述符来构造一个调用门,基础结构如下
struct CallGateDescriptor { unsigned short offset_low; // 目标代码段偏移地址的低 16 位 unsigned short selector; // 目标代码段的段选择子 unsigned char param_count : 5; // 参数数量 unsigned char reserved : 3; // 保留位(通常为 0) unsigned char Type : 4;//属性 1110(对应 32 位调用门) unsigned char s : 1;//0系统段 1数据段存储段 unsigned char dpl : 2; // 描述符特权级别(DPL) unsigned char present : 1; // 存在位(P) unsigned short offset_high; // 目标代码段偏移地址的高 16 位 };其中两段Offset相加则为跳转目标地址,
-
代码测试
步骤一:代码测试,并观察堆栈与寄存器的变化.
记录执行前的寄存器值:
CS SS ESP
步骤二:在测试代码中加入特权指令并读取高2G内存.
课后练习
-
实现调用门(无参提权)

找到未被使用的段描述符,位置在第9个,修改为
0000EC0000080000`eq 8003f048 0000EC00`00080000此时跳转地址为0地址,下面的代码是一定会报错的
测试代码
#include <iostream> #include <Windows.h> void __declspec(naked) GetRegisetr() { __asm { int 3 retf } } int main() { char buf[6]; *(DWORD*) &buf[0] = 0x12345678; *(WORD*) &buf[4] = 0x48; __asm { call fword ptr[buf] } getchar(); return 0; }运行后报错

那如何通过调用门去调用代码所写的
GetRegisetr函数?那就需要获取GetRegisetr的地址了
在调试中获取了GetRegisetr的地址为00401030,那么就修改调用门为eq 8003f048 0040EC00`00081030系统卡死,可以在Windbg里看到系统被断下来了,因为我们的
GetRegisetr函数里写了int 3断点指令现在回去观察一下寄存器
在执行调用门前,寄存器(长调用(跨段提权)所push的寄存器)为CS: 0x1B SS: 0x23 ESP: 0012FF28继续运行代码,在Windbg里断下

CS: 0x8 SS: 0x10 ESP: b168cdd0可以看到ESP的地址远远大于8,已经进入0环了,再看看堆栈

00401068 0000001b 0012ff28 00000023再看看之前长调用(跨段提权)的堆栈图
不难发现对应关系为CS: 0x00000023 ESP: 0x0012ff28 SS: 0x0000001B 返回地址: 0x00401068既然成功提权,那么就可以做一下只有内核能做的事了,比如读取高2G内存
#include <iostream> #include <Windows.h> BYTE GDT[6] = {0}; DWORD dwH2GValue = 0; void __declspec(naked) GetRegisetr() { __asm { pushad pushfd mov eax,0x8003f00c // 读取高2G地址 mov ebx,[eax] mov dwH2GValue,ebx sgdt GDT popfd popad retf } } void PrintRegisetr() { DWORD GDT_ADDR = *(PDWORD)(&GDT[2]); WORD GDT_LIMIT = *(PWORD)(&GDT[0]); printf("%x %x %x",dwH2GValue, GDT_ADDR, GDT_LIMIT) } int main() { char buf[6]; *(DWORD*) &buf[0] = 0x12345678; *(WORD*) &buf[4] = 0x48; __asm { call fword ptr[buf] } PrintRegisetr(); getchar(); return 0; }再次查找
GetRegisetr的地址为00401040,设置调用门eq 8003f048 0040EC00`00081040执行代码

再对照
Windbg里0x8003f00c地址处的内容
可以看到我们成功在用户代码区获取了高2G内存的数据
-
实现调用门(有参提权)
和上面的做法差不多,只需要**修改调用门的ParamCount(传入参数个数)**即可,直接给代码
eq 8003f048 0040EC03`00081030#include <windows.h> #include <stdio.h> DWORD x = 0; DWORD y = 0; DWORD z = 0; void __declspec(naked) Func() { __asm { int 3; pushad; pushfd; mov eax,[esp+0x24+0x8+0x8]; mov dword ptr ds:[x],eax; mov eax,[esp+0x24+0x8+0x4]; mov dword ptr ds:[y],eax; mov eax,[esp+0x24+0x8]; mov dword ptr ds:[z],eax; } __asm { popfd; popad; retf 0xc; // push了3个参数,需要手动平衡堆栈 } } void PrintRegisetr() { printf("%x %x %x",x, y, z); } int main() { char buf[6]; *(DWORD*) &buf[0] = 0x12345678; *(WORD*) &buf[4] = 0x48; __asm { push 3; push 2; push 1; call fword ptr [buf]; } PrintRegisetr(); return 0; }
伪代码
push SS push ESP push 3 push 2 push 1 push CS push 返回地址
调用
printf报错C05我检查了寄存器执行前后的变化,如图


可以看到
FS从3B变为了00,这是不正确的,查找资料发现构造用门描述符前后堆栈变化中
FS从3B变成了30,是由于Func()中INT 3造成的,INT3会改变FS值在线上班中提出了一些问题,我若是在
call里修改了ebp,eax,ecx等寄存器,返回后会恢复原来的寄存器还是为我在0环修改的内容,经过实验,发现返回后是我在0环修改的内容,所以需要在call内部手动保存寄存器
小测
需求
1、构造一个调用门,实现3环读取高2G内存。 2、在第一题的基础上进行修改,实现通过翻墙的方式返回到其他地址。 3、在第一题的基础上进行修改,在门中再建一个门跳转到其他地址。 工 要求: 代码正常执行不蓝屏
实现
-
构造一个调用门,实现3环读取高2G内存。
调用门描述符
eq 8003f048 0040EC00`00081020- 代码
#include <windows.h> #include <stdio.h> DWORD dwH2GValue = 0; void __declspec(naked) Func1() { __asm { pushad; pushfd; mov eax, 0x8003f048 // 读取高2G地址 mov ebx, [eax] mov dwH2GValue, ebx } __asm { popfd; popad; retf; } } int main() { char buf[6]; *(DWORD*)&buf[0] = 0x12345678; *(WORD*)&buf[4] = 0x48; __asm { call fword ptr[buf]; } printf("%x", dwH2GValue); return 0; }
-
在第一题的基础上进行修改,实现通过翻墙的方式返回到其他地址
调用门描述符
eq 8003f048 0040EC00`00081030- 代码
#include <windows.h> #include <stdio.h> DWORD dwH2GValue = 0; void __declspec(naked) Func1() { __asm { pushad; pushfd; mov eax, 0x8003f048 // 读取高2G地址 mov ebx, [eax] mov dwH2GValue, ebx } __asm { popfd; popad; mov dword ptr[esp],0x401050 // 跳转到Func2的地址 retf; } } void __declspec(naked) Func2() { __asm { push 0x00401098 // 返回main函数的地址 mov dwH2GValue,0x12345678 } __asm { ret; } } int main() { char buf[6]; *(DWORD*)&buf[0] = 0x12345678; *(WORD*)&buf[4] = 0x48; __asm { call fword ptr[buf]; } printf("%x", dwH2GValue); return 0; }
-
在第一题的基础上进行修改,在门中再建一个门跳转到其他地址
找到两个能写调用门描述符的地方

一个是原先的0x8003f048
另一个是0x8003f090
调用门描述符
eq 0x8003f048 0040EC00`00081030
eq 0x8003f090 0040EC00`00081050
- 代码
#include <windows.h>
#include <stdio.h>
DWORD dwH2GValue = 0;
char buf1[6];
char buf2[6];
void __declspec(naked) Func1()
{
__asm
{
pushad;
pushfd;
mov eax, 0x8003f048 // 读取高2G地址
mov ebx, [eax]
mov dwH2GValue, ebx
__asm
{
call fword ptr[buf2];
}
}
__asm
{
popfd;
popad;
retf;
}
}
void __declspec(naked) Func2()
{
__asm
{
pushad;
pushfd;
mov dwH2GValue,0x12345678 // 重新赋值,验证是否被执行
}
__asm
{
popfd;
popad;
retf;
}
}
int main()
{
*(DWORD*)&buf1[0] = 0x12345678;
*(WORD*)&buf1[4] = 0x48;
*(DWORD*)&buf2[0] = 0x12345678;
*(WORD*)&buf2[4] = 0x90;
__asm
{
call fword ptr[buf1];
}
printf("%x", dwH2GValue);
return 0;
}

中断门
笔记


- 与调用门的区别
| 数据位 | 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 |

构造基础中断门描述符
| 数据位 | 31-16 | 15 | 14-13 | 12 | 11-8 | 7-5 | 4-0 |
|---|---|---|---|---|---|---|---|
| 含义 | offset | P | DPL | S | Type | 无 | param |
| 解释 | 段中偏移 | 有效位 | 特权等级 | 系统段描述符 | 32位中断门 | 值为000 | 不允许传参 |
| 值(二进制) | 0 | 1 | 11 | 0 | 1110 | 000 | 0000 |
| 数据位 | 31-16 | 15-0 |
|---|---|---|
| 含义 | selector | offset |
| 解释 | 段选择子 | 段中偏移 |
| 值(十六进制) | 0x0008 | 0 |
0000EE00`00080000
课后练习
- 实现中断门
找到未被使用的中断门描述符,使用公式
$$index = \frac{IDT地址 - IDT基址}{8}$$
找到目标地址为0x8003f500
计算索引
- 测试代码
#include <windows.h>
#include <stdio.h>
DWORD dwH2GValue = 0;
void __declspec(naked) Func1() // 0x401020
{
__asm
{
pushad
pushfd
mov eax,0x8003f500
mov eax,[eax]
mov dwH2GValue,eax
}
__asm
{
popfd
popad
iretd // iret蓝屏
}
}
int main()
{
int a;
a = 0;
__asm
{
int 0x20;
}
printf("%x", dwH2GValue);
getchar();
return 0;
}
写入IDT
eq 8003f500 0040EE00`00081020
执行代码成功访问高2G内存

查看在进入中断门前寄存器的值
在Func1中写入int3,触发断点,查看寄存器变化
| 寄存器 | 说明 | 执行前的值 | 执行后的值 | 是否变化 |
|---|---|---|---|---|
| ESP | 栈顶寄存器 | 12FF2C | b177cdcc | √ |
| EBP | 栈底寄存器 | 12FF80 | 12FF80 | × |
| CS | 代码段寄存器 | 1B | 08 | √ |
| DS | 数据段寄存器 | 23 | 23 | × |
| ES | 附加段寄存器 | 23 | 23 | × |
| SS | 堆栈段寄存器 | 23 | 10 | √ |
| FS | 附加段寄存器 | 3B | 30 | √ |
| GS | 附加段寄存器 | 0 | 0 | × |
| EFL | 标志寄存器 | 212 | 12 | √ |
查看堆栈

可以看到相比于调用门,中断门多传入了一个EFL寄存器,并且在进入中断门后EFL寄存器的IF位归0
- 在调用门中使用iret返回,在中断门中使用retf返回
原理都差不多,这里只说说中断门的retf返回
只需要把堆栈变得和调用门一样,然后再retf即可
#include <windows.h>
#include <stdio.h>
DWORD dwH2GValue = 0;
DWORD dwEFLValue = 0;
void __declspec(naked) Func1() // 0x401020
{
__asm
{
pushad
pushfd
mov eax,0x8003f500
mov eax,[eax]
mov dwH2GValue,eax
mov eax,[esp + 0x24] //ret
mov edx,[esp + 0x24 + 0x4] //CS
mov ecx,[esp + 0x24 + 0x8] //EFL
mov [esp + 0x24 + 0x8], edx
mov [esp + 0x24 + 0x4], eax
mov [esp + 0x24], ecx
}
__asm
{
popfd
popad
popfd // 这一步将EFL恢复原来的值
retf
}
}
int main()
{
int a;
a = 0;
__asm
{
int 0x20;
}
printf("%x", dwH2GValue);
getchar();
return 0;
}
可以看到结果正常输出,并且EFL的值与之前一样
陷阱门
笔记
与中断门几乎相同,不同在于中断门会将IF位清0,陷阱门不会
- IF标志位为1表示当前CPU允许响应INTR可屏蔽中断请求
- IF标志位为0则表示CPU不会响应可屏蔽中断请求
任务段
笔记
在调用门、中断门与陷阱门中,一旦出现权限切换,那么就会有堆栈的切换。而且,由于CS的CPL发生改变,也导致了SS也必须要切换。
切换时,会有新的ESP和SS(CS是由中断门或者调用门指定)这2个值从哪里来的呢?
答案:TSS (Task-state segment ),任务状态段.

TSS是一块104字节的内存
一个TSS对应一个LDT,通过TSS的LDT段选择子定位
TSS的切换可以类比线程切换,但是Windows系统和Linux系统均不使用TSS段进行线程切换,这部分等到操作系统章节再看
不要把TSS与“任务切换”联系到一起
TSS的意义就在于可以同时换掉”一堆”寄存器
如何找到TSS段的地址?如下图所示

首先找TR段寄存器,通过TR段寄存器的段选择子去找GDT表中的TSS段描述符,再填充TR段寄存器的Attributr,Base,Limit字段
其中Base指向TSS的起始地址,Limit为TSS的长度

这里主要讲几个标志位
| 标志位 | 含义 |
|---|---|
| B(Busy) | 1表示忙碌,0表示空闲,为了确保只有一个Busy Flag与任务相关联,每个TSS应该只有一个指向它的TSS描述符 |
| G(Granularity) | 1表示段界限粒度为4KB,0表示段界限粒度为1B |
-
TR寄存器的读写
-
将TSS段描述符加载到TR寄存器
指令:LTR
说明:
-
用LTR指令去装载的话,仅仅是改变TR寄存器的值(96位) ,并没有真正改变TSS -
LTR指令只能在系统层使用 -
加载后TSS段描述符会状态位会发生改变
MOV AX,SelectorTSS LTR AX -
-
读TR寄存器
指令:
STR说明:如果用STR去读的话,只读了TR的16位,也就是选择子
STR AX
-
-
修改TR寄存器
-
在Ring0 我们可以通过
LTR指令去修改TR寄存器 -
在Ring3 我们可以通过
CALL FAR或者JMP FAR指令来修改
用JMP去访问一个代码段的时候,改变的是CS和EIP :
-
JMP 0x48:0x123456 如果0x48是代码段
-
执行后:CS–>0x48 EIP–>0x123456
用JMP去访问一个任务段的时候:
- 如果0x48是TSS段描述符,先修改TR寄存器,
- 再用TR.Base指向的TSS中的值修改当前的寄存器
-
-
构造TSS段描述符
XX00E9XX XXXX0068其中XXXXXXXX表示Base,就是我们提供的104字节的TSS段的起始地址
-
课堂代码
#include <windows.h>
#include <stdio.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) func() // 0x00401020
{
dwOK = 1;
__asm {
mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr[dwCS], ax
// 回去的代码没写
iretd
}
}
int main(int argc, char* argv[])
{
char bu[0x10]; // 0x12ff70
int iCr3;
printf("input CR3:\n");
scanf("%x", &iCr3); // 通过 windbg 工具 !process 0 0 指令获取
DWORD tTss[0x68] = {
0x00000000, // link
(DWORD)bu, // esp0
0x00000000, // ss0
0x00000000, // esp1
0x00000000, // ss1
0x00000000, // esp2
0x00000000, // ss2
(DWORD)iCr3, // cr3
0x00000000, // eip
0x00000000, // eflags
0x00000000, // eax
0x00000000, // ecx
0x00000000, // edx
0x00000000, // ebx
0x00000000, // esp
0x00000000, // ebp
0x00000000, // esi
0x00000000, // edi
0x00000008, // es
0x0000001B, // cs
0x00000010, // ss
0x00000023, // ds
0x00000030, // fs
0x0000003B, // gs
0x20ac0000, // ldt?
};
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0xC0;
__asm {
call fword ptr[buff]
}
printf("ok = %d ESP = %x CS = %x \n", dwOK, dwESP, dwCS);
return 0;
}
- 完善代码
#include <windows.h>
#include <stdio.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) func() // 0x00401020
{
dwOK = 1;
__asm {
int 3 // int 3 会修改FS
mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr[dwCS], ax
iretd
}
}
int main(int argc, char* argv[])
{
DWORD dwCr3;
char esp[0x1000];
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("TSS = %x\n", TSS);
printf("input CR3:\n");
scanf("%x", &dwCr3); // 通过 windbg 工具 !process 0 0 指令获取
TSS[0] = 0x00000000; // link
TSS[1] = 0x00000000; // esp0
TSS[2] = 0x00000000; // ss0
TSS[3] = 0x00000000; // esp1
TSS[4] = 0x00000000; // ss1
TSS[5] = 0x00000000; // esp2
TSS[6] = 0x00000000; // ss2
TSS[7] = dwCr3; // cr3
TSS[8] = (DWORD)func; // eip
TSS[9] = 0x00000000; // eflags
TSS[10] = 0x00000000; // eax
TSS[11] = 0x00000000; // ecx
TSS[12] = 0x00000000; // edx
TSS[13] = 0x00000000; // ebx
TSS[14] = (DWORD)esp+0x900; // esp
TSS[15] = 0x00000000; // ebp
TSS[16] = 0x00000000; // esi
TSS[17] = 0x00000000; // edi
TSS[18] = 0x00000023; // es
TSS[19] = 0x00000008; // cs
TSS[20] = 0x00000010; // ss
TSS[21] = 0x00000023; // ds
TSS[22] = 0x00000030; // fs
TSS[23] = 0x00000000; // gs
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x00000000; // I/O Map Base Address
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm {
call fword ptr[buff]
}
printf("ok = %d ESP = %x CS = %x \n", dwOK, dwESP, dwCS);
return 0;
}

在我的代码中,我通过VirtualAlloc给我的TSS分配了页面内存,关于页的概念后续会学到,这里我直接用了是因为课上已经说明了若是不在同一页会蓝屏,并且提到了VirtualAlloc可以解决这个问题
这里还有几个小问题,课堂的代码的TSS是不完整的,最后的LDT部分被写上了0x20ac0000,但是I/O Map Base Address没有写,我把这些全置0了,结果没有蓝屏,但是我不知道这是否合理
若要让系统执行我的func,我需要修改eip为我的函数的地址,否则任务切换的时候不会执行我的函数,所以我在TSS中把eip指向了func的地址
最后困扰我的蓝屏问题是esp的设置,虽然发生了权限切换,但是任务切换与先前的调用门,中断门不同,他并不会走esp0的堆栈,而是从我当前的堆栈开始,若是填0,则会蓝屏,所以我给esp分配了堆栈上的空间char esp[0x1000];,并且写入TSS[14] = (DWORD)esp+0x900;,加上0x900偏移是因为堆栈从高地址向低地址增长,不写在栈顶是因为需要留出一个页避免越界
任务门
构造任务门

构造任务门:0000 e500 00XX 0000
>eq 8003f500 0000e500`00XX0000
其中XX为TSS段选择子
flowchart TD
A[INT N 中断指令] --> B[查询IDT表]
B --> C[找到任务门描述符]
C --> D[通过任务门描述符查询GDT表]
D --> E[找到TSS段描述符]
E --> F[使用TSS段中的值修改TR寄存器]
F --> G[IRETD返回]
实现一个任务门
#include <windows.h>
#include <stdio.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) func() // 0x00401020
{
dwOK = 1;
__asm {
mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr[dwCS], ax
iretd
}
}
int main(int argc, char* argv[])
{
DWORD dwCr3;
char esp[0x1000];
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("TSS = %x\n", TSS);
printf("input CR3:\n");
scanf("%x", &dwCr3); // 通过 windbg 工具 !process 0 0 指令获取
TSS[0] = 0x00000000; // link
TSS[1] = 0x00000000; // esp0
TSS[2] = 0x00000000; // ss0
TSS[3] = 0x00000000; // esp1
TSS[4] = 0x00000000; // ss1
TSS[5] = 0x00000000; // esp2
TSS[6] = 0x00000000; // ss2
TSS[7] = dwCr3; // cr3
TSS[8] = (DWORD)func; // eip
TSS[9] = 0x00000000; // eflags
TSS[10] = 0x00000000; // eax
TSS[11] = 0x00000000; // ecx
TSS[12] = 0x00000000; // edx
TSS[13] = 0x00000000; // ebx
TSS[14] = (DWORD)esp+0x900; // esp
TSS[15] = 0x00000000; // ebp
TSS[16] = 0x00000000; // esi
TSS[17] = 0x00000000; // edi
TSS[18] = 0x00000023; // es
TSS[19] = 0x00000008; // cs
TSS[20] = 0x00000010; // ss
TSS[21] = 0x00000023; // ds
TSS[22] = 0x00000030; // fs
TSS[23] = 0x00000000; // gs
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x00000000; // I/O Map Base Address
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm {
int 0x20;
}
printf("ok = %d ESP = %x CS = %x \n", dwOK, dwESP, dwCS);
return 0;
}
eq 8003f048 0000e93a`00000068
eq 8003f500 0000e500`00480000

任务门进一环
Windows系统并没有使用1环,需要手动往gdt表中写入描述符
// TSS段描述符与任务门
eq 8003f048 0000e93a`00000068
eq 8003f500 0000e500`00480000
// 1环代码段描述符
eq 8003f090 00cfbb00`0000ffff
// 1环数据段描述符
eq 8003f098 00cfb300`0000ffff
然后需要修改cs与ss与其他的段寄存器,其中cs与ss指向上面构造的1环代码段与数据段
fs,es,ds指向DPL≥1的段,这里我直接使用了用户段0x23
#include <windows.h>
#include <stdio.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) func() // 0x00401020
{
dwOK = 1;
__asm {
// int 3
mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr[dwCS], ax
iretd
}
}
int main(int argc, char* argv[])
{
DWORD dwCr3;
char esp[0x1000];
DWORD *TSS = (DWORD*)VirtualAlloc(NULL,104,MEM_COMMIT,PAGE_READWRITE);
if (TSS == NULL)
{
printf("VirtualAlloc 失败,%d\n", GetLastError());
getchar();
return -1;
}
printf("TSS = %x\n", TSS);
printf("input CR3:\n");
scanf("%x", &dwCr3); // 通过 windbg 工具 !process 0 0 指令获取
TSS[0] = 0x00000000; // link
TSS[1] = 0x00000000; // esp0
TSS[2] = 0x00000000; // ss0
TSS[3] = 0x00000000; // esp1
TSS[4] = 0x00000000; // ss1
TSS[5] = 0x00000000; // esp2
TSS[6] = 0x00000000; // ss2
TSS[7] = dwCr3; // cr3
TSS[8] = (DWORD)func; // eip
TSS[9] = 0x00000000; // eflags
TSS[10] = 0x00000000; // eax
TSS[11] = 0x00000000; // ecx
TSS[12] = 0x00000000; // edx
TSS[13] = 0x00000000; // ebx
TSS[14] = (DWORD)esp+0x900; // esp
TSS[15] = 0x00000000; // ebp
TSS[16] = 0x00000000; // esi
TSS[17] = 0x00000000; // edi
TSS[18] = 0x00000023; // es
TSS[19] = 0x00000091; // cs
TSS[20] = 0x00000099; // ss
TSS[21] = 0x00000023; // ds
TSS[22] = 0x00000023; // fs
TSS[23] = 0x00000023; // gs
TSS[24] = 0x00000000; // LDT Segment Selector
TSS[25] = 0x00000000; // I/O Map Base Address
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm {
int 0x20;
}
printf("ok = %d ESP = %x CS = %x \n", dwOK, dwESP, dwCS);
return 0;
}

成功进入1环
10-10-12分页
引入
首先说下,有效地址 ,线性地址,物理地址:
MOV eax,dword ptr ds:[0x12345678]
- 其中,0x12345678 是有效地址
ds.Base + 0x12345678是线性地址- 物理地址就不用说了,要找的就是这个
x86中分页的方式有两种:
10-10-122-9-9-12
在boot.ini里,给启动参数加上/execute=optin,重启后就是10-10-12分页
查找记事本文本内容的物理地址
打开一个记事本,随便写入一些内容,打开Cheat Engine,附加记事本后搜索String类型的内容(勾选Unicode),如图所示

搜索结果可能有多个,需要改变字符串,移动窗口等操作去确定目标字符串,找到后记录下来,在我这是0x000B1250

按照10-10-12bit拆分线性地址
000B1250
0000 0000 00 // 0 * 4(每个成员4字节)
00 1011 0001 // 0xB1 * 4(每个成员4字节)
0x250 // 0x250
有了线性地址,就能通过进程的CR3找到页目录表的物理地址,再通过页目录表找到页表的物理地址,最后找到目标字符串的物理地址
每个进程都有一个CR3(这里存的是物理地址),准确的说是都一个CR3的值,CR3本身是个寄存器,一个核,只有一套寄存器
可以理解为物理地址就是一本书,而每个进程的CR3就是该进程的目录
CR3指向一个物理页,一共4096字节

之后找到CR3,也就是DirBase:0x1c803000
然后逐级查找,查物理内存的命令是!dd

- 第一级
得到内容1c631867,其中最后三个字直接忽略,这是属性,所以第二级从1c631000开始找
- 第二级

- 第三级

成功找到字符串
PDE/PTE
在上一节中所提到的CR3寻找物理地址中,我们通过线性地址的拆分去寻找物理页,在拆分寻找的过程中所提到的第一级,第二级,第三级,都是页目录表,页表,页内偏移地址,都是通过PDE/PTE来实现的

PTE可以不指向物理页,多个PTE也可以指向同一个物理页
往0地址读写数据
正常编程中,不能读写NULL,原因是NULL指针没有对应的物理页,因此,只要我们让NULL指针最终映射到一块可读写的物理页,就可以用NULL去读写数据了
-
示例代码:
#include <stdio.h> int main() { int x = 1; printf("x的地址%x", &x); getchar(); *(int*)0 = 0x123; printf("0的值为%x", *(int*)0); return 0; }读取到x的地址为
12ff7c -
拆分线性地址
0012ff7c 0000 0000 00 // 0 01 0010 1111 // 12f*4=4BC 0xF7C -
修改PTE 修改0地址的PTE,使其指向一块可读可写的物理页,这里我指向了x的地址

可以看到0地址被成功写入,并且正常返回了

证明了0地址之所以不能读写,是因为它没有对应的物理页,而我们通过修改PTE指向一块可读写的物理页,使得0地址有了对应的物理页,就可以读写了
PDE/PTE属性(P_RW)
物理页的属性 = PDE属性 & PTE属性

-
R/W位
R/W = 0 只读 R/W = 1 可读可写
-
P位
P=1 才是有效的物理页
-
代码修改常量
#include<stdio.h>
int main()
{
char *str = "Hello World";
int addr = (int)str;
printf("线性地址:%x\n",addr);
getchar(); // 修改 PDE PTE 的 RW 位为1,使物理页可读可写
str[0] = 'M';
printf("修改后:%s\n", str);
return 0;
}
得到线性地址:0x42303C
0x0042303C
0000 0000 01 // 1 * 4
000 0010 0011 // 23 * 4
03C

修改PTE的RW=1即可:
