システムコールで捕まえれば嘘をつけない
一言でいうと
Falcoは、カーネルからシステムコールを受け取ってルールと照合する、ランタイム検知エンジンです。ルール1本は、rule・desc・condition・output・priorityの5項目からなり、繰り返される条件はmacroに、値の一覧はlistに切り出して再利用します。
なぜ必要なのか
ログベースの検知には、構造的な限界が1つあります。ログには、プログラムが残すことにしたものだけが残ります。侵入者がログを残さない方法で動くと、たとえば/tmpにバイナリを落として、すぐ実行すると、どのアプリケーションもそれを記録しません。
システムコールは違います。プロセスを作るのも、ファイルを開くのも、ソケットを接続するのも、カーネルを経由する必要があり、カーネルをだますことはできません。そのため、「実際に何が起きたか」を問う場所として、システムコールが最も下の層です。カーネル監査(auditd)も同じ場所を見ますが、auditdがルールごとに残すかどうかを決める記録装置なら、Falcoは条件を評価して、今アラートを出すかを決める判定装置です。2つは代替ではなく、ペアです。
どう動くのか
ルールの基本要素は、次のように整理されます。必須は、rule(短く一意の名前)・desc(何を捕まえるか)・condition(イベントに適用するフィルター式)・output(一致したときに出す文)・priority(深刻度)の5つで、任意としてenabled・tags・source・exceptionsがあります。priorityに使える値は8つです。EMERGENCY・ALERT・CRITICAL・ERROR・WARNING・NOTICE・INFORMATIONAL・DEBUG。
- list: outbound_clients
items: [curl, wget, nc]
- macro: from_scheduler
condition: (proc.aname[2]=cron or proc.aname[3]=cron)
- rule: Scheduled job made an outbound request
desc: 예약 작업 밑에서 외부 요청 도구가 실행됐다
condition: spawned_process and proc.name in (outbound_clients) and from_scheduler
output: "예약 작업이 바깥으로 (user=%user.name proc=%proc.name cmd=%proc.cmdline)"
priority: WARNING
tags: [labhub, cron]
このコードブロックの韓国語は、descが「スケジュールされたジョブの下で、外部リクエストのツールが実行された」、outputが「スケジュールされたジョブが外部に」という意味です。
listとmacroには、順序の制約があります。どちらも自分より前に定義されたものだけを参照できます。そのため、ファイルの中で、リスト → マクロ → ルールの順に書くのが習慣になります。spawned_processのように名前だけが出てくる条件は、Falcoが一緒に配布しているデフォルトのマクロです。このラボVMで測ってみたところ、falco --validateに自分のルールファイルだけを渡すと、Undefined macro 'spawned_process'で落ち、デフォルトのルールファイルも一緒に渡して初めて通りました。検証も、ロードと同じ条件で行う必要があるという意味です。
条件に使えるフィールド名は、サポートされているフィールドの一覧にあります。プロセス系で特に役に立つのが、祖先の名前です。proc.pnameは親1つだけですが、proc.aname[2]は祖父を指します。cronがシェルを起動し、そのシェルがcurlを呼ぶ構造では、親がshなので、祖先まで見て初めて、「スケジュールされたジョブから出てきた」と言えます。
アラートをどこに送るかは、出力の設定が決めます。既定は標準出力なので、systemdの下ではジャーナルに入りますが、朝に機械で数えるには、JSONに変えてファイルにも落とすほうがよいです。出力の文の中の%proc.cmdlineのようなプレースホルダーは、出力フォーマットのドキュメントにまとめられています。
誤検知を減らす場所が、exceptionsです(例外のドキュメント)。fieldsに見るフィールドを、compsに比較方法を、valuesに値のまとまりを書くと、Falcoがそれをand not (…)として条件のあとに付けます。条件の文字列を直接直すのと結果は似ていますが、2つの点が違います。例外がルールと分離されているので、あとで上書きして値だけを変えられることと、何をなぜ除外したのかが目に見えることです。
現場での姿
最もよくある失敗は、例外を条件に溶かし込んでしまうことです。and not proc.cmdline contains backupを条件の末尾に付けると、6か月後に、その断片が何のためのものだったのか、誰も分かりません。そして、そうして付けた例外は、たいてい広すぎます。backupという単語が入ったすべてのコマンドが、静かになります。
2つ目は、ルールを有効にしているのに、出力を見ないことです。このラボVMで、既定の設定のままにしておくと、アラートがジャーナルにだけ流れ、そのVMが消えると、一緒に消えます。アラートがファイルや収集基盤に行かない検知は、「動いている」という安心感だけを与えます。
3つ目は、優先度のインフレです。すべてのルールをCRITICALで出すと、等級が情報であることをやめます。人を明け方に起こすものと、朝にまとめて見ればよいものを分けることが、ルールを書く仕事の半分です。
次のラボですること
ラボでは、FalcoがインストールされたVMでルールを自分で書きます。リストとマクロを分け、出力をJSONファイルに回し、タイトルのインシデント(スケジュールされたジョブが毎分、外部にリクエストする)を実際に仕込んで、ルールが捕まえることをファイルで確認します。次に、同じルールに引っかかる正常な作業をもう1つ作り、それだけをexceptionsで除外して、悪いものが引き続き捕まるかを、もう一度確認します。システムコールの計装はカーネルの機能なので、このラボもVMで動きます。