保护模式

段寄存器介绍

当我们用汇编写某一个地址的时候:mov dword ptr ds:[0x123456],eax我们真正读写的地址是:ds.base + 0x123456

段寄存器ES CS SS DS FS GS LDTR TR 一共是8个,因为之前看的16位的时候知道一些,CS是code代码base,DS是data数据base,SS是stack栈的base,实模式的时候是没有段选择子的,之前是base*16 + 地址,因为有20根线,因为有些地址并不能取到所以运用了这个方式取,但是进入了保护模式也不能将这些段寄存器丢掉所以就有了段选择子的概念

image-20210315213038267

像这些ES,CS,SS,DS…都是

我们用里面的DS开始举例子,DS = 0x2B这个只是段选择,段寄存器一共是96位

image-20210315213714054

struct SegMent
{
	WORD Selector;	//16位的Selector
	WORD Attribute;	//16位的Attribute
	DWORD Base;	//32位的Base
	DWORD Limit;	//32位的Limit
}

0x2B拆解:0010 1 011 后面的两位代表了 Requested Privilege Level(RPL),后面的两位代表的是请求的特权级别在几环 11 就是在3环,倒数第三位代表了 0 -> 查询GDT表 1 -> 查询ldt表

image-20210315214803678

因为gtr表的宽度是8所以要乘8

gdt = gdtr + index * 8

解释GDT表和LDT表比较好的图:

LDT的话索引是在LDT表中找,首先之前是要lldt的,然后再找ldtr,再索引

image-20210315221409588

火哥这里取出来的数据是00cff300-0000ffff

base: 00000000
Attribute: 0cf3
limit: fffffffff

image-20210315221920971

G位这里是1,代表粒度,1代表4K,0代表字节 C -> 1100进行拆分,因为G位是1,所以段限长:(0xfffff+1) * 0x1000 - 1 = ffffffff,因为有一个地方没用到所以Attribute补上0,因为段限长的3个字节补齐,以及Attribute的一个0的补齐,所以 是增加了16位,所以我们看到的是80位,但是真正是96位的原因

在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限

自己测试一下:

image-20210316185902834

可以看到 ds = 0x2B -> 00101 0 11 说明了现在是在3环查gdt表索引是5

image-20210316190418766

可以看到gdtr表的地址是0xfffff80000b95000,长度为0x7f

image-20210316190856273

看到的是 00cff300 0000ffff -> base = 0x00000000 Attribute = 0x0cf3 limit = 0xffffffff

段寄存器探测

image-20210316192908915

数据可以不一样的,红色的部分是我们能看到,但是如何证明剩下的这些是存在的

可以来写代码来证明一下这个是存在的,自己本机的段寄存器的数值

image-20210316202905410

用代码实现看看权限是否存在就可以将段寄存器复制之后,查看是否报错,就知道是否可以读写

	int var = 0;
00411838  mov         dword ptr [ebp-0Ch],0  
	__asm
	{
		mov ax,ss
0041183F  mov         ax,ss  
		mov ds,ax
00411842  mov         ds,ax  
		mov dword ptr ds:[var],1
00411845  mov         dword ptr [ebp-0Ch],1  
	}
	printf("%d", var);
0041184C  mov         eax,dword ptr [ebp-0Ch]  
0041184F  push        eax  

cs的时候会触发异常

image-20210316203551474

判断段寄存器是否存在段的base

#include <stdio.h>


void main()
{
	__asm
	{
		xor eax, eax
		xor ebx, ebx
		mov bx, ds
		mov ax, fs
		mov es, ax
		mov ebx,dword ptr ds:[0xBA3000]
		mov eax,es:[0]

	}
}

测试是否存在limit的属性

#include <stdio.h>


void main()
{
	int var = 0;
	__asm
	{
		mov ax, fs;
		mov es, ax;
		mov eax, dword ptr es : [0xffc] ;
		mov var, eax;
	}
	printf("%x", var);
}

如果是fs:[0xfff] 我们就多出了三个fff ffe ffd这三个字节,因为给eax是4个字节给的,所以多了三个,每次给的时候应该是4倍数开始取,一直到limit的结束

