セキュリティ警告と監視の一環としてのLinuxプロセスとセッションモデル

illustration-radar-security.png

Elasticで提供されているLinuxプロセスモデルにより、ユーザーは非常にターゲットを絞ったアラートルールを作成し、LinuxサーバーやLinuxデスクトップで何が起きているのかを正確に把握することができます。

このブログでは、Linuxワークロードの表現方法の重要な側面であるLinuxプロセスモデルの背景を説明します。

Linuxは、初期のPOSIXドキュメントによってsetsid()システム呼び出しが導入されたときから判断すると、1980年代にセッションの概念で拡張された1970年代のUnixプロセスモデルに従っています。

Linuxプロセスモデルは、コンピューターのワークロード(どのプログラムが実行されるか)を記録し、これらのイベントに対応するルールを記述するのに適した抽象化です。アラート、コンプライアンス、脅威ハンティングにおいて、誰がどのサーバーでいつ何を行ったのかを明確に示すことができます。

プロセスの作成、権限の昇格、およびライフスパンを取り込むことで、アプリケーションやサービスがどのように実装されているか、またプログラムの通常の実行パターンについて深いインサイトが得られます。正常な実行パターンが特定されたら、異常な実行パターンが発生した場合にアラートを送信するルールを作成することができます。

詳細なプロセス情報により、アラート用に非常に的を絞ったルールを作成することができるため、誤検知やアラート疲労を軽減することができます。 また、Linuxセッションを次のいずれかに分類することもできます。

    • ブート時に起動する自律サービス(cronなど)
    • リモートアクセスを提供するサービス(sshdなど)
    • 対話型(一般的には人間による)リモートアクセス(ssh経由で起動するbashターミナルなど)
    • 非対話型リモートアクセス(Ansibleによるssh経由でのソフトウェアのインストールなど)

これらの分類により、非常に正確なルールとレビューが可能になります。たとえば、選択した時間枠内の特定のサーバー上のすべての対話型セッションをレビューすることができます。

この記事では、Linux プロセスモデルがどのように機能するかについて説明し、ワークロードイベントに対するアラートと対応のルールの作成を支援します。Linuxのプロセスモデルを理解することも、コンテナーやコンテナーが構成される名前空間とcgroupsを理解するための重要な第一歩です。

プロセスモデルの取り込みとシステム呼び出しログの比較

新しいプロセス、新しいセッション、終了するプロセスなど、セッションモデルへの変更を取り込むことは、それらの変更を実行するために使われるシステム呼び出しを取り込むことよりもシンプルで明確です。Linuxには約400のシステム呼び出しがあり、一度リリースされるとリファクタリングされません。このアプローチでは、安定したアプリケーションバイナリインターフェース(ABI)が維持されるため、何年も前にLinux上で実行するためにコンパイルされたプログラムでも、ソースコードから再構築しなくても、現在もLinux上で実行し続けることができます。

新しいシステム呼び出しは、既存のシステム呼び出しをリファクタリングする代わりに、機能やセキュリティを向上させるために追加されます(ABIの破壊を回避)。つまり、時間順に並んだシステム呼び出しとそのパラメーターのリストを、それらが実行する論理的なアクションにマッピングするには、かなりの専門知識が必要だということです。さらに、io_uring のような新しいシステム呼び出しは、カーネル空間とユーザー空間の間にマッピングされたメモリを使用することで、追加のシステム呼び出しなしでファイルやソケットの読み書きを可能にします。

対照的に、プロセスモデルは安定しており(1970年代からあまり変化していない)、ファイルアクセス、ネットワーク、その他の論理操作を含めると、システム上で行われるアクションを包括的にカバーしています。

プロセス形成:initはブート後の最初のプロセスです。

Linuxカーネルが起動すると、"initプロセス"と呼ばれる特別なプロセスが作成されます。プロセスは、1つ以上のプログラムの実行を具現化します。initプロセスは常に1のプロセスID(PID)が割り当てられ、0(root)のユーザーIDで実行されます。最新のLinuxディストリビューションのほとんどは、initプロセスの実行プログラムとしてsystemdを使っています。

initの役割は、データベース、Webサーバー、sshdなどのリモートアクセスサービスといった構成されたサービスを起動することです。通常、これらのサービスは独自のセッション内でカプセル化され、各サービスのすべてのプロセスを単一のセッションID(SID)の下にグループ化することで、サービスの開始と停止を簡素化します。

sshdサービスへのSSHプロトコル経由などのリモートアクセスは、アクセスするユーザーの新しいLinuxセッションを作成します。このセッションは最初にリモートユーザーが要求したプログラム(通常は対話型シェル)を実行し、関連するプロセスはすべて同じSIDが設定されます。

プロセス作成の仕組み

