Named Pipe header

執筆者:Farid Mustafayev(ThreatLocker サイバーセキュリティ専門家)

名前付きパイプは、同じ Windows コンピュータ上で実行されているアプリケーション間の通信に一般的に使用されます。処理が高速で、オペレーティングシステムによって直接サポートされており、Windows サービス、デスクトップアプリケーション、トレイプロセス、コマンドラインユーティリティ、バックグラウンドエージェント間の通信に最適です。

典型的な設計では、特権を持つWindowsサービスが名前付きパイプのサーバーとして機能し、ユーザー向けのアプリケーションがクライアントとして接続します。両方のプロセスが同じコンピュータ上で実行されるため、開発者はこの通信を内部的なもの、つまり信頼できるものとみなすことがよくあります。

実際には、このパイプには、異なるユーザー、セッション、セキュリティコンテキストの下で、多くの無関係なプロセスが実行されている環境からもアクセス可能です。

「ローカル」は「信頼できる」を意味しない

名前付きパイプは、同じコンピュータ上のアプリケーション間の通信に使用されるため、しばしばプライベートなものとして扱われます。しかし、その前提は安全ではありません。

Windows ワークステーションでは、LocalSystem、管理者、標準ユーザー、サービス アカウント、および個別の対話型またはリモート セッションの下でプロセスが実行される可能性があります。また、侵害されたアカウントの下で動作するサードパーティ製ソフトウェア、スクリプト、診断ツール、マルウェアが含まれている可能性もあります。

パイプ名を知っており、十分なアクセス権限を持つプロセスであれば、誰でも接続を試みることができます。Windowsは、開発者がどの実行ファイルでそのパイプを使用することを意図していたかを、本質的には把握していません。

そのため、名前付きパイプは公開されたローカルインターフェースとして扱う必要があります。リクエストを処理する前に、アプリケーションは、誰が接続したのか、その識別子が何を許可されているのか、そして提供されたデータが安全かどうかを判断しなければなりません。

識別情報、アクセス制御、および権限の境界

特権を持つ Windows サービスが、それより特権の低いデスクトップ アプリケーションと通信する場合、リスクが最も高くなります。

LocalSystemとして実行されているサービスは、保護されたファイルやレジストリキーの変更、プロセスの起動、システム構成の変更、他のユーザーのデータへのアクセス、あるいはカーネルドライバとの通信が可能である場合があります。これらの操作が名前付きパイプを通じて公開されると、そのパイプは特権機能への API となってしまいます。

接続が成功したとしても、それは単にクライアントがパイプを開くことを許可されていたことを示すに過ぎません。以下のことは証明されません:

  • クライアントが想定されたアプリケーションであること;
  • 接続したユーザーが権限を有していること;
  • 要求された操作が許可されていること;
  • 指定されたコマンドが安全であること。

したがって、パイプの権限は明示的に定義し、適切な最小限の識別子のセットに制限する必要があります。「Everyone」、「Authenticated Users」、またはすべての対話型ユーザーに対する広範な権限を付与すると、無関係なプロセスがパイプにアクセスできるようになる可能性があります。

また、認証と認可は常に分離しておく必要があります。ユーザーにはサービスの状態を照会することは許可されていても、サービスの停止、保護された設定の変更、プロセスの起動、または任意のファイルへのアクセスは許可されない場合があります。機密性の高いコマンドについては、個別に認可を行う必要があります。

なりすましは、クライアントのセキュリティコンテキストの下で操作を実行する上で役立つ場合がありますが、慎重に扱う必要があります。サーバーは、なりすましが成功したことを確認し、なりすまし中に実行される作業を制限し、常に元のIDを復元する必要があります。

AIを活用したサイバー攻撃に対抗する

過剰な権限が、AIツールを深刻なセキュリティリスクに変えてしまう仕組みをご覧ください。

実用的なゼロトラスト戦略が、AIを活用した脅威が拡散する前に封じ込めるのにどのように役立つかをご覧ください。

ブログを読む

信頼できないサーバー、コマンド、およびデータ

サーバーがクライアントを検証するのと同様に、クライアントもサーバーを検証する必要があります。

予測可能なパイプ名は単なる識別子に過ぎません。それは秘密情報ではなく、どのプロセスがそのパイプを作成したかを証明するものでもありません。攻撃者は、正当なサーバーが起動する前に予想される名前を使用してパイプを作成し、クライアントを攻撃者が制御するプロセスに接続させることがあります。

「first-pipe-instance」オプションを使用すると、その名前がすでに使用されていることを検出するのに役立ちますが、適切なアクセス制御やサーバーの身元確認に代わるものではありません。

パイプを通じて受信したメッセージも、信頼できない入力として扱う必要があります。認証済みのクライアントであっても、次のようなものを送信する可能性があります:

  • 不正な形式またはサイズ超過のペイロード;
  • 無効なファイルパスやレジストリパス;
  • サポートされていないコマンドの組み合わせ;
  • 破損したシリアライズ済みオブジェクト;
  • エラー状態を引き起こすように設計された値。

このような入力をファイル、レジストリ、プロセス、またはコマンドライン操作に直接変換する特権サービスは、「混乱した代理人(confused deputy)」となる可能性があります。つまり、攻撃者が指示を提供し、サービスが特権を提供する形になります。

リクエストでは、厳格なメッセージの枠組み、サイズ制限、コマンドの許可リスト、スキーマ検証、パスの正規化、操作固有の認証、および安全なエラー処理を採用する必要があります。

可用性とリモートへの露出

名前付きパイプのセキュリティ上の問題は、権限の昇格や不正なコマンドだけに限定されません。

