每年几次,Elastic Security Labs 的研究人员都可以自由选择并深入研究他们喜欢的项目,无论是单独还是作为一个团队。这段时间在内部称为“On-Week”项目。这是关于微软开发的时间旅行调试 (TTD) 技术的系列文章的第一篇,该技术在最近的 On-Week 会议期间得到了详细探讨。
尽管 TTD 已公开发布多年,但在信息安全社区中,对 TTD 及其潜力的认识被大大低估了。我们希望这个由两部分组成的系列文章能够帮助阐明 TTD 如何用于程序调试、漏洞研究和利用以及恶意软件分析。
这项研究首先涉及理解 TTD 的内部工作原理,然后评估一些有趣的适用用途。这篇文章将重点介绍研究人员如何深入了解 TTD,分享他们的方法以及一些有趣的发现。第二部分将详细介绍 TTD 在恶意软件分析中的适用用途以及与 Elastic Security 的集成。
背景
时间旅行调试是微软研究院开发的一种工具,它允许用户记录执行过程并自由导航到二进制文件的用户模式运行时。TTD 本身依赖于两项技术:用于二进制转换的 Nirvana 和用于跟踪读取/写入过程的 iDNA。自 Windows 7 以来,TTD 内部结构首次在公开发布的论文中进行了详细介绍。从那时起,微软和独立研究人员都对此进行了详细介绍。因此,我们不会深入探讨这两项技术的内部结构。相反,Elastic 研究人员调查了使 TTD 实现工作的生态系统,即执行文件、DLL 和驱动程序。这使得人们对 TTD 以及 Windows 本身产生了一些有趣的发现,因为 TTD 利用了一些(未公开的)技术来按预期在特殊情况下工作,例如受保护的进程。
但是为什么要调查 TTD 呢?除了纯粹的好奇心之外,该技术可能的预期用途之一很可能是在生产环境中发现错误。当错误难以触发或重现时,拥有“记录一次,始终重播”类型的环境有助于弥补该困难,这正是 TTD 与 WinDbg 结合使用时所实现的。
WinDbg 等调试工具一直是反转 Windows 组件时获取大量信息的来源,因为它们提供了额外的易于理解的信息,通常以纯文本形式提供。调试工具(尤其是调试器)必须与底层操作系统配合工作,这可能涉及调试接口和/或操作系统中先前未公开的功能。TTD 符合该模式。
高级概述
TTD 的工作方式是首先创建跟踪应用程序执行的每条指令的记录,并将其存储在数据库中(后缀为 .run)。可以使用 WinDbg 调试器随意重播记录的跟踪,该调试器在首次访问时将索引 .run 文件,从而可以更快地浏览数据库。为了能够跟踪任意进程的执行,TTD 会注入一个负责按需记录活动的 DLL,这使其可以通过生成进程来记录进程,但也可能附加到已运行的进程。
TTD 可以作为 MS Store 中 WinDbg Preview 包的一部分免费下载。它可以直接从 WinDbg Preview(又名 WinDbgX)使用,但它是一个独立组件,位于 C:\Program Files\WindowsApps\Microsoft.WinDbg_<版本号></版本号>_<架构>__8wekyb3d8bbwe\amd64\ttd
(x64 架构),我们将在本文中重点介绍。x86 和 arm64 版本也可在 MS Store 中下载。
该软件包包含两个 EXE 文件(TTD.exe 和 TTDInject.exe)和一些 DLL。这项研究的重点是主要 DLL,它负责与 Nirvana/iDNA 无关的所有内容(即负责会话管理、驱动程序通信、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 结构中的一个简单字节,因此无法从用户模式直接修改。
保护级别字节的值是众所周知的,并在下表中进行了总结。
Windows 上的本地安全授权子系统 (lsass.exe) 可以配置为以受保护进程轻 (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]))。 然后,解码后的值用于调用 syscall NtSetInformationProcess(ProcessDebugAuthInformation) ([5])。 该令牌似乎没有在其他任何地方使用,这使我们仔细检查了该系统调用。
进程信息类 ProcessDebugAuthInformation 在 RS4 中添加。 快速查看 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,并编写了一个简单的 PowerShell 脚本来触发 NtOpenProcess 系统调用
通过在 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 文件。 有两种类型的策略:基本策略和补充策略。 基本策略的示例可以在其 XML 格式的“C:\Windows\schemas\CodeIntegrity\ExamplePolicies”中找到。 这是基本策略在 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 进行比较;可以使用系统调用 NtQuerySystemEnvironmentValueEx 和 GUID {EAEC226F-C9A3-477A-A826-DDC716CDC0E3}
轻松检索这些值 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(即 - MS NT5 Lab (szOID_NT5_CRYPTO))的 Microsoft 所有证书的私钥 - 并且上述证书不得被吊销或过期
那么,这把我们带到哪里了? 现在我们已经确认,与传统观点相反,PPL 进程可以由另一个进程打开,而无需加载内核驱动程序的额外步骤。 但是,还应该强调的是,这种情况很特殊,因为只有 Microsoft(实际上)拥有将此技术用于非常有针对性的机器的密钥。 尽管如此,这种情况仍然是用于调试目的的 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 监视进程创建的任何保护(如此处所述)。 我们为这项研究开发了一个基本的设备驱动程序客户端,可以在此处找到。
CreateDump.exe
另一个有趣的事实:即使它不是 TTD 的严格组成部分,WinDbgX 包也提供了一个 .NET 签名的二进制文件,其名称完美地总结了它的功能:createdump.exe。这个二进制文件位于 “C:\Program Files\WindowsApps\Microsoft.WinDbg_*\createdump.exe”。
这个二进制文件可以用来快照并转储作为参数提供的进程的上下文,这与其他的 LOLBAS 一脉相承。
这再次强调了避免依赖静态签名和文件名阻止列表条目来防御诸如凭据转储之类的攻击,并倾向于更强大的方法,例如 RunAsPPL、Credential Guard 或 Elastic Endpoint 的凭据强化的必要性。
防御性 TTD
阻止 TTD
尽管 TTD 是一个非常有用的功能,但在非开发或测试机器(如生产服务器或工作站)上需要启用它的情况很少见。即使在撰写本文时,这似乎在很大程度上没有文档记录,但 ttdrecord.dll 允许通过简单地创建或更新位于 “HKEY_LOCAL_MACHINE\Software\Microsoft\TTD” 下的注册表项,并将 DWORD32 值 RecordingPolicy 更新为 2 来实现提前退出场景。进一步尝试使用任何 TTD 服务(TTD.exe、TTDInject.exe、TTDService.exe)将被停止,并且将生成 ETW 事件来跟踪尝试。
检测 TTD
阻止使用 TTD 可能对所有环境来说都太过极端 - 但是,存在一些指标可以检测 TTD 的使用。正在被跟踪的进程具有以下属性
- 一个线程将运行 TTDRecordCPU.dll 中的代码,这可以使用一个简单的内置 Windows 命令进行验证:tasklist /m TTDRecordCPU.dll
- 即使这可以被绕过,被记录进程的父 PID(或者在启用递归跟踪的情况下,第一个父 PID)将是 TTD.exe 本身
- 此外,_KPROCESS.InstrumentationCallback 指针将被设置为指向可执行文件的 TTDRecordCPU.dll BSS 部分
因此,可以通过用户模式和内核模式方法来实现检测 TTD 的跟踪。
结论
这结束了本次专注于 TTD 的“On-Week”研究的第一部分。深入研究 TTD 生态系统的内部机制揭示了一些非常有趣、鲜为人知的 Windows 内置机制,这些机制是使 TTD 适用于某些极端情况(例如跟踪 PPL 进程)所必需的。
尽管这项研究没有揭示针对 PPL 进程的新秘密后门,但它确实展示了一种内置于 Windows 的未探索技术来做到这一点。如果有什么的话,这项研究强调了基于强大密码学(此处通过 CI.dll)的模型的重要性,以及在适当实施时,它如何在保持高安全性的同时带来很大的灵活性。
本系列的第二部分将更少以研究为导向,更多的是实践操作,发布一个我们也在 On-Week 中开发的小工具。这有助于通过 TTD 使用 Windows 沙盒进行二进制分析的过程。
致谢
由于这项研究已经结束并且文章正在撰写中,作者了解到了一项涵盖类似主题的研究以及关于同一技术(PPL 调试令牌)的发现。该研究由 Lucas George(来自 Synacktiv 公司)进行,他在 SSTIC 2022 上展示了他的发现。