代码间的跳转(跨段跳转)

同时修改CS和EIP的指令
JMP FAR/CALL FAR/RETF/INT/IRETED
只改变EIP的指令
JMP/CALL/JCC/RET

长调用有两种段间跳转、跨段跳转

段间跳转 使用 长调用 JMP FAR
具体说明可以查看 https://www.cnblogs.com/q1NgX1ang/p/21823504

跨段跳转,它可以实现提权的效果,可以让当前的CPU从3环跳转到0环执行
实现跨段跳转 CALL FAR
同JMP FAR 后面为48位全指针(16位选择子 + 32位偏移)
CALL FAR 后面的32位是无效的,只是满足语法规范,32位偏移没有任何实际意义

既然我们要实现调用,那就离不开堆栈,代码和堆栈是密不可分的
我们先来分析一下 长调用 和 短调用 的堆栈变化

短调用堆栈的调用变化

o_2109221101557-1

执行 CALL [EAX], 把返回地址(当前地址 + CALL [EAX] 的指令长度)压栈(PUSH)

CALL 和 RET 一般都是成对出现

RET 就是恢复堆栈,把 返回地址出栈(POP)到 EIP

长调用堆栈的调用变化

长调用堆栈的调用变化存在两种情况
1.在3环调用(调用的函数代码位置在3环)
2.在0环调用(调用的函数代码位置在0环)

还记得跨段跳转的EIP没有用吧
那我们的偏移在哪里呢?
段描述符也没有提供偏移啊,指提供了Base

这里就需要映入门的概念了
CALL FAR 这里的选择子,会指向一个段描述符
这个段描述符 P = 0是系统段,type = 1100
根据下面这个表,我们可以知道此时的段描述符被解释成了32位-Bit Call Gate(调用门)
3284323-20260722211227953-1648734828

此时我们也将赋予对这个 段描述符的结构 不同的意义
此时的段描述符结构如下
o_2109221115257-4

我们会发现我们在找的偏移被定义在了高4字节的高16位 和 低4字节的低16位,拼在一起就是我们需要的Offset
低4字节的高16位,存储的是一个新的选择子(引导我们访问)

3环调用(不需要提权)

o_2109221102027-2

这里有一个问题
使用调用门不提权,也就意味这,调用门里面的段选择子,指向的段描述符 DPL == 3
那断点就不会被windbg捕获,而是直接被3环的调试器捕获。
我都使用调用门,还在3环执行,那还不如直接Call呢,吃力不讨好,所以这个就没有多大的实际意义了。

0环调用 (需要提权)

o_2109221102087-3

这里也有一个有趣的问题
在实验1里,我们构建了无参调用门,对比实验2的有参调用门,我们会发现有参情况下,会把先压入 espss,因为这里涉及到了换栈,我们在3环压入堆栈的函数,会被"镜像"一份到0环的堆栈,因为我们可能要在 0环 使用这些参数,根据实验我们知道了压栈的顺序。(个人理解,仅供参考)。

实验

1.构造无参调用门(提权、无参数)
#include "stdafx.h"

void __declspec(naked) test()    //生成裸函数,不生成 ebp寻址 等代码
{
    _asm
    {
        int 3;    //让断点停在WinDbg中
        retf;
    }
}

const char buffer[6]={0,0,0,0,0x4B,0};

int main(int argc, char* argv[])
{

    _asm
    {
        call fword ptr ds:[buffer];    //在此处下断点,填写正确的调用门
    }

    return 0;
}

调用门的地址 Address = 0x00401010
PixPin_2026-07-26_09-50-29
Index = 9处 构建调用门

调用门对应的段描述符为 0040EC00 00081010
PixPin_2026-07-26_09-21-08

PixPin_2026-07-26_09-47-58

我们观察堆栈的变化,会发现CS段寄存器 和 返回地址 SS ESP被压入堆栈
调用门提权无参堆栈

验证一下返回地址(此处我发现了点错误进行了修改,所以上面的图和下面的图返回值不是一样的)
PixPin_2026-07-26_10-00-19