悪意のあるプロセスや誤動作するプロセスは、繰り返し接続したり、接続を開いたままにしたり、不完全なメッセージを送信したり、過度の CPU、メモリ、またはカーネルリソースを消費するリクエストを送信したりする可能性があります。

サーバーは、必要に応じて、接続制限、タイムアウト、キャンセル、メッセージサイズの制限、同時実行数の制御、およびレート制限を使用する必要があります。

また、すべての名前付きパイプがローカルコンピュータからのみアクセス可能であると想定することも安全ではありません。Windows の名前付きパイプは、一部の構成においてリモートアクセスをサポートしています。

ローカル IPC 専用に意図されたパイプは、NT AUTHORITYNETWORK などのネットワーク ID を明示的にブロックするか、ローカルのみでの通信を保証するメカニズムを使用する必要があります。

正しい脅威モデルは単純です。クライアントまたはサーバーの ID、権限、要求された操作、およびメッセージの内容がすべて検証されるまでは、すべての名前付きパイプ接続を潜在的に敵対的なものと見なすべきです。

名前付きパイプがセキュリティ境界となる場合

ネームドパイプは、その両端のプロセスが異なる権限で実行されている場合、または異なる信頼レベルの下で動作している場合に、セキュリティ境界となります。

一般的な例としては、LocalSystemとして実行される Windows サービスと、標準ユーザーアカウントで実行されるデスクトップアプリケーションが挙げられます。サービスは、保護されたファイルやレジストリキーの変更、プロセスの起動、システム全体の設定変更、他のユーザーに属するデータへのアクセス、あるいはカーネルドライバとの通信が可能である場合があります。一方、デスクトップアプリケーションは通常、これらの操作を直接実行することはできません。

サービスが名前付きパイプを介してコマンドを受け入れると、そのパイプは、そうした特権的な機能へのインターフェースとなります。パイプの権限、身元確認、コマンドの検証、または認可ロジックに何らかの脆弱性が存在する場合、信頼できないローカルプロセスがサービスの特権を悪用する可能性があります。

接続が成功したからといって、そのクライアントが想定されたアプリケーションであることを証明するものではありません。それは、接続したプロセスがパイプを開くのに十分な権限を持っていたことを示すに過ぎません。同じユーザーアカウントで実行されている別のプロセスも、まったく同じアクセス権を持っている可能性があります。したがって、サーバーは、プロセス名、実行ファイルのパス、またはパイプ名の秘密性に依存するのではなく、接続の背後にあるセキュリティIDを検証する必要があります。

また、サーバーは各操作について個別に認可を行う必要があります。サービスのステータス照会が許可されているクライアントであっても、サービスの停止、保護された設定の変更、プロセスの起動、または任意のファイルへのアクセス要求が自動的に許可されるべきではありません。

認証は誰が接続したかを決定し、認可はそのアイデンティティが何を行えるかを決定します。

この区別は、サーバーがクライアントが制御するパス、コマンドライン引数、レジストリの場所、実行ファイル名、またはシリアル化されたコマンドを処理する場合に特に重要です。厳格な検証が行われない場合、サービスは「混乱した代理人(confused deputy)」となってしまいます。つまり、クライアントがアクションを選択するものの、特権を持つサービスがそれを実行してしまうのです。

たとえば、次のような一見無害に見えるリクエストがあります。

ファイルの読み取り: C:ProgramDataProductstatus.json

も、クライアントがパスを次のように置き換えることができれば、危険なものになり得ます:

ファイルの読み取り: C:WindowsSystem32configSAM

この問題は、プロセスの起動、ファイルの削除、レジストリ値の更新、コンポーネントのインストール、またはドライバとの通信を行うリクエストにも当てはまります。サービスは、コマンドの構文が正しいかどうかを単に検証するだけでは不十分です。接続された識別子が、その特定のリソースに対して、まさにその操作を実行する権限を持っていることを確認しなければなりません。

したがって、安全な名前付きパイプサーバーは、特権リクエストを実行する前に、以下のチェックをいくつか実施する必要があります:

  • 接続されたクライアントの Windows 識別情報を検証する;
  • 明示的なパイプセキュリティ記述子を通じてアクセスを制限する;
  • 各コマンドを個別に承認する;
  • すべてのパス、引数、識別子、およびペイロードサイズを検証すること;
  • サポートされていない、または曖昧な操作を拒否すること;
  • 汎用的な特権機能の公開を避ける。

最後の点は極めて重要です。「この値を任意のレジストリキーに書き込む」といったコマンドは、「この特定のアプリケーション設定を更新する」といった狭義に定義されたコマンドよりも、はるかに大きな攻撃対象領域を生み出します。パイププロトコルが汎用的になればなるほど、特権のあるローカルAPIに似てくるため、セキュリティ対策もより慎重に行う必要があります。

正しい設計原則は単純明快です。パイプサーバーは、接続されたクライアントが要求したという理由だけで操作を実行してはなりません。誰が要求したか、その身元が承認されているか、そして要求が厳密に定義されたセキュリティ境界内にとどまっているかを確認した上で、初めて操作を実行すべきです。

アクセス制御とクライアントの認証

名前付きパイプサーバーは、メッセージの処理を開始する前に、誰が接続できるかを決定する必要があります。これは、特定のユーザー SID、サービスアカウント、管理者グループ、またはログオンセッションなど、必要な Windows 識別体にのみアクセスを許可する明示的なセキュリティ記述子から始まります。

パイプの DACL は、名前付きパイプの両端へのアクセスを制御します。クライアントが接続を試みると、Windows はそのクライアントのアクセストークンと要求された権限を、その DACL と照合します。デフォルトの記述子に依存することは危険です。その権限が、アプリケーションが必要とする範囲よりも広すぎる可能性があるためです。

