概述:本文是基于伪装 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、IDAWindbg

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

查看对应的服务

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

# 查看 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
 
# 侵入式调试
.process /p EPROCESS
.reload /f /user
.process /p /i EPROCESS

调试思路

windbg 时间旅行

调试说明

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

确认调用流程

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

[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 的哪个接口。

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

详细调试步骤

第一次 UAC

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

查看伪装发起的调用

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

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

    经验之谈

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

# 打开日志文件
.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 对象如下所示: ![](【COM】通过COM组件IFileOperation越权复制文件/IMG-20241010121053434.png) 3. 继续执行会触发第二次 CocreateInstanceEx 断点,查看 CLSID,值为 `{f0ae1542-f497-484b-a175-a20db09144ba}`, 对应的对象如下所示: ![](【COM】通过COM组件IFileOperation越权复制文件/IMG-20241010121053888.png) 4. 继续执行触发第四次断点,查看 CLSID, 值为 `{cdc82860-468d-4d4e-b7e7-c298ff23ab2c}` ![](【COM】通过COM组件IFileOperation越权复制文件/IMG-20241010121054164.png) 5. 到这里基本就是 windows. storage.dll 的互调了,添加 rpc 断点 ``` bp RPCRT4!NdrClientCall2 ``` rpc 断点触发后查看 CoCreateInstanceEx 创建的 COM 对象是哪个 示例如下: 堆栈如上,查看创建的 COM 对象是哪个:
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

    # 打开日志
    .logopen /t c:\logs\mylogfile.txt
     
     bu RPCRT4!Invoke+0x73-2d "u r10 l5;gc"
  • 中间步骤的调试结果就是找错方向了,浪费了太多精力。伪装进程并不是通过创建 COM 对象来传递数据的。而是通过如下所示的堆栈,准确地说,是通过 combase!connect(Win11) 这个接口,在 Win11 操作系统上已经将这个接口改动到 Lambda 表达式中。结合我在一些错误尝试之后写的脚本,重新梳理下。 image2024-11-1_11-57-55

  • 借助 RpcView 分析接口

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

    image-20241101121422689

    查看接口

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

    image-20241101121647686

    查找伪装进程调用接口

    connect 函数说明

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

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

    image-20241101140313936

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

    # 添加断点
    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 接口。

    GetConnection

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

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

    lambda 函数说明 combase!Connect ,最终由 RPCSS 服务中的rpcss!_Connect 接口完成本次 RPC 调用。

    这里虽然调用的是 combase 中的 lambda 接口,最终还是会通过 inline call 的方式调用到

    另外就是在 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 则最好是在内核调试状态下。

    调试准备
    # 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 接口吧。 image-20241021101651798

    2. CProcess 构造函数

      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 断点触发时,可以观察下相关信息:

    # 堆栈
    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: 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 的处理

    调式准备
    # 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 所在进程的所有线程,查找相关线程:

    # 这里会输出所有线程的堆栈信息 ffffa18d9265a2c0
    !process ffffa18d9265a2c0 7
     
    # 找相关的那个就行,如下所示,搜索 consent
    THREAD ffffa18d92e95080  Cid 02a0.0aac  [[【WIN】TEB(线程环境块)|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 等一部分参数:

    image-20241029180846506

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

    image-20241107182802211

    AiCheckSecureAPPLicationDirectory 验证的路径主要如下所示:

    image2025-2-13_10-29-53

    获取 RPC 客户端信息

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

    image-20241108103503181

    I_RpcBindInqLocalClientPID 调用后查看获取到的 PID

    image-20241108103938976

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

    image-20241108104046037

    提示

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

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

    补充1

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

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

    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 进程情况下,执行如下所示命令:

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

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

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

    image-20241107164547335

    # 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

    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 所在的进程

    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 弹窗还在时可以使用这种方式继续调试部分流程)

    # 查看 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 函数(不论是否是白名单进程都会执行这个接口):

    __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 参数

      _CONSENTUI_PARAM_HEADER 参数实例

    • 第一处判断 image-20241106101457435

    • 第二处判断

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

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

      image-20241106104341486

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

    image-20241106112330661

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

    image-20241029153249800

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

    image-20241029153512222

    关于上述的 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 进程传入的结构体中

    image-20241107171224863

    流程总结

    大致流程如下所示:

    flowchart TD
    	A[伪装进程] --> |RPC调用|A1
    	A1[rpcss!connect] --> |RPC 调用|B
    	B[appinfo!RAiGetTokenForCOM]-->|"构建 Consent 参数,传递路径"|C1
    	C1[AipGetTokenForService] --> C2
    	C2[AiCheckSecureApplicationDirectory] --> 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:
    • offset+0x30 条件2:
    条件1

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

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

    image-20241106164434840

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

    image-20241106164551285

    CuiCheckElevationAutoApprovalMedium 函数执行逻辑:

    image-20241106173259179

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

      image-20241107092247073

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

    条件2

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

    图 RAiGetTokenForCOM 请求

    图 AipGetTokenForService 参数1 的判断

    总结

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

    第二次 UAC

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

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

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

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

    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 伪装时堆栈如下所示:

    [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 来创建另一个进程:

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

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

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

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

    [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

    判断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(取决于传递的路径)

    AipGetTokenForService

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

    文件启动路径

    获取程序运行路径的堆栈

    [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 的堆栈

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