initプロセス以外のすべてのプロセスは、1つの親プロセスがあります。各プロセスは親プロセスのプロセスIDであるPPIDが設定されます(initの場合は0/親なし)。親プロセスが子プロセスも終了させずに終了すると、親子関係の再構築が発生する可能性があります。

通常、親子関係の再構築は、initを新しい親として選びます。そして、initには特殊なコードがあり、子を採用した後に終了するときに、クリーンアップを実行します。この採用とクリーンアップのコードがなければ、孤立した子プロセスは「ゾンビ」プロセスになってしまいます(冗談ではありません!)。子プログラムは、親プログラムが(子プログラムがタスクを正常に完了したかどうかを示す)終了コードを調べることができるように、親プログラムが子プログラムをリープするまで待機します。

「コンテナー」、特にpid名前空間の登場により、init以外のプロセスを「サブリーパー」(孤立したプロセスを積極的に採用するプロセス)として指定する機能が必要になりました。通常、サブリーパーはコンテナーの最初のプロセスです。これは、コンテナー内のプロセスが上位のpid名前空間内のプロセスを「見る」ことができないためです(つまり、親が上位のpid名前空間にある場合、そのPPID値は意味を持ちません)。

子プロセスを作成するには、親プロセスがfork()またはclone()システム呼び出しで自分自身を複製します。派生/複製の後、実行はただちに子(vfork()とclone()のCLONE_VFORKオプションを無視)の両方で継続されますが、fork()/clone()からの戻りコード値によって異なるコードパスに従って実行されます。

1つのfork()/clone()システム呼び出しが2つの異なるプロセスで戻りコードを提供することを正しく理解してください。親は子のPIDを戻りコードとして受け取り、子は0を受け取るので、親と子の共有コードはその値に基づいて分岐することができます。マルチスレッドの親と効率化のためのコピー・オン・ライト・メモリに関する複製のニュアンスについては、ここで詳しく説明する必要があります。子プロセスは、親プロセスのメモリ状態、開いているファイル、ネットワークソケット、制御ターミナル(ある場合)を継承します。

通常、親プロセスは子プロセスのライフサイクルを監視するために、子プロセスのPIDを取得します(上記のリープを参照)。子プロセスの動作は、自分自身を複製したプログラムに依存します(fork()からの戻りコードに基づいて従う実行パスを提供します)。

nginxのようなWebサーバーは、http接続を処理するために子プロセスを作成し、自分自身を複製する場合があります。このような場合、子プロセスは新しいプログラムを実行するのではなく、単に同じプログラム内の別のコードパスを実行し、この場合のhttp接続を処理します。複製や派生の戻り値は、子プロセスがこのコードパスを選択できるように、子プロセスに子プロセスであることを伝えます。

対話型シェルプロセス(例:bash、sh、fish、zshなど、制御ターミナルを使用するプロセス)は、おそらくsshセッションから、コマンドが入力されるたびに自分自身を複製します。子プロセスは、親/シェルからのコードパスを実行したまま、IOリダイレクトのためのファイル記述子の設定、プロセスグループの設定などを行います。その後に、子プロセスのコードパスがexecve()システム呼び出しなどを呼び出して、そのプロセス内で別のプログラムを実行します。

シェルにlsと入力すると、シェルが派生され、上記のセットアップがシェル/子プロセスで行われ、lsプログラム(通常は/usr/bin/lsファイルから)が実行され、そのプロセスの内容がlsのマシンコードに置き換えられます。シェルのジョブ制御の実装に関するこの記事は、シェルとプロセスグループの内部動作に関する重要な知見を提供します。

プロセスがexecve()を複数回呼び出す可能性があるため、ワークロードキャプチャデータモデルはこれも処理しなければならないことに注意することが重要です。これは、プロセスが終了するまでに、親プロセスのプログラムにオプションで1つのプログラムが続くだけでなく、多数の異なるプログラムになる可能性があることを意味します。シェルでこれを行う方法(つまり、シェルプログラムを同じプロセス内の別のプログラムに置き換える方法)については、シェルexec builtinコマンドを参照してください。

プロセス内でプログラムを実行するもう1つの側面は、開いているファイル記述子(close-on-execとして指定)が新しいプログラムの実行前に閉じられる可能性がある一方で、他のファイル記述子は新しいプログラムでも使用可能な場合があることです。1回のfork()/clone()呼び出しで、親と子の2つのプロセスで戻りコードが返されることを思い出してください。execve()システム呼び出しも奇妙です。execve()が成功した場合は、新しいプログラムが実行されるため、成功した場合の戻りコードがありません。したがって、execve()が失敗した場合以外は、戻る場所がありません。

新しいセッションの作成

