什么是段寄存器?
当我们用汇编读写某一个地址时,比如用下面的代码:
mov dword ptr ds:[0x123456],eax
其实我门真正读写的地址是:d5.ba5e+x123456。并不是123456,不过正好的是ds段寄存器的基址是0而已。
一些段寄存器
段寄存器有这几个:ES、CS、SS、DS、FS、GS、LDTR、TR,它们各有自己特殊的用途。
段寄存器的结构可用下图表示:

一个完整的段是有96位组成的,不可见的 80 位中,64 位来自段描述符(8 字节),另外 16 位是处理器内部维护的属性扩展,而非
段寄存器具有96位,但我们可见的只有16位。
| 部分 | 位数 | 名称 | 可见性 | 作用 |
|---|
| 第一部分 | 16 位 | 段选择子(Selector) | 完全可见 | 程序员可通过指令读写,包含索引、TI 位、RPL 特权级 |
| 第二部分 | 80 位 | 描述符高速缓存器(Descriptor Cache) | 完全不可见 | 处理器自动维护,包含 32 位基址 + 32 位段界限 + 16 位属性 |
只有 16 位(段选择子)对程序员 / 内核可见,64 位段描述符存储在内存的 GDT/LDT 中,需通过选择子索引获取,且加载后存入 80 位不可见缓存器。内核也无法直接读写这 80 位缓存器,只能通过修改 GDT/LDT 并重新加载选择子来间接更新。
16 位是段选择子,是程序员主动加载的(如mov ax, selector; mov ds, ax),用于索引 GDT/LDT 中的 64 位段描述符;而不可见缓存器中的 16 位属性是直接从段描述符中提取的,并非计算得出。
我们可以用0D随意加载一个程序,如下图所示:

既然是寄存器了,那就可以进行读写操作,如下将介绍读写段寄存器的操作:
·MoV指令:MOV AX,E5,但只能读16位的可见部分;mov Ds,Ax写段寄存器,写的是96位。
·读写LDTR的指令为:SLDT/LLDT
读写TR的指令为:STR/LTR
段寄存器属性探测:我介绍过段寄存器有6位,但我们只能看见16位,那如果证明Attribute、Base、Limit的存在呢?我们将在下面进行初步探测。
段寄存器成员简介:既然讨论段寄存器属性,首先要知道它们存着啥,下面表格的内容是我从虚拟机里查询到的值,可能和我的不一样,但无所谓。它们的属性我已查询并把它们的权限写到表格中,之所以为什么我之后将会介绍。
Windows操作系统并不会使用GS寄存器,故用-表示。

探测Attribute:

这个是错误的,因为mov ds,ax是错误的,cs段是不可写的(权限是这样),所以报错了。翻译过来就是地址访问冲突,这是什么原因呢?这就是由于Cs段寄存器是可读的,而不是可写的。原来的s是可读可写的,但将cs通过ax赋值给ds时候,ds不再是原来的ds,而是cs,故会引发此错误。
探测Base:老生常谈程序的罗地址无法访问。但零地址一定是无法访问吗?我们将用以下代码进行验证base:

这里的mov eax,es:[0]实际取得就是fs,fs在内核里面是KPCR的一个结构,实际上取得是KPCR这个结构的首个4字节成员,才会用这种方式去取的。
探测Limit
我们将用以下代码进行验证Limit:

由于fs最大寻址范围是0xFFF,所以这里肯定会出问题,
踩坑问题
里面有一些坑我还没让你深,你髁一深看看。请你思考出答案或者百思不得其解的时候再来看答案。
1.在验证属性的时候,用下面的代码,结果运行mov dword ptr ds:[a],10正常通过,放开程序跑后内存访问冲突。

