Windows保护模式

Windows保护模式

双机调试环境

系统下载地址:https://msdn.itellyou.cn/

下载操作系统安装[Win7 X86版本 sp1]

添加启动引导:
cmd管理员身份运行
bcdedit /copy {current} /d 51hookDebug//引导名称
bcdedit /displayorder {3e4ebb7c-ee22-11eb-a448-89140888b722} /addlast //设置引导显示顺序
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200  //设置调试端口波特率
bcdedit /debug {3e4ebb7c-ee22-11eb-a448-89140888b722} ON//对新加的启动项增加调试功能
bcdedit /timeout 20//选择等待时长

虚拟机设置:
添加串行端口
//./pipe/com_1
Windbg配置:

-k com:port=//./pipe/com_1,baud=115200,pipe
D:\tools\Debuggers\x86\windbg.exe -k com:port=//./pipe/com_1,baud=115200,pipe

配置符号文件路径
srv*D:\symbol*;srv*D:\symbol*http://msdl.microsoft.com/download/symbols

段寄存器

段寄存器:ES CS SS DS FS GS...他们其实是96位寄存器
不过我们能看到的只有16位。后面我们再看另外80位在哪
这16位也叫做段选择子或者段选择符。

mov ecx,dword ptr ss:[0x42910C]
解读:ss.base + 0x42910C = 真正要去的内存地址。

就像8086CPU里面,是通过段寄存器 * 0x10 + 偏移地址 = 真正的地址

所以我们前面一直直接全部当做取[]内存地址的数据是不对的。

Base在OD里面是显示在段寄存器的位数后面的。
然后我们发现ES CS SS DS GS的BASE都一样,所以我们直接当做内存地址看没发生问题。
但是当我们使用fs,一样的内存地址就出现问题了,因为FS的BASE不一样。

mov ecx,dword ptr fs:[0x42910C]
== 
mov ecx,dword ptr [C26000 + 0x42910C]
所以才会出现问题。

这也是为什么我们前面取TEB,fs:[0]没有问题,不然按照直接的理解,取0地址的东西我们肯定是有问题的。
而实际上是取 fs.base + 0地址的内容,所以才不会报错。

然后我们往cs指向的内存单元写入东西也是有问题的。
因为cs和ip指向的为当前的程序需要执行的指令。
我们是没有权限往cs里面写入东西的。
代码段有读,执行的权限,没有写的权限。

数据段有读写权限。

#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

using namespace std;

int g_int;

int main(){
	__asm {
		mov dx, cs
		mov ds, dx
		mov eax, 1
		mov dword ptr ds:[g_int] , eax
	};
	system("pause");
	return 0;
}

发现报错了,所以真正控制权限的不是寄存器的名字,而是选择子选定的权限。

总结:
  段寄存器的作用1:寻址  寻址公式  段寄存器.base + [offset]
  段寄存器的作用2:控制内存操作权限
  
我们在前面实验,把cs的值给了ds,然后再去操作全局变量发现报错了。
那么其实就不管段寄存器的名字,在我们想的当中就是段寄存器的名字对应不同的权限
其实是段选择子控制的寄存器的权限。即002B 0023 002B 002B 0053 002B这些

段寄描述符

OD中段寄存器的显示:
  段选择子/段选择符 位数 Base(Limit)
  
Limit:段的限长:寻址长度的限制
  -那么寻址就是 Base + Limit 最大
  
mov eax,dword ptr fs:[0x7FFF]
  
这里的fs的Base是7FFDF000,然后Limit是FFF
那么就是说最大寻址就是FFF,我们肯定这里就会存在问题了。
因为我们这里是偏移是7FFF ,已经超过了FFF。
  
不过我们这样写也有问题:
mov eax,dword ptr fs:[0xFFF]
那么我们分析这条指令:从fs.Base + 0xfff取一个dword出来,那肯定就有问题了。
最多我们就只能取出来一个字节,另外三个字节已经超过我们的寻址范围了。
所以要取dword,不能取到FFF。
  
mov al,byte ptr fs:[0xFFF]
这样就没有问题了,因为0xFFF这里的这一个字节我们是可以寻址到的,没有超出我们的最大寻址范围。
  
其实我们真正能看的只有段选择子那16位,后面的Base和Limit其实都是OD帮我们翻译出来的。

那也就是说我们通过一个段选择子就可以拿出来那么多信息,那么就是说这个段选择子不可能包含那么多信息的。就16位。
  
那么段寄存器这么多属性都存放到哪了?
  -段寄存器的哪一堆属性存放到了一张表里。
这张表我们可以想象成一个数组,里面存放的数组是QWORD类型的数据。

那么我们要怎么找到这个数组呢?
给我们提供的线索只有段选择子/段选择符。
那么我们肯定要通过段选择子去获取信息。

段选择子被拆成三个部分【按照二进制,按位看】:
  -第一个部分被拆成两位RPL【请求特权级别】
  -第二个部分是一位TI,这个标志决定了我们要去那个表查询数据
    -0:GDT global decriptor table
    -1:LDT local decriptor table
  -第三个部分是INDEX,数组的索引值
  
GDT表可以认为是一个数组,这个数组的元素是一个QWORD大小的数据。
INDEX:数据在表的索引值

比如我们看ES段寄存器:0023

0010 0011
那么权限就是11
然后是存放在0号表,即GDT表中
索引INDEX是00100,即第5项,下标为4的位置

也就是说我们去GDT表中查索引值为4的数据。

那么GDT表的地址在哪里呢?
有一个寄存器叫做GDTR,这个寄存器专门用来存放GDT表的地址的。
但是这个GDTR并不是一个真实的寄存器,而是一个伪寄存器。

通过windbg的r命令可以查看寄存器。
  -r gdtr
然后我们就可以看到gdtr的地址了80b95000,然后记住每一个元素是一个QWORD,也就是64位,也就是8个16进制,16个16进制位

windbg查看内存地址命令跟OD一样。
  -dq 80b95000
  
那么我们就找到索引为4的这个元素。00cff300`0000ffff
这个东西也叫做段描述符。我们要取出来属性,就要解析这个断描述符

段描述符解析

00cff300`0000ffff

低32位0000ffff
  -0~15位是我们的Limit FFFF
  -16~31位是Base 0000

高32位00cff300
  -16~19位是我们的Limit,F 那么现在Limit就是 FFFFF
  -0~7位是我们的Base,00 那么现在Base就是 000000
  -24~31位也是我们的Base,00 那么现在Base就是 00000000
  -8~11位是Type,3
  -12~15位是f,1111 那么s是1,DPL是11,也就是3,P是1
  -20~23位是c,1100 那么AVL = 0,D/B = 1,G = 1

那么Base完整了,Limit还少了三个FFF。其实Limit还要结合G看。

G表示Limit的单位,用专业术语的话叫做粒度。
  -当G=0的时候,Limit单位为字节
  -当G=1的时候,Limit的单位为页,0x1000,4096。
    =(Limit + 1) * 0x1000 = 1 0000 0000 - 1 = FFFFFFFF

Limit为什么要+1,因为还要算上0
  -比如0~9盒子里面每个盒子放10个苹果,实际上10 * 10 = 100
Limit为什么要-1,因为要转为限长
  -比如100个苹果取编号,其实就是0~99

Type要结合S一起看。
  -当S为0的时候,表示该段是系统段
  -当S为1的时候,表示该段是数据段或者代码段
再去查Type【等于1查下面那张表,0有另外一张】
  -当S=1的时候,Type=0~7是数据段,大于7是代码段
  -当S=1的时候,Type = 3,表示当前是一个数据段,并且可读可写,被访问过

P位标识段是否有效,当P=0的时候无效,为1的时候有效

AVL提供给用户使用到,具体意义由我们赋予

D/B位的作用还得看当前是数据段还是代码段。
  -如果D/B位是1,那么堆栈的偏移是4
  -如果D/B位是0,那么堆栈的偏移是2
  
  对DS,ES等数据段:
  D=1,段上限为4GB
  D=0,段上限为64KB

  对SS段:
  D=1,使用32位堆栈指针寄存器;
  D=0,使用16位堆栈指针寄存器.

  对CS段:
  D=1,采用32位寻址方式;
  D=0,采用16位寻址方式。
  
