K0shl
漏洞说明
漏洞说明
hitb gsec ctf babyshellcode writeup
test my static rep0rt
A simple story of DsSvc, "Live and Die"
Author: k0shl of 360 Vulcan Team
OverviewDsSvc is a data sharing service that provides data sharing between processes. I have not conducted an in-depth analysis of the specific functions of this service. It is known that it provides some methods of file sharing between processes. As shown in the following figure, the process specifies a shared file through DsSvc, and calls CoCreateGuid to create a GUID as a file token, and stores information such as its token and file path into DbTable. Other processes can obtain file objects through this token and perform other files operating.
Data sharing services contain many file operations, which also bring a lot of security issues. Microsoft spent nearly a year to fix the logical issue in this service. The security issue caused by file operations is one of the types of logical vulnerability. Important partitions, it's necessary to be careful when dealing with files' operation, especially for file security attributes. I will analyze the logical vulnerabilities in DsSvc, as well as Microsoft's patch, and bypass. Let's start our story.
The Beginning of story...In November 2018, Microsoft patched a data sharing service vulnerability discovered by SandboxEscaper (PolarBear). SandboxEscaper shared details about this vulnerability on the blog. Since this article on the SandboxEscaper's blog is inaccessible, it is not possible to reference the SandboxEscaper blog address. A description of vulnerability is as follows:
Bug description: RpcDSSMoveFromSharedFile(handle,L"token",L"c:\\blah1\\pci.sys"); This function exposed over alpc, has a arbitrary delete vuln. Hitting the timing was pretty annoying. But my PoC will keep rerunning until c:\windows\system32\drivers\pci.sys is deleted. I believe it’s impossible to hit the timing on a single core VM. I was able to trigger it using 4 cores on my VM. (Sadly I wasn’t able to use OPLOCKS with this particular bug) Root cause is basically just a delete without impersonation because of an early revert to self. Should be straight forward to fix it… Exploitation wise.. you either try to trigger dll hijacking issues in 3rd party software.. or delete temp files used by a system service in c:\windows\temp and hijack them and hopefully do some evil stuff.This is an arbitrary file deletion vulnerability. The vulnerability occurs in the RPC interface RpcDSSMoveFromSharedFile. The issue existed in function PolicyChecker::CheckFilePermission. The code is as follows:
__int64 __fastcall PolicyChecker::CheckFilePermission(const WCHAR *FileName, unsigned int a2, unsigned int a3, int a4, __int64 a5) { [...CreateFile flag check...] [...Impersonate...] v12 = CreateFileW(v5, dwDesiredAccess, dwShareMode, 0i64, 4u, 0x80u, 0i64); [...RevertToSelf...] if ( v12 == (void *)-1i64 ) { [...] } else { v17 = v14; if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v12, a5), (v8 & 0x80000000) == 0) ) { CloseHandle(v12);//Close v12 = (void *)-1i64; if ( v17 ) return v8; DeleteFileW(v5);//arbitrary file deletion } if ( v13 != (void *)-1i64 ) CloseHandle(v13); } return v8; }In this function, FileName is defined by the user. First, the parameters DesiredAccess and ShareMode will be checked. Then RpcImpersonateClient will be called to impersonate client and call CreateFile to open the file. DsSvc will delete the file after RevertToSelf.
Although DsSvc calls ImpersonateClient to open the file, which means that when I try to open a limit file, it will fail and return, but there is still have TOCTOU issue. Before calling CeateFile, you can create a junction to the user-controllable path, so CreateFile will succeed. After that, you can change the junction to a limit directory. It will invoke DeleteFile to delete limit file finally.
In the patch, Microsoft no longer uses DeleteFile but uses the FileDispositionInfo class of SetFileInformationByHandle to delete file. Thus, calling SetFileInformationByHandle refers to the file handle created by CreateFile instead of the file path. The final deletion is a normal file opened after ImpersonateClient.
else { v17 = v14; if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v13, a5), v8 >= 0) ) { if ( !v17 ) { FileInformation = 1; if ( !SetFileInformationByHandle(v13, FileDispositionInfo, &FileInformation, 1u) ) { [...] } } } CloseHandle(v13); My research has started...After this vulnerability was exposed, I started my study on the logic vulnerability. One day, when I was chatting with my friend 0x9k, he talked about the complete full chain of 11 Android logical vulnerabilities used by MWRLab in Pwn2Own. Then I read MWRLab's slide about this full chain exploitation which they talked on CanSecWest.
There is a show in this slide about the tool jandroid that they use when finding android logic vulnerabilities.
After read about jandroid in slide, I came up with the idea that such this method can be used to finding logical vulnerabilities on Windows? The answer is yes.
I think I can assist the subsequent reverse engineering by parsing the path travesal of the sensitive operation on the RPC-related dll. I took some time to implement my idea. (Later in August 2019 Adam Chester published a blog post about his RPC parsing implementation idea)
I used James Forshaw's project NtApiDotNet when writing the parsing code. It can complete pre-working in my parsing framework, there is a class called NdrProcedureDefinition in NtApiDotNet, which plays a key role in RPC interface parsing, it can parse out of the RPC interface of the DCE syntax, I made a few modifications to the NdrProcedureDefinition part of the method, so that it can parse the RPC interface of the Ndr64 syntax, which may resolve more potential attack surfaces in the x64 system. (The figure below shows the analysis result of Chakra.dll which use Ndr64 syntax)
Here are two points to mention.
[+] The first is that almost all RPC dlls in Windows x64 systems use DCE syntax, but also contain a very small number of RPC dlls for Ndr64 syntax, such as Chakra.dll.
[+] And the second is that I am not find the way to parsing incoming parameter of RPC interface of Ndr64 syntax, so I can only parse the RPC interface function without parameters, but this does not affect sensitive operation path travesal parsing.
The following picture shows the logs of some of the path travesals after I run my parsing code. Actually, I found out some interesting path travesals in DsSvc after that time, but James Forshaw report most of them :).
Get down to business
In January 2019, Microsoft patched 5 DsSvc EoP Vulnerabilities reported by James Forshaw , there is a interesting patch in these 5 vulnerabilities which about the MoveFileInheritSecurity function, the vulnerability code is as follows:
__int64 __fastcall PolicyChecker::MoveFileInheritSecurity(const WCHAR *lpNewFileName, const WCHAR *lpExistingFileName) { [...] if ( MoveFileExW(lpNewFileName, lpExistingFileName, 3u) ) { if ( !InitializeAcl(&pAcl, 8u, 2u) ) { LABEL_4: v6 = GetLastError(); goto LABEL_10; } v6 = SetNamedSecurityInfoW(lpExistingFileName, SE_FILE_OBJECT, 0x20000004u, 0i64, 0i64, &pAcl, 0i64); if ( v6 ) MoveFileExW(lpExistingFileName, lpNewFileName, 3u); } [...] }As the code show, DsSvc will set the DACL of the new file through SetNamedSecurityInfoW after the MoveFile. James forshaw create a hardlink to a limit file, and call RpcDSSMoveFromSharedFile interface, the DsSvc get the file path directly, but not check if the file is accessible, it will finally set the limit file's DACL.
Before patch:
__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2) { [...] v3 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64); if ( v3 != (HANDLE)-1i64 ) { v2 = v3; LABEL_6: v5 = DSUtils::VerifyPathFromHandle(v1, v2); goto LABEL_7; } [...] }After patch:
__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2) { [...] v5 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64); if ( v5 != (HANDLE)-1i64 ) { v4 = v5; LABEL_6: v7 = DSUtils::VerifyPathFromHandle(v3, v4); if ( v7 >= 0 ) v7 = DSUtils::VerifyFileIdFromHandle(v2, v4); goto LABEL_8; } [...] }After patch, function DSUtils::VerifyFileIdFromHandle is added. The function contains a check for the hardlink. The BY_HANDLE_FILE_INFORMATION structure returned by calling the GetFileInformationByHandle function contains the member variable nNumberOfLinks. If it is greater than 1, it indicates that there is a symbolic link, and function will fail and return.
__int64 __fastcall DSUtils::GetFileIdFromHandle(HANDLE hFile, __int64 a2) { [...] if ( GetFileInformationByHandle(v3, &FileInformation) ) goto LABEL_19; [...] LABEL_19: if ( FileInformation.nNumberOfLinks <= 1 ) { v11 = (_WORD *)*v2; v2[1] = *v2; *v11 = 0; } [...] } The story is far from ending...Obviously, Microsoft's patch still have problem. I found that there is still a time window between the end of the check of the symbolic link and the call to the MoveFileInheritSecurity function, which means that there is a TOCTOU vulnerability, I can make a hardlink to limit file after the symbolink check, so that when DsSvc calls the MoveFileInheritSecurity function, it will set the limit file's DACL finally. I later reported this vulnerability to Microsoft.
Microsoft's patch is very simple, they canceled RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile two RPC interfaces of DsSvc in Windows 10 rs6 and later.
///After parse DsSvc RPC interface you can find out that ///RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile are canceled [uuid("bf4dc912-e52f-4904-8ebe-9317c1bdd497"), version(1.0)] interface intf_bf4dc912_e52f_4904_8ebe_9317c1bdd497 { HRESULT RpcDSSCreateSharedFileToken( handle_t p0, [In] wchar_t[1]* p1, [In] struct Struct_0* p2, [In] /* ENUM16 */ int p3, [In] /* ENUM16 */ int p4, [Out] wchar_t** p5); HRESULT RpcDSSGetSharedFileName( handle_t p0, [In] wchar_t[1]* p1, [Out] wchar_t** p2); HRESULT RpcDSSGetSharingTokenInformation( handle_t p0, [In] wchar_t[1]* p1, [Out] wchar_t** p2, [Out] wchar_t** p3, [Out] /* ENUM16 */ int* p4); HRESULT RpcDSSDelegateSharingToken( handle_t p0, [In] wchar_t[1]* p1, [In] struct Struct_1* p2); HRESULT RpcDSSRemoveSharingToken( handle_t p0, [In] wchar_t[1]* p1); HRESULT RpcDSSOpenSharedFile( handle_t p0, [In] wchar_t[1]* p1, [In] int p2, [Out] long* p3); HRESULT RpcDSSCopyFromSharedFile( handle_t p0, [In] wchar_t[1]* p1, [In] wchar_t[1]* p2); HRESULT RpcDSSRemoveExpiredTokens(); }The old version still exists these two interfaces. In the old version, Microsoft's patch is also very simple. The PolicyChecker::MoveFileInheritSecurity function is directly deleted, and DsSvc use another method for file copy. I will share this method later.
Other attack surfacesIn my RPC parsing log, I noticed another RPC interface, DSSCopyFromSharedFile, which calls the CopyFile function to copy file to a controllable path.
__int64 __fastcall DSSCopyFromSharedFile(const unsigned __int16 *a1, wchar_t *a2) { [...Check File...] if ( !CopyFileW(*(LPCWSTR *)(v10 + 184), v4, 0) ) { [...] } [...] }This vulnerability is very obvious, although DsSvc checked the permissions of the copied target file before CopyFile, I can still use race condition to link the file to the limit file after checking the target file permissions, and finally copy the shared file to limit file. In this function, the shared file can be specified by the user, so it is easy to write the payload into the file under system32.
When I reported this vulnerability, Microsoft did not award bounty for the vulnerability because Microsoft introduced a mitigation for hardlink. Whether normal user have control permission for the target file, if not, the hard link cannot be created, that is, the hardlink cannot be used by normal user in rs6 and WIP.
DsSvc security feature bypassAfter receiving the reply from Microsoft, I made a quick review on the DsSvc service code again and found a very interesting place. In DsSvc, it will protect the folder where the target file is to be operated. Create a lock file to prevent this folder from being mounted to another directory. The function that implements this security feature is DSUtils::DirectoryLock::Lock. The function code is as follows:
__int64 __fastcall DSUtils::DirectoryLock::Lock(signed __int64 this, const unsigned __int16 *a2) { [...] v20 = CreateFileW(v19, 0x80000000, 7u, 0i64, 4u, 0x4000100u, 0i64); [...] }The function calls CreateFile to create the lock file. I found that the lock file inherits the security descriptor of the parent directory, so actually, I have full control on lock file. It means I can delete the lock file after the lock file is created and after that I mount the directory to limit directory(the directory will be empty after I delete file). This way, even if I don't use hardlink, I can finally call CopyFile to copy the payload to a limit file in the limit directory.
The story is still going onMicrosoft finally patched vulnerability I report. In CopyFromSharedFile, Microsoft use a new function DSUtils::CopyFileWithProgress with the following code:
__int64 __fastcall DSUtils::CopyFileWithProgress(BOOL *this, DSUtils *a2, DSUtils *a3, const unsigned __int16 *a4) { DSUtils::OpenFile((const WCHAR *)a2, (const unsigned __int16 *)0x80000000i64, 7u, 0, &hObject); v7 = DSUtils::OpenFile((const WCHAR *)a3, (const unsigned __int16 *)0x80000000i64, 3u, 0, &pbCancel); [...] DSUtils::IsHardLinkFile(pbCancel, &vars0); [...] v8 = DSUtils::GetFinalPathFromHandle(v7, (__int64)&lpData); if ( v8 >= 0 && !CopyFileExW( (LPCWSTR)a2, (LPCWSTR)a3, (LPPROGRESS_ROUTINE)CopyFileProgressRoutine, lpData, (LPBOOL)dwCopyFlags, 0) ) [...] }In the patch, Microsoft not only checks whether the file is hardlink, but also uses CopyFileExW function. This function calls a callback function CopyFileProgressRoutine when copying the file. The callback function will check if the target file path is the same as before the IsHardLinkFile check.
signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, DWORD dwStreamNumber, DWORD dwCallbackReason, HANDLE hSourceFile, HANDLE hDestinationFile, LPVOID lpData) { [...] v9 = DSUtils::GetFinalPathFromHandle(hDestinationFile, (__int64)&Str2); if ( v9 >= 0 && _wcsicmp((const wchar_t *)lpData, Str2) ) [...] } Is it really over?After I analyze Microsoft's patch, I sent an email to Microsoft to confirm whether they fixed the problem I reported later, that is, the problem about lock file created. At that time, my suggestion was to create a lock file that can not be controlled by normal users. This way the user cannot delete the lock file and mount the current directory to another directory. Microsoft confirmed that it had fixed the previous problem, but they may did not understand my suggestion.
Although Microsoft added multiple checks, it can be found that Microsoft has finished checking the target file when it calls DSUtils::OpenFile to open the file and the callback function CopyFileProgressRoutine compares the file path before and after, so there is a very obvious issue:
If I still have full control over the lock file, I can still bypass the check in another way, James Forshaw's symboliclink-testing-tools introduce a method, this way it mounts the directory to the root directory of the namespace, and then links the named object to other files. This method will cause the file path to be resolved to other file when NT parsing the file path. Instead of setting the symbolic link through the SetFileInformation method, the advantage of this method is that the target file parsed when calling GetFileInformationByHandle is the target file, so the member variable nNumberOfLinks is still 1, you can easily bypass the IsHardlinkFile check.
Therefore, you can link file to other file in this way before OpenFile. After OpenFile, including the GetFinalPathNameByHandle in the callback function, they all will be parsed to the same path. Therefore, the patch is finally bypassed, and the payload file can still be copied to the limit file by CopyFile.
last of the last...Microsoft released a new patch in November 2019. Finally, Microsoft deleted the DirectoryLock function and the MoveFileInheritSecurity function I mentioned earlier, and used a new method DSUtils::OpenFileAlways to open the file and return the file handle which will be used in DSUtils::CopyFileWithProgress, it will opened until the function return. So, when the file is opened, the file no longer can be deleted and the mount point can not be created to other directory. Before CopyFile, the file directory to be copied is parsed by the GetFileInformationByHandle function.
///Instead of file path, DsSvc use file handle as incoming parameter DSUtils::CopyFileWithProgress(v5, (const unsigned __int16 *)hObject, hFile, (void *)v19); { [...] DSUtils::GetFinalPathFromHandle(hObject, (__int64)lpExistingFileName); [...] DSUtils::GetFinalPathFromHandle(hFile, (__int64)lpNewFileName); [...] DSUtils::IsHardLinkFile(hFile, dwCopyFlags, v8); [...] else if ( !CopyFileExW( lpExistingFileName[0], lpNewFileName[0], CopyFileProgressRoutine, v4, (LPBOOL)&dwCopyFlags[1], 0) ) [...] }And also, DsSvc uses ImpersonateClient to make sure the target file is an accessible file.
v6 = AutoImpersonate<1>::ImpersonateClient(&v39); [...] v6 = DSUtils::OpenFileAlways(lpFileName, Str, (unsigned __int64)&hFile); [...] if ( (_DWORD)v39 ) RpcRevertToSelf();Similarly, Microsoft has rewritten the CopyFileProgressRoutine callback function. In the function, DsSvc will compare the source file and target file with the FilePath and the FileID.
signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, __int64 dwStreamNumber, __int64 dwCallbackReason) { [...] v7 = DSUtils::GetFileIdFromHandle(hFile); [...] v7 = DSUtils::GetFileIdFromHandle((HANDLE)dwStreamNumber); [...] v7 = DSUtils::GetFinalPathFromHandle(hFile, (__int64)&v33); [...] v7 = DSUtils::GetFinalPathFromHandle((HANDLE)dwStreamNumber, (__int64)&v36); [...] if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare( dwCallbackReason, &v27) ) [...] if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare( dwCallbackReason + 64, &v30) ) [...] if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare( dwCallbackReason + 32, &v33) ) [...] if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare( dwCallbackReason + 96, &v36) ) [...] }In the old version, MoveFromSharedFile and MoveToSharedFile also use CopyFileWithProgress to move files. The story about DsSvc ends here.
Microsoft Hardlink缓解机制简单分析
Microsoft Hardlink缓解机制简单分析
Author: k0shl of 360 Vulcan Team
简述微软在Insider Preview引入了一个新的缓解机制来阻止普通用户创建硬链接(CreateHardlink),在逻辑漏洞的利用中,hardlink是一个非常实用且便捷的方法,当一个高完整性级别进程对低权限文件操作的时候(这里所谓低权限泛指normal user或更低权限用户可以完全控制的文件),可以利用hardlink将低权限文件链接到高权限文件(高权限是指需高权限例如SYSTEM操作的文件,比如C:\Windows目录下的绝大多数文件及子目录文件),从而会使高权限进程处理高权限文件(比如改变DACL,写入,创建等)这里简述一下利用hardlink的逻辑漏洞利用方法。
关于应用到hardlink技巧的漏洞可以参考Project Zero的James Forshaw的历史漏洞,他提交的逻辑类型漏洞中很多都应用到了hardlink的利用方法。
PS: 文中的代码示例均来自Insider preview(build 18898.1000),除了部分源码展示出处有单独说明。
Hardlink review关于hardlink的创建可以参考James Forshaw的项目symboliclink-testing-tools (https://github.com/googleprojectzero/symboliclink-testing-tools),通过调用NtSetInformationFile设置文件的FileLinkInformation的属性将指定文件链接到目标文件上。
当然,符号链接曾经也是用来sandbox escape的有效手段,在AppContainter(或者低完整性级别进程中,例如Low Integrity)中,许多权限操作需要通过medium integrity进程来帮忙完成,这样可以通过一个AC可以操作的目录文件,硬链接到高权限文件,再利用Medium integrity进程来完成对高权限文件的操作,但微软加入了针对这种方法的Mitigation,如果是在AppContainer中调用NtSetInformationFile,会调用RtlIsSandboxToken检查进程Token,若在沙盒中,则设置需求的访问权限。其实现在ntoskrnl!NtSetInformationFile函数中:
if ( a5 == 0xB || a5 == 0x48 )// 0xB和0x48都是FileLinkInformation { memset(&Dst, 0, 0x20ui64); SeCaptureSubjectContextEx(v6, *(_QWORD *)(v6 + 544), &Dst); v65 = RtlIsSandboxedToken(&Dst, v7);//检查沙盒Token SeReleaseSubjectContext(&Dst); if ( v65 ) v14 |= 0x100u;//设置需求的访问权限,必须要有写权限 }0xB和0x48是FileLinkInformation的enumerate,它最终会调用ntfs.sys中NtSetLinkInfo设置指定文件的硬链接到目标链接,其代码在ntfs!NtfsCommonSetInformation函数中实现
case 0xB: case 0x48: v47 = v11; v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47);ntfs.sys是Windows的文件系统驱动,其调用方法是在ntoskrnl的NT API中调用IofCallDriver,通过调用DriverObject的MajorFunction发送IRP封装,进入Ntfs!NtfsFsdSetInformation函数。
这里微软通过RtlIsSandboxedToken的方法阻止了AC调用硬链接,同样注册表和文件的符号链接,等方法也是通过类似方法缓解。
关于这次Insider Preview之前微软通常会通过两种方法来修补这类逻辑漏洞,第一种是模拟客户端,通过调用RpcImpersonateClient来模拟客户端Token,也就是说在接下来的上下文中,进程将以Client的权限执行代码,最后通过RpcRevertToSelf恢复原进程权限。第二种是通过调用GetFileInformationByHandle获取文件的属性,在GetFileInformationByHandle参数中有一个数据结构。
typedef struct _BY_HANDLE_FILE_INFORMATION { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD dwVolumeSerialNumber; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD nNumberOfLinks; DWORD nFileIndexHigh; DWORD nFileIndexLow; } BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION;其中nNumberOfLinks会获取文件符号链接的数量,如果大于1则证明当前文件存在符号链接,则阻止后续操作进行。
关于修补漏洞的方法这里不做过多讨论,读者可以通过对补丁的对比找到微软修补此类漏洞的方法。
Hardlink Mitigation在Insider Preview(build 18898.1000)中(可能更早),微软引入了针对hardlink的缓解措施,阻止普通用户创建高权限文件的硬链接,其主要思路是检查FileObject中ContextControlBlock的Flags,若其Flags不满足要求(RequirAccess),则最终调用SeAccessCheck检查进程对硬链接目标的真实访问权限,从而阻止普通用户创建高权限文件硬链接。
其主要代码在ntfs!NtfsSetLinkInfo中
if ( !(*(_WORD *)(a5 + 0x68) & 0x310) ) // mitigation { …… if ( !(unsigned __int8)TxfAccessCheck( v6, v90, *(_QWORD *)(v5 + 168), *(_QWORD *)(v6 + 200), 0, 0, 0x100u, 0, 0i64, v91, v99, (__int64)&v156, 0i64, &v157, 0i64) ) { v24 = 0xC0000022;//Access Denied if ( !NtfsStatusDebugFlags ) return (unsigned int)v24; v84 = 995043i64; goto LABEL_200; }TxfAccessCheck函数会调用SeAccessCheckEx检查进程的访问权限(写入,修改,删除等),若权限不满足,则返回0xC0000022拒绝访问。
我们需要回溯到外层函数调用NtfsCommonSetInformation来追踪a5+0x68的值,a5的值来自NtfsSetLinkInfo函数的第五个参数,关于回溯过程这里我不再赘述,a5的值来自于IRP封装的CurrentStackLocation,NtfsCommonSetInformation的第一个参数是一个IRP结构的参数,我简化了NtfsCommonSetInformation的代码,来看一下参数的传递过程。
signed __int64 __usercall NtfsCommonSetInformation@<rax>(_IRP *a1@<rdx>, __int64 a2@<rcx>, signed __int64 a3@<r15>) { v6 = a1->Tail.Overlay.CurrentStackLocation; v8 = *((_QWORD *)v6 + 0x30); v11 = *(_QWORD *)(v8 + 0x20); case 0xB: case 0x48: v47 = v11; v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47); }那么这个参数到底是什么呢,我们需要从CurrentStackLocation看起,CurrentStackLocation是IRP中的一个非常关键的成员,包括IRP封装调用Driver的方法MajorFunction都在此结构中,其数据类型是_IO_STACK_LOCATION。我们可以在这个地方下断点,命中时查看_IO_STACK_LOCATION结构。
3: kd> p Breakpoint 0 hit Ntfs!NtfsSetLinkInfo: fffff803`8434d694 4c8bdc mov r11,rsp 1: kd> dq ffffa402da68f010+b8 l1 ffffa402`da68f0c8 ffffa402`da68f3b0 1: kd> dt _IO_STACK_LOCATION ffffa402`da68f3b0 ntdll!_IO_STACK_LOCATION +0x000 MajorFunction : 0x6 '' +0x001 MinorFunction : 0 '' +0x002 Flags : 0 '' +0x003 Control : 0xe0 '' +0x008 Parameters : <anonymous-tag> +0x028 DeviceObject : 0xffffa402`d68f6030 _DEVICE_OBJECT +0x030 FileObject : 0xffffa402`dac559d0 _FILE_OBJECT这里需要解释一下,我在调试时直接在NtfsSetLinkInfo下断点是因为其第二个参数就是IRP结构,所以可以通过IRP直接跟踪到第五个参数的值,这里IRP结构的地址是0xffffa402da68f010,而_IO_STACK_LOCATION在IRP+0xb8偏移位置。
可以看到_IO_STACK_LOCATION + 0x30位置是FileObject,这个FileObject就是硬链接目标文件的FileObject。
接着可以跟踪FileObject+0x20是什么。
1: kd> dx -id 0,0,ffffa402ddde7080 -r1 ((ntdll!_FILE_OBJECT *)0xffffa402dac559d0) ((ntdll!_FILE_OBJECT *)0xffffa402dac559d0) : 0xffffa402dac559d0 [Type: _FILE_OBJECT *] [+0x000] Type : 5 [Type: short] [+0x002] Size : 216 [Type: short] [+0x008] DeviceObject : 0xffffa402d68cc860 : Device for "\Driver\volmgr" [Type: _DEVICE_OBJECT *] [+0x010] Vpb : 0xffffa402d47fa3a0 [Type: _VPB *] [+0x018] FsContext : 0xffffe388a39a11b0 [Type: void *] [+0x020] FsContext2 : 0xffffe388a6297370 [Type: void *]可以看到,其偏移+0x20处的对象是FsContext2,其值为0xffffe388a6297370,而第五个参数就是FsContext2,而a5+0x68检查的就是FsContext2+0x68位置的Flag。
我们可以从泄露的windows nt源码中找到关于FsContext2结构的蛛丝马迹,windows nt源码kdexts\ntfs.c的第963行
DumpCcb( (ULONG) File_Object.FsContext2, 1 );Ntfs通过调用DumpFileObject函数中的DumpCcb函数获取ccb(ContextControlBlock),看下DumpCcb函数,ntfs.c的第806行:
VOID DumpCcb ( IN ULONG Address,//FileObject->FsContext2 IN ULONG Options ) { …… pCcb = (PCCB) Address; …… }其实PCCB是上下文控制块,CCB中还包含了文件的信息,比如文件名。
1: kd> dt _FILE_OBJECT 0xffff800bbd4832c0 FsContext2 ntdll!_FILE_OBJECT +0x020 FsContext2 : 0xffffcc8a`116bde50 Void 1: kd> dq 0xffffcc8a116bde50 ffffcc8a`116bde50 00000003`00880709 00000000`00000841 ffffcc8a`116bde60 00000000`00380026 ffffcc8a`1320b4b0 1: kd> dc ffffcc8a`1320b4b0 ffffcc8a`1320b4b0 0057005c 006e0069 006f0064 00730077 \.W.i.n.d.o.w.s. ffffcc8a`1320b4c0 0073005c 00730079 00650074 002e006d \.s.y.s.t.e.m... ffffcc8a`1320b4d0 006e0069 00000069 4134342d 31392d45 i.n.iFileObject->FsContext2最终会直接被强制转换成PCCB对象,其实a5+0x68就是PCCB+0x68,这个值是由什么决定的呢?这需要经过一些逆向分析。这里我简述一下分析过程,首先,我们知道FsContext2的值来自于FileObject,而这些结构体都处于IRP封装中,在nt! NtSetInformationFile函数中:
LODWORD(v32) = IopAllocateIrpExReturn(DeviceObject, DeviceObject->StackSize, (unsigned __int8)(v31 ^ 1), retaddr);// Create IRP v34 = v32; Irp = v32; if ( v32 ) { …… *(_QWORD *)(v36 + 48) = v16; // Get FileObject …… }函数会创建IRP结构,如果创建成功,则会为IRP结构赋初值,其中v36+0x30是FileObject,v16的值来自于v83,而v83的则是通过句柄表获取的object,仍然在nt!NtSetInformationFile中。
v15 = ObReferenceObjectByHandle(Handle, v14, (POBJECT_TYPE)IoFileObjectType, v7, &v83, 0i64); v16 = v83;这个值是通过FileHandle获取的,FileHandle则是外层传入的,在James Forshaw的代码中通过OpenFile获取Handle,其打开的Access定义为MAXIMUM_ALLOWED,就是以最大的允许权限打开文件。
最终我们跟踪到NtOpenFile函数中调用ntfs!NtfsSetCcbAccessFlags设置+0x68偏移位置的flag,其调用路径为
2: kd> k # Child-SP RetAddr Call Site 00 ffffca88`5a766e48 fffff807`52d32d20 Ntfs!NtfsSetCcbAccessFlags 01 ffffca88`5a766e50 fffff807`52d37382 Ntfs!NtfsCommonCreate+0x2080 02 ffffca88`5a767040 fffff807`4fa08829 Ntfs!NtfsFsdCreate+0x202 03 ffffca88`5a767270 fffff807`52045b3d nt!IofCallDriver+0x59 …… 0c ffffca88`5a767980 fffff807`4fbdb3a5 nt!NtOpenFile+0x58 2: kd> p Breakpoint 0 hit Ntfs!NtfsSetCcbAccessFlags: fffff807`52c47c90 4c8bdc mov r11,rsp 2: kd> r rdx rdx=ffff800bc33b34e0 2: kd> dq ffff800bc33b34e0+b8 l1 // _IO_STACK_LOCATION ffff800b`c33b3598 ffff800b`c33b3880 2: kd> dt _IO_STACK_LOCATION ffff800b`c33b3880 FileObject ntdll!_IO_STACK_LOCATION +0x030 FileObject : 0xffff800b`c3d13670 _FILE_OBJECT 2: kd> dt _FILE_OBJECT 0xffff800b`c3d13670 FsContext2 FileName ntdll!_FILE_OBJECT +0x020 FsContext2 : 0xffffcc8a`15611620 Void +0x058 FileName : _UNICODE_STRING "\Windows\system.ini" 2: kd> dd 0xffffcc8a`15611620+68 l1 ffffcc8a`15611688 00000000这里还未赋值,接下来跟踪到NtfsSetCcbAccessFlags如下上下文位置,具体代码如下
fffff807`52c47ce0 0fb74714 movzx eax,word ptr [rdi+14h] fffff807`52c47ce4 6623c2 and ax,dx fffff807`52c47ce7 66094168 or word ptr [rcx+68h],ax 3: kd> dq ffff800bc65d22b0+14 ffff800b`c65d22c4 02000000`001200a9可以看到FsContext2+0x68值来源于0x1200a9和0x1a7与运算的结果,最后的值为0xa1,这个值会最后在NtfsSetLinkInfo中判断,而0x1200a9实际上就是ACE AccessMask,0x1200a9表示文件对当前进程只有Read Permission,而FullControl则是0x1f01ff,如果将File变成normal user可控的文件就会发现。
2: kd> dd ffff800b`bf2a7510+0x14 l1 ffff800b`bf2a7524 001f01ff因此最后0xa1和0x310进行与运算的结果是0x0。
1: kd> p Ntfs!NtfsSetLinkInfo+0x212: fffff807`52d4d8a6 b910030000 mov ecx,310h 1: kd> p Ntfs!NtfsSetLinkInfo+0x217: fffff807`52d4d8ab 6641854d68 test word ptr [r13+68h],cx 1: kd> dd r13+68 l1 ffffcc8a`167399c8 000000a1最终会进入SeAccessCheck检查文件访问权限,从而阻止创建硬链接。感兴趣的读者可以尝试用windbg修改FsContext2->Flag的值令其与0x310与运算后值为1,则可以创建高权限文件的硬链接。
DfMarshal系列漏洞CVE-2018-8550调试记录
DfMarshal系列漏洞CVE-2018-8550调试记录
Author: k0shl of 360 Vulcan Team
关于CVE-2018-8550(DfMarshal系列漏洞)前段时间看了一下James forshaw关于DfMarshal的漏洞,在本子上记录了比较多的东西,于是写这篇博客总结一下,漏洞流程并不复杂,DfMarshal在对对象(object)进行散集(UnMarshal)的过程中,如果object通过聚合(Aggregation)的方式自定义列集(Marshal)方法,最终会导致高权限进程使用特定的Unmarshal方法,在这系列漏洞中即DfMarshal接口, DfMarshal调用自己的UnMarshal方法(来自coml2.dll)而非COM默认的Unmarshal方法(来自combase.dll)。
关于这一系列漏洞的成因可以参考james forshaw以及看雪-王cb的帖子,王cb用c++方法重构了james forshaw关于DfMarshal中DuplicateHandle条件竞争(TOCTOU)的漏洞攻击流程,两者都可以作为参考,王cb帖子中关于漏洞成因的逆向工程已经十分详尽,这里就不再赘述。
这里需要再说一下关于整个攻击的流程,首先通过COM方法向audiodg申请一个共享内存section,之后调用NtViewMapofSection方法将section handle映射到当前进程空间,之后将section写入sdfmarshalpackage的hmem成员,之后通过高权限进程(比如BITS)触发DfMarshal->Unmarshal最终导致权限提升。
在这一系列的漏洞中,我比较关注的问题在于james forshaw运用的共享内存的方法,其实在james forshaw去年的slide中已经描述了section/file mapping容易出的问题,仔细阅读wrk源码,file mapping本身也属于section的一部分,关于createsection的实现可以阅读wrk源码base\ntos\mm\creatsec.c中MmCreateSection的实现,section有两种类型,一种是paging file,另一种是file mapping。
关于调试这个漏洞的过程中碰到的有趣故事是我这篇博客的主要内容。
1. 关于”Undocumentation API” NtQuerySection的故事在王cb的帖子中提到了一个未文档化的API NtQuerySection,帖子中说他利用这种方法获取Section句柄,其实NtQuerySection并非真正的未文档化的API,在WRK中包含关于NtQuerySection的实现逻辑,代码部分实现在ntos\mm\querysec.c第27行,NtQuerySection的函数原型如下:
NTSTATUS NtQuerySection( __in HANDLE SectionHandle, __in SECTION_INFORMATION_CLASS SectionInformationClass, __out_bcount(SectionInformationLength) PVOID SectionInformation, __in SIZE_T SectionInformationLength, __out_opt PSIZE_T ReturnLength )其实函数内部逻辑非常简单,其关键部分在64-237行,主要是调用ObReferenceObjectByHandle获取SECTION数据结构,再根据SectionInformationClass的值获取相应的成员变量内容,保存在buffer里交给SectionInformation指针返回给用户,来看一下SECTION数据结构:
typedef struct _SECTION { MMADDRESS_NODE Address; PSEGMENT Segment; LARGE_INTEGER SizeOfSection; union { ULONG LongFlags; MMSECTION_FLAGS Flags; } u; MM_PROTECTION_MASK InitialPageProtection; } SECTION, *PSECTION;可以看到其中并不包含HANDLE,事实上通过NtQuerySection的代码逻辑可以看出其功能并不是获取section object的句柄,而且是通过句柄获取SECTION结构的诸如address,size等信息。因此王cb帖子中关于这个未文档化API作用的描述是有一些失误的,他的代码中也只是应用NtQuerySection获取audiodg.exe中section的大小判断sdfmarshalpackage中开辟的大小是否足够存放section。
而真正获取句柄的方法是通过NtQuerySystemInformaion直接读取句柄表中的objectype为section的句柄。
2.关于Audiodg.exe的故事正如之前所说我比较关心james forshaw所使用的关于section的方法,Audiodg.exe是Audiosrv的一个子进程,真正的父进程是代理在svchost中的,这两者都是SYSTEM进程。在进入正题前首先我们来看一下接口的调用过程,james forshaw的poc中应用IMMDeviceEnumerator接口调用最终获取到IAudioClient接口指针,从而申请section,调用过程为:
IMMDeviceEnumerator-> GetDefaultAudioEndpoint | -----> IMMDevice->Activate | -----> IAudioClientIMMDeviceEnumerator通过MMDeviceEnumerator class创建实例,这个类是一个InprocServer,因此实际上这里创建的是一个进程内调用过程。
而在IMMDevice->Activate方法中会再次调用CoCreateInstance创建IAudioClient实例,其代码实现逻辑在CEndpointDevice::Activate->CSubEndpointDevice::Activate,代码很简单,但是逻辑过长这里我就不再拷贝了,在调用过程中可以这样下断点。
0:000> ba e1 MMDevApi!CSubEndpointDevice::Activate 0:000> sxe ld: AudioSes.dll 0:000> g Breakpoint 0 hit MMDevApi!CSubEndpointDevice::Activate: 00007ff8`72fcc4c0 4055 push rbp 0:000> g ModLoad: 00007ff8`72fc0000 00007ff8`73030000 C:\WINDOWS\System32\AudioSes.dll ntdll!ZwMapViewOfSection+0x14: 00007ff8`7c2dfb94 c3 ret而IAudioClient接口实现依然是一个进程内接口,接口中方法的代码实现在AudioSes.dll中。
在我调试的过程中发现了两个有趣的地方。
第一个是james forshaw使用IMMDeviceEnumerator最终获得IAudioClient接口指针并调用Initialize的AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED方法创建一个section这并不是必须的,Windows下有一个服务叫做计划任务(tasks schedular)服务,其代理在一个SYSTEM权限的svchost中,它的管理职能实现在svchost的子进程taskhostw.exe中。
可以看到它托管了systemsoundsservice的任务,当systemsoundsservice调用audiosrv服务的时候,会令audiosrv启动audiodg.exe子进程并创建section,这个section就是我们在漏洞触发时使用的section。
下面我们来看一下这个过程,首先Audiosrv的svchost中启动audiodg.exe的调用函数是audiosrv! AudioServerInitialize,调试时我们可以通过windbg附加到audiosrv的svchost上,然后通过ba e1 audiosrv!AudioServerInitialize,之后通过任务管理器kill掉audiodg已经存在的子进程(有一种情况是进程不存在,一般都是存在的,后面会解释,若进程不存在可以看我博文后面的部分,其实很多声卡操作可以激活taskhostw中的功能从而创建audiodg.exe,比如右键右下角扬声器,点击打开音量混合器,随便拖动一下:P),立刻就能捕捉到windbg中断在AudioServerInitialize。
AudioServerInitilize会进入内部函数调用AudioServerInitialize_Internal,函数内部有一个虚函数调用,调用到CWindowsPolicyManager::RpcGetProcess,这里会获取RPC Client的processid。(是的,其实AudioServerInitialize就是RPC接口之一,这点后面会提到。)。
0:007> pc audiosrv!AudioServerInitialize_Internal+0x24a: 00007ff9`13cdc40a ff1508f51200 call qword ptr [audiosrv!_guard_dispatch_icall_fptr (00007ff9`13e0b918)] ds:00007ff9`13e0b918=00007ff922fcfc10 0:007> u rax AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess: 00007ff9`13c59740 488bc4 mov rax,rsp 00007ff9`13c59743 48895808 mov qword ptr [rax+8],rbxAudiosrv会通过RpcGetProcess方法内部调用CApplicationManager::RpcGetProcess最终调用I_RpcBindingInqLocalClientPID获取到绑定RPC的Client的PID,具体的的实现在AUDIOSRVPOLICYMANAGER.dll中,
__int32 __fastcall CApplicationManager::RpcGetProcess(CApplicationManager *this, void *a2, struct CProcess **a3) { v121 = a3; v114 = -2i64; v3 = a2; v4 = (CApplicationManager *)g_ApplicationManager; v93 = (CApplicationManager *)g_ApplicationManager; *a3 = 0i64; v5 = I_RpcBindingInqLocalClientPID(a2, &dwProcessId); //获取绑定的rpc client的pid if ( v5 ) return wil::details::in1diag3::Return_Win32( retaddr, (void *)0x3B2, (unsigned __int64)"multimedia\\audiocore\\server\\audiosrv\\windowspolicymanager\\applicationmanager.cpp", (const char *)(unsigned int)v5); v77 = 0i64; v7 = CApplicationManager::TryFindProcessFromProcessId(v4, dwProcessId, (struct CProcess **)&v77); 0:007> pc AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46: 00007ff9`13c6c7fe ff15b40a0200 call qword ptr [AUDIOSRVPOLICYMANAGER!_imp_I_RpcBindingInqLocalClientPID (00007ff9`13c8d2b8)] ds:00007ff9`13c8d2b8={RPCRT4!I_RpcBindingInqLocalClientPID (00007ff9`22935250)} 0:007> k//stack trace Child-SP RetAddr Call Site 00000005`0bbfe810 00007ff9`13c59761 AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46 00000005`0bbfea10 00007ff9`13cdc410 AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess+0x21 00000005`0bbfea50 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x250 00000005`0bbfebc0 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d 00000005`0bbfec20 00007ff9`229bb4a6 RPCRT4!Invoke+0x73其中I_RpcBindingInqLocalClientPID的第二个参数就是目标的PID,作为传出参数步过后可以看到PID的值。
0:007> p AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x4c: 00007ff9`13c6c804 85c0 test eax,eax 0:007> dd rdx l1 00000005`0bbfe8d0 000016a4//pid = 0x16a4其值为0x16a4,十进制为5796,即为我在之前的图片中taskhostw的pid,接下来函数会最终通过CAudioDGProcess::LaunchADGProcess创建audiodg.exe进程,其函数实现如下(中间省略号跳过了赋值audiodg.exe进程安全描述符的过程):
__int64 __fastcall CAudioDGProcess::LaunchADGProcess(__int64 a1, unsigned __int8 a2) { if ( !GetSystemDirectoryW((LPWSTR)&v27, 0x104u) )//获取System32路径 C:\windows\system32 { } v8 = StringCbCatExW((STRSAFE_LPWSTR)&v27, v5, v6, &v22, &v21, dwCreationFlags);// //…… *(_QWORD *)&ProcessInformation.dwProcessId = 0i64; if ( CreateProcessW( 0i64, (LPWSTR)&v27,//创建进程commandline为 c:\windows\system32\audio.exe &ProcessAttributes, 0i64, 1, v2 << 18, 0i64, 0i64, (LPSTARTUPINFOW)&v26, &ProcessInformation) ) 0:009> pc audiosrv!CAudioDGProcess::LaunchADGProcess+0x82: 00007ff9`13cb7426 e8cd190000 call audiosrv!StringCbCatExW (00007ff9`13cb8df8) 0:009> p audiosrv!CAudioDGProcess::LaunchADGProcess+0x87: 00007ff9`13cb742b 8bd8 mov ebx,eax 0:009> dc 50bcfe280 00000005`0bcfe280 003a0043 0057005c 004e0049 004f0044 C.:.\.W.I.N.D.O. 00000005`0bcfe290 00530057 0073005c 00730079 00650074 W.S.\.s.y.s.t.e. 00000005`0bcfe2a0 0033006d 005c0032 00550041 00490044 m.3.2.\.A.U.D.I. 00000005`0bcfe2b0 0044004f 002e0047 00580045 00000045 O.D.G...E.X.E... Stack trace: 0:005> k Child-SP RetAddr Call Site 00000005`0bafdfb0 00007ff9`13cb71af audiosrv!CAudioDGProcess::LaunchADGProcess+0x82 00000005`0bafe310 00007ff9`13cd6642 audiosrv!CAudioDGProcess::LaunchAndWaitForADGStartup+0x47 00000005`0bafe400 00007ff9`13cdc3da audiosrv!CAudioDGProcess::InstantiateADG+0x112 00000005`0bafe4c0 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x21a 00000005`0bafe630 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d 00000005`0bafe690 00007ff9`229bb4a6 RPCRT4!Invoke+0x73当然,当audiodg.exe进程已经存在的时候,AudioServerInitialize_Internal会直接跳转,不会进入到后续分支(比如使用james forshaw的这种方法在调用Initialize的时候会先进入这个函数,但是如果audiodg.exe进程存在则不会进入创建进程的分支),这点感兴趣的读者可以自己调试,代码实现也在AudioServerInitialize_Internal函数中。
因此实际上IMMDevieEnumerator只是确保audiodg.exe一定会被创建出来,若当前系统audiodg.exe已被创建,可以直接通过NtQuerySystemInformation的方法把audiodg.exe进程空间句柄表的section object获取出来,再通过NtMapViewOfSection映射进当前进程空间。
当然关于section的创建并不是在AudioServerInitialize中完成的,这就是第二个有趣的地方,如果想正常调试james forshaw的PoC的内容,我们需要kill掉taskhostw进程,同时kill掉audiodg.exe,这时候不要再做其他的声卡相关操作,否则又会在audiosrv触发创建audio.srv流程(比如SndVol.exe进程)。
第二点我们来看看IAudioClient是怎么把audiodg.exe及section创建出来的,其实我们在当前中调用COM接口,一直是进程内通信,audioses.dll被加载进当前空间并调用它的方法。而之所以会out-of-process调用到audiosrv方法,其实是IAudioClient中调用了RPC接口。
IAuidoClient的Initialize方法会调用CAudioClient::InitializeInternalHelper函数,最终调用CAudioClient::InitializeAudioServer向audioserver发送rpc请求,当audiodg.exe进程不存在时,server调用CreateProcess创建audiodg.exe, InitializeAudioServer的函数实现如下:
__int64 __fastcall CAudioClient::InitializeAudioServer(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7) { LODWORD(v8) = GetAudioServerBindingHandle(a1, L"AudioClientRpc", (RPC_BINDING_HANDLE *)&v10); if ( (signed int)v8 < 0 || (CAudioClient::GetVadServerSettings(v7, (__m128i *)&v12), v8 = NdrClientCall3(&pProxyInfo, 4u, 0i64, v10).Pointer, v11 = (__int64)v8, (signed int)v8 < 0) ) {其中NdrClientCall3最终调用RPC过程,它的第二个参数指向ProcNum,我们可以通过RPCView看到函数调用,或者直接通过IDA pro查看RPC Server调用RpcServerRegisterIf3时的MIDL规范的结构体找到IDL的方法。
RPC Server的注册过程在CAudioSrv::VAD_AudiosrvServiceStart中实现。
最终创建audiodg.exe进程,关于进程创建我在上面已经提到,这里不再赘述,这时虽然audiodg.exe被创建,但是section并未创建在audiodg.exe进程中。
接下来在调用完CAudioClient::InitializeAudioServer之后,会继续调用CAudioClient::CreateRemoteStream,这同样是一个RPC调用过程。
__int64 __fastcall CAudioClient::CreateRemoteStream(__int64 a1) { v1 = *(_DWORD *)(a1 + 180); v2.Pointer = NdrClientCall3(&pProxyInfo, 7u, 0i64, *(_QWORD *)(a1 + 0xD8)).Pointer;ProcNum为7,根据IDL可以知道这个CreateRemoteStream会在AudioServerCreateStream,可以在svchost中通过be a1 audiosrv!AudioServerCreateStream下断点,之后在Client单步执行即可命中断点。之后继续跟踪,调用过程如下:
audiosrv!CVADServer::CreateStream | audiosrv!CAudioResourceManager::CreateStream | audiosrv!InitializeStreamAndModeDescriptors | audiosrv!CCompositeSystemEffect::Initialize最终在CCompositeSystemEffect::Initialize会调用MakeAndInitialize函数之后调用CoCreateInstance创建APOWrapperSrv Class方法实例,在audiodg.exe列集过程中会通过file mapping创建stream的section。可以在audiodg.exe这样下断点:ba e1 ntdll!NtCreateSection。在svchost中单步执行会观察到audiodg.exe进程命中断点。
0:007> pc audiosrv!CCompositeSystemEffect::Initialize+0x127: 00007ffd`04b2bd27 e834a40000 call audiosrv!Microsoft::WRL::Details::MakeAndInitialize<CAPOWrapperClient,IAudioProcessingObject,unsigned short const * __ptr64 & __ptr64,enum APO_TYPE & __ptr64,_GUID const & __ptr64> (00007ffd`04b36160) 0:007> p 0:004> g Breakpoint 0 hit ntdll!NtCreateSection: 00007ffd`11e9ffc0 4c8bd1 mov r10,rcx 0:001> k Child-SP RetAddr Call Site 0000009c`8807b828 00007ffd`0e155521 ntdll!NtCreateSection 0000009c`8807b830 00007ffd`0e156570 KERNELBASE!CreateFileMappingNumaW+0x111 0000009c`8807b8f0 00007ffd`11548049 KERNELBASE!CreateFileMappingW+0x20 0000009c`8807b940 00007ffd`11548477 clbcatq!StgIO::MapFileToMem+0x79 0000009c`8807b980 00007ffd`11546cf8 clbcatq!StgIO::Open+0x25b 0000009c`8807ba00 00007ffd`1153b636 clbcatq!StgDatabase::InitDatabase+0x108 0000009c`8807ba60 00007ffd`1153b4e1 clbcatq!OpenComponentLibraryEx+0x66 0000009c`8807bab0 00007ffd`1153adf1 clbcatq!OpenComponentLibraryTS+0x21 0000009c`8807bae0 00007ffd`1153b2f2 clbcatq!_RegGetICR+0x129 0000009c`8807bda0 00007ffd`11526fd2 clbcatq!CoRegGetICR+0x76 0000009c`8807bdd0 00007ffd`11521b41 clbcatq!CComClass::Init+0x5442 0000009c`8807bf90 00007ffd`1123eb9f clbcatq!CComCLBCatalog::GetClassInfoW+0x81 0000009c`8807bfe0 00007ffd`1123e71d combase!CComCatalog::GetClassInfoInternal+0x3ef [onecore\com\combase\catalog\catalog.cxx @ 3419] 0000009c`8807c220 00007ffd`1125febc combase!CComCatalog::GetClassInfoW+0x5d [onecore\com\combase\catalog\catalog.cxx @ 1114] 0000009c`8807c370 00007ffd`1125cb14 combase!GetClassInfoWithInprocOrLocalServer+0x70 [onecore\com\combase\inc\comcataloghelpers.hpp @ 58] 0000009c`8807c3c0 00007ffd`1125b91b combase!wCoGetTreatAsClass+0x88 [onecore\com\combase\class\cogettreatasclass.cpp @ 44] 0000009c`8807c490 00007ffd`1125b35f combase!CClassCache::CClassEntry::Complete+0x67 [onecore\com\combase\objact\dllcache.cxx @ 751] 0000009c`8807c500 00007ffd`11224f01 combase!CClassCache::CClassEntry::Create+0x4b [onecore\com\combase\objact\dllcache.cxx @ 872] 0000009c`8807c560 00007ffd`1125aec5 combase!CClassCache::GetClassObjectActivator+0x571 [onecore\com\combase\objact\dllcache.cxx @ 5424] 0000009c`8807c6d0 00007ffd`11222606 combase!CClassCache::GetClassObject+0x45 [onecore\com\combase\objact\dllcache.cxx @ 5271] 0000009c`8807c740 00007ffd`1123d937 combase!ICoGetClassObject+0x6f6 [onecore\com\combase\objact\objact.cxx @ 1500] 0000009c`8807cae0 00007ffd`1123ce28 combase!GetPSFactoryInternal+0x1f7 [onecore\com\combase\dcomrem\riftbl.cxx @ 2542] 0000009c`8807cc20 00007ffd`112416d0 combase!CStdMarshal::GetPSFactory+0x50 [onecore\com\combase\dcomrem\marshal.cxx @ 6408] 0000009c`8807cd70 00007ffd`1124584a combase!CStdMarshal::CreateStub+0x120 [onecore\com\combase\dcomrem\marshal.cxx @ 6681] 0000009c`8807cfa0 00007ffd`1124467c combase!CStdMarshal::MarshalObjRefImpl+0x5ca [onecore\com\combase\dcomrem\marshal.cxx @ 1157] 0000009c`8807d110 00007ffd`1123933f combase!CStdMarshal::MarshalObjRef+0x8c [onecore\com\combase\dcomrem\marshal.cxx @ 1078]在MakeAndInitialize中关键调用如下:
v15 = CoCreateInstance( &GUID_3a8b5a92_80b0_48b3_8197_701ecd3261e4, 0i64, 0x17u, &GUID_69fed9b6_5405_48b8_3db0_4ca492fc3677, (LPVOID *)v9 + 7);最终创建APOWrapperSrv Class的IAPOWrapperSrv接口实例,在audiodg中会创建storage类型的database,从而调用file mapping创建一个section。这个section会在后面作为共享stream使用。
待解决的问题:
我是在rs5 x64的环境下调试的这个漏洞,在分析的过程中也发现了james Forshaw在case下留的几点rs5环境下的安全机制,有待后续研究。
作者能力有限,若有错误请指正,感谢阅读。
写在98篇漏洞分析之后---2019.03.09
[CVE-2016-5108]VideoLAN VLC Media Player 2.2.1越界写拒绝服务漏洞
[CVE-2016-5108]VideoLAN VLC Media Player 2.2.1越界写拒绝服务漏洞
作者:k0shl 转载请注明出处:https://whereisk0shl.top
漏洞说明VideoLAN VLC Media Player是一款播放器,这个漏洞编号为CVE-2016-5108,播放器在处理某数据的时候,由于处理某数据时没有对数据长度进行严格校验,导致越界写入引发拒绝服务漏洞,下面对此漏洞进行详细分析。
软件下载:
https://www.exploit-db.com/apps/b8c997e772be343e1664fee14c1fb9b7-vlc-2.2.1-win32.exe
PoC:
https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/sploits/41025.mov
首先打开VLC,打开mov文件,程序崩溃,附加windbg
(1588.788): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=ffffffff ebx=0000178f ecx=00000058 edx=0000178f esi=0d1fd000 edi=1a44fdc8 eip=68322501 esp=1a44fd30 ebp=155cb8de iopl=0 nv up ei ng nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286 *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\VideoLAN\VLC\plugins\codec\libadpcm_plugin.dll - libadpcm_plugin+0x2501: 68322501 66891e mov word ptr [esi],bx ds:0023:0d1fd000=????通过kb回溯堆栈调用
0:024> kb *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\KERNELBASE.dll - ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 1a44fd38 75536a18 7a0e1e88 1a44fdc8 0d1fd000 libadpcm_plugin+0x2501 00000000 00000000 00000000 00000000 00000000 KERNELBASE!InterlockedCompareExchange+0xf8之前只有一个Interlock的原子函数调用,来看一下当前是用bx向esi指针写入数据,esi指针的值是一个无效值,来看一下之前的情况。
0:024> dd 0d1fd000-8 0d1fcff8 c0c0178f c0c0c0c0 ???????? ????????可以看到,前面还有值,这有可能是一个长度控制不严格引发的越界写漏洞。
漏洞分析分析的过程中,发现了这个漏洞处于一个函数中。
int __cdecl sub_714C18B0(int a1, int *a2) { while ( 1 ) { } }函数sub_714c18b0中有一处while循环,这个while循环代码量很长,执行的是一个向内存拷贝的操作,这里我就不具体分析整个代码逻辑,只看关键部分,首先是对待拷贝区的赋值。
.text:714C24B7 ; 846: v83 = 88; .text:714C24B7 cmova ecx, eax .text:714C24BA mov [esi], edx714c24ba地址执行完毕后,edx存放的是待拷贝缓冲区的指针,它会交给esi地址中。
0:012> g Breakpoint 0 hit eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40 eip=6bdf24ba esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x24ba: 6bdf24ba 8916 mov dword ptr [esi],edx ds:0023:16d2fdc8=16d2fd74 0:006> p eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40 eip=6bdf24bc esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x24bc: 6bdf24bc 897c2418 mov dword ptr [esp+18h],edi ss:0023:16d2fd48=16d2fde8 0:006> dd 16d2fdc8 16d2fdc8 0e1f6e40之后每轮都会向这个缓冲区指针内写入值,接下来继续单步跟踪,发现了要拷贝字符串的赋值。
0:006> p eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40 eip=6bdf24c0 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x24c0: 6bdf24c0 e9ef000000 jmp libadpcm_plugin+0x25b4 (6bdf25b4) 0:006> p eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40 eip=6bdf25b4 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x25b4: 6bdf25b4 8b3c8dc062df6b mov edi,dword ptr libadpcm_plugin!vlc_entry_license__2_2_0b+0x2c20 (6bdf62c0)[ecx*4] ds:0023:6bdf6390=00000424在6bdf25b4地址位置执行了一处赋值操作,会把6bdf62c0这个固定缓冲区中的内容拷贝到edi中,而是从ecx*4+6bdf62c0的位置向前拷贝,ecx会逐步减少,拷贝的长度就是(6bdf6390-6bdf62c0)*8,这个后面会提到。
接下来会将edi的值加eax的值,这个eax就是每次循环最后赋值的值,相加后算数右移3位。
0:006> p eax=00000058 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424 eip=6bdf25bf esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x25bf: 6bdf25bf 89f8 mov eax,edi 0:006> p eax=00000424 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424 eip=6bdf25c1 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x25c1: 6bdf25c1 c1f803 sar eax,3 0:006> p eax=00000084 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424 eip=6bdf25c4 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei pl nz na pe cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000207 libadpcm_plugin+0x25c4: 6bdf25c4 89de mov esi,ebx算数右移后的值在eax中,是0x84,随后会将eax的值交给ebx,再把ebx的值和上一轮的值相加。
0:006> p eax=00000084 ebx=00000000 ecx=00000034 edx=000004a8 esi=00000000 edi=00000424 eip=6bdf25cf esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 libadpcm_plugin+0x25cf: 6bdf25cf 0f44d0 cmove edx,eax 0:006> p eax=00000084 ebx=00000000 ecx=00000034 edx=00000084 esi=00000000 edi=16d2fdc8 eip=6bdf25f4 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 libadpcm_plugin+0x25f4: 6bdf25f4 0317 add edx,dword ptr [edi] ds:0023:16d2fdc8=00001200最后会将edx的值先存放进eax指针,这个指针很熟悉吧,就是之前相加的指针,之后每次都会叠加,而edx同时也会交给ebx,最后ebx会执行拷贝。
0:006> p eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8 eip=6bdf3494 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x3494: 6bdf3494 8910 mov dword ptr [eax],edx ds:0023:16d2fdc8=00001200 0:006> p eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8 eip=6bdf3496 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei ng nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293 libadpcm_plugin+0x3496: 6bdf3496 89d3 mov ebx,edx 0:006> p eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8 eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei pl nz ac pe cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000217 libadpcm_plugin+0x2501: 6bdf2501 66891e mov word ptr [esi],bx ds:0023:0e1f6e40=c0c0那么现在这个漏洞的原因就很清晰了,从6bdf6390开始向前依次拷贝内容,每次拷贝的内容都会交给固定指针,这个指针里的值会进行叠加算术右移等操作,之后继续进行拷贝,而这个长度没有进行控制。
而每一轮拷贝,拷贝区长度都会加0x10。
0:006> p eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8 eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0 nv up ei pl nz ac pe cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000217 libadpcm_plugin+0x2501: 6bdf2501 66891e mov word ptr [esi],bx ds:0023:0e1f6e40=c0c0 0:006> g Breakpoint 1 hit eax=00000031 ebx=00001369 ecx=00000058 edx=00001369 esi=0e1f6e50 edi=16d2fdc8 eip=6bdf2501 esp=16d2fd30 ebp=11b208c3 iopl=0 nv up ei pl nz ac po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000213 libadpcm_plugin+0x2501: 6bdf2501 66891e mov word ptr [esi],bx ds:0023:0e1f6e50=c0c0注意esi的值,接下来来看一下几轮拷贝之后esi最开始拷贝位置的值的前后变化。
0:006> dd esi 0e1f6e40 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6e50 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6e60 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6e70 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6e80 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6e90 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0e1f6ea0 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 0:006> dd 0e1f6e40 0e1f6e40 c0c01284 c0c0c0c0 c0c012fc c0c0c0c0 0e1f6e50 c0c01369 c0c0c0c0 c0c013cc c0c0c0c0 0e1f6e60 c0c01426 c0c0c0c0 c0c01478 c0c0c0c0 0e1f6e70 c0c014c2 c0c0c0c0 c0c01506 c0c0c0c0 0e1f6e80 c0c01543 c0c0c0c0 c0c0157b c0c0c0c0 0e1f6e90 c0c015ae c0c0c0c0 c0c015dc c0c0c0c0 0e1f6ea0 c0c01606 c0c0c0c0 c0c0162c c0c0c0c0 0e1f6eb0 c0c0164e c0c0c0c0 c0c0166d c0c0c0c0而拷贝区的内容则是这样
0:006> dd 6bdf6360 6bdf6360 00000151 00000173 00000198 000001c1 6bdf6370 000001ee 00000220 00000256 00000292 6bdf6380 000002d4 0000031c 0000036c 000003c3 6bdf6390 00000424 0000048e 00000502 00000583 6bdf63a0 00000610 000006ab 00000756 00000812 6bdf63b0 000008e0 000009c3 00000abd 00000bd0 6bdf63c0 00000cff 00000e4c 00000fba 0000114c也就是说,每次拷贝4个字节,经过算法处理后,会在待拷贝区偏移8字节,长度还要乘以2,最后也就是说拷贝的长度,超过了申请buffer的长度,造成了越界写,引发了异常。
这个漏洞不能造成任意地址写,因此不能利用,是个拒绝服务漏洞。