段描述符与段选择子

image-20210318232334083

LES( load ES)指令是把内存中指定位置的双字操作数的低位字装入指令中指定的寄存器、高位字装入ES寄存器的功能,les一次操作了6位(为啥:internel白皮书也没有说为什么)

#include <stdio.h>

int main()
{
	unsigned char buffer[6] = { 0x78,0x56,0x34,0x12,0x1B,0x00 };
	__asm
	{
		les eax, fword ptr ds : [buffer] 
		les eax, dword ptr ds : [buffer] 
	}
	return 0;
}

当时学从实模式到保护模式的时候,因为数据段只有一个ds去找的话比较繁琐,所以加了一个es段寄存器去找值

CPL:当前正在执行程序或任务的特权级,存放在代码段寄存器CS和堆栈段寄存器SS的最低两位中。

DPL:段或门的特权级,存放在段或门描述符的DPL字段中。

RPL:赋予段选择符的超越特权级,存放在选择符的最低两位中,访问非一致代码段时,DPL数值必须不大于CPL和RPL的最大值

当 S=1 时TYPE中的4个二进制位情况: 3 2 1 0 执行位 一致位 读写位 访问位

执行位:置1时表示可执行,置0时表示不可执行 一致位:置1时表示一致码段,置0时表示非一致码段 读写位:置1时表示可读可写,置0时表示只读 访问位:置1时表示已访问,置0时表示未访问

image-20210318232525612

当S位为0的时候我们的Type域可以通过下表来查看其含义

image-20210318232652785

int var = 0;
int main(int argc,char * argv[])
{
    __asm
   {
      mov ax,0x48;修改为P=0的段 es选择子
      mov es,ax
      mov ebx,0x1000;
      mov dword ptr es:[var],ebx; //查看结果
   }

   return 0;   
}

R3读写操作系统

段描述符中的 P: 段存在位,该位为 0 表示该段不存在,为 1 表示存在

操作系统正常在R3层的时候不能去修改和读取R0层的东西,也就是高2GB的空间的东西

image-20210320162908888

控制的位置就是U/S位,U/S = 0的时候是超级用户(但是这里的不是指操作系统的那个特权的用户,这里是CPU的超级用户), U/S = 1的时候代表了超级用户和普通用户都可以操作物理页,但是要满足的是PDE的U/S位和PTE的U/S位进行相与的结果是1的时候才可以对物理页进行操作

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

2 9 9 12:

image-20210320172322822

image-20210320172931341

10 10 12

image-20210320172421600

这里有三个表比较好看的我就直接复制下来了:PDPTE

image-20210320181544702

PDE

image-20210320181621513

PTE

image-20210320181633870

实验一下可以在R3读写操作系统的高2GB的内存空间

将地址:0x8003f048拆分成2 9 9 12结构

2: 10	2*8      
9: 000000000000	0

9: 000000111111	3f * 8

12: 000001001000 48

找到了CR3的地址:

image-20210320180817722

查找PDPT

image-20210320181648405

查找PDT

image-20210320181740962

查找PTT

image-20210320181822250

查找physical type

image-20210320182139312

如果在我们什么都没有修改的情况我们写代码去修改这个地方的内存会出现错误的因为我们在r3层并不能去读写r0的值,所以我们要改U/S位

#include "stdafx.h"
#include "stdio.h"
int main(int argc, char* argv[])
{
	int *getaddress = NULL;
	getchar();
	getaddress = (int*)0x8003f048;
	*getaddress = 2;
	printf("%x,%x",getaddress,*getaddress);
	return 0;
}

image-20210320182421533

修改U/S为1测试:

image-20210320182830547

可以看到可以读写了

image-20210320183028553

段描述符属性S_TYPE

S位 = 1 代码段或者数据段描述符

= 0 系统段描述符

image-20210321140604758

首先11位代表的是数据段和代码段 数据段中A代表了如果被加载到段选择子里面会被置1,否则 就是0,W代表是否为可读可写,E段代表了扩展的方向,如果是0的话就是向上增长,如果是1的话就是向下增长

