Wiresharkでsshパケットを復号するためのssh key log作成方法 (1)
TL;DR
- OpenSSHをデバッグビルドできる場合、gdbで
b kexgen.c:kex_gen_client
した後にfin
したタイミングでkex
構造体にclient keyデータが入っている - Wiresharkのssh key log formatで鍵を記録するためのgdbコマンドファイルを作成した
- デバッグシンボルがない場合は、
ssh -v
で実行したときにwrite(2, "debug1: expecting SSH2_MSG_KEX_ECDH_REPLY", 43)
しているところの呼び出し元がkexgen.c
のkex_gen_client()
なので、同様に関数の終わりでkex
構造体を読むと良い
背景
2022年1月にssh dissectorへのssh decryptionの実装がマージされ、Wireshark 4.0.0からsshパケットの復号ができるようになると見込まれる。
Wiresharkにssh decryptionが実装されていたので試してみる。SSLKEYLOGFILEみたいな仕組みがないので鍵の取り出しが超面倒https://t.co/qLYps4rxa4 pic.twitter.com/zmtOgn1rJj
— jptomoya (@_jptomoya) 2022年9月5日
Wiresharkでhttpsパケットを復号する方法として、環境変数SSLKEYLOGFILE
を指定してブラウザにセッション鍵を記録させる方法が知られている。
一方、Linuxディストリビューションにバンドルされ、広く利用されているOpenSSHクライアントには、このような仕組みがないため、sshセッション鍵の記録は敷居が高い。
また、-v -v -v
オプションを指定して、Verbose modeを最大にしてsshコマンドを実行しても鍵情報が出力されることはない。
そこで、OpenSSHをデバッグビルドしてgdbでWiresharkで利用できる形式のclient keyを抽出する方法をまとめた。
(ビルドするのであれば鍵情報を出力するようにソースを変更したほうが楽だったのでは) ⇒ ここで得た知見は、デバッグシンボルのないsshクライアントからの鍵抽出にも利用できる。
検証環境
$ uname -srvi Linux 5.10.102.1-microsoft-standard-WSL2 #1 SMP Wed Mar 2 00:30:59 UTC 2022 x86_64 $ cat /etc/os-release NAME="Ubuntu" VERSION="20.04.4 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.4 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal $ gdb --version GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
$ ssh -V OpenSSH_8.2p1 Ubuntu-4ubuntu0.5, OpenSSL 1.1.1f 31 Mar 2020 $ file /usr/bin/ssh /usr/bin/ssh: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=48d5a82eb9975a1fe8ef84d033667aa76130726b, for GNU/Linux 3.2.0, stripped
以降の記述は、OpenSSH v8.2p1に基づいているため、他のバージョンでは実装が異なるかもしれない。
Wiresharkでsshパケットを復号する方法
Wiresharkでsshパケットを復号するには、ssh dissectorに復号したいsshセッションの鍵を設定する(httpsと同様)。前述の通り、ssh decryption機能はWireshark 4.0.0から利用できるようになる見込みで、Wireshark 3.6.xでは(おそらく)利用できない点に注意*1。Wiresharkの[設定] - [Protocols] - [SSH]でssh key logと呼ばれるファイルを指定する。
フォーマットは、<hex-encoded-cookie> <hex-encoded-key>
とされており、復号したいsshセッションのcookieとkeyをスペース区切りで記述したテキストファイルを用意すれば良いことが分かる。
Wiresharkのソースコードを参照すると、ssh key logの詳細な説明が見つけられる。
/* File format: each line follows the format "<cookie> <key>". * <cookie> is the hex-encoded (client or server) 16 bytes cookie * (32 characters) found in the SSH_MSG_KEXINIT of the endpoint whose * private random is disclosed. * <key> is the private random number that is used to generate the DH * negotiation (length depends on algorithm). In RFC4253 it is called * x for the client and y for the server. * For openssh and DH group exchange, it can be retrieved using * DH_get0_key(kex->dh, NULL, &server_random) * for groupN in file kexdh.c function kex_dh_compute_key * for custom group in file kexgexs.c function input_kex_dh_gex_init * For openssh and curve25519, it can be found in function kex_c25519_enc * in variable server_key. * * Example: * 90d886612f9c35903db5bb30d11f23c2 DEF830C22F6C927E31972FFB20B46C96D0A5F2D5E7BE5A3A8804D6BFC431619ED10AF589EEDFF4750DEA00EFD7AFDB814B6F3528729692B1F2482041521AE9DC */
以降、ssh key logで指定するkeyをclient keyと呼ぶ。
client keyの取り出し~ssh key logファイル作成
題材として、ここではRaspberry Pi OSが動作しているホスト192.168.1.10
でUser=pi, Password=raspberryとしてパスワード認証を試みるsshパケットを復号する。
パケットキャプチャしつつ、gdbでsshクライアントを実行してclient keyを取り出す。
OpenSSHのビルド
検証環境のUbuntuにインストールされていたsshクライアントと同じ、OpenSSH 8.2p1のソースコードを公式サイトからダウンロードしてビルドする。
configure時にCFLAGS=-g
を指定することで、デバッグシンボルを含めてコンパイルする。
なお、コンパイルにはOpenSSLとzlibが必要なので前もってインストールしておく。Ubuntuの場合、apt update && apt install zlib1g-dev libssl-dev
。
$ tar xzf openssh-8.2p1.tar.gz $ cd openssh-8.2p1/ $ ./configure -q CFLAGS=-g OpenSSH has been configured with the following options: User binaries: /usr/local/bin System binaries: /usr/local/sbin Configuration files: /usr/local/etc Askpass program: /usr/local/libexec/ssh-askpass Manual pages: /usr/local/share/man/manX PID file: /var/run Privilege separation chroot path: /var/empty sshd default user PATH: /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin Manpage format: doc PAM support: no OSF SIA support: no KerberosV support: no SELinux support: no MD5 password support: no libedit support: no libldns support: no Solaris process contract support: no Solaris project support: no Solaris privilege support: no IP address in $DISPLAY hack: no Translate v4 in v6 hack: yes BSD Auth support: no Random number source: OpenSSL internal ONLY Privsep sandbox style: seccomp_filter PKCS#11 support: yes U2F/FIDO support: yes Host: x86_64-pc-linux-gnu Compiler: cc Compiler flags: -g -pipe -Wno-error=format-truncation -Wall -Wpointer-arith -Wuninitialized -Wsign-compare -Wformat-security -Wsizeof-pointer-memaccess -Wno-pointer-sign -Wno-unused-result -Wimplicit-fallthrough -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -ftrapv -fno-builtin-memset -fstack-protector-strong -fPIE Preprocessor flags: -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_DEFAULT_SOURCE Linker flags: -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack -fstack-protector-strong -pie Libraries: -lcrypto -ldl -lutil -lz -lcrypt -lresolv $ make (snip) $ ls ./ssh ./ssh $ file ./ssh ./ssh: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=57c9b6dad20f5e4cb2d6cd78f7afb11af4bf41fe, for GNU/Linux 3.2.0, with debug_info, not stripped $ ./ssh -V OpenSSH_8.2p1, OpenSSL 1.1.1f 31 Mar 2020
gdbのセットアップと実行
sshクライアントでは、kexgen.cのkex_gen_client
でclient keyが生成される(KEXアルゴリズムがdiffie-hellman-group-exchange-sha1
とdiffie-hellman-group-exchange-sha256
のときを除く*2)。
そのため、kexgen.c
のkex_gen_client()
でbreakpointを設定しておき、実行する。
breakpointに到達すると、kex_gen_client()
の先頭で停止するのでfin
で関数の終わりまで実行する。このタイミングで、client keyがkex
構造体に格納されている。
$ gdb -q ./ssh Reading symbols from ./ssh... (gdb) b kexgen.c:kex_gen_client Breakpoint 1 at 0x7a15f: file kexgen.c, line 100. (gdb) r pi@192.168.1.10 Starting program: /tmp/openssh-8.2p1/ssh pi@192.168.1.10 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, kex_gen_client (ssh=0x5555555b1088 <sshpkt_get_end+35>) at kexgen.c:100 100 { (gdb) fin Run till exit from #0 kex_gen_client (ssh=0x5555555b1088 <sshpkt_get_end+35>) at kexgen.c:100 0x00005555555cb466 in kex_input_kexinit (type=20, seq=0, ssh=0x555555640150) at kex.c:624 624 return (kex->kex[kex->kex_type])(ssh); Value returned is $1 = 0
client keyの取り出し
kex->kex_type
の値によって、kex
でclient keyが格納されるメンバ変数が異なるため、まずkex_type
の値を調べる。
(gdb) p kex->kex_type $2 = 8
enum kex_exchange
の定義を確認すると、KEX_C25519_SHA256
と分かる。
enum kex_exchange {
KEX_DH_GRP1_SHA1,
KEX_DH_GRP14_SHA1,
KEX_DH_GRP14_SHA256,
KEX_DH_GRP16_SHA512,
KEX_DH_GRP18_SHA512,
KEX_DH_GEX_SHA1,
KEX_DH_GEX_SHA256,
KEX_ECDH_SHA2,
KEX_C25519_SHA256,
KEX_KEM_SNTRUP4591761X25519_SHA512,
KEX_MAX
};
KEX_C25519_SHA256
の場合は、kex->c25519_client_key
にclient keyが格納されているので、printして鍵を取り出す。
(gdb) p/x kex->c25519_client_key $3 = {0x4e, 0xb5, 0x70, 0x1a, 0x86, 0xfb, 0x60, 0xf3, 0x7e, 0x6e, 0xe6, 0xf8, 0xfa, 0xaf, 0x76, 0x32, 0x3e, 0x7c, 0xdf, 0x4c, 0x6c, 0xd3, 0x41, 0x61, 0xa, 0x2b, 0x12, 0x2f, 0xeb, 0x5e, 0xd9, 0x17}
kexgen.cのkex_gen_client()
の処理を追うと、以下のような対応であることが分かる。鍵交換がKEX_C25519_SHA256
以外のアルゴリズムだった場合は、対応するメンバ変数を参照する。
- KEX_DH_GRP1_SHA1 / KEX_DH_GRP14_SHA1 / KEX_DH_GRP14_SHA256 / KEX_DH_GRP16_SHA512 / KEX_DH_GRP18_SHA512 ->
kex->dh
- KEX_ECDH_SHA2 ->
kex->ec_client_key
- KEX_C25519_SHA256 ->
kex->c25519_client_key
cookieの取り出し
cookieはkex->my
に格納されている。OpenSSHのデバッグ用関数sshbuf_dump_data()
を利用して表示させる。
(gdb) p kex->my $4 = (struct sshbuf *) 0x555555641d50 (gdb) call sshbuf_dump_data(*kex->my, 16, stdout) 0000: b1 14 d4 5a 91 cc c7 7a 51 2f 0c 7c d3 2b 45 d7 ...Z...zQ/.|.+E. (gdb) c Continuing. pi@192.168.1.10's password: Permission denied, please try again. pi@192.168.1.10's password:
なお、cookieは鍵交換時に平文で流れているので、手動で取り出す場合は、パケットキャプチャからWireshark等で取り出すこともできる。
その場合は、Client: Key Exchange Init
パケットの値ssh.cookie
が目的の値になる。
ssh key logファイルの作成とWiresharkへの設定
cookieとclient keyの対が揃ったので、以下のようなssh key logファイルを前述の<cookie> <key>
フォーマットで作成する。
b114d45a91ccc77a512f0c7cd32b45d7 4eb5701a86fb60f37e6ee6f8faaf76323e7cdf4c6cd341610a2b122feb5ed917
Wiresharkの[設定] - [Protocols] - [SSH]でKey log filenameに作成したファイルを指定する。 正しく設定されていれば、画面のようにUser=pi, Password=raspberryでパスワード認証を試みる様子が復号される。
gdbコマンドファイル化
(2022/9/12) KEX_ECDH_SHA2, KEX_DH_GEX_SHA1, KEX_DH_GEX_SHA256に対応させた
ここまでの作業をssh接続毎に行うのは大変なので、ssh key log formatでclient keyを記録するするためのgdbコマンドファイルを作成した。書きなぐりなので、あまり安定性はない。 全ての鍵交換アルゴリズムに対応するのは大変だったので、KEX_DH_GRP1_SHA1, KEX_DH_GRP14_SHA1, KEX_DH_GRP14_SHA256, KEX_DH_GRP16_SHA512, KEX_DH_GRP18_SHA512, KEX_C25519_SHA256にのみ対応している。
gdbコマンドファイルを、sshkeylog
という名前で保存しgdb -x
で読み込ませるようにした。以下の実行例のように、gdbは、sshkey.log
というファイル名でcookieとclient keyをssh key log formatで記録する。
$ gdb -q -x ./sshkeylog --args ./ssh pi@192.168.1.10 Reading symbols from ./ssh... Breakpoint 1 at 0x7a15f: file kexgen.c, line 100. [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, kex_gen_client (ssh=0x5555555b1088 <sshpkt_get_end+35>) at kexgen.c:100 100 { 0x00005555555cb466 in kex_input_kexinit (type=20, seq=0, ssh=0x555555640150) at kex.c:624 624 return (kex->kex[kex->kex_type])(ssh); Value returned is $1 = 0 pi@192.168.1.10: Permission denied (publickey). [Inferior 1 (process 26835) exited with code 0377] $ cat sshkey.log b1fc713fd32f9f2f25c0ae35b35f24fe db903bf73a258f969d24abbb09e99e93f7f3e07c43ce74a0fb0c43ca05b88c25
コマンドファイルの内容は以下。
OpenSSHのデバッグシンボルがない場合
client keyの取り出しでフォーカスしてきた、kex_gen_client()
は、処理の終わりの方でdebug("expecting SSH2_MSG_KEX_ECDH_REPLY")
を呼び出してデバッグログを出力するようになっている。
そのためsshを-v
でverbose modeにし、write
システムコールでのbreakpointを起点にすることで、比較的簡単にkex_gen_client()
を見つけることができる。
kex_gen_client()
が特定できれば、kex
のアドレスも分かるため、デバッグシンボルがない状況でも前述のように関数の終わりでkex
構造体の中身を読めば、最終的にclient keyを記録することは可能である。
詳細は別の機会にしたい。