NTFS 日志文件解析器





5.00/5 (8投票s)
用于解析 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 中的目标块
- 目标记录偏移量
- 目标属性偏移量
每次事务后,卷都应处于一致状态。顶层操作,例如向文件追加内容,可以在下一个事务中记录(所有操作的列表在源代码中)。
- 设置属性的新大小
- 更新属性值
- 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)
返回值 lrb
和 lrbb
包含重启元数据。第一个始终包含当前 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 覆盖了。此错误也导致了在迭代日志记录时出现问题。代码已更新,该错误应已修复。