时间旅行调试:从过去引爆调试革命

本文深入介绍微软开发的时间旅行调试(TTD)技术,包括其架构、记录与回放机制、追踪文件格式及JavaScript自动化应用,帮助安全研究者和开发者高效进行漏洞分析和程序调试。

时间旅行调试:它是一场爆炸!(来自过去)

Microsoft安全响应中心(MSRC)致力于快速评估外部报告的漏洞,但如果必须与研究人员确认重现步骤或环境的细节以重现漏洞,可能会浪费时间。微软已公开提供我们的“时间旅行调试”(TTD)工具,使安全研究人员能够轻松提供完整的重现,缩短调查时间,并可能贡献更高的奖金(参见“Microsoft漏洞赏金计划的报告质量定义”)。我们内部也使用它——它使我们能够以常规调试器一半的时间找到复杂软件问题的根本原因。如果您想知道在哪里可以获取TTD工具以及如何使用它,这篇博客文章就是为您准备的。

理解时间旅行调试

无论您称之为“无时间调试”、“记录-回放调试”、“反向调试”还是“时间旅行调试”,都是同一个想法:记录程序执行的能力。一旦有了这个记录,您可以向前或向后导航,并与同事分享。更好的是,执行跟踪是确定性的记录;每个人在同一时间看到相同的行为。当开发人员收到TTD跟踪时,他们甚至不需要重现问题就可以在执行跟踪中旅行,他们只需导航跟踪文件。

通常与时间旅行调试相关的三个关键组件:

  • 一个记录器,您可以将其想象为摄像机,
  • 一个跟踪文件,您可以将其想象为摄像机生成的记录文件,
  • 一个回放器,您可以将其想象为电影播放器。

老式调试器

调试器并不新鲜,调试问题的过程几十年来没有 drastically 改变。过程通常如下:

  1. 在调试器下观察行为。在此步骤中,您重新创建与错误发现者类似的环境。可能很简单,如在您的机器上运行一个简单的概念验证程序并观察错误检查,也可能很复杂,如设置具有特定软件配置的整个基础设施,只是为了能够执行有问题的代码。而且这还是在错误报告准确且详细 enough 以正确设置环境的情况下。
  2. 理解问题发生的原因。这就是调试器的作用。无论架构和平台如何,您对调试器的期望是能够精确控制目标的执行(单步跳过、单步进入各种粒度级别:指令、源代码行)、设置断点、编辑内存以及编辑处理器上下文。这组基本功能使您能够完成工作。但成本通常很高。很多次重现问题,很多次单步进入,很多次“哎呀……我不应该单步跳过,让我们重新开始”。浪费且低效。

无论您是报告漏洞的研究人员还是确认漏洞的团队成员,时间旅行调试都可以帮助调查快速进行,并以最少的来回确认细节。

高级概述

微软开发的技术称为“TTD”,即时间旅行调试。它源于2006年左右的Microsoft Research(参见“程序执行指令级跟踪和分析框架”),后来由微软的调试团队改进并产品化。该项目依赖于代码模拟来记录回放所需的所有事件,以重现完全相同的执行。完全相同的指令序列,完全相同的输入和输出。模拟器跟踪的数据包括内存读取、寄存器值、线程创建、模块加载等。

记录/回放

记录软件CPU,TTDRecordCPU.dll,被注入到目标进程中,并劫持线程的控制流。模拟器将本机指令解码为内部自定义中间语言(模拟简单RISC指令),缓存块,并执行它们。从现在开始,它携带这些线程的执行 forward,并在事件发生时调度回调,例如:当指令被翻译时,等等。这些回调允许跟踪文件写入器组件收集软件CPU基于跟踪文件回放执行所需的信息。

回放软件CPU,TTDReplayCPU.dll,与记录CPU共享大部分相同的代码库, except 它不是从目标内存读取数据,而是直接从跟踪文件加载数据。这使您能够以完全保真度回放程序的执行,而无需运行程序。

跟踪文件