image-20210321140954689

代码段中的A同上,但是R代表了是否为可读可执行,C位代表了如果是0(非一致代码段),那么R0层不可以调用R3层的代码,同样R3也不可以调用R0,但是如果是1的话,那么R3可以调用R0,C如果是1(一致代码段),那么R0中的函数,R3可以调用

做一下实验:

差一下段描述符的表

image-20210321142417801

我们在0x8003f048构造一个E位为1的段00cff700`0000ffff 我们让DPL = 3 ,TYPE的E = 1,我们直接赋值给ds做一下试验

image-20210321142718143

ds: 01001 011 = 0x4B

这个是我们看到的结果,没有出错,我们已经把ds换成了0x4B,而且base+limit已经没有空间了,为什么不报错,是因为我们这里默认了使用了ss段,因为var在这里是局部变量使用了ss段寄存器

image-20210321143632194

将var设置成全局变量测试一下,就会报错

image-20210321143903357

那么我们试一下构造limit不是0xffffffff 把段描述符改成:00c0f700`00000001就可以让程序执行了:

image-20210321144354835

可以执行成功

image-20210321144509603

段描述符TYPE,还有一种属性是系统的:

image-20210321144626755

(这个后面会讲,写不了笔记啦,大概就是什么值就构造了什么门那种)

D/B位详解

在代码段的时候,D/B位叫做D,数据段的时候叫做B

CS段的影响:

D = 1 采用32位的寻址方式,D = 0 采用16位的寻址方式,32位的时候,用前缀67(硬编码)改变寻址方式

SS段的影响:

D = 1 隐式堆栈访问指令(push,call,pop)使用了32位堆栈指针寄存器ESP D = 0 是默认使用了16位堆栈指针寄存器SP

向下拓展的数据段:

B = 1(4GB)

B = 0(64KB)

image-20210322111205616

如果base > limit 那么就是 base ~ base + limit (向上拓展) base + limit ~ B=(1/0) (向下拓展)

如果是base < limit 那么就是 base ~ limit (向上拓展)limit ~ B=(1/0) (向下拓展)

这里介绍了个指令:dg

image-20210322112359216

段权限检查

火哥这里讲的没太懂,但是自己总结了一下

image-20210323103845641

这里火哥的图自己总结一下,首先指令mov ds,ax要检车cs代码段的cpl是否权限大于等于cs的dpl,如果大于dpl证明这句话可以执行,然后判断ds = ax,如果ds == ax啥也不做,但是如果数据段的rpl权限大于等于dpl,且cpl权限大于等于dpl,这样就可以访问数据区域,就可以赋值成功,刷新缓存

总结权限:cpl(代码段) >= dpl(代码段)&& cpl(代码段) >= dpl (数据段)&& rpl (数据段)>= dpl(数据段)

写一下R0层的和R3层的测试一下

vs修改一下配置

image-20210323110458498

image-20210323110513226

#include <ntddk.h>
#include <ntstatus.h>


VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
	UNREFERENCED_PARAMETER(pDriver);
	KdPrint(("驱动卸载\n"));
}

int g_value = 0;

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
	UNREFERENCED_PARAMETER(pReg);
	__asm
	{
		int 3;
		mov ax, 0x4B;
		mov ds, ax;
		mov ebx, 0x64;
		mov dword ptr ds : [g_value] , ebx;
		mov ax, 0x20;
		mov ds, ax;

	}
	KdPrint(("%X\n", g_value));
	pDriver->DriverUnload = DriverUnload;
	return STATUS_SUCCESS;
}

image-20210323111431405

如果我们把 mov ax, 0x4B; 我们构造成0x48,还构造成原来的段的样子就可以通过了不会蓝屏了

image-20210323112208912

测试cpl

image-20210323113619437

r @寄存器 = 0 //就可以修改寄存器的值

跳转流程

这节课主要介绍了跳转的流程和一致代码段好像没啥用 - -

JMP 0x20:0x12345678流程:

RPL >= DPL & CPL >= DPL (0x20的DPL)然后查找GDT表,CS = 0x20,代码段 base+0x12345678

