【COM】通过COM组件IFileOperation越权复制文件

概述:本文是基于伪装 PEB 进行越权复制文件相关学习研究进行的拓展文章,主要记录通过 COM 组件越权复制文件。

[toc]

本篇博客参考的文章较多,然而这些文章也只是其中的一个点或者两个点进行描述,并不详细。这里就不一一罗列了,本文较多内容都是笔者结合相关文章贴上的,相信你看此一篇就可!

主要内容:描述和记录一下伪装 PEB 进行越权复制时,Windows 是如何处理这一系列过程的。

伪装 PEB 进行越权复制

不妨先从最基本的说起,伪装 PEB 并不是将整个 PEB 内容都改写,而是对其中的几处内容进行修改以达到越权目的。

伪装 PEB

主要操作就是修改当前进程的 ImagePathName CommandLineFullDllNameBaseDllName 这几个变量,通过修改成为与系统可信的进程一样的内容进而实现越权操作。3gstudent 提供了 Demo,可以去 Github 看下相关逻辑,这里不做过多解释。

  • _RTL_USER_PROCESS_PARAMETERS.ImagePathName
  • _RTL_USER_PROCESS_PARAMETERS.CommandLine(可选)
  • _LDR_DATA_TABLE_ENTRY.FullDllName
  • _LDR_DATA_TABLE_ENTRY.BaseDllName

源代码路径:https://github.com/3gstudent/Use-COM-objects-to-bypass-UAC/blob/master/MasqueradePEB.cpp

原理:COM 组件通过 Process Status API (PSAPI) 读取进程 PEB 结构中的 Commandline 来识别它们正在运行的进程。如果将 PEB 中的信息修改为可信文件(如 explorer.exe),就能够欺骗 PSAPI,例如调用 COM 组件 IFileOperation 实现越权复制这一操作。

PSAPI:进程状态 API,主要提供用于检索一下信息的函数集:

调试说明

调试环境说明

本文基于上述代码编译并调试,后文所述伪装进程即为当前源码编译产物。

  • **调试环境信息:**Win10
  • **调试程序信息:**32位
  • **相关工具:**RpcView、IDA、Windbg

为了便于调试,可以先看下本文提及的 @脚本文件

查看对应的服务

以下是在双机调试过程可能有帮助于调试的 Windbg 命令,后文中内核调试查找相关 RPC 服务时会用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看 AppInfo 对应的服务
!process /m appinfo.dll 0 0 svchost.exe

# 查看 rpcss 对应的服务
!process /m RpcEpMap.dll 0 0 svchost.exe

# 查看 RPC 被调用的函数
bu RPCRT4!Invoke+0x73-2d ".echo '-->RPCRT4:';u r10 l5;gc"

# 设置内核断点
sxr;!gflag -ksl
!gflag +ksl;sxe cpr consent.exe;sxe ld consent.exe

# 查看伪装进程
!process 0 0 MasqueradePEBtoCopyfile.exe

# 伪装进程的相关断点
bm combase!connect

调试思路

windbg 时间旅行

调试说明
  • 伪装进程:调试伪装进程的相关逻辑时,建议在目标机器上使用用户态的调试模式,建议使用 Time Travel 对伪装进程调试
  • 相关服务:调试完伪装进程的相关逻辑之后,后续 RPC 的处理,UAC 弹窗,Token 读写的操作建议双机调试。

确认调用流程

在不了解任何相关调用流程的情况下,对伪装进程如何发起UAC,需要在用户态对伪装进程进行调试并查看堆栈,得到初步的调用关系如下所示:

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
[0x0]   RPCRT4!LRPC_BASE_CCALL::DoAsyncSend+0x21c   0xbddbc4   0x76538061   
[0x1] RPCRT4!LRPC_CCALL::AsyncSend+0x61 0xbddc48 0x7653796e
[0x2] RPCRT4!I_RpcSend+0x7e 0xbddc64 0x75dda34a
[0x3] combase!CAsyncCall::RpcSendRequest+0xaf 0xbddc8c 0x75e59faa
[0x4] combase!ThreadSendReceive+0xa4f 0xbdddd0 0x75dd77f8
[0x5] combase!CSyncClientCall::SwitchAptAndDispatchCall+0xadb 0xbdddd0 0x75dd77f8
[0x6] combase!CSyncClientCall::SendReceive2+0xbca 0xbdddd0 0x75dd77f8
[0x7] combase!SyncClientCallRetryContext::SendReceiveWithRetry+0x29 0xbddfa8 0x75e5c0a7
[0x8] combase!CSyncClientCall::SendReceiveInRetryContext+0x29 0xbddfa8 0x75e5c0a7
[0x9] combase!ClassicSTAThreadSendReceive+0x98 0xbddfa8 0x75e5c0a7
[0xa] combase!CSyncClientCall::SendReceive+0x2a7 0xbde06c 0x75de0468
[0xb] combase!CClientChannel::SendReceive+0x79 0xbde258 0x76516b23
[0xc] combase!NdrExtpProxySendReceive+0xc8 0xbde258 0x76516b23
[0xd] RPCRT4!NdrClientCall2+0x9e3 0xbde280 0x75eaeaa0
[0xe] combase!ObjectStublessClient+0x70 0xbde6d0 0x75ea6a3f
[0xf] combase!ObjectStubless+0xf 0xbde6f0 0x75e113f5
[0x10] combase!CRpcResolver::DelegateActivationToSCM+0x30e 0xbde700 0x75e8c69c
[0x11] combase!CRpcResolver::CreateInstance+0x14 0xbde80c 0x75e12d54
[0x12] combase!CClientContextActivator::CreateInstance+0x144 0xbde828 0x75e124d4
[0x13] combase!ActivationPropertiesIn::DelegateCreateInstance+0xc4 0xbdea88 0x75e3a762
[0x14] combase!ICoCreateInstanceEx+0xc12 0xbdead4 0x75e399d1
[0x15] combase!CComActivator::DoCreateInstance+0x231 0xbdedd8 0x75f4bec1
[0x16] combase!CComActivator::StandardCreateInstance+0x81 0xbdeecc 0x75ba8686
[0x17] ole32!CLUAMoniker::CreateInstance+0x126 0xbdf73c 0x63e8f20f
[0x18] comsvcs!CNewMoniker::BindToObject+0x12f 0xbdf77c 0x75b869cd
[0x19] ole32!CCompositeMoniker::BindToObject+0x19d 0xbdf7f8 0x75b84f9e
[0x1a] ole32!CoGetObject+0xbe 0xbdf82c 0x74e70e88
[0x1b] windows_storage!CoCreateInstanceAsAdmin+0xb2 0xbdf878 0x74e7e364
[0x1c] windows_storage!CFileOperation::_CreateElevatedCopyengine+0x43 0xbdfb80 0x74e8293a
[0x1d] windows_storage!CFileOperation::_RunElevatedOperation+0x4d 0xbdfbf4 0x74ce1761
[0x1e] windows_storage!CFileOperation::_ProcessLUAOperations+0x118056 0xbdfc28 0x74bc878a
[0x1f] windows_storage!CFileOperation::PrepareAndDoOperations+0x238 0xbdfc7c 0x74bc2274
[0x20] windows_storage!CFileOperation::PerformOperations+0xd4 0xbdfcec 0xa03f55
[0x21] MasqueradePEBtoCopyfile!wmain+0x365 0xbdfd1c 0xa047fe

