概述:本文是基于伪装 PEB 进行越权复制文件相关学习研究进行的拓展文章,主要记录通过 COM 组件越权复制文件。
[toc]
本篇博客参考的文章较多,然而这些文章也只是其中的一个点或者两个点进行描述,并不详细。这里就不一一罗列了,本文较多内容都是笔者结合相关文章贴上的,相信你看此一篇就可!
主要内容:描述和记录一下伪装 PEB 进行越权复制时,Windows 是如何处理这一系列过程的。
伪装 PEB 进行越权复制
不妨先从最基本的说起,伪装 PEB 并不是将整个 PEB 内容都改写,而是对其中的几处内容进行修改以达到越权目的。
伪装 PEB
主要操作就是修改当前进程的 ImagePathName
、 CommandLine
、 FullDllName
、BaseDllName
这几个变量,通过修改成为与系统可信的进程一样的内容进而实现越权操作。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 -2 d ".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 去执行下一步操作
windbg 运行目标程序,添加断点,因为已经多次调试这个程序了,这里就简要记录下
经验之谈
随着调试技术的熟练。我们只需要观察 NdrClientCall2()
的调用,再通过堆栈就能找到如下所示的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .logopen C:\windbg.log 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 023 dfaf4 #输出如下所示:0 :003 > dds 023 dfaf4023 dfaf4 023 dfb30023 dfaf8 746b 3c80 windows_storage!CSearchIndexNotificationQueue::s_FlushNotificationQueueThreadProc+0x50 023 dfafc 74629444 windows_storage!_GUID_9e175b6d_f52a_11d8_b9a5_505054503030023 dfb00 00000000 023 dfb04 00000017 023 dfb08 74629454 windows_storage!_GUID_a5eba07a_dae8_4d15_b12f_728efd8a9866023 dfb0c 023 dfb18023 dfb10 02291780
查看 74629444
和 74629454
对应的 COM 对象:
{9E175B6D-F52A-11D8-B9A5-505054503030}
{A5EBA07A-DAE8-4D15-B12F-728EFD8A9866}
到这里就看到请求的服务是 SearchIndexer 了,调试下 SearchIndexer1 2 3 4 # 打开日志 .logopen /t c:\logs\mylogfile.txt bu RPCRT4!Invoke+0x73 -2 d "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 bu rpcss!_Connect ".echo 'rpcss!_Connect Para3:';du @r8;" bp ole32!CoAicGetTokenForCOM bp ole32!CoAicGetTokenForCOM+0x69
查看 rpcss!connect 接口
在上一小节部分,我们已经看到了伪装进程通过 rpc 最终调用到了 rpcss 服务中的 _Connect
接口。那在 rpcss!_Connect
接口中,通过 IDA 的反汇编可以看到,又通过 CProcess::CProcess
来创建了一个进程(猜测)。从 rpcss 到 appinfo 的过程比较复杂,这里还是观察 NdrClientCall2
接口吧。
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 Site00 rpcss!CProcess::CProcess01 rpcss!_Connect+0x4e7 02 RPCRT4!Invoke+0x73 03 RPCRT4!NdrStubCall2Heap+0x342 # 查看参数3 和参数4 0 : kd> dU @r800000187 `d8f4b3e8 "WinSta0\Default" 0 : kd> dU @r900000187 `d8f4b418 "C:\Windows\system32\consent.exe"
而 rpcss
最终调用 CoAicGetTokenForCOM
向 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 1 : kd> kcn # Call Site00 ole32!CoAicGetTokenForCOM ole32!CoAicGetTokenForCOM+0x69 添加权限查看 Token 更明显,地址没变,但是地址内句柄指向的 Token 权限增加了01 rpcss!ActivateFromPropertiesPreamble+0x1f7b 01 retAddr rpcss!ActivateFromPropertiesPreamble+0x1f82 # 可以在此处添加断点观察前后的参数4 ,查看Token权限变化02 rpcss!PerformScmStage03 rpcss!SCMActivatorCreateInstance04 RPCRT4!Invoke05 RPCRT4!NdrStubCall2Heap06 RPCRT4!NdrStubCall207 RPCRT4!NdrServerCall208 RPCRT4!DispatchToStubInCNoAvrf09 RPCRT4!RPC_INTERFACE::DispatchToStubWorker0 a RPCRT4!RPC_INTERFACE::DispatchToStub0b RPCRT4!LRPC_SCALL::DispatchRequest0 c RPCRT4!LRPC_SCALL::HandleRequest0 d RPCRT4!LRPC_ADDRESS::HandleRequest RPCRT4!LRPC_ADDRESS::HandleRequest+0x341 "dt _PORT_MESSAGE @rbp" # 在此处添加断点可以查看发起请求的客户端0 e RPCRT4!LRPC_ADDRESS::ProcessIO0f RPCRT4!LrpcIoComplete10 ntdll!TppAlpcpExecuteCallback11 ntdll!TppWorkerThread12 KERNEL32!BaseThreadInitThunk13 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 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 02 a0.0 aac Teb: 0000008049 ae2000 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`7f 455330 : ffffa18d`00000008 00000000 `ffffffff ffff9c8a`00000000 ffffa18d`95631158 : nt!KiSwapContext+0x76 ffff9c8a`f4a45820 fffff801`7f 45485f : 00000000 `00000000 00000000 `00000000 ffff9c8a`f4a459e0 00000000 `00000000 : nt!KiSwapThread+0x500 ffff9c8a`f4a458d0 fffff801`7f 454103 : 00000000 `00000000 fffff801`00000000 00000000 `00000000 ffffa18d`92e951 c0 : nt!KiCommitThreadWait+0x14f ffff9c8a`f4a45970 fffff801`7f 858dd1 : ffffa18d`94034080 fffff801`00000006 ffff9c8a`f4a45b01 ffff9c8a`f4a45b00 : nt!KeWaitForSingleObject+0x233 ffff9c8a`f4a45a60 fffff801`7f 858d2a : ffffa18d`92e95080 00000000 `00000000 00000000 `00000000 00000000 `00000000 : nt!ObWaitForSingleObject+0x91 ffff9c8a`f4a45ac0 fffff801`7f 624ef5 : ffffa18d`92e90000 00000000 `00001000 00000000 `00000000 00000000 `00000000 : nt!NtWaitForSingleObject+0x6a ffff9c8a`f4a45b00 00007f fd`c99ed064 : 00007f fd`c72730ce 00000000 `00000022 00000023 `00000004 00000004 `00000000 : nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffff9c8a`f4a45b00) 00000080 `4b e7e5b8 00007f fd`c72730ce : 00000000 `00000022 00000023 `00000004 00000004 `00000000 00000000 `00000024 : ntdll!NtWaitForSingleObject+0x14 00000080 `4b e7e5c0 00007f fd`99 a178d9 : 00000000 `00000000 00000000 `00000001 00000080 `00000000 00000000 `00002508 : KERNELBASE!WaitForSingleObjectEx+0x8e 00000080 `4b e7e660 00007f fd`99 a171e3 : 00000000 `00000000 0000024 c`d154cfa0 00000000 `00000000 00000000 `00000004 : appinfo!AiLaunchConsentUI+0x559 00000080 `4b e7e880 00007f fd`99 a32ff1 : 00000000 `00000002 00000000 `00002568 00000000 `00004000 00000000 `00000002 : appinfo!AiCheckLUA+0x343 00000080 `4b e7ea60 00007f fd`99 a339b6 : 0000024 c`d428c760 0000024 c`d428c810 0000024 c`d428c810 0000024 c`d0f7ac08 : appinfo!AipGetTokenForService+0x245 00000080 `4b e7eb80 00007f fd`c7f4b4b3 : 0000024 c`d3236b80 0000024 c`d2f71de0 00000000 `00000000 00000000 `00000002 : appinfo!RAiGetTokenForCOM+0x206 00000080 `4b e7ec40 00007f fd`c7fac5ea : 0000024 c`d3236b80 0000024 c`d00f9320 0000024 c`d0f7a8e0 00007f fd`99 a35e30 : RPCRT4!Invoke+0x73
从上述堆栈可以看到最先开始调用的接口为appinfo!RAiGetTokenForCOM
,并且是通过 RPC 调用进来的。 最后调用的函数为 appinfo!AiLaunchConsentUI
,appinfo 最后的处理则是调用通过 AiLaunchProcess
如下所示:
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 后,调用 NtOpenProcess
、RpcImpersonateClient
、NtOpenThreadToken
、NtDuplicateToken(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 Site00 0000002 e`5b 07e5f8 00007f f9`1 d7610e2 : 000001f f`7f 779901 0000002 e`5b 07e700 000001f f`7f a7bd08 00000000 `00000000 : ole32!CoAicGetTokenForCOM [com\ole32\dll\dll.cpp @ 433 ] 01 0000002 e`5b 07e600 00007f f9`1 d75a209 : 0000002 e`5b 07ea20 000001f f`000000b c 0000002 e`5b 07ea20 00000000 `00000000 : rpcss!ActivateFromPropertiesPreamble+0x1f82 02 0000002 e`5b 07e920 00007f f9`1 d759046 : 000001f f`7f acd5b0 00000000 `000000 a0 00000000 `00000000 00007f f9`1 d77f51e : rpcss!PerformScmStage+0xb79 03 0000002 e`5b 07eae0 00007f f9`21f 5b4b3 : 000001f f`7f acd5b0 000001f f`7f 1f6bf0 000001f f`7f a9a720 000001f f`7f ba6970 : rpcss!SCMActivatorCreateInstance+0x1b6 04 0000002 e`5b 07ee10 00007f f9`21f 5a282 : 00007f f9`1 d84dd22 000001f f`7f ba6790 00000000 `00000000 00007f f9`1 d84dcee : RPCRT4!Invoke+0x73 05 0000002 e`5b 07ee90 00007f f9`21 efe1ca : 00000000 `00000000 00000000 `00000000 000001f f`7f acd700 0000002 e`5b 07f188 : RPCRT4!NdrStubCall2Heap+0x342 06 0000002 e`5b 07f120 00007f f9`21f 3dfda : 00000000 `00000000 000001f f`7f a119b0 000001f f`7f acd700 00007f f9`223305 dd : RPCRT4!NdrStubCall2+0x3a 07 0000002 e`5b 07f150 00007f f9`21f 39188 : 000001f f`7 ec25c84 000001f f`00000001 000001f f`7f acd5b0 00000000 `00000000 : RPCRT4!NdrServerCall2+0x1a 08 0000002 e`5b 07f180 00007f f9`21f 1a3a6 : 000001f f`00501100 000001f f`7 ec53860 0000002 e`5b 07f380 00007f f9`2233 ae20 : RPCRT4!DispatchToStubInCNoAvrf+0x18 09 0000002 e`5b 07f1d0 00007f f9`21f 19cf8 : 000001f f`7 ec53860 00000000 `00000000 00000000 `00000000 00007f f9`2233 cabb : RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1a6 0 a 0000002 e`5b 07f2b0 00007f f9`21f 274bf : 00000000 `00000000 000001f f`0007f 1cd 000001f f`7f acd5b0 00007f f9`2236b c81 : RPCRT4!RPC_INTERFACE::DispatchToStub+0xf8 0b 0000002 e`5b 07f320 00007f f9`21f 268c8 : 00000000 `0008581 d 00000000 `00000005 00000000 `00000000 000001f f`7f a119b0 : RPCRT4!LRPC_SCALL::DispatchRequest+0x31f 0 c 0000002 e`5b 07f3f0 00007f f9`21f 25eb1 : 00000000 `0000104 c 000001f f`7f ad0b70 00000000 `00000000 000001f f`00000000 : RPCRT4!LRPC_SCALL::HandleRequest+0x7f8 0 d 0000002 e`5b 07f500 00007f f9`21f 2591e : 00000000 `00000000 00000000 `00000000 00000000 `00000001 000001f f`7 ec20b60 : RPCRT4!LRPC_ADDRESS::HandleRequest+0x341 0 e 0000002 e`5b 07f5a0 00007f f9`21f 2a032 : 000001f f`7f a119b0 000001f f`7f a119b0 000001f f`7 ec20c68 0000002 e`5b 07f978 : RPCRT4!LRPC_ADDRESS::ProcessIO+0x89e 0f 0000002 e`5b 07f6e0 00007f f9`22330330 : 00000001 `00000000 00000000 `00000000 0000002 e`5b 07f978 00000000 `00000184 : RPCRT4!LrpcIoComplete+0xc2 10 0000002 e`5b 07f780 00007f f9`22362f 86 : 00000000 `00000000 000001f f`7 ec20d00 00000000 `00000000 000001f f`7f 196ae0 : ntdll!TppAlpcpExecuteCallback+0x260 11 0000002 e`5b 07f800 00007f f9`21597344 : 00000000 `00000000 00000000 `00000000 00000000 `00000000 00000000 `00000000 : ntdll!TppWorkerThread+0x456 12 0000002 e`5b 07fb00 00007f f9`223626b 1 : 00000000 `00000000 00000000 `00000000 00000000 `00000000 00000000 `00000000 : KERNEL32!BaseThreadInitThunk+0x14 13 0000002 e`5b 07fb30 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 `7f bac4c0 "WinSta0\Default" @r8 struct _GUID * lpGuid = 0x0000002e `5b 07eb6c {3 AD05575-8857 -4850 -9277 -11B 85BDB8E09} @r9 wchar_t * lpFriendlyName = 0x000001ff `7f aa3120 "文件操作" 0000002 e`5b 07e620 wchar_t * lpServerBinary = 0x000001ff `7f 798250 "C:\Windows\system32\windows.storage.dll" 0000002 e`5b 07e628 wchar_t * lpIconReference = 0x00007ff9 `1 d840a88 "" 0000002 e`5b 07e630 wchar_t * lpRequestorPath = 0x000001ff `7f 7cbf10 "C:\windows\explorer.exe" 0000002 e`5b 07e638 unsigned long dwRunLevel = 2 0000002 e`5b 07e640 unsigned long dwClientFlags = 0x20 0000002 e`5b 07e648 int fAdjustTokenSD = 0 n10000002 e`5b 07e650 void * hTokenIn = 0x00000000 `000011 cc0000002 e`5b 07e658 void ** phNewToken = 0x0000002e `5b 07e6b80 : kd> bp 0x7ff9205b5199 0 : kd> dq 0x0000002e `5b 07e6b80000002 e`5b 07e6b8 00000000 `00000000 00000000 `00000007 0000002 e`5b 07e6c8 00007f f9`00000000 00000000 `00000002 0000002 e`5b 07e6d8 000001f f`7 ebd0000 00000000 `00000000 0000002 e`5b 07e6e8 00007f f9`22317 afb 000001f f`7f 1f60b00000002 e`5b 07e6f8 000001f f`7f b8e730 0000002 e`5b 07e4400000002 e`5b 07e708 00000000 `00000000 000001f f`00001280 0000002 e`5b 07e718 00007f f9`2233 c282 00000000 `00000000 0000002 e`5b 07e728 00000000 `00000f 38 000001f f`7f b078000 : kd> g'rpcss!_Connect Para3:' 000001f f`7f 1d8c1c "C:\Windows\system32\consent.exe" Breakpoint 10 hit ole32!CoAicGetTokenForCOM+0x69 :0033 :00007f f9`205b 5199 4883 c468 add rsp,68 h1 : kd> dq 0x0000002e `5b 07e6b80000002 e`5b 07e6b8 00000000 `00000e54 00000000 `00000007 0000002 e`5b 07e6c8 00007f f9`00000000 00000000 `00000002 0000002 e`5b 07e6d8 000001f f`7 ebd0000 00000000 `00000000 0000002 e`5b 07e6e8 00007f f9`22317 afb 000001f f`7f 1f60b00000002 e`5b 07e6f8 000001f f`7f b8e730 0000002 e`5b 07e4400000002 e`5b 07e708 00000000 `00000000 000001f f`00001280 0000002 e`5b 07e718 00007f f9`2233 c282 00000000 `00000000 0000002 e`5b 07e728 00000000 `00000f 38 000001f f`7f b078001 : kd> !handle 00000000 `00000e54 PROCESS ffff908328f1e340 SessionId: 0 Cid: 03b 4 Peb: 2e5 a6aa000 ParentCid: 02b c DirBase: 13 deb000 ObjectTable: ffffc18c8229b280 HandleCount: 1104. Image: svchost.exe Handle table at ffffc18c8229b280 with 1104 entries in use0e54 : Object: ffffc18c88330380 GrantedAccess: 000f 01ff (Protected) (Audit) Entry: ffffc18c84dff950 Object: ffffc18c88330380 Type: (ffff9083244bfd20) Token ObjectHeader: ffffc18c88330350 (new version) HandleCount: 1 PointerCount: 1
查看 consent 的处理
调试准备
由于 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 0000024 CD154CFA0 # 这个进程就是 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: 02 a0 Peb: 8049 a87000 ParentCid: 02b c DirBase: 2 aa88000 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 0000014 ED7003290 CurrentDirectory : C:\Windows\system32\ DllPath Environment 0 : kd> ? 0 n728 Evaluate expression: 728 = 00000000 `000002 d80 : kd> !process 00000000 `000002 d8 0 Searching for Process with Cid == 2 d8 PROCESS ffffca04136602c0 SessionId: 0 Cid: 02 d8 Peb: 4 db9278000 ParentCid: 02 c4 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 eprocess1 : kd> !process 0 0 consent.exe PROCESS ffffa18d94034080 SessionId: 1 Cid: 1374 Peb: 7e9 b83b000 ParentCid: 02 a0 DirBase: 79408000 ObjectTable: ffffdf0498039900 HandleCount: 138. Image: consent.exe # 加载 PDB1 : kd> .process /p ffffa18d94034080 Implicit process is now ffffa18d`94034080 .cache forcedecodeuser done1 : 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:v a l u e = = 1 value == 1 v a l u e = = 1
offset+0x30 条件2:v a l u e & 0 x 2000 = = 0 value \& 0x2000 == 0 v a l u e & 0 x 2 0 0 0 = = 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 中修改的路径。
相关分析文章
如下所示为启动一个 uac 进程到该进程发起 rpc 的调用堆栈,须知当前进程的提权是通过 RPC 调用到 Appinfo 服务的。流程如下:
uac 通过 RPC 请求到 AppInfo 服务
APPInfo 通过 AiLaunchProcess
接口创建 consent.exe
进程,调用时会传入一个结构体地址,调用命令行为 consent.exe ppid 结构体长度 结构体地址
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
的创建管理员对象
在创建 COM 对象时,会调用 dllHost 来创建另一个进程:
1 C:\WINDOWS \system32 \DllHost.exe /Processid :{3AD05575 -8857-4850-9277-11B85BDB8E09 }
在注册表可以看到该 CLSID 的一些信息如下所示:
1 reg hkey_classes_root\CLSID\{3 AD05575-8857 -4850 -9277 -11 B85BDB8E09}
到达 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 的请求流程。
程序配置为自动提升
如果程序中配置了 autoElevate 为 true,会尝试自动提升
1. 先判断是否限制自动提权策略
2. 判断是否设置了 autoElevate
白名单
判断要执行的程序是否属于白名单,在白名单之内就调用 AipIsValidAutoApprovalEXE 函数检查 程序签名 等信息,如果不在就基本结束这个函数了
依靠上述两个判断还是不够的,在 appinfo 中还有如下所示的几个列表。
g_lpExcludedWindowsDirs
g_lpIncludedWindowsDirs
g_lpIncludedSystemDirs
g_lpIncludedPFDirs
弹窗逻辑
但是中间弹窗的过程被省略了,这里可以调试分析一下。主要就是通过查看 COM 组件有没有自提升权限进而判断是否需要弹窗,查看 CuiIsCOMClassAutoApprovable
即可。
越权逻辑
越权主要还是基于以下两点:
各类 UAC 白名单程序的 DLL 劫持(Dll Hijack)
各类提升权限的 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