保护机制、段、页

保护机制

首先铺垫,在前面学过pe文件格式,且写过反射型注入,有一定知识前置

我们知道,一个软件在运行的时候,会分配一个内存中的基地址(虚拟地址),很好,那么明明是在内存中实体存在的为什么叫虚拟地址?我们还知道,地址是线性的0 1 2 …… 0xffffffffh,线性排布,那么问题来了,我的程序加载到内存中,假如说是0x87654321,欸?此时我有一个指令是mov dword ptr [0] , 1,那么会把内存开始地方的东西改动掉,但是内存最开始的东西肯定是很内核的东西,什么对程序的调用啊,什么的之前在反调试有接触过

自然是不能让你随便动他的,那么怎么才能很安全的做到呢

目前我学到的是分段分页,相当于在原本的0x55555555h的地方一刀斩断,后面又开始0x0h(应该如此,后续回来补充)

嗯不是这样的,是分成一段一段然后通过代码段,数据段来区分,这是段保护机制

页保护估计就是进程线程的吧。。。

windbg的一些指令

r 查看一些寄存器(r eax | r al | r gdtr)

d 查看地址(byte(db) | word(dw) | … ) | (L limit 限制要看多长) | (s 竖着看)

e 写内存(用法和d一致)

idt和gdt

idt和gdt结构前两字节是limit大小,后四字节是地址

idt和gdt的指令

sidt sgdt 用例就是把gdt表和idt表加载出来

lidt lgdt 赋值给他

门符框架

很好前面虽然很乱但是捋一下就清晰了,本篇文章只用来构造框架,具体的内容将在其他文章细记

门符

注意注意,门或符GDT表或IDT中每个单元的名称!!!

每个单元都有-P位 S位 Type位

P-决定该描述符有效

S-决定该描述符属于什么类型

T-决定改描述符更具体类型

声明1:门和xx段描述符感觉差不多一个意思

s=1-段描述符

s=1是段描述符,这时候的T遵循的图就是

t<8-数据段 GDT
t>7-代码段 GDT

数据段或代码段,同时无论是数据段还是代码段都解析的都是如下图

s=0-系统描述符

s=0是系统描述符,这时候的T遵循的图就是

t=c-调用门 GDT

调用门遵循下图

t=6 -中断门 IDT

int 3 int 2 int 1…遵循下图

t=5-任务门 IDT

段描述符

段选择符

长度一字节,比如CS里的1B值就是段选择符(0x18是打印cs值会打印出来的)

1B:0000 0000 0001 1 0 11

0011 查找表 请求权限

3,在表中的第3位 0=GDT Global Decsrctor Table(全局描述表)

1=LDT Local Decsrctor Table(本地描述表)

CS 1B 代码段

SS 23 栈段(栈,局部变量)

DS 23 数据段(堆,全局变量)

ES 23 扩展段(可以还原ds)

FS 上下文环境段 R3代表TEB R0代码是KPCR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// study02.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<windows.h>

int val=0x100;
int val2=0x1;

int _tmain(int argc, _TCHAR* argv[])
{

__asm
{
mov ax,cs;
mov dword ptr ds:[val2],eax;
};
printf("%x\n",val2);
system("pause");
return 0;
}

段描述符

也就是前面的GDT和LDT表的内容

举个例子:

kd> r gdtr

gdtr=80b98800

通过查找指令找到gdt表,gdt表是按照1字也就是两字节的形式为最小单位,所以查找的时候要用dq

kd> dq 80b98800

80b98800 0000000000000000 00cf9b000000ffff

80b98810 00cf93000000ffff 00cffb000000ffff

80b98820 00cff3000000ffff 80008bb98c0020ab

80b98830 804093b9b0004fff 0040f30000000fff

80b98840 0000f2000400ffff 0000000000000000

80b98850 800089b9ad200067 800089b9acb00067

80b98860 0000000000000000 0000000000000000

80b98870 800092b9880003ff 0000000000000000