观察堆栈,了解大致的调用之后(需要对 RPC 的调用有相关了解),可以在 NdrClientCall2() 处添加断点,然后调用 dbgtools.js!NdrClientCall2 方法来解析当前 RPC 是要调用哪个 RPC 的哪个接口。

1
2
// 需要提前加载调试脚本
bu RPCRT4!NdrClientCall2 "!NdrClientCall2"

详细调试步骤

第一次 UAC

这一部分内容为纯调试过程。基于我之前的学习过程,对一部分过程已经有了了解的基础上。与本节之后的内容有重合部分。

查看伪装发起的调用

客户端(也就是伪装进程)获取 发起 UAC 鉴权的进程 的相关信息(包括进程的 ImagePath、CommandLine),然后调用 RPC 去执行下一步操作

  1. windbg 运行目标程序,添加断点,因为已经多次调试这个程序了,这里就简要记录下

    经验之谈

    随着调试技术的熟练。我们只需要观察 NdrClientCall2() 的调用,再通过堆栈就能找到如下所示的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 打开日志文件
.logopen C:\windbg.log

# 前文 NdrClientCall2 断点触发后输出如下所示:
dbgtools> Run !dbgtools show help
0:000> !NdrClientCall2
Fri Nov 1 11:54:56.201 2024 (UTC + 8:00) NdrClientCall2() Stack
[RPC] Request Function: combase!Connect + 0xa8
[RPC] Caller Info:
Name:MasqueradePEBtoCopyfile.exe
PID:1828
Command:C:\windows\explorer.exe
[RPC] GUID
[RPC] GUID&Procederes: {e60c73e6-88f9-11cf-9af1-0020af6e72f4}:0
@$NdrClientCall2()

误入歧途 2. ❌ 这里开始找到的 COM 对象都不对,直接跳转到第 7 步。 创建完 COM 对象之后会首次触发断点,查看 CLSID,值为 `{9ac9fbe1-e0a2-4ad6-b4ee-e212013ea917}`,对应的 com 对象如下所示: 3. 继续执行会触发第二次 CocreateInstanceEx 断点,查看 CLSID,值为 `{f0ae1542-f497-484b-a175-a20db09144ba}`, 对应的对象如下所示: 4. 继续执行触发第四次断点,查看 CLSID, 值为 `{cdc82860-468d-4d4e-b7e7-c298ff23ab2c}` 5. 到这里基本就是 windows. storage.dll 的互调了,添加 rpc 断点
1
bp RPCRT4!NdrClientCall2
rpc 断点触发后查看 CoCreateInstanceEx 创建的 COM 对象是哪个 示例如下: 堆栈如上,查看创建的 COM 对象是哪个:
1
2
3
4
5
6
7
8
9
10
11
12
dds 023dfaf4

#输出如下所示:
0:003> dds 023dfaf4
023dfaf4 023dfb30
023dfaf8 746b3c80 windows_storage!CSearchIndexNotificationQueue::s_FlushNotificationQueueThreadProc+0x50
023dfafc 74629444 windows_storage!_GUID_9e175b6d_f52a_11d8_b9a5_505054503030
023dfb00 00000000
023dfb04 00000017
023dfb08 74629454 windows_storage!_GUID_a5eba07a_dae8_4d15_b12f_728efd8a9866
023dfb0c 023dfb18
023dfb10 02291780

查看 7462944474629454 对应的 COM 对象:

  • {9E175B6D-F52A-11D8-B9A5-505054503030}
  • {A5EBA07A-DAE8-4D15-B12F-728EFD8A9866}
  1. 到这里就看到请求的服务是 SearchIndexer 了,调试下 SearchIndexer
    1
    2
    3
    4
      # 打开日志
    .logopen /t c:\logs\mylogfile.txt

    bu RPCRT4!Invoke+0x73-2d "u r10 l5;gc"

7. 中间步骤的调试结果就是找错方向了,浪费了太多精力。伪装进程并不是通过创建 COM 对象来传递数据的。而是通过如下所示的堆栈,准确地说,是通过 combase!connect(Win11) 这个接口,在 Win11 操作系统上已经将这个接口改动到 Lambda 表达式中。结合我在一些错误尝试之后写的脚本,重新梳理下。

借助 RpcView 分析接口

上述第一步已经看到了 RPC 调用的 GUID接口ID{e60c73e6-88f9-11cf-9af1-0020af6e72f4}:0,直接在 RpcView查看调用的是谁:

查看接口

通过 RpcView 可以看到调用了 rpcss.dll 中偏移为 F510 处的函数 Connect

查找伪装进程调用接口

connect 函数说明

RPC 调用时,客户端最后调用哪个,服务端最先就会调用哪个。

RPC 调用时,客户端发起接口和服务端响应接口基本上是同名的。所以在客户端查找对应的接口有哪些就行

这里就不不用两个都看了,combase!Connect 无疑,在 IDA 中查看 combase!Connect 。这里由于 combase!Connect 的调用方式是 inline caller 并且是 lambda 表达式,所以直接通过 IDA 查找函数名是没有结果的,需要在 combase!Connect 下断点,并在触发时查看堆栈。

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
# 添加断点
bm combase!connect

# 断点触发时查看堆栈
0:000> kcn
#
00 combase!Connect
01 combase!<lambda_7438a189e512c84b56128b16ff6946f3>::operator()
02 combase!CRpcResolver::GetConnection
03 combase!CoInitializeSecurity
04 combase!InitializeSecurity
05 combase!CComApartment::InitRemoting
06 combase!CComApartment::StartServer
07 combase!InitChannelIfNecessary
08 combase!CGIPTable::RegisterInterfaceInGlobalHlp
09 combase!CGIPTable::RegisterInterfaceInGlobal
0a windows_storage!CFreeThreadedItemContainer::Initialize
0b windows_storage!CFSPropertyStoreFactory_CreateInstance
0c windows_storage!CFSFolder::_BindToChild
0d windows_storage!CFSFolder::_Bind
0e windows_storage!CFSFolder::BindToObject
0f windows_storage!CShellItem::BindToHandler
10 windows_storage!CShellItem::_GetPropertyStoreWorker
11 windows_storage!CShellItem::GetPropertyStoreForKeys
12 windows_storage!CShellItem::GetString
13 windows_storage!IShellItem_GetFileName
14 windows_storage!CPendingOperation::AddToTree
15 windows_storage!CCopyTree::InitializeFromPendingList
16 windows_storage!CFileOperation::PerformOperations

可以看到是通过 CRpcResolver::GetConnection 调用到 Connect 接口。

