引言
作为全球应急响应团队(GERT)成员,我们日常依赖取证工件开展调查,UserAssist是最关键的工件之一。它包含宝贵的执行信息,可帮助追踪敌对活动并检测恶意软件。然而,UserAssist尚未被深入研究,存在数据解释和记录条件等方面的知识空白。本文提供对UserAssist工件的深度分析,阐明其数据表示的模糊性。我们将讨论工件创建和更新的工作流程、UEME_CTLSESSION值结构及其在记录UserAssist数据中的作用,并介绍此前未知的UserAssist数据结构。
UserAssist工件回顾
在取证社区,UserAssist是知名的Windows工件,用于注册GUI程序的执行。该工件存储机器上运行的每个GUI应用程序的各种数据:
- 程序名称:完整程序路径。
- 运行计数:程序执行次数。
- 焦点计数:程序被设置为焦点的次数(通过从其他应用程序切换或使其在前台激活)。
- 焦点时间:程序处于焦点的总时间。
- 最后执行时间:程序最后执行的日期和时间。
UserAssist工件是每个NTUSER.DAT配置单元下的注册表项,位于Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist\。该键包含以GUID命名的子键。两个最重要的GUID子键是:
{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}:注册执行的EXE文件。{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}:注册执行的LNK文件。
每个子键都有名为“Count”的子键,包含表示已执行程序的值。值名称是使用ROT-13密码加密的程序路径。值包含结构化的二进制数据,包括相应应用程序的运行计数、焦点计数、焦点时间和最后执行时间。此结构是众所周知的,表示CUACount对象。焦点时间和最后执行时间之间的字节从未被公开描述或分析,但我们成功确定了它们的作用,将在后文解释。最后四个字节未知,在我们分析的所有数据集中都为零。
数据不一致性
在多次调查中,发现UserAssist数据不一致。一些值包含所有上述参数,而其他值仅包含运行计数和最后执行时间。总体而言,我们观察到五种UserAssist数据不一致的组合。
| 案例 | 运行计数 | 焦点计数 | 焦点时间 | 最后执行时间 |
|---|---|---|---|---|
| 1 | ✓ | ✓ | ✓ | ✓ |
| 2 | ✓ | ✕ | ✕ | ✓ |
| 3 | ✕ | ✓ | ✓ | ✕ |
| 4 | ✓ | ✕ | ✓ | ✓ |
| 5 | ✕ | ✕ | ✓ | ✕ |
工作流程分析
深入Shell32函数
为了理解不一致的原因,我们必须检查负责注册和更新UserAssist数据的组件。我们的分析显示,该组件是shell32.dll,更具体地说,是CUserAssist类中名为FireEvent的函数。
|
|
FireEvent参数如下:
- 参数1:GUID,是UserAssist注册表键的子键,包含注册数据。此参数通常取值为
{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA},因为执行的程序主要是EXE文件。 - 参数2:整数枚举值,定义应更新哪些计数器和数据。
- 值0:更新运行计数和最后执行时间
- 值1:更新焦点计数
- 值2:更新焦点时间
- 值3:未知
- 值4:未知(我们假设用于删除条目)。
- 参数3:已执行、聚焦或关闭的完整可执行路径。
- 参数4:在可执行文件上花费的焦点时间(毫秒)。仅当参数2的值为2时,此参数才包含值;否则为零。
此外,FireEvent函数严重依赖shell32.dll中的另外两个函数:s_Read和s_Write。这些函数负责在更新特定应用程序时从注册表读取和写入UserAssist的二进制值数据:
|
|
s_Read函数从注册表读取UserAssist数据的二进制值到内存,而s_Write将UserAssist数据的二进制值从内存写入注册表。两个函数具有相同的参数:
- 参数1:指向内存缓冲区(CUACount结构)的指针,接收或包含UserAssist二进制数据。
- 参数2:要从注册表读取或写入的UserAssist二进制数据的大小(字节)。
- 参数3:包含两个指针的未记录结构。
- 0x0偏移处的CUADBLog实例指针
- 纯文本的完整可执行路径,关联的UserAssist二进制数据需要从注册表读取或写入。
当程序首次执行且UserAssist记录中没有相应条目时,s_Read函数读取UEME_CTLCUACount:ctor值,该值作为UserAssist二进制数据结构(CUACount)的模板。我们将在后文描述此值。
应注意,s_Read和s_Write函数还负责使用ROT-13密码加密值名称。
UserAssist数据更新工作流程
与显示GUI的程序的任何交互都是触发事件,导致调用CUserAssist::FireEvent函数。有四种类型的触发事件:
- 程序执行。
- 程序设置焦点。
- 程序失去焦点。
- 程序关闭。
触发事件决定了CUserAssist::FireEvent函数的执行工作流程。工作流程基于作为第二个参数传递给FireEvent的枚举值,该值定义应更新UserAssist二进制数据中的哪些计数器和数据。
CUserAssist::FireEvent函数调用CUADBLog::s_Read函数从注册表读取二进制数据到内存。然后,CUserAssist::FireEvent函数更新相应的计数器和数据,再调用CUADBLog::s_Write将数据存储回注册表。
下图说明了根据与程序的交互,UserAssist数据更新过程的工作流程。
UserAssist数据更新工作流程
调用FireEvent函数的函数因与程序交互引起的特定触发事件而异。下表显示了每个触发事件的调用堆栈,以及函数的模块。
| 触发事件 | 模块 | 调用堆栈函数 | 详情 |
|---|---|---|---|
| 程序执行(双击) | SHELL32 | CUserAssist::FireEvent | 此调用链更新运行计数和最后执行时间。仅当在文件资源管理器中双击可执行文件时触发,无论是CLI还是GUI。 |
| Windows.storage | UAFireEvent | ||
| Windows.storage | NotifyUserAssistOfLaunch | ||
| Windows.storage | CInvokeCreateProcessVerb::_OnCreatedProcess | ||
| 程序聚焦 | SHELL32 | CUserAssist::FireEvent | 此调用链更新焦点计数,仅适用于GUI可执行文件。 |
| Explorer | UAFireEvent | ||
| Explorer | CApplicationUsageTracker::_FireDelayedSwitch | ||
| Explorer | CApplicationUsageTracker::_FireDelayedSwitchCallback | ||
| 程序失去焦点 | SHELL32 | CUserAssist::FireEvent | 此调用链更新焦点时间,仅适用于GUI可执行文件。 |
| Explorer | UAFireEvent | ||
| Explorer | <lambda_2fe02393908a23e7ac47d9dd501738f1>::operator() | ||
| Explorer | shell::TaskScheduler::CSimpleRunnableTaskParam«lambda_2fe02393908a23e7ac47d9dd501738f1>, CMemString<CMemString_PolicyCoTaskMem»::InternalResumeRT | ||
| 程序关闭 | SHELL32 | CUserAssist::FireEvent | 此调用链更新焦点时间,适用于GUI和CLI可执行文件。但CLI可执行文件仅当通过双击执行或conhost作为子进程生成时更新。 |
| Explorer | UAFireEvent | ||
| Explorer | shell::TaskScheduler::CSimpleRunnableTaskParam«lambda_5b4995a8d0f55408566e10b459ba2cbe>, CMemString<CMemString_PolicyCoTaskMem»::InternalResumeRT |
不一致性分解
如前所述,我们观察到五种UserAssist数据组合。我们的彻底分析显示,这些不一致源于与程序的交互以及调用FireEvent函数的各种函数。现在,让我们详细检查导致这些不一致的触发事件。
1. 所有数据
第一种组合是UserAssist记录中注册的所有四个参数:运行计数、焦点计数、焦点时间和最后执行时间。在此场景中,程序通常遵循正常执行流程,具有GUI并通过在Windows资源管理器中双击执行。
- 当程序执行时,调用FireEvent函数更新运行计数和最后执行时间。
- 当设置焦点时,调用FireEvent函数更新焦点计数。
- 当失去焦点或关闭时,调用FireEvent函数更新焦点时间。
2. 运行计数和最后执行时间
第二种组合发生在记录仅包含运行计数和最后执行时间时。在此场景中,程序通过在Windows资源管理器中双击运行,但出现的GUI属于另一个程序。此场景的示例包括使用LNK快捷方式启动应用程序或使用运行不同GUI程序的安装程序,这将焦点切换到其他程序文件。
在我们的测试中,calc.exe的副本在Windows资源管理器中使用双击方法执行。然而,弹出的GUI程序是计算器的UWP应用程序Microsoft.WindowsCalculator_8wekyb3d8bbwe!App。
UserAssist中有calc.exe桌面副本的记录,但仅包含运行计数和最后执行时间。然而,焦点计数和焦点时间记录在UWP计算器Microsoft.WindowsCalculator_8wekyb3d8bbwe!App UserAssist条目下。
3. 焦点计数和焦点时间
第三种组合是仅包含焦点计数和焦点时间的记录。在此场景中,程序具有GUI,但通过双击Windows资源管理器以外的其他方式执行,例如通过命令行界面。
在我们的测试中,Sysinternals Suite中的Process Explorer副本通过cmd执行,并在UserAssist中记录,仅包含焦点计数和焦点时间。
4. 运行计数、最后执行时间和焦点时间
第四种组合是记录包含运行计数、最后执行时间和焦点时间。此场景仅适用于通过双击运行然后立即关闭的CLI程序。双击执行导致运行计数和最后执行时间被注册。接下来,程序关闭事件将调用FireEvent函数更新焦点时间,由lambda函数(5b4995a8d0f55408566e10b459ba2cbe)触发。
在我们的测试中,whoami.exe的副本通过双击执行,打开控制台GUI一瞬间后关闭。
5. 焦点时间
第五种组合是仅注册焦点时间的记录。此场景仅适用于通过双击以外的其他方式执行的CLI程序,打开控制台GUI一瞬间后立即关闭。
在我们的测试中,whoami.exe的副本使用PsExec而不是cmd执行。PsExec将whoami作为其自己的子进程执行,导致whoami生成conhost.exe进程。此条件必须满足,CLI程序才能在此场景中注册到UserAssist。
我们在下表中总结了所有五种组合及其相应的解释。
| 不一致组合 | 解释 | 触发事件 |
|---|---|---|
| 所有数据 | GUI程序通过双击正常执行和关闭。 | - 程序执行 - 程序聚焦 - 程序失去焦点 - 程序关闭 |
| 运行计数和最后执行时间 | GUI程序通过双击执行但焦点切换到另一个程序。 | - 程序执行 |
| 焦点计数和焦点时间 | GUI程序通过其他方式执行。 | - 程序聚焦 - 程序失去焦点 - 程序关闭 |
| 运行计数、最后执行时间和焦点时间 | CLI程序通过双击执行然后关闭。 | - 程序执行 - 程序关闭 |
| 焦点时间 | CLI程序通过双击以外的其他方式执行,生成conhost进程然后关闭。 | - 程序关闭 |
CUASession和UEME_CTLSESSION
既然我们已经解决了UserAssist工件的不一致性问题,本研究的第二部分将解释UserAssist的另一个方面:CUASession类和UEME_CTLSESSION值。
UserAssist数据库包含每个已执行程序的值名称,但有一个未知值:UEME_CTLSESSION。与为每个程序记录的二进制数据不同,此值包含更大的二进制数据:1612字节,而已执行程序的常规值大小为72字节。
CUASession是shell32.dll中的一个类,负责维护所有程序的整个UserAssist记录会话的统计信息。这些统计信息包括总运行计数、总焦点计数、总焦点时间和三个顶级程序条目,称为NMax条目,我们将在下文描述。UEME_CTLSESSION值包含CUASession对象的属性。以下是CUASession类的一些函数:
|
|
在CUASession和UEME_CTLSESSION的上下文中,当讨论记录会话中所有已执行程序的参数而不是单个程序的数据时,我们将运行计数称为启动(launches),焦点计数称为切换(switches),焦点时间称为用户时间(user time)。
UEME_CTLSESSION值具有以下特定数据结构:
- 0x0偏移:常规总统计信息(16字节)
- 0x0:记录会话ID(4字节)
- 0x4:总启动次数(4字节)
- 0x8:总切换次数(4字节)
- 0xC:总用户时间(毫秒)(4字节)
- 0x10偏移:三个NMax条目(1596字节)
- 0x10:第一个NMax条目(532字节)
- 0x224:第二个NMax条目(532字节)
- 0x438:第三个NMax条目(532字节)
UEME_CTLSESSION结构
每次调用FireEvent函数更新程序数据时,CUASession更新其自己的属性并将其保存到UEME_CTLSESSION。
- 当调用FireEvent更新程序的运行计数时,CUASession增加UEME_CTLSESSION中的总启动次数。
- 当调用FireEvent更新程序的焦点计数时,CUASession增加总切换次数。
- 当调用FireEvent更新程序的焦点时间时,CUASession更新总用户时间。
NMax条目
NMax条目是特定程序的UserAssist数据的一部分,包含程序的运行计数、焦点计数、焦点时间和完整路径。NMax条目是UEME_CTLSESSION值的一部分。每个NMax条目具有以下数据结构:
- 0x0偏移:程序的运行计数(4字节)
- 0x4偏移:程序的焦点计数(4字节)
- 0x8偏移:程序的焦点时间(毫秒)(4字节)
- 0xc偏移:程序的名称/完整路径(Unicode)(520字节,Windows最大路径长度乘以二)
NMax条目结构
NMax条目跟踪最常执行、切换和使用的程序。每当调用FireEvent函数更新程序时,调用CUADBLog::_CheckUpdateNMax函数以检查并相应更新NMax条目。
第一个NMax条目存储基于运行计数最常执行的程序的数据。如果两个程序(先前保存在NMax条目中的程序数据和触发FireEvent更新的程序)具有相等的运行计数,则根据两个程序之间较高的计算值(称为N值)更新条目。N值方程如下:
|
|
第二个NMax条目存储具有最多切换次数的程序的数据,基于其焦点计数。如果两个程序具有相等的焦点计数,则根据最高的计算N值更新条目。
第三个NMax条目存储使用最多的程序的数据,基于最高的N值。
解析的UEME_CTLSESSION结构与NMax条目如下所示。
|
|
UEME_CTLSESSION数据
UserAssist重置
UEME_CTLSESSION在注销或重启后仍将持久存在。然而,当达到其总用户时间