弄个代码段出来先,CS=1B 11011。权限11,0号标GDT,11即3号索引
00cffb00`0000ffff
低32位0000ffff
  -0~15是Limit ffff
  -16~31是Base 0000
  
高32位00cffb00
  -0~7是Base 00
  -24~31也是Base 00 那么现在Base就是00000000
  -11~15是f,1111 那么s是1,DPL是11,也就是3,P是1
  -7~11是Type,是B,S = 1,查表,大于7那么是代码段,读,执行,访问
  -16~19,是Limit,f,那么现在Limit是FFFFF
  -20~23是c,是1100,那么AVL=0,D/B=1,G=1

当G=1的时候,单位是页,0x1000,Limit = (Limit + 1) * 0x1000 = 1 0000 0000
然后Limit = 1 0000 0000 - 1 = FFFFFFFF

权限级别

先说说当S=1的时候,即Type表示数据段或者代码段的时候。
如果表示数据段的话,有三个属性:
  -E表示扩展方向 E = 0向上扩展,E = 1向下扩展
    -比如Base:0,Limit:0xFFFFFFFF
    -如果E = 0向上扩展,那么就是0~0xFFFFFFF是可以使用部分
    -如果E = 1向下扩展,反之,0~0xFFFFFFFF不可使用
  -W表示是否可写,W = 1表示可写,W = 0表示不可写
  -A表示是否访问过,A = 1表示访问过,A = 0表示没有访问过

找ds段做个实验:ds = 0023
即 00100 0 11
那么就是索引为4的

00cff300`0000ffff
低32位0000ffff
  -Limit = FFFF
  -Base = 0000
高32位00cff300
  -Base0~7 = 00,Base24~31 = 00,Base = 0000 0000
  -Type8~11 = 3
  -12~15 = f,1111,S = 1,DPL = 3,P = 1
  -16~19 Limit = F,即Limit = FFFFF
  -20~23 C 1100,AVL = 0,D/B = 1,G = 1
  
因为G = 1,Limit单位是0x1000,那么(Limit + 1)*0x1000 - 1 = 0xFFFFFFFF
然后P = 1,当前段有效
D/B = 1,表示当前是32根线的方式
S = 1,所以表示当前不是系统段,所以Type=3就是一个数据段。可写访问过。E=0向上扩展。

但是发现是向上扩展的,我们想实验的向下扩展,那么我们就需要把它改为6,也就是把Type改成6,就是8~11位变为6

