如何获取 VSS 快照的"下一个USN"日志条目?



创建VSS快照后,我希望能够查询USN日志。这是否可能,或者 USN 日志是否无法从 VSS 快照访问?

我的目标是能够在两个 VSS 快照之间的增量备份中使用 USN 日志。 备份过程将是

  1. 拍摄 VSS 快照并备份卷,记下每个文件的 USN 条目
  2. 。使用文件系统,添加/删除/修改文件
  3. 拍摄第二个 VSS 快照,然后使用 USN 日志检测在步骤 #2 期间更改的任何内容

我现在失败的是我试图在 VSS 快照上获取最高 USN 条目的部分

  1. 创建 VSS 快照
  2. 使用 CreateFile(\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy25( 打开快照
  3. 设备IO控制(FSCTL_QUERY_USN_JOURNAL( - 失败并显示 GLE:1179"卷更改日志未激活">

我可以从命令行模拟这一点,如下所示

C:>vssadmin list shadows
vssadmin 1.1 - Volume Shadow Copy Service administrative command-line tool
(C) Copyright 2001-2005 Microsoft Corp.
Contents of shadow copy set ID: {54fc99fb-65f2-4558-8e12-9308979327f0}
   Contained 1 shadow copies at creation time: 5/10/2012 6:44:19 PM
      Shadow Copy ID: {a2d2c155-9916-47d3-96fd-94fae1c2f802}
         Original Volume: (T:)\?Volume{a420b1fa-9744-11e1-9082-889ffaf52b70}
         Shadow Copy Volume: \?GLOBALROOTDeviceHarddiskVolumeShadowCopy25
         Originating Machine: computer
         Service Machine: computer
         Provider: 'Microsoft Software Shadow Copy provider 1.0'
         Type: Backup
         Attributes: Differential

C:>fsutil usn queryjournal \?Volume{a420b1fa-9744-11e1-9082-889ffaf52b70}
Usn Journal ID   : 0x01cd2ebe9c795b57
First Usn        : 0x0000000000000000
Next Usn         : 0x000000000001b5f8
Lowest Valid Usn : 0x0000000000000000
Max Usn          : 0x7fffffffffff0000
Maximum Size     : 0x0000000000100000
Allocation Delta : 0x0000000000040000
C:>fsutil usn queryjournal \?GLOBALROOTDeviceHarddiskVolumeShadowCopy25
Error:  The volume change journal is not active.

如果可能的话,知道我做错了什么吗?

这个问题对我正在从事的项目非常重要,所以我终于(几乎(100%工作了。

注意:以下所有代码片段都是 C#

感谢Hannes de Jager之前的回答,他为我指出了正确的方向和文档,我现在可以从VSS快照或任何其他常规API无法使用的特殊设备中读取USN日志;就我而言,我的意思是使用VDDK(VMware SDK用于VM磁盘(装载的VMware快照。

我还重用或导入了来自伟大项目的代码:

  • C#中的USN日志浏览器,来自StCroixSkipper (http://www.dreamincode.net/forums/blog/1017-stcroixskippers-blog/(。仅使用官方 API 读取 USN(因此此处没有 VSS(,但提供了有用的 pinvoke 和 Win32 API 结构以及有关 USN 工作原理的一般信息

  • AlphaFS (https://github.com/alphaleonis/AlphaFS/( ,它模仿了System.IO命名空间的大部分,但允许访问特殊的窗口路径(VSS 快照、原始设备(并提供有用的扩展。

如果其他人感兴趣,我分享了我现在使用的代码,仍然处于相当粗糙的状态,但可以工作。

它是如何工作的?

首先,您必须获得所需的 Usn 期刊组合。它们作为隐藏条目中的 ADS(备用数据流(位于设备的根目录下。它们无法使用标准System.IO命名空间访问,这就是为什么我之前告诉我我使用了 AlphaFS 项目,但调用CreateFile()ReadFile()就足够了。

1/2

条目$Extend$UsnJrnl:$Max包含有关期刊当前状态的全局信息。最重要的部分是 USN 日志 ID(如果要比较多个 VSS 快照,可以使用它来检查日志是否尚未重置(和最低的有效 USN 日志序列号。

美国海军期刊结构:

  // can be directly extracted from $MAX entry using Bitconverter.ToUint64
 public struct USN_JOURNAL_DATA{
        public UInt64 MaximumSize; //offset 0
        public UInt64 AllocationDelta; // offset 8
        public UInt64 UsnJournalID; // offset 16
        public Int64 LowestValidUsn; // offset 24
    }
2

/2

条目$Extend$UsnJrnl:$J包含日记记录。它是一个稀疏文件,因此其磁盘使用率远低于其大小。

为了回答最初的问题,如何从以前的 VSS 快照中知道 Max 使用的 USN 序列并将其与另一个快照的序列进行比较?好吧,NextUsn 值只是等于$Usnjrnl:$J条目的大小。

如果要解析在两个快照之间更改的记录,则可以在"新"vss 快照 USN 日志上查找"引用"VSS 快照最大 USN,然后再开始解析记录。

一般来说,每个 USN 日记账分录作为一个唯一的 ID(USN 编号(,它是在日记账分录本身所在的位置$J内部的偏移量。每个条目都有一个可变的大小,所以要按顺序读取,我们必须计算:

next entry offset inside $J = 
    offset of current entry (or its USN sequennce number + length of current entry

幸运的是,记录长度也是 USN 条目记录的一个字段。说够了,这里是USN记录类:

public class UsnEntry : IComparable<UsnEntry>{
        private const int FR_OFFSET = 8;
        private const int PFR_OFFSET = 16;
        private const int USN_OFFSET = 24;
        private const int REASON_OFFSET = 40;
        private const int FA_OFFSET = 52;
        private const int FNL_OFFSET = 56;
        private const int FN_OFFSET = 58;

        public UInt32 RecordLength {get; private set;}
        public Int64 USN {get; private set;}
        public UInt64 FileReferenceNumber {get;private set;}
        public UInt64 ParentFileReferenceNumber {get; private set;}
        public UInt32 Reason{get; set;}
        public string Name {get; private set;}
        public string OldName{get; private set;}
        private UInt32 _fileAttributes;
        public bool IsFolder{
            get{
                bool bRtn = false;
                if (0 != (_fileAttributes & Win32Api.FILE_ATTRIBUTE_DIRECTORY))
                    bRtn = true;
                return bRtn;
            }
        }
        public bool IsFile{
            get{
                bool bRtn = false;
                if (0 == (_fileAttributes & Win32Api.FILE_ATTRIBUTE_DIRECTORY))
                    bRtn = true;
                return bRtn;
            }
        }
         /// <summary>
        /// USN Record Constructor
        /// </summary>
        /// <param name="p">Buffer pointer to first byte of the USN Record</param>
        public UsnEntry(IntPtr ptrToUsnRecord){
            RecordLength = (UInt32)Marshal.ReadInt32(ptrToUsnRecord); //record size
            FileReferenceNumber = (UInt64)Marshal.ReadInt64(ptrToUsnRecord, FR_OFFSET);
            ParentFileReferenceNumber = (UInt64)Marshal.ReadInt64(ptrToUsnRecord, PFR_OFFSET);
            USN = (Int64)Marshal.ReadInt64(ptrToUsnRecord, USN_OFFSET);
            Reason = (UInt32)Marshal.ReadInt32(ptrToUsnRecord, REASON_OFFSET);
            _fileAttributes = (UInt32)Marshal.ReadInt32(ptrToUsnRecord, FA_OFFSET);
            short fileNameLength = Marshal.ReadInt16(ptrToUsnRecord, FNL_OFFSET);
            short fileNameOffset = Marshal.ReadInt16(ptrToUsnRecord, FN_OFFSET);
            Name = Marshal.PtrToStringUni(new IntPtr(ptrToUsnRecord.ToInt32() + fileNameOffset), fileNameLength / sizeof(char));
        }

        public int CompareTo(UsnEntry other){
            return string.Compare(this.Name, other.Name, true);
        }
        public override string ToString(){
            return string.Format ("[UsnEntry: RecordLength={0}, USN={1}, FileReferenceNumber={2}, ParentFileReferenceNumber={3}, Reason={4}, Name={5}, OldName={6}, IsFolder={7}, IsFile={8}", RecordLength, USN, (int)FileReferenceNumber, (int)ParentFileReferenceNumber, Reason, Name, OldName, IsFolder, IsFile);
        }
    }

我试图从最低的有效部分开始,隔离可以解析 USN 日志并提取其条目的最小代码部分。请记住,记录的长度是可变的;另请注意,某些记录指向下一个空记录(前 4 个字节,通常是记录长度,已归零(。在这种情况下,我寻求 4 个字节并重试解析,直到获得下一条记录。用 Python 编写过类似解析工具的人也报告了这种行为,所以我想我在这里并没有错。

string vol = @"\?path_to_your_VSS_snapshot";
string maxHandle = vol + @"$Extend$UsnJrnl:$Max";
string rawJournal= vol + @"$Extend$UsnJrnl:$J";
// cannot use regular System.IO here, but pinvoking ReadFile() should be enough
FileStream maxStream = Alphaleonis.Win32.Filesystem.File.OpenRead(maxHandle);
byte[] maxData = new byte[32];
maxStream.Read(maxData, 0, 32);
//first valid entry
long lowestUsn = BitConverter.ToInt64(maxData, 24);
// max (last) entry, is the size of the $J ADS
IntPtr journalDataHandle = Win32Api.CreateFile(rawJournal, 
            0, 
            Win32Api.FILE_SHARE_READ| Win32Api.FILE_SHARE_WRITE,
            IntPtr.Zero, Win32Api.OPEN_EXISTING,  
            0, IntPtr.Zero);
Win32Api.BY_HANDLE_FILE_INFORMATION fileInfo = new Win32Api.BY_HANDLE_FILE_INFORMATION();
Win32Api.GetFileInformationByHandle(journalDataHandle, out fileInfo);
Win32Api.CloseHandle(journalDataHandle);
long lastUsn = fileInfo.FileSizeLow;

int read = 0;
byte[] usnrecord;
byte[] usnraw = new byte[4]; // first byte array is to store the record length
// same here : pinvoke ReadFile() to avoid AlphaFS dependancy
FileStream rawJStream = Alphaleonis.Win32.Filesystem.File.OpenRead(rawJournal);
int recordSize = 0;
long pos = lowestUsn;
while(pos < newUsnState.NextUsn){
seeked = rawJStream.Seek(pos, SeekOrigin.Begin);
read = rawJStream.Read(usnraw, 0, usnraw.Length);
recordSize = BitConverter.ToInt32(usnraw, 0);
    if(recordSize == 0){
    pos = pos+4;
    continue;
}
    usnrecord = new byte[recordSize];
rawJStream.Read(usnrecord, 4, recordSize-4);
Array.Copy(usnraw, 0, usnrecord, 0, 4);
fixed (byte* p = usnrecord){
    IntPtr ptr = (IntPtr)p;
        // here we use the previously defined UsnEntry class
    Win32Api.UsnEntry entry = new Win32Api.UsnEntry(ptr);
    Console.WriteLine ("entry: "+entry.ToString());
    ptr = IntPtr.Zero;
}
    pos += recordSize;
}

以下是我使用的 pinvokes:

public class Win32Api{
   [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct BY_HANDLE_FILE_INFORMATION{
        public uint FileAttributes;
        public FILETIME CreationTime;
        public FILETIME LastAccessTime;
        public FILETIME LastWriteTime;
        public uint VolumeSerialNumber;
        public uint FileSizeHigh;
        public uint FileSizeLow;
        public uint NumberOfLinks;
        /*public uint FileIndexHigh;
        public uint FileIndexLow;*/
        public FileID FileIndex;
    }
    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool 
        GetFileInformationByHandle(
        IntPtr hFile,
        out BY_HANDLE_FILE_INFORMATION lpFileInformation);
   [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr 
        CreateFile(string lpFileName, 
        uint dwDesiredAccess,
        uint dwShareMode, 
        IntPtr lpSecurityAttributes, 
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes, 
        IntPtr hTemplateFile);

}

这绝对不是地球上最好的代码,但我认为它将为任何必须做同样事情的人提供一个很好的起点。

你可能想再考虑一下鲁本的答案:

通过读取快照 VSS 卷内的特殊文件,快照卷中的 USN 日志绝对是可读的。如果 Windows API 不允许您读取快照卷的 USN 日志,那么这可能是一个可行的选择,尽管我确信这感觉像是一个黑客。

问题是,尽管NTFS

没有开放规范,但它是由多个项目发现的,其中包括NTFS驱动程序的Linux实现。Ruben 为您发布的文档最初是为了帮助开发此驱动程序而编写的。

就像我提到的,USN 期刊内容位于 NTFS 卷上的特殊文件中(就像 NTFS 中的许多内容一样,例如 NTFS 主文件表。实际上据说NTFS中的所有内容都是文件(。NTFS中的特殊文件以美元符号$开头,jou正在寻找的文件名为$UsnJrnl,而该文件又位于名为$Extend的特殊目录中。所以在你的 C: 卷上,该文件是

C:$Extend$UsnJrnl 

或者对于您来说,这将是快照

?GLOBALROOTDeviceHarddiskVolumeShadowCopy25$Extend$UsnJrnl

您要查找的信息位于名为$J流的备用数据流中,并且具有以下格式的条目(请参阅 Ruben 的参考文档(:

Offset(in hex) Size Description
0x00 4 Size of entry
0x04 2 Major Version
0x06 2 Minor Version
0x08 8 MFT Reference
0x10 8 Parent MFT Reference
0x18 8 Offset of this entry in $J
0x20 8 Timestamp
0x28 4 Reason (see table below)
0x2B 4 SourceInfo (see table below)
0x30 4 SecurityID
0x34 4 FileAttributes
0x38 2 Size of filename (in bytes)
0x3A 2 Offset to filename
0x3C V Filename
V+0x3C P Padding (align to 8 bytes)

因此,您可以读取此特殊文件的$J流以获取所需的 USN 条目。我想告诉你如何派生你需要的 USN 号码,但我有点生疏。如果我再次弄清楚,我将更新这个答案。但是看看以这种方式阅读特殊文件,它很有趣;-(。我使用此方法读取未装载的 VHD 文件中的主文件表(特殊文件 $MFT(,以便枚举 VHD 内卷上的所有文件。

我认为在

卷未挂载的情况下无法使用 WinAPI 接口查询 USN 日志。

您可以尝试打开文件"$UsnJrnl"并手动解析所需的信息。

看:

NTFS文档由Richard Russon和Yuval Fledel编写

也许这可能有用:日记条目不会跨越集群边界。每个簇(通常每个簇8个扇区(都以一个新条目开始。如果在这个集群的末尾,下一个条目不适合剩余的集群空间,这个空间被填充为零,下一个条目存储在下一个集群的开头(很遗憾这没有用"条目大小"来表达(。所以你不需要解析这个空间 - 只需跳转到下一个集群(!不要忘记使用日志的 RUN 来获取下一个有效的磁盘集群(。罗伯特

顺便说一句。您可以使用 USN(此条目的偏移量,以 $J 为单位(、集群编号和此条目在集群中的位置来检查条目的有效性。

相关内容

  • 没有找到相关文章

最新更新