如何使用.NET读取和修改NTFS备用数据流

55

如何使用.NET读取和修改“NTFS备用数据流”?

似乎没有.NET的原生支持。我应该使用哪些Win32 API?同时,由于我认为这并没有记录下来,因此我该如何使用它们?


顺便提一下,如果你想使用标准文件复制进度对话框来复制文件,就不能使用::SHFileOperation() - 它根本不支持AltDataStreams(在Windows 7上进行了检查)。 至于::CopyFileEx(),它在某些情况下可以工作(例如,在调用进度回调时可以将文件复制到AltDataStream中),但在其他情况下则无法工作。 - Nishi
1
原来这很容易实现:File.WriteAllText("asdf.txt:stream", "inside ads") - csstudent1418
@csstudent1418 - 我喜欢你的解决方案!有没有一种简单的方法来列出文件的流? - Tony Valenti
1
@csstudent1418 这取决于你使用的.NET版本。它在4.8或以下版本上无法工作。 - Jedidja
可悲的是,从命令提示符中运行type file.txt>newfile.txt就能轻松地去除文件的ADS(Alternate Data Stream)。这在批处理文件中非常方便,但对于编译代码来说需要更多的努力。 - undefined
5个回答

34

这是一个针对C#的版本。

using System.Runtime.InteropServices;

class Program
{
    static void Main(string[] args)
    {
        var mainStream = NativeMethods.CreateFileW(
            "testfile",
            NativeConstants.GENERIC_WRITE,
            NativeConstants.FILE_SHARE_WRITE,
            IntPtr.Zero,
            NativeConstants.OPEN_ALWAYS,
            0,
            IntPtr.Zero);

        var stream = NativeMethods.CreateFileW(
            "testfile:stream",
            NativeConstants.GENERIC_WRITE,
            NativeConstants.FILE_SHARE_WRITE,
            IntPtr.Zero,
            NativeConstants.OPEN_ALWAYS,
            0,
            IntPtr.Zero);
    }
}

public partial class NativeMethods
{

    /// Return Type: HANDLE->void*
    ///lpFileName: LPCWSTR->WCHAR*
    ///dwDesiredAccess: DWORD->unsigned int
    ///dwShareMode: DWORD->unsigned int
    ///lpSecurityAttributes: LPSECURITY_ATTRIBUTES->_SECURITY_ATTRIBUTES*
    ///dwCreationDisposition: DWORD->unsigned int
    ///dwFlagsAndAttributes: DWORD->unsigned int
    ///hTemplateFile: HANDLE->void*
    [DllImportAttribute("kernel32.dll", EntryPoint = "CreateFileW")]
    public static extern System.IntPtr CreateFileW(
        [InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string lpFileName, 
        uint dwDesiredAccess, 
        uint dwShareMode, 
        [InAttribute()] System.IntPtr lpSecurityAttributes, 
        uint dwCreationDisposition, 
        uint dwFlagsAndAttributes, 
        [InAttribute()] System.IntPtr hTemplateFile
    );

}


public partial class NativeConstants
{

    /// GENERIC_WRITE -> (0x40000000L)
    public const int GENERIC_WRITE = 1073741824;

    /// FILE_SHARE_DELETE -> 0x00000004
    public const int FILE_SHARE_DELETE = 4;

    /// FILE_SHARE_WRITE -> 0x00000002
    public const int FILE_SHARE_WRITE = 2;

    /// FILE_SHARE_READ -> 0x00000001
    public const int FILE_SHARE_READ = 1;