00cff600`0000ffff
那么我们就要找一个空白的地方填入,那么我们就要放在索引为9的地方。
  -eq 80b95048 00cff6000000ffff 写入内存

00100 011
权限和表不动,把前面变为9,也就是1001
1001 011
也就是要把ds的段选择子改为4B。
  -mov ax,0x4B
  -mov ds,ax
像段寄存器的赋值,我们不能直接给一个常量,所以我们需要中转一下。

然后测试代码:
mov dword ptr ds:[0xDA1000],eax
报错。

为什么呢,因为我们现在是向下扩展了,从Base + Limit这块其实是不能用的。
也就是0~FFFFFFFF是不能用的

DPL【13~14位】:段权限级别
段选择子RPL:请求权限级别
CPL:当前权限级别

比如DS:004B
01001 0 11
那么RPL权限就是11,也就是3

00cff200~0000ffff
  -12~15位 = f,1111,那么S = 1,DPL = 11 = 3,P = 1
那么DPL权限也是3

CPL保存在CS或者SS中,他们两个保存的CPL的值是一样大。
那么这里取SS,即0023,00100 0 11
那么CPL权限也是11,也就是3

X86定义了四种特权级别:【数字越小特权越大,Window只用了0和3】
  -0,0环,内核级别
  -1
  -2
  -3,3环,用户级别
 
段是什么类型的段
  -普通数据段ds,堆栈数据段ss,还是代码段
  
代码段cs和堆栈数据段的时候,要求三种权限都是一样的。
  -要么全是0要么全是3

普通数据段ds,es...
  -段选择子/段选择符前2位,RPL 当前我以什么身份去办事情
  -段描述符,DPL,去办什么权限级别的事情
  -CS/SS当前权限级别,CPL,我真实的身份级别是什么
  
CPL:我是一个学生
RPL:我以校长的身份去
DPL:打扫卫生
那么是成立的,虽然我真实的身份是一个学生,以校长的身份去打扫卫生,打扫卫生我才懒得理你是什么身份

CPL:我是一个学生
RPL:我以校长的身份去
DPL:去校长办公室修改校规
那么肯定是不成立的,因为你真实的身份就是一个学生,你没有这个权限

CPL:我是一个校长
RPL:我以学生的身份去
DPL:去校长办公室修改校规
那么肯定是不成立的,虽然你真实的身份是一个校长,但是你现在说你是一个学生,那你是学生,我肯定不让你改校规啊

提权实验

RPL(段选择子前2位):请求权限级别,我们以什么身份去干某事
DPL(段描述符13~14位):干这件事情需要什么身份
CPL(CS,DS的RPL):我们的真实身份是

然后首先我们要先看当前是什么段,如果是CS代码段,SS堆栈数据段,那么三个都是一致的。

普通数据段我们才要区分这三个。
RPL就去段选择子的低2位找,DPL就去段描述符的13~14位找,CPL就去CS或者SS的RPL找。

如果我们要修改权限,我们要修改什么?
那么我们要去做一件事情,肯定要把自己的真实身份提高,那么CPL肯定要提高的。
RPL也要提高,我们干这件事情的时候以什么身份去干。
而DPL是当前我们要去做什么事情的身份,做什么事情就没有必要提高了,比如前面说的扫地,修改校规。

主要还是看CPL当前的身份,和RPL以什么身份去干。
有一个不满足要求我们都干不了。
比如前面真实身份CPL是校长,但是以学生身份RPL去修改校规,也不行
或者真实身份CPL是学生,但是以校长身份RPL去修改校规也不行。

所以我们就是要修改CPL和RPL。也就是修改CS和SS的权限,和RPL即段选择子前2位的权限。

我们以ds段做实验。0023
即00100 0 11
那么就是段描述符,在GDT表中索引为4的位置。
然后RPL = 3即当前处于用户态权限。
那么我们就是要把11改为00.
0010 0000
那么就是20。

我们要把ds的值修改为20,那么ds的RPL权限就提高为内核权限了。
000A2700      66:B8 2000    mov ax,0x20
000A2704      8ed8          mov ds,eax

那么以什么身份去做,我们就搞定了。

我们尝试去访问0环才能范围的地址。80b95040
000A2706      A1 4050B980   mov eax,dword ptr ds:[0x80B95040]
然后其实我们干不成这件事情的。
因为虽然我们是以0环的身份去干
但是我们当前真实的身份还是3环用户态。

所以还要修改真实的身份,也就是CS和SS。
CS = 1B,SS = 23

SS 00100 0 11 
那么就是00100000 = 0x20
000A2706      66:B8 2000    mov ax,0x20
000A270A      8ed0          mov ss,eax
然后发现不让我们修改。

那么我们尝试修改CS。
00011 0 11
那么就是00011000 = 0x18
000A2706      ea 13270a00 1>jmp far 0018:000a2713

但是我们发现,我们的CS并没有变,即我们的0环权限也没有变。

那么就是说通过这种方式去修改是不可行的。
尝试另一种修改CS的方式:
000A2706      9a 13270a00 1>call far 0018:000a2713
然后CS还是没有被修改。

所以我们就只能改RPL,CPL无论我们是修改CS还是SS都无法进行修改。

调用门

CPU给我们提供了一个调用门,通过这个我们才能对CPL进行提权。

我们前面说过,当S为0的时候是系统段,为1的时候是代码段/数据段。
而为1的话,Type是我们前面用的那张图。
如果为0的话,系统段,Type我们要用下面这张新的图。

调用门的Type值是C.

调用门低32位:
  -0~15位,跳转的地址
  -16~31位,段选择子【提权的关键】
  
调用门高32位:
  -16~31位,跳转到地址高位
  -0~4位,参数的数量
  -5~7位,固定0
  -8~11位,就是我们的Type,固定是1100这才表示是调用门
  -12~15位,是S DPL 和P,S固定是0,因为调用门是系统段,DPL要设置为3,
做这件事情的需要什么权限,那么我们要使用调用门,肯定以用户权限去执行,一开始哪有内核权限

0040 E0C0 0008 1000
然后我们找个空白的位置写入,我们写入到gdtr中的第9索引位置。

然后要用调用门,我们需要使用call far,不能使用jmp far,jmp far不能提权。

#include <Windows.h>
#include <iostream>

void __declspec(naked) test() {
	__asm {
		push fs; // 保存fs
		int 3;
		pop fs; // 还原fs的权限
		mov eax, 0x80b95040;
		mov eax, [eax];
		// ret // 这样直接返回会蓝屏,因为我们提权变为了0环,回去的话,程序是3环
		// 我们要恢复成3环才行
		retf // call far 有一个对应的指令叫做retf,它有降权的效果
	};
}

int main(){
	// 前4个EIP 后2个段选择子
	// EIP随便 主要是段选择子
	// 段选择子我们 要修改为GDT表中的第9项,然后权限为00
	// 1001 0 00 ==》 0x48
	// 因为第9项已经被我们设置为调用门了
	// 调用们里面包含了要跳转到函数地址
	// 参数数量那些信息了 就会跳转到调用门中的函数地址。
	BYTE code[6] = {0,0,0,0,0x48,0};
	__asm {
		call far fword ptr code; // 进入以后 cs ss 和 fs的值都会被修改为0环,但是cs 和 ss可以retf还原,fs不能
	};
	// test:0x00401000
	printf("test:%x\n",test);
	system("pause");
	return 0;
}

我们再构造2个有参数的调用门试下
0040 EC02 0008 1000

kd> dds esp l8
a0e84c94  0012003b   fs
a0e84c98  00401085   要跳转回去的地址EIP
a0e84c9c  0000001b   CS的段选择子
a0e84ca0  00000456   参数1
a0e84ca4  00000123   参数2
a0e84ca8  0012ff2c   esp
a0e84cac  00000023   SS的选择子
a0e84cb0  00000000

那么retf和ret的区别,retf除了retn eip,还retn了cs
就是多了一个pop cs
push cs
push ip

pop ip
pop cs

#include <Windows.h>
#include <iostream>

void __declspec(naked) test() {
	__asm {
		push fs; // 保存fs
		int 3;
		pop fs; // 还原fs的权限
		mov eax, 0x80b95040;
		mov eax, [eax];
		// ret // 这样直接返回会蓝屏,因为我们提权变为了0环,回去的话,程序是3环
		// 我们要恢复成3环才行
		retf 8; // call far 有一个对应的指令叫做retf,它有降权的效果
	};
}

int main(){
	// 前4个EIP 后2个段选择子
	// EIP随便 主要是段选择子
	// 段选择子我们 要修改为GDT表中的第9项,然后权限为00
	// 1001 0 00 ==》 0x48
	// 因为第9项已经被我们设置为调用门了
	// 调用们里面包含了要跳转到函数地址
	// 参数数量那些信息了 就会跳转到调用门中的函数地址。
	BYTE code[6] = {0,0,0,0,0x48,0};
	__asm {
		push 0x123;
		push 0x456;
		call far fword ptr code; // 进入以后 cs ss 和 fs的值都会被修改为0环,但是cs 和 ss可以retf还原,fs不能
	};
	// test:0x00401000
	printf("test:%x\n",test);
	system("pause");
	return 0;
}

中断门

中断门的结构和调用门差不多。

中断门也是属于系统段的,所以我们的S需要设置为0.
然后中断门处的索引

低32位:
  -0~15函数地址的低位
  -16~31是段选择子

高32位:
  -16~31函数地址的高位
  -0~4位是保留位【和调用门不一样,调用门0~4位是参数的数量】
  -5~7位,固定0
  -8~11位是Type,我们要设置为1110才是中断门,所以我们把D替换为1
  -12~15位是S DPL 和 P,同理这里的S我们要设置为0系统段,DPL我们要设置为3,做这件事情要用户权限,我们刚开始也没内核权限。

中断门存放在IDT表,idtr,也就是
内核CS: 0008

0040 EE00 0008 1000
然后把这个放到IDT表中空白的位置,第33项

int 3就表示IDT 表中的第3个中断程序
那么我们直接 int 0x20 就是第33个中断程序了。下标是0x20

#include <Windows.h>
#include <iostream>

void __declspec(naked) test() {
	__asm {
		push fs; // 保存fs
		int 3;
		pop fs; // 还原fs的权限
		mov eax, 0x80b95040;
		mov eax, [eax];
		iretd;  // int 和 iretd  对应
	};
}

int main(){
	// 前4个EIP 后2个段选择子
	// EIP随便 主要是段选择子
	// 段选择子我们 要修改为GDT表中的第9项,然后权限为00
	// 1001 0 00 ==》 0x48
	// 因为第9项已经被我们设置为调用门了
	// 调用们里面包含了要跳转到函数地址
	// 参数数量那些信息了 就会跳转到调用门中的函数地址。
	BYTE code[6] = {0,0,0,0,0x48,0};
	__asm {
		int 0x20; // 进入以后 cs ss 和 fs的值都会被修改为0环,但是cs 和 ss可以retf还原,fs不能
	};
	// test:0x00401000
	printf("test:%x\n",test);
	system("pause");
	return 0;
}

eflags的第9位是IF标志位。
它是中断使能标志位,中断使能是通过I/O操作设置外部的中断控制器,决定当某一个中断请求发生的时候,中断控制器是否向处理器发送中断信号。
设置为1以响应可屏蔽中断请求。
设置位0以禁止可屏蔽中断请求。

中断分为可屏蔽中断和不可屏蔽中断。

这个IF标志位只对可屏蔽中断有效。设置为0可以进行屏蔽。

堆栈情况
00401065 三环的EIP(返回地址)
0000001b 三环的CS段选择子
00000216 三环的EFLAGS寄存器
0012ff44 三环的ESP
00000023 三环的SS段寄存器
可以看到和调用门不同的是,中断门比调用门多保存了一个EFLAGS寄存器

总结:堆栈情况,中断门比调用门多保存了一个EFLAGS寄存器

陷阱门

中断门和陷阱门都在IDT表中,和调用门在GDT表不同。

通过中断门进入中断服务程序的时候CPU会自动将中断关闭。
也就是将CPU中的EFLAGS寄存器中IF标志复位。防止嵌套中断的发生。
而通过陷阱门进入服务程序的时候维持IF标志位不变。这是中断门和陷阱门唯一的区别。

中断门执行时,会将IF标志位清零,但陷阱门不会
IF=0 时:程序不再接收可屏蔽中断

这样中断门就不会嵌套处理中断,而陷阱门IF对它没影响。

低32位:
  -0~15位,函数地址的低16位
  -16~31位,段选择子
高32位:
  -0~4位,保留位
  -5~7位,固定0
  -8~11位,在表中的第几项,陷阱门是15,也就是1111,所以要把D替换为1
  -12~15位,S = 0,DPL = 3, P = 1。S = 0系统段,D[L我们肯定也是做这件事情要用户权限就好了,那么就是1 11 0

陷阱门使用也是:int xx

构造陷阱门
0040EF0000081000
我们也把他放在第33项,即下表为32,0x20的位置。

然后代码我们都不用动。

8ab17c9c  00401070  函数的返回地址 EIP
8ab17ca0  0000001b  CS的段选择子
8ab17ca4  00000212  eflag
8ab17ca8  0012ff38  esp
8ab17cac  00000023  ss

所以陷阱门的堆栈和中断门一样,都保存了eflag,比起调用门的区别。

#include <Windows.h>
#include <iostream>

void __declspec(naked) test() {
	__asm {
		push fs; // 保存fs
		int 3;
		pop fs; // 还原fs的权限
		mov eax, 0x80b95040;
		mov eax, [eax];
		iretd;  // int 和 iretd  对应
	};
}

int main(){
	// 前4个EIP 后2个段选择子
	// EIP随便 主要是段选择子
	// 段选择子我们 要修改为GDT表中的第9项,然后权限为00
	// 1001 0 00 ==》 0x48
	// 因为第9项已经被我们设置为调用门了
	// 调用们里面包含了要跳转到函数地址
	// 参数数量那些信息了 就会跳转到调用门中的函数地址。
	BYTE code[6] = {0,0,0,0,0x48,0};
	__asm {
		int 0x20; // 进入以后 cs ss 和 fs的值都会被修改为0环,但是cs 和 ss可以retf还原,fs不能
	};
	// test:0x00401000
	printf("test:%x\n",test);
	system("pause");
	return 0;
}

任务段

Xxx程序员 有三个领导 A领导 B领导 C领导
三个领导同时给程序发了任务 A任务 B任务 C任务
按照我们的想法,是哪个紧急先干哪个。
不过A B C领导比较特殊,每隔一会就来问这个程序员进度。
那我们肯定不能先干一个。

这个程序员就想了一办法,三个任务来回切换,这样三个领导都不会得罪了。

回到单核CPU上,它的工作模式其实就跟这个程序员是一样的。
然后每个任务做了一部分,那肯定需要保存进度,下次才能继续做。
CPU也是这样,它在切换任务的时候,要保存当前任务的环境,寄存器环境 标志寄存器环境 继续执行的地址那些。

任务段TSS。
TSS就是保存了这些任务的环境。这样才能根据保存的这些环境恢复任务的状况。

下面的SS0 ESP0 SS1 ESP1这些对应的是提权的几环,提权到几环就是几。
SS0 ESP0 0环
SS1 ESP1 1环
.....
按照windows机制,我们只用关心0环。

TSS内存的地址保存在tr寄存器的段选择子指向的段描述符的Base中。
tr寄存器,是一个段选择器。段选择子0x28。
00101 0 00
那么权限就是0环,然后GDT表,然后索引为5的位置
80008b1e`400020ab

