保护模式-第五讲-门-调用门

保护模式-第五讲-门-调用门

一丶长调用与短调用

1.1 长跳转 与长调用

在上一讲 实现了利用 长跳转 来实现了段间的跳转

jmp far 0x00xx:xxxx地址

并且构造段描述符. 将段描述符放入GDT表中. 构造段选择子来实现了 段间跳转.

但是长跳转只限于 段间跳转. 也就是一个段中. 因为在一个段中. 最后的偏移 加 段描述符.base才能构成真正的跳转地址.

那么如果想实现跨段调用. 就必须要用长调用

长调用 本质就是 call 与 ret的组合. 只不过有些许不同 因为会堆栈产生影响

call far 指令来实现长调用

1.1.1短调用

短调用本质就是 call + ret组合.

查看如下堆栈图:

在我们正常调用的时候对战图就会如上. 首先有参数 然后紧接着返回地址.在每调用call 之前.参数位置就是esp的位置call 之后 esp就递减 改变的寄存器有 ESP EIP 这就是短调用. 在ring3 x86下.遇到的call 基本都是段调用

call 其实我们要进行分开操作可以是如下

sub esp,4
mov eip,xxxx
push 下一行地址

call 只不过将三行汇编进行了总结.变成了一行.

ret指令的原理

mov eip,返回地址
如果是stdcall add esp,xxx
jmp eip

1.1.2 长调用 (跨段不提权的调用)

提权:

 提权之后.在ring3下可以使用特权指令.跟内核权限一样

指令给格式为:

call cs:eip (EIP并不使用)

EIP不会使用的. cs是一个段选择子. 这个段选择子还是去查表. 查询的是GDT表.

只不过有些许不同.查询出来的GDT表中的段描述符.也是8个字节. 不过结构确实 门描述符

结构

也就说.长调用 最终调用在哪里. 是由调用们(段描述符)来指定的. 而不是EIP执行的. 所以说

EIP是废弃的

跨段不提权的意思:

我们经过上面引出了 长调用. 长调用其实是去GDT表中查表. 查到的段描述符记录着一个

地址.以及对应的选择子.这个后面会说. 跨段不提权的意思就是. 你当前调用的环境CPL 是3

而调用门中记录的 段选择子以及对应的地址.DPL也是三. DPL为3 代表你CPL可以访问.

虽然跨段了.但是权限还是一样的. 所以这就叫做跨段不提权. 但是如果我们将构造的调用门的

DPL改为0. 那么在进行调用的时候就会发生提权了. 当然我们的RPL CPL也要进行修改.

跨段不提权的堆栈图

与上面唯一不同的是.参数要进行保存.保存我们的CS寄存器.

发生改变的寄存器有 ESP EIP CS

在段调用中.我们是 call 与 ret 配合使用. 在长调用中. 比如是 retf 这点需要注意.

retf执行之后.我们的堆栈会产生如下结果

1.修改esp的值

2.将保存的cs的值 还原.

1.1.3 长调用(跨段并且提权)

指令格式是一样. 堆栈图进行了改变

call 执行之后的对战图

可以看到,在进行call 调用之前.会保存调用者的SS ESP CS

为什么要保存堆栈. 原因是 当跨段提权的时候. 堆栈不是ring3的堆栈了. 也就是 调用call 之前的esp 是ring3

的堆栈 .而在保存调用者的ss esp cs的时候堆栈已经切换到 ring0的了.

1.1.4 总结

1.跨段调用的时候. 一旦有权限的切换.那么就会切换堆栈

2.如果cs的权限一旦改变.那么对应的ss权限也要该表. cs与ss等级必须是一致. 这是inter规定

3.跨段跳转(jmp far) 只能跳到同级别 非一致代码段. 但是call far则可以通过调用们 提权. 提升CPL权限.

之前我们做实验 jmp far 跳转的时候.如果段描述如的DPL修改了. 就算你ring3的 RPL修改了.也是没用了.

因为你的CPL还是3 也就是低权限. 所以我们要想办法提权.

二丶调用门

2.1 调用门的执行流程

调用门 依赖于 call far 指令.指令格式为

call cs:EIP

当执行这条指令的时候指令的执行流程如下

1.根据CS段选择子 查询GDT表. 找到对应的段描述符. 这个段描述符一样也是8个字节. 不过这个段描述符要解析为

调用门描述符

2.调用们描述符中 存储这个一个你给定的地址. 以及一个代码段的段选择子. 以及DPL等权限

3.调用门中有地址. 也有段选择子. 再根据段选择子查询对应段描述符. 从对应的段描述符中 找出记录的BASE

