C# - MoTW と Zone.Identifierストリーム

## はじめに

Excel マクロがブロックされたり、EXE ファイルの実行が SmartScreen によって制限されることがあります。
本記事では、まず、これらの制約の原因となる MoTW（Mark of the Web）と、その実装メカニズムである Zone.Identifier ストリームについて記載します。
その後、C#、PowerShell、コマンドプロンプトでの Zone.Identifier 操作を記載します。


### 参考情報

https://ascii.jp/elem/000/001/550/1550399/

https://qiita.com/SAITO_Keita/items/4ce171bce91b6daa5cbf

### テスト環境

ここに記載した情報は、Windows 11 24H2 で動作確認しています。
C#ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールで確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、基本的に C# 7.3 機能範囲で記述しています。
:::

## MoTW と Zone.Identifier ストリーム
### MoTW

MoTW（Mark of the Web）は、Windows 独自のセキュリティ機能で、インターネット経由で取得したファイルに適用される属性です。
MoTW が付与されたファイルは、特定の条件下で実行が制限されることがあります。

- マクロ付きのOfficeファイル（Excel、Wordなど）は、マクロの実行が制限されます
- スクリプトファイル（.ps1, .vbs, .jsなど）は、 PowerShell スクリプトや VBScript の実行が制限されます
- 実行ファイル（.exe, .dllなど）は、Windows の SmartScreen フィルターによって実行が制限されます

### Zone.Identifier ストリーム

MoTW の実装には、Zone.Identifier ストリームと呼ばれる特殊な NTFS 代替データストリーム（ADS：Alternate Data Streams）が使用されます。
Zone.Identifier ストリームには、ファイルの出所に関する情報が記録されます。

```
[ZoneTransfer]
ZoneId=3
ReferrerUrl=http://example.com
HostUrl=http://download.example.com/file.exe
```

- ZoneId: ファイルの取得元のセキュリティゾーン
- ReferrerUrl: ダウンロード元の Web ページ URL
- HostUrl: ファイルを取得したサーバーの URL
- HostIpAddress: ダウンロード元の IP アドレス（記録されない場合あり）

::: note
ブラウザでダウンロードした場合は、Zone.Identifier が作成されます。
PowerShell の Invoke-WebRequest や curl.exe など、一部のコマンドラインツールでは、Zone.Identifier が作成されません。
:::

ZoneIdの種類:

| ZoneId | 意味                 | 影響                       |
| ------ | -------------------- | ------------------------- |
| 0      | ローカルコンピュータ   | 制限なし                    |
| 1      | ローカルイントラネット | 制限なし                    |
| 2      | 信頼済みサイト        | 制限なし                    |
| 3      | インターネット        | ブロック、もしくは、警告       |
| 4      | 制限付きサイト        | 基本的にブロック               |

Chrome、FireFox は独自のセキュリティモデルに基づいて、ZoneId を決定します。
Edge でダウンロードした場合、Windows「インターネットオプション」セキュリティタブの各ゾーンに設定されているサイトに基づいて、ZoneId を決定します。

![MoTW-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/31a94d27-a49f-40ec-bb41-510b49b3da04.png)

::: note
上記は、ファイル名を指定して実行で「inetcpl.cpl」を入力して起動できます。
:::

マクロ付き Office ファイルの影響：

- ZoneId=0,1,2 は、マクロの実行が許可されます
- ZoneId=3 は、保護ビューで開かれ、マクロの実行が制限されます
- ZoneId=4 は、基本的にブロックされます

スクリプトファイルの影響：

- ZoneId=0,1,2 は、スクリプト実行が許可されます
- ZoneId=3 は、Windows のセキュリティ機能（SmartScreen、PowerShell 実行ポリシーなど）によってブロック、もしくは、警告が表示されます
- ZoneId=4 は、基本的にブロックされます

実行ファイルの影響：

- ZoneId=0,1,2 は、実行が許可されます
- ZoneId=3 は、SmartScreen による警告が表示されます
- ZoneId=4 は、基本的にブロックされます

::: note
デジタル署名を付与すると、制限が緩和される場合があります。
:::


### アーカイブからの継承

MoTW が付与された（Zone.Identifier ストリームが付与された）アーカイブを解凍したファイルに、対象属性値が継承されるかは、解凍ソフトが Zone.Identifier ストリームをどのように扱うかに依存します。

- Windows 標準 API を利用した解凍（Explorer、PowerShell Expand-Archive）
  - アーカイブの ZoneId が、解凍後のファイルにも継承されます
  - ZoneId 以外の属性値は継承されないことがあります（ReferrerUrl が、ダウンロード元の Web ページ URLではなく、ローカルの対象アーカイブパスに変更される等）
- 7zip、WinRAR などのサードパーティーソフト
  - 基本的に、Zone.Identifier は継承されません


## C# での操作
### Zone.Identifier 存在確認

確認対象 `<filename>` の場合、`<filename>:Zone.Identifier` の存在有無で確認できます。

```CSharp
string filePath = @"C:\path\to\hoge.exe:Zone.Identifier";
// Zone.Identifier 存在確認
if (File.Exists(filePath))
{
  Console.WriteLine("Zone.Identifier が存在します。");
}
else
{
  Console.WriteLine("Zone.Identifier は存在しません。");
}
```

