感觉很久都没写文章了,以后还是要养成记录学习过程的习惯,这样才不会老摸鱼。。。
Reparse Points
概念
之前学习的概念如下:
Windows仅仅支持两种类型的文件:普通文件以及文件目录。这两种文件都可以作为一个NTFS重解析点,一种特殊的文件,拥有一个修改的头部和一个可变的数据块。头部包括了一个表示当前重解析点的类型,这个tag将会被文件系统过滤驱动处理;或者包含内置的重解析点类型类型,即I/O管理器本身
为什么叫做重解析呢?这里要看一个例子:
1 C:\Symlink_to_File\File_to_SYS
首先解析C:\,发现是一个驱动
然后解析Symlink_to_File
,发现其实是一个符号链接,于是重新解析当前路径 ,得到的路径其实为File Path
继续解析File_to_SYS
,发现也是一个符号链接,于是再次发生重新解析 ,得到路径为SYS
实际发生解析的路径变成了C:\File Path\SYS
由于这个过程 重新解析了文件信息,所以称为重解析点 。重解析通常用于符号链接或者挂载 。在2019年之后,微软修改了符号链接的权限,现在规定只有管理员权限才能够随意创建符号链接,并且经过观察发现,即使是管理员,默认的创建权限也是关闭的,需要特殊开启
然而不知为何,挂载点MountPoint
的创建却没有被限制。于是可以利用挂载点进行重解析,来对路径进行类似符号链接的重定向。
实验:如何创建重解析点
这边参考了ProjectZero 的仓库,核心代码如左。
这里首先介绍一下核心代码:
首先,需要使用一个结构体:
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 typedef struct _REPARSE_DATA_BUFFER { ULONG ReparseTag; USHORT ReparseDataLength; USHORT Reserved; union { struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; ULONG Flags; WCHAR PathBuffer[1 ]; } SymbolicLinkReparseBuffer; struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; WCHAR PathBuffer[1 ]; } MountPointReparseBuffer; struct { UCHAR DataBuffer[1 ]; } GenericReparseBuffer; } DUMMYUNIONNAME; } REPARSE_DATA_BUFFER, * PREPARSE_DATA_BUFFER;
这个结构体是一个union,同时处理了SymbolicLink
和MountPoint
两个点。同时需要引进一些常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define REPARSE_DATA_BUFFER_HEADER_LENGTH FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer.DataBuffer) #define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L) #define IO_REPARSE_TAG_HSM (0xC0000004L) #define IO_REPARSE_TAG_DRIVE_EXTENDER (0x80000005L) #define IO_REPARSE_TAG_HSM2 (0x80000006L) #define IO_REPARSE_TAG_SIS (0x80000007L) #define IO_REPARSE_TAG_WIM (0x80000008L) #define IO_REPARSE_TAG_CSV (0x80000009L) #define IO_REPARSE_TAG_DFS (0x8000000AL) #define IO_REPARSE_TAG_FILTER_MANAGER (0x8000000BL) #define IO_REPARSE_TAG_SYMLINK (0xA000000CL) #define IO_REPARSE_TAG_IIS_CACHE (0xA0000010L) #define IO_REPARSE_TAG_DFSR (0x80000012L) #define IO_REPARSE_TAG_DEDUP (0x80000013L) #define IO_REPARSE_TAG_APPXSTRM (0xC0000014L) #define IO_REPARSE_TAG_NFS (0x80000014L) #define IO_REPARSE_TAG_FILE_PLACEHOLDER (0x80000015L) #define IO_REPARSE_TAG_DFM (0x80000016L) #define IO_REPARSE_TAG_WOF (0x80000017L)
这些都是一些在Reparse
处理过程中可能用到的一些变量。
然后再之后的代码中,我们首先打开两个目录,一个叫做srcPath
,一个叫做targetPath
,其中我们假定一个如下的场景:
目前srcPath是我们有写权限的目录,但是targetPath我们没有
通过重解析,我们需要让srcPath解析绑定到targetPath上
当发生重解析之后,我们往srcPath中写入文件,最终会写入到targetPath目录中
当需要将当前的文件作为重解析句柄打开的时候,需要使用如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 HANDLE handle = CreateFile (path, GENERIC_READ | GENERIC_WRITE, 0 , 0 , OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0 ); if (handle == INVALID_HANDLE_VALUE) { printf ("Create Reparse Point failed with error code %d\n" , GetLastError ()); return NULL ; }
关键在于FILE_FLAG_OPEN_REPARSE_POINT
,这个变量表明当前打开的句柄需要作为重解析点去处理。
然后代码需要构建之前提到的_REPARSE_DATA_BUFFER
:
1 2 3 4 5 6 7 std::wstring target = FixupPath (wszTargetFullDir); const size_t target_byte_size = target.size () * 2 ;std::wstring printname = L"" ; const size_t printname_byte_size = printname.size () * 2 ;const size_t path_buffer_size = target_byte_size + printname_byte_size + 4 + 8 ;const size_t total_size = path_buffer_size + REPARSE_DATA_BUFFER_HEADER_LENGTH;
其中printname_byte_size
之所以还要跟着4+8
是因为,首先路径末尾需要预留\0
的位置(Unicode所以是2个字节大小)然后成员变量里面有两个Unicode对象,所以需要4,而8则是来自ntoskrnl!FsRtlValidateReparsePointBuffer
的分析和调试。之后写如下逻辑:
1 2 3 4 5 6 7 8 9 10 buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; buffer->ReparseDataLength = static_cast <USHORT>(path_buffer_size); buffer->Reserved = 0 ; buffer->MountPointReparseBuffer.SubstituteNameOffset = 0 ; buffer->MountPointReparseBuffer.SubstituteNameLength = static_cast <USHORT>(target_byte_size); memcpy (buffer->MountPointReparseBuffer.PathBuffer, target.c_str (), target_byte_size + 2 ); buffer->MountPointReparseBuffer.PrintNameOffset = static_cast <USHORT>(target_byte_size + 2 ); buffer->MountPointReparseBuffer.PrintNameLength = static_cast <USHORT>(printname_byte_size); memcpy (buffer->MountPointReparseBuffer.PathBuffer + target.size () + 1 , printname.c_str (), printname_byte_size + 2 );
由于PrintName
只是一个展示的数据,所以这个位置的数据可以为空字符串。
之后对之前打开的Reparse
文件描述符,使用IOCTL发送请求数据:
1 2 3 4 5 6 bool ret = DeviceIoControl (handle, FSCTL_SET_REPARSE_POINT, reparse_buffer, dwReparseSize, nullptr , 0 , &cb, nullptr ) == TRUE; if (!ret) { printf ("SetReparsePoint failed with error code:%d\n" , GetLastError ()); }
其中dwReparseSize
为之前算好的total_size
,表示此时发送的数据大小。一旦调用成功之后,srcPath
就会被设置成重解析点,此时就能够起到类似符号链接的作用:
未设置重解析点
设置了重解析点
图标发生了变化
可以看到,设置重解析点之后,srcDir
拥有了一个link属性,此时再srcDir中创建的所有文件其实等价于再targetDir中创建文件。
当完成了工作之后,可以通过如下的方式将这个重解析数据头删除:
1 2 3 4 5 6 7 8 9 10 11 12 buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; buffer->ReparseDataLength = 0 ; bool ret = false ;DWORD dwIOCTLOutSize = 0 ; ret = DeviceIoControl (hSrc, FSCTL_DELETE_REPARSE_POINT, buffer, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL , NULL , &dwIOCTLOutSize, NULL ); if (!ret){ printf ("Reset the Reparse Point failed with error:%d\n" , GetLastError ()); return -1 ; }
关于CVE的相关分析
CVE-2022-22718
其实是关于printer
一个老漏洞CVE-2020–1030
的一个新的思路。这里简单介绍一下漏洞详情,以及上述提到的重解析漏洞在这个地方的利用方式。
漏洞成因
实际控制的进程
核心原因在于:对于文件夹的权限检查和创建没有再同一个时刻完成。
出现问题的API是SetPrinterDataEx
1 2 3 4 5 6 7 8 DWORD SetPrinterDataEx ( _In_ HANDLE hPrinter, _In_ LPCTSTR pKeyName, _In_ LPCTSTR pValueName, _In_ DWORD Type, _In_ LPBYTE pData, _In_ DWORD cbData ) ;
这个API实际上是一个COM调用,通过这个调用能够对打印机的一些注册表配置进行修改,其中修改的注册表其实就是左边Printers
展开后的这些打印机
由于调用这个API,需要用户对这个打印机存在PRINTER_ACCESS_ADMINISTER
的权限。如果无法打开现有的打印机的话,可以通过添加一个新的打印机来规避这个权限的月书。调用这个API可以往这些位置添加对应的注册表项。
这个API在SpoolDirectory
这个值设置的时候,首先会检查相关权限(COM调用的场合都是没问题的),之后会进入相关的逻辑如下:
这个地方会尝试创建我们传入的目录,并且是具备可写的权限。如果可以创建,则检查这个文件的符号链接数量是否为1,具体逻辑如下
首先调用AdjustFileName
将路径调整为CanonicalPath
,也就是如下的形式:
然后调用下列逻辑,对当前的路径链接数进行比对
或者这个文件之前就存在了,那么就会把这个路径写入注册表,否则就会离开当前逻辑。
总结以下,上述的整体逻辑如下:
检查当前写入SpoolDirectory
的路径是否可以创建
如果可以创建,则创建路径,并且将这个路径写入注册表
于是这里就出现了一个逻辑问题:对目录进行check的时候,和创建文件并不是发生在同一个时机 ,这里就存在一个类似竞争的问题。举个例子
首先,我们假设我们可以写入的路径是C:\\My\\Dir
我们将这个路径传入注册表,此时必然是可以通过检查的,包括是否可写,以及是否为符号链接
当调用玩SetPrinterDataEx
函数之后,将这个路径用重解析的方式重定向到C:\\Windows\\System32
此时,C:\\My\\Dir
其实本质上就指向了C:\\Windows\\System32
。然后我们通过让spool进程重启,他就会去调用BuildPrinterInfo
,就会尝试将注册表中的路径读出来,并且尝试去创建对应的文件:
于是通过Repase Point
,我们就能够获得一个任意目录创建的机会。
漏洞利用点
通过上述方法,我们能够获得一个任意目录写的机会,那么如何利用这个漏洞呢?这个就要扯到打印机的一个特性:
再任意一个打印机注册表目录项下,如果存在CopyFiles
开头的键,那么这个键对应的Modules
值中填写一个DLL路径,这个DLL将会被当前COM服务加载,并且load到打印机服务中。这个进程是SYSTEM权限。
打印机在加载DLL的时候,会在函数IsModuleFilePathAllowed
检查加载的DLL路径是否为C:\\Windows\\System32
或者C:\Windows\System32\spool\drivers\<ARCH>
这两个目录,否则的话DLL不会加载。
为了实现上一步,首先需要能够修改对应的Module值,然后需要能够往这个目录下写入DLL
修改Module值可以用通过直接使用APISetPrinterDataEx
进行调用。然而在这个过程中有一个小问题:在BuildPrinterInfo
中,会检查注册表中读出来的路径,是否为DriverPath
,而我们的目标就是C:\Windows\System32\spool\drivers\<ARCH>
,也就是DriverPath
,于是我们需要一个绕过的策略
上图为匹配逻辑,可以很容易的发现,他这边是直接简单的比较路径,其中PrinterPathName就是C:\Windows\System32\spool\drivers\<ARCH>
。
于是现在的问题变成:
为了让SplLoadLibraryTheCopyFileModule
加载DLL,我们的DLL路径必须要指向C:\Windows\System32\spool\drivers\<ARCH>
。
为了让BuildPrinterInfo
通过,此时我们利用任意文件创建的攻击原语(attack primitive)创建的路径必须与字符串字符串C:\Windows\System32\spool\drivers\<ARCH>
不同
可以看到,第二项check比较的是字符串,而第一项要求的只是路径能够只想对应位置的DLL,因为在这个SplLoadLibraryTheCopyFileModule
加载逻辑如下:
可以看到,在检查文件DLL是否合法(满足两个路径,就是IsModuleFilePathAllowed
这个地方)之前,调用了一个关键函数MakeCanonicalPath
,这个函数跟进去可以看到
可以看到这边会尝试获取这个文件,并且调用GetFinalPathNameByHandleW
,这个API的作用是能够解析符号链接 ,也就是说,在真正的检查之前,这个路径会被先规范化 ,比如说
上述路径被我们用重解析点解析到了
那么这个规范化解析就会变成
然后实际IsModuleFilePathAllowed
比较的时候会先比较前四个字节是否为\\?\
,如果是的话,会对其进行跳过,然后比较之后的路径是否为C:\Windows\system32
。
为了这里可以使用另一种路径的表达方式:
1 \\localhost\C$\spooldir\printers\
根据文章提到的,在某些版本中比较路径的时候,在进行路径比较之前会将\\?\
删除,所以这边如果使用上述的路径的话,当结构化处理的时候会转换成
1 \\?\UNC\C:\spooldir\printers
于是当进行比较的时候,就永远不会相等了。于是这一个攻击流程就是
首先调用SetPinterDataEx
设置SpoolDirectory
,此时填入任意一个可控的目录的UNC目录,此时可以通过第一层check->符号链接数量为1
然后使用ReparsePoint
,将路径重定向到C:\Windows\system32\spool\driver\<ARCH>
然后第二次调用SetPinterDataEx
,此时增添\\CopyFiles
,并且指定DLL路径为system32
下的APPVTerminator.dll
程序重启,读取SpoolDirectory
之后进行第二次比较,此时由于路径为UNC目录,可以绕过第二个比较->此时不能为print driver目录,并且创建对应的路径4
此时往重解析点中,新建的目录4为任意用户可写,于是把攻击用的dll写入
然后第二次调用SetPinterDataEx
,此时增添\\CopyFiles
,并且写入攻击dll,攻击完成
修复后(修复后,在此处创建文件会提示没有权限,具体原因不明)
同时删除了AppVTerminator.dll
附录
完整的Reparse实验代码:
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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 #include <Windows.h> #include <iostream> #include <stdio.h> #define SYMLINK_FLAG_RELATIVE 1 typedef struct _REPARSE_DATA_BUFFER { ULONG ReparseTag; USHORT ReparseDataLength; USHORT Reserved; union { struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; ULONG Flags; WCHAR PathBuffer[1 ]; } SymbolicLinkReparseBuffer; struct { USHORT SubstituteNameOffset; USHORT SubstituteNameLength; USHORT PrintNameOffset; USHORT PrintNameLength; WCHAR PathBuffer[1 ]; } MountPointReparseBuffer; struct { UCHAR DataBuffer[1 ]; } GenericReparseBuffer; } DUMMYUNIONNAME; } REPARSE_DATA_BUFFER, * PREPARSE_DATA_BUFFER; #define REPARSE_DATA_BUFFER_HEADER_LENGTH FIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer.DataBuffer) #define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L) #define IO_REPARSE_TAG_HSM (0xC0000004L) #define IO_REPARSE_TAG_DRIVE_EXTENDER (0x80000005L) #define IO_REPARSE_TAG_HSM2 (0x80000006L) #define IO_REPARSE_TAG_SIS (0x80000007L) #define IO_REPARSE_TAG_WIM (0x80000008L) #define IO_REPARSE_TAG_CSV (0x80000009L) #define IO_REPARSE_TAG_DFS (0x8000000AL) #define IO_REPARSE_TAG_FILTER_MANAGER (0x8000000BL) #define IO_REPARSE_TAG_SYMLINK (0xA000000CL) #define IO_REPARSE_TAG_IIS_CACHE (0xA0000010L) #define IO_REPARSE_TAG_DFSR (0x80000012L) #define IO_REPARSE_TAG_DEDUP (0x80000013L) #define IO_REPARSE_TAG_APPXSTRM (0xC0000014L) #define IO_REPARSE_TAG_NFS (0x80000014L) #define IO_REPARSE_TAG_FILE_PLACEHOLDER (0x80000015L) #define IO_REPARSE_TAG_DFM (0x80000016L) #define IO_REPARSE_TAG_WOF (0x80000017L) static int g_last_error = 0 ;bool CreateMyDirectory (const WCHAR path[]) { wprintf (L"Create Directory:%s\n" , path); if (CreateDirectory (path, NULL ) || (GetLastError () == ERROR_ALREADY_EXISTS)) { return true ; } else { return false ; } } HANDLE OpenReparsePoint (const WCHAR path[]) { HANDLE handle = CreateFile (path, GENERIC_READ | GENERIC_WRITE, 0 , 0 , OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0 ); if (handle == INVALID_HANDLE_VALUE) { printf ("Create Reparse Point failed with error code %d\n" , GetLastError ()); return NULL ; } return handle; } bool SetReparsePoint (HANDLE handle, REPARSE_DATA_BUFFER* reparse_buffer, DWORD dwReparseSize) { DWORD cb; bool ret = DeviceIoControl (handle, FSCTL_SET_REPARSE_POINT, reparse_buffer, dwReparseSize, nullptr , 0 , &cb, nullptr ) == TRUE; if (!ret) { printf ("SetReparsePoint failed with error code:%d\n" , GetLastError ()); } return ret; } std::wstring FixupPath (std::wstring str) { if (str[0 ] != '\\' ) { return L"\\??\\" + str; } return str; } int main () { std::cout << "[+] Reparse Test! [+]" << std::endl; WCHAR wcsFullDir[MAX_PATH] = { 0 }; GetCurrentDirectory (MAX_PATH, wcsFullDir); WCHAR srcDir[] = L"\\srcDir" ; WCHAR targetDir[] = L"\\targetDir" ; std::wstring wszSrcFullDir = wcsFullDir; wszSrcFullDir += srcDir; std::wstring wszTargetFullDir = wcsFullDir; wszTargetFullDir += targetDir; std::wcout << L"Current FullDir is " << wcsFullDir << std::endl; if (!CreateMyDirectory (wszSrcFullDir.c_str ())) { std::wcout << "Create " << wszSrcFullDir << "Failed!" << std::endl; return -1 ; } if (!CreateMyDirectory (wszTargetFullDir.c_str ())) { std::wcout << "Create " << wszTargetFullDir << "Failed!" << std::endl; return -1 ; } puts ("[+] Now open directory as reparse point [+] \n" ); HANDLE hSrc = OpenReparsePoint (wszSrcFullDir.c_str ()); if (!hSrc) { puts ("[+] Open handle failed\n" ); return -1 ; } std::wstring target = FixupPath (wszTargetFullDir); const size_t target_byte_size = target.size () * 2 ; std::wstring printname = L"" ; const size_t printname_byte_size = printname.size () * 2 ; const size_t path_buffer_size = target_byte_size + printname_byte_size + 4 + 8 ; const size_t total_size = path_buffer_size + REPARSE_DATA_BUFFER_HEADER_LENGTH; printf ("The ReparseDataLength = %d \n" , path_buffer_size); printf ("The total_size = %d \n" , total_size); REPARSE_DATA_BUFFER* buffer = new REPARSE_DATA_BUFFER (); buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; buffer->ReparseDataLength = static_cast <USHORT>(path_buffer_size); buffer->Reserved = 0 ; buffer->MountPointReparseBuffer.SubstituteNameOffset = 0 ; buffer->MountPointReparseBuffer.SubstituteNameLength = static_cast <USHORT>(target_byte_size); memcpy (buffer->MountPointReparseBuffer.PathBuffer, target.c_str (), target_byte_size + 2 ); buffer->MountPointReparseBuffer.PrintNameOffset = static_cast <USHORT>(target_byte_size + 2 ); buffer->MountPointReparseBuffer.PrintNameLength = static_cast <USHORT>(printname_byte_size); memcpy (buffer->MountPointReparseBuffer.PathBuffer + target.size () + 1 , printname.c_str (), printname_byte_size + 2 ); if (!SetReparsePoint (hSrc, buffer, total_size)) { printf ("Set Reparse Point failed with error :%d\n" , GetLastError ()); return -1 ; } puts ("[+] Create Symbolic success! now try create one file at src path [+]" ); std::wstring TargetFilePath = wszSrcFullDir; TargetFilePath += L"\\TestFile" ; HANDLE hFile = CreateFile ( TargetFilePath.c_str (), GENERIC_READ | GENERIC_WRITE, 0 , 0 , CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile == INVALID_HANDLE_VALUE) { printf ("Create file failed with error code:%d\n" ,GetLastError ()); return -1 ; } std::string content = "Success!" ; DWORD dwByteWrite = 0 ; WriteFile (hFile, content.c_str (), content.size (), &dwByteWrite, NULL ); CloseHandle (hFile); puts ("[+] Success! enter any character to delete symbolic\n" ); char c = getchar (); buffer->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; buffer->ReparseDataLength = 0 ; bool ret = false ; DWORD dwIOCTLOutSize = 0 ; ret = DeviceIoControl (hSrc, FSCTL_DELETE_REPARSE_POINT, buffer, REPARSE_GUID_DATA_BUFFER_HEADER_SIZE, NULL , NULL , &dwIOCTLOutSize, NULL ); if (!ret) { printf ("Reset the Reparse Point failed with error:%d\n" , GetLastError ()); return -1 ; } CloseHandle (hSrc); puts ("[+] Reparse Success!\n" ); return 0 ; }