-
-
[原创]8.滴水中级班(内核驱动)——代码跨段跳转实验
-
发表于: 2025-9-21 15:12 536
-
本节课来讲一下如何通过JMP FAR来修改CS段寄存器。

第一步是段选择子的拆分,这一步能确认RPL是什么、TI是什么、索引是什么;第二步是查询GDT表得到对应的段描述符,段描述符有四种情况,在这个阶段做实验必须是代码段,另外三种情况还没讲;第三步是权限检查,在权限检查这个阶段,一致性代码检查和非一致性代码检查是不一样的;如果前三步都通过了,第四步才是真正的加载段描述符到段寄存器,也就是真正修改CS;第五步是修改EIP并执行,修改EIP是新的CS.Base+Offset赋值给EIP寄存器来执行代码。

这里就开始进行我们的实验了:首先先区分非一致代码段和一致代码段,根据从左往右数第五个十六进制数是9或者f来判断是代码段(或数据段)或系统段;当第六位大于8的时候,就是表示一定是一个代码段。
x86保护模式下,GDT或LDT里的每一个段描述符(8字节),都规定了一个段的基址、限长、类型、权限等属性。最关键的判断信息在属性字节(通常是从左往右数第5个和第6个字节)。
判断代码段/数据段/系统段的方法
- 第5个十六进制数:类型和S位
- 段描述符第5字节(属性字节)主要包含:Type(段类型)、S(描述符类型)、DPL(特权级)、P(存在位)等。
- Type字段的低四位(4 bits)决定段的类型。
- Type为9或F(即二进制1001或1111):常见于代码段,含“可执行”属性。
- 数据段通常为1、2、3、5、6、7、A、B、C、E等。
- S位:若S=1,就是代码段或数据段;S=0是系统段(门、TSS等)。
- 第6个十六进制数:D/B位和G位相关
- 通常情况下,第6字节的高四位包括D/B(默认操作数/栈大小),G(粒度)位。
- “第六位大于8”这句话实际上是指,如果这一字节大于0x8(即二进制1000),Type字段的“可执行”位为1,说明必定是代码段,不会是数据段。这是因为只有代码段的“可执行”位为1。
- 数据段Type的“可执行”位一定为0。
快速判断的原因和依据
- 段描述符代码段的Type属性,可执行位Bit3是1,即9(1001)、B(1011)、D(1101)、F(1111)等,为“代码段”。
- “数据段”可执行位Bit3是0;即1(0001)、3(0011)、5(0101)、7(0111)、A(1010)、C(1100)、E(1110)等值。
- “系统段”S位(Bit4)为0。
- 所以通过简单观察指定字节的值,可以快速初步判定段类型。
实际举例
假设段描述符如下(简化排列):
AABBCCDDEEFF0011
从左往右第5字节和第6字节分别是D和E。
- D=13 (二进制1101):可执行位为1,判断为代码段。
- E=14:大于8,通常说明有特殊标记,更可能为代码段。
总结/记忆技巧
- 第5字节看Type字段,值为9或F时极可能是代码段。
- 第6个字节(通常高于8)说明代码段属性,有更多允许的操作或空间(对32位代码段很常见)。
- 只要Type属性的“可执行”位是1,就是代码段;否则就是数据段;S=0时是系统段。
这个快速判断方法适合逆向和内核分析时,通过十六进制观察段描述符,不用逐个解码位属性就能第一时间知道类型来源。
这种技巧是基于x86段描述符的结构和二进制属性字段推导出来的,能大大提升读二进制结构和分析保护模式底层的效率。
接下来开始做实验:



我们执行这个指令,由于前两个字节是004B,B的二进制是1011,所以后两位是1也即RPL=3,说明是3环程序,指向的是段描述符就是刚才在GDT表中加的段描述符(没有做笔记,因为不想回头翻阅视频了,记住就行),之后EIP会跳转到0041840D这个位置,并且段寄存器CS的值和EIP的值均会发生改变。我们运行一下,CS的值变化了,从1B改编为4B,EIP从004183D7变为了0041840D,说明CS的值修改已经成功了。


