クラウドコンピューティングが必要ですか? 今すぐ始める

MS-RPC とそのセキュリティメカニズムの概要

Ben Barnea

執筆者

Ben Barnea

December 08, 2022

Ben Barnea

執筆者

Ben Barnea

Ben Barnea は、Akamai の Security Researcher です。Windows、Linux、IoT、モバイルなど、さまざまなアーキテクチャのローレベルなセキュリティ調査や脆弱性調査への関心が高く、豊富な調査経験を有しています。複雑なメカニズムがどのように機能するか、そして最も重要な問題である、どのように失敗するかを知ることに生きがいを感じています。

RPC は、リモートベクトルであるというその性質上、セキュリティの観点から注目すべき点が多くあります。
RPC は、リモートベクトルであるというその性質上、セキュリティの観点から注目すべき点が多くあります。

RPC とは?

リモート・プロシージャ・コール(RPC)は、プロセス間通信(IPC)の一形態です。RPC を使用することで、クライアントは RPC サーバーが公開するプロシージャを呼び出すことができます。クライアントは、通常のプロシージャコールと同じように関数を呼び出すことができるため、リモートインタラクションの詳細をコード化する必要は(ほとんど)ありません。サーバーは、同じマシンまたはリモートマシン上に別のプロセスでホストできます。

この記事では、Microsoft の RPC 実装(MS-RPC)について考察します。MS-RPC は、分散コンピューティング環境のコアである RPC プロトコルのリファレンス実装(V1.1)に基づいています。

RPC は、タスクのスケジュール設定、サービス作成、プリンタと共有の設定、リモートで保存される暗号化データの管理など、Windows のさまざまなサービスで頻繁に使用されています。RPC は、リモートベクトルであるというその性質上、セキュリティの観点から注目すべき点が多くあります。このブログ投稿では、MS-RPC の基本的な仕組みについて、組み込まれているセキュリティメカニズムに焦点をあてながら解説していきます。

MS-RPC の仕組み

プロシージャとそのパラメーターは、Interface Definition Language(IDL)と呼ばれる記述言語で定義されています。各インターフェースには、UUID と呼ばれる一意の識別子が割り当てられます。サーバーとクライアントのどちらも、サーバーが公開する正しいインターフェースにアクセスするために UUID を使用します。

その後、Microsoft IDL コンパイラによって IDL ファイルがコンパイルされ、ヘッダーファイルと、ランタイム機能を含むソースコードファイルが生成されます。専門用語で説明すると、これらはサーバーとクライアントの両方に使用されるスタブです。スタブは、rpcrt4.dll で実装されている RPC ランタイムに制御を渡します。クライアント側の RPC ランタイムは、データをマーシャリングして相手側の RPC ランタイムに渡します(図 1)。

RPC ランタイム

図 1:RPC ランタイム

ネットワーク(またはローカル)の観点から見ると、サーバーとクライアントはどのように通信しているのでしょうか。サーバーは、プロトコルシーケンスとエンドポイントの組み合わせを登録することで、RPC 接続の着信をリッスンします。プロトコルシーケンスは、たとえば、 ncacn_ip_tcp (TCP)、 ncacn_np (名前付きパイプ)、または ncalrpc (LPC)などです。エンドポイントは、TCP 5555 や \\pipe\\example(名前付きパイプが使用される場合)のようなポートです。名前付きパイプは、TCP ポート 445 を介して SMB トランスポートで伝送されます。これには非表示の IPC$ 共有が使用されます。プロトコルシーケンスの全リスト については、 Microsoft の Web サイトを参照してください。

サーバーは何らかのエンドポイントで接続をリッスンしますが、クライアントは接続先をどのように把握するのでしょうか。その答えはエンドポイントのタイプ、つまり動的か well-known(静的)かによって異なります。

動的なエンドポイントとは、サーバー側でエンドポイントマッパーを通じて登録されたエンドポイントです。エンドポイントマッパー(別名 epmapper)は、サービスを実際のエンドポイントにマッピングする RPC サービスです。epmapper は、RPC over HTTP に TCP ポート 135 と 593 を使用します。そのためクライアントは、epmapper を使用してリモートマシンで動的に登録された RPC サーバーをすべて列挙できます( 指定された APIを使用)。

