windows x32调用门/中断门实现 ring3提权

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows x32调用门/中断门实现 ring3提权相关的知识,希望对你有一定的参考价值。

概述

调用门是Intel提供的一个机制,用于控制不同权限级(ring0-ring3)的程序函数调用。简单点就是提供了一个ring3 调用ring0 函数的机制。

intel手册volume3-Chapter 5.83描述如下

Call gates facilitate controlled transfers of program control between different privilege levels.
They are typically used only in operating systems or executives that use the privilege-level protection mechanism

详细可参阅intel Volume3-Chapter5

实现调用门需要构造一个调用门描述符Call-Gate Descriptor放入GDT或者LDT中。


Segment Selector 指向代码段的段选择子,P表示门是否有效,如果栈转化那么Param Count 指示要从调用方栈拷贝到目标栈的word(16位)数。type固定为1100.

typedef struct _GateDescriptor 
	unsigned int offset_low16 : 16;
	unsigned int selector : 16;
	unsigned int Param_count : 5;
	unsigned int res : 3;
	unsigned int type : 4;
	unsigned int s : 1;
	unsigned int dp1 : 2;
	unsigned int p : 1;
	unsigned int offset_hei16 : 16;
GateDescriptor;

栈转化

如果调用的代码段是ring3权限(CPL),而目标调用门是ring0(RPL)权限,栈区是不共享的因此需要将栈区的参数拷贝到目标栈中。

因此调用调用门intel会自动按照如下图进行栈拷贝。

但是你需要注意FS寄存器intel并没有保存,但是window在ring3程序fs存储的TIB,在ring0存储KPCR,也就是说在window下你需要手动处理。

调用调用门

call 调用门选择子
jmp 调用门选择子

比如下面的汇编调用0x4bh的选择子

call    0x004B:00000000

但是VC编译器无法编写上面的指令,你只能利用下面的汇编指令

__asm 
		_emit 0x9a;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x4b;
		_emit 0x00;
	
		//call    0x004B:00000000;
	

调用门函数的编写

首先函数一般使用裸函数编写,结尾使用retf返回,如下图所示

void Syscall() 
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);

//对外提供的调用门函数
__declspec(naked) void SyscallProxy() 
	__asm 
		push ebp;
		mov ebp, esp;
		//windwow ring 0 fs应该指向30h
		//注意!! windbg调试内核的话会自动修改fs为30h
		mov ax, 30h;
		mov fs, ax;
		call Syscall;
		mov esp, ebp;
		pop ebp;
		//这里要返回到ring 3所以应该还原fs
		//ring 3程序固定指向3bh
		mov ax, 3bh;
		mov fs,ax;
		retf 0;
	


为什么要使用裸函数?假设我们代码如下:

void SyscallProxy()

对应的汇编指令

可以发现返回的时候使用ret而不是retf,两个指令最大的差别在于是否会修正cs等。如果直接使用原始的函数那么调用门将不会正确的返回。(cs没有被正确的修正)

实现调用门

我们通过一个驱动程序来编写一个调用门函数。

首先我们需要查看系统哪个GDT表项是空的,让我们插入自己实现的调用门描述符。

//查看gdt表 0到100的表项
dg 0 100


我们注意到0x48是空白的,所以我们可以利用这个进行插入我们自己的调用门描述符。0x48对应的ring3 的段选择子是0x4bh,
计算过程如下:
首先段选择子格式

index: 1001(第9个gdt项)
TI :0
RPL:11
RPL 表示当前权限因为是ring3 所以是11
组合上面的数据后就是 1001011 也就是4bh


#pragma push
#pragma pack(1)
typedef struct _GDTR 
	short limit;
	int base;
GDTR;
#pragma pop

typedef struct _GateDescriptor 
	unsigned int offset_low16 : 16;
	unsigned int selector : 16;
	unsigned int Param_count : 5;
	unsigned int res : 3;
	unsigned int type : 4;
	unsigned int s : 1;
	unsigned int dp1 : 2;
	unsigned int p : 1;
	unsigned int offset_hei16 : 16;
