🌊

OpenSSH: 認証が完了したあとの各プロセスの状態についてのメモ

2024/12/23に公開

SSHログインしたあと、sshdbashなどのプロセスがどういう状態になっているかに関する調査。対象はOpenSSHのV_8_7_P1

SSHでログインした直後の各プロセスの状態は以下のようになっている。

image

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

接続を受け付けるまでの流れ

sshdserver_accept_loopでクライアントからの接続を待ち、クライアントから接続があるとforkしてacceptedという名前の子プロセスを生成します。

image-2

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/emptychrootし、SSH_PRIVSEP_USERpasswdエントリーを取得してSSH_PRIVSEP_USERユーザーのuidをセットします。SSH_PRIVSEP_USERsshdです。そして、特権分離のあとにdo_authentication2を実行し認証処理を開始します。

子プロセス(sshd [net])は、認証が成功するまでクライアントと認証処理を続けます。子プロセス(sshd [net])は、クライアントに許可された認証メソッドを渡しクライアントが選択した認証メソッド(Authmethods)を実行します。

例えばクライアントが公開鍵認証(publickey)を選択した場合、Authmethods(method_pubkey)が子プロセス(sshd [net])で実行されます。

子プロセス(sshd [net])はuserauth_pubkeymm_sshkey_verifyを実行し、親プロセス(sshd [accepted])にMONITOR_REQ_KEYVERIFYを送信して親プロセス(sshd [accepted])からMONITOR_ANS_KEYVERIFYが来るのを待ちます

MONITOR_REQ_KEYVERIFYを受け取った親プロセス(sshd [accepted])はmm_answer_keyverifyssh_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_dispatchdispatcherが登録されます。

疑似端末の用意

クライアントはsshdpty-reqリクエストを送ります。pty-reqを受け取った子プロセスのsshdsession_pty_reqを実行します。session-pty-reqでは、mm_pty_allocateを実行しsshd [priv]MONITOR_REQ_PTYリクエストを送ります。MONITOR_REQ_PTYを受け取った親プロセス(sshd [priv])はmm_answer_ptyを実行します。mm_answer_ptypty_allocateを実行し疑似端末を用意します。

シェルの用意

クライアントはsshdshellリクエストを送ります。shellリクエストを受け取った子のsshdsession_shell_req を実行しdo_exec を実行します。do_execdo_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で標準入力、標準出力、標準エラーとしexecvebashなどのシェルになります。

image-3

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