Security

暗号化されたSSH秘密鍵はなぜ破られる?ssh2johnとjohnの仕組み

海洋

SSHの秘密鍵は、パスフレーズで暗号化しておけば、ファイルを盗まれてもそのままでは使えませんが、パスフレーズが弱ければあまり意味をなしません。この記事では、SSH秘密鍵の突破方法について解説します。

はじめに:暗号化されたSSH秘密鍵とは何か

SSHの公開鍵認証では、手元に置いた「秘密鍵」と、サーバに登録した「公開鍵」のペアで本人確認をします。秘密鍵はいわば合鍵そのもので、これを持っていればパスワードなしでログインできます。

ここで問題になるのが、その秘密鍵ファイルが盗まれた場合です。ファイルをコピーされれば、誰でもなりすませてしまう。そこで多くの秘密鍵は、ファイル自体をパスフレーズで暗号化して保存します。こうしておけば、たとえファイルを盗まれても、パスフレーズを知らない限り鍵として使えません。

つまり秘密鍵の安全はファイルが漏れないことだけでなく、このパスフレーズが大事になってきます。裏を返せば、パスフレーズが弱ければ、鍵が守られているつもりでも守られていないことになります。

flowchart TD
    A["暗号化された秘密鍵を入手<br/>Proc-Type: 4,ENCRYPTED"] --> B["ssh2john で変換<br/>鍵をハッシュ形式へ"]
    B --> C["john で辞書総当たり<br/>手元だけ・サーバに触れない"]
    C --> D{"パスフレーズ<br/>が割れたか"}
    D -.->|"割れない"| E["強いパスフレーズなら<br/>ここで止まる"]
    D -->|"割れた"| F["chmod 600 で<br/>鍵の権限を絞る"]
    F --> G["ssh -i でログイン<br/>パスフレーズを入力"]
    G --> H["対象ユーザーとしてログイン成功"]

    classDef normal fill:#6474ef,color:#ffffff,stroke-width:0px
    classDef goal fill:#1f7a8c,color:#ffffff,stroke-width:0px
    classDef error fill:#fdecec,stroke:#c1121f,stroke-width:2px,color:#c1121f

    class A,B,C,F,G normal
    class H goal
    class E error
    class D normal

秘密鍵ファイルの冒頭を読む:ENCRYPTED の意味

暗号化された(古い形式の)RSA秘密鍵をテキストで開くと、冒頭がこうなっています。

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,6ABA7DE35CDB65070B92C1F760E2FE75

(Base64のかたまりが続く)
-----END RSA PRIVATE KEY-----
Plaintext

ここで注目すべきは2行目と3行目のヘッダーです。暗号化されていない秘密鍵にはこの2行がありません。それぞれの意味は次のとおりです。

ヘッダー意味
Proc-Type: 4,ENCRYPTEDこの鍵は暗号化されている、という宣言。ENCRYPTED の一語がその印
DEK-Info: AES-128-CBC,6ABA...暗号化に使ったアルゴリズム(AES-128のCBCモード)と、その初期化ベクトル(IV)

重要なのは、鍵の本体(BEGIN と END の間にあるBase64のかたまり)が、まるごと暗号化されているという点です。暗号化されていない秘密鍵なら、このBase64を復号すると鍵の数学的な構造(ASN.1)がそのまま出てきますが、暗号化されているとこれは意味をなさないバイト列になっています。パスフレーズから導いた鍵で復号して、はじめて中身が現れます。Martin Kleppmann氏の解説では、この復号の過程を手作業でたどっていて、DEK-Info のIVがそのまま復号のパラメータになる様子が確認できます。

つまり秘密鍵は二重のロックになっています。

  • 秘密鍵ファイルそのもの(これがサーバへの合鍵)
  • その合鍵を暗号化しているパスフレーズ