GateDescriptor;

void Syscall() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);

//对外暴露的调用门函数
__declspec(naked) void SyscallProxy() 
	__asm 
		//int 3;
		push ebp;
		mov ebp, esp;
		//push fs;
		mov ax, 30h;
		mov fs, ax;
		call Syscall;
		mov esp, ebp;
		pop ebp;
		mov ax, 3bh;
		mov fs,ax;
		retf 0;
	


//安装调用门到GDT中
void InstallGate() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);
	GateDescriptor gate =  0 ;
	//指向代码段的选择子,因为ring0代码段是gdt第1个项目且DPL是0
	gate.selector = 0x8;
	//函数
	gate.offset_low16 = (ULONG)SyscallProxy & 0xffff;
	gate.offset_hei16 = ((ULONG)SyscallProxy >> 16) & 0xffff;
	//参数是0
	gate.Param_count = 0;
	//固定数值
	gate.type = 0xc;
	gate.s = 0;
	gate.p = 1;
	//权限因为是给ring3准备的所以是3 
	gate.dp1 = 3;
	


	KAFFINITY mask = KeQueryActiveProcessors();
	KAFFINITY shift = 1;
	while (mask)
	
		KeSetSystemAffinityThread(shift);
		GDTR gdt =  0 ;
		__asm sgdt  gdt;

		DbgPrint("[My learning] %s base:%p limit %p \\r\\n", __FUNCTION__, gdt.base, gdt.limit);

		GateDescriptor*pGate = (GateDescriptor*)gdt.base;

		if (MmIsAddressValid(pGate))
		
			pGate[9] = gate;
		
		shift <<= 1;
		mask >>= 1;
	



//卸载函数
void UnInstallGate() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);


	KAFFINITY mask = KeQueryActiveProcessors();
	KAFFINITY shift = 1;
	while (mask)
	
		KeSetSystemAffinityThread(shift);
		GDTR gdt =  0 ;
		__asm sgdt  gdt;
		DbgPrint("[My learning] %s base:%p limit %p \\r\\n", __FUNCTION__, gdt.base, gdt.limit);


		GateDescriptor*pGate = (GateDescriptor*)gdt.base;

		if (MmIsAddressValid(pGate))
		
			pGate[9].p = 0;
		
		shift <<= 1;
		mask >>= 1;
	


结合驱动代码

#include<ntifs.h>
#include <Ntddk.h>
#include<intrin.h>



#pragma push
#pragma pack(1)
typedef struct _GDTR 
	short limit;
	int base;
GDTR;
#pragma pop

typedef struct _GateDescriptor 
	unsigned int offset_low16 : 16;
	unsigned int selector : 16;
	unsigned int Param_count : 5;
	unsigned int res : 3;
	unsigned int type : 4;
	unsigned int s : 1;
	unsigned int dp1 : 2;
	unsigned int p : 1;
	unsigned int offset_hei16 : 16;
GateDescriptor;

void Syscall() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);

__declspec(naked) void SyscallProxy() 
	__asm 
		//int 3;
		push ebp;
		mov ebp, esp;
		//push fs;
		mov ax, 30h;
		mov fs, ax;
		call Syscall;
		mov esp, ebp;
		pop ebp;
		mov ax, 3bh;
		mov fs,ax;
		retf 0;
	



void InstallGate() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);
	GateDescriptor gate =  0 ;
	gate.selector = 0x8;
	gate.offset_low16 = (ULONG)SyscallProxy & 0xffff;
	gate.offset_hei16 = ((ULONG)SyscallProxy >> 16) & 0xffff;
	gate.Param_count = 0;
	gate.type = 0xc;
	gate.s = 0;
	gate.dp1 = 3;
	gate.p = 1;


	KAFFINITY mask = KeQueryActiveProcessors();
	KAFFINITY shift = 1;
	while (mask)
	
		KeSetSystemAffinityThread(shift);
		GDTR gdt =  0 ;
		__asm sgdt  gdt;

		DbgPrint("[My learning] %s base:%p limit %p \\r\\n", __FUNCTION__, gdt.base, gdt.limit);

		GateDescriptor*pGate = (GateDescriptor*)gdt.base;

		if (MmIsAddressValid(pGate))
		
			pGate[9] = gate;
		
		shift <<= 1;
		mask >>= 1;
	



