代码间的跳转(跨段跳转)
同时修改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位偏移没有任何实际意义
既然我们要实现调用,那就离不开堆栈,代码和堆栈是密不可分的
我们先来分析一下 长调用 和 短调用 的堆栈变化
短调用堆栈的调用变化

执行 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(调用门)

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

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

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

这里也有一个有趣的问题
在实验1里,我们构建了无参调用门,对比实验2的有参调用门,我们会发现有参情况下,会把先压入 esp 和 ss,因为这里涉及到了换栈,我们在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

在Index = 9处 构建调用门
调用门对应的段描述符为 0040EC00 00081010


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

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

执行 retf 后,出现回到VC6(3环)
运行后发现,断点断在了windbg

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;
}

我们会发现,怎么没有参数!我明明压栈了啊?
待我研究研究
蓝屏了.....

我们忽略了一个很关键的东西!
在调用门结构中有一个成员是Param.Count这个是用来记录参数的个数
我们并没有更改,所以看不到有参数入栈的痕迹。
我们需要更改调用门
原:0040EC00 00081010
现:0040EC03 00081010
更改后运行程序,然后查看堆栈,我们可以看到压入的参数,还有调用者的 ss段寄存器 以及 ESP栈顶指针

我们去验证一下


通过上述的实验,我们可以论证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环堆栈的状态是什么样的呢?实验看看。

修改后的代码,地址并没有改变,所以我门还是可以使用之前的调用门。
我们运行代码,分析windbg断点的位置,查看寄存器,观察堆栈

我们可以清晰的看到 通用寄存器 和 标志寄存器 被压栈了
以我们对堆栈的了解,我们该怎么在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]
我们验证一些我们的代码..... 虚拟机卡死了....这玩意真不听话!
重启之后

我们会发生test函数的地址发生了变化,所以我们也要调整 调用门 的 Offest
更改如下
0040EC03 0008D6F0
我们更改GDT表,进行实验。

运行代码,在windbg下单步步过,观察 eax 的值


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

浙公网安备 33010602011771号