按照上面索引第三个(注意,从0开始数) 00cffb00`0000ffff,和段选择符一样,要拆分分析

对照上表,高字节部分和低字节部分

31 24 19 16 12 8 0

1
00  c       f   f       b   00

1100 1111

h: b gb0a l hd s t b

0000 ffff

l: b l

limit:f+ffff=0xfffff 段限制大小(sizeof(cs)):(h&l)+(l&l)

base:00+00+0000 基址大小(base):(h&b)+(h&b)+(l&b)

type:(h&t)b tpye作用见下表

s:(h&s)1

DPL:(h&d)3

p:(h&p)1 p用来证明该段是否有效,1有效,0无效

avl:(h&a)0

0:(h&0)0

D/B:(h&b)1

G:(h&g)1 若为1,则以页为单位(影响的是段限制的单位)(一个页4096字节) 转换为hex->0x1000

故真正限制大小:(limit+1)*(g) 按照上面的计算也就是(0xfffff+1)*0x1000=0x1 0000 0000,范围就是0-0xf ffff fff 也就是4个g

type作用:

练习代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// study02.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<windows.h>

int val=0x100;

int _tmain(int argc, _TCHAR* argv[])
{
int val2=0x1;
//段保护
__asm{
mov ax,cs;
mov ds,ax;
mov eax,dword ptr ds:[val]; //ds:[val]就是全局变量val
//mov dword ptr ds:[val2],eax;//直接这样会报错,因为cs给了ds,cs段是可读可执行不可写,这里相当于写入所以不可以
//恢复ds环境
mov ax,es;
mov ds,ax;
//可以赋值
mov [val2],eax;
}
printf("val2=%x\n",val2);
//0x100

//查看段选择符
__asm
{
mov ax,cs;
mov [val2],eax;
};
printf("cs=0x%x\n",val2);
//0x1b

//对base的探测
__asm
{
mov ax,0x4b; //这里用windbg将对应gdt表动过,也就是让base+1了
mov ds,ax;
mov eax,dword ptr ds:[val];
mov dword ptr ss:[val2],eax;
//mov cx,es;
//mov ds,cx;
}
printf("0x%x\n",val2);
//正常来说这里是100,但是如果改变base基地址,那么赋值的时候eax给的就是base+offset val2+1,,没有改变val2地址的值,后面恢复了ds就正常了
//同理,将前面的恢复环境注释掉,后面再恢复
__asm
{
mov cx,es;
mov ds,cx;
}
//若是这样子,应该是没有变化的

////对段长度的探测
__asm
{
//limit
//mov eax,fs:[0x1000-4];//这里如果是0x1000-4 - 0x1000之内的都会出错,因为超出大小了
//mov val2,eax;
//p
//在gdt表中将p改成0就可以无效了,这里正常运行上面的那个代码就会报错
}
printf("fs:[0]=0x%x\n",val2);

system("pause");
return 0;
}

再次总结一下,cs,ds这些段如果是cs:[x],那么会调用代码段中的x地址,但是如果只是cs,也就是cs中的值,并不是基地址,而是段选择符,也就是可以拆解的,有时候就会去对照gdt表,然后查看gdt里的东西

d/b

话解上题,通过windbg更改端段描述符cs,ds可以做到全局改变,d/b的作用是用来规定操作符大小的,比如正常push指令push的是一字节,但是如果d/b位改变,这里有可能只push一字等,也就是本来的push xx xx xx xx就会变成push xx xx

个人感觉就像是改变这个电脑是32位还是16位还是64位,存在用处也很明显,就是可以兼容低版本(这里注意改变的是段描述符里的d/b也就是说,该段相关改变,比如ss(栈)改了的话,call指令就会有问题,因为call相当于pop jmp,但是操作字节变少了,原本是call 0x12345678,pop出来就只有0x1234,jmp 0x1234,所以就会出现问题

win系统内部权限分化,R0和R3是内核层和应用层,这个倒是很好理解,去调用

判别当前代码是当前层的办法就是CS和SS

这里阐述一个观点,段的存在是划分硬件资源的,内存中是线性排列的一群1和0,是段来定位划分每一块的作用,最后cs,ss,ds诸如此类的段就是对这个划分块的描述

DPL: Descriptor Privilege Level 描述符特权级

CPL: Current Privilege Level 当前特权级 CS段描述符的DPL

RPL: Request Privilege Level 请求者特权级 CS DS SS,在段描述符的后三位

这里卡了有点久,起初我不是很理解gpt所说的

段类型 DPL 规则
代码段 CPL ≤ DPL
数据段 max(CPL, RPL) ≤ DPL
栈段(SS) RPL=CPL = DPL

上述表格,但是结合上图就会发现,0才是内核层,假如现在代码是内核代码,那么这段代码的DPL就是0,如果你自己写了一段代码,那么大概率在R3,应用层,此时你的代码描述符cs里的CPL就会是3(DPL应该也是3)

此时如果你要调用内核层的代码,则会访问失败(但我不知道这个怎么调用)

以此类推,后两个也不难理解

提权跳转-符

嗯后面的我好像也懂了,补刚刚的坑,那么众所周知cs在应用层是23,那么

call 23h:0040100h就是跳转到当前程序的0x401000地址,并且跳跃过去的段权限对应是3,那么在gdt表中把一个无用位置改成CPL值为2的cs,然后手动call 48h:4010000h,48就是改变的gdt表导向,也就是call提权

那么跳转指令还有jmp\ret\retf等等,这些的跳转可以不可以提权呢

jmp 在调用门 只能同权限跳转

retf 只能同权限或向低权限跳转

call 同权限或提权

本来换下一节课了,听到调用门一愣,调用门似乎他没说,我不清楚为什么感觉容易漏东西,所以自己补了一下,网上找了一篇文章讲的还不错

我一看这个调用门描述符,和段描述符十分相似

所以段又描述符,调用门也有?后面看来好像是这样的,而且构造和段描述符差不多

我去,,,误会了我没看到这节课,好吧回来接着学

那么前面说的和提权跳转差不多,当然也有可能是这两个本来就是一样的,所以我决定好好复现这个程序

那么首先我们要获得cs的值,并且分解分析

1
2
3
4
5
6
7
8
9
int _tmain(int argc, _TCHAR* argv[])
{
int a;
__asm{
mov ax,cs;
mov a,eax;
}
printf("%x",a);
}

这里获取的值是1b ->0001 1011,也就是3的位置

r gdtr获取地址-80b98800

dp 80b98800后发现80b98848后的位置适合放更改DPL的位置

原值是0000ffff 00cffb00-f-1111-DPL-11

需要改成DPL-00-1001-9-0000ffff 00cf9b00

我突然发现有现成的1,其实可以改成0000 1011- b

那么接下来要获得函数的地址-0x0401000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdafx.h"

void __declspec(naked) test(){
__asm{
retf;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\n",test);//0x0401000

char bufcode[6]={ 0x00, 0x0, 0x0, 0, 0x48, 0};
*(int *)&bufcode[0]=(int )test;
__asm{
//push addr;
//jmp fword ptr bufcode;
jmp far bufcode;
addr:
}
return 0;
}

int 3

哈哈中断一下,因为确实这里中断了好几次,我当时自己写的代码一直都不能跑,会中断,我复现课里的内容也还是不可以,最后发现他改的是 00cffb00`0000ffff