主要做实验来看一致代码段是否有用:

因为书上说的是一致代码段可以R3去调用R0的函数,但是实际上这个中间要通过很多的过程并不是直接可以调用的

#include <ntddk.h>


VOID DriverUpload(PDRIVER_OBJECT pDriver)
{
	DbgPrint("upload sucessful!\n");
}


__declspec(naked) void test()
{
	__asm
	{
		int 3;
		ret;
	}
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
	DbgPrint("welcome!\n");
	DbgPrint("%x\n", test);
	return STATUS_SUCCESS;
}

我们构建一致代码段,看是否在r3 可否调用0环的代码

image-20210323151153970

image-20210323151557951

image-20210323151708765

发现并不可以

image-20210323151740670

一般分页模式后三位都是0的情况下是 10 10 12 分页 不是0的情况下是 2 9 9 12分页

image-20210323153355606

地址是f78de040

0xf78de040
11				3*8
1 1011 1100		1BC * 8			
0 1101 1110		DE * 8
0000 0100 0000	40

看一下PDPTE PDE PTE 物理页

image-20210323154116309

修改完:

image-20210324110227704

这里发现了个问题!群里问了一下!

image-20210323161126524

image-20210323161141214

小路哥哥这边说是cow机制,CopyOnWrite,写时复制

改了内存属性,系统会在你修改内存的时候,帮你复制然后拷贝一个新的物理页面出来,那时候pte的内容就不一样了

测试:

// 1234.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

int var = 0;

int main(int argc, char* argv[])
{
	__asm
	{
		mov ax,0x48;
		mov ds,ax;
		mov ebx , 0x68;
		mov dword ptr ds:[var],ebx;
	}
	printf("%x\n",var);
	return 0;
}

!vtop cr3 va

img

发触发了写时复制的情况

image-20210323170116270

段跳转实验-一致代码段分析

这里主要是实验了 一致代码段的问题,这里设置成一致代码段的条件之外,还需要把PDE,PTE的权限U/S改成1,这样就可以在r3层去访问r0的函数代码

实验代码:

#include <stdio.h>
#include <stdlib.h>


int gupdate_value = 0;
int main(int argc,char * argv[])
{
	char buf[]={0x0,0,0,0,0x90,0};
	unsigned int value = 0;
	*((unsigned int *) &buf[0])=0xF8AD1060;
	printf("%X\n",&gupdate_value);
	system("pause");
	__asm
	{
		mov eax,0xF8AD1060;
		mov eax,[eax];
		mov value,eax;
		call fword ptr ds:[buf]
	}
	printf("%X\n",gupdate_value);
	printf("%X\n",value);
	system("pause");
	return 0;
}
#include <ntddk.h>

VOID DriverUpload(PDRIVER_OBJECT pDriver)
{
	KdPrint(("卸载完成\n"));
}

int g_value = 10;

void  __declspec(naked) test()
{
	__asm
	{
		int 3;
		mov eax, 0x429C78; 
		mov ebx, 0x100;
		mov [eax], ebx;
		
		retf;

	}
	
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
	KdPrint(("welcome to driver world\n"));
	KdPrint(("%X\n", test));
	pDriver->DriverUnload = DriverUpload;
	return STATUS_SUCCESS;
}

image-20210324135640141

image-20210324135621607

长调用与短调用

主要介绍了jmp call retf iretd四个指令

image-20210326133430256

可以看到cpl = 3 ,我构建的dpl = 0 rpl = 0,这种情况下应该是跳不过去的

image-20210326133528175

修改dpl = 3即可,主要看一下far call的堆栈和寄存器的变化,把cpl权限修改成了3环的权限,堆栈的变化,把上次的eip和cs寄存器的值压入堆栈,但是我这么返回只会返回eip,因为ret只返回eip导致cs寄存器还在栈中,堆栈不平衡

image-20210326133702470

image-20210326134026963

我们试一下retf,是没有问题的

image-20210326134234470

我们构造一下调用门,来测试一下,retf和堆栈的变化

压入了四个值:eip cs esp ss

image-20210326140102317

