Christophe Alladoum

深入了解 TTD 生态系统

这是重点介绍微软开发的时间旅行调试 (TTD) 技术的系列文章中的第一篇,该技术在最近的独立研究期间进行了详细探讨。

阅读时间:20 分钟安全研究
深入了解 TTD 生态系统

每年几次,Elastic Security Labs 的研究人员可以自由选择和深入研究他们喜欢的项目 - 无论是单独还是以团队的形式。 这段时间在内部被称为“On-Week”项目。 这是专注于微软开发的时间旅行调试(TTD) 技术的系列文章中的第一篇,该技术在最近的 On-Week 会议上进行了详细探讨。

尽管已经公开多年,但信息安全社区对 TTD 及其潜力的认识却被大大低估了。 我们希望这个由两部分组成的系列文章能够帮助阐明 TTD 如何用于程序调试、漏洞研究和利用以及恶意软件分析。

这项研究首先涉及了解 TTD 的内部工作原理,然后评估可以从中获得的一些有趣的应用用途。 这篇文章将重点介绍研究人员如何深入研究 TTD,分享他们的方法以及一些有趣的发现。 第二部分将详细介绍 TTD 在恶意软件分析和与 Elastic Security 集成方面的适用用途。

背景

时间旅行调试是微软研究院开发的一款工具,允许用户记录执行情况并自由导航到二进制文件的用户模式运行时。 TTD 本身依赖于两项技术:Nirvana 用于二进制翻译,iDNA 用于跟踪读写过程。 自 Windows 7 推出以来,TTD 内部结构首次在一份公开的论文中进行了详细介绍。 从那时起,微软独立研究人员都对其进行了详细的报道。 因此,我们不会深入探讨这两种技术的内部原理。 相反,Elastic 研究人员调查了使 TTD 实现发挥作用的生态系统(或可执行文件、DLL 和驱动程序)。 这导致了一些关于 TTD 以及 Windows 本身的有趣发现,因为 TTD 利用一些(未记录的)技术在特殊情况下按预期工作,例如受保护的进程

但为什么要调查 TTD 呢? 除了纯粹的好奇心之外,该技术的可能用途之一可能是发现生产环境中的错误。 当错误难以触发或重现时,拥有“一次记录,始终重放”类型的环境有助于弥补这种困难,这正是 TTD 与 WinDbg 结合使用时所实现的。

在逆向 Windows 组件时, WinDbg等调试工具一直是巨大的信息来源,因为它们提供额外的可理解的信息(通常以纯文本形式)。 调试工具(尤其是调试器)必须与底层操作系统配合,这可能涉及调试接口和/或操作系统以前未公开的功能。 TTD 符合该模式。

高级概述

TTD 的工作原理是首先创建一个记录,跟踪应用程序执行的每条指令,并将其存储在数据库中(后缀为 .run)。 可以使用 WinDbg 调试器随意重放记录的跟踪,该调试器在第一次访问时将索引 .run 文件,从而可以更快地浏览数据库。 为了能够跟踪任意进程的执行,TTD 注入了一个负责按需记录活动的 DLL,这使得它能够通过生成进程来记录进程,但也可以附加到已经运行的进程。

TTD 作为 MS Store 中 WinDbg 预览包的一部分可供免费下载。 它可以直接从 WinDbg Preview(又名 WinDbgX)中使用,但它是一个位于C:\Program Files\WindowsApps\Microsoft.WinDbg_<version></version>_<arch>__8wekyb3d8bbwe\amd64\ttd的 x64 架构独立组件,我们将在本文中重点介绍它。 x86 和 arm64 版本也可以在 MS Store 下载。

该包包含两个 EXE 文件(TTD.exe 和 TTDInject.exe) 以及少量的 DLL。 本研究重点关注与 Nirvana/iDNA 无关的所有主要 DLL(即 负责会话管理、驱动程序通信、DLL 注入等):ttdrecord.dll

