この記事は Linux Advent Calendar 2021 の25日目の記事です。
はじめに
KRSI は、 LSM hook と eBPF を用いて、ユーザが自分自身の MAC ポリシーを定義することができるようになる仕組みである。具体的には、 LSM フック可能な場所に BPF プログラムをアタッチして、通信をブロックしたりすることが可能である。
さらに、この KRSI の開発過程で、セキュリティな人達以外に対しても KRSI の仕組みを汎用的に提供するために、 fmod_ret が導入された。
fmod_ret のフック対象は  LSM フック箇所に限らず、エラーインジェクションが可能な関数にも広がっている。
今回は libbpf を用いて、その自由度の高さを体験していこうと思う。
なお、あまりにも神にも悪魔にもなれる機能なので、本投稿の疑似コードはそのままでは使えないよう抜粋した形で掲載する。
LSM フック
わかりやすさのため、ありがちな例として指定プログラムのシグナル送信を制限してみる。具体的には、引数で指定されたプログラムが SIGKILL を発行していた場合に EPERM を返すサンプルを実装する。
ちなみに、CONFIG_LSM に bpf が入っていないと思うので、その場合は GRUB の設定ファイルを調整してほしい。
BPF 側の疑似コードとしては下記のようなイメージになる。
SEC("lsm/task_kill")
int BPF_PROG(masked_signal, struct task_struct *p, struct kernel_siginfo *info,
                int sig, const struct cred *cred)
{
        u64 pid_tgid = bpf_get_current_pid_tgid();
        u32 tgid = pid_tgid >> 32;
        struct event *e;
        if (!is_targetted_comm()) {
                return 0; // Not targeted program
        } else {
                if (info->si_signo == SIGKILL) {
                        e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
                        if (e) {
                                e->pid = tgid;
                                e->killed_pid = p->pid;
                                bpf_ringbuf_submit(e, 0);
                        }
                }
                return -EPERM;
        }
        return 0;
}
それでは、シグナル制限のサンプルを起動して、別のプロンプトから sleep しているプロセスを kill してみよう。
$ ps aux | grep sleep | grep -v grep |  awk '{ print "kill -9", $2 }' | bash
bash: 1 行: kill: (4405) - 許可されていない操作です
実際に sleep プロセスを kill 出来なかったことがわかる。
上記の疑似コードを呼び出す側で、リングバッファの内容を表示するように実装しているので確認してみると、下記のように SIGKILL が送られた記録が残っていた。
Started
SIGKILL: sent from 4911 to 4405
上記の結果から、 KRSI の導入によって eBPF を活用してプログラマブルにセキュリティ機能を実装できることがわかった。
fmod_ret
前述した通り、 fmod_ret のフック先は LSM フックに限定されてない。
そこで今回は write システムコールをフックして、あたかも書き込みに成功したかのように振る舞うサンプルを書いてみよう。
なお、fmod_ret に関しては CONFIG_LSM の設定は不要である。
BPF の疑似コードとしては下記のようなイメージになる。
int BPF_PROG(fake_write, struct pt_regs *regs)
{
        struct event *e;
        if (!is_targetted_comm()) {
                return 0;
        } else {
                int fd = PT_REGS_PARM1(regs);
                size_t count = PT_REGS_PARM3(regs);
                if (fd > 2) {
                        e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
                        if (e) {
                                e->fd = fd;
                                e->count = count;
                                bpf_ringbuf_submit(e, 0);
                        }
                }
                return count;
        }
}
ユーザ空間から指定された読み取りサイズをそのまま戻り値として返すが、実際にはデータをファイルに書き込まないようにしている。
すなわち、本実装を応用すれば、アプリケーションには気づかれないまま任意の BPF コードを実行した後にアプリケーション側の処理を継続させることも出来てしまう。
次に、単純にファイルに書き込みをするサンプルコードを下記のように準備する。
# include <stdio.h>
# include <stdlib.h>
int main() {
        FILE *fp;
        fp = fopen("aiueo.log", "w");
        if (fp == NULL)
                return EXIT_FAILURE;
        fprintf(fp, "%s\n", "aiueo");
        fclose(fp);
        return EXIT_SUCCESS;
}
上記のサンプル(test_write)を実行すると、通常は aiueo.log というファイルに aiueo が書き込まれる。
一方、 BPF のサンプルを起動後に、test_write を実行すると空の aiueo.log が作成されていることがわかる。
$ ./test_write
$ cat aiueo.log 
今回もリングバッファの内容を表示するように実装しているので確認してみると、戻り値としての 6 byte が記録されていることがわかる。
Started
fd: 3, unwritten_count: 6
つまり、 test_write は 6 byte のデータが書き込まれたと信じたまま処理を正常終了させたことになる。
おわりに
KRSI によって、容易にプログラマブルで軽量なセキュリティ基盤を構築する仕組みが提供された。
また、 fmod_ret はセキュリティだけでなくトレーシング等にも活用できそうだが、自由すぎることによる危険性の高さも合わせて実感することが出来た。