x64内核实验2-段机制的变化
ia-32e模式简介
x86下的段描述符结构图如下
在x86环境下段描述符主要分为3个部分的内容:base、limit、attribute,而到了64位环境下段的限制越来越少,主要体现在base和limit已经不再使用而是直接置空,也就是默认强制为平坦模式
ia-32e模式使用的是64位内核文件,legacy是兼容模式使用的还是32位内核文件,我们这里不关注兼容模式
ia-32e模式下是支持32位和64位的文件的,此模式下就是之前说的强制平坦模式并且取消掉了任务门切换的能力(可能是本来在32位的环境下各个操作系统的实现就没有使用任务切换所以cpu在64位下把这个能力删掉了)
ia-32e模式的启动流程跟32位下差不多也是进来是实模式然后在msr寄存器里设置一些位开启ia-32e模式模式
ia-32e模式下的系统调用跟32位下差不多是通过syscall进入0环
rdmsr 命令可以查看msr寄存器的值
0: kd> rdmsr c0000080
msr[c0000080] = 00000000`00000d01
0: kd> .formats d01
Evaluate expression:
Hex: 00000000`00000d01
Decimal: 3329
Decimal (unsigned) : 3329
Octal: 0000000000000000006401
Binary: 00000000 00000000 00000000 00000000 00000000 00000000 00001101 00000001
Chars: ........
Time: Thu Jan 1 08:55:29 1970
Float: low 4.66492e-042 high 0
Double: 1.64474e-320
可以看到我的模式是d01通过.formats查看具体描述可以看到我当前的二进制位第8位是1(第八位是第九个)说明处于IA-32e模式
这里我们先介绍这些后面慢慢遇到了在说
非系统段描述符的变化
首先看一下白皮书里对64位代码段描述符给出的图
可以看到大部分段描述符的长度依旧是64位,但是基址和界限已经不再存储了,而是默认平坦,留下的大部分内容和32位是一致的,因为图例给出的是代码段所以s位是1type的第一位也是1,要说的是L位现在描述的是32位还是64位段
有区别的地方在于只有s位=1的时候段描述符是64位,s=0的时候段描述符是128位,这是因为在64位环境下我们的数据段和代码段都是强制平坦模式无需关注base和limit而系统段不是这样在系统段里存了段选择子和offset而64位环境下offset也就有64位所以他必须扩到16个字节来存储offset
举例说明就是如果是1就是64位,如果L=1那么push Xax会被翻译为push rax反之则是push eax
下面我们分析几个段描述符看一下
0: kd> r gdtr
gdtr=fffff80580a99fb0
0: kd> dq fffff80580a99fb0
fffff805`80a99fb0 00000000`00000000 00000000`00000000
fffff805`80a99fc0 00209b00`00000000 00409300`00000000
fffff805`80a99fd0 00cffb00`0000ffff 00cff300`0000ffff
fffff805`80a99fe0 0020fb00`00000000 00000000`00000000
fffff805`80a99ff0 80008ba9`80000067 00000000`fffff805
fffff805`80a9a000 0040f300`00003c00 00000000`00000000
fffff805`80a9a010 00000000`00000000 00000000`00000000
fffff805`80a9a020 00000000`00000000 00000000`00000000
00209b00 00000000 g=0 d=0 l=1 avl=0 p=1 dpl=0 s=1 type=1011
说明是一个64位的代码段描述符 dpl=0
00409300 00000000 g=0 d=1 l=0 avl=0 p=1 dpl=0 s=1 type=0011
说明是一个64位数据段描述符 dpl=0
这里要说明一下这个l位只对代码段生效
00cffb00 0000ffff g=1 d=1 l=0 avl=0 p=1 dpl=3 s=1 type=1011
是一个3环32位代码段
00cff300 0000ffff g=1 d=1 l=0 avl=0 p=1 dpl=3 s=1 type=0011
是一个3环32位数据段
00000000 fffff805 80008ba9 80000067这就是一个系统tss段
他的base是fffff805 80a98000系统段我们后面再说
怎么去验证呢,在windbg中可以使用dg命令加上段的偏移就可以显示目标段描述符的详细信息
如下可以看到我们分析的是对的
0: kd> dg 10
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0010 00000000`00000000 00000000`00000000 Code RE Ac 0 Nb By P Lo 0000029b
0: kd> dg 3*8
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0018 00000000`00000000 00000000`00000000 Data RW Ac 0 Bg By P Nl 00000493
系统段的变化
我们这边只介绍调用门和中断门,任务门在白皮书里明确说明x64下不提供此能力,陷阱门没找到对应章节暂不处理
1.调用门
首先看一下调用门在x64下的描述符结构和32位下的对比
这是32位下的
可以看到描述符扩充到了128位低32位跟32位环境下是一模一样的只是不在提供有参的形式了(这也是对32位环境兼容的一种做法)高64位的低32字节用作offset,图中高64位的type暂时没看到有什么用
下面我们做一些实验来熟悉调用门
实验:
- windbg创建自己的调用门,并在应用层调用
首先描述出我们要添加的调用门的属性
selector 0013
offset 00007ff8 85420960
p=1
dpl=11
s=0
type=1100
根据属性构造出调用门(注意windbg的eq是一次写入64位而我们分两次写要小心大小端序的问题)
8542ec0000130960 0000000000007ff8
我们的selector是 0x63
2.TSS段描述符
tss段是变化比较大的一个系统段描述符,下面是白皮书里的段描述结构图,他变化的点不在于这个段描述符的结构变化而是TSS所描述的内存块中存储的内容发生了比较大的变化,也就是TSS这个数据结构变化是比较大的
我们来对比这看一下
这是我们比较熟悉的32位环境下的tss结构,然后我们在看一下64位下的变化
这是64位下的tss结构,可以发现里面不在存储各种各样的寄存器了而是存储的ISTX
这个IST是中断栈表,每一个都存储了64位的esp地址相当于32位下的esp0 esp1 esp2等位置,其中IST0就是esp0,IST3就是esp3,这种变化的原因我想可能有两个,一个是在64位下不再提供任务切换的能力,第二个就是tss这个结构本身在32位操作系统中也就只使用了esp0,所以在64位下干脆就不存储其他的寄存器只提供存储各种各样的esp的能力,在线程切换时候就可以直接替换对应的ist0或者ist3就可以切换当前的栈
大家可能觉得疑惑我们只有0123四种权限级别为啥提供了一大堆的ist都干到ist7了,这是因为64位的中断门也发生了变化中断门会使用这个ist位,等下我们看到中断门的时候再详细描述,现在我们只要记住tss中存储了一大堆的esp基址分别对应了一个和一个的ist变化就可以了
3.中断门
首先我们还是看一下白皮书里对64位中断门的描述
下面是我在网上找的32位下中断门描述符的图
可以看到64位和32位下中断门的变化跟调用门差不多属性位置都没有变然后高64位中多存储了32位的偏移地址,不过注意看一下在64位下多出了一个属性字段IST,我们回想一下刚刚的tss中是不是有一大堆istX,那个就是给这里用的,当我们通过中断进入0环的时候会根据中断门的ist位来切换当前的堆栈
我们来看一下我环境上的idt表
我图中圈出来的就是ist位,它代表了调用这个中断门切换esp时候所使用的tss中对应的ist编号
如下,部分中断就会用一些比较特殊的栈
大家可以自己调用一下中断来看一下栈的变化能够更好的理解中断门在64位环境下的变化
SAMP和SEMP
SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention,管理模式执行保护)的作用分别是禁止内核CPU访问用户空间的数据和执行用户空间的代码,并不会因为你权限高就能访问/执行低权限的资源,你的就是你的,我的就是我的
这里两个检查的控制位分别是cr4的第20位和第21位
当这两位为1的时候意味着0环不可以访问和执行三环地址,运行时就会进行检查
现在我们可以做一些实验了
实验1:
通过驱动在0环读取idt表和idt表并解析出信息打印出来,大家可以试一下不开smap和开启smap的时候我们的代码运行时有什么区别
实验代码:
#include <wdm.h>
//#include <ntifs.h>
#define NTSTRSAFE_LIB
#include <ntstrsafe.h>
#include <intrin.h>
#pragma pack(1)
struct Attribute
{
UINT64 offset1 : 16;
UINT64 p : 1;
UINT64 dpl : 2;
UINT64 s : 1;
UINT64 type : 4;
UINT64 unuse : 6;
UINT64 ist : 2;
UINT64 selector : 16;
UINT64 offset2 : 16;
};
typedef struct _IDT_ENTRY64 {
union hightStruct
{
UINT64 lower;
struct Attribute attribute;
};
UINT64 hight;
}IDT_ENTRY64, * PIDT_ENTRY64;
typedef struct _IDTR
{
UINT16 limit;
UINT64 base;
}IDTR, *PIDTR;
#pragma pack()
VOID Unload(PDRIVER_OBJECT pDriver) {
KdPrint(("unload\r\n"));
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath) {
NTSTATUS status = STATUS_SUCCESS;
pDriver->DriverUnload = Unload;
KdPrint(("start\r\n"));
//关闭smap和smep
UINT64 cr4 = __readcr4();
cr4 &= 0xffffffffffcfffff;
__writecr4(cr4);
IDTR idtr = { 0 };
PIDT_ENTRY64 idtEntryArr = NULL;
__sidt(&idtr);
KdPrint(("idt base:0x%llx, limit:0x%x\r\n",idtr.base ,idtr.limit));
if (idtr.base == NULL && idtr.limit <= 0) {
return STATUS_UNSUCCESSFUL;
}
idtEntryArr = (PIDT_ENTRY64)idtr.base;
DbgBreakPoint();
SIZE_T i = 0;
while (i < (idtr.limit / 16))
{
UINT64 hight = idtEntryArr->hight << 32;
UINT64 lower_1 = (idtEntryArr->lower & 0xffff000000000000) >> 32;
UINT64 lower_2 = (idtEntryArr->lower & 0x000000000000ffff);
UINT64 offset = hight + lower_1 + lower_2;
UINT16 selector = (idtEntryArr->lower & 0x00000000ffff0000) >> 16;
KdPrint(("中断门 index=%llu---offset=0x%llx---selector=0x%x---p=%d---dpl=%d---type=%d---ist=%d\r\n",
i, offset, (UINT32)selector, (UINT32)idtEntryArr->attribute.p,
(UINT32)idtEntryArr->attribute.dpl, (UINT32)idtEntryArr->attribute.type,
(UINT32)idtEntryArr->attribute.ist));
i++;
idtEntryArr++;
}
KdPrint(("end\r\n"));
return status;
}
T32)idtEntryArr->attribute.p,
(UINT32)idtEntryArr->attribute.dpl, (UINT32)idtEntryArr->attribute.type,
(UINT32)idtEntryArr->attribute.ist));
i++;
idtEntryArr++;
}
KdPrint((“end\r\n”));
return status;
}