如下所示这张 IDA 的截图,正是 RPC 发起的起点,在 Lambda 函数接口内,通过 _NdrClientCall4 的调用发起 RPC 请求,并且传入了大量的参数,参数 szExePath 即通过 GetModuleFileNameW 获取的伪装进程的伪装路径。

至此,客户端发起 RPC 调用的逻辑理顺了。

lambda 函数说明

> 这里虽然调用的是 combase 中的 lambda 接口,最终还是会通过 inline call 的方式调用到 combase!Connect ,最终由 RPCSS 服务中的rpcss!_Connect 接口完成本次 RPC 调用。

> 另外就是在 IDA 并不能直接看到 Connect 函数(Lambda表达式)。需要触发断点后查看调用堆栈再去 IDA 查找 Connect 函数。

> 后文有相关堆栈,这里先补充下是从哪个函数调用过去的:combase!CRpcResolver::GetConnection

关于如何确定是通过 combase!connect 接口

可以看下文章 【调试技术】RPC ,这篇文章专门以本实例做演示,介绍了相关原理,并说明了如何查找 RPC 服务端及调用接口。

到这里就可以结合 IDA,就可以看到 connect 函数传递了哪些内容了。

combase!connect 函数入参相当多了。这里主要关注进程伪装的参数是如何传递的。

小节总结

这一小节伪装进程发起的调用到了由 rpcss.dll 持有的 rpc 接口,接口名为 Connect

查看 rpcss 的处理

在 RpcView 的截图中可以看到,rpcss.dll 是在 svchost 进程中,svchost 是系统服务进程,那么调试 rpcss 则最好是在内核调试状态下。

调试准备
1
2
3
4
# rpcss 相关断点
bu rpcss!_Connect ".echo 'rpcss!_Connect Para3:';du @r8;"
bp ole32!CoAicGetTokenForCOM
bp ole32!CoAicGetTokenForCOM+0x69

查看 rpcss!connect 接口

  1. 在上一小节部分,我们已经看到了伪装进程通过 rpc 最终调用到了 rpcss 服务中的 _Connect 接口。那在 rpcss!_Connect 接口中,通过 IDA 的反汇编可以看到,又通过 CProcess::CProcess 来创建了一个进程(猜测)。从 rpcss 到 appinfo 的过程比较复杂,这里还是观察 NdrClientCall2 接口吧。

  2. CProcess 构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    CProcess *__fastcall CProcess::CProcess(
    CProcess *this,
    struct CToken **a2,
    const unsigned __int16 *a3,
    const unsigned __int16 *lpImagePathName,
    __int64 a5,
    unsigned int a6,
    struct __MIDL_ILocalObjectExporter_0001 *a7,
    RPC_BINDING_HANDLE Binding,
    int *a9)
开始调试

便于观察,可以在 CProcess::CProcess 接口处下断点。如果基于要学习的目的,可以在 rpcss::_Connect 接口处下断点,当 CProcess::CProcess 断点触发时,可以观察下相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 堆栈
0: kd> kcn 4
# Call Site
00 rpcss!CProcess::CProcess
01 rpcss!_Connect+0x4e7
02 RPCRT4!Invoke+0x73
03 RPCRT4!NdrStubCall2Heap+0x342

# 查看参数3和参数4
0: kd> dU @r8
00000187`d8f4b3e8 "WinSta0\Default"
0: kd> dU @r9
00000187`d8f4b418 "C:\Windows\system32\consent.exe"
  1. rpcss 最终调用 CoAicGetTokenForCOMAppInfo 发起调用,堆栈如下所示
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
1: kd> kcn
# Call Site
00 ole32!CoAicGetTokenForCOM
ole32!CoAicGetTokenForCOM+0x69 添加权限查看 Token 更明显,地址没变,但是地址内句柄指向的 Token 权限增加了
01 rpcss!ActivateFromPropertiesPreamble+0x1f7b
01 retAddr rpcss!ActivateFromPropertiesPreamble+0x1f82 # 可以在此处添加断点观察前后的参数4,查看Token权限变化
02 rpcss!PerformScmStage
03 rpcss!SCMActivatorCreateInstance
04 RPCRT4!Invoke
05 RPCRT4!NdrStubCall2Heap
06 RPCRT4!NdrStubCall2
07 RPCRT4!NdrServerCall2
08 RPCRT4!DispatchToStubInCNoAvrf
09 RPCRT4!RPC_INTERFACE::DispatchToStubWorker
0a RPCRT4!RPC_INTERFACE::DispatchToStub
0b RPCRT4!LRPC_SCALL::DispatchRequest
0c RPCRT4!LRPC_SCALL::HandleRequest
0d RPCRT4!LRPC_ADDRESS::HandleRequest
RPCRT4!LRPC_ADDRESS::HandleRequest+0x341 "dt _PORT_MESSAGE @rbp" # 在此处添加断点可以查看发起请求的客户端
0e RPCRT4!LRPC_ADDRESS::ProcessIO
0f RPCRT4!LrpcIoComplete
10 ntdll!TppAlpcpExecuteCallback
11 ntdll!TppWorkerThread
12 KERNEL32!BaseThreadInitThunk
13 ntdll!RtlUserThreadStart

观察上述堆栈可以看到在 rpcss 中,最先调用的时 rpcss!SCMActivatorCreateInstance,也就是客户端最开始发起的接口是这个。在 RPCRT4!LRPC_ADDRESS::HandleRequest+0x341 查看发起调用的客户端。

CoAicGetTokenForCom 函数开始,观察 NdrClientCall2 发送的请求,可以看到 RPC 请求到了 appinfo.dll 的接口。

查看 appinfo 的处理

调式准备
1
2
3
4
5
6
7
8
9
# appinfo 断点
bp appinfo!RAiGetTokenForCOM
bp appinfo!AiBuildCOMParams
bp appinfo!AipGetTokenForService
bp appinfo!AiGetClientInformation
bp appinfo!AiCheckLUA
bp appinfo!AiLaunchConsentUI
bp appinfo!AiLaunchProcess
bp appinfo!AiCheckSecureApplicationDirectory
开始调试

查找调用线程

根据上一小节的内容,此时已经执行到了 Appinfo 所在的进程。查看 appinfo 所在进程的所有线程,查找相关线程:

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
# 这里会输出所有线程的堆栈信息 ffffa18d9265a2c0
!process ffffa18d9265a2c0 7

