windows2-保护机制

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

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

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

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

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

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

段检测

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;
}

该程序结果是1b,所以cs的值是1b

windbg的一些指令

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

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

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

段选择符

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

1B:0000 0000 0001 1 0 11

0011 查找表 请求权限

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

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

段描述符

也就是前面的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

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,所以就会出现问题