深入解析UserAssist:Windows取证中的关键工件与IR应用

本文详细解析Windows UserAssist工件的结构、数据不一致性原因及在事件响应中的应用,涵盖FireEvent函数工作机制、UEME_CTLSESSION会话统计和新增的r0值解析,为安全分析提供深度技术洞察。

引言

作为全球应急响应团队(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的函数。

1
virtual long CUserAssist::FireEvent(struct _GUID const *, enum tagUAEVENT, unsigned short const *, unsigned long)

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的二进制值数据:

1
2
static long CUADBLog::s_Read(void *, unsigned long, struct NRWINFO *)
static long CUADBLog::s_Write(void *, unsigned long, struct NRWINFO *)

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函数。有四种类型的触发事件:

  1. 程序执行。
  2. 程序设置焦点。
  3. 程序失去焦点。
  4. 程序关闭。

触发事件决定了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类的一些函数:

1
2
3
4
5
6
7
8
CUASession::AddLaunches(uint)
CUASession::GetTotalLaunches(void)
CUASession::AddSwitches(uint)
CUASession::GetTotalSwitches(void)
CUASession::AddUserTime(ulong)
CUASession::GetTotalUserTime(void)
CUASession::GetNMaxCandidate(enum _tagNMAXCOLS, struct SNMaxEntry *)
CUASession::SetNMaxCandidate(enum _tagNMAXCOLS, struct SNMaxEntry const *)

在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值方程如下:

1
N值 = 程序的运行计数 * (总用户时间 / 总启动次数) + 程序的焦点时间 + 程序的焦点计数 * (总用户时间 / 总切换次数)

第二个NMax条目存储具有最多切换次数的程序的数据,基于其焦点计数。如果两个程序具有相等的焦点计数,则根据最高的计算N值更新条目。

第三个NMax条目存储使用最多的程序的数据,基于最高的N值。

解析的UEME_CTLSESSION结构与NMax条目如下所示。

 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
{
    "stats": {
        "Session ID": 40,
        "Total Launches": 118,
        "Total Switches": 1972,
        "Total User Time": 154055403
    },
    "NMax": [
        {
            "Run Count": 20,
            "Focus Count": 122,
            "Focus Time": 4148483,
            "Executable Path": "Microsoft.Windows.Explorer"
        },
        {
            "Run Count": 9,
            "Focus Count": 318,
            "Focus Time": 34684910,
            "Executable Path": "Chrome"
        },
        {
            "Run Count": 9,
            "Focus Count": 318,
            "Focus Time": 34684910,
            "Executable Path": "Chrome"
        }
    ]
}

UEME_CTLSESSION数据

UserAssist重置

UEME_CTLSESSION在注销或重启后仍将持久存在。然而,当达到其总用户时间

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计