この記事は、@sxarp さんの名記事 システムコールとは何なのか を入口に、「システムコールとは何か」を押さえたうえで、ランタイムセキュリティツール Falco の真髄が "なぜそこ(syscall)にあるのか" を説明する読み物です。
前半でシステムコールを手を動かして確認し、後半で「だから Falco は速くて正確で、しかも止められる」という話につなげます。
※筆者は Sysdig 所属ですが、本記事は個人の理解に基づく整理であり、所属組織の公式見解ではありません。
Part 1. System Call とは何か
1-1. ひとことで言うと
システムコールは 「アプリケーションが OS(カーネル)の機能を呼び出すための窓口」 です。
ファイルを開く、ネットワークに繋ぐ、プロセスを生成する——これらはアプリが勝手にハードウェアを触るのではなく、必ずカーネルに「お願い」して代行してもらいます。その「お願いの作法」がシステムコールです。
逆に言えば、システムコールを使わずにアプリができることはほとんどありません。画面に文字を出すのも(write)、ファイルを読むのも(openat / read)、通信するのも(socket / connect)、すべて syscall を経由します。
1-2. なぜ「窓口」が必要なのか — 特権の境界
CPU には**特権レベル(リング)**があります。ざっくり言うと、
- ユーザ空間(ring 3) … 一般のアプリが動く。ハードウェアを直接触れない
- カーネル空間(ring 0) … OS が動く。ハードウェア・メモリ・他プロセスを管理できる
アプリが直接ハードウェアを触れてしまうと、安定性もセキュリティも崩壊します。そこで「ユーザ空間とカーネル空間の間にゲートを1つだけ設け、そこを通る時だけ特権の世界に入れる」という設計になっています。
このゲートこそが本記事の主役です。「アプリが世界に対して何かする時は、必ずこの1点を通る」——これがあとで Falco の話に効いてきます。
1-3. 呼び出しの仕組み(x86-64)
ユーザ空間からカーネルへ入る方法は、x86-64 では syscall 命令です。お作法はシンプルで、
-
raxにシステムコール番号を入れる(例:writeは1) -
引数をレジスタに入れる(
rdi,rsi,rdx, ...) -
syscall命令を発行する
libc も標準ライブラリも使わず、write と exit の2つの syscall だけで「hi と出力して終了する」最小プログラム(NASM 構文, hi.s)を書いてみます。
section .data
msg db "hi", 10 ; 出力する文字列 "hi\n"
len equ $ - msg ; その長さ(アセンブラが計算)
section .text
global _start
_start:
; --- write(1, msg, len) ---
mov rax, 1 ; syscall 番号: write
mov rdi, 1 ; 第1引数: fd = 1(標準出力)
mov rsi, msg ; 第2引数: バッファ先頭アドレス
mov rdx, len ; 第3引数: 長さ
syscall ; ← ここでユーザ空間からカーネルへ「ゲート」を通る
; --- exit(0) ---
mov rax, 60 ; syscall 番号: exit
xor rdi, rdi ; 第1引数: 終了コード 0
syscall
ビルドして実行し、strace で「本当にその syscall だけを発行しているか」を確認します。
$ nasm -f elf64 hi.s -o hi.o
$ ld hi.o -o hi
$ ./hi
hi
$ strace ./hi
execve("./hi", ["./hi"], 0x... /* 0 vars */) = 0
write(1, "hi\n", 3) = 3
exit(0) = ?
+++ exited with 0 +++
libc を一切使っていないので、出てくる syscall は execve(起動)→ write(出力)→ exit(終了)だけ。普段は printf() のようにlibc がこのレジスタ設定と syscall 命令を肩代わりしてくれているだけで、最終的に通る場所は同じです。
呼び出しの「往復」を図にすると、ユーザ空間とカーネル空間を syscall 命令が一度だけまたぐのが分かります。
補足:
syscall命令(および古いint 0x80)は、ユーザ空間からは触れないカーネルのエントリへ CPU レベルで制御を移す「トラップ=同期的な割り込み」です。これにより特権が ring 3 から ring 0 に切り替わり、戻るときに ring 3 へ復帰します。「ゲートを通る」の正体はこの特権遷移です。
カーネル側では
SYSCALL_DEFINE3(write, ...)のようなマクロでエントリが定義され、番号→ハンドラのテーブル経由で実体に届きます。詳しいカーネル内部の追い方は冒頭の参考記事が秀逸なので、ぜひそちらを。
1-4. 手を動かして「見る」 — strace
理屈だけだとピンと来ないので、実際に syscall を覗いてみましょう。strace は、あるプロセスが発行する syscall を端から表示してくれるツールです。
$ strace -f -e trace=openat,read,write,connect -- curl -s https://example.com
...
openat(AT_FDCWD, "/etc/ssl/certs/ca-certificates.crt", O_RDONLY) = 5
connect(6, {sa_family=AF_INET, sin_port=htons(443), ...}) = ...
write(6, "\x16\x03\x01...", 517) = 517 # TLS ClientHello
read(6, "\x16\x03\x03...", 16384) = ...
write(1, "<!doctype html>...", 1256) = 1256 # 画面出力
...
curl というアプリの「やったこと」が、ファイルを開いた / どこに繋いだ / 何を送受信した / 何を出力したという syscall の列として丸見えになります。これがポイントです。アプリの実装言語や中身が何であれ、外界とのやり取りは syscall として観測できるのです。
ここまでが Part 1。「syscall = ユーザ空間とカーネルの唯一のゲートであり、アプリの振る舞いがすべて通る場所」という結論だけ持って、後半へ進みます。
Part 2. Falco の真髄
2-1. Falco とは
Falco は CNCF 卒業プロジェクトのランタイムセキュリティツールです。ひとことで言うと、
システムコールのストリームをリアルタイムに観測し、ルールに照らして「不審な振る舞い」を検知する
エンジンです。コンテナ・Kubernetes・ホストの上で、いま実際に起きている挙動を見張ります。
2-2. 真髄:監視を「syscall の境界」に置いたこと
Part 1 の結論を思い出してください。アプリが世界に対して何かする時は、必ず syscall というゲートを通ります。
Falco の本質は、まさにこのゲートに観測点を置いたことにあります。
なぜこれが強いのか。「OS の抽象化・特権の境界」=「セキュリティ上もっとも意味のある境界」だからです。攻撃者がどんな手口を使おうと、その「行動」は最終的に必ず特定の syscall に落ちます。
どんな入口(脆弱性・盗んだ鍵・サプライチェーン汚染)から入っても、外界に作用した瞬間に syscall として現れる。その1点を押さえているので、攻撃の「動き」を逃さない。これが Falco の真髄です。
2-3. だから「速くて、正確で、評価しやすい」
syscall を直接取ることが、そのまま3つの実利になります。
① 速い(リアルタイム)
カーネルで発生した syscall イベントをその場でストリームとして受け取り、即座にルール評価します。ログをファイルに書いて集約して…という遅延がありません。挙動が起きた瞬間に検知できます。
② 正確(グラウンドトゥルース)
見ているのは**「実際にカーネルに対して発行された syscall」という事実そのもの**です。アプリのログのように「出力されなければ分からない」ことがなく、難読化されたバイナリでも、知らない言語で書かれていても、外界に作用した瞬間に観測されます。回避が難しいのはこのためです。
③ 文脈で評価できる
Falco は生の syscall にコンテナ / Kubernetes / プロセスのメタデータを付与します。「どのコンテナの、どのイメージ由来の、どのプロセスが、誰として、何をしたか」まで揃うので、ルールが意味のある単位で書けます。
2-4. ルールの形 — 「振る舞い」を記述する
Falco のルールは YAML で、syscall イベントのフィールドに対する条件として書きます。例えば「コンテナ内でシェルが起動された」を捉えるイメージはこうです。
- rule: Terminal shell in container
desc: コンテナ内で対話シェルが起動された
condition: >
spawned_process
and container
and shell_procs # execve で bash/sh などが起動
and proc.tty != 0 # 対話端末あり
output: >
コンテナ内でシェル起動 (user=%user.name container=%container.name
proc=%proc.cmdline image=%container.image.repository)
priority: NOTICE
tags: [container, shell, mitre_execution]
注目してほしいのは、spawned_process(=execve 系 syscall)や proc.tty、container.name といった条件の素材がすべて syscall イベント由来だという点です。「ログにこう出たら」ではなく「OS に対してこう作用したら」で書ける——これがランタイム検知の表現力になります。
2-5. 検知だけでなく「止められる」
syscall 単位で「どのプロセスが何をしたか」まで分かっているということは、対象を特定して対応できるということです。Falco の検知をトリガに、Falco Talon のようなレスポンスエンジンや、Sysdig などの製品のレスポンスアクションで、
- 不審なプロセスの kill
- コンテナの隔離・停止(pause / kill)
- ネットワークの遮断
といった初動を自動で打てます。「見えるだけ」で終わらず、攻撃の連鎖をその場で断ち切れる。検知点を syscall に置いたからこそ、対応もプロセス単位で正確に効きます。
2-6. 補足:どうやって syscall を捕まえているのか
Falco がカーネルで syscall を捕捉する方式は歴史的に変遷しています。
- カーネルモジュール … 初期からある方式
- eBPF プローブ … 現在の主流。カーネルを安全に拡張する仕組みで、モジュールを差し込まずに syscall フックを実現
- modern eBPF(CO-RE) … カーネルバージョン差を吸収し、ビルドレス・ポータブルに動く新しい既定
いずれの方式でも、ユーザ空間の Falco エンジン(libsinsp / libscap)に正規化された syscall イベント列が流れ込む点は共通です。「どこから取るか」は実装の進化、「syscall を見る」という思想は不変、というわけです。
まとめ
- システムコールは、ユーザ空間とカーネル空間をつなぐ唯一のゲート。アプリが世界に作用する時は必ずここを通る。
-
Falco の真髄は、監視点をまさにこの syscall の境界に置いたこと。だから——
- 速い:カーネルイベントを即時評価(ログ集約の遅延なし)
- 正確:実際に発行された syscall という事実を見る(難読化・言語非依存・回避困難)
- 意味で書ける:コンテナ / K8s 文脈を付与して「振る舞い」でルール化
- 止められる:プロセス単位で特定でき、kill・隔離まで自動化
- 「ログを後で読む」のではなく「OS の境界で、起きた瞬間に見て、止める」。これが syscall を起点にしたランタイムセキュリティの強さです。
システムコールを理解すると、Falco が「なぜそこを見るのか」が腹落ちします。逆に Falco を知ると、syscall という境界がいかにセキュリティ上おいしい場所かが見えてきます。両方そろって初めて、ランタイム検知の"真髄"が腑に落ちるはずです。
参考リンク
- システムコールとは何なのか #Linux(@sxarp) — 本記事 Part 1 の土台。カーネル内部まで追う秀逸な解説
- Falco 公式サイト
- Falco Documentation
- The Falco Project(CNCF)