void UnInstallGate() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);


	KAFFINITY mask = KeQueryActiveProcessors();
	KAFFINITY shift = 1;
	while (mask)
	
		KeSetSystemAffinityThread(shift);
		GDTR gdt =  0 ;
		__asm sgdt  gdt;
		DbgPrint("[My learning] %s base:%p limit %p \\r\\n", __FUNCTION__, gdt.base, gdt.limit);


		GateDescriptor*pGate = (GateDescriptor*)gdt.base;

		if (MmIsAddressValid(pGate))
		
			pGate[9].p = 0;
		
		shift <<= 1;
		mask >>= 1;
	




//这个函数被注册用于驱动卸载调用
VOID myUnload(
	struct _DRIVER_OBJECT* DriverObject
) 
	UNREFERENCED_PARAMETER(DriverObject);

	DbgPrint("hello  drive unloaded");

	PDEVICE_OBJECT DeviceObject = DriverObject->DeviceObject;
	UnInstallGate();
	if (DriverObject->DeviceObject != NULL)
	
		DbgPrint("驱动文件不为空执行删除");
		IoDeleteDevice(DeviceObject);


		UNICODE_STRING symbolDevName;
		RtlInitUnicodeString(&symbolDevName, L"\\\\DosDevices\\\\MytestDriver");
		IoDeleteSymbolicLink(&symbolDevName);
	




//驱动被加载的时候会调用此函数
NTSTATUS
DriverEntry(
	_In_ struct _DRIVER_OBJECT* DriverObject,
	_In_ PUNICODE_STRING    RegistryPath
)

	//如果你没有用到参数需要告诉系统。
	UNREFERENCED_PARAMETER(RegistryPath);


	InstallGate();

	//打印信息
	DbgPrint("[My learning]  drive loaded");
	
	DriverObject->DriverUnload = myUnload;

	

	UNICODE_STRING ustrDevName;
	RtlInitUnicodeString(&ustrDevName, L"\\\\Device\\\\MytestDriver");
	PDEVICE_OBJECT  pDevObj = NULL;

	auto ret = IoCreateDevice(DriverObject, 0, &ustrDevName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDevObj);




	if (NT_SUCCESS(ret))
	
		//指定IO模式
		pDevObj->Flags |= DO_DIRECT_IO;
		DbgPrint("IoCreateDevice 成功 \\r\\n");
	
	else 
		DbgPrint("IoCreateDevice 失败 %d\\r\\n", ret);
		return STATUS_FAIL_CHECK;
	


	UNICODE_STRING symbolDevName;
	RtlInitUnicodeString(&symbolDevName, L"\\\\DosDevices\\\\MytestDriver");
	ret = IoCreateSymbolicLink(&symbolDevName, &ustrDevName);
	if (NT_SUCCESS(ret))
	
		DbgPrint("IoCreateSymbolicLink 成功 \\r\\n");
	
	else 
		DbgPrint("IoCreateSymbolicLink 失败%d\\r\\n", ret);

		IoDeleteDevice(pDevObj);

		return STATUS_FAIL_CHECK;
	

	return STATUS_SUCCESS;


最后ring3 层的调用代码

// ring3Demo.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>


int main()




	__asm 
		_emit 0x9a;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x4b;
		_emit 0x00;
		
	
	printf("调用完毕syscall");
	system("pause");
	return 0;



改良代码

上面代码每次加一个函数都需要进行很多额外的步骤,我们因此自己实现一个跳转方式且自己进行栈参数拷贝。

我们目标如下:在一个内核提供多个函数定义,用户可以根据自定义的调用协议调用下面多个函数。eax传递调用g_SysCall函数下标,edx传递3环程序拷贝栈参数的地址。

//定义函数1
void Syscall() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);

//定义函数2
void Syscall2(int p1) 
	//KdBreakPoint();
	DbgPrint("[My learning] %s  %x \\r\\n", __FUNCTION__,p1);

