TT Lab
はじめる
学ぶ 学習パス コース

cron が午前三時に curl を叩いた

「誰が何をしたか」はどこに残るのか

TT Labで続きを見る

一言でいうと

検知は、ルールから始まりません。そのイベントが、そもそもどこかに記録されるかから始まります。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で動きます。