跟踪文件是您文件系统上的常规文件,以“run”扩展名结尾。该文件使用自定义文件格式和压缩来优化文件大小。您还可以将此文件视为充满丰富信息的数据库。为了非常快速地访问调试器所需的信息,“WinDbg Preview”在您第一次打开跟踪文件时创建索引文件。通常需要几分钟来创建。通常,此索引大约是原始跟踪文件大小的一到两倍。例如,在我的机器上,ping.exe的跟踪生成一个37MB的跟踪文件和一个41MB的索引文件。大约有1,973,647条指令(每条指令约132位)。请注意,在这种情况下,跟踪文件如此之小,以至于跟踪文件的内部结构占用了大部分空间开销。较大的执行跟踪通常每条指令包含约1到2位。

使用WinDbg Preview记录跟踪

现在您熟悉了TTD的各个部分,这里是如何使用它们。 获取TTD:TTD目前通过“WinDbg Preview”应用程序在Windows 10上可用,您可以在Microsoft商店中找到:https://www.microsoft.com/en-us/p/windbg-preview/9pgjgd53tn86?activetab=pivot:overviewtab。

安装应用程序后,“时间旅行调试 - 记录跟踪”教程将引导您记录第一个执行跟踪。

使用TTD构建自动化

Windows调试器的最新改进是添加了调试器数据模型以及通过JavaScript(以及C++)与之交互的能力。数据模型的细节超出了本博客的范围,但您可以将其视为一种向用户和调试器扩展 both 消费和暴露结构化数据的方式。TTD通过引入非常强大和独特的功能扩展了数据模型,这些功能在@$cursession.TTD和@$curprocess.TTD节点下可用。

TTD.Calls是一个函数,允许您回答诸如“给我foo!bar被调用的每个位置”或“跟踪中是否有调用foo!bar返回10”的问题。更好的是,像数据模型中的每个集合一样,您可以使用LINQ操作符查询它们。以下是TTD.Calls对象的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
0:000> dx @$cursession.TTD.Calls("msvcrt!write").First()
@$cursession.TTD.Calls("msvcrt!write").First()
    EventType        : Call
    ThreadId         : 0x194
    UniqueThreadId   : 0x2
    TimeStart        : 1310:A81 [Time Travel]
    TimeEnd          : 1345:14 [Time Travel]
    Function         : msvcrt!_write
    FunctionAddress  : 0x7ffec9bbfb50
    ReturnAddress    : 0x7ffec9be74a2
    ReturnValue      : 401
    Parameters

API完全隐藏了ISA specific 细节,因此您可以构建架构独立的查询。

TTD.Calls:重建stdout

为了演示利用这些功能有多强大和容易,我们记录“ping.exe 127.0.0.1”的执行,并从记录中重建控制台输出。 用JavaScript构建这个非常容易:

  • 迭代对msvcrt!write的每次调用,按时间位置排序,
  • 读取第二个参数指向的多个字节(数量在第三个参数中),
  • 显示累积结果。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
'use strict';
function initializeScript() {
    return [new host.apiVersionSupport(1, 3)];
}
function invokeScript() {
    const logln = p => host.diagnostics.debugLog(p + '\n');
    const CurrentSession = host.currentSession;
    const Memory = host.memory;
    const Bytes = [];
    for(const Call of CurrentSession.TTD.Calls('msvcrt!write').OrderBy(p => p.TimeStart)) {
        Call.TimeStart.SeekTo();
        const [_, Address, Count] = Call.Parameters;

    }
    logln(Bytes.filter(p => p != 0).map(
        p => String.fromCharCode(p)
    ).join(''));
}

TTD.Memory:找到每个触及LastErrorValue的线程

TTD.Memory是一个强大的API,允许您查询跟踪文件以获取内存范围内某些类型(读、写、执行)的内存访问。每次内存查询的结果对象如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
0:000> dx @$cursession.TTD.Memory(0x000007fffffde068, 0x000007fffffde070, "w").First()
@$cursession.TTD.Memory(0x000007fffffde068, 0x000007fffffde070, "w").First()
    EventType        : MemoryAccess
    ThreadId         : 0xb10
    UniqueThreadId   : 0x2
    TimeStart        : 215:27 [Time Travel]
    TimeEnd          : 215:27 [Time Travel]
    AccessType       : Write
    IP               : 0x76e6c8be
    Address          : 0x7fffffde068
    Size             : 0x4
    Value            : 0x0