    /// OPEN_ALWAYS -> 4
    public const int OPEN_ALWAYS = 4;
}

11
在这里应该使用从SafeHandle派生出来的类型,以确保清理这些文件句柄。 - Richard
8
你展示了如何使用本地API,但没有展示如何使用从“CreateFileW”返回的指针。我真的很想看到一个更完整的示例,它可以写入Windows资源管理器中文件属性摘要标签中可用的常见属性。 - Bernhard Hofmann
似乎比简单的 File.WriteAllText("asdf.txt:stream", "inside ads") 复杂得多。我错过了什么? - csstudent1418
1
@csstudent1418 这在.NET Framework 4.8(或以下版本)上不起作用。 - Jedidja

18

首先,Microsoft® .NET Framework中没有提供此功能。如果您需要它,简单明了地说,您需要进行某种类型的Interop,直接使用或者使用第三方库。

如果您使用的是Windows Server™ 2003或更高版本,则Kernel32.dll公开了FindFirstFile和FindNextFile的对应函数,这些函数提供了您要查找的确切功能。FindFirstStreamW和FindNextStreamW允许您查找并枚举特定文件中的所有备用数据流,检索有关每个备用数据流的信息,包括其名称和长度。从托管代码中使用这些函数的代码与我在12月份专栏中展示的代码非常相似,并显示在图1中。

图1 使用FindFirstStreamW和FindNextStreamW

[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid {

    private SafeFindHandle() : base(true) { }

    protected override bool ReleaseHandle() {
        return FindClose(this.handle);
    }

    [DllImport("kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    private static extern bool FindClose(IntPtr handle);

}

public class FileStreamSearcher {
    private const int ERROR_HANDLE_EOF = 38;
    private enum StreamInfoLevels { FindStreamInfoStandard = 0 }

    [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
    private static extern SafeFindHandle FindFirstStreamW(string lpFileName, StreamInfoLevels InfoLevel, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_STREAM_DATA lpFindStreamData, uint dwFlags);

    [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FindNextStreamW(SafeFindHandle hndFindFile, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_STREAM_DATA lpFindStreamData);
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private class WIN32_FIND_STREAM_DATA {
        public long StreamSize;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 296)]
        public string cStreamName;
    }

    public static IEnumerable<string> GetStreams(FileInfo file) {
        if (file == null) throw new ArgumentNullException("file");
        WIN32_FIND_STREAM_DATA findStreamData = new WIN32_FIND_STREAM_DATA();
        SafeFindHandle handle = FindFirstStreamW(file.FullName, StreamInfoLevels.FindStreamInfoStandard, findStreamData, 0);
        if (handle.IsInvalid) throw new Win32Exception();
        try {
            do {
                yield return findStreamData.cStreamName;
            } while (FindNextStreamW(handle, findStreamData));
            int lastError = Marshal.GetLastWin32Error();
            if (lastError != ERROR_HANDLE_EOF) throw new Win32Exception(lastError);
        } finally {
            handle.Dispose();
        }
    }
}

您只需调用FindFirstStreamW函数,将目标文件的完整路径传递给它。FindFirstStreamW函数的第二个参数决定了您希望返回的数据详细程度;目前只有一个级别(FindStreamInfoStandard),其数字值为0。该函数的第三个参数是指向WIN32_FIND_STREAM_DATA结构的指针(技术上讲,第三个参数指向的内容由第二个参数详细信息级别的值所决定,但由于目前只有一个级别,因此在实际目的上,这是一个WIN32_FIND_STREAM_DATA)。我已经将该结构的托管对应项声明为类,并在互操作签名中将其标记为指向结构体的指针进行处理。最后一个参数保留供将来使用,应为0。 如果从FindFirstStreamW返回有效句柄,则WIN32_FIND_STREAM_DATA实例包含有关找到的流的信息,其cStreamName值可以作为第一个可用流名称返回给调用者。如果存在下一个流,则FindNextStreamW接受从FindFirstStreamW返回的句柄,并填充提供的WIN32_FIND_STREAM_DATA以获取有关下一个可用流的信息。如果还有其他流可用,则FindNextStreamW返回true,否则返回false。 因此,我不断调用FindNextStreamW并产生结果流名称,直到FindNextStreamW返回false。当发生这种情况时,我会仔细检查最后一个错误值,以确保迭代停止是因为FindNextStreamW用尽了流而不是出现了某些意外情况。 不幸的是,如果您使用的是Windows® XP或Windows 2000 Server,则这些函数对您不可用,但有几种替代方案。第一种解决方案涉及一个当前从Kernel32.dll导出的未记录函数NTQueryInformationFile。然而,未记录的函数之所以未被记录,是由于某种原因,它们可能会在将来任何时候被更改甚至删除。最好不要使用它们。如果您确实想使用此函数,请搜索Web,您会找到大量参考资料和示例源代码。但请自行承担风险。 另一种解决方案,我在图2中演示了它,依赖于从Kernel32.dll导出的两个函数,这些函数是有记录的。正如它们的名称所示,BackupRead和BackupSeek是Win32®备份支持API的一部分。
BOOL BackupRead(HANDLE hFile, LPBYTE lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, BOOL bAbort, BOOL bProcessSecurity, LPVOID* lpContext);
BOOL BackupSeek(HANDLE hFile, DWORD dwLowBytesToSeek, DWORD dwHighBytesToSeek, LPDWORD lpdwLowByteSeeked, LPDWORD lpdwHighByteSeeked, LPVOID* lpContext);

图2 使用BackupRead和BackupSeek函数

public enum StreamType {
    Data = 1,
    ExternalData = 2,
    SecurityData = 3,
    AlternateData = 4,
    Link = 5,
    PropertyData = 6,
    ObjectID = 7,
    ReparseData = 8,
    SparseDock = 9
}

public struct StreamInfo {
    public StreamInfo(string name, StreamType type, long size) {
        Name = name;
        Type = type;
        Size = size;
    }
    readonly string Name;
    public readonly StreamType Type;
    public readonly long Size;
}

public class FileStreamSearcher {
    [DllImport("kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool BackupRead(SafeFileHandle hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, [MarshalAs(UnmanagedType.Bool)] bool bAbort, [MarshalAs(UnmanagedType.Bool)] bool bProcessSecurity, ref IntPtr lpContext);[DllImport("kernel32.dll")]

    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool BackupSeek(SafeFileHandle hFile, uint dwLowBytesToSeek, uint dwHighBytesToSeek, out uint lpdwLowByteSeeked, out uint lpdwHighByteSeeked, ref IntPtr lpContext); public static IEnumerable<StreamInfo> GetStreams(FileInfo file) {
        const int bufferSize = 4096;
        using (FileStream fs = file.OpenRead()) {
            IntPtr context = IntPtr.Zero;
            IntPtr buffer = Marshal.AllocHGlobal(bufferSize);
            try {
                while (true) {
                    uint numRead;
                    if (!BackupRead(fs.SafeFileHandle, buffer, (uint)Marshal.SizeOf(typeof(Win32StreamID)), out numRead, false, true, ref context)) throw new Win32Exception();
                    if (numRead > 0) {
                        Win32StreamID streamID = (Win32StreamID)Marshal.PtrToStructure(buffer, typeof(Win32StreamID));
                        string name = null;
                        if (streamID.dwStreamNameSize > 0) {
                            if (!BackupRead(fs.SafeFileHandle, buffer, (uint)Math.Min(bufferSize, streamID.dwStreamNameSize), out numRead, false, true, ref context)) throw new Win32Exception(); name = Marshal.PtrToStringUni(buffer, (int)numRead / 2);
                        }
                        yield return new StreamInfo(name, streamID.dwStreamId, streamID.Size);
                        if (streamID.Size > 0) {
                            uint lo, hi; BackupSeek(fs.SafeFileHandle, uint.MaxValue, int.MaxValue, out lo, out hi, ref context);
                        }
                    } else break;
                }
            } finally {
                Marshal.FreeHGlobal(buffer);
                uint numRead;
                if (!BackupRead(fs.SafeFileHandle, IntPtr.Zero, 0, out numRead, true, false, ref context)) throw new Win32Exception();
            }
        }
    }
}

BackupRead的理念是将数据从文件读入缓冲区,然后写入备份存储介质。但是,BackupRead也非常方便用于查找组成目标文件的每个备用数据流的信息。它将文件中的所有数据处理为一系列离散的字节流(每个备用数据流都是其中之一),而每个流都以WIN32_STREAM_ID结构开头。因此,为了枚举所有流,您只需从每个流的开头读取所有这些WIN32_STREAM_ID结构即可(这就是BackupSeek非常方便的地方,它可以用于跳转从一个流到另一个流,而不必读取文件中的所有数据)。 首先,您需要为未管理的WIN32_STREAM_ID结构创建托管对应项:

typedef struct _WIN32_STREAM_ID { 
    DWORD dwStreamId; DWORD dwStreamAttributes;
    LARGE_INTEGER Size; 
    DWORD dwStreamNameSize; 
    WCHAR cStreamName[ANYSIZE_ARRAY];
} WIN32_STREAM_ID;

大部分情况下,这与您通过P/Invoke调用的任何其他结构类似。但是,有一些复杂性。首先,WIN32_STREAM_ID是一个可变大小的结构。它的最后一个成员cStreamName是一个具有长度ANYSIZE_ARRAY的数组。虽然ANYSIZE_ARRAY被定义为1,但cStreamName只是前四个字段之后结构中其余数据的地址,这意味着如果该结构被分配为大于sizeof(WIN32_STREAM_ID)字节,则额外空间实际上将成为cStreamName数组的一部分。前一个字段dwStreamNameSize指定数组的确切长度。 虽然这对Win32开发非常有用,但它会给需要将此数据从非托管内存复制到托管内存作为Interop调用BackupRead的一部分的编组器带来麻烦。编组器如何知道WIN32_STREAM_ID结构实际上有多大,考虑到它是可变大小的?它不知道。 第二个问题与打包和对齐有关。暂时忽略cStreamName,考虑您托管的WIN32_STREAM_ID对应项的以下可能性:
[StructLayout(LayoutKind.Sequential)] 
public struct Win32StreamID { 
    public int dwStreamId; 
    public int dwStreamAttributes; 
    public long Size; 
    public int dwStreamNameSize;
}

Int32类型大小为4字节,Int64类型大小为8字节。因此,你期望这个结构体大小为20字节。然而,如果你运行以下代码,你会发现两个值都是24,而不是20:

int size1 = Marshal.SizeOf(typeof(Win32StreamID));
int size2 = sizeof(Win32StreamID); // in an unsafe context

问题在于编译器想确保这些结构内的值始终对齐到正确的边界上。四字节的值应该在地址可被 4 整除的位置,8 字节的值应该在地址可被 8 整除的位置,依此类推。现在假设你要创建一个 Win32StreamID 结构的数组。数组中第一个实例中的所有字段都将被正确对齐。例如,由于 Size 字段跟随两个 32 位整数,它将距离数组开始处有 8 个字节的长度,完美地适合 8 字节的值。然而,如果该结构的大小为 20 字节,则数组中的第二个实例将不能使其所有成员都正确对齐。整数值都很好,但 long 值将距离数组开始处有 28 个字节的长度,这个值不是 8 的倍数。为了解决这个问题,编译器会填充该结构,使其大小为 24,以使所有字段始终正确对齐(假设数组本身是正确的)。 如果编译器做得没错,那么你可能会想知道我为什么会关心这个问题。如果你看一下图2中的代码,就会明白原因。为了解决我所描述的第一个封送问题,我确实将 cStreamName 留在了 Win32StreamID 结构之外。我使用 BackupRead 读取足够的字节来填充我的 Win32StreamID 结构,然后检查结构中的 dwStreamNameSize 字段。现在我知道名字有多长了,我可以再次使用 BackupRead 从文件中读取字符串的值。这都很好,但如果 Marshal.SizeOf 返回 24 而不是 20,则我将尝试读取过多的数据。 为避免这种情况,我需要确保 Win32StreamID 的大小实际上是 20 而不是 24。这可以通过 StructLayoutAttribute 上的字段以两种不同的方式来完成。第一种方法是使用 Size 字段,它告诉运行时结构应该有多大:
[StructLayout(LayoutKind.Sequential, Size = 20)]

第二个选项是使用Pack字段。当指定LayoutKind.Sequential值时,Pack表示应使用的打包大小,并控制结构中字段的对齐方式。托管结构的默认打包大小为8。如果我将其更改为4,则得到了我正在寻找的20字节的结构(由于我实际上没有在数组中使用它,因此不会失去由此引起的效率或稳定性)。
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct Win32StreamID {
    public StreamType dwStreamId;
    public int dwStreamAttributes;
    public long Size;
    public int dwStreamNameSize; // WCHAR cStreamName[1];
}

有了这段代码,我现在可以枚举文件中的所有流,如下所示:

static void Main(string[] args) {
    foreach (string path in args) {
        Console.WriteLine(path + ":");
        foreach (StreamInfo stream in FileStreamSearcher.GetStreams(new FileInfo(path))) {
            Console.WriteLine("\t{0}\t{1}\t{2}", stream.Name != null ? stream.Name : "(unnamed)", stream.Type, stream.Size);
        }
    }
}

你会注意到,这个版本的FileStreamSearcher返回的信息比使用FindFirstStreamW和FindNextStreamW的版本要多。BackupRead可以提供的不仅仅是主数据流和备用数据流的数据,还可以操作包含安全信息、重解析数据等的流。如果你只想查看备用数据流,可以根据StreamInfo的Type属性进行过滤,它将为Alternate Data Streams设置StreamType.AlternateData。
要测试这段代码,你可以在命令提示符下使用echo命令创建一个具有备用数据流的文件。
> echo ".NET Matters" > C:\test.txt
> echo "MSDN Magazine" > C:\test.txt:magStream
> StreamEnumerator.exe C:\test.txt
test.txt:
        (unnamed)               SecurityData    164
        (unnamed)               Data            17
        :magStream:$DATA        AlternateData   18
> type C:\test.txt
".NET Matters"
> more < C:\test.txt:magStream
"MSDN Magazine"

现在,您可以检索存储在文件中的所有备用数据流的名称。很好,但是如果您想实际操作其中一个流中的数据该怎么办呢?不幸的是,如果您尝试将备用数据流的路径传递给其中一个FileStream构造函数,将会抛出NotSupportedException异常:"不支持给定路径格式"。

为了解决这个问题,您可以绕过FileStream的路径规范化检查,直接访问kernel32.dll公开的CreateFile函数(见图3)。我使用了CreateFile函数的P / Invoke来打开并检索指定路径的SafeFileHandle,而没有对路径执行任何托管权限检查,因此它可以包括备用数据流标识符。然后使用此SafeFileHandle来创建新的托管FileStream,提供所需的访问权限。有了这个功能,就可以使用System.IO命名空间的功能来操作备用数据流的内容。以下示例读取并打印出先前示例中创建的C:\test.txt:magStream的内容:

string path = @"C:\test.txt:magStream"; 
using (StreamReader reader = new StreamReader(CreateFileStream(path, FileAccess.Read, FileMode.Open, FileShare.Read))) { 
    Console.WriteLine(reader.ReadToEnd());
}

图3 使用P/Invoke调用CreateFile函数

private static FileStream CreateFileStream(string path, FileAccess access, FileMode mode, FileShare share) {
    if (mode == FileMode.Append) mode = FileMode.OpenOrCreate; SafeFileHandle handle = CreateFile(path, access, share, IntPtr.Zero, mode, 0, IntPtr.Zero);
    if (handle.IsInvalid) throw new IOException("Could not open file stream.", new Win32Exception());
    return new FileStream(handle, access);
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string lpFileName, FileAccess dwDesiredAccess, FileShare dwShareMode, IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

Stephen Toub2006年1月的 MSDN Magazine 中发表了相关的技术文章。


6
为什么只给出链接的答案是不好的一个很好的例子。 - Carey Gregory
所有指向 MSDN 杂志的链接都已失效,而指向 MSDN 网站的链接也将很快失效。请在您的回答中提供更多详细信息。 - AaA
1
@AaA 看起来微软终于认识到这是一个问题,并设置了重定向,使旧的MSDN链接现在被重定向到适当的存档。我知道,这是一篇旧文章,但仍然:P - Corey
但是现在我遇到了内容被屏蔽的问题。这不仅仅发生在您提供的链接上,有时在hotmail、outlook等网站也会发生。微软最近变得非常非常烦人。我更喜欢尽可能从不同的网站获取所需信息。 - AaA

17

这些操作在.NET中没有本地支持,你需要使用P/Invoke来调用本地Win32方法。

要创建它们,请使用类似于filename.txt:streamname的路径调用CreateFile。如果使用返回SafeFileHandle的Interop调用,则可以使用它来构建一个FileStream,然后再读取和写入。

要列出文件上存在的流,请使用FindFirstStreamWFindNextStreamW(仅适用于Server 2003及更高版本-不适用于XP)。

我不认为你可以删除流,除非复制文件的其余部分并省略其中一个流。将长度设置为0也可能有效,但我没有尝试过。

您还可以在目录上拥有备用数据流。您可以像文件一样访问它们-C:\ some \ directory:streamname

流可以独立于默认流设置压缩、加密和稀疏性。


9
您可以删除一个流:只需使用“文件名:流名称”调用DeleteFile API。显然,您可以对ADS执行与普通文件几乎相同的任何操作。唯一的原因是FileStream无法处理它是因为它验证路径,并且如果包含“:”则会失败。 - Thomas Levesque
3
过时的回答(就像这里的大多数其他回答):.NET本身是支持这个的,例如File.WriteAllText("asdf.txt:stream", "inside ads")似乎可以正常工作。 - csstudent1418
1
@csstudent1418 再次强调,这取决于你使用的.NET版本。4.8不支持此功能。 - Jedidja

15

请注意,这个已经过时了;我在https://github.com/RichardD2/NTFS-Streams找到了一个替代方案。 - undefined

4

不适用于 .NET:

http://support.microsoft.com/kb/105763

(注:此文为链接,未做翻译)
#include <windows.h>
   #include <stdio.h>

   void main( )
   {
      HANDLE hFile, hStream;
      DWORD dwRet;

      hFile = CreateFile( "testfile",
                       GENERIC_WRITE,
                    FILE_SHARE_WRITE,
                                NULL,
                         OPEN_ALWAYS,
                                   0,
                                NULL );
      if( hFile == INVALID_HANDLE_VALUE )
         printf( "Cannot open testfile\n" );
      else
          WriteFile( hFile, "This is testfile", 16, &dwRet, NULL );

      hStream = CreateFile( "testfile:stream",
                                GENERIC_WRITE,
                             FILE_SHARE_WRITE,
                                         NULL,
                                  OPEN_ALWAYS,
                                            0,
                                         NULL );
      if( hStream == INVALID_HANDLE_VALUE )
         printf( "Cannot open testfile:stream\n" );
      else
         WriteFile(hStream, "This is testfile:stream", 23, &dwRet, NULL);
   }

10
两个缺失的 CloseHandle 调用... 操作系统会清理,但在真正的应用程序中可能会出现问题。 - Richard
3
@Richard - 只是从微软的支持网站复制过来... - Otávio Décio
1
你可以从C#中使用P/Invoke调用这些函数。 - Tim Lloyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接