_注意:本研究主要使用两个版本的 ttdrecord DLL:主要是 2018 版本(1.9.106.0 SHA256=aca1786a1f9c96bbe1ea9cef0810c4d164abbf2c80c9ecaf0a1ab91600da6630) 和早期 2022 版本 (10.0.19041.1 SHA256=1FF7F54A4C865E4FBD63057D5127A73DA30248C1FF28B99FF1A43238071CBB5C)。 发现旧版本有更多的符号,这有助于加快逆向工程过程。 然后,我们将结构和功能名称重新调整为最新版本。 因此,如果您尝试在较新的版本上进行复制,这里解释的某些结构可能不一样。 _

检查 TTD 功能

命令行参数

读者应该注意到,TTD.exe 本质上是 ttdrecord!ExecuteTTTracerCommandLine 的包装器:

HRESULT wmain()
{
v28 = 0xFFFFFFFFFFFFFFFEui64;
hRes = CoInitializeEx(0i64, 0);
if ( hRes >= 0 )
{
ModuleHandleW = GetModuleHandleW(L"TTDRecord.dll");
[...]
TTD::DiagnosticsSink::DiagnosticsSink(DiagnosticsSink, &v22);
CommandLineW = GetCommandLineW();
lpDiagnosticsSink = Microsoft::WRL::Details::Make<TTD::CppToComDiagnosticsSink,TTD::DiagnosticsSink>(&v31, DiagnosticsSink);
hRes = ExecuteTTTracerCommandLine(*lpDiagnosticsSink, CommandLineW, 2i64);
[...]

上面代码摘录的最后一行显示了对 ExecuteTTTracerCommandLine 的调用,它将整数作为最后一个参数。 此参数对应于所需的跟踪模式,它们是:- 0 -> FullTracingMode,- 1 -> UnrestrictedTracing 和 - 2 -> Standalone(TTD.exe 公共版本的硬编码模式)

强制 TTD 以完整跟踪模式运行会显示可用选项,其中包括一些隐藏功能,例如进程重新父化(-parent)和程序和服务的自动跟踪直到重新启动(-onLaunch)。

转储 TTDRecord.dll 的完整选项集揭示了有趣的隐藏命令行选项,例如:

-persistent Trace programs or services each time they are started (forever). You must specify a full path to the output location with -out.
-delete Stop future tracing of a program previously specified with -onLaunch or -persistent. Does not stop current tracing. For -plm apps you can only specify the package (-delete <package>) and all apps within that package will be removed from future tracing
-initialize Manually initialize your system for tracing. You can trace without administrator privileges after the system is initialized.

设置Nirvana的过程需要TTD在目标_EPROCESS中设置InstrumentationCallback字段。 这是通过(未记录但已知的)NtSetInformationProcess(ProcessInstrumentationCallback)系统调用(ProcessInstrumentationCallback,其值为 40)实现的。 由于潜在的安全隐患,调用此系统调用需要提升权限。 有趣的是,-initialize 标志还暗示 TTD 可以部署为 Windows 服务。 该服务将负责代理跟踪请求至任意进程。 可以通过执行它并查看生成的错误消息来确认这一点:

尽管很容易找到证实 TTDService.exe 存在的证据,但该文件并非作为公共包的一部分提供,因此除了注意到 TTD 可以作为服务运行之外,我们不会在本文中介绍它。

TTD进程注入

如上所述,TTD 跟踪文件可以从独立二进制文件 TTD.exe 创建,也可以通过服务 TTDService.exe(私有)创建,两者都必须在特权上下文中运行。 然而,这些只是启动器,注入录制 DLL(名为 TTDRecordCPU.dll)是另一个进程 TTDInject.exe 的工作。

TTDInject.exe 是另一个明显比 TTD.exe 大的可执行文件,但其目的非常简单:准备跟踪会话。 简单来说,TTD.exe 将首先以挂起状态启动要记录的进程。 然后它将产生 TTDInject.exe, 向其传递准备会话所需的所有参数。 请注意,TTDInject 还可以根据我们前面提到的跟踪模式直接生成进程 - 因此,我们正在描述最常见的行为(即从 TTD.exe 生成时)。

TTDInject 会在记录的进程中创建一个线程来执行 TTDLoader!InjectThread,经过各种验证后,会加载负责记录所有进程活动的库 TTDRecordCPU.dll。

从那时起,执行期间遇到的所有指令、内存访问、触发的异常或 CPU 状态都将被记录。

一旦了解了 TTD 的一般工作流程,就会清楚地发现会话初始化后几乎不可能进行任何操作。 因此,我们进一步关注了 ttdrecord.dll 支持的参数。 由于 C++ 混淆函数格式,可以从函数名称本身检索大量关键信息,这使得分析命令行参数解析器相对简单。 发现的一个有趣的标志是 PplDebuggingToken。 该标志是隐藏的并且仅在不受限制的模式下可用。

此标志的存在立即引发了疑问:TTD 首先是围绕 Windows 7 和 8 构建的,然后在 Windows 8.1+ 上构建。 进程中添加了保护级别的概念,规定进程只能打开具有同等或较低保护级别的进程的句柄。 它是内核中 _EPROCESS 结构的一个简单字节,因此不能从用户模式直接修改。

保护级别字节的值是众所周知的,并在下表中进行了总结。

本地安全机构子系统 (lsass.exe) 在 Windows 上可以配置为作为 Protected Process Light 运行,旨在限制获得主机最大权限的入侵者的触及范围。 通过在内核级别执行操作,任何用户模式进程都无法打开 lsass 的句柄,无论其权限如何。

但 PplDebuggingToken 标志似乎表明并非如此。 如果存在这样的标志,那将是任何渗透测试人员/红队成员的梦想:一个(神奇的)令牌,可以让他们注入受保护的进程并记录它们、转储它们的内存等。 命令行解析器似乎暗示命令标志的内容仅仅是一个宽字符串。 这可能是 PPL 后门吗?

追逐 PPL 调试令牌

回到 ttdrecord.dll, PplDebuggingToken 命令行选项与创建 TTD 会话所需的所有选项一起被解析并存储在上下文结构中。 该值可以追溯到多个位置,其中一个有趣的位置是 TTD::InitializeForAttach 中,其行为在以下伪代码中简化:

ErrorCode TTD::InitializeForAttach(TtdSession *ctx)
{
  [...]
  EnableDebugPrivilege(GetCurrentProcess()); // [1]
  HANDLE hProcess = OpenProcess(0x101040u, 0, ctx->dwProcessId);
  if(hProcess == INVALID_HANDLE_VALUE)
 {
    goto Exit;
  }
  [...]
  HMODULE ModuleHandleW = GetModuleHandleW(L"crypt32.dll");
  if ( ModuleHandleW )
  pfnCryptStringToBinaryW = GetProcAddress(ModuleHandleW, "CryptStringToBinaryW"); // [2]

  if ( ctx->ProcessDebugInformationLength ) // [3]
  {
DecodedProcessInformationLength = ctx->ProcessDebugInformationLength;
DecodedProcessInformation = std::vector<unsigned char>(DecodedProcessInformationLength);
wchar_t* b64PplDebuggingTokenArg = ctx->CmdLine_PplDebugToken;
if ( *pfnCryptStringToBinaryW )
{
  if( ERROR_SUCCESS == pfnCryptStringToBinaryW( // [4]
                      b64PplDebuggingTokenArg,
                      DecodedProcessInformationLength,
                      CRYPT_STRING_BASE64,
                      DecodedProcessInformation.get(),
                      &DecodedProcessInformationLength,
                      0, 0))
  {
    Status = NtSetInformationProcess( // [5]
               NtGetCurrentProcess(),
               ProcessDebugAuthInformation,
               DecodedProcessInformation.get(),
               DecodedProcessInformationLength);
  }
[...]

为当前进程启用 SeDebugPrivilege 标志([1])并获取要附加到的进程的句柄([2])后,该函数解析用于执行字符串操作的导出通用函数:crypt32!CryptStringToBinaryW。 在这种情况下,如果命令行提供了 PplDebuggingToken 上下文选项的 base64 编码值([3]、[4]),它将用于解码该值。 然后使用解码后的值来调用系统调用 NtSetInformationProcess(ProcessDebugAuthInformation) ([5])。 该令牌似乎没有在其他地方使用,这让我们仔细检查该系统调用。

RS4中添加了进程信息类 ProcessDebugAuthInformation。 快速查看 ntoskrnl 可以发现,此系统调用只是将缓冲区传递给位于 ci.dll 中的 CiSetInformationProcess,ci.dll 是代码完整性驱动程序 DLL。 然后,缓冲区通过完全控制的参数传递给 ci!CiSetDebugAuthInformation。

下图从高层次总结了 TTD 执行流程中发生这种情况的位置。

CiSetDebugAuthInformation 中的执行流程非常简单:将包含 base64 解码的 PplDebuggingToken 的缓冲区及其长度作为解析和验证的参数传递给 ci!SbValidateAndParseDebugAuthToken。 如果验证成功,并且经过一些额外的验证后,执行系统调用的进程的句柄(请记住我们仍在处理系统调用 nt!NtSetInformationProcess)将插入到进程调试信息对象中,然后存储在全局列表条目中。

但这有什么有趣的呢? 因为此列表仅在一个位置访问:在 ci!CiCheckProcessDebugAccessPolicy 中,并且此函数在 NtOpenProcess 系统调用期间到达。 并且,正如之前新发现的标志的名称所暗示的那样,任何 PID 位于该列表中的进程都会绕过保护级别强制执行。 通过在该列表上设置访问断点,在KD会话中实际上已确认了这一点(在我们的 ci.dll 版本中,该断点位于 ci+364d8)。 我们还在LSASS 上启用了 PPL ,并编写了一个可以触发 NtOpenProcess 系统调用的简单 PowerShell 脚本:

通过中断 nt!PspProcessOpen 中对 nt!PsTestProtectedProcessIncompatibility 的调用,我们可以确认我们的 PowerShell 进程尝试以 lsass.exe 为目标, 这是一个 PPL 过程:

现在通过强制调用 nt!PsTestProtectedProcessIncompatibility 的返回值来确认 PplDebuggingToken 参数将做什么的初步理论:

我们在调用 nt!PsTestProtectedProcessIncompatibility (仅调用 CI!CiCheckProcessDebugAccessPolicy)之后的指令处中断,并强制返回值为 0 (如前所述,值为 1 表示不兼容):

成功! 尽管 LSASS 是 PPL,但我们还是获得了它的句柄,证实了我们的理论。 总而言之,如果我们能够找到一个“有效值”(我们很快就会深入研究它),它将通过 ci!CiSetDebugAuthInformation() 中 SbValidateAndParseDebugAuthToken() 的检查,这样我们就可以获得通用的 PPL 绕过。 如果这听起来好得令人难以置信,那主要是因为它确实如此——但要证实这一点需要更好地了解 CI.dll 正在做什么。

了解代码完整性策略

基于代码完整性的限制(例如 AppLocker 使用的限制)可以通过策略强制执行,这些策略的可读形式是 XML 文件。 有两种类型的政策:基本政策和补充政策。 您可以在“C:\Windows\schemas\CodeIntegrity\ExamplePolicies”中找到 XML 格式的基本策略示例。 这是基本策略的 XML 格式(取自“C:\Windows\schemas\CodeIntegrity\ExamplePolicies\AllowAll.xml”),它以纯文本形式清楚地揭示了我们感兴趣的大部分细节。

<?xml version="1.0" encoding="utf-8"?>
<SiPolicy xmlns="urn:schemas-microsoft-com:sipolicy">
<VersionEx>1.0.1.0</VersionEx>
<PolicyID>{A244370E-44C9-4C06-B551-F6016E563076}</PolicyID>
<BasePolicyID>{A244370E-44C9-4C06-B551-F6016E563076}</BasePolicyID>
<PlatformID>{2E07F7E4-194C-4D20-B7C9-6F44A6C5A234}</PlatformID>
<Rules>
<Rule><Option>Enabled:Unsigned System Integrity Policy</Option></Rule>
<Rule><Option>Enabled:Advanced Boot Options Menu</Option></Rule>
<Rule><Option>Enabled:UMCI</Option></Rule>
<Rule><Option>Enabled:Update Policy No Reboot</Option></Rule>
</Rules>
<!--EKUS-- >
<EKUs />
<!--File Rules-- >
<FileRules>
<Allow ID="ID_ALLOW_A_1" FileName="*" />
<Allow ID="ID_ALLOW_A_2" FileName="*" />
</FileRules>
<!--Signers-- >
<Signers />
<!--Driver Signing Scenarios-- >
<SigningScenarios>
<SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1" FriendlyName="Auto generated policy on 08-17-2015">
  <ProductSigners>
    <FileRulesRef><FileRuleRef RuleID="ID_ALLOW_A_1" /></FileRulesRef>
  </ProductSigners>
</SigningScenario>
<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS" FriendlyName="Auto generated policy on 08-17-2015">
  <ProductSigners>
    <FileRulesRef><FileRuleRef RuleID="ID_ALLOW_A_2" /></FileRulesRef>
  </ProductSigners>
</SigningScenario>
</SigningScenarios>
<UpdatePolicySigners />
<CiSigners />
<HvciOptions>0</HvciOptions>
<Settings>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Name">
  <Value><String>AllowAll</String></Value>
</Setting>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Id">
  <Value><String>041417</String></Value>
</Setting>
</Settings>
</SiPolicy>

可以使用 ConvertFrom-CiPolicy PowerShell cmdlet 将 XML 格式的策略编译为二进制格式:

基本策略允许细粒度,能够通过名称、路径、哈希或签名者(有或没有特定的EKU )进行限制;而且也可以在其操作模式(审计或强制)中进行限制。

补充策略被设计为基础策略的扩展,以提供更大的灵活性,例如允许将策略应用于(或不应用于)特定的一组工作站或服务器。 因此,它们更加具体,但也可以比基本政策更加宽松。 有趣的是,在 2016 年之前,补充政策 并不绑定到特定设备 ,允许绕过媒体 广泛报道 MS16-094 MS16-100 所修复的其他缓解措施。

记住这些信息,就可以更清晰地回到 ci!SbValidateAndParseDebugAuthToken:该函数基本上遵循三个步骤:1. 调用 ci!SbParseAndVerifySignedSupplementalPolicy 来解析来自系统调用的输入缓冲区,并确定它是否是有效签名的补充策略 2. 调用 ci!SbIsSupplementalPolicyBoundToDevice 来将补充策略中的 DeviceUnlockId 与当前系统的 DeviceUnlockId 进行比较;可以使用带有 GUID {EAEC226F-C9A3-477A-A826-DDC716CDC0E3}的系统调用 NtQuerySystemEnvironmentValueEx 轻松检索这些值 3. 最后,从策略中提取两个变量:一个对应于保护级别的整数(DWORD)和一个(UNICODE_STRING)调试授权。

由于可以制作策略文件(通过 XML 或 PowerShell 脚本),因此步骤 3 不是问题。 第 2 步也不是,因为只要我们拥有 SeSystemEnvironmentPrivilege 权限,就可以使用系统调用NtSetSystemEnvironmentValueEx({EAEC226F-C9A3-477A-A826-DDC716CDC0E3})伪造 DeviceUnlockId。 但需要注意的是,UnlockId 是一个易失性值,重启后将会恢复。

但是,绕过步骤 1 几乎是不可能的,因为它需要: - 拥有特定OID 为 1.3.6.1.4.1.311.10.3.6的 Microsoft 所拥有证书的私钥(即 - MS NT5 Lab (szOID_NT5_CRYPTO)) - 并且上述证书不得被撤销或过期

那么,我们该怎么办呢? 我们现在已经确认,与传统观点相反,PPL 进程可以由另一个进程打开,而无需额外加载内核驱动程序。 然而,还应该强调的是,这种用例是小众的,因为只有微软(从字面上看)掌握着针对特定机器使用这种技术的关键。 尽管如此,这种情况仍然是利用 CI 进行调试的一个很好的例子。

进攻性 TTD

注意:提醒一下,TTD.exe 需要提升的权限,下面讨论的所有技术都假定这一点。

在整个研究过程中,我们发现了 TTD 的一些潜在有趣的进攻和防御用例。

跟踪 != 调试

TTD 不是调试器! 因此,对于执行基本反调试检查的进程,它将完美地不被发现,例如使用 IsDebuggerPresent()(或任何其他依赖于 PEB.BeingDebugged 的方式)。 以下屏幕截图通过将 TTD 附加到一个简单的记事本进程来演示此细节:

从调试器中,我们可以检查记事本 PEB 中的 BeingDebugged 字段,该字段显示未设置标志:

ProcLaunchMon 的奇怪案例

TTD 提供的另一个有趣的技巧是滥用内置的 Windows 驱动程序 ProcLaunchMon.sys。 当作为服务运行时(即 在 TTDService.exe 中,ttdrecord.dll 将创建服务实例、加载驱动程序并与 .\com_microsoft_idna_ProcLaunchMon 上可用的设备进行通信,以注册新跟踪的客户端。

驱动程序本身将用于监视 TTD 服务创建的新进程,然后直接从内核暂停这些进程,从而绕过仅使用创建标志 CREATE_SUSPENDED 监视进程创建的任何保护(例如如此处所述)。 我们为这项研究开发了一个基本的设备驱动程序客户端,可以在这里找到。

创建转储文件工具

另一个有趣的事实:尽管它不是 TTD 的严格组成部分,但 WinDbgX 包提供了一个 .NET 签名的二进制文件,其名称完美地概括了其功能:createdump.exe。 该二进制文件位于“C:\Program Files\WindowsApps\Microsoft.WinDbg_*\createdump.exe”。

该二进制文件可用于对作为参数提供的进程的上下文进行快照和转储,以及其他LOLBAS的直接谱系。

这再次强调了需要避免依赖静态签名和文件名黑列表条目来防范凭证转储等攻击,而应采用更为稳健的方法,如RunAsPPLCredential GuardElastic Endpoint 的 Credential Hardening

防御型 TTD

阻断 TTD

虽然 TTD 是一项非常有用的功能,但需要在非开发或测试机器(例如生产服务器或工作站)上启用它的情况很少见。 尽管在撰写本文时这似乎在很大程度上没有记录,但 ttdrecord.dll 允许提前退出场景,只需创建或更新位于“HKEY_LOCAL_MACHINE\Software\Microsoft\TTD”下的注册表项,并将 DWORD32 值 RecordingPolicy 更新为 2。进一步尝试使用任何 TTD 服务(TTD.exe, TTDInject.exe, 运行以下命令将停止服务端 TTDService.exe:

检测 TTD

对于所有环境来说,防止使用 TTD 可能过于极端 — — 但是,存在几种检测 TTD 使用的指标。 被跟踪的进程具有以下属性:

  • 一个线程将运行来自 TTDRecordCPU.dll 的代码, 可以使用简单的内置 Windows 命令进行验证:tasklist /m TTDRecordCPU.dll
  • 尽管可以绕过这一问题,记录进程的父 PID(如果启用了递归跟踪,则是第一个进程)将是 TTD.exe 本身:

  • 此外,_KPROCESS.InstrumentationCallback 指针将被设置为位于可执行文件的 TTDRecordCPU.dll BSS 部分:

因此,可以通过用户模式和内核模式的方法从 TTD 检测跟踪。

结论

这就是针对 TTD 的“周内”研究的第一部分。 深入研究 TTD 生态系统的内部,可以发现一些非常有趣、鲜为人知的 Windows 内置机制,这些机制对于使 TTD 适用于某些边缘情况(例如 PPL 进程的跟踪)是必需的。

尽管这项研究没有揭示针对 PPL 进程的新秘密后门,但它确实展示了 Windows 中内置的一种未经探索的技术。 无论如何,这项研究强调了基于强加密模型(这里通过 CI.dll)的重要性,以及它在充分实施时如何带来很大的灵活性——同时保持高水平的安全性。

该系列的第二部分将较少地注重研究,而更多地注重实践,我们将发布一款作为 On-Week 的一部分开发的小工具。 这有助于使用 Windows 沙盒通过 TTD 进行二进制分析的过程。

致谢

由于这项研究已经结束并且文章正在进行中,作者意识到涵盖类似主题的研究和关于相同技术(PPL 调试令牌)的发现。 该研究由 Lucas George(来自 Synacktiv 公司)进行,他在SSTIC 2022上展示了他的研究成果。