# 找相关的那个就行,如下所示,搜索 consent
THREAD ffffa18d92e95080 Cid 02a0.0aac Teb: 0000008049ae2000 Win32Thread: ffffa18d92eb12d0 WAIT: (UserRequest) UserMode Non-Alertable
ffffa18d94034080 ProcessObject
Not impersonating
DeviceMap ffffdf048f637420
Owning Process ffffa18d9265a2c0 Image: svchost.exe
Attached Process N/A Image: N/A
Wait Start TickCount 28038 Ticks: 25 (0:00:00:00.390)
Context Switch Count 3626 IdealProcessor: 1
UserTime 00:00:00.015
KernelTime 00:00:00.375
Win32 Start Address ntdll!TppWorkerThread (0x00007ffdc99a2b30)
Stack Init ffff9c8af4a45c90 Current ffff9c8af4a456a0
Base ffff9c8af4a46000 Limit ffff9c8af4a40000 Call 0000000000000000
Priority 9 BasePriority 8 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffff9c8a`f4a456e0 fffff801`7f455330 : ffffa18d`00000008 00000000`ffffffff ffff9c8a`00000000 ffffa18d`95631158 : nt!KiSwapContext+0x76
ffff9c8a`f4a45820 fffff801`7f45485f : 00000000`00000000 00000000`00000000 ffff9c8a`f4a459e0 00000000`00000000 : nt!KiSwapThread+0x500
ffff9c8a`f4a458d0 fffff801`7f454103 : 00000000`00000000 fffff801`00000000 00000000`00000000 ffffa18d`92e951c0 : nt!KiCommitThreadWait+0x14f
ffff9c8a`f4a45970 fffff801`7f858dd1 : ffffa18d`94034080 fffff801`00000006 ffff9c8a`f4a45b01 ffff9c8a`f4a45b00 : nt!KeWaitForSingleObject+0x233
ffff9c8a`f4a45a60 fffff801`7f858d2a : ffffa18d`92e95080 00000000`00000000 00000000`00000000 00000000`00000000 : nt!ObWaitForSingleObject+0x91
ffff9c8a`f4a45ac0 fffff801`7f624ef5 : ffffa18d`92e90000 00000000`00001000 00000000`00000000 00000000`00000000 : nt!NtWaitForSingleObject+0x6a
ffff9c8a`f4a45b00 00007ffd`c99ed064 : 00007ffd`c72730ce 00000000`00000022 00000023`00000004 00000004`00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffff9c8a`f4a45b00)
00000080`4be7e5b8 00007ffd`c72730ce : 00000000`00000022 00000023`00000004 00000004`00000000 00000000`00000024 : ntdll!NtWaitForSingleObject+0x14
00000080`4be7e5c0 00007ffd`99a178d9 : 00000000`00000000 00000000`00000001 00000080`00000000 00000000`00002508 : KERNELBASE!WaitForSingleObjectEx+0x8e
00000080`4be7e660 00007ffd`99a171e3 : 00000000`00000000 0000024c`d154cfa0 00000000`00000000 00000000`00000004 : appinfo!AiLaunchConsentUI+0x559
00000080`4be7e880 00007ffd`99a32ff1 : 00000000`00000002 00000000`00002568 00000000`00004000 00000000`00000002 : appinfo!AiCheckLUA+0x343
00000080`4be7ea60 00007ffd`99a339b6 : 0000024c`d428c760 0000024c`d428c810 0000024c`d428c810 0000024c`d0f7ac08 : appinfo!AipGetTokenForService+0x245
00000080`4be7eb80 00007ffd`c7f4b4b3 : 0000024c`d3236b80 0000024c`d2f71de0 00000000`00000000 00000000`00000002 : appinfo!RAiGetTokenForCOM+0x206
00000080`4be7ec40 00007ffd`c7fac5ea : 0000024c`d3236b80 0000024c`d00f9320 0000024c`d0f7a8e0 00007ffd`99a35e30 : RPCRT4!Invoke+0x73

从上述堆栈可以看到最先开始调用的接口为appinfo!RAiGetTokenForCOM,并且是通过 RPC 调用进来的。 最后调用的函数为 appinfo!AiLaunchConsentUI,appinfo 最后的处理则是调用通过 AiLaunchProcess 如下所示:

启动 Consent进程

Buffer 内容如下所示

  • 参数1:创建 consent 进程的PID
  • 参数2:传入参数的大小和当前进程的句柄
  • 参数3:传入参数的地址,对应结构体为 _CONSENTUI_PARAM_HEADER,包含了 com地址、PE文件路径、GUID、Token等信息

下一步就是调试 consent.exe 了。

AppInfo 封装参数

appinfo 的另外一个作用就是将请求客户端的信息进行封装(封装到 _CONSENTUI_PARAM_HEADER)并传递到 consent.exe。其中后文提到的 Token 也是通过 appinfo!AiGetClientInformation 获取的。

如下所示为封装 token 等一部分参数:

在封装的参数中,同时包含了一个传入的 Token,位于 _CONSENTUI_PARAM_HEADER+0n24 处,该 Token 是调用 AiGetClientInformation 获取的,下图 LINE81,这个Token 也是后文关键,伪装进程传出的 Token 是基于这个传入 Token 复制:

获取 RPC 客户端信息

AiGetClientInformation 获取发起方 Token,在这个接口中可以看到 RPC 客户端是哪个进程:

I_RpcBindInqLocalClientPID 调用后查看获取到的 PID

可以看到是由 dllhost.exe 发起:

提示

所以这里就可以确定是由 DllHost 调用 RAiGetTokenForCOM 发起 RPC 请求了。

在获取客户端 PID 后,调用 NtOpenProcessRpcImpersonateClientNtOpenThreadTokenNtDuplicateToken(TokenPrimary) 获取到了传入 Token。

补充1

如果要观察传入 consent 的参数前后有什么变化,就可以在如下所示堆栈位置 KERNELBASE!WaitForSingleObjectEx+0x8e 的位置添加断点查看调用 consent 之后传入参数的位置发生了什么变化。

这里可以在 ole32!GetTokenForCom函数尾(ole32!CoAicGetTokenForCOM+0x69)添加断点查看 phNewToken 前后的变换,补充我这边的记录如下所示:

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
0: kd> kvn
# Child-SP RetAddr : Args to Child : Call Site
00 0000002e`5b07e5f8 00007ff9`1d7610e2 : 000001ff`7f779901 0000002e`5b07e700 000001ff`7fa7bd08 00000000`00000000 : ole32!CoAicGetTokenForCOM [com\ole32\dll\dll.cpp @ 433]
01 0000002e`5b07e600 00007ff9`1d75a209 : 0000002e`5b07ea20 000001ff`000000bc 0000002e`5b07ea20 00000000`00000000 : rpcss!ActivateFromPropertiesPreamble+0x1f82
02 0000002e`5b07e920 00007ff9`1d759046 : 000001ff`7facd5b0 00000000`000000a0 00000000`00000000 00007ff9`1d77f51e : rpcss!PerformScmStage+0xb79
03 0000002e`5b07eae0 00007ff9`21f5b4b3 : 000001ff`7facd5b0 000001ff`7f1f6bf0 000001ff`7fa9a720 000001ff`7fba6970 : rpcss!SCMActivatorCreateInstance+0x1b6
04 0000002e`5b07ee10 00007ff9`21f5a282 : 00007ff9`1d84dd22 000001ff`7fba6790 00000000`00000000 00007ff9`1d84dcee : RPCRT4!Invoke+0x73
05 0000002e`5b07ee90 00007ff9`21efe1ca : 00000000`00000000 00000000`00000000 000001ff`7facd700 0000002e`5b07f188 : RPCRT4!NdrStubCall2Heap+0x342
06 0000002e`5b07f120 00007ff9`21f3dfda : 00000000`00000000 000001ff`7fa119b0 000001ff`7facd700 00007ff9`223305dd : RPCRT4!NdrStubCall2+0x3a
07 0000002e`5b07f150 00007ff9`21f39188 : 000001ff`7ec25c84 000001ff`00000001 000001ff`7facd5b0 00000000`00000000 : RPCRT4!NdrServerCall2+0x1a
08 0000002e`5b07f180 00007ff9`21f1a3a6 : 000001ff`00501100 000001ff`7ec53860 0000002e`5b07f380 00007ff9`2233ae20 : RPCRT4!DispatchToStubInCNoAvrf+0x18
09 0000002e`5b07f1d0 00007ff9`21f19cf8 : 000001ff`7ec53860 00000000`00000000 00000000`00000000 00007ff9`2233cabb : RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1a6
0a 0000002e`5b07f2b0 00007ff9`21f274bf : 00000000`00000000 000001ff`0007f1cd 000001ff`7facd5b0 00007ff9`2236bc81 : RPCRT4!RPC_INTERFACE::DispatchToStub+0xf8
0b 0000002e`5b07f320 00007ff9`21f268c8 : 00000000`0008581d 00000000`00000005 00000000`00000000 000001ff`7fa119b0 : RPCRT4!LRPC_SCALL::DispatchRequest+0x31f
0c 0000002e`5b07f3f0 00007ff9`21f25eb1 : 00000000`0000104c 000001ff`7fad0b70 00000000`00000000 000001ff`00000000 : RPCRT4!LRPC_SCALL::HandleRequest+0x7f8
0d 0000002e`5b07f500 00007ff9`21f2591e : 00000000`00000000 00000000`00000000 00000000`00000001 000001ff`7ec20b60 : RPCRT4!LRPC_ADDRESS::HandleRequest+0x341
0e 0000002e`5b07f5a0 00007ff9`21f2a032 : 000001ff`7fa119b0 000001ff`7fa119b0 000001ff`7ec20c68 0000002e`5b07f978 : RPCRT4!LRPC_ADDRESS::ProcessIO+0x89e
0f 0000002e`5b07f6e0 00007ff9`22330330 : 00000001`00000000 00000000`00000000 0000002e`5b07f978 00000000`00000184 : RPCRT4!LrpcIoComplete+0xc2
10 0000002e`5b07f780 00007ff9`22362f86 : 00000000`00000000 000001ff`7ec20d00 00000000`00000000 000001ff`7f196ae0 : ntdll!TppAlpcpExecuteCallback+0x260
11 0000002e`5b07f800 00007ff9`21597344 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!TppWorkerThread+0x456
12 0000002e`5b07fb00 00007ff9`223626b1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
13 0000002e`5b07fb30 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
0: kd> dv /t /v
@rcx struct HWND__ * hwndParent = 0x00000000`00000000
@rdx wchar_t * lpDesktop = 0x000001ff`7fbac4c0 "WinSta0\Default"
@r8 struct _GUID * lpGuid = 0x0000002e`5b07eb6c {3AD05575-8857-4850-9277-11B85BDB8E09}
@r9 wchar_t * lpFriendlyName = 0x000001ff`7faa3120 "文件操作"
0000002e`5b07e620 wchar_t * lpServerBinary = 0x000001ff`7f798250 "C:\Windows\system32\windows.storage.dll"
0000002e`5b07e628 wchar_t * lpIconReference = 0x00007ff9`1d840a88 ""
0000002e`5b07e630 wchar_t * lpRequestorPath = 0x000001ff`7f7cbf10 "C:\windows\explorer.exe"
0000002e`5b07e638 unsigned long dwRunLevel = 2
0000002e`5b07e640 unsigned long dwClientFlags = 0x20
0000002e`5b07e648 int fAdjustTokenSD = 0n1
0000002e`5b07e650 void * hTokenIn = 0x00000000`000011cc
0000002e`5b07e658 void ** phNewToken = 0x0000002e`5b07e6b8
0: kd> bp 0x7ff9205b5199
0: kd> dq 0x0000002e`5b07e6b8
0000002e`5b07e6b8 00000000`00000000 00000000`00000007
0000002e`5b07e6c8 00007ff9`00000000 00000000`00000002
0000002e`5b07e6d8 000001ff`7ebd0000 00000000`00000000
0000002e`5b07e6e8 00007ff9`22317afb 000001ff`7f1f60b0
0000002e`5b07e6f8 000001ff`7fb8e730 0000002e`5b07e440
0000002e`5b07e708 00000000`00000000 000001ff`00001280
0000002e`5b07e718 00007ff9`2233c282 00000000`00000000
0000002e`5b07e728 00000000`00000f38 000001ff`7fb07800
0: kd> g
'rpcss!_Connect Para3:'
000001ff`7f1d8c1c "C:\Windows\system32\consent.exe"
Breakpoint 10 hit
ole32!CoAicGetTokenForCOM+0x69:
0033:00007ff9`205b5199 4883c468 add rsp,68h
1: kd> dq 0x0000002e`5b07e6b8
0000002e`5b07e6b8 00000000`00000e54 00000000`00000007
0000002e`5b07e6c8 00007ff9`00000000 00000000`00000002
0000002e`5b07e6d8 000001ff`7ebd0000 00000000`00000000
0000002e`5b07e6e8 00007ff9`22317afb 000001ff`7f1f60b0
0000002e`5b07e6f8 000001ff`7fb8e730 0000002e`5b07e440
0000002e`5b07e708 00000000`00000000 000001ff`00001280
0000002e`5b07e718 00007ff9`2233c282 00000000`00000000
0000002e`5b07e728 00000000`00000f38 000001ff`7fb07800
1: kd> !handle 00000000`00000e54