执行 retf 后,出现回到VC6(3环)

运行后发现,断点断在了windbg
PixPin_2026-07-26_09-53-13

2.构造有参调用门(提权、有参数)

加上参数,查看反汇编,我们会发现 test函数 的地址还是0x00401010,所以我们不需要更改调用门

#include "stdafx.h"

void __declspec(naked) test()    //生成裸函数,不生成 ebp寻址 等代码
{
    _asm
    {
        int 3;    //让断点停在WinDbg中
        retf 0xC;
    }
}

const char buffer[6]={0,0,0,0,0x4B,0};

int main(int argc, char* argv[])
{

    _asm
    {
		push 1;
		push 2;
		push 3;
        call fword ptr ds:[buffer];    //在此处下断点,填写正确的调用门
    }

    return 0;
}

PixPin_2026-07-26_10-10-36

我们会发现,怎么没有参数!我明明压栈了啊?
待我研究研究
蓝屏了.....
PixPin_2026-07-26_10-16-31

我们忽略了一个很关键的东西!
在调用门结构中有一个成员是Param.Count这个是用来记录参数的个数
我们并没有更改,所以看不到有参数入栈的痕迹。

我们需要更改调用门
原:0040EC00 00081010
现:0040EC03 00081010
更改后运行程序,然后查看堆栈,我们可以看到压入的参数,还有调用者的 ss段寄存器 以及 ESP栈顶指针
PixPin_2026-07-26_10-32-35

我们去验证一下
PixPin_2026-07-26_10-37-02
PixPin_2026-07-26_10-37-53

通过上述的实验,我们可以论证0环堆栈存储数据的顺序
依次压入 SS ESP 参数(如果存在) CS 返回地址

3.分析如下代码的意义

更改函数test的代码

void __declspec(naked) test()    //生成裸函数,不生成 ebp寻址 等代码
{
    _asm
    {
		pushad;
		pushfd;
        int 3;    //让断点停在WinDbg中
		popfd;
		popad;
        retf 0xC;
    }
}

我们在函数中加入了pushad pushfd popfd popad
pushad 8个通用寄存器压栈
pushfd 标志寄存器压栈
popfd 标志寄存器出栈
popad 8个通用寄存器出栈

执行这些指令的主要作用是"保护现场",对应一些场景是必须的
类似于加了壳的程序,壳程序一开始也会保存通用寄存器的初始值,这样在解密完受保护的数据之后,寄存器的初始值可以还原到程序开始运行的状态

那这里的寄存器数据压入0环堆栈的状态是什么样的呢?实验看看。
PixPin_2026-07-26_19-28-05
修改后的代码,地址并没有改变,所以我门还是可以使用之前的调用门。
我们运行代码,分析windbg断点的位置,查看寄存器,观察堆栈
PixPin_2026-07-26_19-39-39
我们可以清晰的看到 通用寄存器 和 标志寄存器 被压栈了

以我们对堆栈的了解,我们该怎么在test函数中使用 参数 或者 寄存器 呢?
我们以现在的代码为例,让 eax 依次保存参数1、2、3

mov eax, [esp + 0x4 + 8 * 0x4 + 0x8 + 0x8]
mov eax, [esp + 0x4 + 8 * 0x4 + 0x8 + 0x4]
mov eax, [esp + 0x4 + 8 * 0x4 + 0x8 + 0]

我们验证一些我们的代码..... 虚拟机卡死了....这玩意真不听话!
重启之后
PixPin_2026-07-26_20-10-11
我们会发生test函数的地址发生了变化,所以我们也要调整 调用门 的 Offest
更改如下
0040EC03 0008D6F0
我们更改GDT表,进行实验。
PixPin_2026-07-26_20-14-41

运行代码,在windbg下单步步过,观察 eax 的值
PixPin_2026-07-26_20-34-42

PixPin_2026-07-26_20-36-13

我们也实现了 eax 依次保存参数1、2、3,还是很简单的吧!

posted @ 2026-07-26 20:57  书与兔子  阅读(4)  评论(0)    收藏  举报