「誰が何をしたか」はどこに残るのか
一言でいうと
検知は、ルールから始まりません。そのイベントが、そもそもどこかに記録されるかから始まります。Linuxホストで、その場所は3か所で、3か所が見ているものは重なりません。カーネル監査(auditd)はシステムコールを、journaldはプログラムが自分で語ったものを、Kubernetesの監査ログはAPIサーバーに入ってきたリクエストを見ます。
なぜ必要なのか
午前3時に、スケジュールされたジョブが外部にリクエストを送ったとします。翌朝出勤して、この一文を証明するには、4つのことを知る必要があります。何が実行されたのか、誰がそれをスケジュールしたのか、いつそのスケジュールができたのか、そしてそのリクエストがどこへ行ったのか。
ここで人が最もよくぶつかる壁が、「ログはすべて集めているのに、答えがない」です。ログを集めることと、問いに答えられる記録を残すことは、別だからです。/var/log/syslogには、cronが何を実行したかは残りますが、そのcronファイルを誰がいつ作ったのかはありません。逆に、カーネル監査は、ファイルが作られた瞬間を正確に残しますが、監視ルールをかけていないパスは、そもそも見ません。
そのため、検知エンジニアリングの最初の仕事は、ルールの作成ではなく、ソース調査です。問いを先に書き、その問いに答えられる記録が今残っているかを、1つずつ確認します。残っていないなら、それを残すようにすることが最初の作業で、そのあとで初めて、ルールを書く資格が生まれます。
どう動くのか
カーネル監査サブシステムは、2種類のルールを受け付けます。1つはファイル監視で、もう1つはシステムコールフィルターです。
-w /etc/cron.d -p wa -k cron_change
└ 경로 └ 권한 └ 검색용 키
이 디렉터리에 쓰기(w)나 속성 변경(a)이 일어나면 남긴다
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/curl -k outbound_exec
└ 언제 └ 아키텍처 └ 시스콜 └ 추가 필터 └ 키
64비트 execve 중 실행 파일이 curl 인 것만 남긴다
このコードブロックの韓国語は、1つ目のルールについて、下の3つの注記が順に、パス、権限、検索用のキーを指し、このディレクトリに対する書き込み(w)や属性変更(a)が起きたら記録する、という意味で、2つ目のルールについて、注記が順に、いつ、アーキテクチャ、システムコール、追加のフィルター、キーを指し、64ビットのexecveのうち実行ファイルがcurlのものだけを記録する、という意味です。
-wは、実はシステムコールルールの省略表現で、-kで付けたキーが、あとでausearch -kの検索キーになります。ルールの文法とフィルターフィールドはauditctl(8)に、ルールファイルに書く形式はaudit.rules(7)にあります。手でauditctlを打って入れたルールは、再起動すると消えるので、生き残らせるには、/etc/audit/rules.d/に書き、augenrules(8)にそれをまとめてロードさせます。読むときは、ausearch(1)がキー・ユーザー・時刻で切り出してくれ、aureport(8)が集計を出します。
journaldは、性格がまったく違います。カーネルが観察するのではなく、プログラムが自分で語ったものを書き留めます。そのため、cronが残す(root) CMD (…)の1行は、cronが親切だからあるのであって、カーネルが保証するものではありません。その代わり、journaldは構造化フィールドを一緒に持っています。_PID、_UID、_SYSTEMD_UNIT、SYSLOG_IDENTIFIERのようなもので、一覧はsystemd.journal-fields(7)にあります。journalctl -o jsonで取り出すと、このフィールドがそのまま出て、機械が読めます(journalctl(1))。
Kubernetesは、また別の層です。APIサーバーの前で起きたことは、ホストのログには残りません。監査ログは既定でオフになっていて、--audit-policy-fileでポリシーを与えて初めて有効になります。ポリシーはルールごとにレベルを選びます。Noneは残さず、Metadataは、リクエスト者・時刻・リソース・動詞だけ、Requestはリクエスト本文まで、RequestResponseはレスポンス本文まで残します。ステージも4つです。RequestReceived、ResponseStarted、ResponseComplete、Panic。この選択が、そのままコストであり、証拠の解像度です。
現場での姿
最初の姿は、「ルールはあるのに、場所が間違っている」場合です。/etc/cron.dは監視しているのに、/usr/local/binは監視していないホストがよくあります。攻撃者は、スケジュールされたジョブを新しく作る必要がありません。すでにスケジュールされているスクリプトの内容を、1行直せばよいのです。その瞬間、監査ログは静かです。静かなことは、安全という意味ではなく、その場所を見ていないという意味です。
2つ目は、「記録はあるのに、読めない」場合です。カーネル監査は、人が読みにくい数字で残ります。uidは数字で、システムコールも数字です。ausearch -iがそれを名前に変えてくれますが、このオプションを知らずに、原本だけをのぞいて、「うちの監査ログは役に立たない」という結論に達したチームを、何度も見ました。
3つ目は、Kubernetes側です。ノードが侵害されると、そのノードのkubeletの証明書も一緒に持ち出されます。ホストのログは、プロセスがファイルを読んだところまでしか見せてくれず、その証明書でクラスターで何をしたかは、APIサーバーの監査ログにしかありません。2つの記録をつなげられないと、「ファイル1つが読まれた」で、調査が終わります。
次のラボですること
ラボでは、VM1台に自分で入って監査ルールをかけ、イベントをわざと発生させ、その記録をausearchで取り出してみます。次に、同じイベントをjournald側でもう一度探して、2つの記録がそれぞれ何を持っているかを比べます。最後に、監視していない場所を1つ選んで、記録が残らないことを自分で確認し、その場所を覆うルールを、新しいキーで追加します。Podにはカーネル権限が1つもないので、このラボはVMで動きます。