以 BASE+调用门中记录的地址 进行调用. 记录的地址就是我们真正要执行的地址. 只不过我们要 以base+偏移的方式进行组合.组合为一个真正的地址.

2.2 调用门描述符

根据inter手册所说.调用们总共完成了六种功能

1.确定了你要访问的代码段(也就是低16-31位 记录了一个代码段的选择子.可以继续进行查表得出代码段)

2.定义了在指定的代码中的一个例程入口 (也就是指定了一个偏移地址.我们知道都是段.base+偏移进行调用的

高16-31位 记录一个偏移地址的高地址,低32位字节中的 0-15位 记录了偏移的低地址 也就是我们要执行一个函数

要把这个函数的地址填写到这里.根据高低位分开)

3.指明了该例程的特权级别 (也就是DPL )

4.如果栈发生了切换. 要确定在栈之间拷贝的 可选参数的个数 (也就是高位 0-4 代表了参数个数)

5.定义了栈的尺寸. 16位执行16位的栈 32执行32的栈

6.确定了调用们描述符是否有效(P位)

我们之前的段描述符 有 s位以及type位. 在调用门描述符里面. 统一进行写死了. 第12位 = 0 11-8固定的是1100

因为 s位 我们学过段描述符 当 s位 = 0 代表是系统段描述符. 调用门描述符就是系统段描述符. 而当其type = 1100的时候.才代表是一个调用门描述符

2.3 调用门进行代码段访问的流程

调用门进行代码段访问的时候会执行如下流程

1.验证CPL当前的特权级别

2.验证调用门的段选择子的RPL

3.验证调用门的 DPL权限

4.验证以下目标代码段中的 段一致性 (这句意思就是 我们门不是保存了一个代码段的段选择子吗. 段选择自己进行查表得出段描述符. 然后验证这个段描述符的 CFLAG)

2.4 构造门描述符表

根据门描述表我们先进行构造

1.31-16位 为Base位. 我们不知道首先设置为0 0x0000

2.p DPL 以及第13位. 其中P DPL都为1 DPL位0的话我们ring3无法访问. 的初始 1110 16进制 = 0xE

3.type位是写死的. 所以是 0x1100 16进制 = 0xC

4.第八位到第0位.其中包括参数个数.以及一些标志.这里没有都设置为0 = 0x00

高四个字节总结出来位

0x0000EC00

低32位构造

1.31-16段选择子 设置为8 拆分开 =1000 也就是第一项.RPL = 0 找到的GDT表也就是第一项(注意从0开始)

这里设置位GDT[1] 要注意GDT[1]的DPL权限. 我们上已经构造 jmp far的时候讲过了.

而我们说的调用们提权与不提权 就是这里的段选择子的设置. 0x008代表提权. RPL = 0 如果不想提权 就寻找ring3的可访问的代码段. 比如 0x1B 二进制 = 11011 RPL = 11 TI = 0 高两位也是11 也即是3 也就是寻找GDT[2]

2.跳转地址的低2位. 没有也设置为0

总结出来段描述符 = 0x0000EC00~0x00080000

这个就是一个段描述符.但是我们没有指定地址. 如何指定地址.需要我们在ring3中写代码.

然后将函数地址填入到里面.

比如你ring3的函数地址为 0x00401230 那么构造到门描述符中就是

0x0040EC00 ~ 0X00081230 此时将调用们写入到GDT表中即可. 然后ring3 使用 Call Far cs:xxxx 来调用即可. 其中cs是段选择子. 这个段选择子就是你写入GDT表中的门描述符

2.5 代码实现 调用门 无参提权

首先调用门我们可以构造出来

0x0040EC00 ~ 0X00081230 当然我程序中的地址可能不一样.这里举个例子

然后将调用们写入到 GDT表中空项中

构造选择子给ring3 使用

其中比如0x401230 是我们在ring3看的. 我们想让它跳转到哪里. 我们就切换到反汇编窗口进行拷贝地址. 然后设置进去.

所以我们的ring3调用代码.首先就要先调试以下获取 要跳转的函数地址.

代码如下:

#include <stdio.h>
#include <WINDOWS.H>
#include <STDLIB.H>

//我们要跳转的地址在这里. 地址是 0x00401020
__declspec(naked) void run()
{
	__asm 
	{
		int 3;

		retf;
	}
}
int main(int argc, char* argv[])
{
	char szAddress[6] = {0};

	*(unsigned int*)&szAddress[0] = 0x00000000;  //EIP No use  EIP 没有 使用
	*(unsigned short *)&szAddress[4] = 0x00;     //设置调用门 所在的段描述符的选择子 待我们将门描述符设置到GDT表中在修改

	_asm
	{
		// call far fword ptr ss:[ebp-xxx];
		call  fword ptr[szAddress];
	}
	return 0;
}