### Zone.Identifier 内容取得

```CSharp
string filePath = @"C:\path\to\hoge.exe:Zone.Identifier";
// Zone.Identifier 存在確認
if (File.Exists(filePath))
{
  // Zone.Identifier 内容取得
  using (var reader = new StreamReader(filePath))
  {
    Console.WriteLine(reader.ReadToEnd());
  }
}
else
{
  Console.WriteLine("Zone.Identifier は存在しません。");
}
```

### Zone.Identifier 削除

Windows 11：
`<filename>:Zone.Identifier` を System.IO.File.DeleteFile すると、Zone.Identifier のみを削除できます。（後述、Windows 10 以前の手法でも削除可能）

```CSharp
string filePath = @"C:\path\to\hoge.exe:Zone.Identifier";
// Zone.Identifier 存在確認
if (File.Exists(filePath))
{
  try
  {
    // Zone.Identifier 削除
    File.Delete(filePath);
  }
  catch
  {
    Console.WriteLine("Zone.Identifier 削除に失敗しました。");
  }
}
else
{
  Console.WriteLine("Zone.Identifier は存在しません。");
} 
```

Windows 10 以前：
Zone.Identifier のみを削除するには、WIN32API - DeleteFile 利用が必要です。

```CSharp
string filePath = @"C:\path\to\hoge.exe:Zone.Identifier";
// Zone.Identifier 存在確認
if (File.Exists(filePath))
{
  // Zone.Identifier 削除
  if (NativeMethods.DeleteFile(filePath))
  {
    Console.WriteLine("Zone.Identifier を削除しました。");
  }
  else
  {
    Console.WriteLine("Zone.Identifier 削除に失敗しました。");
  }
}
else
{
  Console.WriteLine("Zone.Identifier は存在しません。");
} 
```
```CSharp
private static class NativeMethods
{
  // WIN32QPI
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
  public static extern bool DeleteFile(string name);
}
```

::: note
WIN32API を直接記述していますが、Microsoft が提供している CsWin32 を利用するという手もあります。
- [GitHub - microsoft/CsWin32:](https://github.com/microsoft/CsWin32)
- [NuGet Gallery | Microsoft.Windows.CsWin32](https://www.nuget.org/packages/Microsoft.Windows.CsWin32)
:::

### Zone.Identifier 更新

```CSharp
string filePath = @"C:\path\to\hoge.exe:Zone.Identifier";
var sb = new StringBuilder();
sb.AppendLine("[ZoneTransfer]");
sb.AppendLine("ZoneId=3");
sb.Append("ReferrerUrl=https://example.com");

try
{
  // Zone.Identifier 更新
  File.WriteAllText(filePath, sb.ToString());
}
catch
{
  Console.WriteLine("Zone.Identifier 更新に失敗しました。");
}
```


## PowerShell での操作
### Zone.Identifier 存在確認

```PowerShell
Get-Item -Path "C:\path\to\hoge.exe" -Stream Zone.Identifier
```

### Zone.Identifier 内容表示

```PowerShell
Get-Content -Path "C:\path\to\hoge.exe" -Stream Zone.Identifier
```

### Zone.Identifier 削除

対象ファイルの Zone.Identifier 削除は、２パターン存在します。

```PowerShell
Remove-Item -Path "C:\path\to\hoge.exe" -Stream Zone.Identifier
```
```PowerShell
Unblock-File -Path "C:\path\to\hoge.exe"
```

フォルダ内のすべてのファイルを対象として、Zone.Identifier 削除は下記で可能です。

```PowerShell
Get-ChildItem -Path "C:\path\to\folder" -Recurse | Unblock-File
```

### Zone.Identifier 更新

```PowerShell
# 既存のファイルの Zone.Identifier を上書き（完全に置き換える）
Set-Content -Path "C:\path\to\hoge.exe" -Stream "Zone.Identifier"　-Value @(
"[ZoneTransfer]", "ZoneId=3", "ReferrerUrl=https://example.com")

# 既存のファイルの Zone.Identifier を保持し、新しいデータを追記
Add-Content -Path "C:\path\to\hoge.exe" -Value "追加の内容"
```

## コマンドプロンプトでの操作

NTFS 代替データストリーム（ADS）を調査・管理するためのツールとして、[Steram.exe - Sysinternals | Microsoft Learn](https://learn.microsoft.com/ja-jp/sysinternals/downloads/streams) が提供されていますが、標準コマンドの範囲で記載します。

### Zone.Identifier 存在確認

dir を /r オプションで実行して `<filename>` とペアで `<filename>:Zone.Identifier:$DATA` が存在するか否かで確認できます。　

```
dir <対象フォルダパス> /r
```
```
2025/06/01 00:00    1,234,567 hoge.zip
                          257 hoge.zip:Zone.Identifier:$DATA
```


### Zone.Identifier 内容表示

more を利用することで、Zone.Identifier を表示することができます。

```
more < hoge.zip:Zone.Identifier
```
```
[ZoneTransfer]
ZoneId=3
ReferrerUrl=http://example.com
HostUrl=http://download.example.com/file.exe
```