well-known エンドポイントとは、epmapper 経由で登録されなかったエンドポイントのことです。クライアントは、サーバーがあらかじめ登録していたエンドポイントを知っている必要があります。これが可能となるのは、そのエンドポイントがクライアントとサーバーの両方のコードにハードコードされている場合、またはそのエンドポイントが IDL ファイル内にある場合です。以下にプリント・スプーラー・サービスの IDL の例を示します。

[
uuid(12345678-1234-ABCD-EF00-0123456789AB),
version(1.0),
ms_union,
endpoint("ncacn_np:[\\pipe\\spoolss]"),
pointer_default(unique)
]

RPC は、バインディングハンドルを使用した、クライアントとサーバー間の論理接続です。サーバーとクライアントのどちらも、認証情報の設定時などに、関数を使用してバインディングデータを操作します。クライアントがバインディングに認証を設定すると、そのバインディングは認証済みとみなされます。クライアントプログラムが RPC 関数を呼び出すと、ネットワークバインディングが発生します。ネットワークトラフィックの観点から言えば、RPC クライアントは、認証情報を含むパケットでバインド要求を送信し、RPC インタラクションを開始します。サーバーは、bind_ack(確認)または bind_nak(エラーが発生)で応答します。

動的エンドポイント解決を示す Wireshark スニペット

図 2:動的エンドポイント解決を示す Wireshark スニペット

図 2 の Wireshark スニペットには、Task Scheduler インターフェースの動的エンドポイント解決が示されています。クライアントは Task Scheduler のエンドポイント情報を入手すると、新しい接続を作成します。その後、別のバインディングプロセスとの新たな接続が見られ、バインディング認証の一部として AUTH3 パケットが含まれています。

エンドポイントと同様に、バインディングにも、自動、暗黙的、明示的など、いくつかのタイプがあります(図 3)。アプリケーションがバインディングプロセスを制御する度合いはこれらの各タイプで異なります。 

  • 自動バインディング(廃止):クライアントとサーバーのアプリケーションは、バインディングプロセスを処理せず、代わりに RPC ランタイムがこのプロセスを完全に制御します。
  • 暗黙的バインディング:クライアントには、バインディングを実行する前にバインディングハンドルを設定するオプションが与えられます。クライアントがバインディングを確立すると、RPC ランタイムライブラリが残りの処理を行います。
  • 明示的バインディング:クライアントがバインディングハンドルを設定する必要があります。 RPC ランタイムはそれをサーバーに渡すだけです。
異なるバインディングタイプの比較

リモート要求に対する RPC のセキュリティ

ここまで、RPC の基本的な仕組みを説明してきました。ここからは、クライアントが関数にアクセスできないようにするためのアクション、ポリシー、メカニズムについて見ていきます。これは、攻撃と防御の両面で興味深いトピックです。

トランスポート認証

名前付きパイプや HTTP などの一部のトランスポートには、プロトコルの一部として認証が含まれています。たとえば、名前付きパイプは、SMB を介して伝送され、SMB に認証が含まれています。基本的に、サーバーが名前付きパイプエンドポイントを登録すると、そのエンドポイントには有効なユーザーの資格情報を持つクライアントしか接続できません。ドメイン環境では、同じドメイン内にドメインユーザーがいるだけで、認証チェックに合格できます。マシンがドメインに属していない場合、クライアントは、リモートサーバー上のローカルユーザーの資格情報を必要とします(図 4)。除外については後述します。

アクセスを拒否するシステムエラー

図 4:アクセスを拒否するシステムエラーの例 

バインディング認証

サーバーは、バインディング認証を使用することで認証メカニズムを実現できます。バインディング認証を行うためには、サーバーが RpcServerRegisterAuthInfo を呼び出して認証情報を登録する必要があります。クライアントは、サーバーが使用する正しいサービスプリンシパル名と Security Support Provider メソッドを使用する必要があります。使用しないと、サーバーから「RPC_S_UNKNOWN_AUTHN_SERVICE」を受信します。

クライアントは、RpcBindingSetAuthInfo や RpcBindingSetAuthInfoEx などの API を使用して、バインディングに認証および許可データを設定することにより、サーバーに対する認証を実行できます。クライアントは、認証メソッド(NTLM、Kerberos、Negotiate、SCHANNEL など)とサービスプリンシパル名を指定します。