現在、Linuxは、setsid()という1つのシステム呼び出しを使って新しいセッションを作成しています。setsid()は、新しいセッションリーダーになるプロセスによって呼び出されます。このシステム呼び出しは、多くの場合、複製された子プロセスのコードパスの一部であり、そのプロセスで別のプログラムを実行する前に実行されます(つまり、親プロセスのコードによって計画され、親プロセスのコードに含まれます)。セッション内のすべてのプロセスは同じSIDを共有します。これは、setsid()を呼び出されたプロセスのPIDと同じで、セッションリーダーとしても知られています。言い換えると、セッションリーダーとは、そのSIDと一致するPIDを持つすべてのプロセスのことです。セッションリーダープロセスが終了すると、その直下の子プロセスグループが終了します。

新しいプロセスグループの作成

Linuxでは、セッション内で一緒に動作するプロセスのグループを識別するために、プロセスグループを使用します。これらはすべて同じSIDとプロセスグループID(PGID)が割り当てられます。PGIDは、プロセスグループリーダーのPIDです。プロセスグループリーダーには特別なステータスはありません。プロセスグループの他のメンバーに影響を与えずに終了することができ、そのPIDを持つプロセスが存在しなくなったとしても、同じPGIDを保持します。

pid-wrap(ビジーなシステムで最近使用されたpidの再利用)であっても、Linuxカーネルは、終了したプロセスグループリーダーのpidが、そのプロセスグループのすべてのメンバーが終了するまで再利用されないことを保証します(つまり、そのPGIDが誤って新しいプロセスを参照する可能性はありません)。

プロセスグループは、次のようなシェルパイプラインコマンドで有用です。

cat foo.txt | grep bar | wc -l

これにより、3つの異なるプログラム(cat、grep、wc)用に3つのプロセスが作成され、パイプで連結されます。シェルは、lsのような単一プログラムのコマンドでも新しいプロセスグループを作成します。プロセスグループの目的は、シグナルのターゲットをプロセスの集合に絞ることを許可し、セッションの制御ターミナル(ある場合)へのフル読み書きアクセスが許可されているプロセスの集合(フォアグラウンドプロセスグループ)を識別することです。

言い換えると、シェルのcontrol-Cは、フォアグラウンドのプロセスグループ内のすべてのプロセスに割り込みシグナルを送信します(シグナルのpidターゲットとしての負のPGID値は、グループとプロセスグループリーダプロセス自体を区別します)。制御ターミナルの関連付けによって、ターミナルから入力を読み取るプロセスが互いに競合して問題を引き起こさないことが保証されます(ターミナルの出力は、非フォアグラウンドのプロセスグループから許可される場合があります)。

ユーザーとグループ

前述のように、initプロセスはユーザーID 0(root)が割り当てられます。各プロセスには関連するユーザーとグループがあり、これらを使ってシステム呼び出しやファイルへのアクセスを制限することができます。ユーザーとグループには数値のIDがあり、rootやmsのような関連する名前が設定されている場合があります。rootユーザーはあらゆる操作を実行できるスーパーユーザーであり、セキュリティ上の理由から絶対に必要な場合にのみ使用することをお勧めします。

Linuxカーネルにとって重要なのはidのみです。名前は任意で、/etc/passwd/etc/groupファイルによって人間のユーザーにとっての利便性のために提供されます。Name Service Switch(NSS)を使うと、これらのファイルをLDAPや他のディレクトリのユーザーやグループで拡張することができます(/etc/passwdとNSSが提供するユーザーの組み合わせを表示する場合はgetent passwdを使います)。

各プロセスには、それに関連する複数のユーザーとグループ(実グループ、有効グループ、保存グループ、補助グループ)を設定できます。詳細については、man 7 credentialsを参照してください。

ルートファイルシステムがコンテナーイメージによって定義されるコンテナーの使用が増えたため、/etc/passwdと/etc/groupが存在しなかったり、使用中のユーザーIDやグループIDの名前が見つからなかったりする可能性が高くなりました。Linuxカーネルではこれらの名前は重要ではなく、idのみが必要であるため、この点は問題ありません。

まとめ

Linuxプロセスモデルは、サーバーのワークロードを正確かつ簡潔に表現する方法を提供し、これにより非常に的を絞った警告ルールとレビューが可能になります。ブラウザーでプロセスモデルをセッションごとにわかりやすくレンダリングすることで、サーバーのワークロードに関する優れたインサイトが得られます。

無料のElastic Cloudの14日間トライアルを利用してお試しください。また、セルフマネージド版のElastic Stackを無料でダウンロードしてお使いいただくこともできます。

詳細

Linuxのmanページは優れた情報源です。上記のLinuxプロセスモデルの詳細については、以下のmanページを参照してください。