PROCESS ffff908328f1e340
SessionId: 0 Cid: 03b4 Peb: 2e5a6aa000 ParentCid: 02bc
DirBase: 13deb000 ObjectTable: ffffc18c8229b280 HandleCount: 1104.
Image: svchost.exe

Handle table at ffffc18c8229b280 with 1104 entries in use

0e54: Object: ffffc18c88330380 GrantedAccess: 000f01ff (Protected) (Audit) Entry: ffffc18c84dff950
Object: ffffc18c88330380 Type: (ffff9083244bfd20) Token
ObjectHeader: ffffc18c88330350 (new version)
HandleCount: 1 PointerCount: 1

调试准备

由于 consent 进程是 svchost 进程创建的,所以需要从内核下断点才可以,在内核从未加载过 consent 进程情况下,执行如下所示命令:

1
2
sxr;!gflag -ksl
!gflag +ksl;sxe cpr consent.exe;sxe ld consent.exe

如果断点没有生效,则需要重启操作系统,重新设置内核断点。

内核断点触发时输出如下所示,断点触发后添加 consent 断点:

1
2
3
4
5
6
# consent 进程相关断点
bp consent!winmain;
bp consent!CuiGetTokenForApp;
bp consent!CuipGetParameters;
bp consent!GetConsent;
bp consent!CuiPlayConsentLaunchedSound
开始调试

