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
|
-----> IAudioClient
IMMDeviceEnumerator通过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],rbx
Audiosrv会通过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环境下的安全机制,有待后续研究。
作者能力有限,若有错误请指正,感谢阅读。
我是文章作者王cb
我微信号cbwang505
我对你的研究很感兴趣,能否留下联系方式
a test