2023年3月28日火曜日

PowerShell で Mark of The Web 属性を付加するとエラーになる?

Mark of The Web

Mark of The Web (略して MoTW)は Windows のファイル拡張属性で、インターネットからダウンロードされたファイルに特別なしるしを付けて警告を促せるようにするものです。
メールソフト用のプラグインを作っていますので、この属性を使用するのは至極当然のことで。
というわけで、弊社の製品にも実装しました。

問題に遭遇

例によって、いろいろな問題に遭遇したのですが、特に顕著な問題について、ここで報告しておきます。
C:\Temp フォルダに2つのテキストファイルを置きます。(中身はなんでもいいです)
ファイル名は Test_(1.txt, Test_(2.txt です。

まず、PowerShell を起動して(*0)、以下を実行してみます。

PS C:\Temp> Set-Content -Path "C:\Temp\test_(1.txt" -Stream Zone.Identifier -Encoding oem -NoNewline -Value "[ZoneTransfer]`r`nZoneId=3"

エクスプローラで、プロパティを参照すると、セキュリティ属性が付加されているのがわかります。

プログラムから呼び出してみる

次に、対象ファイルを Test_(2.txt に変えて、同じ動作をするプログラムを実装して、実行してみます。
使用したのは VisualStudio 2022, .NET 4.8 ターゲットの C# コンソールアプリケーションです。
using 行を端折ってますが、デフォルトのものでかまいません。

namespace MoTW
{
    class Program
    {

        static void Main(string[] args)
        {
            string file2 = @"C:\Temp\test_(2.txt";
            setMoTW_byPowerShell(file2);
            Console.ReadLine();
        }

        // set MoTW by PowerShell
        private static bool setMoTW_byPowerShell(string filename)
        {
            bool rc = false;
            string arg_format = "Set-Content -Path \"{0}\" -Stream Zone.Identifier -Encoding oem -NoNewline -Value \"[ZoneTransfer]`r`nZoneId=3\"";
            try
            {
                var psinfo = new ProcessStartInfo();
                psinfo.FileName = "powershell.exe";
                psinfo.UseShellExecute = true;
                psinfo.Arguments = string.Format(arg_format, filename);
                psinfo.CreateNoWindow = true;
                psinfo.WindowStyle = ProcessWindowStyle.Hidden;
                Process proc = Process.Start(psinfo);
                proc.WaitForExit();
                rc = (proc.ExitCode == 0);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                rc = false;
            }
            return rc;
        }
    }
}

Power Shell、普段使わないので、改行の指定方法が '`r`n' だということも今回調べましたよw
で、結果。エクスプローラで Test_(2.txt のプロパティを見るとですね。
MoTW 付いていないんですね、これが。
実際に作ったのは、アドインソフトウェアなので DLL ファイルなんですが、同じように MoTW 付いてくれなくて、本当に困ってしまいました。
ちなみに、ファイル名が Test1.txt, Test2.txt であれば、PowerShell も C# プログラムも正常に MoTW が付加されます。

つまり、'(' の文字が含まれていると、実行プログラムからの呼び出しではうまくいかないんですよ。

メールソフト用のアドインでは

1.添付ファイルをディスク上に保存する。
2.ShellExecuteEx を使って(*1)、保存したファイルを開く。
という動作を実装しているので、MoTW 属性のついていないファイルがそのまま開かれたり実行されてしまうんですね。
ZIP書庫を展開したファイルが実行ファイルだった場合、無条件に実行されてしまうことになってしまいます。
危険な状態です。
とても MoTW 対応です!とは言えませんねw

まとめ

結局、各種記号のいくつかが含まれるファイル名では失敗することがわかったので

1.ファイル名を Base64Url エンコードした名前に変更する。
2.PowerShell で Set-Content する(上記のコード)。
3.ファイル名を Base64Url デコードする。
という、本来やらなくてもいいような処理を実装することで問題を回避できました。
めでたしめでたし。(*2)

*0

MoTW を付加する方法には、「デバイスパス指定子」を使って FileStream を操作し Zone識別ファイルを作成する方法もあります。
わんくま同盟の掲示板で魔界の仮面弁士さんに教えていただきました。
この方法は、.NET 4.6.2 以上でうまくいくのですが、Outlook 用のアドインを .NET 4.6.2 以上で構築してもDLL内では .NET4.6.2以前の場合と同じようにエラーとなってしまうため、利用を断念しました。
Outlook がなにかやらかしてるのだと思います。

*1

保存したファイルを開く処理で、System.Diagnostics.Process を使って Start() すると、MoTW を無視して実行してしまいます。
んで、MoTW 属性も消されてしまいます。
しかたなく WIN32API の ShellExecuteEx で Mask 値を SEE_MASK_NOZONECHECKS がセットされないようにして呼び出すようにしました。

*2

「めでたしめでたし」じゃないんですよ。
多くのウィルスソフトやマルウェアなどがメールを介して感染しています。
弊社のプログラムはこれで問題を解決できましたが、同様の処理を実装しているメール用アドインなんかでは致命的です。

とっても致命的な問題だと思ったので、マイクロソフトに報告しておきました。