接下来接着做实验,第二张图中的改为目标00cf9b00是9,而刚才做实验的时候,那个位置(图一)是00cffb00,即f对应值是四个1,所以这里的DPL是11,那么权限级别就是3,用OD做测试的时候,CPL也是3(根据CS里面的最后两位4B的B判断出来的),所以申请的时候,权限检查是没有问题的。首先咱们这个00cffb00`0000ffff是一个非一致代码段,如果把这个非一致代码段的DPL权限改为0,那么此时就会加载失败了。因为非一致代码段要求我们的CPL和DPL一致并且RPL<=DPL才可以执行,而这里满足不了CPL和RPL一致,所以加载失败。
这样设计是很有道理的,如果这样跳转可以成功的话,谁能拦得住一个3环的程序跳到0环去执行呢?这时候就没人能拦截了,但是我们系统段的描述符,DPL一直就是0,如果在3环想要直接跳到系统段里面的代码去执行的话,是达不到目的的。原因就是这里有限制的、有权限检查的。对于非一致代码段,我们有一个直观的认识,接下来我们回想一下,之前将跨段跳转的时候,可以是一致的、也可以是非一致的,如果是非一致的,那么要求DPL=CPL并且RPL<=DPL;如果是一个一致代码段,就意味着我们可以在低权限访问一个高权限的代码段,换而言之,如果下图中的004B改为别的,改为指向一个一致代码段,那么即使DPL为0,仍然可以访问。

做一个实验,


可以看到要求b改为f,而后者的第五位是9,即DPL是0。即CPL的3环改为DPL的0环,这样就变成了一致代码段,如果不清楚为何这样改会导致DPL变为零或变成一致代码段,那是因为对段描述符属性的结构理解不够深入。建议回顾段描述符的结构。
如果正常来说,CPL为3环,想要访问DPL为0的0环程序,是权限不够无法访问的,但是如果是一致代码段,那么就可以访问了。

之前留下来的那道分析GDT表里面的数据段代码段那道题,可以发现windows大部分代码段都是非一致代码段,但是也有一致代码段。非一致代码段就是为了保护内核的就是内核的、用户的就是用户的,但是有一些通用的功能,希望3环也可以访问,那么这个时候就会提供一些通用代码段,通过一致代码段来进行修饰,这样3环的程序就可以访问一个比它权限更高的代码段,但是无论是访问一致的还是非一致的,CPL的权限并不会发生改变。如果硬是要CPL为3时访问DPL为0的东西,那么有一个方法可以用门来提升权限从而访问。

对比项 | 实模式 | 保护模式 |
最大寻址空间 | 1MB | 4GB及以上 |
地址结构 | 段<<4 + 偏移 | 段选择子 + 描述符 + 页表 |
保护机制 | 无(任意访问) | 强(多特权级、内存保护、进程隔离) |
寄存器宽度 | 16位 | 32/64位 |
进程隔离 | 无 | 有 |
多任务支持 | 无 | 有 |
启动用途 | 加电后初始化阶段 | 操作系统主运行阶段 |
编程/管理 | 直接操作物理地址,简单但危险 | 自动内存管理,安全且强大 |
例1:实模式下的代码跨段跳转
在实模式,每个段都有独立的16位段寄存器。代码跨段访问就是用far jump进行CS修改。
; 汇编语言示例(masm/tasm语法,伪代码注释)assume cs:code1code1 segment mov ax,seg code2 ; 取目标段地址 mov dx,offset entry2 ; 取目标入口偏移 ; 跨段跳转,CS=code2, IP=entry2 jmp dx:ax code1 endscode2 segmententry2: mov ah, 09h mov dx, offset message int 21h retmessage db '跨段代码执行成功', 0dh, 0ah, '$'code2 endsend
mov ax, seg code2:获取目标段基址jmp dx:ax:进行“段间跳转”,CS/IP被完整赋值,实现跨段执行
例2:保护模式下跨段访问(伪代码,逆向/内核更常见)
在保护模式下,“jmp far ptr”或“call far ptr”实现代码段切换(必须段描述符为代码段)。
jmp far ptr code2_selector:offset_entry2; code2_selector为GDT/LDT中的代码段选择子,; offset_entry2为目标代码段的偏移
一般编译器可以写成:
jmp far [selector:offset]
- 实验时,可以用调试器设置断点,查看跳转后CS/EIP变化。
- 也可以在内核或汇编bootloader里人为构造GDT描述符,完成段间代码跳转。