欸?这不是没改嘛,对就是没改,那不对呀,没改怎么提权了,哎这里真的想了很久,因为RPL在段描述符中,他用的是48,0100 1000,RPL是0,R0层,哦对他也没说这个实验可以提权,而是申请对吧

。。所以这里就是这个啊。。。哎不记了接着看,反正也懂了,始终切记DPL在gdt里,RPL在段描述符里

系统段描述符

这里的门是系统段的门,也算是描述符,就像是指引路的传送门,比如调用门,门后的就是代码段,中断门,门后的是处理中断的代码段,任务门门后的是TSS任务段

还是看完再测试比较好

调用门

调用门作用是跳转提供基地址等一系列参数的

具体框架在另一篇文章,这里只阐述注意点

1.调用门即地址前寻址的cs ds ss,但是并不是段选择符的那些cs ds ss,call jmp等指令使用到调用门时便遵循调用门解析跳转

2.jmp xx:aaaa 遵循xx的地址跳转

3.提权跳转,先解析需跳转地址,构造出对应的调用门

4.调用门结构中Segment Selector(段选择符)就是前面的符,以符来作为基地址跳转,所以在这里可以选择R0或R3层,进行提权

5.jmp 在调用门 只能同权限跳转 | retf 只能同权限或向低权限跳转 | call 同权限或提权

中断门

中断门作用是int系列或中断指令触发时,决定跳转到哪个地方来解决

具体框架在另一篇文章,这里只阐述注意点

1.中断门即int 1/2/3,当程序运行上述命令时,遵循中断门的跳转,类调用门

2.int xx 遵循xx的地址跳转

3.查看idt表,会发现,0 1 2 3会有对应可分析的中断门,每个单元都是int后对应的中断门,所以自己如果要增加中断门在32元素位置,那么中断门就是int 32

4.中断门的跳转极其类似调用门,所以不多记

陷阱门

陷阱门≈中断门

陷阱门与中断门几乎一致,这里只阐述不同点