观察 consent.exe 的创建。继上一步骤之后,如果对 consent 的创建感兴趣可以按调试准备所述添加断点查看 consent.exe 的创建。

F5 继续执行,这时候断点会断在 nt!DebugService2,查看 PEB 确定是否为 consent.exe,在我的环境输出如下所示:

这里也可以观察下是哪个进程创建的 consent 进程,consent 进程的命令行的第一个参数 672 是父进程的PID

1
2
3
4
5
6
7
8
9
consent.exe 672 252 0000024CD154CFA0

# 这个进程就是 appinfo.dll 所在服务,与 !process /m appinfo.dll 0 0 svchost.exe 查找结果一致
1: kd> !process /m appinfo.dll 0 0 svchost.exe
Searching processes with loaded module 'appinfo.dll'
PROCESS ffffa18d9265a2c0
SessionId: 0 Cid: 02a0 Peb: 8049a87000 ParentCid: 02bc
DirBase: 2aa88000 ObjectTable: ffffdf04938e9180 HandleCount: 2313.
Image: svchost.exe
查看 consent 命令行参数

在调试 consent 之前先看下 appinfo 传入的参数(当前输出为另一次调试后获取的输出),728 对应的是 appinfo 所在的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0: kd> dx -r1 Debugger.State.Scripts.CodeFlow.Contents.host.currentProcess.Attributes
Debugger.State.Scripts.CodeFlow.Contents.host.currentProcess.Attributes
CommandLine : consent.exe 728 252 0000014ED7003290
CurrentDirectory : C:\Windows\system32\
DllPath
Environment
0: kd> ? 0n728
Evaluate expression: 728 = 00000000`000002d8
0: kd> !process 00000000`000002d8 0
Searching for Process with Cid == 2d8
PROCESS ffffca04136602c0
SessionId: 0 Cid: 02d8 Peb: 4db9278000 ParentCid: 02c4
DirBase: 13538000 ObjectTable: ffffe70e57e91e00 HandleCount: 2039.
Image: svchost.exe