低32位400020ab:
  -0~15是Limit 20ab
  -16~31是Base 4000
高32位80008b1e:
  -0~7是Base 1e
  -24~31是Base 80 那么Base就是 801e4000
  -7~11是Type,那么就是b
  -12~15是8,1000,那么S = 0系统段,DPL = 0,事情权限0环,P = 1
  -16~19 Limit 0 Limit 20ab
  -20~23是0 AVL 0,D/B 0,G 0,G 0就表示单位是字节,所以Limit就是20ab,是1才表示是页,然后D/B是0表示是16根线寻址

那么tr寄存器,内存的起始地址就是:801e4000

dt _结构体名称 内存
以某个结构体的方式去解析这块内存。
kd> dt _KTSS 801e4000

Backlink这个属性,表示上一个TSS的段选择子,类似链表,不过是单向链表,指向上一个

LTR 汇编指令可以直接修改TR寄存器的值,不过这个指令是一个特权指令,需要0环权限。
STR 汇编指令是读取TR寄存器的值,这个指令也是一个特权指令,需要0环权限。

通过CALL FAR进行TSS任务切换实验。

那么我们要构造一个TSS.
  -Base就是我们自定义的TSS的首地址。
  -Limit就是这个内存的大小 也就是104个字节,然后G设置为0,因为单位是字节
  -Type中的B表示当前的状态,第一次创建设置为0表示空闲
  
tss地址:0x00403018
  
0000E94030180068

通过 !process 0 0,查看所有进程,找到我们刚刚运行的进程DirBase就是我们要的DR3

dg 段选择子  这样会自动帮我们解析

ESP就是给我们要执行的函数当作栈空间使用的。
ESP0给提权到0环的时候用的

ired返回是靠esp返回还是tss返回取决于我们的EFLAGS标志寄存器
EFLAGS标志寄存器中第14位NT标志
  -如果为0 表示靠ESP返回
  -如果为1 表示靠当前TSS的BackLink上一个TSS的数据进行恢复
    -因为上一个TSS保存了执行当前TSS任务之前的环境,那么我执行完当前TSS我肯定要恢复成上一个TSS的环境。

#include <Windows.h>
#include <iostream>

typedef struct _KTSS
{
    // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
    USHORT link;                                                        	 //0x0
    // 这 6 个值是固定不变的,用于提权,CPU 切换栈的时候用
    USHORT Reserved0;                                                       //0x2
    ULONG esp0;                                                             //0x4
    USHORT ss0;                                                             //0x8
    USHORT Reserved1;                                                       //0xa
    ULONG notUsed1[4];                                                      //0xc
    ULONG CR3;                                                              //0x1c
    ULONG eip;                                                              //0x20
    ULONG eflags;                                                           //0x24
    ULONG eax;                                                              //0x28
    ULONG ecx;                                                              //0x2c
    ULONG edx;                                                              //0x30
    ULONG ebx;                                                              //0x34
    ULONG esp;                                                              //0x38
    ULONG ebp;                                                              //0x3c
    ULONG esi;                                                              //0x40
    ULONG edi;                                                              //0x44
    USHORT es;                                                              //0x48
    USHORT Reserved2;                                                       //0x4a
    USHORT cs;                                                              //0x4c
    USHORT Reserved3;                                                       //0x4e
    USHORT ss;                                                              //0x50
    USHORT Reserved4;                                                       //0x52
    USHORT ds;                                                              //0x54
    USHORT Reserved5;                                                       //0x56
    USHORT fs;                                                              //0x58
    USHORT Reserved6;                                                       //0x5a
    USHORT gs;                                                              //0x5c
    USHORT Reserved7;                                                       //0x5e
    USHORT LDT;                                                             //0x60
    USHORT Reserved8;                                                       //0x62
    USHORT flags;                                                           //0x64
    USHORT IoMapBase;                                                       //0x66
}TSS;

// 自己构造一个tss 来让CPU进行覆盖
TSS tss = {0};
BYTE esp0[0x2000]; // 0环栈空间
BYTE esp[0x2000]; // 3环栈空间

void __declspec(naked) test() {
	__asm {
        int 3 // 会把nt位清0 
        pushfd; // 保存eflags
        pop eax;
        or eax, 0x4000; // nt位设置为1
        push eax;
        popfd; // 还原eflags
        iretd;
	};
}

int main(){
    memset(esp0, 0, 0x2000);
    memset(esp, 0, 0x2000);
    tss.eip = (ULONG)test; // 这个TSS任务要执行的任务起始位置,那么 在切换的时候就会把eip替换为函数的地址
    tss.esp0 = ((ULONG)esp0 + 0x1600); // 不能直接给栈顶 这样就直接满了
    tss.esp = ((ULONG)esp + 0x1600); // 不能直接给栈顶 这样就直接满了
    // 要提权 所以cs 和 ss要设置0环的值
    tss.cs = 0x08;
    tss.ss = 0x10;
    // ds这些直接给3环的值
    tss.ds = 0x23;
    tss.es = 0x23;
    tss.fs = 0x30; // fs也要给0环的值 0环30 3环3B
    tss.ss0 = 0x10; // 0环的ss值
    DWORD dCR3 = 0;
    // tss:0x00403018
    printf("tss:%x\n", &tss);
    printf("请输入Cr3\n");
    scanf_s("%x", &dCR3);
    tss.CR3 = dCR3; // CR3 通过 !process 0 0
    // 那么我们就构造完一个TSS任务了。

	// 最后两个是段选择子
	// 01001 0 00 GDT表,以0环身份去做,在GDT表中下标9的位置
	BYTE code[6] = {0,0,0,0,0X48,0};
	__asm {
		call far fword ptr code;
	};
	system("pause");
	return 0;
}

任务门

任务门就是间接调用TSS段的一种途径。

TSS段存放在GDT表里面,任务门存放在IDT表,。

IDT表的第9项,索引为8的。
00008500`00500000

当触发多重异常的时候,就会跳到这个任务门里面。

正常如果只触发了一次异常,异常处理程序可以获取我们的异常信息。
但是如果在异常处理程序里面再一次触发异常的话,那么我们第一次的异常信息就会丢失。

所以CPU提供了这个任务门,再次触发异常的时候就会到这里面,到任务段里面,对异常信息进行保存。

任务门的调用方式int指令。。我们到现在中断门 陷阱门 任务门都是用int,并且都是在IDT表。
而调用门,TSS段是在GDT表中,用call far fword ptr

前面我们是直接通过把TSS段的段选择子放到GDT表中,然后直接调用。

说白了,现在任务门,我们就是通过构造一个任务门,然后再去调用我们的TSS任务段。
就是中转了一层。

BYTE code[6] = {0,0,0,0,0X48,0};
__asm {
	call far fword ptr code;
};
  
原本我们是直接call TSS的段选择子,然后TSS的段选择子里面又保存了要执行的函数地址,信息那些

0000 E500`0048 0000

然后我们现在构造的任务门,放在LDT表中
然后我们通过int指令去执行。

