65.9K
CodeProject 正在变化。 阅读更多。
Home

NTFS 日志文件解析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2016年1月20日

CPOL

7分钟阅读

viewsIcon

31342

downloadIcon

729

用于解析 NTFS 日志文件的工具

引言

NTFS 文件系统的关键特性之一是其可恢复性。与修复程序(例如 chkdsk.exe)一起,NTFS 即使在断电等严重故障后也能保证数据一致性。为此,NTFS 采用了一种称为日志记录的特殊机制。由于文件系统更改不能在创建后立即写入,因此日志记录会在一个名为 $LogFile 的文件中跟踪这些更改。理解 NTFS 日志文件是卷修复过程的第一步。

那么,日志文件中存储了哪些数据?据我所知,日志文件包含所有更改 NTFS 结构的内容,从 MFT 数据流到位图流再到索引分配。此外,还必须存储描述日志状态的一些元数据,例如最后一个日志记录是哪个,或者哪些记录尚未刷新到卷。有一点可以肯定的是,您不会在那里找到对非驻数据属性的更改(MFT 表除外)。

日志文件结构

Logfile 是 MFT 表的第三个文件,其大小因平台而异。通常,出于性能原因,文件不会碎片化。在记录过程中,日志文件不会扩展,而是会循环 - 溢出日志文件末尾的记录会从开头继续,覆盖旧的记录。因此,一个 64MB 的日志文件可以容纳例如 3 小时的互联网浏览所做的更改。

写入的每条记录都有其 **日志序列号** - **LSN**。LSN 可以从记录文件偏移量和序列号轻松计算得出。据我所知,每次日志文件循环或卷挂载时,序列号都会增加。此数字用于区分新旧记录。

日志文件的基本结构可以描述为一组页面,其中前两页保留用于元数据,其余页面用于日志记录。让我们从元数据开始。这些页面以 **RSTR** (restart) 签名开头,并包含一些基本信息

  • 系统页面大小(分区创建者的)
  • 日志页面大小(可以等于系统页面大小)
  • 日志客户端列表。我从未见过超过一个客户端,即 NTFS 本身
  • 每个客户端的 **快照** LSN
  • 日志文件大小
  • 版本

重启页面中的重要信息是客户端快照的 LSN。快照由日志功能定期拍摄并写入日志文件。稍后,此快照可在卷恢复过程中使用。日志文件使用两个重启页面,这是因为当写入第一个页面时发生故障,第二个页面有备份(反之亦然)。当 PC 正常关机时,重启页面应相等。

剩余页面用于已记录的记录。从开头开始是所谓的缓冲区区域。每条记录首先写入缓冲区区域的一个页面,然后将整个页面复制到日志中的原始位置。

< metadata > < buffering >  <    log area     >
+-----+-----+-----+-----+ ~ +-----+-----+-----+ ~
|RSTR |RSTR |RCRD |RCRD |   |RCRD |RCRD |RCRD |
+-----+-----+-----+-----+ ~ +-----+-----+-----+ ~
   |                                ^
   +------ Snapshot LSN ------------+

每个包含记录的页面(**RCRD** 签名)都以一个标题开头,该标题告诉我们以下信息

  • 页面上最后一条记录的 LSN
  • 在此页面上结束的最后一条记录的 LSN(有时可能与上一条相同)
  • 页面计数和页面位置

这些信息用于组织目的。在每个页面上,更改记录都链接到事务。写入日志文件的每项更改都包含两部分:日志记录头客户端数据。日志记录头包含有关当前事务的信息

  • 事务 ID
  • 事务中上一条记录的 LSN
  • 在此事务中应撤消的下一条记录的 LSN
  • 客户端数据的长度

客户端数据紧跟在日志记录头之后,包含以下信息

  • 重做此更改时要执行的操作类型 - 重做操作
  • 撤消此更改时要执行的操作类型 - 撤消操作
  • 重做操作的数据
  • 撤消操作的数据
  • 操作将应用的目标属性
  • 操作将应用的目标虚拟集群编号 (VCN)
  • VCN 中的目标
  • 目标记录偏移量
  • 目标属性偏移量

每次事务后,卷都应处于一致状态。顶层操作,例如向文件追加内容,可以在下一个事务中记录(所有操作的列表在源代码中)。

  1. 设置属性的新大小
  2. 更新属性值
  3. Commit

这应该就是关于基本日志文件结构的大部分内容了。现在我想稍微提一下快照的结构。它的 LSN 可以在重启页面中找到,并具有以下结构

  • 检查点记录的 LSN。检查点记录是恢复过程分析过程的入口点。下面提到的表不反映在此检查点记录之后写入的记录的状态。
  • 打开的属性表,保存了快照拍摄时正在使用的非驻属性的信息。
  • 属性名称表,包含打开的属性表中的属性名称。
  • 脏页表,包含快照拍摄时已脏(已修改)的每个页面的一个条目。
  • 事务表,包含快照拍摄时每个未提交事务的一个条目。

Using the Code

首先,我们需要一个日志文件。但是,它是隐藏的,无法直接访问。这就是为什么我还制作了一个小的 dump_logfile.py 脚本来将内容转储到常规文件中。

> dump_logfile.py -p \\.\C:

日志文件处理保存在 Logfile.py 模块中。出于演示目的,项目包含一个 main 文件,我在其中放置了 LogFile 类的基本用法。因此,一旦日志文件准备就绪,我们就可以从前两个页面检索元数据。

with open(args.file[0], "rb+") as logfile_stream:
    lrb, lrbb = logfile.get_lsn_restart_blocks(logfile_stream)