retf会检查段选择子看看是不是调用了门,像这些ss cs esp这些值都是从TSS中来的,可以看到返回结果

image-20210326140507508

调用门

调用门构建:

image-20210330115023495

offset in Segment是我们想要去的便宜地址,Param.Count为参数格式,Segment Selector指向的选择子,地址就是我们选择子指向的base加上我们调用的offset,segment selector作为我们call过去的一个cs段的值

image-20210330114939127

调用门格式指令:CALL CS:EIP (EIP是废弃的)根据CS的值 查GDT表,找到对应的段描述符,这个描述符是一个调用门,在调用门描述符中存储另一个代码段段的选择子,选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址

调用门总结:

当通过门,权限不变的时候,只会push两个值:cs 返回地址,新的cs的值由调用门来决定

当通过门,权限改变的时候,会push四个值:ss esp cs 返回地址,新的cs的值由调用门决定,新的ss和esp由tss提供

通过门调用时,要执行哪行代码由调用门来决定,但使用retf返回时,由堆栈中压入的值来决定,进门的只能按指定的路线走,出门的时候只要改变堆栈的值就可以想去哪就去哪

也可以进了门后再建立个门出去

不管是进0环也好还是进3环也好都要注意fs的切换

在内核态FS=0x30, 在用户态FS=0x3B,我们现在看一下用户态的FS

image-20210330134948810

得到的FS 指向的虚拟内存地址,这个地址正好是线程的TEB的地址,实际上,KPCR里有字段直接指向了线程的TEB

因为tss中fs不会改变,所以到了R0层返回到R3的时候是0x30所以0环的权限在三环不可用,所以fs会变成0,gdt[0] = 0那么线程的结构就没有了,就会导致报错,所以要push fs,pop fs的操作避免错误的发生

有参数的调用结构:

返回地址
cs
参数1
参数2
参数3
参数4
esp
ss

image-20210330140602693

要保持堆栈平衡

image-20210330141947736

经过我的多次实验,总结的dpl rpl cpl的权限为,构建的调用门的dpl需要权限小于等于cpl的权限,门的选择子的rpl权限要大于等于门的dpl的权限,指向选择子的那个rpl要大于等于构建该段的dpl

一般在选择调用门的时候,都选择8003f090以后的,因为前面的大部分在多核中容易被操作系统自己修改导致构建的调用们不稳定

中断门

中断门,CPU执行如下的指令:INT N,查询的是另外一张表,这张表叫IDT表

image-20210331110211164

这里面的D代表了default默认是1

image-20210331110714430

这条INTR是可屏蔽终端

image-20210331112805319

IF 中断标志位(Interrupt Flag)

决定CPU是否响应外部可屏蔽中断请求。IF为1时,CPU允许响应外部的可屏蔽中断请求

试验一下中断门:

// i1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"

__declspec(naked) void test()
{
	__asm
	{
		int 3;
		iretd;
	}
}

int main(int argc, char* argv[])
{
	printf("%X\n",test); 
	system("pause");
	__asm
	{
		push fs;
		int 32;
		pop fs;
	}
	return 0;
}

可以看到使用中断门压入栈的 返回地址 cs eflag esp ss

image-20210401110103263

这里并不能用iret因为iret是16位的,但是像OD这类的工具他会将16位的iret自带扩展为iretd指令

image-20210401110316913

这里可以知道我们的iretd返回了栈中的5个值,到返回地址 cs eflag esp ss,但是retf 返回的是返回地址 cs(调用们的时候会返回 esp ss)(加了参数返回参数)

测试调用门使用iretd,中断门使用retf(不崩溃):

// i1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"

__declspec(naked) void test()
{
	__asm
	{
		retf 0x4;
	}
}

int main(int argc, char* argv[])
{
	printf("%X\n",test);
	system("pause");
	__asm
	{
		pushad;
		pushfd;
		push fs;
		int 32;
		sub esp,0x4;
		pop fs;
		popfd;
		popad;
	}
	return 0;
}
// i1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"