当我们使用int指令的时候,回去LDT表中找到这个任务门的段描述符。
这个任务门的段描述符又指向了TSS段的段描述符,那么就回去执行GDT表中的TSS段描述符
然后TSS的段描述符里面又保存了要执行的函数地址,信息那些。
然后就可以正常执行了。

原本:call -> gdt中TSS段描述符 -> 执行函数
现在:int -> ldt中任务门的段描述符[有TSS的段选择子]  ->  gdt中TSS段描述符 -> 执行函数

#include <Windows.h>
#include <iostream>

typedef struct _KTSS
{
    // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
    USHORT link;                                                        	 //0x0
    // 这 6 个值是固定不变的,用于提权,CPU 切换栈的时候用
    USHORT Reserved0;                                                       //0x2
    ULONG esp0;                                                             //0x4
    USHORT ss0;                                                             //0x8
    USHORT Reserved1;                                                       //0xa
    ULONG notUsed1[4];                                                      //0xc
    ULONG CR3;                                                              //0x1c
    ULONG eip;                                                              //0x20
    ULONG eflags;                                                           //0x24
    ULONG eax;                                                              //0x28
    ULONG ecx;                                                              //0x2c
    ULONG edx;                                                              //0x30
    ULONG ebx;                                                              //0x34
    ULONG esp;                                                              //0x38
    ULONG ebp;                                                              //0x3c
    ULONG esi;                                                              //0x40
    ULONG edi;                                                              //0x44
    USHORT es;                                                              //0x48
    USHORT Reserved2;                                                       //0x4a
    USHORT cs;                                                              //0x4c
    USHORT Reserved3;                                                       //0x4e
    USHORT ss;                                                              //0x50
    USHORT Reserved4;                                                       //0x52
    USHORT ds;                                                              //0x54
    USHORT Reserved5;                                                       //0x56
    USHORT fs;                                                              //0x58
    USHORT Reserved6;                                                       //0x5a
    USHORT gs;                                                              //0x5c
    USHORT Reserved7;                                                       //0x5e
    USHORT LDT;                                                             //0x60
    USHORT Reserved8;                                                       //0x62
    USHORT flags;                                                           //0x64
    USHORT IoMapBase;                                                       //0x66
}TSS;

// 自己构造一个tss 来让CPU进行覆盖
TSS tss = {0};
BYTE esp0[0x2000]; // 0环栈空间
BYTE esp[0x2000]; // 3环栈空间

void __declspec(naked) test() {
	__asm {
        int 3 // 会把nt位清0 
        pushfd; // 保存eflags
        pop eax;
        or eax, 0x4000; // nt位设置为1
        push eax;
        popfd; // 还原eflags
        iretd;
	};
}

int main(){
    memset(esp0, 0, 0x2000);
    memset(esp, 0, 0x2000);
    tss.eip = (ULONG)test; // 这个TSS任务要执行的任务起始位置,那么 在切换的时候就会把eip替换为函数的地址
    tss.esp0 = ((ULONG)esp0 + 0x1600); // 不能直接给栈顶 这样就直接满了
    tss.esp = ((ULONG)esp + 0x1600); // 不能直接给栈顶 这样就直接满了
    // 要提权 所以cs 和 ss要设置0环的值
    tss.cs = 0x08;
    tss.ss = 0x10;
    // ds这些直接给3环的值
    tss.ds = 0x23;
    tss.es = 0x23;
    tss.fs = 0x30; // fs也要给0环的值 0环30 3环3B
    tss.ss0 = 0x10; // 0环的ss值
    DWORD dCR3 = 0;
    // tss:0x00403018
    printf("tss:%x\n", &tss);
    printf("请输入Cr3\n");
    scanf_s("%x", &dCR3);
    tss.CR3 = dCR3; // CR3 通过 !process 0 0
    // 那么我们就构造完一个TSS任务了。

	__asm {
        int 0x20;
	};
	system("pause");
	return 0;
}

分页

mov eax,dword ptr ds:[0x1234]
我们知道是取ds.Base+0x1234地址中四个字节的内容
这里的 0x1234我们叫做逻辑地址
而 ds.Base + 0x1234 计算出来的结果我们叫做线性地址

32位系统下分页模式有两种:
10-10-12 2-9-9-12

10 10 12分页配置:
bcdedit /set {current} nx AlwaysOff
bcdedit /set {current} PAE ForceDisable

通过!process 0 0 这个命令
如果dirBase相差 0x20 0x40 0x60这种就是 2-9-9-12 分页模式

10-10-12的dirBase后面基本是000的形式结尾的。

10-10-12分页

00401480 > $  E8 C5030000   call 反调试.0040184A

那么我们现在任务就是找到这个虚拟地址对应的真实物理地址。

那么我们就要拆分虚拟地址。
先转为二进制位,然后按照10-10-12拆分。
00401480 

0000000001 0000000001 010010000000
    1         1          480 

然后通过!process 0 0 查看所有进程信息
找到我们调试的这个进程。


PROCESS 862dcd40  SessionId: 1  Cid: 0fb8    Peb: 7ffdb000  ParentCid: 0a68
DirBase: 124cb000  ObjectTable: a0742f00  HandleCount:   9.
Image: starzjx.exe

然后我们找到DirBase:124cb000
这是一个物理地址,我们无法通过dd dw这些命令查看。
我们需要要!dd !dw这样的形式访问。

然后使用我们拆分出来的数据。
0000000001 0000000001 010010000000
    1         1          480 
    
第一个拆分出来的数据*4 = 1 * 4 = 4  OFFSET

然后使用DirBase + 计算出来的第一个OFFSET 
然后查看内存单元,把对应的值取出来:
12858867 
然后把后三位改为0后三位的是这个内存的属性
12858000

第二个拆分出来的数据*4 = 1 * 4 = 4 OFFSET2
12858004
然后用上面的地址再加上这个值,把对应内存单元的值取出来:
04909025 
同样的把后三位内存属性改为
004909000

第三个拆分出来的数据直接加上就好了
04909025+480
=04909480  

这个就是我们那个虚拟地址的真实物理地址了。

然后验证,查看内存字节码,一模一样。确实就是我们的物理地址了。

一个页是4096,也就是0x1024,然后4G的内存空间可以拆分出来0x100000块。

这些页需要管理,所以用多个数组来管理多个页的首地址。
每个数组的大小是4KB,也就是4096。
数组的每个元素是4个字节,也就是一个INT类型的数组,那么就可以放1024个元素。
每个元素就存放着页的首地址。

0x100000块个页,需要0x100个这样的INT数组来进行管理。
编号从0~FF

然后这0x100个数组又需要一个数组来维护他们的信息,这个数组的大小也是4KB。
这4KB,足够存放前面那0x100个数组的信息了。0x100也就256个。
每个元素存放着数组的首地址。

那么CPU想通过某个虚拟的地址去找到真实的物理地址,它是怎么找的呢?
也就是说,我们怎么找到一个维护了0x100个页维护数组的首地址的数组。
因为有了这一个,我就可以找到100个维护页首地址的数组,找到所有页、、。

CPU把这个数组放在了一个寄存器里面。要找这个数组直接取出值出来就好了。
这个寄存器的名字叫做CR3。
也就是DirBase,DirBase就是CR3寄存器的值。

那么现在我们就要研究前面为什么前两个参数需要*4,再加起来取地址内容,最后一个直接加起来。
0000000001 0000000001 010010000000
    1         1          480 
DirBase:124cb000

DirBase就是我们的CR3.那么我们就找到了第一个维护了0x100个维护页的数组的首地址的数组。
0000000001
    1     
第一个部分就是CR3这个数组中,第几个元素的索引,然后因为每一个元素是一个int,所以我们需要*4.

然后取出来内存单元的值,12858867,这个值就是维护了页的首地址的0x100个数组中的其中一个。
0000000001
   1      
第二个部分也就这个维护了页的首地址的数组的索引,一个元素也是一个int,所以我们也要*4
12858000 + 

然后取出来内存单元的值:04909025 ,这个值就是某个分页的首地址了,一个分页的的大小是1024,单位是BYTE.
010010000000
   480 
然后因为单位是BYTE,所以最后一层不用*4,直接+就好了,那么就定位到了具体的分页了。


页属性

除了第6位和第7位不同,PDE和PTE其他的位都相同

这4个字节中,前12位表示物理页的属性。

当PDE和PTE的属性一样的话,取&运算计算出来的就是物理页的属性。
PDE和PTE的属性不一样的话,那就不用运算了,因为各自表述的东西都不一样。