此结果标识了完成的内存访问类型、开始和结束的时间戳、访问内存的线程、访问的内存地址、访问的位置以及读/写/执行的值。 为了演示其 power,让我们创建另一个脚本,收集每次应用程序写入当前线程环境块中的LastErrorValue时的调用堆栈:

  • 迭代对&@$teb->LastErrorValue的每次内存写入访问,
  • 旅行到目的地,转储当前调用堆栈,
  • 显示结果。
 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
'use strict';
function initializeScript() {
    return [new host.apiVersionSupport(1, 3)];
}
function invokeScript() {
    const logln = p => host.diagnostics.debugLog(p + '\n');
    const CurrentThread = host.currentThread;
    const CurrentSession = host.currentSession;
    const Teb = CurrentThread.Environment.EnvironmentBlock;
    const LastErrorValueOffset = Teb.targetType.fields.LastErrorValue.offset;
    const LastErrorValueAddress = Teb.address.add(LastErrorValueOffset);
    const Callstacks = new Set();
    for(const Access of CurrentSession.TTD.Memory(
        LastErrorValueAddress, LastErrorValueAddress.add(8), 'w'
    )) {
        Access.TimeStart.SeekTo();
        const Callstack = Array.from(CurrentThread.Stack.Frames);
        Callstacks.add(Callstack);
    }
    for(const Callstack of Callstacks) {
        for(const [Idx, Frame] of Callstack.entries()) {
            logln(Idx + ': ' + Frame);
        }
        logln('----');
    }
}

请注意,还有更多TTD specific 对象可用于获取与跟踪中发生的事件、线程生命周期等相关的信息。所有这些都在“时间旅行调试对象介绍”页面上有文档记录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
0:000> dx @$curprocess.TTD.Lifetime
@$curprocess.TTD.Lifetime                 : [F:0, 1F4B:0]
    MinPosition      : F:0 [Time Travel]
    MaxPosition      : 1F4B:0 [Time Travel]
0:000> dx @$curprocess.Threads.Select(p => p.TTD.Position)
@$curprocess.Threads.Select(p => p.TTD.Position)
    [0x194]          : 1E21:104 [Time Travel]
    [0x7e88]         : 717:1 [Time Travel]
    [0x5fa4]         : 723:1 [Time Travel]
    [0x176c]         : B58:1 [Time Travel]
    [0x76a0]         : 1938:1 [Time Travel]

总结

时间旅行调试是安全软件工程师的强大工具,也可能有益于恶意软件分析、漏洞狩猎和性能分析。我们希望您发现这篇TTD介绍有用,并鼓励您使用它为发现的安全问题创建执行跟踪。TTD生成的跟踪文件压缩得很好;我们建议在使用7zip(通常将文件缩小到原始大小的约10%)之前将其上传到您喜欢的文件存储服务。 Axel Souchet Microsoft安全响应中心(MSRC)

常见问题解答

我可以在回放时编辑内存吗?

不行。由于记录器只保存回放程序中特定执行路径所需的内容,它没有保存足够的信息来能够重新模拟不同的执行。

为什么读取文件时看不到字节?

记录器只知道它模拟的内容。这意味着如果另一个实体(这里是NT内核,但也可能是另一个进程写入共享内存部分)将数据写入内存,模拟器无法知道。因此,如果目标程序从不读回这些值,它们将永远不会出现在跟踪文件中。如果后来读取,那么当模拟器再次获取内存时,它们的值将在那时可用。这是团队计划很快改进的领域,所以请关注这个空间😊。

我需要私有符号或源代码吗?

您不需要源代码或私有符号来使用TTD。记录器使用本机代码,不需要任何额外的东西来完成其工作。如果私有符号和源代码可用,调试器将使用它们并提供与使用源/符号调试时相同的体验。

我可以记录内核模式执行吗?

TTD仅用于用户模式执行。

记录器支持自修改代码吗?

是的,它支持!

有任何已知的不兼容性吗?

有一些,您可以在“需要注意的事项”中阅读。

我需要WinDbg Preview来记录跟踪吗?

是的。截至今天,TTD记录器仅作为“WinDbg Preview”的一部分提供,只能从Microsoft Store下载。

参考

时间旅行调试

JavaScript / WinDbg / 数据模型

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