要点回顾
前文提到了调用门的执行过程,那么本篇文章就一起来了解一下:调用门的参数传递。
代码回顾
void __declspec(naked) GetRegister()
{
__asm
{
pushad
pushfd
mov eax,0x8003f00c //读取高2G内存
mov ebx,[eax]
mov dwH2GValue,ebx
sgdt GDT; //读取GDT
popfd
popad
retf //注意返回, 不能是ret
}
}
上面的代码在前文中出现过,通过调用门提升到了0环权限后执行。
真正起作用的是 mov eax,0x8003f00c、mov ebx,[eax]、mov dwH2GValue,ebx、sgdt GDT。
其中 sgdt GDT的sgdt并不是读取GDT表,而是读取GDTR这个寄存器。
SGDT这个指令通常是给操作系统软件使用的,但是如果要在应用层去使用它,也不会导致异常。
也就是说 sgdt GDT 这条代码在3环没有提权的情况下也是可以使用的。
调用门描述符
前文提到过调用门的门描述符S位必须为0,当它为0的时候才能说明是一个系统段描述符,而不是代码段/数据段描述符,且TYPE位为1100才是一个调用门。
构造有参数的调用门门描述符
0040EC03`00081030:3个参数。
kd> eq 8003f048 0040EC03`00081030
代码论证
#include "stdafx.h"
#include <windows.h>
DWORD x;
DWORD y;
DWORD z;
void __declspec(naked) GateProc()
{
__asm
{
pushad
pushfd
//以下是读取参数并赋值给全局变量
mov eax,[esp+0x24+0x8+0x8]
mov dword ptr ds:[x],eax
mov eax,[esp+0x24+0x8+0x4]
mov dword ptr ds:[y],eax
mov eax,[esp+0x24+0x8+0x0]
mov dword ptr ds:[z],eax
//以上是读取参数并赋值给全局变量
popfd
popad
retf 0xC //注意堆栈平衡, 写错直接蓝屏
}
}
void PrintRegister()
{
//打印确认参数是否正确
printf("%x %x %x\n",x,y,z);
}
int main()
{
char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[6] = 0x48;
__asm
{
push 1
push 2
push 3
call fword ptr[buff]
}
PrintRegister();
getchar();
return 0;
}
注意:当使用有参数的调用门,参数是需要自己压栈的,如代码中压入了1、2、3,然后才调用。
总结
- 当通过调用门,权限不变的时候,会 PUSH 两个值:CS、返回地址。新的CS的值由调用门决定。
- 当通过调用门,权限改变的时候,会 PUSH 四个值:SS(原来的堆栈段选择子)、ESP(原来的栈顶)、CS(原来的段选择子)、返回地址。新的CS的值由调用门决定,新的SS和ESP由TSS提供。
- 通过调用门调用时,要执行哪行代码由调用门决定,但使用 RETF 返回时,由堆栈中压入的值决定。也就是说:进入时只能按照固定路线行动,离开时可以翻墙(只要改变堆栈里面的值就可以前往任意地方)。
- 可不可以再建立一个门出去呢?也就是使用 CALL。答案是可以,前门进后门出。
思考
思考一:
代码论证中的代码 pushad、pushfd、popad、popfd有必要吗?
思考二:
代码论证中的以下代码中标红的部分读取参数为什么要这么写呢?
mov eax,[esp+0x24+0x8+0x8]
mov dword ptr ds:[x],eax
mov eax,[esp+0x24+0x8+0x4]
mov dword ptr ds:[y],eax
mov eax,[esp+0x24+0x8+0x0]
mov dword ptr ds:[z],eax