PDE属性 & PTE属性 = 物理页属性。

P:这个物理页是否有效,1有效,0无效
R/W:1表示可写,0表示只读
  -const char *str=""; 这个常量不可写就是这个页决定的,如果我们把常量在的页设置为1,可写了,那么常量就可写了。
  -VirtualProtect 这个也可以修改页的属性

实验手动修改物理页的属性,让const char *被成功修改。
0x004020fc

0000000001 0000000010 000011111100
  1          2           fc
  
然后找到CR3:3d094000  
CR3 = CR3 + 第一个参数 * 4 =  3d094004
取出来这个地址里面的内容,也就是PDE

PDE:0f722867
867这12位就是页属性:1000 0110 0111
这里的第二位是1那么是可写的。那么我们就要去找PTE.
物理页属性相同的属性 = PDE & PTE

0f722000+ 第二个参数 * 4  = 0f722008
取这个地址里面的内容
PTE:3a2d3025 
025这12位是页属性:0000 0010 0101
果然这一位的R/W是0,所以PDE & PTE 最终的页数形式R/W是0,即只读不写写。
所以我们要把PTE的第二位设置为1
0000 0010 0111
就是要把025,修改为027

然后这样页属性应该就变成可写了。
发现确实,这个时候const char *就变为可写了。

#include <Windows.h>
#include <iostream>

int main(){
	const char* str ="hello starHook";
	// 4020fc
	printf("%x:",str);
	const char* temp = &str[0];
	system("pause");
	*(char *)temp = 'H';
	printf("%s:", str);

	system("pause");
	return 0;
}

U/S位:权限位,拥有什么权限才能使用这一块内存 U普通用户,S超级用户,0是超级用户,1是普通用户
我们要使用高2G的内核内存,我们就要吧这一位设置为1。或者使用提权。

A位:这个内存是否被访问过

D位:这个内存是否被写入过。

PS位:当PS=0的时候,表明PDE指向一个4KB的页表PTT,这种线性地址对应的物理页,通常称为小页。
当PS=1的时候,表明PDE指向一个4MB的大小的普通物理页,而不是PTT,这种页称为大页。


分析VirtualProtect
0x00402104
0000000001 0000000010 000100000100
    1          2        104

CR3:2ae41000  
CR3 = CR3 + 参数1 * 4 = 2ae41004

PDE:183fa867 

PTE = 183fa000 + 参数2 * 4 取内容 = 396d8025 

物理地址 = 396d8000 + 104 = 396d8104 

执行完VirutalProtect,PDE:183fa867 PTE:396d8867 
发现PTE的属性值变化了,我们对比一下025 和 867
0000 0010 0101
1000 0110 0111
R/W从只读,变成了可写
PAT从0变为了1
AVL从0变成了1
最关键的地方就是R/W从只读变成了可写,这就是为什么我们修改之后,我们的const char *就可以被修改了
实际上跟我们自己手动修改物理页的只读属性是一样大。他也是修改了这个属性。

#include <Windows.h>
#include <iostream>

int main(){
	const char* str ="hello starHook";
	// 4020fc
	printf("%x:",str);
	system("pause");

	DWORD oldProctect = 0;
	VirtualProtect((LPVOID)str,0x1,PAGE_READWRITE,&oldProctect);

	const char* temp = &str[0];
	*(char *)temp = 'H';
	printf("%s:", str);

	system("pause");
	return 0;
}

不提权访问高2G内核空间
前面我们说了修改U/S,提升权限,或者使用提权页可以完成操作。
那么这里要求不使用提权,那么我们只能修改U/S的值了,0是超级用户,1是普通用户。
拥有什么权限才能使用这块内存,那么我们就要把超级用户降为普通用户。
拥有普通用户权限就可以使用这块内存。

线性地址:80b95000
1000000010 1110010101 000000000000
    202        395        0

DR3 = 14d9c000  

PDE = 14d9c000 + 202 * 4 取内容 = 0018a063 
0000 0110 0011
这里U/S是0,也就是需要超级用户权限,那么我们要把这里设置为1
除了U/S位需要改,我们还要把G位设置为0才行
0000 0110 0111
= 067

PTE = 0018a000 + 395 * 4 取内容 = 00b95163 
0001 0110 0011
这里的U/S也是0,也是需要超级用户权限,所以我们也要把这里设置为1
除了U/S位需要改,我们还要把G位设置为0才行
0000 0110 0111
= 067

这个时候 PDE & PTE 的结果就是1了,那么就只需要用户权限就可以访问内核空间了。

#include <Windows.h>
#include <iostream>

int main(){
	system("pause");
	__asm {
		mov eax, 0x80b95000;
		mov eax, [eax];
	};

	system("pause");
	return 0;
}

给0地址挂物理地址。
a:0x0012ff38
0000000000 0100101111 111100111000
    0         12F        F38

DR3 = DirBase = 140cc000  

PDE:地址:140cc000   内容:3c351867 
PTE:地址:3c3514BC    内容:0d399867 
物理地址:0d399F38
  
给0地址挂上PTE,这样0地址就有了物理页了。
然后发现没有问题,pa这个0地址现在的物理地址和a是一样的了。
那么我们就给0地址成功挂上了物理页

#include <Windows.h>
#include <iostream>

int main(){
	int a = 0x1234;
	printf("a %x:",&a); // 把a的物理页给pa挂上
	system("pause");
	int* pa = 0;
	*pa = 0x456;
	printf("a = %x:", a);
	system("pause");
	return 0;
}

101012分页内存管理

给你一个虚拟地址,你自己写代码判断这个地址是否有效
不能使用WINDOWS的API。

自己想的话,那肯定就是解析这个地址,然后拿到CR3。再拿到PDE和PTE,看看是否挂载上了物理地址。
没挂载的话就是无效,挂载了我们就去判断属性位。

但是这里面有一个很严重的问题,这个CR3存放的手一个物理地址,我们根本没有办法去直接使用。

我们只能在程序里面直接使用线性地址,那么操作系统肯定也是这样的。
那么这个CR3的物理地址一定也会映射出来一个线性地址。

那么接下来我们就是要找到线性地址。

10-10-12分页模式对应的内核Windows/system32/ntoskrnl.exe

因为是一个内核程序,我们肯定不能动态调试,我们使用IDA进行静态调试。
然后通过微软的PDB函数修复器,对名字进行修复。
https://docs.microsoft.com/zh-cn/archive/blogs/webtopics/pdb-downloader

找到这个函数:
MmIsAddressValid(x)	 【地址是否有效】
我们分析这个函数内部。

假设我们现在有这么个地址 0x00701000
10 10 12拆分
0000000001 1100000001 000000000000
     1         301         0
     
我们手工是这样拆分出来,然后找到CR3,再通过第一个参数,也就是前10位,去找到PDE。

那么我们看他是怎么找到PDE的,因为他页不可能直接去使用CR3,就跟我们说的一样。
在程序里面我们肯定直接直接使用线性地址。而不是物理地址。

.text:00489B98                 mov     eax, ecx
.text:00489B9A                 shr     eax, 14h
.text:00489B9D                 and     eax, 0FFCh
.text:00489BA2                 sub     eax, 3FD00000h
.text:00489BA7                 mov     eax, [eax]
.text:00489BA9                 test    al, 1
.text:00489BAB                 jnz     short loc_489BB0
.text:00489BAD
.text:00489BAD loc_489BAD:                             ; CODE XREF: MiIsAddressValid(x,x)+32↓j
.text:00489BAD                 xor     al, al
.text:00489BAF                 retn

地址 0x00701000
0000 0000 0111 0000 0001 0000 0000 0000

shr     eax, 14h 它首先把这个线性地址右移 0x14h位

0000 0000 0000 0000 0000 0000 0000 0111

and     eax, 0FFCh 然后跟0x0FFFCH做与运算

0000 0000 0000 0000 0000 0000 0000 0111
0000 0000 0000 0000 0000 1111 1111 1100
---------------------------------------
0000 0000 0000 0000 0000 0000 0000 0100
结果就是:4

诶!我们惊奇的发现,就是我们都前10位,1 * 4的结果。
我们手工就是 CR3 = CR3 + 前10位 * 4 
而这里就计算出来了前10位 * 4的结果