这其实是编译问题,为什么会出现这个问题呢?我明明代码很正常但就不行,难道只是因为局部变量的问题呢。但全局变量和局部变量在内存上根本没有区别。全局变量只是一个死地址,局部变量是该变量所在函数临时的地址,但访问上根本没有区别。我们看一看编译器到底把咱们的内联汇编到底翻译成了什么?为什么会出现这个问题呢?我明明代码很正常但就不行呢,难道只是因为局部变量的问题呢。但全局变量和局部变量在内存上根本没有区别。全局变量只是一个死地址,局部变量是该变量所在函数临时的地址,但访问上根本没有区别。我们看一看编译器到底把咱们的内联汇编到底翻译成了什么?
mov ax,cs;
mov ax,cs
mov ds,ax
mov ds,ax
mov dword ptr ds:[a],10;
dword ptr [ebp-4],0Ah
}
return 0;
xor eax,eax
前面的内敛汇编编译器给我一五一十的直接翻译了,但这个 mov dword ptr ds:[a],10; 内联汇编给我翻译成了啥,过分了!结果压根没有用 ds 的权限来访问,而是默认的 ss 来访问,这和预期结果一样才怪。
- 在探测 Base 属性的时候,使用
gs 作为试验寄存器,单步执行到 mov eax,gs:[0] 时,出现内存访问冲突错误。
#include "stdafx.h"int a = 0;int main(int argc, char* argv[]){
__asm {
mov ax, fs;
mov gs, ax;
mov eax, gs:[0];
mov dword ptr ds:[a], eax;
}
return 0;}踩坑说明:是因为每次单步调试,就会触发单步调试异常,进入内核,内核会把 gs 清零了,故导致实验无法成功。
GDT 与 LDT
GDT 是全局描述符表。LDT 为局部描述符表,但 Windows 并没有使用它,故不再介绍,感兴趣请查询 Intel 白皮书。当我们执行类似 MOV DS, AX 指令时,CPU 会查表,根据 AX 的值来决定查找 GDT 还是 LDT,并找到对应的段描述符。段描述符将会在后面部分进行介绍。
GDT 表存在于内存之中。CPU 要想找到它,就必须知道它的位置。于是乎 CPU 有一个寄存器,它被称之为 GDTR,存储了 GDT 表的位置和大小,是一个 48 位的寄存器,用 C 语言表示如下:
struct GDTR{
DWORD GDTBase; // GDT表的地址
SHORT limit; // GDT表的大小
};一个段寄存器是96位的,有16位在内核中是可见的,然后这个段寄存器的64位就是存在GDT表里面,
那么,我们如何通过 WinDbg 来获取 GDT 的地址、大小和 GDT 表里的成员呢?首先提一句,段描述符大小为 64 位。有关其操作见下图:
kd> r gdtr
gdtr=8003f000
kd> dq 8003f000 l20
ReadVirtual: 8003f000 not properly sign extended
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 00000f200`0400ffff 00000000`00000000
8003f050 80008955`22000068 80008955`22680068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
8003f080 80009240`0000ffff 00009200`00000000
8003f090 00000000`00000000 00000000`00000000
8003f0a0 89008992`8bb80068 00000000`00000000
8003f0b0 00000000`00000000 00000000`00000000
8003f0c0 00000000`00000000 00000000`00000000
8003f0d0 00000000`00000000 00000000`00000000
8003f0e0 00000000`8003f100 00009200`0000ffff
8003f0f0 8003984d`9b2084cf 00009200`0000ffff
kd> r gdtl
gdtl=00003ff
r gdtr 指令表示读取 GDT 表的地址;r gdtl 指令表示读取 GDT 的大小,元素一共有128 个;dq 8003f000 l20 指令表示从 0x8003f000 地址(即为 GDT 表的地址)读取 0x20 个 64 位数据,如果没有 l20,默认 0x10 个。这些都是以后常用的指令,需要熟练掌握。如果用dd的话,显示方式是有问题的。
段选择子
段选择子结构简单,那我先介绍它。它是一个 16 位的描述符,指向了定义该段的段描述符(段描述符比较复杂,后面将会完整介绍)。段选择子结构如下图所示:

我们在用户层看到的是16位的数据,
它的成员解释如下:
- RPL:请求特权级别,通俗的讲我用什么权限来请求。
- TI:TI=0 时,查 GDT 表;TI=1 时,查 LDT 表。
- Index:处理器将索引值乘以 8再加上 GDT 或者 LDT 的基地址,就是要加载的段描述符。
是不是很简单?该结构一定要牢记于心,后面将给出练习训练。
比如0x23,拆出来就是0010 0011,在前面补充上0,那么结构就非常清晰了:0000 0000 0010 0011,所以RPL特权请求级是3(用户层3环),TI是0也就是GDT全局描述符表,Index是00100是4,所以index索引是4。
那么我们去查就行:

从0开始数,数4个就行,就是下面的00cff300`0000ffff。
段描述符如图所示

段描述符有很多成员,它的成员将会在下面详细介绍,学习的时候一定要按照我介绍的顺序进行学习:
P 位
P = 1 段描述符有效,P = 0 段描述符无效。
Base
Base 被分成了三个部分,从图可知:Base 的低 16 位被放到了段描述符的低四个字节,高 16 位被均分到段描述符的高四个字节的头和尾。把它们依次拼接起来就是完整的 Base。
Limit
由图可知,把段描述符中所有的 Limit 拼接起来就只有 20 位。上一节教程说它有 32 位的 Limit,那就是要看 G 位了。
G 位
如果 G = 0,说明段描述符中的 Limit 的单位是字节,段长度 Limit 范围可从 1B~1MB,即在 20 位的前面补 3 个 0 即可;如果 G = 1,说明段描述符中的 Limit 的单位是 4KB,即段长度 Limit 范围可从 4KB~4GB,在 20 位的后面补充 FFF 即可。举个例子,如果 Limit 拼接后的为 FFFFF,如果 G 为 0 则为 000FFFFF,反之为 FFFFFFFF。
S 位
S = 1 代码段或者数据段描述符,S = 0 系统段描述符。
TYPE 域
TYPE 域 是比较复杂的成员,它表示的含义受 S 位 的影响。
当 S 位为 1 时此时段描述符表示的是代码段或者数据段,如下图所示:

对于表格中 Type 域的属性和含义,如下表格所示:
| 属性 | 含义 | 属性 | 含义 |
|---|
| A | 访问位 | E | 向下扩展位 |
| R | 可读位 | W | 可写位 |
| C | 一致位 |
|
|
对于比较特殊的属性,我们将进一步介绍:
C 位
C = 1:一致代码段;C = 0:非一致代码段。什么是一致代码段,什么是非一致代码段,将在后面的教程进行介绍。
E 位
什么是向下拓展位,我们以 fs 为例来看一下如下示意图:
那么接下来比如我们现在对着这个图来拆一下我们的0x23的段选择子的值:00cff300`0000ffff。
这里有几个小技巧,首先是Base我们知道除了Fs是0x7FFFDE000以外,其他的都是0,那么可以拆成00 cff3 00 ` 0000 ffff。注意这里的G是控制这个段的范围(也就是Limit),所以是控制上下那两个Limit的数加起来是多少。D/B位是表示用32位还是16,但是16位本质上属于实模式,现在没人用实模式,所以这个位基本默认就是1的。x86的第21位用不到所以默认是0,但是x64是会用到的。
type域的值受到S位的影响