OpenSSH: 認証が完了したあとの各プロセスの状態についてのメモ
SSHログインしたあと、sshd
やbash
などのプロセスがどういう状態になっているかに関する調査。対象はOpenSSHのV_8_7_P1。
SSHでログインした直後の各プロセスの状態は以下のようになっている。
plantuml
@startuml
!theme blueprint
rectangle sshd1 as "sshd [listener]"
rectangle client as "SSH CLient"
scale 1024 width
card "process \ngroup" as pg1 {
rectangle "sshd [priv]" as sshd2
rectangle "sshd\nec2-user@pts/0" as sshd3
}
card "session" as session {
card "process \ngroup\n(Foreground)\n(Session Leader)" as pg2 {
rectangle "bash" as bash
}
}
rectangle "カーネル" as kernel {
rectangle "pty master" as ptmx
rectangle "pty slave\n(制御端末)" as pts
}
sshd1 -r-> sshd2 : "accepted\n+\nfork"
sshd2 -r-> sshd3 : "fork"
sshd3 -r-> bash : "fork + exec"
sshd3 <-d[hidden]-> ptmx
sshd2 -d- ptmx
sshd3 -d- ptmx
session -d- pts
ptmx <-r-> pts
client -u- sshd2
client .u.> sshd1
@enduml
接続を受け付けるまでの流れ
sshd
はserver_accept_loop
でクライアントからの接続を待ち、クライアントから接続があるとforkしてaccepted
という名前の子プロセスを生成します。
plantuml
@startuml
!theme blueprint
rectangle sshd1 as "sshd [listener]"
rectangle client as "SSH CLient"
scale 1024 width
card "process \ngroup" as pg1 {
rectangle "sshd [accepted]" as sshd2
}
sshd1 -r-> sshd2 : "accepted\n+\nfork"
client -u- sshd2
client .u.> sshd1
@enduml
接続を受け付けてから認証が完了するまでの流れ
sshd [accepted]
は接続を受け付けた後に認証を開始します。認証は特権分離されたプロセスで行われます。特権分離されたプロセスで認証を行うことで、認証前にこのプロセスがおかしくなっても(corrupt)、root権限の取得が非常に難しくなります。
The goal of privilege separation is to make sure pre-authentication attacks cannot compromise the root account even though other parts of OpenSSH do run with root privileges.
OpenSSH Privilege Separation and Sandbox - Attack Surface Analysis
sshd [accepted]
はprivsep_preauthを実行しforkして子プロセス(sshd [net]
)を作成します。鍵交換やユーザー認証などクライアントとネットワーク通信をするのは、特権分離されたプロセスのsshd [net]
です。sshd [accepted]
はmonitor_child_preauthで子プロセス(sshd [net]
)の認証が完了するまで待ちます。
子プロセス(sshd [net]
)はprivsep_preauth_childを実行し特権分離をします。/var/emptyにchrootし、SSH_PRIVSEP_USERのpasswd
エントリーを取得してSSH_PRIVSEP_USER
ユーザーのuidをセットします。SSH_PRIVSEP_USER
はsshdです。そして、特権分離のあとにdo_authentication2を実行し認証処理を開始します。
子プロセス(sshd [net]
)は、認証が成功するまでクライアントと認証処理を続けます。子プロセス(sshd [net]
)は、クライアントに許可された認証メソッドを渡し、クライアントが選択した認証メソッド(Authmethods
)を実行します。
例えばクライアントが公開鍵認証(publickey
)を選択した場合、Authmethods
(method_pubkey
)が子プロセス(sshd [net]
)で実行されます。
子プロセス(sshd [net]
)はuserauth_pubkey
でmm_sshkey_verify
を実行し、親プロセス(sshd [accepted]
)にMONITOR_REQ_KEYVERIFY
を送信して親プロセス(sshd [accepted]
)からMONITOR_ANS_KEYVERIFY
が来るのを待ちます。
MONITOR_REQ_KEYVERIFY
を受け取った親プロセス(sshd [accepted]
)はmm_answer_keyverify
でssh_keyverify
を実行します。sshkey_verify
は成功したら0を返し、mm_answer_keyverify
は認証が成功したら1を返します。その後、親プロセスはMONITOR_ANS_KEYVERIFY
を子プロセスに送ります
ここで認証が成功したので親プロセス(sshd [accepted]
)はループをbreakし、子プロセス(sshd [net]
)のプロセスの終了を待ちます。MONITOR_ANS_KEYVERIFY
を受け取り認証が成功した子プロセス(sshd [net]
)はexit(0)で終了します。
子プロセス(sshd [net]
)の終了後、親プロセス(sshd [accepted]
)は1を返し、authenticatedブロックへ移ります。
なお、この段階で親プロセスはsshd [accepted]
からsshd [priv]
に名前が変わっています。
plantuml
@startuml
!theme blueprint
rectangle sshd1 as "sshd [listener]"
rectangle client as "SSH CLient"
scale 1024 width
card "process \ngroup" as pg1 {
rectangle "sshd [priv]" as sshd2
rectangle "sshd [net]" as sshd3 #line.dotted
}
sshd1 -r-> sshd2 : "accepted\n+\nfork"
sshd2 -r-> sshd3 : "fork"
client -u- sshd2
client .u.> sshd1
@enduml
認証完了後
sshd [priv]
はprivsep_postauthを実行しforkして子プロセスを作ります。子プロセスはdo_setusercontextでユーザー権限で動くsshd
プロセスになります。親プロセスであるsshd [priv]
はmonitor_child_postauthを実行し子プロセスのリクエストを受けるループになります。
生成された子プロセスはsshセッションの準備を初めます。do_authenticated2を実行しクライアントからのリクエストを受けるためのループを実行します。server_init_dispatchでdispatcherが登録されます。
疑似端末の用意
クライアントはsshd
にpty-reqリクエストを送ります。pty-req
を受け取った子プロセスのsshd
はsession_pty_reqを実行します。session-pty-reqでは、mm_pty_allocateを実行しsshd [priv]
にMONITOR_REQ_PTYリクエストを送ります。MONITOR_REQ_PTY
を受け取った親プロセス(sshd [priv]
)はmm_answer_ptyを実行します。mm_answer_ptyでpty_allocateを実行し疑似端末を用意します。
シェルの用意
クライアントはsshd
にshellリクエストを送ります。shell
リクエストを受け取った子のsshd
はsession_shell_req を実行しdo_exec を実行します。do_execでdo_exec_pty実行しforkして後にシェルとなる子プロセスを生成します。その後、pty_make_controlling_terminalを実行しsetsidシステムコールで生成した子プロセスをセッションリーダーにします。
The calling process is the leader of the new session
https://man7.org/linux/man-pages/man2/setsid.2.html
setsid
の仕様でこのセッションはまだ制御端末を持っていません。
Initially, the new session has no controlling terminal.
その後、先に用意した疑似端末のslave側をopenしてそれを制御端末にします。さらに、疑似端末のslave側をdup2で標準入力、標準出力、標準エラーとしexecveでbash
などのシェルになります。
plantuml
@startuml
!theme blueprint
rectangle sshd1 as "sshd [listener]"
rectangle client as "SSH CLient"
scale 1024 width
card "process \ngroup" as pg1 {
rectangle "sshd [priv]" as sshd2
rectangle "sshd\nec2-user@pts/0" as sshd3
}
card "session" as session {
card "process \ngroup\n(Foreground)\n(Session Leader)" as pg2 {
rectangle "bash" as bash
}
}
rectangle "カーネル" as kernel {
rectangle "pty master" as ptmx
rectangle "pty slave\n(制御端末)" as pts
}
sshd1 -r-> sshd2 : "accepted\n+\nfork"
sshd2 -r-> sshd3 : "fork"
sshd3 -r-> bash : "fork + exec"
sshd3 <-d[hidden]-> ptmx
sshd2 -d- ptmx
sshd3 -d- ptmx
session -d- pts
ptmx <-r-> pts
client -u- sshd2
client .u.> sshd1
@enduml
Discussion