flowchart TD
    A["入手した<br/>暗号化済み秘密鍵ファイル"] --> B{"パスフレーズが<br/>分かるか"}
    B -.->|"分からない"| C["ただのバイト列<br/>鍵として使えない"]
    B -->|"分かる"| D["復号された秘密鍵"]
    D --> E["サーバへSSHログインできる"]

    classDef normal fill:#6474ef,color:#ffffff,stroke-width:0px
    classDef goal fill:#1f7a8c,color:#ffffff,stroke-width:0px
    classDef error fill:#fdecec,stroke:#c1121f,stroke-width:2px,color:#c1121f

    class A,D normal
    class E goal
    class C error
    class B normal

ファイルを入手してもパスフレーズが分からないと意味がない。この状態を突破するのが次のステップです。

なぜ「手元だけ」で破れるのか:オンラインとオフラインの違い

パスフレーズを破る、と聞くと「サーバに何度もログインを試すのか」と思うかもしれませんが、そうではありません。ここで、パスワードを破る手法には大きく2種類あることを押さえておくと、話がすっきりします。

手法やること相手サーバに触れるか
オンライン・ブルートフォース生きているサービスに、実際にログインを何度も試行する触れる
オフライン・クラック手元にあるハッシュや暗号化ファイルに対して、総当たりする触れない

パスフレーズの検証は、秘密鍵ファイルの中身に対して行われる処理です。「このパスフレーズで復号したら、正しい鍵の構造になるか?」を確かめるだけなので、サーバは一切関係ありません。だから、秘密鍵ファイルさえ手元にあれば、自分のマシンの中だけで総当たりできます。これがオフライン・クラックです。

この区別が、秘密鍵を暗号化する意味につながります。ファイルが盗まれること自体は防げなくても、パスフレーズが十分に複雑なら、手元で総当たりしても現実的な時間では割れない。逆に、パスフレーズが弱ければこの守りは簡単に崩れます。鍵の強さは、結局このパスフレーズの強さで決まる、ということです。

パスフレーズを破る道具:John the Ripper と ssh2john

オフラインでパスワードを破る定番ツールが John the Ripper(以下 John)です。公式リポジトリの説明どおり、Johnは「オフライン・パスワードクラッカー」で、Unixのパスワードハッシュをはじめ、数百種類のハッシュや暗号を扱えます。SSHの秘密鍵もその対象に含まれています。

ただし、ここに一つ引っかかりどころがあります。Johnは秘密鍵ファイルをそのままでは扱えないのです。Johnが理解できるのは「ハッシュの形式」であって、SSHの秘密鍵ファイルはその形式ではありません。そこで間に翻訳役をかませます。それが ssh2john です。

ssh2john は、秘密鍵ファイルを読み込んで、Johnが総当たりできる「ハッシュ形式」の文字列に変換します。名前のとおり「SSH(の鍵)to John(が読める形)」への変換ツールです。Johnには、こうした「◯◯2john」という変換ツールが多数付属していて(ZIPやPDF、Officeファイル用など)、秘密鍵用がこの ssh2john にあたります。

変換のイメージはこうです。

ssh2john id_rsa > id_rsa.hash
Bash

左半分(ssh2john id_rsa)で秘密鍵を読み、Johnが読める形に変換し、右半分(> id_rsa.hash)でその結果をファイルに書き出しています。> はシェルのリダイレクトで、「コマンドの出力を画面ではなくファイルへ送る」記号です。

変換後のファイルを開くと、こんな形の1行になっています。

id_rsa:$sshng$1$16$6ABA7DE35CDB...(長い文字列が続く)
Plaintext

先頭が元のファイル名、続いて $sshng$ から始まる文字列が本体です。この $sshng$ が「SSHの鍵のハッシュ」という種類を表すラベルで、公式の ssh2john.py のソースを見ると、鍵の暗号方式に応じてこの形式の文字列を組み立てているのが分かります。これがJohnに渡すものになります。