开始调试 consent 进程(断点不生效情况下,UAC 弹窗还在时可以使用这种方式继续调试部分流程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看 consent eprocess
1: kd> !process 0 0 consent.exe
PROCESS ffffa18d94034080
SessionId: 1 Cid: 1374 Peb: 7e9b83b000 ParentCid: 02a0
DirBase: 79408000 ObjectTable: ffffdf0498039900 HandleCount: 138.
Image: consent.exe

# 加载 PDB
1: kd> .process /p ffffa18d94034080
Implicit process is now ffffa18d`94034080
.cache forcedecodeuser done
1: kd> .reload /f /user

# 侵入式调试
1: kd> .process /i /p ffffa18d94034080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.

这里仍需要我们使用 IDA 查看 consent 的 main 函数。在 main 函数中调用了 CuiGetTokenForApp 函数(不论是否是白名单进程都会执行这个接口):

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall CuiGetTokenForApp(
__int64 a1,
__int64 pConsentHeader,
const struct _CREDUI_CONTEXT *crdcnt,
__int64 LastError,
int nWPPSFFlag,
int a6,
DWORD a7,
PHANDLE outHandle,
void **outSmartCardHandle)

CuiGetTokenForApp 函数中进行了一系列的判断,然后调用了 CuiGetTokenForApp 后回到 WinMain 之后开始复制 Token。也正是这些判断才是未装机进程与普通进程的不同之处,主要在 _CONSENTUI_PARAM_HEADER 结构体偏移 48 和偏移 8 处。判断逻辑为以下两处,执行结束后返回到 WinMain

  • CuiGetTokenForApp 函数的 入参 _CONSENTUI_PARAM_HEADER 参数

  • 第一处判断

  • 第二处判断

    nSecFlag 在 Line66 和 Line72 的两处判断都很好判断,最后执行了 Line82 复制了 _CONSENTUI_PARAM_HEADER 偏移 24 的 Token 句柄,此时的句柄并没有什么权限,只是复制一个主句柄到传出参数 NewTokenHandle 中。

    可以看下输入句柄与输出句柄的对比,除了 Token ID 以外,其他内容是一样的。

CuiGetTokenForApp 返回后,执行情况如下所示,直接跳转到标记2:

WinMain 执行到 NtQueryInformationToken 接口,第一个 NtQueryInformationToken 查询结果为空,所以最终会调用第二个 NtQueryInformationToken。这一步简直大变活人。从一个没有管理员权限的句柄,Link 出来了一堆权限。

执行 TokenLinkedToken 前后的句柄变化如下所示,参考下图,左边是参数1,右边是参数3:

关于上述的 NtQueryInformationToken 调用 TokenLinkedToken 后 Token 权限更新,这里涉及多个知识点。

相关文章:
TokenLinkedToken

TokenLinkedToken 简单理解就是要获得与当前进程关联的 Token,或者将两个 Token 关联起来。

Opens a copy of a token from the linked logon session or links logon sessions of the two tokens.

关键点:

管理员进程下,可以使用 TokenLinkedToken 获得一个标准用户进程。实现所谓的降权。

除此之外,在普通权限情况下,当然也可以实现提权(准确说是将权限调整到管理员权限)的操作,但是该操作必须是当前进程具有 SeTcbPrivilege 权限,而 SeTcbPrivilege 权限是 SYSTEM 用户的权限。简单说,SYSTEM 用户的权限是高于管理员权限的。如果想要在系统服务中以管理员权限创建一个进程,那么这时候可以先获得一个普通用户的权限,再通过 TokenLinkedToken 就可以创建一个管理员权限。

补充:

结合上图可以看到在 TokenLinkedToken 之后,令牌 Level 由 Impersonation 变为了 Identification。当调用进程具有 SeTcbPrivilege 权限时,将返回一个主令牌,否则返回一个标识令牌。

提权操作结束后,调用 NtWriteVirtualMemory 将 Token 写回给 appinfo 进程传入的结构体中

流程总结

大致流程如下所示:

   flowchart TD
   	A[伪装进程] --> |RPC调用|A1
   	A1[rpcss!connect] --> |RPC 调用|B
   	B[appinfo!RAiGetTokenForCOM]-->|.....|C
   	C[appinfo!AiLaunchConsentUI]-->D
   	D[consent!CuiCheckElevationAutoApprovalMedium] --> D1
   	D1[consent!CuiGetTokenForApp]--> |传入 _CONSENTUI_PARAM_HEADER 参数|E
   	E[consent!NtQueryInformationToken] --> |TokenLinedToken| F
   	F[consent!NtWriteVirtualMemory] --> |写回到 _CONSENTUI_PARAM_HEADER+0x38| G
   	G[appinfo!NtDuplicateObject]--> |AipGetTokenForService+334|H[...]

到这里可以就整个流程完事了。可以看到决定直接写回 Token 操作的主要还是取决于 _CONSENTUI_PARAM_HEADER 的几个成员:

  • offset+0x8 条件1:value==1value == 1
  • offset+0x30 条件2:value&0x2000==0value \& 0x2000 == 0
条件1

条件1满足的其中一个条件是条件2满足,另外一个则需要进入到 consent 进程后再看了,因为在 consent!CuipGetParameters 解析参数后,+8 偏移处的值仍为2。

只有这一处给 offset+0x8 赋值为 1,而 nAutoApprove 则是通过 CuiCheckElevationAutoApprovalMedium 获取的。

调用 CuiCheckElevationAutoApprovalMedium 函数以及返回值赋值:

CuiCheckElevationAutoApprovalMedium 函数执行逻辑:

  • 标记1:*((_DWORD *)consentHeader + 1) 执行偏移为 4 处,从 RAiGetTokenForCOM 请求来的值都为1

  • 标记2:调用的 COM 对象 GUID 为 3AD05575-8857-4850-9277-11B85BDB8E09,是否可自动提升查询注册表 SOFTWARE\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList\{GUID} 即可

条件2

条件2很好满足,只要是从 RAiGetTokenForCOM 接口请求过去的都满足,如下图所示:

总结

通过 RAiGetTokenForCOM 伪装成系统路径,获取

第二次 UAC

第二次 UAC 为常规操作,这里主要记录下发起方。

第二次 UAC 由执行文件操作的 COM 对象发起,也就是 windows.storage.dll

1
C:\Windows\SysWOW64\DllHost.exe /Processid:{3AD05575-8857-4850-9277-11B85BDB8E09}

而上述所示的 DLLHost 则由 DcomLaunch 进程创建。

1
C:\Windows\system32\svchost.exe -k DcomLaunch -p

在上述过程中应该是涉及了第二次 RPC 请求,伪装进程通过 RPC 请求到 DcomLaunch , DcomLaunch 进程在收到相关请求后创建了 DLLHost 进程。

伪装进程与第一次不同的地方在于调用 AiCheckSecureApplicationDirectory 时传递的参数不同,第二次 COM 对象传递的是伪装进程的真是路径,而第一次则是 PEB 中修改的路径。

COM 调用堆栈如下所示

相关分析文章

如下所示为启动一个 uac 进程到该进程发起 rpc 的调用堆栈,须知当前进程的提权是通过 RPC 调用到 Appinfo 服务的。流程如下:

  1. uac 通过 RPC 请求到 AppInfo 服务
  2. APPInfo 通过 AiLaunchProcess 接口创建 consent.exe 进程,调用时会传入一个结构体地址,调用命令行为 consent.exe ppid 结构体长度 结构体地址
  3. consent.exe 会根据用户操作将结果写入结构体中。

修改 PEB 伪装时堆栈如下所示:

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
[0x0]   RPCRT4!LRPC_BASE_CCALL::DoAsyncSend   0x291e17c   0x752cda21   
[0x1] RPCRT4!LRPC_CCALL::AsyncSend+0x61 0x291e180 0x752cd30e
[0x2] RPCRT4!I_RpcSend+0x7e 0x291e19c 0x74e87d8d
[0x3] combase!CAsyncCall::RpcSendRequest+0xcc 0x291e1cc 0x74e3d579
[0x4] combase!ThreadSendReceive+0xd9 0x291e30c 0x74dfc8f6
[0x5] combase!CSyncClientCall::SwitchAptAndDispatchCall+0x31c 0x291e30c 0x74dfc8f6
[0x6] combase!CSyncClientCall::SendReceive2+0x579 0x291e30c 0x74dfc8f6
[0x7] combase!SyncClientCallRetryContext::SendReceiveWithRetry+0x3c 0x291e4a0 0x74dfc885
[0x8] combase!CSyncClientCall::SendReceiveInRetryContext+0x2a 0x291e4c0 0x74dfc831
[0x9] combase!ClassicSTAThreadSendReceive+0x51 0x291e4e4 0x74e3abce
[0xa] combase!CSyncClientCall::SendReceive+0x44e 0x291e5a4 0x74de94f4
[0xb] combase!CClientChannel::SendReceive+0x75 0x291e710 0x752a69e9
[0xc] combase!NdrExtpProxySendReceive+0xc4 0x291e710 0x752a69e9
[0xd] RPCRT4!NdrClientCall2+0xb09 0x291e738 0x74ece96a
[0xe] combase!ObjectStublessClient+0xaa 0x291ebc8 0x74ec68bf
[0xf] combase!ObjectStubless+0xf 0x291ebe8 0x74e26efa
[0x10] combase!CRpcResolver::DelegateActivationToSCM+0x2eb 0x291ebf8 0x74e114e8
[0x11] combase!CRpcResolver::CreateInstance+0x14 0x291ed00 0x74e11484
[0x12] combase!CClientContextActivator::CreateInstance+0x144 0x291ed1c 0x74e112be
[0x13] combase!ActivationPropertiesIn::DelegateCreateInstance+0x7e 0x291ed68 0x74e4f518
[0x14] combase!ICoCreateInstanceEx+0x968 0x291edb4 0x74e4fdfa
[0x15] combase!CComActivator::DoCreateInstance+0x22a 0x291f740 0x74e50101
[0x16] combase!CoCreateInstanceEx+0x130 0x291f814 0x6acb5269
[0x17] combase!CoCreateInstance+0x191 0x291f814 0x6acb5269
[0x18] apphelp!InsHook_CoCreateInstance+0x39 0x291f870 0x7296f400
[0x19] windows_storage!CSearchIndexNotificationQueue::s_FlushNotificationQueueThreadProc+0x50 0x291f8c0 0x76baf8f9

CoGetObject 的创建管理员对象

image-20231205182252389

在创建 COM 对象时,会调用 dllHost 来创建另一个进程:

1
C:\WINDOWS\system32\DllHost.exe /Processid:{3AD05575-8857-4850-9277-11B85BDB8E09}

在注册表可以看到该 CLSID 的一些信息如下所示:

1
reg hkey_classes_root\CLSID\{3AD05575-8857-4850-9277-11B85BDB8E09}

到达 AppInfo 后, 调用堆栈如下所示:

1
2
3
4
5
6
7
8
9
10
[0x0]   ntdll!NtCreateUserProcess   0xdf231fc698   0x7ff9da72b473   
[0x1] KERNELBASE!CreateProcessInternalW+0xfe3 0xdf231fc6a0 0x7ff9da728a03
[0x2] KERNELBASE!CreateProcessAsUserW+0x63 0xdf231fdc70 0x7ff9daa4de30
[0x3] KERNEL32!CreateProcessAsUserWStub+0x60 0xdf231fdce0 0x7ff9d4a4526b
[0x4] appinfo!AiLaunchProcess+0x8eb 0xdf231fdd50 0x7ff9d4a4789d
[0x5] appinfo!AiLaunchConsentUI+0x51d 0xdf231fec40 0x7ff9d4a471e3
[0x6] appinfo!AiCheckLUA+0x343 0xdf231fee60 0x7ff9d4a62ff1
[0x7] appinfo!AipGetTokenForService+0x245 0xdf231ff040 0x7ff9d4a639b6
[0x8] appinfo!RAiGetTokenForCOM+0x206 0xdf231ff160 0x7ff9db4ab4b3
[0x9] RPCRT4!Invoke+0x73 0xdf231ff220 0x7ff9db50c5ea

调用 RAiLaunchAdminProcess 越权

基于以上描述,我们大概了解了启动一个 UAC 进程的流程和过程。那么,到底是在哪一步判断中进程的 UAC 鉴权操作。

处理逻辑:AIS(appinfo.dll) 服务:处理提升请求

主要看一下 通过 RAiLaunchAdminProcess 发起 UAC 请求的处理。以下 2 种情况可能不会弹 UAC 对话框,会自动提升至管理员权限(越权也是基于白名单):

UAC 流程

简单说明一下 UAC 的请求流程。

  1. 程序配置为自动提升

如果程序中配置了 autoElevate 为 true,会尝试自动提升

​ 1. 先判断是否限制自动提权策略

​ 2. 判断是否设置了 autoElevate

  1. 白名单

判断要执行的程序是否属于白名单,在白名单之内就调用 AipIsValidAutoApprovalEXE 函数检查 程序签名 等信息,如果不在就基本结束这个函数了

依靠上述两个判断还是不够的,在 appinfo 中还有如下所示的几个列表。

  1. g_lpExcludedWindowsDirs

    g_lpExcludedWindowsDirs
  2. g_lpIncludedWindowsDirs

    g_lpIncludedWindowsDirs
  3. g_lpIncludedSystemDirs

    g_lpIncludedSystemDirs
  4. g_lpIncludedPFDirs

    g_lpIncludedPFDirs

弹窗逻辑

但是中间弹窗的过程被省略了,这里可以调试分析一下。主要就是通过查看 COM 组件有没有自提升权限进而判断是否需要弹窗,查看 CuiIsCOMClassAutoApprovable 即可。

越权逻辑

越权主要还是基于以下两点:

  1. 各类 UAC 白名单程序的 DLL 劫持(Dll Hijack)

  2. 各类提升权限的 COM 接口利用(Elevated COM interface)

    可利用的 COM 接口有哪些,可以在 HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\UAC\\COMAutoApprovalList 注册表下查看值为 键值项为 1 的项为可以自提升的。

本文伪装 PEB 所示的代码通过白名单 g_lpIncludedSystemDirs 和 提升权限的 COM 接口实现。

检查的堆栈为:

appinfo! RAiGetTokenForCOM -> appinfo! AipGetTokenForService(或者 AipGetTokenForService) -> appinfo! AiCheckSecureApplicationDirectory -> appinfo! AipCheckSecureWindowsDirectory 或者 AipCheckSecurePFDirectory(取决于传递的路径)

修改为 C:\windows\explorer.exe 之后,其相当于从白名单启动的进程,因此可以绕过 consent,直接获取权限。

文件启动路径

获取程序运行路径的堆栈

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
[0x0]   combase!<lambda_7438a189e512c84b56128b16ff6946f3>::operator()+0x9d   0x71d5d8   0x75070f45   
[0x1] combase!CRpcResolver::GetConnection+0x70 0x71db48 0x75051915
[0x2] combase!CoInitializeSecurity+0xe5 0x71db78 0x750ed384
[0x3] combase!InitializeSecurity+0x4f 0x71ddec 0x7509196b
[0x4] combase!CComApartment::InitRemoting+0x8d 0x71de20 0x750b1ce2
[0x5] combase!CComApartment::StartServer+0x60 0x71de64 0x7506dd3d
[0x6] combase!InitChannelIfNecessary+0xb2 0x71de64 0x7506dd3d
[0x7] combase!CGIPTable::RegisterInterfaceInGlobalHlp+0x4d 0x71de80 0x7506dce5
[0x8] combase!CGIPTable::RegisterInterfaceInGlobal+0x15 0x71dec0 0x7453b91b
[0x9] windows_storage!CFreeThreadedItemContainer::Initialize+0x8b 0x71ded8 0x7453b1b0
[0xa] windows_storage!CFSPropertyStoreFactory_CreateInstance+0x360 0x71df0c 0x7451ab6b
[0xb] windows_storage!CFSFolder::_BindToChild+0x10e 0x71e15c 0x7454e000
[0xc] windows_storage!CFSFolder::_Bind+0xa10 0x71e928 0x7454d279
[0xd] windows_storage!CFSFolder::BindToObject+0x4c9 0x71ec3c 0x74562745
[0xe] windows_storage!CShellItem::BindToHandler+0x525 0x71ef1c 0x7455e92e
[0xf] windows_storage!CShellItem::_GetPropertyStoreWorker+0x2ee 0x71f1ec 0x7457313d
[0x10] windows_storage!CShellItem::GetPropertyStoreForKeys+0x11d 0x71f260 0x745a1977
[0x11] windows_storage!CShellItem::GetString+0x57 0x71f4e8 0x745a18e3
[0x12] windows_storage!IShellItem_GetFileName+0x5b 0x71f530 0x745a0934
[0x13] windows_storage!CPendingOperation::AddToTree+0x114 0x71f55c 0x745a07fb
[0x14] windows_storage!CCopyTree::InitializeFromPendingList+0x9a 0x71f7b0 0x7459224f
[0x15] windows_storage!CFileOperation::PerformOperations+0xaf 0x71f7d4 0x543f55
[0x16] MasqueradePEBtoCopyfile!wmain+0x365 0x71f818 0x5447fe
[0x17] MasqueradePEBtoCopyfile!__scrt_wide_environment_policy::initialize_environment+0x2e 0x71f97c 0x544667
[0x18] MasqueradePEBtoCopyfile!__crt_char_traits<wchar_t>::tcscpy_s<wchar_t * &,unsigned int,wchar_t const * const &>+0x1d7 0x71f990 0x5444fd
[0x19] MasqueradePEBtoCopyfile!__crt_char_traits<wchar_t>::tcscpy_s<wchar_t * &,unsigned int,wchar_t const * const &>+0x6d 0x71f9ec 0x544878
[0x1a] MasqueradePEBtoCopyfile!wmainCRTStartup+0x8 0x71f9f4 0x7612fcc9
[0x1b] KERNEL32!BaseThreadInitThunk+0x19 0x71f9fc 0x76fd7c6e
[0x1c] ntdll!__RtlUserThreadStart+0x2f 0x71fa0c 0x76fd7c3e
[0x1d] ntdll!_RtlUserThreadStart+0x1b 0x71fa68 0x0

获取 dll 的堆栈

1
2
[0x0]   ntdll!LdrGetDllFullName   0x71d974   0x76de5a56   
[0x1] KERNELBASE!GetModuleFileNameW+0x46 0x71d978 0x750ff6ee

【COM】通过COM组件IFileOperation越权复制文件
https://hodlyounger.github.io/2023/11/21/A_OS/Windows/COM/【COM】通过COM组件IFileOperation越权复制文件/
作者
mingming
发布于
2023年11月21日
许可协议