sub     eax, 3FD00000h 接着 4 - 3FD00000h
C030 0004
也就是相当于 C030 0000 + 4
那么3FD00000h 可以理解为PDT,+4 取出来PDE的地址
PDT == C030 0000

mov     eax, [eax] 然后取出来C030 0004这个地址里面内容

应该eax就是我们的PDE的值了

test    al, 1 取出来PDE的低8位跟1做与运算,那么就是判断P是否有效。
P = 1即物理页有效,P = 0即物理页无效。
所以如果物理页有效的话,ZERO FLAG这个标志位应该是0
否则的话物理页无效,即这个线性地址是无效的。
所以如果是0,就直接返回,这个线性地址无效。

.text:00489BB0                 test    al, al
.text:00489BB2                 jns     short loc_489BB7
.text:00489BB4                 mov     al, 1
.text:00489BB6                 retn

test    al, al
jns     short loc_489BB7 
就是判断最高位是不是为1,如果不是负数的话,那么跳转。
如果是负数的话,那么把al的值设置为1,然后返回TRUE。
al的最高位,是不是为1,那么就是第7位。也就是我们的PS位。
如果PS位为1,那么指向的是4MB的普通物理页,也叫大页。
如果PS位为0,那么指向的是4KB的页表PTT,这种线性地址对应的物理页,页叫做小页。
所以如果第7位PS = 1,那么我们就不用继续寻找了,因为指向的是物理页了,不是PTT。

如果是0的话,我们才需要继续解析PTT

.text:00489BB7                 shr     ecx, 0Ah
.text:00489BBA                 and     ecx, 3FFFFCh
.text:00489BC0                 sub     ecx, 40000000h
.text:00489BC6                 mov     eax, [ecx]
.text:00489BC8                 test    al, 1
.text:00489BCA                 jz      short loc_489BAD
.text:00489BCC                 and     al, 80h
.text:00489BCE                 cmp     al, 80h ; '€'
.text:00489BD0                 setnz   al
.text:00489BD3                 retn

如果是小页,那么我们就要找到PTT了。再根据PTT来找到PTE,然后找到物理地址。
0x00701000
10 10 12拆分
0000000001 1100000001 000000000000
     1         301         0

shr     ecx, 0Ah  右移10位。ecx是我们的虚拟地址
0000 0000 0000 0000 0001 1100 0000 0100

and     ecx, 3FFFFCh
0000 0000 0000 0000 0001 1100 0000 0100
0000 0000 0011 1111 1111 1111 1111 1100
---------------------------------------
0000 0000 0000 0000 0001 1100 0000 0100
1C04

sub     ecx, 40000000h =》 C000 1C04 这里拿到的应该就是PTE的地址
40000000 => C000 0000 这个应该就是PTT的地址
PTT: C000 0000

test    al, 1 同理判断PTE的第一位,也就是P位【有效的物理页】是否为1,
如果为0直接跳转,retn,表示这是一个无效的物理地址。
如果为1:
and     al, 80h  最高位,也就是PAT位,和1000 0000做与运算,其实就是看PAT是否为1
cmp     al, 80h ; 那么就是要比较与运算结果为0,返回才是1,那么要求PAT = 0,如果PAT = 1,那么与的结果不为0
setnz   al  ZF取反,如果ZF标志位为1,那么就设置为0,否则设置为1

所以如果PAT=0,这个物理地址才是有效的,如果PAT=1,那么这个物理地址是无效的

总结操作系统判断线性地址是否是一个有效的地址:
  -1.判断判断PDE P位是否=1有效,如果等于1看PS位是否=1,等于1大页直接返回
  -2.如果是小页,判断PTE的 P位是否=1有效
  -3.如果PTE的 P为=1有效,那么判断PAT是否=0有效,如果有效是有效的线性地址
  
是高2G的地址,所以无法直接在3环使用,要提权
PDT == C030 0000 
PTT == C000 0000

PDE = PDT + 第一个参数偏移 * 4
PTE = PTT + 偏移(线性地址右移10位 & 3FFFFCh)

IDA快捷键:
  -r键 把参数名变为ebp寻址方式  
  -c键 把ebp寻址的方式变为参数名
  

29912分页

前面我们说过32位下面有两种分页方式:10-10-12和2-9-9-12分页
10-10-12分页方式,在这种分页方式下物理地址最大支持4GB

想要扩大物理地址:
  -1.修改分页模式
  -2.增加地址总线由32 to 36 = 64G【线性地址还是32位的4个G】
  
和10-10-12分页模式不同:
  -PDT表由一张变为了多张PDT表
  -多了一张PDPTT表。
  -元素从4个字节变成了8个字节
  
所以我们的寻址方式:
  -CR3 + 第一个参数 * 8 拿到PDPTE的内容也就是PDT
  -然后通过PDT + 第二个参数 * 8 拿到PDE的内容,也就是PTT
  -在通过PTT + 第三个参数 * 8 拿到PTE的内容,也就是物理页
  -最后再加上第四个参数的内容
  
00401480 > $  E8 C5030000   call starzjx.0040184A

00 000000010 000000001 010010000000
0      2        1         480

CR3 = DirBase = 3f3265a0  