//定义函数3
void Syscall3(int p1, int p2) 
	//KdBreakPoint();
	DbgPrint("[My learning] %s ,%x %x \\r\\n", __FUNCTION__,p1,p2);

//定义函数4
void Syscall4(int p1, int p2, int p3) 
	//KdBreakPoint();
	DbgPrint("[My learning] %s %x %x %x\\r\\n", __FUNCTION__,p1,p2,p3);

//定义一个函数指针
typedef void(*SYSSCALL)();
//函数数组
SYSSCALL g_SysCall[] = 
	(SYSSCALL)&Syscall ,
	(SYSSCALL)&Syscall2,
	(SYSSCALL)&Syscall3,
	(SYSSCALL)&Syscall4 ;

//定义拷贝栈区字节数
unsigned char g_SysCallParam[] = 
	0,
	4,
	8,
	12,
;

//eax 系统调用g_SysCall的编号 edx ESP参数位置
__declspec(naked) void SyscallProxy( ) 
	__asm 
		//int 3;
		//保存使用的变量
		push ebp;
		push ebx;
		push ecx;
		//基础栈操作
		mov ebp, esp;
		//intel芯片不会自动切fs,内核固定30h;
		mov bx, 30h;
		mov fs, bx;
		//拷贝栈字节数 到ecx moczx拷贝16到目标寄存器其余用0填充
		movzx ecx, byte ptr g_SysCallParam[eax];
		//抬栈
		sub esp, ecx;
		//拷贝栈到内核
		mov esi, edx;
		mov edi, esp;
		rep movsb;
       //此时esp栈顶就是参数 所以直接call就可以调用		
		call dword ptr[g_SysCall + eax * 4];
       //intel芯片不会自动切fs,3环固定3bh;
		mov bx, 3bh;
		mov fs, bx;
		//还原栈
		mov esp, ebp;
		//还原寄存器还原
		pop ecx;
		pop ebx;
		pop ebp;
        //调用门返回		
		retf 0;
	


安装调用门


void InstallGate() 
	//KdBreakPoint();
	DbgPrint("[My learning] %s \\r\\n", __FUNCTION__);
	GateDescriptor gate =  0 ;
	gate.selector = 0x8;
	//修改地址
	gate.offset_low16 = (ULONG)SyscallProxy & 0xffff;
	gate.offset_hei16 = ((ULONG)SyscallProxy >> 16) & 0xffff;
	//....


最后是3环的调用代码:


#include <iostream>


__declspec(naked) void GateSyscall() 
	__asm 
		int 3;
		lea edx, [ebp + 8];
		_emit 0x9a;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x00;
		_emit 0x4b;
		_emit 0x00;
		ret
	

void(*g_SysCall)() = &GateSyscall;

void SysCall1() 
	__asm 
		mov eax, 0;
		call g_SysCall
	

void SysCall2(int p1) 
	__asm 
		mov eax, 1;
		call g_SysCall
		
	



int main()

	SysCall2(0x1234);

	short n;
	__asm mov ax, fs;
	__asm mov n, ax;
	printf("调用完毕syscall fx %d\\r\\n", n);
	system("pause");
	return 0;



中断门

中断门和调用门差不多,不过中断表放在idtr寄存器中,如下图所示

在windbg相关命令:

//查看当前中断表地址
r idtr 
//查看中断表当前的长度
r idtl
//查看所有中断表内容
!idt -a

运行效果如下:

有趣的小实验:
断点键盘中断,看看每次按键盘是否有断点

运行!idt -a找到键盘中断,然后对函数执行断点


执行断点命令 bp 818131f0,然后操作按下被调试系统的键盘按键

以上是关于windows x32调用门/中断门实现 ring3提权的主要内容,如果未能解决你的问题,请参考以下文章

六.Windows内核保护机制--中断门

中断描述符表描述符:任务门,中断门,陷阱门(调用门)

CPU和CPUID是啥关系?

“调用门”和“软件中断”的区别?

中断异常和系统调用

中断门