pageant.exe の仕組みと危険性

PuTTY で SSH2 接続するとき、毎回パスフレーズを入力するのがめんどくさいという理由で pageant を常時起動してる人も多いのでは。

そんな pageant の仕組みと、使っていく上でのリスクが気になったので、ちょっくらソースを読んでみた。

プロセス間通信の仕組み

PuTTY や WinSCP3 など、pageant を利用するアプリケーションは、何らかの方法で pageant と通信しているはずだ。この通信処理を実装しているのが winpgntc.c であり、PuTTY も WinSCP3 もソースコードに winpgntc.c を含んでいる。

リクエスト側

細かくなるけど、PuTTY や WinSCP3 がリクエストするときの手順は次のようになっている。

  1. pageant の(非表示になっている)ウインドウを FindWindow 関数で探し出す。
  2. CreateFileMapping 関数でプロセス間で共有できるメモリを確保する。メモリ名は「PageantRequest[スレッドID]」。
  3. 共有メモリに所定の形式でリクエスト用の ID やデータを突っ込む。詳細は後述。
  4. 1. で取得したウインドウに WM_COPYDATA メッセージを送る。パラメータの COPYDATASTRUCT には AGENT_COPYDATA_ID(0x804e50ba)と 2. で作成した共有メモリ名が入っている。

pageant 側

次に WM_COPYDATA を受信したあとの pageant.exe の処理をみていく。winpgnt.c の WndProc 関数にて実装されている。

  1. WM_COPYDATA を受け取ったら、パラメータをチェックする(パラメータに AGENT_COPYDATA_ID が渡されていない場合はエラーを返す)。
  2. 同じくパラメータの共有メモリ名をもとに、OpenFileMapping 関数を使って共有メモリを開く。
  3. (WinNT 系のみ) 共有メモリの owner と pageant.exe を起動したユーザーが同じかどうかを確認する。異なる場合はエラーを返す(pageant を起動したユーザー以外がリクエストできなくするため)。
  4. リクエストに応じて処理を行い、結果を共有メモリに書き込む(answer_msg 関数)。

pageant と PuTTY でやり取りされるデータ

通信の仕組みが分かったので、次は SSH2 接続するときの流れを見ていく。

リクエストの種類に応じた ID が定義されている。

SSH1_AGENTC_REQUEST_RSA_IDENTITIES
SSH2_AGENTC_REQUEST_IDENTITIES
SSH1_AGENTC_RSA_CHALLENGE
SSH2_AGENTC_SIGN_REQUEST
SSH1_AGENTC_ADD_RSA_IDENTITY
SSH2_AGENTC_ADD_IDENTITY
SSH1_AGENTC_REMOVE_RSA_IDENTITY
SSH2_AGENTC_REMOVE_IDENTITY
SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES
SSH2_AGENTC_REMOVE_ALL_IDENTITIES

このうち、PuTTY が SSH2 するときには、SSH2_AGENTC_REQUEST_IDENTITIES と SSH2_AGENTC_SIGN_REQUEST のみが利用される。

処理の流れを見てみよう。

  1. PuTTY が接続先のサーバーに SSH2 接続を開始し、公開鍵と公開鍵で署名(暗号化)されたデータを受け取る。署名前のデータはサーバーのみが知っている。
  2. PuTTY は pageant に SSH2_AGENTC_REQUEST_IDENTITIES をリクエストする。この結果、pageant が知っている鍵の一覧を取得できる。秘密鍵ではなく、公開鍵の一覧が渡されることに注意。
  3. PuTTY は 1. で知った公開鍵が、2. の一覧に存在するか調べる。存在する場合は、pageant が秘密鍵を知っていることを意味する。存在しない場合は、pageant の利用を諦める。
  4. PuTTY は 1. でサーバーから得た公開鍵と暗号化されたデータを pageant に渡す。このときのリクエスト ID が SSH2_AGENTC_SIGN_REQUEST。データを秘密鍵で署名してくれ、ということ。
  5. SSH2_AGENTC_SIGN_REQUEST を受け取った pageant は、メモリ上に保有している秘密鍵で与えられたデータを署名して、共有メモリに書き込んで PuTTY に処理を返す。
  6. PuTTY は秘密鍵を知ることなく、秘密鍵で署名されたデータを受け取る。
    • サーバーが公開鍵で署名したデータを、秘密鍵で署名した状態。つまり、サーバーが暗号化する前の原文を知ることができた。
    • これ以降の通信では、共通鍵が用いられるので pageant の出番はない。

注目すべきは、pageant が保有している秘密鍵が外に漏れることはないこと。pageant は秘密鍵は漏らさず、与えられたデータを秘密鍵で署名する機能だけを公開している。

分かったこと

当たり前のことなんだけど、pageant が起動中は(同じユーザー権限で起動している)任意のプロセスが pageant が知っている秘密鍵を使って、データを署名できる

悪意のあるプロセスは、プロセス間通信を利用して SSH 接続を開始するのに十分な情報を知ることができるわけだ。しかも、これらのプロセス間通信は、利用者が気づかないところで行われる(警告のウインドウは表示されない)。

この問題に対処すべく、PuTTY ごった煮版の pageant では、右クリックのメニューに「常に要求を確認」という項目が追加されている。チェックしておくと、何らかのプロセス(PuTTY や WinSCP3 を含む)が秘密鍵での署名を要求してきたときに、次のような確認ダイアログが出るようになる。

悪意のあるプロセスが署名しようとしても、その手前で気づけるわけだ。Windows Vista の UAC のようなイメージ。ちょっと面倒だけど、落としどころの1つとしては妥当な線かもしれない。

もちろん、pageant を使う上でのリスクは、まだまだ残っている。一番でかい危険性は、メモリ上に生の秘密鍵を持っているところ。秘密鍵とパスフレーズが同時にばれたような状態だ。メモリダンプされたら一貫の終わり。pageant が起動した状態でハイバネートすると、生の秘密鍵が HDD に書かれてしまう。

おまけ:ssh-agent は?

OpenSSH FreeBSD 版の ssh-agent にも軽く目を通してみた。こちらは socket でプロセス間通信している模様。送信元プロセスの uid 比較も行っているようだ(参考:ssh-agent.c)。

ssh-agent のマニュアルによると

エージェントは要求されたチャンネルを経由して秘密鍵を送るようなことは決してしません。かわりに、秘密鍵が必要な操作はすべてエージェント側でおこない、結果だけが要求した側に返されるようになっています。このためエージェントを使うことによって秘密鍵がクライアントに漏れるようなことはありません。

SSH-AGENT (1)

とあるので、pageant と似たような仕組みなんだろう。

とはいえ、root 権限を持っている人は、su して ssh-agent と通信はできるはず。試してないけど。信頼できない人が管理しているサーバーで、ssh-agent を利用するのは、たとえ一時的であっても控えたほうがよさそうだ。

ssh-agent のご利用も計画的に。