2 つの重要ポイント:

  1. クライアントがバインディングに認証を設定しても、サーバー側が認証情報をまったく登録していない場合、サーバーは「RPC_S_UNKNOWN_AUTHN_SERVICE」を返します。

  2. サーバーが認証バインディングを登録したからといって、クライアントが必ず認証バインディングを使用しなければならないというわけではありません。また、RPC ランタイムは、無効な資格情報を持つクライアント認証バインディングは送出しません。サーバーがどのようにして未認証のバインディングへのアクセスを防止するかについては後述します。

RPC ランタイムが認証情報をグローバルに保存することは注目すべき重要ポイントです。つまり、2 つの RPC サーバーが同じプロセスを共有し、そのうちの 1 つが認証情報を登録すると、もう 1 つのサーバーにも認証情報が与えられます。クライアントは、どちらのサーバーにアクセスするときにもそのバインディングを認証できるようになりました。これは SSPI 多重化と呼ばれています。

エンドポイントセキュリティ

サーバーは、エンドポイントにセキュリティ記述子を設定できます。セキュリティ記述子は、Windows の一般的なアクセス・チェック・セキュリティ・メカニズムです。これによりオブジェクトへのアクセスが許可されるユーザーと拒否されるユーザーに関する「ルール」を作成できます。オブジェクトへのアクセスが試行されると、オペレーティングシステムは呼び出し側のアクセストークンとセキュリティ記述子を比較して、アクセスが許可されているかどうかを確認します。この場合、オブジェクトはエンドポイントであり、アクセストークンはトランスポートプロトコル認証から取得されたクライアントのアクセストークンです。このチェックは、名前付きパイプ、ALPC、HTTP(認証が使用されている場合)などの認証済みトランスポートにのみ適用されます。認証されないトランスポートプロトコルである TCP では、このアクセスチェックは実行されません。

SSPI 多重化と同様に、エンドポイントも多重化されます。インターフェースとエンドポイントはバインドされません。サーバーは、インターフェースとエンドポイントを別々に登録します。 プロセスに複数の登録済みエンドポイントがある場合、このプロセスに登録されている各インターフェースには、これらのエンドポイントのそれぞれからアクセスできます。

たとえば、インターフェースを登録し、さらに \\pipe\mypipe でリッスンするエンドポイントを作成するとします。同じプロセスでホストされている別の RPC サーバーは、TCP 7777 に独自のエンドポイントを登録しました。この場合、このインターフェースには TCP でもアクセスできるようになります。これにより、セキュリティ記述子など、エンドポイントに課されるセキュリティ制限が回避されます(図 5)。したがって、 推奨されるのは、エンドポイントセキュリティに依存しないこと、または想定されている転送プロトコルをクライアントが使用しているかどうかを確認することです。 

エンドポイントの多重化によってエンドポイントセキュリティ記述子がバイパスされる例

図 5:エンドポイントの多重化によってエンドポイントセキュリティ記述子がバイパスされる例

インターフェースセキュリティ

インターフェースのセキュリティについて考察すると、インターフェースのセキュリティを確保する方法は 3 種類あることがわかります。それらは、セキュリティコールバックの設定、インターフェースでのセキュリティ記述子の設定、インターフェースフラグの使用です。

セキュリティコールバックの設定

1 つ目のインターフェース・セキュリティ・メカニズムは、セキュリティコールバックです。セキュリティコールバックは、サーバー開発者が実装するカスタムコールバックです。セキュリティコールバック内のロジックは、開発者に任せられています。このロジックによってサーバーはインターフェースへのアクセスを制限できます。コールバックが RPC_S_OKを返す場合、クライアントは許可されています。

セキュリティコールバックが登録されていると、未認証のクライアントは RPC ランタイムによって自動的に拒否されます。ただし、フラグ RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTHが設定されている場合は拒否されません。 

セキュリティコールバックを設定して、RPC_IF_ALLOW_SECURE_ONLY フラグを設定しても、クライアントの特権レベルが高いとは限りません。 そのため、サーバーはセキュリティコールバックでクライアントの特権レベルを照会するのです。これは、RpcBindingInqAuthClient を呼び出すことによって実行されます。もう 1 つの一般的なチェックとして、クライアントが使用する伝送方法の確認があります。これは名前付きパイプなどのセキュアな認証済みの伝送方法を強制するために行われます。具体的には、RpcBindingServerFromClient → RpcBindingToStringBinding → RpcStringBindingParse を呼び出して、Protseq(プロトコルシーケンス)パラメーターを比較します。これにより、エンドポイント多重化の不正利用を防ぐこともできます。

