前言
在上一节中,我们的实验通过修改GDT表,验证了CALL FAR指令在一致与非一致代码段中的权限检查规则。结论表明:在未利用操作系统特定机制的前提下,常规的远调用无法实现特权级的提升。
除了CS和EIP寄存器的状态更新,执行流的跳转同时也涉及堆栈状态的变化。普通的短调用(CALL)与长调用(CALL FAR)在底层硬件的压栈机制上存在区别。本节我们深入讨论这套堆栈结构的变化过程,为后续研究系统级提权机制的必要前提。
一、 短调用(Short Call):段内调用
短调用是应用层程序中最常见的函数调用方式。
- 汇编指令 :
CALL 立即数 / 寄存器 / 内存(例如call 0x401000) - 硬件逻辑 :短调用属于段内跳转。其目标代码与当前代码位于同一物理段,
CS寄存器保持不变,仅更新EIP寄存器的值。 - 堆栈动作 :由于未发生跨段跳转,硬件自动将当前的 EIP(即CALL 指令的下一条指令地址)压入堆栈(
PUSH EIP)。 - 返回机制 :目标函数执行完毕后,配合使用
RET指令,CPU从栈顶弹出4字节数据并赋值给EIP,从而恢复原始执行流。
二、 长调用(Long Call):同特权级段间调用
当执行流需要跨越物理段(即更新 CS 寄存器)时,需使用长调用指令。
- 汇编指令 :
CALL FAR CS:EIP(例如call fword ptr [far_pointer]) - 硬件逻辑 :即使目标代码段与当前代码段同属Ring 3(权限校验通过),只要重新加载了
CS寄存器,CPU必须保存调用前的代码段状态。 - 堆栈动作 :CPU 硬件会自动执行双重压栈操作:先压入调用前的
CS,再压入调用前的EIP(PUSH CS->PUSH EIP)。 - 返回机制 :由于堆栈中包含了
CS和EIP,常规的RET指令无法正确平衡堆栈。目标函数必须使用RETF(Return Far) 指令。RETF指令会依次从堆栈中弹出 4 字节赋值给EIP,再弹出 4 字节赋值给CS,以恢复调用前的执行上下文。
三、 实验验证:长短调用堆栈状态对比
为验证底层硬件的压栈行为,编写了以下代码。将 CALL FAR 的目标段选择子设置为 0x1B(Windows默认的Ring 3代码段),以执行一次合法的同特权级段间跳转。实验代码如下所示:
c
// Pm_Lab6.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>
#pragma pack(push, 1)
struct FWORD_PTR
{
DWORD offset;
WORD selector;
};
#pragma pack(pop)
__declspec(noinline) void TestShortCall()
{
printf(" -> Inside Short Call.\n");
}
void __declspec(naked) TestLongCall()
{
__asm
{
// nop指令作为特征标识
nop
nop
nop
retf
}
}
int main()
{
printf("[*] Starting Stack Behavior Experiment...\n");
// 触发短调用
TestShortCall();
// 触发长调用
FWORD_PTR far_pointer;
far_pointer.offset = (DWORD)TestLongCall;
far_pointer.selector = 0x1B;
__asm
{
call fword ptr [far_pointer]
}
printf("[*] Execution Finished Safely!\n");
system("pause");
return 0;
}
场景一:短调用(段内跳转)的堆栈现场
我们在OllyDbg中定位到call TestShortCall指令。记录执行前的堆栈指针状态,随后单步步入该函数,此时观察堆栈窗口的变化:

底层数据分析:由于未发生跨段跳转,CPU硬件的现场保护机制仅触发了最基础的动作:将CALL指令的下一条指令地址(即返回的EIP)压入栈顶。在目标函数末尾,常规的RET指令即可弹出该4字节数据,完美恢复执行流。
场景二:长调用(段间跳转)的堆栈现场
我们将执行流推进至跨段调用指令call far fword ptr ss:[ebp-8](长调用)。虽然目标选择子0x1B仍属Ring 3特权级,但对CPU而言,CS寄存器的重载动作已经触发。单步步入该裸函数,再次观察堆栈窗口:

底层数据分析:当段间跳转发生时,硬件强制执行了双重压栈保护(PUSH CS -> PUSH EIP)。堆栈内存中连续的8个字节记录了调用前的完整段落与偏移信息。面对长调用的堆栈结构,使用RET指令将导致堆栈失衡。程序必须通过RETF(Return Far)指令,依次弹出EIP与CS,能够正确平衡堆栈并返回。
四、 总结
- 短调用 :段内跳转,仅保护
EIP。 - 同级长调用 :平级跨段,保护
CS+EIP