内存是计算机系统的关键资源,程序必须被加载到内存中才可以被CPU所执行。程序运行过程中,也要使用内存来记录数据和动态信息。在一个多任务系统中,每个任务都需要使用内存资源,因此系统需要有一套机制来隔离不同任务所使用的内存。要使用这种隔离既安全又高效,那么硬件一级的支持是必须的。IA32 CPU提供了多种内存管理机制,这些机制为操作系统实现内存管理功能提供了硬件基础。
CPU的段机制提供了一种手段可以将系统的内存空间划分为一个个较小的受保护区域,其中每个区域称为一个段。每个段都有字节的起始地址(基地址),边界(limit),和访问权限等属性。当根据(段基址:段偏移)来获取程序的逻辑地址的时候,就需要将段寄存器中保存的基地址加上偏移地址才可以获得,比如ds:[0x12345678]这个地址,最终会获得的逻辑地址就是ds段寄存器中保存的基地址加上0x12345678。下图就是这一过程的说明
而段寄存器中的基地址在何处,如何被加载就需要理解段机制,要理解这一套机制,就需要理解IA-32 CPU的段寄存器,段选择子以及段描述符的内容。
为了减少地址转换时间和编码复杂度,处理器提供了可容纳多达6个段寄存器。每个段寄存器都支持一种特定类型的内存引用(代码、堆栈或数据)。对于几乎任何类型的程序执行,至少代码段(CS)、数据段(DS)和堆栈段(SS)寄存器必须加载有效的段选择器。处理器还提供了三个额外的数据段寄存器(ES、FS和GS),它们可用于为当前执行的程序(或任务)提供其他数据段。
要让程序访问段,段的段选择器必须已加载到其中一个段寄存器中。因此,尽管一个系统可以定义数千个分段,但只有6个分段可以立即使用。在程序执行期间,通过将其段选择器加载到这些寄存器中,可以提供其他段。
段寄存器的结构如下图所示
一个段寄存器以供有96位,其中的16位段选择子是可见的,其余的80位不可见的部分中包含着32位的基地址,32位的边界以及16位的段属性。当段寄存器加载16位的段选择子的时候,处理器还会从段描述符中加载不可见的80位的内容。
有两种加载段寄存器的方法:
直接加载指令,如MOV、POP、LDS、LES、LSS、LGS和LFS指令。这些指令可以显式地引用段寄存器
隐含的加载指令,如CALL、JMP和RET指令的远跳转版本、系统输入和系统退出指令,以及IRET、INTn、INTO和INT3指令。这些指令将CS寄存器(有时还有其他段寄存器)的内容作为其操作的附带部分进行更改
MOV指令可以将段寄存器的可见部分存储在通用寄存器中。
为了让程序正常运行,操作系统在程序加载进内存要执行之前都会初始化段寄存器的值,不同操作系统初始化的值是不一样的。下图就是在我的Win10系统中对段寄存器的情况:
这些段寄存器的各项值的内容如下:
可以看到这些段寄存器多数内容都一样,除了CS属性可执行不可写,FS的基地址和边界不同和其他段寄存器不同。由于可见的只有16位,所以要证明其他80位的存在,只能通过实验进行证明。
以下代码可以证明段寄存器中有32位的基地址
正常情况下,程序是无法对0地址的内容进行读写的,可是当将ds段寄存器的内容改成fs段寄存器的内容的时候,就可以实现对偏移为0的地址的读写,说明此时的段寄存器中包含了基地址,这样最终得到的地址才不是0地址
以下代码可以证明段寄存器中有32为的边界
由于将fs段寄存器的内容赋值到了ds寄存器中,此时ds的边界只到0xFFF,无法访问整个进程空间了,此时对全局变量的读写超出了范围就导致程序运行的崩溃
以下代码可以证明段寄存器中有16位的访问权限
根据上表可以知道CS段寄存器是不具有可写的属性的,将ds段寄存器内容修改为cs段寄存器内容以后,在对ds段的内存区域进行写入操作程序就会崩溃。可见,将cs段寄存器并不具有可写的属性
96位的段寄存器中有16位可见部分是段选择子,处理器就是通过它去段描述符中加载不可见的80位加入到段寄存器中。所以段选择子并不直接指向段,而是指向定义段的段描述符,它结构如下图所示:
从图中可以知道,一个16位的段选择子分成了三个部分,这三部分的描述如下
指定要查询的段描述符
GDT:1
LDT:0
由此可知,处理器根据段选择子中的Index来获得需要的段描述符的地址,以此来加载隐藏的80位。
处理器不使用GDT的第一个条目。指向GDT的此条目的段选择器(即索引为0且TI标志设置为0的段选择器)被用作“空段选择器”。当段寄存器(除了CS或SS寄存器)加载了空选择器时,处理器不会产生异常。但是,当使用持有空选择器的段寄存器来访问内存时,它确实会产生一个异常。空选择器可用于初始化未使用的段寄存器。加载带有空段选择器的CS或SS寄存器会导致生成通用保护异常。
段选择器作为指针变量的一部分对应用程序可见,但选择器的值通常由链接编辑器或链接加载器分配或修改,而不是应用程序。
由上面内容可以知道,处理器要根据段选择子找到需要的段描述符,以此填充段选择子的隐藏的80位的内容,此时的段选择子就代表了一个段。但是,在一个多任务中通常会同时存在着很多个任务,每个任务会涉及多个段,每个段都需要一个段描述符,因此系统中会有很多段描述符,为了方便管理,系统用线性表来存放段描述符。根据用途不同,IA-32处理器有3中描述符表:全局描述符表(GDT),局部描述符表(LDT)和中断描述符表(IDT)。
GDT是全局的,一个系统中通常只有一个GDT,供系统中的所有程序和任务使用。LDT与任务有关,每个任务可以有一个LDT,也可也让多个任务共享一个LDT。IDT的数量是和处理器的数量相关的,系统通常会为每个CPU建立一个IDT。
GDTR和IDTR寄存器分别用来标识GDT和IDT的基地址和边界。这两个寄存器的格式是相同的,在32位模式下,长度是48位,高32位是基地址,低16位是边界。
LGDT和SGDT指令分别用来读取和设置GDTR寄存器。LIDT和SIDT指令分别用来读取和设置IDTR寄存器。操作系统在启动初期会建立GDT和IDT并初始化GDTR和IDTR寄存器。
当创建LDT时,GDT已经准备好,因此,LDT被创建为一种特殊的系统段,其段描述符被放在GDT表中。GDT表本身只是一个数据结构,没有对应的段描述符。
想要知道这两个寄存器中保存的内容,可以使用WinDbg的r命令来查看
从上面的gdtl值可以知道这个GDT的边界是0x3FF(1023),总长度是1024字节(1KB),由于每个段描述符占8个字节,所以一共有1024/8=128个表项。IDT的长度是2KB,共有256个表项。
当然也可也使用下面的代码来获得对应的内容
可以看到输出和WinDbg的输出是一样的
在保护模式下,每个内存段都有一个段描述符,段描述符是GDT或LDT中的一种数据结构,它为处理器提供段的大小和位置,以及访问控制和状态信息。段描述符通常由编译器、链接器、加载器或操作系统或执行程序创建,但而不是应用程序。下图是段描述符的一般格式,可以看到一个段描述符占8个字节,分为高32位和低32位。
段描述符最基本的内容是这个段的基地址和边界。基地址是以4个字节表示(字节2,3,4和7),它可以是4GB线性地址空间中的任意地址(0x00000000~0xFFFFFFFF)。边界是用20个比特位表示(字节0,1和字节6的低4位),其单位由粒度位(字节6的最高位)决定,当G=0时,段边界的单位是1字节,当G=1时是4KB。因此一个段的最大边界值是(2^20-1),最大长度是2^20*4KB=4GB。
在段描述符中一共只有20位来表示边,最大为0xFFFFF,而要表示32位的边界值就需要使用到G位
G位为0:在0xFFFFF高位补3个0x0,此时边界就是0x000FFFFF
G位为1:在0xFFFFF低位补3个0xF,此时边界就是0xFFFFFFFF
下表介绍了段描述符的其他各个域的含义
指示该段是否存在于内存中。如果P=0,则当将指向段描述符的段选择器加载到段寄存器中时,处理器将生成段不存在异常。内存管理软件可以使用此标志来控制哪些段实际加载到物理内存中。它除了提供分页外,还提供了一个管理虚拟内存的控件。
图3-9显示了当P=0时段描述符的格式。当P=0,操作系统或执行人员可以自由使用标记为“可用”的位置来存储自己的数据。
对于代码段,该位表示的是这个代码段默认的位数(Default Bit)。
D=0:16位代码段
D=1:32位代码段
对于栈段,该位称为B(Big)标志。
B=1:使用32位堆栈指针(保存在ESP中)
B=0:使用16位堆栈指针(保存在SP中)
对于数据段,该位称为B(Big)标志,指定了段的上界。
B=1:段的上界是0xFFFFFFFF(4GB)
B=0:段的上界是0xFFFF(64KB)
用于描述IA-32e模式下的代码段。
L=1:代码段包含64位代码
L=0:代码段包含兼容模型的代码
当S=1的时候,段类型为代码段,数据段或者堆栈段,此时type各比特的值以及对应的含义如下图
根据上图可以知道,type的最高位来决定这是一个数据段还是代码段,当最高位为0,这就是一个数据段,当最高位为1这就是一个代码段。无论是对数据段还是代码段,最低位(A)都是表示是否访问过,如果是1则已被访问,如果是0则未被访问。
对于数据段来说:
第9位(W)表示该段是否可写,如果是1则该段可读可写,否则可读不可写。
第10位(E)表示该段的扩展方向,如果是1则表示向上扩展,否则表示向下扩展。
对于代码段来说:
第9位(R)表示该段是否可读,如果是1则该段可执行可读,否则该段可执行不可读。
第10位(C)表示该段是否是一致代码段,如果是1则表示是,否则就不是
对于向下扩展的栈数据段,段边界指定的是该段的最小偏移,B标志用来指定偏移的最大有效值(即上边界),当B=1时,最大偏移是0xFFFFFFFF,这样,如果limit=0,那么段总长度就是4GB(G=0),如果B=0,那么上边界便是0xFFFF(64KB)。
当S=0的时候,段类型是系统段,处理器可以识别以下类型的系统段描述:
本地描述符表(LDT)段描述符
任务状态段(TSS)描述符
调用门描述符
中断门描述符
陷阱门描述符
任务门描述符
这些描述符类型可分为两类:系统段描述符和门描述符。系统段描述符指向系统段(LDT和TSS段)。门描述符本身就是“门”,它保存指向代码段中的过程入口点(调用、中断和陷阱门)的指针,或者保存针对TSS(任务门)的段选择器
显示了系统段描述符和门描述符的类型字段的编码。请注意,IA-32e模式下的系统描述符为16字节,而不是8字节
不同的系统段有不同的特点和作用,详细内容后面再说,现在先做几个实验来熟悉上面的内容。
就以系统默认加载的这几个段寄存器中的段选择子,也就是0x0023,0x001B,0x003B来进行实验。
0x0023对应的二进制是0000000000100 0 11,此时RPL=11(0x3),意味着请求特权级为3。TI=0,意味着要去GDT表查询相应的段描述符。索引Index=0100(0x4),所以相应的段描述符在内存中的地址就等于GDT表基地址+0x04 * 8。
根据WinDbg的输出可以知道,GDT表的地址是0x8003F000。
所以段选择子0x0023的段描述符的地址就是0x8003F000 + 0x4 * 8 = 0x8003F020
根据WinDbg输出可以知道,此时的段描述符为0x00 CFF3 00 0000 FFFF。根据段描述符的格式可以知道,此时的Base为0x00000000,对于高32位的8-23位的0xCFF3,对应的二进制是1 1 0 0 1111 1 11 1 0011,各个比特位的内容及含义如下:
由于段描述符第32位的低16位为0xFFFF且SegLimit为0xF且G=1,所以此时段边界是0xFFFFFFFF,该段在内存中有效,特权级为3,是一个可读可写且向上扩展的数据段。
0x001B的二进制为 0000000000011 0 11,TI=0意味着查询GDT表,RPL=3意味着请求者特权级为3,索引Index=3,用上面的方法去WinDbg查看对应的段描述符
得到此时段描述符为0x00 CFFB 00 0000 FFFF。此时段的Base为0x00000000,对于高32位的8-23位的0xCFFB对应二进制是1 1 0 0 1111 1 11 1 1011,各个比特位的内容及含义如下
由于段描述符低32位的低16位为0xFFFF且SegLimit为0xF且G=1,所以此时段边界为0xFFFFFFFF,该段在内存中有效,特权级为3,是一个可读可执行已访问的非一致代码段。
0x003B的二进制为0000000000111 0 11,TI=1意味着查询的是GDT表,RPL=3意味着请求者特权级为3,索引为7,用上面的方法在WinDbg中得到输出如下所示
得到此时段描述符为0x00 40F3 00 0000 0FFF。此时的Base为0x00000000,对于高32位的8-23位的0x40F3对应的二进制是0 1 0 0 0000 1 11 1 0011,各个比特位的内容及含义如下
由于低32位的低16位为0x0FFF且SegLimit为0且G=0,所以此时的边界是0x00000FFF,该段在内存中有效,特权级为3,是一个可读可写已访问的向上拓展的数据段。
由此可以得出结论,当对段寄存器赋值的时候,处理器会根据段选择子中的TI会来决定是去GDT还是IDT表中查找相应的段描述符,随后根据索引到相应的表中找到对应的段描述符,在将段描述符中的内容赋值到段寄存器中,下图是对TI的不同的说明,注意这里的GDT表的第一项是没有使用的
可以使用mov,les,lss,lds,lfs,lgs指令修改ES,SS,DS,FS,GS,LDTR,TR这几个段寄存器,但不能修改CS段寄存器。原因是CS段寄存器的改变意味着EIP的改变,所以在修改CS段寄存器的时候需要同时修改EIP,所以无法使用那些指令来修改CS段寄存器,接下来就介绍几种修改CS段寄存器的方法。
上面说过,不能像修改其他段寄存器一样修改CS段寄存器,想要修改CS段寄存器需要通过以下的方法
远跳转
调用门
中断门
陷阱门
任务段
任务门
jmp far指令(指令编码为0xEA),也称为远跳转指令,该指令的格式是jmp far cs:eip,其中EIP是随后的4个字节,而cs则是EIP随后的2个字节。也就是说对于0xEA 78563412 3000,其对应的指令是jmp far 0x0030:0x12345678。
而当程序运行这条指令的时候,会按照以下步骤进行:
将0x0030作为段选择子拆分,得到TI=0。
此时就要去GDT表中查找段描述符。
找到相应的段描述符以后,进行权限检查
通过了权限检查,CPU就会将段描述符中的内容加载到CS寄存器中
CPU将CS寄存器中保存的基地址与偏移0x12345678相加就得到了要执行的代码的地址
通过第三步的权限检查分为两种情况:
如果是非一致代码段,此时要求CPL 数值上等于DPL并且RPL 数值上小于等于 DPL。
如果是一致代码段,此时要求CPL 数值上大于等于DPL。
也就是说,在一致代码段中,特权级低的代码(ring3)可以访问特权级高(ring0)的数据,而在一致代码段中,只能同样级别的代码段可以进行跳转。
接下来就通过几个跳转实验来验证上面的内容,要完成这些实验就需要在GDT表中找到P位为0的段描述符,这样才可以构造我们想要的段描述符来实现功能
由于p位是段描述符高32位中的第15位,所以要它为0,则需要低位开始第3个16进制小于8,由WinDbg的输出可以知道0x8003F048的位置的p位为0。
接下来构造一致代码段,首先段描述符的基址和边界分别是0x0000000和0xFFFFFFFF,所以可以根据下表来构造需要的非一致代码段描述符
由此可以构造出需要的段描述符高32位为0x00CFFB00,低32位为0x0000FFFF,接着在WinDbg中增加这一段描述符
可以看到在0x8003F048的位置成功添加了构造的段描述符。接下来就需要构造相应的段选择子,因为段描述符的地址是0x8003F048,相比于GDT表的基地址0x8003F000来说,它是第10个段描述符,所以此时的选择子的Index=9=1001,TI=0,RPL=11,最终得到的段选择子就是0x004B。
接下来就可以通过下面的代码来实现跳转
接下来就在OD中观察跳转前后CS段寄存器的变化,首先跳转前,此时的CS为0x001B。
跳转成功以后,此时的CS段寄存器就变成构造的0x004B了
此时需要更改段描述符高32位中的第13~14位为0,此时段描述符高32位为0x00CF9B00,低32位为0x0000FFFF,此时在WinDbg中通过命令修改相应的段描述符表的地址
此时再次在OD中运行上面的程序,在跳转前此时的CS依然是0x001B
这次在跳转,跳的不是我们写入的目的地址,CS段寄存器也没有发生改变,且根据OD提示的函数名可以知道,程序接下来要抛出异常,可想知道,这次的跳转失败了,接下来会执行异常程序的代码
要构造一致代码段,只需要第8~11位的Type为1111(0xF)就可以,此时段描述符高32位为0x00CFFF00,低32位为0x0000FFFF,在WinDbg对相应位置进行修改:
继续使用上面的程序在OD中测试,跳转前,此时的CS等于0x001B
成功跳转以后,此时的CS就是0x004B
DPL=0的一致代码段的段描述符高32位为0x00CF9F00,低32位为0x0000FFFF,在WinDbg中对相应位置进行更改
继续用上面的方法验证,跳转前,此时的CS为0x001B
成功跳转以后,此时的CS寄存器的内容变为0x004B
长调用call far对应的指令编码是0x9A,它和长跳转的功能是一样的,只不过在使用call far跳转的时候,程序会将原先的CS中的段选择子和返回地址入栈。用非一致代码段DPL=3的例子来说明,此时将相应的段描述符用WinDbg添加进去:
接着将代码修改为如下的长调用代码
接着在OD中观察在调用之前,cs段寄存器的内容是0x001B,返回地址为0x00401091,esp为0x0012FF2C
接着是进行长调用,可以看到此时的CS寄存器的内容为0x004B,栈中压入了原来的cs段寄存器中的段选择子以及返回地址
也因为此时有了正确地返回地址,所以程序可以正常地退出
总结:
为了对数据进行保护,非一致代码段是禁止不同级别进行访问的。ring 3代码不能访问ring0的数据,同样,ring0的代码也不能访问ring3的数据.
如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段,这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问权限高的数据
直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变。如果要提升CPL的权限,只能通过"门"。
上面说到了想要提升CS段寄存器地CPL权限,需要通过调用门的方式来实现。当段描述符的S域为0的时候,此时为系统段描述符,而根据下图可以知道,当Type域为1100(0xC)的时候,此时的段描述符就是一个调用门。
调用门的格式如下图所示:
可以看到此时的段描述符的字段发生了以下的变化:
高32位的16~31位和低32位的0~15位组成了偏移地址
低32位的16~31位变成了一个段选择子
高32位的0~4指定了此次调用是否有参数传递
由此可以知道,此时call far cs:eip的时候,这个指定的eip并没有作用,因为此时偏移地址是由调用门来决定的,而调用门之所以可以提权是因为在调用门中有段选择子,可以段选择子决定了调用完调用门以后CS段寄存器的段选择子。
所以此时调用call far cs:eip的过程如下:
根据CS段寄存器段选择子查表找到段描述符,发现是个调用门
获得调用门中的段选择子的内容,作为装载到CS段寄存器中的段选择子
此时CS寄存器中的段选择子的Base加上调用门的偏移地址就是要执行的目的地址
因为调用门分为有参调用门和无参调用门,所以接下来分别对它们进行实验观察区别。要进行这两个实现,首先要在WinDbg中查找可以用来做实验的段描述符
根据WinDbg的结果显示,地址0x8003F048的地方可以拥抱保存调用门段描述符,而0x8003F008的段描述符0x00CF9B00`0000FFFF可以用来作为用来提权的段描述符。
此时段选择子的内容根据上面的内容可以确定是0x0008,而执行完调用门以后要执行的程序地址可以通过在编译器中获得,这样就只要根据是否有参数就可以构造需要的调用门描述符。
对于无参构造门,调用门的段描述符就是0x0040EC00`00081020,在WinDbg中完成相应的更改
接着使用以下的代码进行测试,查看test函数中的CPL
程序运行到test函数中的int 3就会中断,中断以后,在WinDbg查看寄存器信息,可以看到此时的cs和ss的权限都变成了ring0权限,eip也是上面指定的地址
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-1-6 18:52
被1900编辑
,原因: