在上一篇文章中,我展示了如何使用USO客户端与USO服务进行交互,并通过 StartScan
命令让其按需加载windowscoredeviceinfo.dll
。不过这并没有达到我们最终的目的。所以,我对客户端和服务器的一部分进行了逆向工程,以便通过代码模拟它的行为,我实现了一个独立的项目,可以在漏洞利用中进行代码重用,简化漏洞利用的过程。
USO客户端 – 静态分析
虽然我在研究过程中也使用了Ghidra,但为了保持一致性,我在这次演示中会使用IDA。
在IDA中打开usoclient.exe
之前,我用下面的命令下载了相应的PDB文件。理论上,如果pdb在同一目录下,IDA会自动完成这个操作,但我发现它并不总是有效。然后可以用File > Load File > PDB File...
加载PDB文件。
symchk
工具与Windows SDK一起,一般位于C:/Program Files (x86)Windows Kits10/Debuggersx64
中。我们使用它下载对应的pdb文件。
symchk /s "srv*c:symbols*https://msdl.microsoft.com/download/symbols" "c:windowssystem32usoclient.exe"
注: PDB代表 “程序数据库”。程序数据库(PDB)是一种专有的文件格式(由微软开发),用于存储程序(或通常是程序模块,如DLL或EXE)的调试信息。维基百科
usoclient.exe
现在已经在IDA中打开了,符号也加载完毕,我们从哪里开始呢?我们知道 StartScan
的命令是一个有效的 “触发器”,所以,我们自然会在二进制文件中寻找这个字符串的出现,并通过 “Xref” 来找出它的使用位置。
StartScan
字符串在两个函数中被使用:PerformOperationOnSession()
和PerformOperationOnManager()
。我们来检查第一个函数,检查它对应的伪代码。
这似乎是一个 Switch Case。输入的内容与一系列硬编码命令进行比较:”StartScan”、”StartDownload”、”StartInstall “等。如果有匹配的命令,就会进入对应的分支。
例如,当使用 “StartScan “选项时,会运行以下代码。
v5 = *(_QWORD *)(*(_QWORD *)v3 + 168i64);
v6 = _guard_dispatch_icall_fptr(v3, 0i64);
if ( v6 >= 0 )
return 0i64;
这段代码没有什么意义。
所以,我暂时认为这是一个死胡同,决定改用寻找这个函数的Xrefs
来往上走。
这个函数只被调用一次。
然后,我快速查看了一下伪代码,我立刻发现了以下调用。CoInitializeEx()
、CoCreateInstance()
、CoSetProxyBlanket()
等。因为之前已经玩过COM(Component Object Model),所以我认出了API调用的顺序。
我们来仔细看看下面的调用。
根据微软的文档,你可以调用CoCreateInstance()
来创建一个与指定的CLSID相关联的类的单个未初始化对象(CoCreateInstance函数)
函数原型:
HRESULT CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID *ppv
);
-
rclsid
是与创建对象的数据和代码相关的CLSID。 -
riid
是与对象通信的接口标识符的引用。
如果我们想要将此用于USO客户端的调用,则意味只需要创建CLSID为b91d5831-bbbd-4608-8198-d72e155020f7
的对象,并使用IID为07f3afac-7c8a-4ce7-a5e0-3d24ee8a77e0
的接口与其通信。
在读了James Forshaw的文章利用任意文件写入进行本地权限提升后,我知道接下来要做什么了。多亏了他的名为OleViewDotNet
的工具,对DCOM对象进行逆向非常的容易。
如果你熟悉这个概念,可以跳过下一部分。此处了解更多信息。
关于(D)COM的简单介绍
正如我前面所说,COM是 Component Object Model 的缩写。它是微软定义的进程间通信标准。由于我自己对这个技术不是很了解,所以就不详细介绍了。
不过需要注意的关键点是客户端和服务器之间的通信是如何完成的。下面的图上有描述。客户端的调用经过一个Proxy,然后经过一个Channel,这个 Channel 是 COM 的一部分。经过marshaled的调用由于 RPC Runtime 传输到服务器的进程中,最后由Stub对参数进行unmarshaled,再转发给服务器。
后果是,我们只能找到客户端代理的定义,而且,我们可能会错过一些服务器端的关键信息。
对COM通信进行逆向
让我们开始COM对象的逆向工程。使用OleViewDotNet
让我们知道它的CLSID,这一步比较的简单。
首先,我们可以通过 Registry > Local Services
,列举主机上运行的服务所暴露的所有对象。因为我们也知道服务的名称,所以我们可以通过关键字orchestrator
缩小列表范围。这将产生一些对象,我们可以手动检查以找到我们要找的对象。UpdateSessionOrchestrator
.这个CLSID与我们之前在逆向工程USO客户端时看到的**CLSID一致:b91d5831-b1bd-4608-8198-d72e155020f7
。
下一步将是展开相应的节点,以便枚举对象的所有接口。然而,在这种情况下,有时它会失败,并产生以下错误:Error querying COM interface - ClassFactory cannot supply requested class
。
我们只能手动操作了,对客户端进行动态分析,以了解RPC调用的工作情况。
为此,我使用了这三个工具:
- IDA(配置了调试符号)。
-
IDA的x86/64 Windows调试服务器-
C:Program Files (x86)IDA 6.8dbgsrvwin64_remotex64.exe
。 - WinDbg(配置了调试符号)。
We already know that the CoCreateInstance()
call is used to instantiate the remote COM object. As a result the variable pInterface
, as its name implies, holds a pointer to the interface with the IID 07f3afac-7c8a-4ce7-a5e0-3d24ee8a77e0
, which will be used to communicate with the object. My goal now is to understand what happens next. Therefore, I put a breakpoint on the first _guard_dispatch_icall_fptr
call that comes right after.
我们已经知道,CoCreateInstance()
的调用是用来实例化远程COM对象的,因此变量 pInterface
如它的名字一样,有一个指向IID为 “07f3afac-7c8a-4ce7-a5e0-3d” 接口的指针,它将被用来与对象进行通信。我现在的目标是了解接下来会发生什么。因此,我在紧接着的第一个_guard_dispatch_icall_fptr
调用上设置了一个断点。
以下是调用前程序的执行过程:
-
RCX'寄存器保存着接口指针的位置(即
pInterface`)。 -
RCX
所指向的值被写入RAX,即RAX=pInterface
。 - 储存在 “RSI “中的值被复制到 “RDX “中。
-
RAX+0x28
指向的值被载入RAX
,即ProxyVTable[5]
。
RCX
的值是0x000002344FA53D68
。让我们看看用WinDbg能在这个地址找到什么。
0:000> dqs 0x00002344FA53D68 L1
00000234`4fa53d68 00007ff8`e48fd560 usoapi!IUpdateSessionOrchestratorProxyVtbl+0x10
我们找到UpdateSessionOrchestrator的接口的Proxy VTable的起始地址。然后我们可以查看VTable中的所有指针。
0:000> dqs 0x00007ff8e48fd560 LB
00007ff8`e48fd560 00007ff8`e48f8040 usoapi!IUnknown_QueryInterface_Proxy
00007ff8`e48fd568 00007ff8`e48f7d90 usoapi!IUnknown_AddRef_Proxy
00007ff8`e48fd570 00007ff8`e48f7ed0 usoapi!IUnknown_Release_Proxy
00007ff8`e48fd578 00007ff8`e48f7dc0 usoapi!ObjectStublessClient3
00007ff8`e48fd580 00007ff8`e48f8090 usoapi!ObjectStublessClient4
00007ff8`e48fd588 00007ff8`e48f7e80 usoapi!ObjectStublessClient5
00007ff8`e48fd590 00007ff8`e48f7ef0 usoapi!ObjectStublessClient6
00007ff8`e48fd598 00007ff8`e48f7e60 usoapi!ObjectStublessClient7
00007ff8`e48fd5a0 00007ff8`e49068b0 usoapi!IID_IMoUsoUpdate
00007ff8`e48fd5a8 00007ff8`e48fefb0 usoapi!CAutomaticUpdates::`vftable'+0x3b0
00007ff8`e48fd5b0 00000000`00000019
前三个函数是QueryInterface
、AddRef
和Release
。这三个函数是COM接口从IUnknown
继承的函数。然后,后面还有5个函数,但我们不知道它们的名字。
为了找到更多关于VTable的信息,我们必须检查服务端。我们知道COM对象的名字—“UpdateSessionOrchestrator”,我们也知道服务的名字—“USOsvc”,所以理论上,我们应该能在 “usosvc.dll” 中找到我们需要的所有信息。
.rdata:00000001800582F8 dq offset UpdateSessionOrchestrator::QueryInterface(void)
.rdata:0000000180058300 dq offset UpdateSessionOrchestrator::AddRef(void)
.rdata:0000000180058308 dq offset UpdateSessionOrchestrator::Release(void)
.rdata:0000000180058310 dq offset UpdateSessionOrchestrator::CreateUpdateSession(tagUpdateSessionType,_GUID const &,void * *)
.rdata:0000000180058318 dq offset UpdateSessionOrchestrator::GetCurrentActiveUpdateSessions(IUsoSessionCollection * *)
.rdata:0000000180058320 dq offset UpdateSessionOrchestrator::LogTaskRunning(ushort const *)
.rdata:0000000180058328 dq offset UpdateSessionOrchestrator::CreateUxUpdateManager(IUxUpdateManager * *)
.rdata:0000000180058330 dq offset UpdateSessionOrchestrator::CreateUniversalOrchestrator(IUniversalOrchestrator * *)
这里是完整的VTable,我们可以看到偏移量5的函数是UpdateSessionOrchestrator::LogTaskRunning(ush)
最后,RDX的值是0x000002344FA39450
。我们也来看看这个地址能找到什么,这次用IDA找找看
这个地方只是一个指针,指向unicode字符串L"StartScan"
。
所有这些信息可以归纳如下:
RAX = VTable[5] = `UpdateSessionOrchestrator::LogTaskRunning(ushort const *)`
RCX = argv[0] = `UpdateSessionOrchestrator pInterface`
RDX = argv[1] = L"StartScan"
如果我们考虑到Windows的x86_64调用惯例,可以用下面的伪代码来表示。
pInterface->LogTaskRunning(L"StartScan");
同样的过程可以应用于下一次调用。
这将产生以下结果:
RAX = VTable[0] = `UpdateSessionOrchestrator::QueryInterface()`
RCX = argv[0] = `UpdateSessionOrchestrator pInterface`
RDX = argv[1] = `*GUID(c57692f8-8f5f-47cb-9381-34329b40285a)`
R8 = argv[2] = Output pointer location
这里,返回的值是 “NULL”,所以,”if”语句后面的所有代码都将被忽略。
因此,我们可以跳过它,直接跳转到这里。
不错,我们越来越接近目标PerformOperationOnSession()
了。
在逆向工程过程中,我们发现如下的调用。
RAX = VTable[3] = `UpdateSessionOrchestrator::CreateUpdateSession(tagUpdateSessionType,_GUID const &,void * *)`
RCX = argv[0] = `UpdateSessionOrchestrator pInterface`
RDX = argv[1] = 1
R8 = argv[2] = `*GUID(fccc288d-b47e-41fa-970c-935ec952f4a4)`
R9 = argv[3] = `void **param_3 (usoapi!IUsoSessionCommonProxyVtbl+0x10)` --> IUsoSessionCommon pProxy
在这里,我们可以看到另一个接口被嵌入。IUsoSessionCommon
.它的IID是fccc288d-b47e-41fa-970c-935ec952f4a4
,它的VTable有68个条目,所以我在这里不列出所有的功能。
Next there is a CoSetProxyBlanket()
call. This is a standard WinApi function that is used to set the authentication information that will be used to make calls on the specified proxy (Source: CoSetProxyBlanket function).
接下来有一个CoSetProxyBlanket()
的调用。这是一个标准的WinApi函数,用于 设置在指定代理上进行调用的认证信息 (CoSetProxyBlanket函数)。
如果我们将所有的十六进制值变成Win32常量,就会产生以下API调用。
IUsoSessionCommonPtr usoSessionCommon;
CoSetProxyBlanket(usoSessionCommon, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, NULL);
现在,我们可以进入 “PerformOperationOnSession() “函数查看,又回到了之前那段没有意义的代码。不过,由于我们刚刚的逆向,目标现在已经越来越清晰了。这是一个对IUsoSessionCommon
代理的简单调用。我们只需要确定调用哪个函数,用哪个参数。
有了这个最后的断点,函数的偏移量和参数就可以很容易地确定。
RAX = VTable[21] = combase_NdrProxyForwardingFunction21
RCX = argv[0] = IUsoSessionCommon pProxy
RDX = argv[1] = 0
R8 = argv[2] = 0
R9 = argv[3] = L"ScanTriggerUsoClient"
这就相当于执行了下面的伪代码。
pProxy->Proc21(0, 0, L"ScanTriggerUsoClient");
如果把所有的部分放在一起,USO客户端中的 “StartScan “操作可以用下面的代码来表示。
HRESULT hResult;
// Initialize the COM library
hResult = CoInitializeEx(0, COINIT_MULTITHREADED);
// Create the remote UpdateSessionOrchestrator object
GUID CLSID_UpdateSessionOrchestrator = { 0xb91d5831, 0xb1bd, 0x4608, { 0x81, 0x98, 0xd7, 0x2e, 0x15, 0x50, 0x20, 0xf7 } };
IUpdateSessionOrchestratorPtr updateSessionOrchestrator;
hResult = CoCreateInstance(CLSID_UpdateSessionOrchestrator, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&updateSessionOrchestrator));
// Invoke LogTaskRunning()
updateSessionOrchestrator->LogTaskRunning(L"StartScan");
// Create an update session
IUsoSessionCommonPtr usoSessionCommon;
GUID IID_IUsoSessionCommon = { 0xfccc288d, 0xb47e, 0x41fa, { 0x97, 0x0c, 0x93, 0x5e, 0xc9, 0x52, 0xf4, 0xa4 } };
updateSessionOrchestrator->CreateUpdateSession(1, &IID_IUsoSessionCommon, &usoSessionCommon);
// Set the authentication information
CoSetProxyBlanket(usoSessionCommon, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, NULL);
// Trigger the "StartScan" action
usoSessionCommon->Proc21(0, 0, L"ScanTriggerUsoClient")
// Close the COM library
CoUninitialize();
结论
知道了USO客户端的工作原理以及它如何触发特权操作,现在就可以将这种行为复制为独立的应用程序。UsoDllLoader。当然,从这个逆向工程过程过渡到实际的C++代码需要更多的工作。
关于逆向工程部分,我不得不说这并不难,因为COM客户端已经存在,而且Windows默认提供。OleViewDotNet
最后也确实帮了大忙。它能够生成第二个接口(UsoSessionCommon)的代码—你知道的,有68个函数的那个接口。
好了,这篇文章就到此为止。希望大家喜欢。
参考文献
- 微软文档 – CoCreateInstance。
https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance - 微软文件 — — 对象间通信
https://docs.microsoft.com/en-us/windows/win32/com/inter-object-communication - Microosft文件 — — x64调用惯例
https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019 - Windows开发技巧。利用任意文件写入来提升本地的权限。
https://googleprojectzero.blogspot.com/2018/04/windows-exploitation-tricks-exploiting.html
发表评论
您还未登录,请先登录。
登录