あとはJohnに辞書を指定して回すだけです。

john --wordlist=/usr/share/wordlists/rockyou.txt id_rsa.hash
Bash

--wordlist で指定している rockyou.txt は、実際に流出したパスワードを集めた定番の辞書で、弱いパスフレーズであれば数秒のうちに出てきます。

最後の関門:なぜ chmod 600 を求められるのか

パスフレーズが割れたら、その鍵で実際にログインします。

ssh -i id_rsa ユーザー名@ホスト
Bash

-i は「この秘密鍵を使え」という指定です。ところが、入手したばかりの鍵ファイルでこれを実行すると、多くの場合ログインの前にSSHクライアント自身が止めてきます。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!         @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Plaintext

これはエラーではなく、SSHの安全装置です。秘密鍵は本人だけが読めるべき秘密情報なので、「所有者以外も読める状態の秘密鍵は、盗み見られた可能性がある=信用できない」とみなして、使用を拒否します。テキストエディタで普通に作ったファイルは、他のユーザーも読める緩い権限(0644 など)になりがちで、この検査に引っかかります。

直し方は、権限を所有者だけに絞ることです。

chmod 600 id_rsa
Bash

600 の意味を分解すると次のようになります。Linuxのパーミッションは「所有者・グループ・その他」の3者それぞれに「読み(4)・書き(2)・実行(1)」を割り当てる仕組みです。

桁対象権限数字の内訳
6所有者読み+書き4+2
0グループなし0
0その他なし0

つまり「自分だけが読み書きでき、他の誰も触れない」状態です。これがSSHの要求する秘密鍵の正しい姿で、TechEarlの記事でも指摘しているとおり、グループやその他に1ビットでも読み権限があるとこの検査に引っかかります。同じ考え方はサーバ側にもあり、sshd の StrictModes が有効だと、権限の緩い ~/.ssh や authorized_keys は黙って無視されます。

補足すると、公開鍵(.pub)の方は誰に見られても構わないので 644 で問題ありません。秘密の側だけを厳しくする、という非対称が公開鍵暗号の考え方そのものを反映しています。

権限を直したら、あらためてログインします。今度はパスフレーズを聞かれるので、Johnで割った文字列を入力することでログインすることができます。

ssh -i id_rsa ユーザー名@ホスト
Enter passphrase for key 'id_rsa':
Bash

まとめ

  • 暗号化された秘密鍵は二重ロックになっている。ファイル自体がサーバへの合鍵で、その合鍵にさらにパスフレーズという鍵がかかっている。
  • 冒頭の Proc-Type: 4,ENCRYPTED が暗号化の印。鍵の本体はまるごと暗号化されていて、パスフレーズで復号しないと使えない。
  • パスフレーズの検証は秘密鍵ファイルの中身に対する処理なので、サーバに触れずオフラインで総当たりできる。だからこそ、強いパスフレーズが守りの要になる。
  • Johnは秘密鍵をそのまま読めないため、ssh2john で「Johnが読めるハッシュ形式」に変換してから渡す。
  • 入手した鍵は権限が緩く、SSHが安全装置で使用を拒否する。chmod 600 で所有者だけが読める状態にする必要がある。同じルールはサーバ側の StrictModes にもある。
  • 秘密鍵の強度は、パスフレーズの強さと保管方法で決まる。弱いパスフレーズや平文保管は、この一連の流れで簡単に破られる。

参考にしたサイト

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

ABOUT ME
海洋
海洋
NWエンジニア
ネットワーク・セキュリティを中心に、業務や検証環境で実際に触れた内容を記録しています。
現職ではネットワークの運用・構築を担当。

保有資格
ネットワーク:ネットワークスペシャリスト / CCNA
セキュリティ:情報セキュリティマネジメント / ISC2 CC
クラウド  :AZ-104 / AZ-305
その他   :応用情報技術者 / LinuC レベル1

記事URLをコピーしました