パイプへのアクセスは、利用可能なすべてのコマンドを自動的に許可するものではありません。クライアントは、ステータス情報の取得は許可されていても、設定の変更、プロセスの起動、または保護されたファイルへのアクセスは拒否される場合があります。したがって、認証は接続確立時の1回だけでなく、機密性の高い操作ごとに実行する必要があります。

ローカルでのアプリケーション間通信の場合、アプリケーションはパイプの反対側に関連付けられたプロセスを確認することもできます。

  • サーバーはGetNamedPipeClientProcessId を呼び出すことができます。
  • クライアントはGetNamedPipeServerProcessId を呼び出すことができます。

これらの Windows API は、接続されたクライアントまたはサーバーに関連付けられたプロセス識別子を返します。これらは、パイプ接続が確立された後にのみ呼び出す必要があります。

次の C# ヘルパー関数は、ネイティブの Windows API を使用して相手側の PID を取得します:

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeClientProcessId(SafePipeHandle pipe, out uint clientProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetNamedPipeClientProcessId(SafePipeHandle pipe, out uint clientProcessId);

また、kernel32.dll の別の関数であるQueryFullProcessImageName を呼び出すことで、PROCESS_QUERY_INFORMATIONまたはPROCESS_QUERY_LIMITED_INFORMATION で開かれたプロセスハンドルから実行ファイルのパスを取得することもできます。 返されたパスは、追加の検証手順として、想定される実行ファイルの場所と比較することができます。

サーバー側では、接続を受け入れた直後、コマンドの読み取りや実行を行う前に検証を行う必要があります。

想定される実行ファイルは、標準ユーザーが変更できないディレクトリに配置する必要があります。そうしないと、攻撃者が想定されるパスを維持したままファイルを置き換えてしまう可能性があります。

より強力な検証を行うために、アプリケーションは実行ファイルの Authenticode 署名を検証したり、承認済みの暗号ハッシュと比較したりすることもできます。Windows では、署名付き実行ファイルの検証にWinVerifyTrust が提供されています。

ただし、PIDおよび実行ファイルパスのチェックは、主要な認証メカニズムではなく、二次的な制御手段にとどめる必要があります。セキュリティ研究により、名前付きパイプのクライアントに対して報告されるPIDを偽装する方法や、接続済みのパイプハンドルを別のプロセスに転送する方法が実証されています。返されるPIDは、接続を開いたプロセスを特定することはできても、現在どのプロセスが各メッセージを送信しているかを証明するものではありません。

したがって、安全な実装では、以下の複数の制御を組み合わせる必要があります。

  • 明示的かつ制限的なパイプ DACL;
  • クライアントの Windows 識別情報または SID の検証;
  • 各特権コマンドに対する認可;
  • メッセージ内容の厳格な検証;
  • 多層防御の一環として、オプションでPID、実行ファイルのパス、署名、またはハッシュの検証。

IDの検証に失敗した場合、または検証を完了できない場合は、接続を拒否する必要があります。特権サービスは、パイプ接続自体が成功したという理由だけで、リクエストを受け入れるようなフォールバックを行ってはなりません。

なりすましと特権操作

名前付きパイプサーバーは、多くの場合、それに接続しているクライアントよりも高い権限で実行されます。 たとえば、Windows サービスはLocalSystem として実行される一方、クライアントアプリケーションは標準ユーザーアカウントの下で実行される場合があります。サービスが要求されたすべての操作を自身の ID で実行すると、クライアントは、直接アクセスできなかったファイル、レジストリキー、プロセス、およびシステムリソースに間接的にアクセスできるようになる可能性があります。

名前付きパイプのなりすましにより、サーバーは接続されたクライアントのセキュリティ コンテキストの下で一時的にコードを実行できるようになります。これにより、Windows はサービス アカウントのトークンではなく、クライアントのトークンを使用してリソースへのアクセスを評価します。

.NET では、NamedPipeServerStream.RunAsClientメソッドを使用することで、接続されたクライアントになりすますことを制御された方法で実現できます。

server.WaitForConnection();

server.RunAsClient(() =>
{
    string path = @"C:ProgramDataMyApplicationsettings.json";

    // 接続されたクライアントの識別情報を使用してアクセスがチェックされます。
    string content = File.ReadAllText(path);

    ProcessClientData(content);
});

このアプローチは、クライアント自身の Windows アカウントがすでに権限を持っている場合にのみ、クライアントが操作を実行できるようにする必要がある場合に役立ちます。たとえば、ユーザーが所有するファイルの読み取り、ユーザー固有のレジストリキーへのアクセス、またはクライアントが保護されたリソースにアクセスできるかどうかを検証する際に、なりすましを使用できます。

ただし、インパーソネーションは認証の代わりにはなりません。サーバーは、クライアントがその操作を要求する権限を持っているかどうかを依然として確認する必要があります。インパーソネーションは、Windows がアクセスチェックを実行するセキュリティコンテキストを変更するだけのものであり、コマンド自体が適切かどうかを判断するものではありません。

特権を持つサービスは、クライアントの識別子とサービスの識別子との間で不必要に切り替えることも避けるべきです。サービスに対してファイルを読み取り、その内容を構成としてインストールするよう要求するリクエストを例に考えてみましょう。

ファイルの読み取りはクライアントを偽装した状態で行われるかもしれませんが、インストールは後でLocalSystem の権限下で行われる可能性があります。その場合、リクエストの一部が偽装の下で処理されたとしても、クライアントは依然として特権操作に影響を与える可能性があります。

より安全な設計としては、操作を明確に定義された段階に分割することです。

  1. クライアントの認証と認可を行う。
  2. クライアントが制御するすべてのパス、引数、およびデータを検証する。
  3. クライアントの権限を使用すべき操作にのみ、インパーソネーションを行う。
  4. 厳密に定義された特権作業を実行する前に、サービス ID に戻す。
  5. なりすまし段階から特権段階へ移行するデータはすべて、再度検証を行う。

なりすましの範囲は可能な限り狭くすべきです。長時間実行される処理、コールバック、非同期操作、および無関係なサービスロジックは、クライアントのIDの下で実行してはなりません。

ネイティブの Windows API を使用する場合も、同様のパターンが適用されます:

[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ImpersonateNamedPipeClient(SafePipeHandle pipe);

[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RevertToSelf();

サーバー側は、ImpersonateNamedPipeClientが成功したかどうかを確認し、必ずfinallyブロック内でRevertToSelfを呼び出す必要があります:

if (!ImpersonateNamedPipeClient(server.SafePipeHandle))
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

try
{
    // 接続されたクライアントのセキュリティコンテキストで実行されます。
    PerformClientScopedOperation();
}
finally
{
    if (!RevertToSelf())
    {
        throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

失敗時の処理は極めて重要です。なりすましが失敗したにもかかわらずサービスが処理を続行すると、その操作がサービスの本来の特権 ID で実行されてしまう可能性があります。したがって、なりすましの試みが失敗した場合は、サーバーアカウントに黙ってフォールバックするのではなく、リクエストを拒否する必要があります。

この原則は、なりすまし処理の後にも適用されます。アプリケーションは、別のクライアントを処理したり、無関係な作業を実行したりする前に、確実に元の識別情報を復元しなければなりません。そうしないと、その後の操作が誤って前のクライアントのコンテキスト下で実行されてしまう可能性があります。

特権パイプコマンドも、範囲を限定し、特定の目的に特化したものであるべきです。次のようなコマンドは:

任意のレジストリキーに任意の値を書き込む

というコマンドは、次のようなコマンドよりもはるかに大きな攻撃対象領域を生み出します:

アプリケーションの承認済みポリシー設定を更新する

サービスは、単にそれらの操作を実行できるという理由だけで、汎用的なファイルアクセス、レジストリの変更、プロセスの作成、またはコマンドの実行を公開してはなりません。各特権コマンドは、どのリソースにアクセスできるか、どの値が受け入れられるか、およびどのクライアントIDがそれを呼び出せるかを正確に定義する必要があります。

なりすましは、より広範なセキュリティ設計の一層として使用された場合に最も効果的です。サーバーは、依然として制限的なパイプ権限を適用し、接続されたクライアントを検証し、各コマンドを承認し、すべてのリクエストを検証し、特権操作の範囲を狭く保つ必要があります。

パイプメッセージを信頼できない入力として扱う

名前付きパイプに接続されているプロセスを検証したからといって、そのメッセージが安全になるわけではありません。正当なアプリケーションであっても、侵害されていたり、脆弱性を含んでいたり、ユーザーが制御するデータをパイプに渡したりする可能性があります。また、悪意のあるプロセスが有効なパイプハンドルを取得したり、継承したりする可能性もあります。

このため、名前付きパイプを通じて受信したすべてのメッセージは、信頼できない入力として扱う必要があります。サーバーは、特権アクションを実行する前に、メッセージの構造と、そのメッセージが要求する操作の両方を検証する必要があります。

危険な実装では、リクエストをデシリアライズして直接実行してしまう可能性があります:

PipeRequest request = Deserialize(data);

File.WriteAllText(request.Path, request.Content);

request が期待される構造を持っていても、PathContentなどの値はクライアントによって制御されたままです。そのため、特権を持つサービスに対して、アプリケーションディレクトリ外のファイルを上書きしたり、保護された構成を変更したり、過剰なディスク容量を消費したりするよう指示される可能性があります。

より安全なアプローチは、厳密に定義されたコマンドのみを公開し、すべてのフィールドを検証することです:

private static void ProcessRequest(PipeRequest request)
{
    if (request == null)
        throw new InvalidDataException("リクエストがありません。");

    switch (request.Command)
    {
        case PipeCommand.UpdateConfiguration:
            ValidateConfiguration(request.Configuration);
            UpdateApprovedConfiguration(request.Configuration);
            break;

        case PipeCommand.GetStatus:
            ReturnApplicationStatus();
            break;

        default:
            throw new InvalidDataException("サポートされていないコマンドです。");
    }
}

このプロトコルでは、次のような汎用的な操作は避けるべきです:

WriteFile(path, content)
StartProcess(path, arguments)
SetRegistryValue(key, name, value)
ExecuteCommand(command)

これらのコマンドでは、クライアントが特権操作とその対象の両方を選択できてしまいます。許可される動作がサーバーによって制御される、アプリケーション固有のリクエストを優先してください:

UpdateApplicationConfiguration(configuration)
RequestApplicationRepair()
InstallApprovedUpdate(updateId)
GetServiceStatus()

メッセージの構造とサイズの検証

アプリケーションが意図的にメッセージ送信モードを使用しない限り、名前付きパイプ接続はバイトストリームです。1回のRead呼び出しでアプリケーションメッセージ全体が返されるとは保証されず、サーバーは読み取り境界がリクエスト境界と一致すると仮定してはなりません。

プロトコルでは、固定サイズのヘッダーの後に長さがプレフィックスで指定されたペイロードが続くなど、明確なメッセージのフレーミングを定義する必要があります:

[バージョン][コマンド][ペイロード長][ペイロード]

メモリを割り当てるか、ペイロードを読み取る前に、宣言された長さを検証する必要があります:

private const int MaxMessageSize = 1024 * 1024;

private static async Task<byte[]> ReadPayloadAsync(
    Stream pipe,
    int payloadLength,
    CancellationToken cancellationToken)
{
    if (payloadLength < 0 || payloadLength > MaxMessageSize)
        throw new InvalidDataException("ペイロードの長さが無効です。");

    byte[] payload = new byte[payloadLength];
    int offset = 0;

    while (offset < payload.Length)
    {
        int read = await pipe.ReadAsync(
            payload,
            offset,
            payload.Length - offset,
            cancellationToken);

        if (read == 0)
            throw new EndOfStreamException(
                "メッセージの受信が完了する前にパイプが閉じられました。");

        offset += read;
    }

    return payload;
}

最大サイズが設定されていない場合、攻撃者は非常に大きなペイロードを宣言し、サービスに過剰なメモリの割り当てを強制する可能性があります。また、アプリケーションでは、コレクションのサイズ、文字列の長さ、ネストの深さ、およびデシリアライザが受け入れるオブジェクトの数についても制限を設ける必要があります。

型だけでなく値も検証する

デシリアライゼーションが成功したとしても、それはペイロードが期待されるオブジェクト型に変換できたことを示すに過ぎません。値が許容範囲内であることを証明するものではありません。

たとえば、ファイルパスは正規化され、承認されたディレクトリと照合される必要があります:

private static string ValidatePath(
    string suppliedPath,
    string allowedDirectory)
{
    string fullPath = Path.GetFullPath(suppliedPath);
    string fullDirectory = Path.GetFullPath(allowedDirectory)
        .TrimEnd(Path.DirectorySeparatorChar)
        + Path.DirectorySeparatorChar;

    if (!fullPath.StartsWith(
            fullDirectory,
            StringComparison.OrdinalIgnoreCase))
    {
        throw new UnauthorizedAccessException(
            "要求されたパスは許可されたディレクトリの範囲外です。");
    }

    return fullPath;
}

この原則は、レジストリパス、プロセス引数、URL、識別子、更新パッケージ、および設定値にも同様に適用されます。サーバーは、既知の危険な値をブロックしようと試みるのではなく、各値を許可リストまたは厳密に定義された範囲に対して検証する必要があります。

パスチェックでは、シンボリックリンク、ジャンクション、再解析ポイント、およびチェック時と使用時の競合についても注意が必要です。機密性の高いファイル操作の場合、文字列パスだけの検証では不十分な場合があります。

無効なリクエストを安全に拒否する

形式が不正なメッセージや未承認のメッセージは、部分的な処理を続行することなく拒否する必要があります。サーバーは、スタックトレース、内部パス、セキュリティトークン、または詳細な例外情報をクライアントに返さないようにすべきです。

パイプを通じて送信されるエラーには、小規模で管理された一連の応答コードを使用する必要があります:

public enum PipeResult
{
    Success,
    InvalidRequest,
    Unauthorized,
    UnsupportedCommand,
    InternalError
}

詳細な診断情報は保護されたサービスログに記録し、クライアントには障害の処理に必要な情報のみを返すようにします。

したがって、各リクエストは予測可能な一連の処理を経る必要があります:

  1. 境界が定義されたメッセージを読み取る。
  2. プロトコルのバージョンとメッセージ構造を検証する。
  3. 接続されたクライアントの認証および認可を行う。
  4. クライアントが制御するすべての値を検証する。
  5. 厳密に定義された操作のみを実行する。
  6. 制御された応答を返す。

名前付きパイプは単なる転送メカニズムに過ぎません。それ自体は、データの信頼性を保証したり、正しいメッセージのフレーミングを保証したり、接続されたプロセスによる悪意のあるリクエストの送信を防止したりするものではありません。プロトコルの遵守と、それを通じて公開されるすべての操作の保護については、受信側のアプリケーションが引き続き責任を負います。

サービス拒否(DoS)およびリモートアクセスに関するリスク

名前付きパイプのエンドポイントは、不正なコマンドから保護されていても、サービス拒否攻撃に対しては依然として脆弱である可能性があります。攻撃者が特権操作を実行するために必ずしも権限を必要とするわけではありません。正当なアプリケーションがサービスと通信できないようにするだけで、製品の機能を妨害するのに十分である場合があります。

悪意のあるプロセスや誤動作するプロセスは、パイプに繰り返し接続したり、利用可能なインスタンスをすべて占有したり、完全なメッセージを送信せずに接続を開いたままにしたり、切断された後に継続的に再接続したりする可能性があります。サーバーインスタンスがすべて占有されると、正当なクライアントは接続を確立できなくなる可能性があります。

接続が受け入れられた後も、同様のリスクが存在します。クライアントは、データの送信を極端に遅くしたり、大きすぎるペイロードを宣言したり、メッセージの途中で送信を停止したり、有効ではあるがリソースを大量に消費するリクエストでサーバーを飽和させたりする可能性があります。制限がなければ、これらの動作によってスレッド、タスク、メモリ、CPU 時間、ハンドル、および内部リクエストキューが消費される可能性があります。

名前付きパイプのバッファも、カーネルの非ページングプールを消費します。したがって、パイプインスタンスの数とバッファに格納されるデータ量は、システムリソースによって制限されます。制限なくインスタンスを作成したり、不必要に大きなバッファを選択したりすると、リソースの枯渇を招く可能性があります。

防御的なサーバーでは、以下の項目について明確な制限を設ける必要があります:

  • 同時接続数およびパイプインスタンス数;
  • メッセージおよびフィールドのサイズ;
  • リクエストの確立および完了に許容される時間;
  • クライアントごとの保留中のリクエスト数;
  • 並行して実行される負荷の高い操作の数;
  • リクエストの頻度;
  • 内部キューの容量。

ブロッキング操作はキャンセルに対応すべきであり、クライアントからのさらなるデータ送信を無期限に待機してはならない。クライアントが時間、サイズ、またはリクエストの制限を超えた場合、サーバーはその接続を終了し、速やかにリソースを解放すべきである。

制限は、負荷の高い処理が開始される前に適用されるべきである。例えば、サーバーは、対応するバッファを割り当てる前に、宣言されたペイロードサイズが過大である場合はそれを拒否すべきである。同様に、認証や基本的なリクエストの検証は、ディスクアクセス、プロセスの作成、暗号処理、データベースクエリ、またはカーネルドライバとの通信が行われる前に実施されるべきである。

また、アプリケーションは、接続ごとに制限のないワーカースレッドを1つずつ作成することを避けるべきである。制限付きの並行処理モデルを採用することで、多数の接続クライアントによってプロセスのスレッドプールが枯渇したり、制御不能なバックログが発生したりするのを防ぐことができる。レート制限は、アプリケーションのアーキテクチャに応じて、接続ごと、プロセスごと、ユーザーIDごと、またはログオンセッションごとに適用される。

ただし、可用性の制御はクライアントのPIDのみに依存してはなりません。プロセスは繰り返し再起動したり、複数のプロセスを使用したり、同じユーザーアカウントで接続を確立したりする可能性があります。複数のシグナルを組み合わせて考慮する必要がある場合もあり、クライアントごとの制御が存在する場合でも、サーバーはグローバルな制限を維持しなければなりません。

もう 1 つ、見過ごされがちなリスクとして、リモートからのアクセスがあります。Windows の名前付きパイプは、必ずしもローカル コンピュータ内の通信に限定されているわけではありません。ネットワークを介したコンピュータ間の通信もサポートしており、Microsoft によれば、Windows Server サービスが実行されている場合、名前付きパイプにリモートからアクセスできる可能性があります。

つまり、ローカルパイプ名を使用すること自体が、ローカルのみでの通信を保証するものではない。ローカルサービスとローカルのデスクトップアプリケーション間の通信を目的とするパイプでは、その要件を明示的に強制する必要がある。

ネイティブパイプサーバーでは、PIPE_REJECT_REMOTE_CLIENTS を指定することで、Windows がリモート接続を自動的に拒否するように設定できます。このオプションがない場合、リモートクライアントが受け入れられ、パイプのセキュリティ記述子に基づいて評価される可能性があります。

また、パイプのアクセス制御リスト (ACL) を使用して、NT AUTHORITYNETWORK識別体へのアクセスを拒否することも可能です。アクセスを 1 つの対話型セッションに制限する必要がある場合、サーバーは、ローカルユーザーとリモートユーザーが共有する広範なグループではなく、適切なログオン SID に対してアクセス権を付与することができます。

これらの保護策は、互いに代替手段として扱うのではなく、組み合わせて使用すべきです:

  • APIがサポートしている場合は、パイプの作成時にリモートクライアントを拒否する;
  • パイプのセキュリティ記述子でネットワーク識別体を拒否する;
  • 必要なユーザーまたはログオン セッションにのみアクセスを許可する;
  • 接続されたプロセスの身元を確認する;
  • 接続、タイムアウト、サイズ、および同時接続数の制限を適用する。

サービス拒否(DoS)対策およびリモートアクセス制限は、パイプのセキュリティモデルの一部です。名前付きパイプサーバーは、単に不正なコマンドを拒否するだけでは安全とは言えません。正当なクライアントに対しては利用可能であり続け、接続がローカルコンピュータ以外から発信されることを許可するかどうかを強制する必要があります。

セキュアな名前付きパイプアーキテクチャの設計

安全な名前付きパイプの設計では、公開される操作の数と、クライアントが制御するデータを直接処理する特権コードの量を最小限に抑える必要があります。パイプは、オペレーティングシステムへの汎用インターフェースとしてではなく、狭い通信境界として機能すべきです。

実用的なアーキテクチャでは、接続処理、検証、認証、および特権実行を分離します。

Architecture

クライアントは、汎用的な特権機能と直接通信してはなりません。その代わりに、パイプゲートウェイに対して厳密に定義されたリクエストを送信する必要があります。ゲートウェイはメッセージ形式を検証し、構造化されたリクエストのみを認証レイヤーに渡します。特権処理は、すべてのセキュリティチェックが成功した後にのみ開始されます。

パイププロトコルは狭く保つ

パイププロトコルは、オペレーティングシステムのプリミティブではなく、ビジネス操作を公開すべきです。

たとえば、アプリケーションは、ポリシーの更新を要求したり、承認された更新をインストールしたり、サービスのステータスを取得したり、特定の設定値を更新したりすることを正当に必要とする場合があります。通常、任意のファイルへの書き込み、任意のレジストリキーの変更、任意の実行ファイルの起動、またはコマンドライン命令の実行といった、制限のないコマンドは必要としません。

操作範囲を限定することで、認証と検証が現実的に行えるようになります。サーバーは、各コマンドがどのリソースにアクセスできるか、どのフィールドが期待されるか、そしてどのクライアントIDがそれを呼び出せるかを把握しています。

優れたプロトコルには以下が含まれるべきです:

  • 明示的なプロトコルバージョン;
  • 固定されたリクエストタイプのセット;
  • 一意のリクエスト識別子;
  • ペイロードサイズの制限;
  • 予測可能なレスポンスおよびエラー形式;
  • サポートされていないメッセージや形式不備のメッセージに対する明確なルール。

サーバーは、未知のバージョン、コマンド、フィールド、および状態を、寛容に解釈しようとするのではなく、拒否すべきである。

接続アクセスとコマンドの権限を分離する

パイプへの接続許可は、そのパイプを通じて公開されているすべての機能を使用する許可を意味するものではない。

パイプのセキュリティ記述子では、接続を確立できる Windows 識別子を制限する必要があります。接続後、サーバーはクライアントを識別し、各コマンドを個別に承認する必要があります。

これにより、同じサービスを通じて異なる信頼レベルをサポートすることが可能になります。例えば、一般ユーザーにはステータスの照会のみを許可し、保護された設定の変更は管理者または信頼された管理プロセスのみに限定することができます。

特に機密性の高い操作については、個別の名前付きパイプを使用することが望ましい場合があります:

Product.Status       読み取り専用情報
Product.UserActions  制限付きユーザー操作
Product.Admin        管理操作
Product.Internal     信頼されたコンポーネント間の通信

これにより、各パイプに独自のアクセス制御ルール、メッセージ制限、およびサポートされるコマンドセットを設定できます。これは通常、すべての操作を1つの大規模なプロトコルにまとめ、内部のコマンドチェックのみに完全に依存するよりも安全です。

ただし、パイプを追加したからといって、自動的にセキュリティが向上するわけではありません。新しいエンドポイントが追加されるたびに攻撃対象領域が拡大するため、それぞれを個別に保護する必要があります。パイプの分離は、それらが真に異なる信頼境界を表す場合にのみ行うべきです。

多層的な本人確認の実施

単一の本人確認を決定的なものとみなしてはなりません。

このアーキテクチャでは、以下の要素を組み合わせることができます:

  • 制限的なパイプ DACL;
  • 接続しているユーザーのSID;
  • クライアントのログオンセッション;
  • 対等プロセスのID;
  • 実行ファイルのパス;
  • 実行ファイルのデジタル署名;
  • アプリケーションレベルのチャレンジ・レスポンス;
  • 操作固有の認証。

プロセスIDや実行ファイルのパスのチェックは、予期しないアプリケーションの検出に役立ちますが、これらはあくまで多層防御の一環として位置づけるべきです。プロセスは変更される可能性があり、ハンドルは継承または譲渡される可能性があり、信頼されたプロセス自体が侵害される可能性もあるからです。

最も堅牢な判断は、表面上の実行ファイル名だけでなく、WindowsのセキュリティIDと厳密に定義された権限に基づいて行うべきである。

特権実行の隔離

パイプメッセージの読み取りを担当するコンポーネントは、特権を伴う処理を可能な限り最小限に抑えるべきである。

接続処理、逆シリアル化、フレーミング、および基本的な検証は、攻撃者が制御する入力にさらされています。このロジックを特権操作から分離しておくことで、パーサーやプロトコルの脆弱性による影響を軽減できます。

特権操作層は、検証済みで強型付けされた命令のみを受け取るべきです。クライアントから直接、生のメッセージバッファ、任意のパス、コマンドライン、またはシリアライズされたオブジェクトを受け取ってはなりません。

機密性の高いアプリケーションの場合、パイプゲートウェイと特権ワーカーを別々のプロセスに分離することで、設計をさらに強化できます。ゲートウェイは特権を制限した状態で実行し、着信リクエストを検証した上で、承認された操作のみを、2つ目の制限付きチャネルを通じて、より小規模な特権コンポーネントに転送します。

この追加のプロセス境界により複雑さは増しますが、LocalSystemやその他の強力なアカウントとして実行される、攻撃にさらされるコードの量を大幅に削減できます。

すべての接続のライフサイクルを管理する

受け入れられた各接続には、明確かつ範囲が限定されたライフサイクルを設定する必要があります:

  1. 接続を受け入れる。
  2. 相手先を識別し、検証する。
  3. 接続レベルの制限を適用する。
  4. 範囲が限定されたリクエストを読み取る。
  5. 要求された操作の承認と検証を行う。
  6. 承認されたアクションを実行する。
  7. 制御された応答を返す。
  8. 接続を切断するか、次のバウンドされたリクエストを待ちます。

サーバーは、認証されていないクライアントが接続を無期限に保持することを許してはなりません。アイドルタイムアウト、リクエストの期限、接続制限、キャンセル、およびバウンドキューは、最初からアーキテクチャの一部として組み込まれるべきです。

長時間実行される操作は、パイプのリーダーを不必要にブロックし続けてはなりません。サービスはリクエストを受け付け、操作識別子を割り当て、クライアントが別のステータスリクエストを通じて進行状況を照会できるようにすることができます。これにより、1 つの接続がサーバーリソースを独占することを防ぎます。

サーバーを権威ある存在にする

クライアントは結果を要求すべきであり、その結果をどのように達成するかはサーバーが決定する。

たとえば、クライアントは識別子に基づいて承認済みのアップデートのインストールを要求する場合があります。サーバーは、パッケージの場所を特定し、その署名を検証し、インストールコマンドを決定し、許可されたインストール先を強制する必要があります。クライアントは、実行ファイルのパス、ダウンロードURL、コマンドライン引数、およびターゲットディレクトリを指定すべきではありません。

これにより、セキュリティ上重要な決定は信頼されたコンポーネント内にとどまり、権限境界を越えるクライアント制御の値の数が減少します。

また、サーバーは、クライアントが以前に下したセキュリティ上の決定を鵜呑みにしてはなりません。「ユーザーは管理者である」、「このファイルは署名されている」、「このパスは安全である」といった主張は、サーバーが独自に検証する必要があります。

セキュリティに関連するアクティビティの監査

安全なアーキテクチャでは、機密データを露出させることなく、不審な動作を調査するのに十分な情報を記録する必要があります。

有用な監査イベントには、次のようなものがあります。

  • 接続の拒否
  • 失敗した本人確認;
  • 不正なコマンド;
  • 形式不備またはサイズ超過のメッセージ;
  • 繰り返されるタイムアウト;
  • 予期しないプロセス識別子;
  • 特権操作およびその結果;
  • 異常な接続またはリクエストの頻度。

ログには、必要に応じて、Windowsユーザー、セッション、ピアPID、コマンドの種類、および結果を特定する必要があります。生の機密情報、認証トークン、および機密性の高いペイロードの全文は、ログに記録してはなりません。

繰り返される失敗は攻撃を示唆している可能性がありますが、クライアントバージョンの不具合や導入上の問題を示している場合もあります。したがって、監査データは、セキュリティ調査と運用上のトラブルシューティングの両方をサポートできるものでなければなりません。

推奨されるアーキテクチャ

ほとんどの特権 Windows サービスシナリオにおいて、防御可能な設計は以下の要素で構成されます:

  • 明示的なセキュリティ記述子を持つローカルのみでの名前付きパイプ;
  • 信頼レベルが本質的に異なるものごとに個別のエンドポイント;
  • Windows 識別情報と対等プロセスの双方の検証;
  • バージョン管理され、長さが制限された、アプリケーション固有のプロトコル;
  • 各コマンドに対する承認;
  • クライアントが制御するすべての値に対する厳格な検証;
  • 短く、厳密に制御されたなりすましスコープ;
  • 小規模な特権実行レイヤー;
  • 接続数、キュー、および実行時間の制限;
  • セキュリティ重視の監査ログ。

中心となる原則は、名前付きパイプが信頼レベル間で可能な限り最小限のインターフェースのみを公開すべきであるということです。安全なアーキテクチャは、任意の特権操作を安全にしようと試みるものではありません。そもそも、任意の特権操作を公開しないようにするのです。

実用的な名前付きパイプのセキュリティチェックリスト

名前付きパイプを通じてアプリケーション機能を公開する前に、設計が以下の各項目を適切に考慮していることを確認してください:

  • 信頼境界を定義する。特に片側が昇格された権限で実行されている場合は、パイプを公開されたローカルインターフェースとして扱う。
  • パイプへのアクセスを明示的に制限する。デフォルトのアクセス許可や「Everyone」などの広範なグループに依存するのではなく、範囲を限定したセキュリティ記述子を使用する。
  • リモートクライアントを拒否する。リモートアクセスが不要な場合は、パイプをローカルのみでの通信に設定し、ネットワーク ID を拒否するように構成する。
  • 両端点を検証する。接続された Windows 識別情報を確認し、必要に応じて、相手側の PID、実行ファイルのパス、およびデジタル署名を確認する。
  • パイプ名を信頼しないでください。予測可能な名前はエンドポイントを識別しますが、それを作成したプロセスの認証にはなりません。
  • すべてのコマンドを承認してください。接続の許可によって、サーバーが公開するすべての操作へのアクセス権が付与されるべきではありません。
  • プロトコルの範囲を狭く保ってください。任意のファイル、レジストリ、プロセス、またはコマンド実行機能ではなく、アプリケーション固有のアクションのみを公開してください。
  • すべてのメッセージを信頼できないものとして扱う。フレーミング、プロトコルバージョン、コマンドタイプ、ペイロードサイズ、フィールド値、パス、およびオブジェクト数を検証する。
  • 制限は早期に適用する。メモリを割り当てたり、負荷の高い処理を開始したりする前に、無効なサイズやサポートされていないリクエストを拒否する。
  • なりすましは慎重に使用する。操作でクライアントの権限を使用する必要がある場合にのみなりすましを行い、その範囲を狭く保ち、なりすましが失敗した場合はクローズド失敗とする。
  • 特権実行を隔離してください。解析と検証を、特権操作を実行するコードから分離してください。
  • リソースの使用を制御する。同時接続数、保留中のリクエスト数、アイドル時間、実行時間、キューの深さ、およびリクエスト頻度を制限する。
  • 制御されたエラーを返す。スタックトレース、内部パス、トークン、その他の機密性の高い実装詳細を公開しないようにする。
  • セキュリティに関連するイベントを監査してください。拒否された接続、失敗したIDチェック、形式不備のリクエスト、不正なコマンド、および特権操作を記録してください。
  • 「Fail Closed」を採用する身元確認、認可、検証、またはなりすましが確実に完了できない場合は、リクエストを拒否する。

安全な名前付きパイプの実装は、単一の保護手段に依存してはなりません。最も強固な設計とは、制限的なアクセス制御、エンドポイントの検証、操作レベルの認可、厳格な入力検証、リソース使用量の制限、および範囲を狭く限定した特権機能の組み合わせによるものです。

ThreatLockerがネームドパイプへの攻撃からどのように保護できるかについて詳しく知りたい場合は、デモをご予約ください。


著者紹介:

Farid Mustafayev氏は、ThreatLockerのソフトウェア開発者であり、Microsoft Windowsサービスの開発とサイバーセキュリティを専門としています。15年以上の業界経験を持ち、ASP.NET WebAPI、Windowsサービス、Windows Forms、WPF、RESTful API、およびWindowsの低レベルな内部構造を含む.NET技術に深い専門知識を有しています。 彼は、マルウェアやランサムウェアからシステムを保護するために設計されたWindowsサービスの開発と強化を主導し、カーネルレベルの統合やカスタムドライバーの機能強化にも携わってきました。

以前はテクニカルリードとして、アーキテクチャの決定を主導し、開発者の指導を行い、スケーラブルで保守性の高いシステムの構築に携わっていました。また、AWS上のマイクロサービスベースのアーキテクチャやクラウドネイティブソリューションに関する経験も持ち、分散環境全体における可用性、パフォーマンス、セキュリティに重点を置いています。

ThreatLockerによる提供・執筆。