劇団ひとり
Table of Contents
劇団ひとり(sshの幕)
前回やった、X11-forwardingの現場をフィールドワークしたい。 フィールドワークって、前回楽しく読んだバッタ博士の余韻が頭にこびりつい ていたからだ。
少し自分に制限を課す。だってフィールドでは、自分の思い通りにならないか らね。制限は、OpenBSDの中だけでやれって事にする。
普通、こんなのの観察は、FreeBSDも動員して、X11を転送実験するけど、それ じゃ当たり前すぎて面白くない。
攻略方法
戦略を練る。1. OpenBSDではXが起動してないと始まらないな。2. gdbからssh を起動するのもマストだな。3. 接続先は、無難にlocalhostだろう。4. 何処 でgdbを停止させようか?
こうして、劇団ひとりプロジェクトを始動させた。この場合のひとりってのは、 ひとり相撲ならぬ、一つのOS上って意味だ。勝手に芸名をパクッししまって、 ごめんなさい。まあ、ネーミング大事ってのは、バッタ博士さんもmatzさんも 声を大にして訴えておられますから。
gdb出来るssh
まずは、これだな。FreeBSDならこんな手間は必要無いけど、OpenBSDは、その 点チト不便。秘伝のレシピを探し出す所から、作業開始。
ob$ grep Makefile -r . -l ./Makefile ./Makefile.inc ./scp/Makefile ./sftp/Makefile ./moduli-gen/Makefile ./sftp-server/Makefile ./ssh/Makefile ./ssh-add/Makefile ./ssh-agent/Makefile ./ssh-keygen/Makefile ./ssh-keyscan/Makefile ./ssh-keysign/Makefile ./ssh-pkcs11-helper/Makefile ./ssh-sk-helper/Makefile ./sshd/Makefile
どうやらコマンド毎にMakefileが存在する様だ(直接トップに有るMakefileを 点検する方が良かったかも)。
ssh/Makefile
SRCS= ssh.c readconf.c clientloop.c sshtty.c sshconnect.c sshconnect2.c mux.c SRCS+= authfd.c compat.c dns.c fatal.c hostfile.c readpass.c utf8.c SRCS+= ${SRCS_BASE} ${SRCS_KEX} ${SRCS_KEXC} ${SRCS_KEY} ${SRCS_KEYP} \ ${SRCS_KRL} ${SRCS_PROT} ${SRCS_PKT} ${SRCS_UTL} ${SRCS_PKCS11} \ ${SRCS_SK_CLIENT} PROG= ssh CFLAGS= -g -O0 # <--- add for gdb
gdb出来る様にMakefileに一行追加してから、doas make する。コンパイルの 残骸と共に、sshなバイナリーが作成されるので、それを ~ に移動。残骸は一 応削除しておく。
目のつけどころ
どこで待ち伏せしよう? いわゆる何処にBPを置くか? それが問題だ。 獣が通る所で、ちゃんと待ち伏せしないと、徒労に終わる。
ざっとソースを点検し、関係が有りそうな場所を推定する。
ob$ grep xauth *.c|wc 59 297 3137
キーワードを指定して、grep。結構な候補がでてくるな。ここからは目grepす る? オイラーの目は、節穴だからなあ。ならば、
ob$ grep xauth *.c -l clientloop.c mux.c readconf.c servconf.c session.c ssh.c
この中から候補を絞れ。選択と集中作戦。いかにもそうな、session.cに意識
を集中。すると、 do_rc_files()
に、キーワードが集中してた。
ならば、この関数にBPを置いてみるか。gdbして、b do_TAB
で補完するも、
担当者は不在でした。
怪しい奴なんだけどなーー。ここらでちゃんと白黒をはっきりしよう。
ob$ grep session.c */Makefile sshd/Makefile: serverloop.c auth.c auth2.c auth-options.c session.c auth2-chall.c \
こんな一手間で、はっきりしたよ。奴はsshdの住人だったのね。
次に怪しそうなのはどれか? 手当たり次第、しらみ潰し、ローラー作戦で、 ソースに聞き込みですよ。
観察を実施
これで舞台の準備を完了した。今迄は、この過程を省略しちゃって、いきなり gdbの結果を提示してたけど、今回は丁寧に過程を開示してみた。願わくば、 オイラーに続いて、ソースを追跡する知的な楽しみを共有したいからね。
(gdb) b client_x11_get_proto Breakpoint 1 at 0x67990: file ../clientloop.c, line 289. (gdb) r -X localhost Starting program: /home/sakae/ssh -X localhost sakae@localhost's password: Breakpoint 1, client_x11_get_proto (ssh=0x5c84e880, display=0x0, xauth_path=0x448262c0 "/usr/X11R6/bin/xauth", trusted=0, timeout=1200, _proto=0xcf7e126c, _data=0xcf7e1268) at ../clientloop.c:289 289 int got_data = 0, generated = 0, do_unlink = 0, r; (gdb) bt #0 client_x11_get_proto (ssh=0x5c84e880, display=0x0, xauth_path=0x448262c0 "/usr/X11R6/bin/xauth", trusted=0, timeout=1200, _proto=0xcf7e126c, _data=0xcf7e1268) at ../clientloop.c:289 #1 0x14485458 in ssh_session2_setup (ssh=0x5c84e880, id=0, success=1, arg=0x0) at ../ssh.c:2108 #2 0x1451c838 in channel_input_open_confirmation (type=91, seq=7, ssh=0x5c84e880) at ../channels.c:3555 #3 0x145341a7 in ssh_dispatch_run (ssh=0x5c84e880, mode=1, done=0x34430cf0 <quit_pending>) at ../dispatch.c:110 #4 0x145342b6 in ssh_dispatch_run_fatal (ssh=0x5c84e880, mode=1, done=0x34430cf0 <quit_pending>) at ../dispatch.c:130 #5 0x14499023 in client_process_buffered_input_packets (ssh=0x5c84e880) at ../clientloop.c:1385 #6 0x14497987 in client_loop (ssh=0x5c84e880, have_pty=1, escape_char_arg=126, ssh2_chan_id=0) at ../clientloop.c:1541 #7 0x144830e3 in ssh_session2 (ssh=0x5c84e880, cinfo=0x4473f510) at ../ssh.c:2287 #8 0x1447fc32 in main (ac=0, av=0xcf7e1a80) at ../ssh.c:1770
X11が起動していなくても、ここに到達するな。この後どうなるかな?
実行できるのか?
実際にlocalhostを指定して、xtermが出現するのだろか? Xを起動しておいて 確認してみる。まずは、 Xを起動した時の状況
ob$ xauth list ob.my.domain/unix:0 MIT-MAGIC-COOKIE-1 f9e9f2334e11baada40a7b0123456789 ob.my.domain:0 MIT-MAGIC-COOKIE-1 f9e9f2334e11baada40a7b0123456789 ob$ echo $DISPLAY :0
いざ、実行
ob$ ssh -X localhost xterm sakae@localhost's password: X11 forwarding request failed on channel 0 xterm: Xt error: Can't open display: xterm: DISPLAY is not set
掘り下げていったら、面白そうな雰囲気ではありますが、先に進みます。
劇団ひとり(sshdの幕)
今度は趣向を変えて、sshと対になるsshdを調べてみたい。
問題は、現環境に、sshdが存在する事だ。同一マシンに同じアプリが存在でき ないという鉄の掟が存在する。さーどうする?
案を2つ妄想した。A案は、違うポートで待受するサーバーを起動すればいいん でないかい。B案は、一時的に現在稼動中のサーバーを停止しちゃえばいいだ ろう。そうすれば、大手を振って22番でsshdを起動できるだろう。
とりあえず敵情偵察って事で、sshd(8)すると、
-p port Specifies the port on which the server listens for connections (default 22). Multiple port options are permitted. Ports specified in the configuration file with the Port option are ignored when a command-line port is specified. Ports specified using the ListenAddress option override command-line ports.
これは、A案用のオプションだな。なんか、あたり前すぎて、感動が湧かない と思うぞ。
茨の道
今稼動してる、sshdは、普通と違って、inetdから起動してる。昔inetdの挙動 を調べた時の残骸だ。ならば、/etc/inetd.confに定義が有るはず。
ob$ grep sshd /etc/inetd.conf ssh stream tcp nowait root /usr/sbin/sshd sshd -i -4 #ssh stream tcp6 nowait root /usr/sbin/sshd sshd -i -6
IPv4の方もコメントしちゃって、doas rcctl stop inetdすれば、もうsshdが 起動してくる事は無い。今接続されてるsshは温存されるから、logoutするま では、大丈夫だ。
とりあえず、gdbできる様に、sshdをコンパイルしといた。何かの時に役立つ だろう。
敵情視察で、楽しいオプションを発見したぞ。-D と -d だ。
ob$ doas /home/sakae/sshd -dd doas (sakae@ob.my.domain) password: debug2: load_server_config: filename /etc/ssh/sshd_config debug2: load_server_config: done config len = 2261 debug2: parse_server_config_depth: config /etc/ssh/sshd_config len 2261 debug1: sshd version OpenSSH_9.7, LibreSSL 3.9.0 : debug1: rexec_argv[0]='/home/sakae/sshd' debug1: rexec_argv[1]='-dd' debug2: fd 3 setting O_NONBLOCK debug1: Bind to port 22 on 0.0.0.0. Server listening on 0.0.0.0 port 22. debug1: fd 4 clearing O_NONBLOCK debug1: Server will not fork when running in debugging mode. : debug1: session_input_channel_req: session 0 req shell debug2: channel_set_xtype: labeled channel 0 as session:shell (inactive timeout 0) Starting session: shell on ttyp3 for sakae from 127.0.0.1 port 25713 id 0
別端末から、sshしてみる。
ob$ ssh -X localhost sakae@localhost's password: Last login: Sun Jul 28 05:27:49 2024 from 127.0.0.1 OpenBSD 7.5 (GENERIC) #175: Wed Mar 20 16:33:37 MDT 2024 Welcome to OpenBSD: The proactively secure Unix-like operating system. Environment: USER=sakae LOGNAME=sakae HOME=/home/sakae PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin MAIL=/var/mail/sakae SHELL=/bin/ksh TERM=tmux SSH_CLIENT=127.0.0.1 25713 22 SSH_CONNECTION=127.0.0.1 25713 127.0.0.1 22 SSH_TTY=/dev/ttyp3 ob$ exit
こんな風に環境が報告される副作用が有るのね。
別なプランを実行
最初に検討した、sshd -p 2022 とかの事ね。これだと、inetdから起動してる sshdと共存できるはず。但し、利用するには、ssh -p2022 localhostとかしな きゃいけないけど。
現状のinetdからの起動状況 psコマンドの並びを自由に設定できる事を知ったので、試してみた。
ob$ ps -Ao ppid,pid,command | egrep '(inetd|sshd)' 1 23186 /usr/sbin/inetd -R 10 23186 83588 sshd: sakae [priv] (sshd) 83588 8149 sshd: sakae@ttyp0 (sshd)
これの図的表現。
ob$ pstree -+= 00001 root /sbin/init |-+= 23186 root /usr/sbin/inetd -R 10 | \-+= 83588 root sshd: sakae [priv] (sshd) | \-+- 08149 sakae sshd: sakae@ttyp0 (sshd) | \-+= 26472 sakae -ksh (ksh) | \--= 53017 sakae tmux: client (/tmp/tmux-1000/default) (tmux)
始祖のinitが居て、その下にダエモン君の元締めであるinetdが、ユーザーか らの要求を監視してる。マシンへの接続要求で、権限の強い(root)sshdが起動。更には、ターミ ナルとのやり取りの為に、子分のsshdが起動(こちらは通常のユーザーなので 権限に制限がある)。その端末で、ログイン・シェル であるkshが走り出す。すかさず、tmuxを動作させて、複数の端末をサポート するように指示したんだな。これが、通常のオイラーの使用方法だ。
そんじゃ早速gdbに駆けてみる。止める所は、面倒なくforkにしとく。
ob$ doas gdb -q sshd Reading symbols from sshd... (gdb) b fork Breakpoint 1 at 0x1419f0 (gdb) r -p 2022 -D Starting program: /home/sakae/sshd -p 2022 -D Breakpoint 1, _libc_fork_wrap () at /usr/src/lib/libc/sys/w_fork.c:48 48 if (TAILQ_FIRST(&_atfork_list) == NULL) { (gdb) bt #0 _libc_fork_wrap () at /usr/src/lib/libc/sys/w_fork.c:48 #1 0x17aa6f95 in server_accept_loop (sock_in=0xcf7f4ba0, sock_out=0xcf7f4b9c, newsock=0xcf7f4b98, config_s=0xcf7f4ba8) at ../sshd.c:1276 #2 0x17aa3c67 in main (ac=4, av=0xcf7f4c04) at ../sshd.c:1967
素直な作りっぽい。
ob$ pstree|grep sshd | \-+= 83588 root sshd: sakae [priv] (sshd) | \-+- 08149 sakae sshd: sakae@ttyp0 (sshd) | | \--- 70173 sakae grep sshd |--= 76737 root sshd: /home/sakae/sshd -p 2022 -D [listener] 0 of 10-100 startups (sshd)
二重起動 ?
ob# /home/sakae/sshd -p2022 ob# /home/sakae/sshd -p2022
まちがって、二重起動したけど、エラーにならず。無視されてるっぽい。これ は大人の仕様なのか? 起動してたら、黙って終了 — exit だろうね。
ob# gdb -q sshd Reading symbols from sshd... (gdb) b exit Breakpoint 1 at 0x1416e0 (gdb) r -p 2022 Starting program: /home/sakae/sshd -p 2022 [Inferior 1 (process 90879) exited normally] (gdb) q
gdbって特殊な環境だろう。普通にやってみると
ob# /home/sakae/sshd -p2022 ob# echo $? 0
まて、/var/log/* に何か出ていないか。ダエモン君は、朴訥で話をしてこな いのさ。authlogに記録されてた。
Jul 28 07:06:50 ob sshd[31565]: Server listening on 0.0.0.0 port 2022. Jul 28 07:07:52 ob sshd[10833]: error: Bind to port 2022 on 0.0.0.0 failed: Address already in use. Jul 28 07:07:52 ob sshd[10833]: fatal: Cannot bind any address. Jul 28 07:08:21 ob sshd[3538]: error: Bind to port 2022 on 0.0.0.0 failed: Address already in use. Jul 28 07:08:21 ob sshd[3538]: fatal: Cannot bind any address.
ちょっと、何処で、終了してるか調べてみたいぞ。それには、sshdのソースに 限定してgrepすれば良い。限定なソース・リストはMakefileに記載されてるん で、これを利用できるはず。ゲストを招聘して、技を披露してもらおう。
Q: BSDMakeについて質問します。 SRCS= sshd.c auth-rhosts.c auth-passwd.c … のソース・ファイル群について grep したいです。どういう記述が良いですか? .for ~ .endfor の構文を見つけましたが、これはルールを記述する為のもののようで エラーになってしまいました。
A: BSDMakeでソースファイル群を指定してgrepを行いたい場合、`.for`ループを使って各ファイルに対してコマンドを実行する方法が一般的です。ただし、Makefileの中で直接`grep`コマンドを実行する場合、少し工夫が必要です。
例えば、以下のようなMakefileの記述を使って、指定したソースファイル群に対してgrepを実行することができます。
SRCS= sshd.c auth-rhosts.c auth-passwd.c # 目的のgrepコマンドを記述 GREP_COMMAND=grep "search_string" # Makefileのターゲットを定義 grep: @echo "Searching in source files..." @for file in $(SRCS); do \ echo "Searching in $$file"; \ $(GREP_COMMAND) $$file; \ done
上記の例では、`SRCS`変数にソースファイル群をリストし、`grep`ターゲットを定義しています。このターゲットが実行されると、各ソースファイルに対して`grep`コマンドが実行され、指定した文字列を検索します。
実際に使う場合は以下のようにMakefileを実行します。
make grep
この方法を使えば、Makefileの中でソースファイル群に対してgrepを実行する ことができます。`$(SRCS)`変数に追加のソースファイルを記述するだけで、 対象ファイルを簡単に増やすことが可能です。
実際に試してみると、2個目以降でファイルがみつからないエラーになった。 で、再度質問
A: BSDMakeの`@for`ループの実行で2つ目以降のファイルでエラーが出る場合、エラーの原因は`grep`コマンドがファイル内で一致する行を見つけられなかった場合など、何かしらのエラーが発生している可能性があります。
`.for`構文を使って同じことを行う方法もあります。以下のように`.for`構文を使用して、エラーが発生しないようにMakefileを記述します。
SRCS= sshd.c auth-rhosts.c auth-passwd.c GREP_COMMAND=grep "search_string" grep: @echo "Searching in source files..." @.for file in ${SRCS} @echo "Searching in ${file}" @${GREP_COMMAND} ${file} || true @.endfor
.forはshellコマンドではありませんって事で、.for: not found とか文句を くらった。そんな事より、
ここで重要なのは、`|| true`を追加して、grepコマンドが失敗した場合でもエラーを無視するようにしている点です。これにより、1つ目のファイルで検索に成功しても、2つ目以降のファイルで一致する行がなくてもエラーを発生させずに続行します。
ob# cat makefile # $OpenBSD: Makefile,v 1.109 2024/02/10 11:28:52 naddy Exp $ .PATH: ${.CURDIR}/.. SRCS= sshd.c auth-rhosts.c auth-passwd.c sshpty.c sshlogin.c servconf.c \ serverloop.c auth.c auth2.c auth-options.c session.c auth2-chall.c \ groupaccess.c auth-bsdauth.c auth2-hostbased.c auth2-kbdint.c \ auth2-none.c auth2-passwd.c auth2-pubkey.c auth2-pubkeyfile.c \ monitor.c monitor_wrap.c \ sftp-server.c sftp-common.c sftp-realpath.c sandbox-pledge.c srclimit.c SRCS+= authfd.c compat.c dns.c fatal.c hostfile.c readpass.c utf8.c uidswap.c SRCS+= ${SRCS_BASE} ${SRCS_KEX} ${SRCS_KEXS} ${SRCS_KEY} ${SRCS_KEYP} \ ${SRCS_KRL} ${SRCS_PROT} ${SRCS_PKT} ${SRCS_UTL} ${SRCS_PKCS11} \ ${SRCS_SK_CLIENT} GREP_COMMAND=grep "exit" grep: @for file in $(SRCS); do \ echo "==== Searching in $$file"; \ $(GREP_COMMAND) ../$$file || true; \ done
結局こんなのにした。ファイル名は、小文字でmakefile。大文字で始まる Makefileより強いからね(Makefileの使い回し、DRYの原則ですよ)。大事なのは、trueを入れてのエラー回避と、ファイ ルの位置は、一つ上の階層だよって指定。.PATHでmakefileの存在するCURDIR より上だよって指定があるけど、それは今回のコードの範囲外だった。
ob# make : ==== Searching in fatal.c cleanup_exit(255); ==== Searching in hostfile.c ==== Searching in readpass.c _exit(1); ==== Searching in utf8.c ==== Searching in uidswap.c
結局、どういう終了方法をとってるか、/sys/sys/syscall.hを参照して、検討。
(gdb) b _exit Breakpoint 1 at 0x141a40 (gdb) r -p 2022 Starting program: /home/sakae/sshd -p 2022 Breakpoint 1, _thread_sys__exit () at /tmp/-:2 2 /tmp/-: No such file or directory. (gdb) bt #0 _thread_sys__exit () at /tmp/-:2 #1 0x012d62ca in daemon (nochdir=0, noclose=0) at /usr/src/lib/libc/gen/daemon.c:47 #2 0x199fb8b0 in main (ac=3, av=0xcf7dcf34) at ../sshd.c:1921 (gdb) c Continuing. [Inferior 1 (process 71300) exited normally]
sshd -D
sshdの起動時に、-D を付けている。
-D When this option is specified, sshd will not detach and does not become a daemon. This allows easy monitoring of sshd.
これが無くても、下記のgdb設定により、何とかなるかと思ったけど、どうに もならなかった。
set follow-fork-mode (child|parent) set follow-exec-mode (new|same)
ちょっと悔しいので、現場を確認。 sshd.c
case 'D': no_daemon_flag = 1; break;
オプションの解析はmain()の中で実施され、それに見あうフラグを立てるのが 定石。
already_daemon = daemonized(); if (!(debug_flag || inetd_flag || no_daemon_flag || already_daemon)) { if (daemon(0, 0) == -1) fatal("daemon() failed: %.200s", strerror(errno)); disconnect_controlling_tty(); }
フラグで検索すると、現場に到達できる。
興味は、ダエモン君になったプロセスにgdbでアタッチできるかという点。
ob$ ps awx|grep 2022 23518 ?? I 0:00.00 sshd: /home/sakae/sshd -p 2022 [listener] 0 of 10-100 startups (sshd) ob$ doas gdb -q /home/sakae/sshd -p 23518 Reading symbols from /home/sakae/sshd... Attaching to program: /home/sakae/sshd, process 23518 Reading symbols from /usr/lib/libcrypto.so.53.0... Reading symbols from /usr/lib/libutil.so.18.0... Reading symbols from /usr/lib/libz.so.7.0... Reading symbols from /usr/lib/libc.so.99.0... Reading symbols from /usr/libexec/ld.so... [Switching to thread 606834] _thread_sys_ppoll () at /tmp/-:2 2 /tmp/-: No such file or directory. (gdb) b fork Breakpoint 1 at 0x86da937: file /usr/src/lib/libc/sys/w_fork.c, line 48. (gdb) c Continuing.
別端末から ssh -p2022 localhost する。
Breakpoint 1, _libc_fork_wrap () at /usr/src/lib/libc/sys/w_fork.c:48 48 if (TAILQ_FIRST(&_atfork_list) == NULL) { (gdb) bt #0 _libc_fork_wrap () at /usr/src/lib/libc/sys/w_fork.c:48 #1 0x19ed3f95 in server_accept_loop (sock_in=0xcf7ee740, sock_out=0xcf7ee73c, newsock=0xcf7ee738, config_s=0xcf7ee748) at ../sshd.c:1276 #2 0x19ed0c67 in main (ac=3, av=0xcf7ee7a4) at ../sshd.c:1967
ダエモン君になっていても、ちゃんとgdbに反応してくれた。ワーイ、嬉しい な。こうして経験値があがっていくのです。
(gdb) bt #0 _thread_sys_ppoll () at /tmp/-:2 #1 0x086a760a in _libc_ppoll_cancel (fds=0x63198660, nfds=1, timeout=0x0, sigmask=0x6d) at /usr/src/lib/libc/sys/w_ppoll.c:36 #2 0x19ed3687 in server_accept_loop (sock_in=0xcf7ee740, sock_out=0xcf7ee73c, newsock=0xcf7ee738, config_s=0xcf7ee748) at ../sshd.c:1152 #3 0x19ed0c67 in main (ac=3, av=0xcf7ee7a4) at ../sshd.c:1967
こんな所で、待受してるんだな。
at ArchLinux
普通のLinuxでは、どうなっているか観察しておく。
[sakae@arch ~]$ ps -eo ppid,pid,stat,command | grep sshd 1 373 Ss sshd: /usr/bin/sshd -D [listener] 0 of 10-100 startups 373 451 Ss sshd-session: sakae [priv] 451 470 S sshd-session: sakae@pts/0 1 508 Ss sshd: /usr/sbin/sshd -p 2022 [listener] 0 of 10-100 startups 508 514 Ss sshd-session: sakae [priv] 514 518 S sshd-session: sakae@pts/3
図的な表現。親はsystemdになってて、そこからsessionを経由して、下々の要 求に答るようになってるのね。
[sakae@arch ~]$ pstree systemd─┬─agetty ├─sshd─┬─sshd-session───sshd-session───bash───tmux: client │ └─sshd-session───sshd-session───bash ├─sshd
わざわざ -D を付けて起動してるって事は、systemdの配下に置いて、よから ぬ事を綰らんでいるのか脳? リナの事は、よう分からんわい。