Wiresharkでsshパケットを復号するためのssh key log作成方法 (2)
前回は、Wireshark 4.0.0以降で利用できるようになるssh復号機能を背景に、OpenSSHクライアントのデバッグシンボルがある場合にclient keyを抽出する方法を紹介した。
前回の方法では、関数名でbreakpointを設定したり、client keyをprintするのに変数名を指定したりできるようにOpenSSHをデバッグビルド(-g
オプションでコンパイル)する必要があった。
sshクライアント環境ごとにOpenSSHをデバッグビルドするのは実用性にやや難があるので、今回は、より汎用的な方法としてgdbを使ってUbuntuにバンドルされているsshコマンドからclient keyを抽出する。
検証環境
$ 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に基づいているため、他のバージョンでは実装が異なるかもしれない。
OpenSSHのデバッグシンボルがない場合の鍵抽出方針
題材として、前回同様にRaspberry Pi OSが動作しているホスト192.168.1.6
でUser=pi, Password=raspberryとしてパスワード認証を試みるsshパケットを復号する。 パケットキャプチャしつつ、gdbでsshクライアントを実行してclient keyを取り出す。
前回も言及したように、client keyの生成されるkex_gen_client()
は、処理の終わりの方でdebug("expecting SSH2_MSG_KEX_ECDH_REPLY")
を呼び出してデバッグログを出力している。
そのため、write
システムコールでのbreakpointを設定した状態でsshを-v
でverbose modeにして実行し、当該文字列を出力している部分を見つければ、比較的簡単にkex_gen_client()
のアドレスを特定できる。
kex_gen_client()
のアドレスが特定できれば、その後の処理からkex
のアドレスも分かるため、デバッグシンボルがない状況でも関数の終わりでkex
構造体の中身を読めば、最終的にclient keyを抽出することは可能である。
kex_gen_client()の特定
$ man 2 write
コマンドでwrite
システムコールの定義を確認すると、以下の通り第2引数にbufを渡して書き込む文字列を指定するようになっている。
SYNOPSIS #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
gdbを起動し、write
にbreakpointを設定する。あわせて、停止時にbufの内容を確認するために第2引数の内容を表示するようにしたい。x86_64環境なので、第2引数はrsiレジスタを見ればよい。rsiレジスタの指す文字列をdisplayで自動表示するようにする。
準備ができたら、sshコマンドを-v
オプション付きで実行する。
そうすると、write
の呼び出しで停止し、sshのverbose出力を捉えられていることが分かる。この後、何度かcontinueして目的の文字列"expecting SSH2_MSG_KEX_ECDH_REPLY"
での呼び出しを見つけることになる。
$ gdb -q /usr/bin/ssh Reading symbols from /usr/bin/ssh... (No debugging symbols found in /usr/bin/ssh) (gdb) b write Breakpoint 1 at 0xb8e0 (gdb) display/s $rsi 1: x/s $rsi <error: No registers.> (gdb) r -v pi@192.168.1.6 Starting program: /usr/bin/ssh -v pi@192.168.1.6 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, __GI___libc_write (fd=2, buf=0x7fffffffaec0, nbytes=62) at ../sysdeps/unix/sysv/linux/write.c:25 25 ../sysdeps/unix/sysv/linux/write.c: No such file or directory. 1: x/s $rsi 0x7fffffffaec0: "OpenSSH_8.2p1 Ubuntu-4ubuntu0.5, OpenSSL 1.1.1f 31 Mar 2020\r\n"
目的の文字列を通り過ぎないように注意しながら、十数回continueを繰り返すのは大変なので、文字列一致でconditional breakpointを設定する(今回は、writeのシンボルが読み込まれていたので、bufを利用)。
(gdb) condition 1 strcmp(buf, "debug1: expecting SSH2_MSG_KEX_ECDH_REPLY\r\n")==0
continueすると、一気にwrite(2, "expecting SSH2_MSG_KEX_ECDH_REPLY", 43)
で停止するので、btでバックトレースを表示する。
(gdb) c Continuing. (snip) debug1: SSH2_MSG_KEXINIT sent debug1: SSH2_MSG_KEXINIT received debug1: kex: algorithm: curve25519-sha256 debug1: kex: host key algorithm: ecdsa-sha2-nistp256 debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none Breakpoint 1, __GI___libc_write (fd=2, buf=0x7fffffffac40, nbytes=43) at ../sysdeps/unix/sysv/linux/write.c:25 25 in ../sysdeps/unix/sysv/linux/write.c 1: x/s $rsi 0x7fffffffac40: "debug1: expecting SSH2_MSG_KEX_ECDH_REPLY\r\n" (gdb) bt #0 __GI___libc_write (fd=2, buf=0x7fffffffac40, nbytes=43) at ../sysdeps/unix/sysv/linux/write.c:25 #1 0x000055555559a344 in ?? () #2 0x000055555559a728 in ?? () #3 0x00005555555b7327 in ?? () #4 0x00005555555b5035 in ?? () #5 0x00005555555a3cba in ?? () #6 0x00005555555a3d6d in ?? () #7 0x0000555555576f06 in ?? () #8 0x00005555555732ef in ?? () #9 0x0000555555563652 in ?? () #10 0x00007ffff7a61083 in __libc_start_main (main=0x555555561200, argc=3, argv=0x7fffffffde98, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde88) at ../csu/libc-start.c:308 #11 0x0000555555564dce in ?? ()
今、OpenSSHのdebug()
経由で呼び出されたwrite
で止まっているので、いくつか上のフレームがdebug()
で、それを呼び出している部分がkex_gen_client()
ということになる。
finで関数の終わりまで実行しながら、適当に手前の逆アセンブルを見る。
(gdb) d display 1 (gdb) fin Run till exit from #0 __GI___libc_write (fd=2, buf=0x7fffffffac40, nbytes=43) at ../sysdeps/unix/sysv/linux/write.c:25 debug1: expecting SSH2_MSG_KEX_ECDH_REPLY 0x000055555559a344 in ?? () Value returned is $2 = 43 (gdb) x/16i $rip-0x20 0x55555559a324: test $0x8080,%eax 0x55555559a329: cmove %ecx,%eax 0x55555559a32c: lea 0x2(%rdx),%rcx 0x55555559a330: cmove %rcx,%rdx 0x55555559a334: mov %eax,%ebx 0x55555559a336: add %al,%bl 0x55555559a338: sbb $0x3,%rdx 0x55555559a33c: sub %r15,%rdx 0x55555559a33f: callq 0x55555555f8e0 <write@plt> => 0x55555559a344: pop %rax 0x55555559a345: pop %rdx 0x55555559a346: jmpq 0x55555559a118 0x55555559a34b: callq 0x555555560720 <__stack_chk_fail@plt> 0x55555559a350 <error>: endbr64 0x55555559a354 <error+4>: sub $0xd8,%rsp 0x55555559a35b <error+11>: mov %rdi,%r10 (gdb) fin Run till exit from #0 0x000055555559a344 in ?? () 0x000055555559a728 in ?? () (gdb) x/16i $rip-0x20 0x55555559a708: add %al,(%rax) 0x55555559a70a: lea 0x20(%rsp),%rax 0x55555559a70f: mov %rax,0x10(%rsp) 0x55555559a714: movl $0x8,(%rsp) 0x55555559a71b: movl $0x30,0x4(%rsp) 0x55555559a723: callq 0x55555559a020 => 0x55555559a728: mov 0x18(%rsp),%rax 0x55555559a72d: xor %fs:0x28,%rax 0x55555559a736: jne 0x55555559a740 0x55555559a738: add $0xd8,%rsp 0x55555559a73f: retq 0x55555559a740: callq 0x555555560720 <__stack_chk_fail@plt> 0x55555559a745: data16 nopw %cs:0x0(%rax,%rax,1) 0x55555559a750: endbr64 0x55555559a754: sub $0xd8,%rsp 0x55555559a75b: mov %rdi,%r10 (gdb) fin Run till exit from #0 0x000055555559a728 in ?? () 0x00005555555b7327 in ?? () (gdb) x/16i $rip-0x20 0x5555555b7307: shlb $0x48,-0x5f(%rbp) 0x5555555b730b: mov %ebp,%edi 0x5555555b730d: callq 0x55555559f270 0x5555555b7312: mov %eax,%r12d 0x5555555b7315: test %eax,%eax 0x5555555b7317: jne 0x5555555b72ab 0x5555555b7319: lea 0x2a4a0(%rip),%rdi # 0x5555555e17c0 0x5555555b7320: xor %eax,%eax 0x5555555b7322: callq 0x55555559a680 => 0x5555555b7327: lea -0x75e(%rip),%rdx # 0x5555555b6bd0 0x5555555b732e: mov $0x1f,%esi 0x5555555b7333: mov %rbp,%rdi 0x5555555b7336: callq 0x5555555a3c20 0x5555555b733b: jmpq 0x5555555b72ab 0x5555555b7340: mov %rbx,%rdi 0x5555555b7343: callq 0x5555555c1f60
#3のフレーム(0x5555555b7327)まで上ったところで、lea 0x2a4a0(%rip),%rdi
でrdiレジスタに値をセットしてからcallq 0x55555559a680
している部分が引数を1つだけ取るdebug()
に見える。
rdiレジスタにセットされる0x5555555e17c0の内容を確認する。
(gdb) x/s 0x5555555e17c0 0x5555555e17c0: "expecting SSH2_MSG_KEX_ECDH_REPLY"
0x5555555b7322のcallq
は、見事にkex_gen_client
でのdebug
の呼び出し方と一致しているので、#4のフレームの手前の逆アセンブルを調べる。
(gdb) x/8i 0x5555555b5035-0x10 0x5555555b5025: add %al,(%rax) 0x5555555b5027: test %rax,%rax 0x5555555b502a: je 0x5555555b526c 0x5555555b5030: mov %rbp,%rdi 0x5555555b5033: callq *%rax 0x5555555b5035: mov %eax,%r14d 0x5555555b5038: jmpq 0x5555555b4af0 0x5555555b503d: nopl (%rax)
callq *%rax
でkex_gen_client
が呼び出されていると考えられるので、0x5555555b5033にbreakpointを設定して実行しなおす。
(gdb) d Delete all breakpoints? (y or n) y (gdb) b *0x5555555b5033 Breakpoint 2 at 0x5555555b5033 (gdb) k Kill the program being debugged? (y or n) y [Inferior 1 (process 235) killed] (gdb) r -v pi@192.168.1.6 Starting program: /usr/bin/ssh -v pi@192.168.1.6 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". OpenSSH_8.2p1 Ubuntu-4ubuntu0.5, OpenSSL 1.1.1f 31 Mar 2020 (snip) Breakpoint 2, 0x00005555555b5033 in ?? () (gdb) x/i $rip => 0x5555555b5033: callq *%rax (gdb) si 0x00005555555b7270 in ?? () (gdb) fin Run till exit from #0 0x00005555555b7270 in ?? () debug1: expecting SSH2_MSG_KEX_ECDH_REPLY 0x00005555555b5035 in ?? ()
callq *%rax
で0x5555555b7270が呼び出されており、finで終わりまで実行すると、debug1:~文字列が出力されることが分かる。
このことから、0x5555555b7270がkex_gen_client()
と特定できた。
client keyの取り出し
特定したkex_gen_client()
の0x5555555b7270でbreakpointを設定し直して、sshを実行する。直接breakpointを設定するので、この段階でsshの実行時に-v
オプションは不要(付けても良い)。
kex_gen_client()
は第1引数でstruct ssh *ssh
を受け取るので、停止時点でのrdiレジスタの値がssh
のアドレスを指している。
$ gdb -q /usr/bin/ssh Reading symbols from /usr/bin/ssh... (No debugging symbols found in /usr/bin/ssh) (gdb) b *0x5555555b7270 Breakpoint 1 at 0x5555555b7270 (gdb) r pi@192.168.1.6 Starting program: /usr/bin/ssh pi@192.168.1.6 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, 0x00005555555b7270 in ?? () (gdb) i r rdi rdi 0x55555563d2b0 93824993186480 (gdb) x/16i $rip => 0x5555555b7270: endbr64 0x5555555b7274: push %r12 0x5555555b7276: push %rbp 0x5555555b7277: mov %rdi,%rbp 0x5555555b727a: push %rbx 0x5555555b727b: mov 0x8(%rdi),%rbx 0x5555555b727f: mov 0x48(%rbx),%eax 0x5555555b7282: cmp $0x8,%eax 0x5555555b7285: je 0x5555555b7340 0x5555555b728b: ja 0x5555555b72b8 0x5555555b728d: cmp $0x4,%eax 0x5555555b7290: jbe 0x5555555b7360 0x5555555b7296: cmp $0x7,%eax 0x5555555b7299: jne 0x5555555b72d0 0x5555555b729b: mov %rbx,%rdi 0x5555555b729e: callq 0x5555555c1c00
前回同様に、まず使用されているKEXアルゴリズムを示すkex->kex_type
の値を調べる。
kex_gen_client
関数の処理では、まずstruct kex *kex = ssh->kex;
とint r;
でローカル変数が初期化された後、switch (kex->kex_type)
に入る。
あわせて、ssh構造体の定義を確認すると、1番目のメンバ変数はstate
へのポインタで、2番目のメンバ変数にkex
構造体へのポインタが入っている。
これらを踏まえて、上記逆アセンブルを解釈すると、mov 0x8(%rdi),%rbx
でrbxレジスタにssh->kex
の値が入り、mov 0x48(%rbx),%eax
でeaxレジスタにkex->kex_type
の値が入ることが分かる。
7回ステップ実行して、ここまで実行する。
(gdb) si 7 0x00005555555b7282 in ?? () (gdb) i r rbx eax rbx 0x55555563db30 93824993188656 eax 0x8 8 (gdb) x/2ga $rdi 0x55555563d2b0: 0x55555563ad90 0x55555563db30
この時点で、rbxレジスタの値がssh->kex
の値を指している。rdiレジスタの指すアドレスから2番目のポインタが指すアドレスと一致していることからも、ssh構造体の定義通りになっている。
また、eaxレジスタの値はkex->kex_type
の値で8
なので、enum kex_exchange
の定義からKEX_C25519_SHA256
と分かる。
ここで、kex
構造体のシンボルがないため、c25519_client_key
までのオフセットが分からない問題に直面する。
参考に、前回デバッグシンボル付きでコンパイルしたOpenSSH 8.2p1のkex構造体の大きさを調べると1984
となっている。
アーキテクチャとOpenSSHのバージョンが同じなので、Ubuntu向けビルドとkex構造体の配置も同じと考えてしまいそうだが、コンパイラのアラインメント等でUbuntu向けビルドとはkex構造体の各メンバ変数のオフセットは異なる可能性がある*1。
$ gdb -q ./ssh Reading symbols from ./ssh... (gdb) p sizeof(struct kex) $1 = 1984 (gdb) p (int)&((struct kex*)0)->my $2 = 96 (gdb) p (int)&((struct kex*)0)->c25519_client_key $3 = 312
そこで少し強引な方法ではあるが、kex
の先頭から1984バイトより少し余裕を持たせた2048バイト分にwatchpointを設定して、メモリの値に変化があった部分がclient keyの書き込みとみなすこととする。
kex_gen_client()
の戻り先である、0x5555555b5035にtemporary breakpointを設定したうえで、続行する。
(gdb) watch (char[2048])*0x55555563db30 Watchpoint 2: (char[2048])*0x55555563db30 (gdb) bt #0 0x00005555555b7282 in ?? () #1 0x00005555555b5035 in ?? () #2 0x00005555555a3cba in ?? () #3 0x00005555555a3d6d in ?? () #4 0x0000555555576f06 in ?? () #5 0x00005555555732ef in ?? () #6 0x0000555555563652 in ?? () #7 0x00007ffff7a61083 in __libc_start_main (main=0x555555561200, argc=2, argv=0x7fffffffde98, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde88) at ../csu/libc-start.c:308 #8 0x0000555555564dce in ?? () (gdb) tb *0x00005555555b5035 Temporary breakpoint 3 at 0x5555555b5035 (gdb) c Continuing. Watchpoint 2: (char[2048])*0x55555563db30 Old value = '\000' <repeats 16 times>, "\340\305cUUU\000\000 \263cUUU\000\000@\000\000\000@", '\000' <repeats 11 times>, "\340'dUUU\000\000\220\004dUUU\000\000\002\000\000\000\237\001\000\000\b", '\000' <repeats 23 times>, " \231cUUU\000\000\220\310cUUU\000\000p\231cUUU\000\000p\224cUUU\000\000\000\000\000\000\003\000\000\000\002", '\000' <repeats 39 times>, "P6WUUU", '\000' <repeats 34 times>... New value = '\000' <repeats 16 times>, "\340\305cUUU\000\000 \263cUUU\000\000@\000\000\000@", '\000' <repeats 11 times>, "\340'dUUU\000\000\220\004dUUU\000\000\002\000\000\000\237\001\000\000\b", '\000' <repeats 23 times>, " \231cUUU\000\000\220\310cUUU\000\000p\231cUUU\000\000p\224cUUU\000\000\000\000\000\000\003\000\000\000\002", '\000' <repeats 39 times>, "P6WUUU", '\000' <repeats 34 times>... __memmove_sse2_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:241 241 ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory. (gdb) x/8i $rip-0x10 0x7ffff7af8bbe <__memmove_sse2_unaligned_erms+14>: cmp $0x20,%edx 0x7ffff7af8bc1 <__memmove_sse2_unaligned_erms+17>: ja 0x7ffff7af8c41 <__memmove_sse2_unaligned_erms+145> 0x7ffff7af8bc3 <__memmove_sse2_unaligned_erms+19>: movups (%rsi),%xmm0 0x7ffff7af8bc6 <__memmove_sse2_unaligned_erms+22>: movups -0x10(%rsi,%rdx,1),%xmm1 0x7ffff7af8bcb <__memmove_sse2_unaligned_erms+27>: movups %xmm0,(%rdi) => 0x7ffff7af8bce <__memmove_sse2_unaligned_erms+30>: movups %xmm1,-0x10(%rdi,%rdx,1) 0x7ffff7af8bd3 <__memmove_sse2_unaligned_erms+35>: retq 0x7ffff7af8bd4 <__memmove_sse2_unaligned_erms+36>: cmp 0x1358dd(%rip),%rdx # 0x7ffff7c2e4b8 <__x86_shared_non_temporal_threshold> (gdb) i r rdi rdi 0x55555563dcb8 93824993189048
movups %xmm0,(%rdi)
でkex->c25519_client_key
への実際の書き込みが行われたと思われるので、rdiレジスタの指す0x55555563dcb8が、kex->c25519_client_key
のアドレスとなる。
kex
先頭からのオフセットは、0x55555563dcb8 - 0x55555563db30 = 392
で、上記自前OpenSSHビルドの312
と異なっている。
ともあれ、乖離した値でもないので、watchpointを削除した後にkex_gen_client
の終わりまで続行して、client keyの値を取り出す。
CURVE25519_SIZE
は32
なので、kex->c25519_client_key
のアドレスから32バイトを表示する。
(gdb) d 2 (gdb) c Continuing. Temporary breakpoint 3, 0x00005555555b5035 in ?? () (gdb) x/32bx 0x55555563dcb8 0x55555563dcb8: 0x07 0x09 0x30 0x71 0x14 0x42 0x37 0x04 0x55555563dcc0: 0xc3 0xf1 0xa7 0xe4 0xb4 0x9c 0xae 0x14 0x55555563dcc8: 0xde 0xb1 0x36 0x8f 0x2c 0x7d 0x07 0x5e 0x55555563dcd0: 0xa5 0xc9 0x67 0x09 0x8d 0x32 0xc2 0x1f
cookieの取り出し
kex構造体の定義を確認すると、cookieの格納されているkex->my
は、kex->kex_type
の3つ先のメンバ変数となっている。
前節でのkex_gen_client()
の逆アセンブル結果から、kex->kex_type
へのオフセットは0x48
と判明している(mov 0x48(%rbx),%eax
)。
kex構造体の定義通り、kex_type
の3つ先にmy
のポインタ0x0000555555639920が入っているので、sshbuf構造体の定義より、my->d
の0x0000555555643a80に入っているcookieの値を表示する。
(gdb) x/8gx 0x55555563db30+0x48 0x55555563db78: 0x0000000000000008 0x0000000000000000 0x55555563db88: 0x0000000000000000 0x0000555555639920 0x55555563db98: 0x000055555563c890 0x0000555555639970 0x55555563dba8: 0x0000555555639470 0x0000000300000000 (gdb) x/8gx 0x0000555555639920 0x555555639920: 0x0000555555643a80 0x0000555555643a80 0x555555639930: 0x0000000000000000 0x00000000000005d8 0x555555639940: 0x0000000008000000 0x0000000000000600 0x555555639950: 0x0000000000000000 0x0000000000000001 (gdb) x/16bx 0x0000555555643a80 0x555555643a80: 0x09 0xb7 0x04 0x00 0xa4 0xb4 0xa5 0x67 0x555555643a88: 0x39 0xaa 0x2c 0xc4 0x2c 0x45 0xfd 0xf1
ssh key logファイルの作成とWiresharkへの設定
cookieとclient keyの対が揃ったので、以下のようなssh key logfile形式(<cookie> <key>
)のssh key logファイルを作成する。
09b70400a4b4a56739aa2cc42c45fdf1 0709307114423704c3f1a7e4b49cae14deb1368f2c7d075ea5c967098d32c21f
Wiresharkの設定で作成したssh key logfileを指定する。 正しく設定されていれば、画面のようにUser=pi, Password=raspberryでパスワード認証を試みる様子が復号される。