前言
在上一节中,我们验证了数据段的访问规则(max(CPL, RPL) <= DPL)。CPU借助这套机制,禁止Ring 3用户态程序越权读取Ring 0内核数据。
但在底层安全攻防中,攻击者的思路更加灵活:既然我不能直接读写内核数据,那我能不能把代码的执行流(EIP)直接跳转到内核的代码段里,让CPU以Ring 0的特权级去帮我执行恶意代码呢?
本节将深入讨论代码段的跨段跳转规则与底层执行流程。我们将会发现,CPU硬件在设计之初就想到了这种提权手段。通过引入一致代码段与非一致代码段的概念,构建了一套执行流的隔离。
一、执行流跳转的本质:指令分类与底层流程
在汇编层面,能够改变EIP的指令有很多,但在保护模式下,它们被划分为两个层级:
- 段内跳转(短跳转/近跳转):例如JMP、CALL、JCC、RET。这类指令仅改变EIP寄存器。因为不涉及CS(代码段寄存器)的更换,所以不会引发特权级(CPL)的变化,属于同层级的跳转。
- 段间跳转(长跳转/远跳转):例如JMP FAR、CALL FAR、RETF、INT等。这类指令会同时修改CS和EIP寄存器,这意味着执行流想要跨越当前的物理段。
当我们执行一条远调用指令(例如CALL FAR 0x4B:0x12345678)时,CPU在底层的标准执行流程如下:
- 段选择子拆分:将0x4B拆分为Index=9, TI=0, RPL=3。
- 查表与权限检查:通过GDT表找到对应的代码段描述符。CPU开始进行严格的特权级检查(对比当前CPL、选择子RPL与目标代码段的DPL)。
- 地址计算:权限检查通过后,获取目标代码段的Base(基址),加上指令提供的偏移地址(0x12345678),最终跳转到线性地址进行执行。
那么,特权级检查应该遵循什么规则?这完全取决于目标代码段的属性分类。
二、一致代码段与非一致代码段
与数据段单一的保护规则不同,CPU将代码段分类为两种类型。
1. 非一致代码段(普通代码段)
绝大多数的操作系统核心代码段(包括Windows 内核)都属于非一致代码段。它的设计理念是绝对的物理隔离。
- 访问规则:仅允许同级访问,严格禁止不同特权级之间的相互跨越(内核态不可直接调用用户态,用户态也不可直接调用内核态)。
- 权限公式:必须严格满足CPL == DPL且RPL <= DPL。
2. 一致代码段(共享代码段)
一致代码段的设计初衷,是为了提供一些允许低权限进程调用的底层共享系统函数。规则如下:
- 向下保护:高特权级程序不能跳转到低特权级的代码段(防止高权限降级或乱入)。
- 向上开放:低特权级程序(如Ring 3)可以跳转到高特权级(如Ring 0)的一致代码段中执行。
- 权限公式:仅需满足CPL >= DPL。
- 注意: 直接对代码段进行JMP FAR或CALL FAR操作,无论目标是一致还是非一致代码段,当前特权级(CPL)都不会发生改变,也就是说普通的远跳转指令永远不能用于系统提权。
三、代码实验与核心验证
- 实验前置知识:裸函数(Naked Function)
在构造远跳转的跳板函数时,我们必须使用MSVC提供的 __declspec(naked) 关键字。因为编译器默认会为函数生成push ebp等栈操作指令,而远调用返回时必须使用特殊的retf指令。使用裸函数可以完全接管生成的汇编代码,避免栈失衡程序崩溃。
实验一:跳入Ring 0非一致代码段
- WinDbg构造段描述符
kd> eq ffffffff80b99048 00CF9A00`0000FFFF
kd> dg 0x48
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0048 00000000 ffffffff Code RE 0 Bg Pg P Nl 00000c9a
- 代码实测
#include "stdafx.h"
#include <stdlib.h>
#include <Windows.h>
#pragma pack(push, 1)
struct FWORD_PTR
{
DWORD offset;
WORD selector;
};
#pragma pack(pop)
DWORD g_success = 0;
void __declspec(naked) TargetFunction()
{
__asm
{
mov g_success, 1
retf
}
}
int _tmain(int argc, _TCHAR* argv[])
{
FWORD_PTR far_pointer;
far_pointer.offset = (DWORD)TargetFunction;
far_pointer.selector = 0x48;
printf("[Action] Executing CALL FAR to Non-Conforming Segment...\n");
__try
{
__asm
{
call fword ptr [far_pointer]
}
if (g_success == 1)
printf("Execution Success!\n");
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("[Result] CPU Intercepted! Access Violation (Crash).\n");
}
system("pause");
return 0;
}
实验结果如下所示:

结论:程序触发异常。因为当前CPL(3) != DPL(0),非一致代码段严格的规则直接在硬件层面拒绝了越权调用。
实验二:跳入Ring 0一致代码段
- WinDbg构造段描述符
我们与实验保持一致,将GDT表的第9个位置修改为Ring 0的一致代码段。如下所示:
kd> eq ffffffff80b99048 00CF9E00`0000FFFF
kd> dg 0x48
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0048 00000000 ffffffff Code RE 0 Bg Pg P Nl 00000c9e
- 代码实测
我们只需更改裸函数TargetFunction,在这个裸函数增加只有Ring 0权限才能执行的内核内存读取指令。如下所示:
void __declspec(naked) TargetFunction()
{
__asm
{
mov g_success, 1
// 尝试读取系统内核空间高位地址 (GDT表基址)
// 如果我们真的提权到了0环,这里代码顺利执行;
// 如果依然是3环,硬件将触发内存访问违规
mov eax, dword ptr ds:[0x80b99000]
retf
}
}
实验结果如下所示:

结论:程序触发异常。虽然跳转符合CPL(3) >= DPL(0)成功进入了0环代码段,但一致代码段的特权级保持原则使得当前CPL,所以权限依然为在3环。当我们试图读取Ring 0的内核数据时,被硬件内存保护机制拦截。

407

被折叠的 条评论
为什么被折叠?