__declspec(naked) void test()
{
	__asm
	{
		mov eax,dword ptr ds:[esp+0x8];
		mov ebx,dword ptr ds:[esp+0xC];
		mov ecx,dword ptr ds:[eax];
		mov dword ptr ds:[esp+0x8],ecx;
		mov dword ptr ds:[esp+0xC],eax;
		mov dword ptr ds:[esp+0x10],ebx;
		iretd;
	}
}

int main(int argc, char* argv[])
{
	char buf[6] = {0,0,0,0,0x48,0};
	printf("%X\n",test);
	system("pause");
	__asm
	{	pushad;
		pushfd;
		call fword ptr ds:[buf];
		popfd;
		popad;
	}
	return 0;
}

陷阱门

陷阱门和中断门差不读,只有type域一位不同,功能差别在IF位,可屏蔽中断

image-20210404231117276

这里有一个知识点,就是retf number这个的时候,在r0层pop参数多少个的时候,在r3层也会做add的操作,举例子:

// i1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"

__declspec(naked) void test()
{
	__asm
	{
		retf 0x8;
	}
}

int main(int argc, char* argv[])
{
	char buf[] ={0,0,0,0,0x48,0};
	printf("%X\n",test);
	system("pause");
	__asm
	{
		pushad;
		pushfd;
		push fs;
		push 1;
		push 2;
		call fword ptr ds:[buf];
		pop fs;
		popfd;
		popad;
	}
	return 0;
}

测试的时候发现,我们的esp上面的压入的参数,被我们的r0层的retf的0x8的规则在r3层的参数pop了出去

image-20210404232112257

地址nextesp:0x12ff04 在压入参数之前是repesp,数值正好是0x8,所以可以证明在r0层进行retf number的操作,r3层也会进行add的操作

image-20210404232648009

image-20210404232756774

中断门和陷阱门的区别的代码:

// i1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "stdio.h"
#include "stdlib.h"

__declspec(naked) void test()
{
	__asm
	{
		int 3;
		iretd;
	}
}

int main(int argc, char* argv[])
{
	printf("%X\n",test);
	system("pause");
	__asm
	{
		pushad;
		pushfd;
		push fs;
		int 32;
		pop fs;
		popfd;
		popad;
	}
	system("pause");
	return 0;
}

中断门:

image-20210404233936491

image-20210404234653550

陷阱门:

image-20210404235138900

可以看到if位不一样,在中断门的时候if位会置0,然后再置1,但是陷阱门的时候不会改变if的值,if位变成0的时候,不可以被外部的中断打断,会执行完这个流程,所以陷阱门的时候会有可能在执行的过程中被打断

实验内容:使用调用门调用0环的api

#include <stdio.h>
#include <stdlib.h>

typedef struct _STRING {
    unsigned short  Length;
    unsigned short MaximumLength;
    char * Buffer;
} STRING;


typedef void ( __stdcall * RtlInitAnsiString)(STRING * DestinationString,const char * SourceString);

RtlInitAnsiString initStrFunction = (RtlInitAnsiString)0x804db265;
STRING str;
const char * bufstr="123456789";

__declspec(naked) test(int x,int y)
{
	__asm
	{
		int 3;
		pushad;
		pushfd;
		push fs;
		mov ax,0x30;
		mov fs,ax
		push bufstr;
		lea eax,str;
		push eax;
		
		call initStrFunction;
		pop fs;
		popfd;
		popad;
		retf 0x8;
	}
}

int main(int argc,char * argv[])
{
	int preesp = 0;
	int nextesp = 0;
	char buf[]={0,0,0,0,0x48,0};
	printf("%X\n",test);
	getchar();
	__asm
	{

	
		push fs;
		push 1;
		push 2;
		mov preesp,esp;
		call fword ptr buf;
		mov nextesp,esp;
		pop fs;
		
	}

	getchar();	
	return 0;
}

任务段

image-20210406154326762

TSS的大小最少是104个字节,他是一个内存,里面存放了这样的结构

image-20210406161138040

TR寄存器来自于GDT表中的TSS段描述符 LTR指令:将TSS段描述符加载至TR寄存器中 STR指令:读取TR寄存器

