てくメモ

trivial な notes

【C#】代替データストリームの単純な読み書き

主にマルウェアの関連で耳にする代替データストリーム (Alternate Data Stream / ADS) を、C#で単純に読み書きする。

なお、マルウェア云々は以降で特に触れないので参考文献を挙げておく。
参考:代替データストリームの危険性と、閲覧/削除機能


C#における代替データストリームについては、検索すると P/Invoke を使った方法が出てくる。
ただ、実は単純に読み書きするだけなら標準のAPIで行うことができる。

試しに、以下のようなパスとストリーム名(と作法としてキャンセレーショントークン)を渡すメソッドを用意してみる。

public class ADS
{
    public static async Task<byte[]> OpenReadAsync(string fileName, string streamName, CancellationToken cancellationToken = default)
    {
        var fullName = $"{fileName}:{streamName}";

        using var handle = File.OpenHandle(fullName);
        if (handle.IsInvalid) return Array.Empty<byte>();

        var size = RandomAccess.GetLength(handle);
        var buf = new byte[size];
        await RandomAccess.ReadAsync(handle, buf.AsMemory(), 0, cancellationToken);
        return buf;
    }
}

パスを$"{fileName}:{streamName}"にしているほかは、特に変わった点はなし。

試しに、代替データストリーム代表格であるZone.Identifier(インターネットでダウンロードしたファイルの実行を警告したりするために使用されている)を読んでみる。

var path = @"適当にインターネットで拾ったファイル.png";

var data = await ADS.OpenReadAsync(path, "Zone.Identifier");
Console.WriteLine(Encoding.ASCII.GetString(data));
// [ZoneTransfer]
// ZoneId=3


このように、単純に読み書きするだけであれば標準だけでOK。
ストリーム名を指定せず代替データストリームを一覧する、みたいなことをやりたい場合は(おそらく)P/Invoke が必要になる。


書き込みもやってみる。

public static async Task OpenWriteAsync(string fileName, string streamName, ReadOnlyMemory<byte> data, CancellationToken cancellation = default)
{
    var fullName = $"{fileName}:{streamName}";

    using var handle = File.OpenHandle(fullName, FileMode.OpenOrCreate, FileAccess.Write);
    if (handle.IsInvalid) return;

    await RandomAccess.WriteAsync(handle, data, 0, cancellation);
}
byte[] bytes = Encoding.ASCII.GetBytes("Hello, world!");
await ADS.OpenWriteAsync(path, "testStream", bytes.AsMemory());

data = await ADS.OpenReadAsync(path, "testStream");
Console.WriteLine(Encoding.ASCII.GetString(data));
// Hello, world!

難しいこともなく書込み可能。


存在確認や削除なども。

Console.WriteLine(File.Exists($"{path}:testStream"));
File.Delete($"{path}:testStream");
Console.WriteLine(File.Exists($"{path}:testStream"));
// True
// False