海哥内核学习札记(保护模式)
滴水自带的编程达人虚拟机好像被修改过有点坑,符号下载下来会有问题,建议自己重新搞一个
0x00 保护模式
0 段的机制
0.1 段寄存器
段寄存器一共96位(16 + 80),读取段寄存器时只能读取16位 eg: mov ax, es
struct SegMent
{
WORD Selector;
WORD Attribute;
DWORD Limit;
DWORD Base;
}
Q&A:
1)段寄存器只能看见16位,如何证明有96位?
2)写段寄存器时,只给了16位,剩下的80位填什么?
0.2 段描述符
GS寄存器 Windows操作系统没有使用,所以一进入R0就会被清空,所以单步调试的时候就无法被赋值。
0.3 段描述符的属性 (8 - 23 字节)
P位:高4字节15位 1 段描述符有效 0 段描述符无效
G位:高4字节23位 0 Limit单位是字节即Limit最大值为000F FFFF 1 Limit单位是4字节即最大值为FFFF FFFF
以ES段寄存器(23)为例:
S位:高4字节12位 1 代码或数据段描述符 0 系统段描述符
TYPE域:高4字节8 - 11位 (不同段TYPE域意义不同)
S位为 1 时 第5位为9或f时为代码段或数据段,第6位大于等于8则为代码段小于8为数据段
数据段E位就是内存取反,代码段C位就是一致代码段
S位为 0 时 系统段描述符
D/B位:高4字节第22位
数据段权限检查,CS段选择子的后两位叫当前特权级别(CPL)(判断程序在0环还是3环)
数据段检查 CPL <= DPL & RPL <= DPL才可访问 值越小等级越高 DPL一般要么全0要么全1
0.4 代码的跨段执行(长跳转)
JMP 0x20:0x004183D7 !!无法改变CPL权限!!
1.拆分段选择子
2.查表得到段描述符(代码段,调用门,TSS任务段,任务门)
3.权限检查
4.加载段描述符
5.代码执行 CS.Base + Offset -> Eip
0.5 长调用与短调用的堆栈图
- CALL : [esp - 4] = 返回地址, jmp xxx
- RET : jmp [esp], esp+4
- CALL CS:EIP : PUSH CS ,PUSH 返回地址 (跨段不提权)
- RETF : POP EIP, POP CS
长调用(跨段并提权),堆栈会发生变化所以ESP和SS都要压栈,SS和ESP从TSS段来
长调用实验(调用门),除了在0环堆栈中保存的四个寄存器,其余在内核中被修改的如果没有进行保存则都会被保留。
//eq ffffffff8003f048 0040EC00`00081020 #include <windows.h> #include <stdio.h> BYTE GDT[6] = {0}; DWORD dwH2GValue; DWORD GDT_ADDR; WORD GDT_LIMIT; __declspec(naked) void getData() { __asm { int 3 pushad pushfd mov eax,0x8003f048 mov ebx,[eax] mov dwH2GValue,ebx sgdt GDT popfd popad retf } } int main(int argc, char* argv[]) { char cs_eip[6] = {0, 0, 0, 0, 0x48, 0}; // 这里的eip被废弃 __asm { call fword ptr [cs_eip] } GDT_LIMIT = *(PWORD)(&GDT[0]); GDT_ADDR = *(PDWORD)(&GDT[2]); printf("%x %x %x\n",dwH2GValue,GDT_ADDR,GDT_LIMIT);//可能会报错,FS段寄存器被清空 getchar(); return 0; }
0.6 调用门(CALL FAR)
- 执行流程
- 门描述符 Type - 1100 调用门 DPL = CPL
带参的调用门
//eq ffffffff8003f048 0040EC03`00081020 #include <windows.h> #include <stdio.h> int g_a, g_b, g_c; __declspec(naked) void getParam(int a, int b, int c) { __asm { int 3 pushad pushfd // .- 8 个通用寄存器和标志寄存器占用大小 36字节(pushad/pushfd) // | .- cs 和 返回地址 占用大小 // | | mov eax, [esp+0x24+0x08+0x08] // 参数 a mov g_a, eax mov eax, [esp+0x24+0x08+0x04] // 参数 b mov g_b, eax mov eax, [esp+0x24+0x08+0x00] // 参数 c mov g_c, eax popfd popad retf 0x0c } } int main(int argc, char* argv[]) { char cs_eip[6] = {0, 0, 0, 0, 0x48, 0}; __asm { push 1 push 2 push 3 call fword ptr [cs_eip]; } return 0; }
堆栈: 返回地址 - CS - 参数三 - 参数二 - 参数一 - ESP - SS
调用门Windows中不使用
0.7 中断门(INT X)
IDT表中都是系统段描述符:1. 任务门描述符 2. 中断门描述符 3. 陷阱门描述符
INT 3// 3 - index 没有RPL了,段权限检查时只查CPL
老式CPU会使用中断门,新式CPU使用的是快速调用
中断门描述符(不允许传参数)
没有提权时的堆栈:返回地址 - EFLAGS - CS
提权时的堆栈: 返回地址 - CS - EFLAGS - ESP - SS
使用IRET/IRETD指令返回 - IRET16位 IRETD32位
//eq ffffffff8003f500 0040EE00`00081030
#include <windows.h>
#include <stdio.h>
DWORD dwH2GValue;
void __declspec(naked) GetH2GValue()
{
__asm
{
int 3
pushad
pushfd
mov eax,[0x8003f00c]
mov ebx,[eax] // 获取高2G地址的值
mov dwH2GValue,ebx
popfd
popad
iretd //iret 会蓝屏
}
}
void PrintH2GValue()
{
printf("%x \n", dwH2GValue);
}
int main(int argc, char* argv[])
{
__asm
{
int 0x20
}
PrintH2GValue();
getchar();
return 0;
}
陷阱门描述符(不允许传参数)
中断门在执行的时候会将IF位(中断允许标志)清零,但陷阱门不会
IF=0 时:程序不再接收可屏蔽中断
可屏蔽中断:比如程序正在运行时,我们通过键盘敲击了锁屏的快捷键,若IF位为1,CPU就能够接收到我们敲击键盘的指令并锁屏
不可屏蔽中断:断电时,电源会向CPU发出一个请求,这个请求叫作不可屏蔽中断,此时不管IF位是否为0,CPU都要去处理这个请求
0.8 任务段
TSS是一块内存,大小为104个字节
CPU如何找到TSS呢?TR段寄存器
TSS段描述符
TYPE = 9,说明该段描述符没有加载到TR寄存器中/TYPE = B,说明该段描述符已经加载到TR寄存器中
LTR 把TSS段描述符的内容加载到TR寄存器里,并修改TYPE位
STR 读取TR的选择子
修改TR寄存器
//eq ffffffff8003f0c0 0000E912`FDCC0068
#include <Windows.h>
#include <stdio.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) func()
{
dwOK = 1;
__asm
{
int 3
mov eax,esp
mov dwESP,eax
mov ax,cs
mov word ptr [dwCS],ax
//回去的代码没写。。。会蓝屏
}
}
int main(int argc, char* argv[])
{
char bu[0x10];
int iCr3;
printf("input CR3:\n");
scanf("%x", &iCr3); //在Windbg中 !process pid 0 获得DirBase
DWORD iTss[0x68] = {
0x00000000, //link
0x00000000, //esp0 //(DWORD)bu
0x00000000, //ss0
0x00000000, //esp1
0x00000000, //ss1
0x00000000, //esp2
0x00000000, //ss2
(DWORD)iCr3, //cr3
0x00401020, //eip 跳过去执行的位置
0x00000000, //eflags
0x00000000, //eax
0x00000000, //ecx
0x00000000, //edx
0x00000000, //ebx
(DWORD)bu, //esp
0x00000000, //ebp
0x00000000, //esi
0x00000000, //edi
0x00000023, //es
0x00000008, //cs 0x0000001B
0x00000010, //ss 0x00000023
0x00000023, //ds
0x00000030, //fs
0x00000000, //gs
0x00000000, //dit
0x20ac0000};
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0xC0;
__asm
{
call fword ptr[buff]
}
return 0;
}
0.9 任务门(INT X)
任务门描述符(IDT表内)
1 页的机制
1.1 10-10-12分页
物理内存按页管理,一页的大小一般是4KB,CR3中存放物理地址
在虚拟机的Boot.ini 中将noexecute 改成 execute 就将分页模式改为10-10-12分页
复制PTE信息修改变量值 x--> 物理页 <--0xf7c
1.2 PDE与PTE
P = 0时四种情况
线性地址的前20位可以判断是否在同一物理页
1.3 PDE-PTE属性(P_R/W_G)
G位(全局位):当PS位 = 0时 PDE的G位无意义
R/W位:0 - 只读 ; 1 - 可读可写
1.4 U/S_PS_A_D位
U/S位(权限位):0 - 特权用户可以访问 ; 1 - 普通用户可以访问
P/S位(PDE特有)
A位(访问位):1 - 被访问过
D位(脏位):1 - 被写过
1.5 页目录表与页表基址
0xC0300000 为页目录表基址,物理内存不连续,线性地址是连续的
0xC0000000 为页表基址,PDT为PTT中一张特殊的表
MmIsAddressValid逆向:
可以查看NT5的源码进行分析,函数所在文件路径为“XPSP1\NT\base\ntos\mm\pagfault.c”
1.6 2-9-9-12分页(PAE分页)
地址位为 12 - 35位
NX(XD)标志位
2-9-9-12分页实验
1.7 TLB
存储在CPU内部
Shadow Walker技术实现内存隐藏 ,原理是数据页表缓存和指令页表缓存的差异,修改指令页表缓存更改指令,比较适用于R0代码(刷新不那么频繁)
TLB实验:
//eq ffffffff8003f048 0040EC00`00081020
#include <windows.h>
#include <stdio.h>
DWORD x;
void __declspec(naked) PageOnNull() {
__asm
{
mov dword ptr ds:[0xc0000000],0x01234867
mov dword ptr ds:[0],0x12345678
//INVLPG dword ptr ds:[0]
//mov eax,cr3
//mov cr3,eax
mov dword ptr ds:[0xc0000000],0x02345867
mov eax,dword ptr ds:[0]
mov x,eax
retf
}
}
int main(int argc, char* argv[]) {
char cs_eip[6] = {0, 0, 0, 0, 0x48, 0};
__asm {
call fword ptr [cs_eip] // 0x48调用门提权
}
printf("%x\n",x);
getchar();
return 0;
}
1.8 中断与异常
中断通常是硬件发起的,异常通常是执行指令检测到错误
中断分为可屏蔽中断(INTR)和不可屏蔽中断(NMI),中断本质就是改变了CPU执行路线
1.9 控制寄存器
Cr1为保留位,Cr3为页目录表基址