1.中断门清除eflag里的VM NT IF TF位

陷阱门不清楚IF位,所以中断门会造成阻塞,需要iretd来返回,或者在进入中断门之前sti,陷阱门不必

任务门

在不同段跳转的时候,比如R0跳R3这种跨段跳转的时候,栈(ss)环境和esp是会改变的,前面实验就能看出

所以任务门担任的就是承接作用 ps:框架越来越完善了

还是老规矩,这里只阐述注意点

基于中断门的hook int 3 hook

通过更改int 3对应cs段基地址然后触发int 3hook到自己的函数

101012

这里火哥的课就又开始”架空”起来了,很多新名词完全没有说过,看一下这篇文章-101012部分,火哥的课不行就跳过这一节,里面也说了”Cr3”

插个眼,这里! process 0 0这个指令还不太懂物理地址懂一半,之前搞项目那会学了一点,但是没太深入,我估计后面学页的时候可以相辅相成

前进一小步,文明一大步

——男厕

页の前章

线性地址对应物理地址

假如我在记事本中写了一串字符”helloworld”,然后用工具比如CE查找到该字符串在内存中的地址00484C58那么可以按照10-10-12的方式解析该虚拟地址的物理地址,所谓101012就是bit位排布,首先把000AE928换成二进制bit位形式0000 0000 0100 1000 0100 1100 0101 1000,然后划分

10 00 0000 0001 1 页目录 单位1*4byte PDE

10 00 1000 0100 84 页表项 单位1*4byte PTE

12 1100 0101 1000 C58 页内偏移 单位1byte

于是就可以找物理地址了,于是就弥补了前文的无知,cr3是什么?cr3就是改进程(书)地址的寄存器,代表了该书,用! process 0 0列举所有cr3的值找到notepad.exe,得到物理基地址03d75000

! dd 03d75000来获取目录物理地址的值,目录中存放的是指针,也不难想到指针指向的地方就是书本内容的地方,比如这个要找的就是! dd 03d75000+14->3cbc7867,这里要去掉867,这个是页属性得到3cbc7000,这就是当前页的起始地址,再接着加上844,得到3d25b867,同理3d25b000,于是就可以看里面的内容了

1
2
3
4
5
6
7
8
9
kd> !db 3d25b000+C58
#3d25bc58 68 00 65 00 6c 00 6c 00-6f 00 77 00 6f 00 72 00 h.e.l.l.o.w.o.r.
#3d25bc68 6c 00 64 00 31 00 31 00-00 00 00 00 00 00 00 00 l.d.1.1.........
#3d25bc78 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#3d25bc88 00 00 00 00 00 00 00 00-08 00 00 08 58 82 00 00 ............X...
#3d25bc98 50 87 48 00 70 e9 47 00-06 00 00 06 5a 82 00 00 P.H.p.G.....Z...
#3d25bca8 50 87 48 00 70 e9 47 00-00 00 00 00 04 00 3b 00 P.H.p.G.......;.
#3d25bcb8 9b a1 37 7b 40 82 00 00-90 4a 48 00 68 09 48 00 ..7{@....JH.h.H.
#3d25bcc8 61 00 72 00 73 00 f6 ff-89 a1 36 68 4e 82 00 08 a.r.s.....6hN...

页属性

当直接查看CR3的时候,看到的后三位并不是0,其实这里代表的就是页属性

可以对照该表

P

0代表无效

US

1代表R3可访问,0代表R0才能访问

A

是否被改变过,改变1

G

如果是1的话,是不会刷新缓存值的

没什么可记的,看完课就好

页の终章

挂页

VirtrualAlloc申请地址在赋值前是不会挂物理页的,在memset等操作进行之后才会赋值,又众所周知,0地址是不可赋值的,因为0地址没有挂物理页,那么就会有一种程序,给0地址挂页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// study05.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
int *a = 0;

int * x = (int* )VirtualAlloc(NULL,0x100,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
memset(x,0,0x100);
printf("%x\n",x);
system("pause");
//手动挂页
*a=100;
printf("%x\n",*x);
system("pause");
return 0;
}
这个还是很好玩的

页下

这个感觉不太好记,比较散,应该也不容易忘就不急了,记脑子里

TLB

虚拟页帧 物理页帧 attr属性 次数 PCID

本质是缓存,里面可以临时存储最近用过的联系?然后再次使用的时候可以先碰撞TLB,看看有没有现成的联系,没有再拆