在介绍今天的内容之前,我们先要知道一些前置的知识
跳过繁琐的介绍,我们单刀直入,介绍一个划时代的CPU 8086
8086
从8086开始,CPU扩展到了16位,地址的位宽扩展到了20位,自此之后我们现在所熟知的计算机结构大体确定了,包括各个寄存器
80386
在8086问世后,唯一有比较大飞跃的就是80386,其首先将位宽扩展到了32位,地址也扩展到了32位,最重要的是,它加入了分页的虚拟内存机制
CPU的两种架构
Risc
定长机器码
Cisc
变长机器码,指令长度由解析的Opcode来决定、
两种架构里面,按理来说应该是不需要解析的Risc性能较好,但是实际中,由于耗电量和CPU振动频率,Cisc的性能更好。
由此,嵌入式设备,手机这种电子设备大多使用的Risc,因为省电,稳定;相反来说,Cisc性能更好,被用于我们的PC机中
段和页
现在回到我们一开始所介绍的划时代的8086上面,在8086中,我们的系统是直接运行在物理地址上的,并且内核和用户并没有严格区分
而我们知道对于汇编来说寻址就是段地址*0x10+段内逻辑地址 = 线性地址
就以最普通的寻址为例
mov ax ,word ptr ds:[0x12345]
这种没有任何隔离的访问方式可以导致在用户层面的代码可以在知道对于段的情况下,直接访问内核的地址,这显然是不利于我们的安全的
这时候我们就可以引入了上面说的80386,它引入了段页的划分
页这种方式就可以轻松的用虚拟地址来将我们的内存隐藏起来避免了我们对物理地址的直接访问
核心就是两点:段是权限的划分,页是物理地址的隐藏
之后我们本节也是会围绕
1.段 如何利用权限的规则
2.页 如何找到真正的物理地址
段寄存器
CS(Code Segment)
- 用途:指向当前正在执行的代码所在的内存段。
- 描述:CS寄存器包含了当前代码段的基地址。处理器使用CS寄存器来确定指令的获取位置。当处理器执行跳转或调用指令时,CS寄存器的值可能会改变。
DS(Data Segment)
- 用途:指向数据所在的内存段。
- 描述:DS寄存器通常用于访问全局变量或静态数据。在许多情况下,编译器会将数据段的选择子加载到DS寄存器中,以便程序能够访问这些数据。
SS(Stack Segment)
- 用途:指向当前使用的栈所在的内存段。
- 描述:SS寄存器包含了当前栈段的基地址。栈用于存储函数调用的返回地址、局部变量以及其他临时数据。当执行压栈或弹栈操作时,处理器会使用SS和SP(栈指针)寄存器来访问栈。
ES(Extra Segment)
- 用途:指向额外的数据段,常用于字符串操作。
- 描述:ES寄存器通常用于某些特定的字符串操作指令,如MOVS(移动字符串)和STOS(存储字符串)。它允许程序访问不同的数据段,而不需要修改DS寄存器。
FS
- 用途:指向额外的数据段,通常用于线程本地存储(Thread Local Storage, TLS)也是本地上下问环境的存储,在R3中存储的TEB,在R0中存储的KPCR
- 描述:FS寄存器常用于访问线程特定的数据,特别是在多线程环境中。操作系统可能会使用FS段来存储线程局部变量。
段寄存器的权限
不同段寄存器指向的段,权限是不同的
我们以一段简单的代码为例(x86)
#include "stdafx.h"
#include <Windows.h>int val = 0x100;
int val2 = 0x200;int _tmain(int argc,_TCHAR* argv[]){__asm{mov ax ,cs;mov ds , ax;mov eax ,dword ptr [val];mov dword ptr [val2],eax;}printf("%x\r\n",val2);system("pause");return 0;
}
可以看见,我们将cs的值移到了ds里面
当我们继续运行时,出现了0xC00005,这说明我们权限出了问题
上面图中的报错表示,我们出错正是在向段所指内存中写值时报错,这证明
1.ds段寄存器和cs段寄存器有着不同的权限,至少有一个没有写权限,通过查阅知道,cs段是代码段是没有写权限的
2.默认去取全局变量的地址时,编译器会智能的将我们的赋值指向ds段,这里我们在上面的代码中交换了cs和ds的值,所以ds里面的值实际上是cs的值
这里我们修改回去,es里面的值与ds相同,这是微软的规定,重新编译
#include "stdafx.h"
#include <Windows.h>int val = 0x100;
int val2 = 0x200;int _tmain(int argc,_TCHAR* argv[]){__asm{mov ax ,cs;mov ds , ax;mov ax,es;mov ds,ax;//Win系统中es和ds值是一样的mov eax ,dword ptr [val];mov dword ptr [val2],eax;}printf("%x\r\n",val2);system("pause");return 0;
}
我们继续实验,如果将两个被定义的变量移入main函数中,看汇编,这时候,段寄存器又变为了ss,也就是栈段。这里就是用实验证明的,如果是局部变量,那么就会用ss寻址
段选择符
我们继续往下,这时候我们可以选择记住目前段寄存器的值
从网上查阅资料可以知道,此时寄存器中存着的就是段选择子
就以CS为例,1B的16位二进制表示为0000 0001 1011
按照上图所示,CS的TI位为0,所以指向的是GDT,结构如下
回到我们的段选择符,前面3到15的index号表明,我们要去找GDT表中的序号为3的项
打开Windbg,用r gdtr命令来看当前GDT表的指向,可以看见这里指向了807d4c20
1: kd> r gdtr
gdtr=807d4c20
我们可以使用dq命令来看内存,d是看内存,q是qword
1: kd> dq 807d4c20
ReadVirtual: 807d4c20 not properly sign extended
807d4c20 00000000`00000000 00cf9b00`0000ffff
807d4c30 00cf9300`0000ffff 00cffb00`0000ffff
807d4c40 00cff300`0000ffff 80008b7c`f75020ab
807d4c50 8040937c`c0003748 0040f300`00004000
807d4c60 0000f200`0400ffff 00000000`00000000
807d4c70 8000897d`1ac00068 8000897d`1b300068
807d4c80 00000000`00000000 00000000`00000000
807d4c90 800092b9`900003ff 00000000`00000000
序号为三,也就是第四项,值为00cffb00`0000ffff
首先我们从低位看起
根据上面的图,0到15位是段的限长,值为ffff,再加上48-51的4位限长还是f,所以最后是fffff
而base的值为 00 0000,再添加上两段分开的00,最后还是0
继续向下解析,39-42为b也就是1011,type就是b,S位(这里图有误,也就是红色的那个1)为1,所以是代码段或者数据段,对应的type如下
如果s为0,那么type则为system段,这里暂时不涉及
DPL为11,也就是3,p为1
最后的52-55为值为c,也就是1100,G位为1,D/B位为1,def位为0,AVL位为0
到此为止,我们已经看完了段描述符的所有信息
一股脑解析完这些信息,我们要知道它的作用
首先,G位为1
那么就表明我们现在以页为单位,再intel中,一个页是4096字节,也就是0x1000
页数也是从0开始所以有(fffff+1)页,每页大小0x1000,因为我们在计算机中算总长度的时候,是从0开始的所以最后要减一
写成公式就是((ffff+1) x 0x1000 )-1 = 0xffffffff
这个值,我们用十六进制看不熟悉,但是如果换成十进制
地址的下标范围到4294967295,长度是4294967296
这个值我们除1024^3,结果就是4,也就是4GB
到此为止,我们得出结论,这个段,最多管理着4GB的内存
段长度的验证
学习到这里,我们怎么去验证我们段描述符所规定的段范围是正确的呢
为了不影响我们程序最后的正常运行,我们将GDT表中的一段标为0的字节作为我们待会放置新GDT
的位置,按照序号来说,它就是9,结合 我们之前的知识,我们ds的值应该替换为4b,也就是0100 1011,index位对应为9
1: kd> dq 807d4c20
ReadVirtual: 807d4c20 not properly sign extended
807d4c20 00000000`00000000 00cf9b00`0000ffff
807d4c30 00cf9300`0000ffff 00cffb00`0000ffff
807d4c40 00cff300`0000ffff 80008b7c`f75020ab
807d4c50 8040937c`c0003748 0040f300`00004000
807d4c60 0000f200`0400ffff 00000000`00000000//在这里
807d4c70 8000897d`1ac00068 8000897d`1b300068
807d4c80 00000000`00000000 00000000`00000000
807d4c90 800092b9`900003ff 00000000`00000000
我们修改我们之前的代码,先将4b这个描述符对应的值送入对应的寄存器中(我这里vmtool暂时除了问题,就偷懒贴个图片了)
写好上面的代码之后,我们先把原段选择子23位置的值,移入到4b里面,这里通过windbg实现
0: kd> eq 80b99048 00cff300`0000ffff
WriteVirtual: 80b99048 not properly sign extended
0: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f7`8c003748 0040f300`00000fff
80b99040 0000f200`0400ffff 00cff300`0000ffff//可以看见这里已经被写进去了
80b99050 830089f7`60000068 830089f7`60680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
这时候我们回去运行我们的程序,验证是否真的将这个4b移入了ds,Val2的结果在最后被赋予了0x100
这以上的一切都是建立在我们的base为0的情况下,那么如果让我们的base为1呢
1: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f7`8c003748 0040f300`00000fff
80b99040 0000f200`0400ffff 00cff300`0000ffff
80b99050 830089f7`60000068 830089f7`60680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
1: kd> eq 80b99048 00cff300`0001ffff
WriteVirtual: 80b99048 not properly sign extended
1: kd> dq 80b99000
ReadVirtual: 80b99000 not properly sign extended
80b99000 00000000`00000000 00cf9b00`0000ffff
80b99010 00cf9300`0000ffff 00cffb00`0000ffff
80b99020 00cff300`0000ffff 80008b1e`400020ab
80b99030 834093f7`8c003748 0040f300`00000fff
80b99040 0000f200`0400ffff 00cff300`0001ffff//在把这里对应base为改为1
80b99050 830089f7`60000068 830089f7`60680068
80b99060 00000000`00000000 00000000`00000000
80b99070 800092b9`900003ff 00000000`00000000
通过汇编得知,得到以下结果
我们原本的存储的dword4个字节,在base为0的情况下出来的是 00 01 00 00,也就是0x100,但是现在我们的base不为0,为1,所以整个读取向右移了一字节,所以实际上读的是 01 00 00 00也就是1,最后的实验结果也和我们预测的相等
所以我们利用段寄存器进行调用,例如ds:[val]
真正的式子应该是**(base + val )+ ds 0x10*,需要加上我们的base
在知道了这个基础知识之后,我们就可以继续向前了
在上面的截图中,我们知道fs的值是3b,对应到我们GDT表中的值里面,我们可以知道它的限长是fff,所以我们写两句asm,来访问一下
不出所料,访问冲突
但是如果我们改为0xffc,这样就是刚好将最后四个字节移入eax中
最后也是能够正确访问到
这就证明了,段限长是确实存在的