利用USO服务实现特权文件写入——下篇

阅读量    189800 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

在上一篇文章中,我展示了如何使用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,这个 ChannelCOM 的一部分。经过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调用上设置了一个断点。

以下是调用前程序的执行过程:

  1. RCX'寄存器保存着接口指针的位置(即pInterface`)。
  2. RCX所指向的值被写入RAX,即RAX=pInterface
  3. 储存在 “RSI “中的值被复制到 “RDX “中。
  4. 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

前三个函数是QueryInterfaceAddRefRelease。这三个函数是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个函数的那个接口。

好了,这篇文章就到此为止。希望大家喜欢。

 

参考文献

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多