TSS是最小104字节的内存,在切换任务、切换寄存器的时候,通过TR寄存器找到,TR寄存器的值是从GDT表中查到的,查到的哪一项称为TSS段描述符,一个任务对应一个TSS段描述符

用call的时候eflag 中的NT(第14位)位会修改,Nesting Task 嵌套任务,指示当前任务是否嵌套于其它任务运行

实验:

call代码:

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
char trs[6]={0};
char gdts[6]={0};



void __declspec(naked)  test()
{
        __asm
        {
                //jmp fword ptr trs;
                iretd;
        }
}

int main(int argc,char * argv[])
{

        char stack[100]={0};
        DWORD cr3=0;
        printf("cr3:");
        scanf("%X",&cr3);

        DWORD tss[0x68]={
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                cr3,
                (DWORD)test,
                0,
                0,
                0,
                0,
                0,
                ((DWORD)stack) - 100,
                0,
                0,
                0,
                0x23,
                0x08,
                0x10,
                0x23,
                0x30,
                0,
                0,
                0x20ac0000

        };

        WORD rs=0;
        _asm
        {
                sgdt gdts;
                str ax;
                mov rs,ax;
        }
        *(WORD*)&trs[4]=rs;
        char buf[6]={0,0,0,0,0x48,0};
        __asm
        {
                call fword ptr buf;
        }

        printf("zsaddfsafdsa\n");
        return 0;
}

jmp代码:

#include <stdio.h>
#include <stlib.h>
char trs[6]={0};
char gdts[6]={0};

void __declspec(naked)  test()
{
        __asm
        {
                jmp fword ptr trs;
                //iretd;
        }
}

int main(int argc,char * argv[])
{

        char stack[100]={0};
        DWORD cr3=0;
        printf("cr3:");
        scanf("%X",&cr3);

        DWORD tss[0x68]={
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                0x0,
                cr3,
                (DWORD)test,
                0,
                0,
                0,
                0,
                0,
                ((DWORD)stack) - 100,
                0,
                0,
                0,
                0x23,
                0x08,
                0x10,
                0x23,
                0x30,
                0,
                0,
                0x20ac0000

        };

        WORD rs=0;
        _asm
        {
                sgdt gdts;
                str ax;
                mov rs,ax;
        }
        *(WORD*)&trs[4]=rs;
        char buf[6]={0,0,0,0,0x48,0};
        __asm
        {
                jmp fword ptr buf;
        }

        printf("zsaddfsafdsa\n");
        return 0;
}

CR3寄存器简述

10 10 12分页的情况下

CR3是一个寄存器,每个核只有一个。他是唯一一个存储着物理地址的寄存器。这个物理地址指向第一级目录(PDE,共4096字节)

通过拆分后的第一个10, ×4后,可以找到PDE中存储的一个地址(PTE)

通过拆分后的第二个10, ×4后, 可以找到PTE中存储的一个地址(物理页)

通过拆分后的第三个12(偏移)加上物理页首地址,得到真正的物理地址

实践转换物理地址:

WIN7默认是2 9 9 12分页使用软件EasyBCD来修改当前的分页模式

image-20211007214716485

在记事本中输入字符串hello world,然后用CE去找到当前的虚拟地址的位置

image-20211007215524804

地址为0x00BF0B4C-> 转换成10 10 12

0000000010 1111110000 101101001100 -> 0x8 0xFC0 0xB4C

!vtop查看一下对应的物理内存地址

image-20211007215455765

下断找一下当前的CR3的寄存器的位置

image-20211007215651898

DirBase = 0x319d1000 直接!dt 查看一下

image-20211007215740603

PTT的位置是0x33484000再加上0xFC0是物理页的首地址

image-20211007215808109

对应的物理地址的位置 = 0x3343a000 + 0xB4C

image-20211007215843033

转换图

image-20211007215928507

MmIsAddressValid逆向分析

10-10-12分页模式下,内核文件为ntoskrnl.exe

2-9-9-12分页模式下,内核文件为ntkrnlpa.exe

image-20211007224053077

公式:

每个PDE:C0300000+4×PDI
每个PTE:C0000000+1000×PDI+4×PTI