セキュリティコールバックのキャッシング

セキュリティコールバックが登録されている場合、セキュリティチェック時にコールバックが呼び出されます。セキュリティコールバックが成功した場合(RPC_S_OK が返された場合)、コールバックの結果がキャッシュされます(キャッシュを使用している場合)。次回に同じクライアントがインターフェースで関数を呼び出した際、コールバックの結果がキャッシュされているため、セキュリティコールバックを再び呼び出す代わりに、キャッシュされたエントリーを使用します。

キャッシングの実装は簡単ですが、いくつかの要因に依存します。

  • キャッシングは、クライアントのセキュリティコンテキスト(バインディング)に依存します。したがって、サーバー(またはプロセス内の他のサーバー)が認証バインディングを登録していない場合、またはクライアントが認証バインディングを設定していない場合、その呼び出しではキャッシングが無効になります。
  • サーバーがインターフェースに RPC_IF_SEC_NO_CACHE フラグを登録している場合は、セキュリティコールバックを呼び出すたびに RPC ランタイムによって呼び出しが強制的に起動されるため、キャッシュメカニズムは無効になります。
  • インターフェースフラグの RPC_IF_SEC_CACHE_PER_PROC が指定されていない場合もキャッシュメカニズムに影響します。サーバーがこのフラグを指定している場合、キャッシュは コール ベースとなります。 インターフェース ベースではありません。つまり、インターフェース TEST の関数 X について正常な値がキャッシュされている場合に、インターフェース TEST で関数 Y を呼び出すと、セキュリティコールバックが再び呼び出されます。 

このキャッシュメカニズムにより、クライアントが呼び出す関数に依存するセキュリティコールバックのロジックに脆弱性が生じるおそれがあります。RPC 開発者/監査担当者は、キャッシュメカニズムについて把握しておく必要があります。 Akamai では、キャッシングの実装に関する詳細な調査(キャッシングに起因する脆弱性など)を公開しています

セキュリティ記述子の設定

インターフェースのセキュリティを確保するための 2 つ目のメカニズムは、インターフェースにセキュリティ記述子を設定することです。インターフェースにセキュリティ記述子を登録できるのは、RpcServerRegisterIf3 関数だけです。セキュリティ記述子が設定されていない場合、 デフォルトのセキュリティ記述子は次のとおりです

  • NT AUTHORITY\ANONYMOUS LOGON
  • 全員
  • NT AUTHORITY\RESTRICTED
  • BUILTIN\Administrators
  • SELF

インターフェースフラグの使用

インターフェースのセキュリティを確保するための 3 つ目の方法は、インターフェースフラグを使用することです。このフラグは、インターフェースの作成時に RpcServerRegisterIf* 関数を使用して設定します。セキュリティに関するインターフェースフラグには次のようなものがあります。

RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH — 使用する転送方式や認証レベルにかかわらず、呼び出しのたびにセキュリティコールバックが呼び出されます。 

RPC_IF_ALLOW_LOCAL_ONLY — ローカルのリクエストのみ許可します。

RPC_IF_ALLOW_SECURE_ONLY — 認証レベルが RPC_C_AUTHN_LEVEL_NONE よりも高いクライアントのみ接続できます。このフラグを指定すると、NULL セッションを使用してアクセスするクライアントは拒否されます。このフラグは、クライアントが有効な認証情報を保有していることを保証するだけであり、呼び出し元のユーザーが特権レベルを保有していることを保証するわけではありません。

RPC_IF_SEC_NO_CACHE — このフラグは、インターフェースのセキュリティコールバックのキャッシュを完全に無効にします。

RPC_IF_SEC_CACHE_PER_PROC — このフラグは、キャッシュメカニズムを完全に無効にするのではなく、デフォルトのふるまいをインターフェースベースからコールベースに変更します。

システム全体のポリシー

マシンの種類(クライアント、サーバー、ドメインコントローラー(DC))に応じて、システム全体のポリシーを複数設定できます。