返回值 lrblrbb 包含重启元数据。第一个始终包含当前 LSN 较高的上下文。第二个作为备份。LSN 快照是我们的起点。从该 LSN 开始,我们将开始解析日志文件中的其余记录。

将上下文的一个传递给 Logfile 类。您可以传递最新的一个 - lrb ,或者备份 lrbb。如果您使用第一个,脚本可能会崩溃,因为一些必需的数据可能未写入日志文件。因此,我建议使用备份。

journal = logfile.LogFile(logfile_stream, lrbb)

传递重启上下文很重要,因为从其结构中,LogFile 类知道如何处理输入文件并使用 LSN 数字。

现在我们可以例如将每个日志页面的最后一个 LSN 转储到文件中。

npages = (journal.lcb.file_size - logfile_stream.tell()) / \
    journal.lcb.log_page_size

for _ in range(int(npages)):
    page = logfile_stream.read(journal.lcb.log_page_size)

    page_header = layout.RecordPageHeader.from_buffer_copy(page)

    pages_stream.write(
        "{:>10};{:>18};{:>18};{}\n".format(
            hex(logfile_stream.tell() - journal.lcb.log_page_size),
            hex(page_header.copy.last_lsn),
            hex(page_header.last_end_lsn),
            hex(page_header.flags)))

在我的情况下,输出如下所示

Page offset;Last LSN;Last end LSN;Flags
...
  0x49a000;       0x14b0935e3;       0x14b0935cb;0x3
  0x49b000;       0x14b09374b;       0x14b09374b;0x3
  0x49c000;       0x1490939e3;       0x1490939e3;0x1
  0x49d000;       0x149093bf2;       0x149093bf2;0x1
...

我的日志文件目前正在向文件偏移量 0x49b000 处的页面进行日志记录。几秒钟后,情况就不再如此,因为该页面不再包含以 0x149 开头的记录。

  0x49a000;       0x14b0935e3;       0x14b0935cb;0x3
  0x49b000;       0x14b0937ca;       0x14b0937bf;0x3
  0x49c000;       0x14b0939f0;       0x14b0939df;0x3
  0x49d000;       0x14b093bf3;       0x14b093b45;0x3
  0x49e000;       0x14b093dbe;       0x14b093db3;0x1
  0x49f000;       0x14b093f12;       0x14b093f12;0x3
  0x4a0000;       0x1490941fa;       0x1490941e9;0x1
  0x4a1000;       0x1490943e3;       0x1490943e3;0x1

要观察写入日志文件的事务,只需从转储的页面中选择一个 LSN 并将其传递给日志记录生成器。

with open(pages_file, "w") as records_stream:
    records_stream.write("""LSN;Previous LSN;Undo next LSN;"""
                         """Redo operation;Undo operation;Transaction\n""")

    for record, data in journal.records(0x1480557170):
        client_header = layout.ClientLogHeader.from_buffer_copy(data)

        records_stream.write(
            "{:>18};{:>18};{:>18};{};{};{}\n".format(
                hex(record.this_lsn),
                hex(record.client_previous_lsn),
                hex(record.client_undo_next_lsn),
                layout.LOG_OPERATION.get(client_header.redo_op),
                layout.LOG_OPERATION.get(client_header.undo_op),
                record.transaction_id))

输出可能有些令人困惑,但它显示了使用 LSN 数字链接到事务的更改记录。最后一个数字是事务 ID,尽管它可能对每个记录都相同。这是因为没有并行事务。

LSN;Previous LSN;Undo next LSN;Redo operation;Undo operation;Transaction
...
      0x1480569bf2;               0x0;               0x0;SetBitsInNonresidentBitMap;ClearBitsInNonresidentBitMap;24
      0x1480569c08;      0x1480569bf2;      0x1480569bf2;Noop;DeallocateFileRecordSegment;24
      0x1480569c14;      0x1480569c08;      0x1480569c08;AddIndexEntryAllocation;DeleteIndexEntryAllocation;24
      0x1480569c2c;      0x1480569c14;      0x1480569c14;InitializeFileRecordSegment;Noop;24
      0x1480569c5c;      0x1480569c2c;      0x1480569c2c;SetNewAttributeSizes;SetNewAttributeSizes;24
      0x1480569c6f;      0x1480569c5c;      0x1480569c5c;UpdateNonResidentValue;Noop;24
      0x1480569c84;      0x1480569c6f;      0x1480569c6f;SetNewAttributeSizes;SetNewAttributeSizes;24
      0x1480569c97;      0x1480569c84;               0x0;ForgetTransaction;CompensationLogRecord;24
...
      0x14805715e4;               0x0;               0x0;OpenAttributeTableDump;Noop;24
      0x1480571ff2;      0x14805715e4;      0x14805715e4;AttributeNamesDump;Noop;24
...

从输出中,我选择了两个事务(事务以 ForgetTransaction 操作结束)。第一个事务(0x1480569bf2)首先设置位图中的位(可能是从 MFT 分配新文件记录),然后更新另一个文件(父目录)的索引(0x1480569c14)。然后初始化新分配的文件记录(0x1480569c2c)并更新其标准属性的大小(0x1480569c84)。但这只是我的猜测:)。第二个事务(0x14805715e4)实际上是创建快照的开始。从打开的属性转储开始,然后是名称转储,下一条记录可能是脏页转储,最后一条是事务表转储。

链接

历史

我在代码中发现了一个错误,其中缓冲区页面的最后一个 LSN 被错误的 LSN 覆盖了。此错误也导致了在迭代日志记录时出现问题。代码已更新,该错误应已修复。

© . All rights reserved.