代码有了.将门描述符写入到GDT表中.

将门描述符写入到GDT表中的第 13项. 因为下标是从0开始. 所以下标为12才为我们实际的段描述符位置. 所以段选择子

构造为 12 RPL = 0 TI = 0 12的二进制为 1100 组合起来 TI RPL = 1100000 = 0x60 所以在代码中我们设置我们的门描述符的段选择子为0x60

当我们进行调用的时候发现我们的内核调试器会优先的捕获到. 可以在内核调试其中看到 我们ring3的代码. int 3 与 retf

这里就说明了我们ring3中调用的函数 是具有内核权限的了.

因为我们是提权了. 调用们中的记录的段选择子是提权的 所以我们看下堆栈

分别记录了 返回地址 调用者的CS 调用者的ESP 调用者的SS

2.6 构造有参调用门

之前讲的调用们是无参数的. 那么有参数的也需要自己构造其中 在调用门的 参数位置我们设置一个参数个数. 然后在调用的时候自己去Push 以及注意平栈

如调用门为: 0x0040EC03 ~ 0X0008D480 这个调用们指定了我们 参数个数为三个. 一个四个字节大小.

2.7 有参调用门代码

// 11.cpp : Defines the entry point for the console application.
//

#include <stdio.h>
#include <WINDOWS.H>
#include <STDLIB.H>


DWORD x;
DWORD y;
DWORD z;

__declspec(naked) void run()
{
	__asm 
	{
		pushad;
		pushfd;
		mov eax,[esp + 0x24 + 0x8];
		mov dword ptr ds:[x],eax;

		mov eax,[esp + 0x24 + 0xC];
		mov dword ptr ds:[y],eax;

		mov eax,[esp + 0x24 + 0x10];
		mov dword ptr ds:[z],eax
		
		popfd;
		popad;
		retf  0xC;   //压入 三个参数 所以平栈的时候也就是3个参数 x86下一个参数4个字节 所以是0xC 
	}
}

void printXyz()
{
	printf("%d,%d,%d \r\n",x,y,z);
}
int main(int argc, char* argv[])
{
	char szAddress[6] = {0};

	*(unsigned int*)&szAddress[0] = 0x12345678;  //EIP No use  EIP 没有 使用
	*(unsigned short *)&szAddress[4] = 0x60;     //设置调用门 所在的段描述符的选择子 

	_asm
	{
		// call far fword ptr ss:[ebp-xxx];
		push 1;
		push 2;
		push 3;
		call  fword ptr[szAddress];
	}

	printXyz();
	return 0;
}


2.8 有参调用门的堆栈

当我们进行有参数调用的时候查看堆栈.

堆栈已经完全改变了. 特别是我们执行完 pushad pushfd 之后.

我们先看一下原始堆栈

画图看一下

查看堆栈图可以看到. 我们的返回地址下面.就是调用者CS. 而调用者CS下面 变成了参数了.而不是调用者的ESP了.

所以我们想要寻到参数的值. 就必须 使用 [esp + 8 + 4] = 参数1 +8 = 参数2 + c = 参数3 +10 = 调用者ESP +14 = 调用者SS

而我们如果pushad pushfd之后.那么显而易见. pushad 是所有通用寄存器入栈. 也就是8个 pushfd 是EFlag入栈. 所以总共

是入栈9个参数. 9个参数之后才是返回地址. 其它依次类推. 所以我们想要寻得参数的值 才必须要写为如下

0x24 = (pushad + pushfd) = 9个入栈. 一个四个字节  9 * 4 = 36 16进制 = 0x24

[esp + 0x24]   == 返回地址位置

[esp + 0x24 + 4] == 调用者CS
[esp + 0x24 + 8] == 参数1
[esp + 0x24 + C] == 参数2
[esp + 0x24 + 10] == 参数3
依次类推
也可以写为如下.直接寻找参数的值
[esp + 0x24 + 8 + 0] = 参数1
[esp + 0x24 + 8 + 4] = 参数2
[esp + 0x24 + 8 + 8] = 参数3

三丶总结

1.调用门提权的话 直接在 门描述符中记录的选择子进行设置即可提权

2.调用门的本质就是 记录一个函数地址. 以及一个代码段的段选择子. 当使用调用门的时候. 本质就是 让代码段的.base+ 记录的偏移进行跳转

3.有参的调用门其堆栈是已经完全改变了. 所以要注意. 因为ring3进入内核权限了. 所以要使用寄存器 必须 pushad pushfd 用来保存.

posted @ 2020-07-07 23:30  iBinary  阅读(1281)  评论(0编辑  收藏  举报