エンドポイントセキュリティに関連するシステムポリシーの 1 つに、「Restrict Unauthenticated RPC Clients(未認証 RPC クライアントを制限)ポリシー」があります。このポリシーには、3 つの値を設定できます。

  1. [Authenticated(認証)]:インターフェースに RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH フラグが設定されていない場合、RPC ランタイムは、事前に認証されていない TCP クライアントへのアクセスを ブロックします。
  2. [Authenticated without exceptions(例外なく認証)]:未認証の接続をすべてブロックします。
  3. [None(なし)]:すべての RPC クライアントが、マシン上で実行されている RPC サーバーに接続できます。""

したがって、RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH フラグが登録されており、TCP 経由でアクセスできるインターフェースは、未認証クライアントのアタックサーフェスとなります。当然、クライアントがインターフェースの関数を呼び出すためには、セキュリティ・コールバック・チェックに成功する必要があります。

前述のとおり、名前付きパイプを使用した接続(SMB 通信)では、SMB プロトコルの一環として独自の認証が行われます。SMB 経由で接続する場合、クライアントは「匿名ログイン」(別名 NULL セッション)を使用できます。関連する 2 つのシステムポリシーとして、「Restrict anonymous access to Named Pipes and Shares(名前付きパイプおよび共有への匿名アクセスを制限)」と「Network access: Named Pipes that can be accessed anonymously(ネットワークアクセス:匿名アクセスを許可する名前付きパイプ)」があります。1 つ目のポリシーを有効にした場合は、2 つ目のポリシーで定義した名前付きパイプのみ匿名でアクセスできます。ワークステーションの場合、2 つ目のポリシーは空となります。つまり、ドメイン内のワークステーションに対しては NULL セッションを使用することはできません。DC マシンの場合、ポリシーには、「\pipe\netlogon」、「\pipe\samr」、「\pipe\lsarpc」などの名前付きパイプがあります。これに目を付けるのが攻撃者です。これらのエンドポイントにはドメイン外のマシンから接続できるためです。

エンドポイントのセキュリティ記述子チェックにおいて、デフォルトでは無効になっているシステムポリシーがあります。それが「Network access: Let Everyone permissions apply to anonymous users(ネットワークアクセス:匿名ユーザーに「全員」権限を適用)」です。これを有効にすると、匿名で接続したユーザー向けに作成されたトークンにセキュリティ識別子「全員」が追加されます。

プロシージャのセキュリティチェック

サーバーのプログラマーは、インターフェース上の関数にセキュリティチェックを含めることができます。つまり、呼び出す関数ごとにチェックのロジックを変更またはカスタマイズできます。一般的なセキュリティチェックとして、図 6 に示すようなアクセスチェック(srvsvc)があります。

一般的なセキュリティチェック(アクセスチェック)の例

図 6:一般的なセキュリティチェック(アクセスチェック)の例

この例では、LocalrSessionGetInfo が SsAccessCheckを呼び出します。 SsAccessCheck は、 RpcImpersonateClientを呼び出してクライアントを偽装し、続いて NtAccessCheckAndAuditAlarm関数を呼び出してアクセスチェックを実行します。さらに、RpcRevertToSelf を呼び出してサーバーのトークンに戻ります。RpcImpersonateClient が失敗に終わった場合、サーバーは、クライアントのプロセスではなく、サーバーのプロセスのセキュリティトークンで実行し続けることになるため、RpcImpersonateClient の戻り値を必ず確認することが重要です。

まとめ

このブログ記事では、MS-RPC とそのセキュリティメカニズムについて詳しく紹介しました。MS-RPC サーバーは大きなアタックサーフェスとなり、新しい脆弱性が発見される可能性もあるため、研究者の皆様には各種 MS-RPC サーバーを調査することを推奨します。研究者の皆様が MS-RPC を調査する際に本ブログ記事がお役に立てば幸いです。 

Akamai の GitHub リポジトリでは、引き続き RPC に関するリソースの作成と編集を行っています。このトピックに関するナレッジと経験を提供してくださった皆様、とりわけ、 James Forshaw 氏および  Carsten Sandker 氏に感謝申し上げます。



Ben Barnea

執筆者

Ben Barnea

December 08, 2022

Ben Barnea

執筆者

Ben Barnea

Ben Barnea は、Akamai の Security Researcher です。Windows、Linux、IoT、モバイルなど、さまざまなアーキテクチャのローレベルなセキュリティ調査や脆弱性調査への関心が高く、豊富な調査経験を有しています。複雑なメカニズムがどのように機能するか、そして最も重要な問題である、どのように失敗するかを知ることに生きがいを感じています。