C#中ADS需通过P/Invoke调用Win32 API操作,System.IO类库完全忽略ADS;读写必须用CreateFile(带FILE_FLAG_BACKUP_SEMANTICS)、ReadFile、WriteFile等,并注意流创建、SetEndOfFile、FlushFileBuffers及句柄释放。

ADS在C#中没有原生API支持,必须调用Windows API
Windows .NET运行时(包括.NET Framework、.NET Core 3.1+ 和 .NET 5+)的System.IO类库完全忽略备用数据流——File.ReadAllText、FileStream、FileInfo.Length等所有标准API均无法感知ADS存在。想读写ADS,唯一可靠方式是P/Invoke调用CreateFile、SetFilePointerEx、ReadFile、WriteFile等Win32函数。
常见错误现象:File.Exists("test.txt:secret")返回false;new FileInfo("test.txt:secret").Length抛出IOException;直接用File.WriteAllText("test.txt:secret", "data")会静默创建主文件test.txt:secret(即冒号被当作文件名一部分),而非向test.txt写入流。
- 必须使用
CreateFile打开带流名的路径,如"test.txt:secret:$DATA"($DATA可省略,但显式写出更清晰) - 打开模式需设为
GENERIC_READ | GENERIC_WRITE,访问方式必须含FILE_FLAG_BACKUP_SEMANTICS(否则AccessDenied) - 句柄必须用
CloseHandle显式释放,.NET GC不管理Win32句柄
读取ADS内容的最小可行代码
以下代码片段能安全读取指定ADS(假设流名为secret):
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
const uint GENERIC_READ = 0x80000000;
const uint GENERIC_WRITE = 0x40000000;
const uint OPEN_EXISTING = 3;
const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
string filePath = @"C:\temp\test.txt:secret";
IntPtr handle = CreateFile(
filePath,
GENERIC_READ,
0,
IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
if (handle == new IntPtr(-1))
{
int error = Marshal.GetLastWin32Error();
throw new IOException($"Failed to open ADS: {error}");
}
try
{
var buffer = new byte[4096];
uint bytesRead;
if (!ReadFile(handle, buffer, (uint)buffer.Length, out bytesRead, IntPtr.Zero))
{
throw new IOException("ReadFile failed");
}
string content = Encoding.UTF8.GetString(buffer, 0, (int)bytesRead);
}
finally
{
CloseHandle(handle);
}
注意:ReadFile和WriteFile需自行P/Invoke声明;缓冲区大小应按需调整,ADS通常很小,但无上限限制;若流不存在,CreateFile返回INVALID_HANDLE_VALUE(-1),不是null。
写入ADS时容易踩的三个坑
写入比读取更易出错,尤其在流不存在时需正确处理创建逻辑:
-
CreateFile的dwCreationDisposition必须用CREATE_ALWAYS或OPEN_ALWAYS,不能用OPEN_EXISTING(否则流不存在时失败) - 写入前未调用
SetEndOfFile会导致旧内容残留——ADS不像主文件有自动截断,必须显式收缩长度 - 写入后未调用
FlushFileBuffers,数据可能滞留在内核缓存,其他进程(如资源管理器)立即读不到最新内容
例如覆盖写入test.txt:hidden:
IntPtr handle = CreateFile(@"test.txt:hidden:$DATA",
GENERIC_WRITE, 0, IntPtr.Zero, CREATE_ALWAYS, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
// ... WriteFile ...
SetEndOfFile(handle); // 关键:清空原有内容
FlushFileBuffers(handle);
CloseHandle(handle);ADS路径解析与权限注意事项
ADS路径格式为主文件名:流名[:流类型],其中流类型默认是$DATA,可省略。但以下情况必须注意:
- 流名不能含
\ / : * ? " |,且不能以空格结尾(Windows会截断) - NTFS权限独立于主文件:即使用户对
test.txt有读写权,也可能因ADS ACL被拒绝——调试时用icacls test.txt /stream:secret检查 - .NET程序需以管理员权限运行?不一定。只要当前用户对文件有
READ_PROPERTIES/WRITE_PROPERTIES权限且启用了备份权限(通常普通用户也有),即可操作ADS;但某些域策略可能禁用SeBackupPrivilege
最常被忽略的是流名编码:Windows内部用UTF-16,但C#字符串已是UTF-16,所以无需额外编码转换;但若从外部输入(如HTTP参数)获取流名,需先验证是否含非法字符,否则CreateFile直接失败且错误码不直观。










