首页
社区
课程
招聘
[原创]X86分段
发表于: 2026-6-9 00:55 515

[原创]X86分段

2026-6-9 00:55
515

什么是段寄存器?

当我们用汇编读写某一个地址时,比如用下面的代码:

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 来访问,这和预期结果一样才怪。

  1. 在探测 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 清零了,故导致实验无法成功。

  • 32 位 Windows 用户态段寄存器规则

    • FS 寄存器在用户态被系统固定指向 TEB(线程环境块),段描述符由操作系统合法配置,因此 mov eax,fs:[0] 是合法操作(TEB+0 位置为TEB.NT_TIB.Self,指向 TEB 自身)。
    • GS 寄存器在 32 位 Windows 用户态默认值为 0,无合法段描述符,直接访问会触发异常;代码中用 mov gs,ax 复制了fs的段选择子,但单步调试会破坏这个值
  • 单步调试的核心影响:调试器单步执行时,CPU 会设置 TF(陷阱标志),每执行一条指令就触发 #DB(调试异常),进入内核态的异常处理流程:

    • 内核会保存 / 恢复用户态寄存器上下文,但GS 这类用户态不常用的段寄存器会被重置为默认值 0(内核不保留用户态修改的 GS 值);
    • 异常处理返回用户态时,GS 已被清零,此时执行 mov eax,gs:[0] 会以GS=0为段基址,访问非法内存地址,触发访问冲突。
  • 解决方案补充

    • 避免单步调试这段代码,直接运行程序(不触发调试异常,GS 寄存器的值不会被内核重置);
    • 或直接使用 FS 寄存器访问 TEB(mov eax,fs:[0]),无需复制到 GS;
    • 若必须使用 GS,可通过关闭调试器单步陷阱、或在内核态 / 驱动程序中操作(不受用户态 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位的影响。

左边表示向上拓展,右边是向下拓展。即向上拓展 `base` 到 `base+limit` 之间区域有效,其余无效;向下拓展 `base` 到 `base+limit` 之间的区域无效,其余有效。这个位针对数据段有效。 当S位为0时 此时段描述符表示的是系统段,系统段有很多种,将会在后面的教程进行详细讲解。Type域每一个数值的含义如下图所示:

红色表示向下拓展能寻址的范围。可以看出,如果 D = 0,就算原来能寻址 4GB,因为 DB 位的限制导致最大范围是 64KB。


DPL

DPL (Descriptor Privilege Level),即描述符特权级别,规定了访问该段所需要的特权级别是什么。如果通俗的理解,就是:如果你想访问我,那么你应该具备什么权限

AVL

AVL 指示是否可供系统软件使用,由操作系统来使用,CPU 并不使用它。


加载段描述符至段寄存器

除了 MOV 指令,我们还可以使用 LESLSSLDSLFSLGS 指令修改寄存器。CS 不能通过上述指令进行修改,CS 为代码段,CS 的改变会导致 EIP 的改变,要改 CS,必须要保证 CSEIP 同时改,后面会讲解。

给一个代码模板,具体如下,作为本节的一个作业:

char buffer[6];// 自己构造一个段选择子// 自己自行赋值,你构造的段描述符位置不一样,段选择子就不一样_asm{
les ecx, fword ptr ds:[buffer]  // 高2个字节给es,低四个字节给ecx}

本节练习

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习比较多,请保质保量的完成,不得使用任何拆分工具





比如找这个00cf9300`0000ffff,



第 1 张图完整内容

俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习比较多,请保质保量的完成,不得使用任何拆分工具。

如下是从虚拟机读取的 GDT 表的前 18 个段描述符,下面的实验均按照此进行练习。

plaintext

1  8003f000  00000000`00000000  00cf9b00`0000ffff
2  8003f010  00cf9300`0000ffff  00cffb00`0000ffff
3  8003f020  00cff300`0000ffff  80008b04`200020ab
4  8003f030  ffc093df`f0000001  0040f300`0000ffff
5  8003f040  0000f200`0400ffff  00000000`00000000
6  8003f050  80008955`22000068  80008955`22680068
7  8003f060  00009302`2f40ffff  0000920b`03003fff
8  8003f070  ff0092ff`700003ff  80009a40`0000ffff
9  8003f080  80009240`0000ffff  00009200`00000000

1 练习读取 GDT 表的位置和长度,并显示 GDT 表前 48 个段描述符。2 在给定的段描述符中,进行拆分练习(至少 10 个)。3 拆分如下段选择子。002B 0023 0010 001B 003B

4 快速辨别给定段描述符是否可用以及段基址、段长(至少 10 个)。5 从给定段描述符,请按照下面的要求进行练习(全部):

  1. 快速找出所有数据段,并分析该段属性:只读、已访问、可读可写、拓展方向
  2. 快速找出所有代码段,并分析该段属性:只执行、可读可执行、已访问、一致代码
  3. 快速找出所有系统段,并分析属性6 自行构造段选择子和段描述符,并用 加载段描述符至段寄存器 中的代码模板和要求取得成功。如果有时间同样把 LSS 、LDS 、 LFS 、 LGS 的实验也类比做了。7 如何在调试器中快速判断程序在几环权限。8 自学修改 GDT 表的相关知识,并思考如下问题。

第 2 张图完整内容

8 自学修改 GDT 表的相关知识,并思考如下问题。

plaintext

1  r gdtr
2  dq 8003f090 00cffb00`0000ffff
3  r gdtr

8003f090 是 GDT 表中的一个段描述符的地址,更改后发现并没有更改,请思考为什么会这样。


第 3 张图完整内容

CPL/RPL/DPL

  • CPL: CPU 当前的权限级别
  • DPL: 如果你想访问我,你应该具备什么样的权限(CPL)
  • RPL: 用什么权限去访问一个段

RPL 存在的意义

举个例子,我们本可以用 读写 的权限去打开一个文件,但为了避免出错,有些时候我们使用 只读 的权限去打开。

一致代码段与非一致代码段

对于一致代码段,也称为共享段:

  • 特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据
  • 特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态

对于非一致代码段:

  • 只允许同级访问
  • 绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态

数据段的权限检查

数值上, CPL ≤ DPL 且 RPL ≤ DPL 。同时满足上述条件才能通过。

代码段的权限检查

下面的比较都是数值上的比较:

  • 如果是非一致代码段,要求: CPL == DPL 且 RPL <= DPL
  • 如果是一致代码段,要求: CPL >= DPL

第 4 张图完整内容

代码跨段基础

代码跨段本质就是修改 CS 段寄存器。前面的教程介绍过段寄存器读写,除 CS 外,其他的段寄存器都可以通过 MOV / LES / LSS / LDS / LFS / LGS 指令进行修改。但是 CS 为什么不可以直接修改呢? CS 的改变意味着 EIP 的改变,改变 CS 的同时必须修改 EIP ,故我们无法使用上面的指令来进行修改,这个也是 CPU 不允许的。

代码间的段间跳转

段间跳转,有 2 种情况,即要跳转的段是一致代码段还是非一致代码段,它们不同做的权限检查就不同。同时修改 CS 与 EIP 的指令如下:JMP FAR / CALL FAR / RETF / INT / IRETED本篇只介绍段间跳转,故只使用 JMP FAR ,即为长跳转。下面我举个示例来进行讲解:

CPU 如何执行这行代码 JMP 0x20:0x004183D7

1 段选择子拆分0x20 对应二进制形式:0000 0000 0010 0000

  • 解析结果:○ RPL = 0○ TI = 0○ Index = 4

2 查表得到段描述符TI=0 所以查 GDT 表,Index=4 找到对应的段描述符。注意四种情况可以跳转:代码段、调用门、TSS 任务段、任务门。后面的几种将会在以后的教程详细讲解。

3 权限检查请参考本节的代码段的权限检查

4 加载段描述符通过上面的权限检查后,CPU 会将段描述符加载到 CS 段寄存器中。

5 代码执行CPU 将 CS.Base + Offset 的值写入 EIP 然后跳转到将要执行的 CS:EIP 处的代码,段间跳转结束。


第 5 张图完整内容

5 代码执行

CPU 将 CS.Base + Offset 的值写入 EIP 然后跳转到将要执行的 CS:EIP 处的代码,段间跳转结束。

直接对代码段进行 JMP 或者 CALL 的操作,无论目标是一致代码段还是非一致代码段,CPL 都不会发生改变。如果要提升 CPL 的权限,只能通过调用门。

本节练习

本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。

俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习很少,请保质保量的完成。

1 记住代码段间跳转的执行流程。2 自己实现一致代码段的段间跳转。3 自己实现非一致代码段的段间跳转。


第 6 张图完整内容

本篇文章涉及调用门、中断门、陷阱门这三个重要的 “门”,那么 “门” 到底是什么。打个比方,“门” 就类似于你去办理身份证必须要进入派出所的门一样。你想办理身份证就必须通过这个门,如果你进不了门就办理不了。调用门、中断门、陷阱门也是如此,通过它们你可以修改段的属性,甚至能提权去做一些应用层做不了的事情。

调用门

在将所有的知识之前,先讲一下调用门,因为后面的知识频繁用到了调用门的概念,调用门的结构如下图所示。它和普通的段描述符结构十分相似。低四个字节改为段选择子,如果指向的段描述符的 DPL 小于 CPL,则会提权。高四个字节低 5 个位是调用调用门需要的参数数目。低四个字节的低 16 位和高四个字节的高 16 位拼接为跳转后新的在段中的偏移,也就是调用后 EIP 的位置。




介绍长调用,我先来讲一下什么是 短调用。短调用就是我们在汇编常见的 CALL 指令,调用格式为:CALL 立即数/寄存器/内存 。为什么是短调用,我们来看一下执行该指令时堆栈的变化:

调用 CALL 指令之后, CPU 只将当前的 EIP 压入堆栈后跳转到目标地址,发生改变的寄存器只有 ESPEIP ,即所谓的短调用。

长调用分为两种,一种提权,一种不提权,调用格式为:指令格式: CALL CS:EIP ,其中 EIP 是废弃的, CS 为指向调用门的段选择子。但是值得注意的是 CS 一旦更换,它的 EIPSS 要同时更换。为什么 EIP 需要更换我就不说了。在代码执行的时候,一定会用到堆栈,堆栈的权限必须与 CS 匹配,这就是为什么 SS 必须更换。我们接下来看看长调用到底是何方神圣。


长调用不提权

当段选择子指向的调用门不提权时。发生改变的寄存器有 ESPEIPCS ,比短调用多一个 CS 。执行情况如下图所示:



当段选择子指向的调用门不提权时。发生改变的寄存器有 ESPEIPCSSS 。执行情况如下图所示:

心细的你获取发现, SS 并没有压入堆栈之中,还有 ESP0 也没用通过已知方式获取得到。它从哪里来呢?下一节教程将会讲解。






进程断链

方法1:

方法2:









[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

最后于 2026-6-11 06:56 被BOSC叛忍编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回