PDPTE地址 = CR3 + 第一个参数 * 8 取内容  00000000`394a8801
PDE地址 = 394a8000 + 第二个参数 * 8 取内容 00000000`13fc3867
PTE地址 = 13fc3000 + 第三个参数 * 8 取内容 00000000`3833a025

物理地址 = 3833a000 + 第四个参数 
发现没有问题。
成功找到物理地址。

TLB

TLB页表缓存。

作用:
缓存,CPU内部的缓存,有什么作用?
无论是101012分页还是29912分页每次拆分线性地址的时候都需要经历很多步骤,为了提高效率CPU将使用过的线性地址和对应的物理地址等属性保存起来。
有了缓存,那么下次就不用再经历那么多步骤了,直接可以找到物理地址,这些信息就存放到了TLB里面。

多核对应多套TLB,每个核拥有自己的一套TLB。

下图的物理页地址实际上是0x12345678,不过他把0x12345当作物理页地址存储,678当作属性存储。

LUR就是使用频率,当缓存的这块内存满了以后,就回看那个缓存即LUR使用次数较低,就清除谁。

不过存在一个问题,每个进程有自己的4G虚拟空间,其中高2G是内核,低2G是用户自己的空间。
A进程有的地址,B进程可能也有。
那么肯定挂的不是一个物理页,那么从TLB取出来肯定有问题。
所以当进程切换的时候TLB就会被废弃了。

当我们在A进程的时候在TLB中存了一套这个东西,当我们切换到B进程的时候,A进程存储在TLB的东西就会被废弃掉。

CR3存放的是页表
当我们切换进程的时候CR3会变化,同理CR3变化页表示我们进程发生了变化

实验:通过调用门,实现给0地址挂上物理页

构造调用门:
函数地址:0x00401000

0040ec4010000008,然后放到gdtr的第9项

#include <Windows.h>
#include <iostream>

DWORD* dp1; 
DWORD* dp2; 
DWORD g_temp;

void _declspec(naked) test() {
	// 实现给0地址挂上物理页
	__asm {
		mov ecx, dp1
		// 挂上PTE就好了 因为PDE是存在的 
		shr ecx, 0x0A
		and ecx, 0x3FFFFC
		sub ecx, 0x40000000
		mov eax, [ecx] // 这样就拿到了真实的物理地址
		mov ebx, 0xC00000000  // 0地址的PTT
		mov [ebx], eax // 把真实的物理地址挂到0地址上

		mov eax, dword ptr ds : [0];  // 取0地址的内容 这个时候0地址指向的物理地址实际上和dp1指向的物理地址一样
		mov g_temp, eax; // 给g_temp

		// 挂上PTE就好了 因为PDE是存在的 
		mov ecx, dp2
		shr ecx, 0x0A
		and ecx, 0x3FFFFC
		sub ecx, 0x40000000
		mov eax, [ecx] // 这样就拿到了真实的物理地址
		mov ebx, 0xC00000000  // 0地址的PTT
		mov[ebx], eax // 把真实的物理地址挂到0地址上

		mov eax, dword ptr ds : [0] ;  // 取0地址的内容 这个时候0地址指向的物理地址实际上和dp1指向的物理地址一样
		mov g_temp, eax; // 给g_temp
		retf
	}
}

int main(){
	dp1 = (DWORD*)VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	dp2 = (DWORD*)VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	g_temp = 0;
	*dp1 = 0x1234;
	*dp2 = 0x5678;

	// 构造调用门前4位无所谓 后2位是调用门的段选择子里面包含了要执行的函数地址和所在GDT表的索引
	// 01001 0 00 以0环的身份 在GDT表 中的第9项 我们的调用们
	BYTE code[6] = {0,0,0,0,0x48,0};
	printf("test : %x\n",test);
	system("pause");

	__asm {
		push fs; // 保存fs寄存器的值 防止内部被修改
		call far fword ptr code; // 提权 通过调用门 cs 和 ss权限变为0环
		pop fs; // 还原fs寄存器的值
	};
	printf("g_temp = %x\n", g_temp);
	system("pause");
	return 0;
}

然后我们刚开始把0地址挂载到dp1上,成功输出1234没问题。
可是我们挂载到dp2上的时候还是1234
这就证明了TLB的缓存存在,同一个虚拟地址指向的物理地址有缓存。
这个时候TLB有缓存了,所以就直接读取原本的dp1了,不理会dp2.

要切换进程,TLB才会失效,然后再切换回来挂载dp2的时候就不会去访问TLB的缓存dp1的物理地址了。
切换进程也就是修改CR3。那么进程页等于刷新了。
所以切换进程,或者修改CR3都行,都相当于让TLB失效。

#include <Windows.h>
#include <iostream>

DWORD* dp1; 
DWORD* dp2; 
DWORD g_temp;

void _declspec(naked) test() {
	// 实现给0地址挂上物理页
	__asm {
		mov ecx, dp1
		// 挂上PTE就好了 因为PDE是存在的 
		shr ecx, 0x0A
		and ecx, 0x3FFFFC
		sub ecx, 0x40000000
		mov eax, [ecx] // 这样就拿到了真实的物理地址
		mov ebx, 0xC00000000  // 0地址的PTT
		mov[ebx], eax // 把真实的物理地址挂到0地址上

		mov eax, dword ptr ds : [0];  // 取0地址的内容 这个时候0地址指向的物理地址实际上和dp1指向的物理地址一样
		mov g_temp, eax; // 给g_temp

		mov eax, cr3;
		mov cr3, eax;
		// 挂上PTE就好了 因为PDE是存在的 
		mov ecx, dp2
		shr ecx, 0x0A
		and ecx, 0x3FFFFC
		sub ecx, 0x40000000
		mov eax, [ecx] // 这样就拿到了真实的物理地址
		mov ebx, 0xC00000000  // 0地址的PTT
		mov[ebx], eax // 把真实的物理地址挂到0地址上

		mov eax, dword ptr ds : [0] ;  // 取0地址的内容 这个时候0地址指向的物理地址实际上和dp1指向的物理地址一样
		mov g_temp, eax; // 给g_temp
		retf
	}
}

int main(){
	dp1 = (DWORD*)VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	dp2 = (DWORD*)VirtualAlloc(0, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	g_temp = 0;
	*dp1 = 0x1234;
	*dp2 = 0x5678;

	// 构造调用门前4位无所谓 后2位是调用门的段选择子里面包含了要执行的函数地址和所在GDT表的索引
	// 01001 0 00 以0环的身份 在GDT表 中的第9项 我们的调用们
	BYTE code[6] = {0,0,0,0,0x48,0};
	printf("test : %x\n",test);
	system("pause");

	__asm {
		push fs; // 保存fs寄存器的值 防止内部被修改
		call far fword ptr code; // 提权 通过调用门 cs 和 ss权限变为0环
		pop fs; // 还原fs寄存器的值
	};
	printf("g_temp = %x\n", g_temp);
	system("pause");
	return 0;
}

每套TLB按照种类分为四类:
  -1.小页指令页表缓存
  -2.小页数据页表缓存
  -3.大页指令页表缓存
  -4.大页数据页表缓存
  
G位=Global的意思。前面我们设置0地址的时候把G设置为了0.
我们也说了每一个进程都有4G的虚拟内存空间,低2G是进程私有的,高2G是内核共有的。
那么高2G是共有的,那么我们切换进程的时候是不是没有必要把TLB给清空。
他们对应的物理地址肯定是一样大。

G位 = 1,切换进程的时候刷新TLB
G位 = 0,切换进程的时候不刷新TLB

汇编指令:INVLPG 
刷新指定的TLB记录

比如刷新0地址的TLB: INVLPG 0

控制寄存器

TLB存储的是线性地址到物理地址的映射信息。

CPU缓存的是物理地址内容。

【TLB缓存的是物理地址,CPU缓存的是物理地址里面的内容】

【指的CPU缓存】
PWT位:为1的时候,写缓存的时候同时将数据写入到内存中
PCD位:为1的时候,禁止当前页数据写入缓存

控制寄存器:CR0 CR1 CR2 CR3 CR4

CR0:
PE:启动保护标志位
  -PE = 1 表示启动段保护模式
  -PE = 0 实模式
这个标志位只是开启了段保护,并没有开启分页机制,要开启分页机制PE PG标志都要设置为1

PG:
  -PG = 1 表示开启分页机制,开启分页机制的前提是必须段保护模式,即PE=1
  
PG=0,PE=0 CPU工作在实模式下面
PG=0,PE=1 CPU工作在段保护模式下面
PG=1,PE=0 不允许
PG=1,PE=1 处理器工作在保护模式下面

WP:写保护位,WP=1的时候处理器禁止特权级用户向普通级用户只读页执行写操作

实模式保护模式:
从80386开始,CPU有三种工作模式:实模式,保护模式和虚拟8086模式。
所谓工作模式就是CPU的寻址方式,寄存器的大小,指令用法,和内存布局等。

实模式的特点:
  -1.所有的段都是可读可写可执行
  -2.式模式下对内存地址的操作都是真实的物理地址
  -3.咩有特权级
段基址 + 逻辑地址 = 真实物理地址

保护模式:
保护模式中内存的管理分为两种:
  -1.段模式
  -2.页模式,页模式是基于段模式的,就是说分为纯段模式和段页模式,我们前面学的内容都是段页模式的

虚拟8086模式:
  -虚拟8086模式其实是保护模式下的一种工作方式,也称为V86模式,主要目的就是为了运行DOS一以其为平台的软件。
  
CR1:保留
CR2:触发缺页异常的时候,CPU会将引起的异常线性地址存放到CR2中。
CR3:存放的就是页表的基址,也就是我们都DirBase
CR4:
  -PAE:分页模式 1表示是2-9-9-12分页模式,0表示是10-10-12分页模式
  -PSE:可以理解为是PS的总开关.PSE=1的时候,PDE的PS位才起作用,PSE位0的时候,无论PS位是啥都是小页。
  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要下载NASM,需要按照以下步骤进行操作: 1. 打开您的Web浏览器并转到NASM官方网站(http://www.nasm.us/)。 2. 在网站的导航菜单中选择“Download”选项。 3. 在下载页面上,找到与您的操作系统相对应的NASM版本。例如,如果您的操作系统是Windows,选择适用于Windows的安装程序。 4. 单击选定的下载链接,然后保存安装程序到您的计算机上。 5. 找到保存的安装程序文件并双击打开它。 6. 按照安装向导的指示进行操作。对于Windows用户,可简单地按照默认选项进行安装。 7. 安装程序将自动下载和安装NASM到您的计算机中。 8. 安装完成后,您可以通过命令提示符或终端窗口执行NASM编译器。 要编写和编译x86从保护模式到实模式的代码,您需要一个文本编辑器来编写代码,以及NASM编译器将代码转换为可执行文件。在您下载并安装NASM之后,可以使用任何文本编辑器(如Notepad++、Sublime Text、Visual Studio等)打开并编写代码。然后,使用命令提示符或终端窗口将代码保存为以.asm为扩展名的文件,并使用NASM编译器编译它。 例如,假设您已经编写了一个名为example.asm的代码文件。在命令提示符或终端窗口中,导航到保存了example.asm文件的目录,并执行以下命令进行编译: ``` nasm -f bin example.asm -o example.bin ``` 上述命令会将example.asm文件编译为一个名为example.bin的可执行文件。现在,您可以执行该可执行文件来运行您的x86代码。 请注意,这里只提供了一种方式来下载和使用NASM编译器。如果您想了解更多关于x86从保护模式到实模式的编程知识,请参考相关的教程、书籍或在线资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值