概要
本稿では、この QEMU による syzkallerの x86_64カーネルのファジングを実施した。
syzkaller は Google が開発したカーネルのファジングツールである。 ファジングを適切に実施することで、規模が膨大なソフトウェア Linuxカーネルの不具合・バグを検出することができる。 しかし、設定方法を誤って実施してしまうと、不具合・バグを見過ごすことになり、リソースの無駄となってしまう。
そこで、公式ドキュメントやブログなどの情報を基に、syzkaller をインストールし、テストを実施した。本稿では、その時のインストール手順やカスタマイズ方法についてまとめる。
はじめに
近年、ソフトウェアの規模が飛躍的に増大しており、潜在バグが存在する可能性が高いため、ソフトウェアテストの実施が必要不可欠になりつつある。
ソフトウェアが期待するに動作することを確認するためにソフトウェアテストを実施することが多い。
しかし、ソフトウェアの規模が大きくなっている昨今では、すべてのケースを網羅したテストを作成・実施することは現実的ではない。
「ファジング」は近年注目されているソフトウェアテストの手法の一つで、検査対象のソフトウェアに対して問題を引き起こしそうなデータを入力し、その応答や挙動を監視する手法である。 ファジングではツールを用いて自動化することが多く、非常に多くのケースを実行することができる。
Linuxカーネルは、ソースコードの行数だけ見ても 2000万行を超える大きなソフトウェアであり、セキュリティの重要性も高くなっている。 syzkaller は Google が開発したカーネルのファジングツールの一つとなっている。
syzkaller 自体は Go言語で記述されたプログラムであり、Linux を含め様々な OS をサポートしている。
実験環境
本稿では、Ubuntu PC(x86-64)上に QEMU によるファジングするような syzkaller を実施する。
本記事で使用した開発用PC (Host PC)の構成は次の通りとなっている。
環境 | 概要 |
---|---|
CPU | AMD Ryzen 3 3300X |
RAM | DDR4-2666 16GB ×2 |
OS | Ubuntu Desktop 22.04.03 LTS |
Host kernel | 6.2.0-37-generic |
syzkaller | 28b24332d95f2f7df44ec7e7a5e0025bcadc6277 |
Guest kernel | v6.6.5 |
セットアップ
公式ドキュメントにある Setup: Ubuntu host, QEMU vm, x86-64 kernel に沿って実施する。
Linux Kernelのビルド
カーネルのビルドに必要なパッケージをインストールする
leava@ubuntu:/work/$ sudo apt update leava@ubuntu:/work/$ sudo apt install make gcc flex bison libncurses-dev libelf-dev libssl-dev
-
leava@ubuntu:/work/$ export KERNEL=linux-6.6.5 leava@ubuntu:/work/$ wget https://cdn.kernel.org/pub/linux/kernel/v${KERNEL:6:1}.x/${KERNEL}.tar.xz leava@ubuntu:/work/$ tar xf ${KERNEL}.tar.gz leava@ubuntu:/work/$ cd ${KERNEL}
QEMU/KVM の実行用にデフォルト構成からコンフィグを生成する
leava@ubuntu:/work/linux-6.6.5/$ make defconfig leava@ubuntu:/work/linux-6.6.5/$ make kvm_guest.config
syzkaller に必要なコンフィグを追記する
leava@ubuntu:/work/linux-6.6.5/$ echo "CONFIG_KCOV=y\nCONFIG_DEBUG_INFO_DWARF4=y\nCONFIG_KASAN=y\nCONFIG_KASAN_INLINE=y\nCONFIG_CONFIGFS_FS=y\nCONFIG_SECURITYFS=y" >> .config
トラブル回避のために、ネットワークインターフェースの名前が予測可能機能を無効にする
leava@ubuntu:/work/linux-6.6.5/$ echo "CONFIG_CMDLINE_BOOL=y\nCONFIG_CMDLINE=\"net.ifnames=0\"" >> .config
-
leava@ubuntu:/work/linux-6.6.5/$ make -j$(nproc)
成果物を確認する
leava@ubuntu:/work/linux-6.6.5/$ ls vmlinux vmlinux leava@ubuntu:/work/linux-6.6.5/$ ls arch/arm64/boot/Image arch/arm64/boot/Image
rootfsの生成
ベースイメージの生成に必要なパッケージをインストールする
leava@ubuntu:/work/$sudo apt install debootstrap
イメージの生成準備
leava@ubuntu:/work/$ export IMAGE=image leava@ubuntu:/work/$ mkdir ${IMAGE} leava@ubuntu:/work/$ cd ${IMAGE}
Debian Bullseye イメージから rootfs を生成する
leava@ubuntu:/work/image$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh leava@ubuntu:/work/image$ chmod +x create-image.sh leava@ubuntu:/work/image$ ./create-image.sh
成果物を確認する
leava@ubuntu:/work/image$ ls bullseye.img bullseye.img
仮想マシンで起動確認
QEMU/KVM の実行に必要なパッケージをインストールする
leava@ubuntu:/work/$ sudo apt install qemu-system-x86
-
leava@ubuntu:/work/$ qemu-system-x86_64 \ -m 2G \ -smp 2 \ -kernel linux-6.6.5/arch/x86/boot/bzImage \ -append "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0" \ -drive file=image/bullseye.img,format=raw \ -net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \ -net nic,model=e1000 \ -enable-kvm \ -nographic \ -pidfile vm.pid \ 2>&1 | tee vm.log
別の端末から QEMUインスタンスに ssh接続できるかどうかを確認する
leava@ubuntu:/work/$ ssh -i image/bullseye.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
syzkaller のビルド
Goのソースコードを公式サイトからダウンロードする
leava@ubuntu:/work/$ wget https://dl.google.com/go/go1.21.4.linux-amd64.tar.gz leava@ubuntu:/work/$ tar -xf go1.21.4.linux-amd64.tar.gz
ホスト環境にGoの実行環境をセットアップする
leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin leava@ubuntu:/work/$ export PATH=$GOROOT/bin:$PATH
Goの実行環境が正しくインストールされているか確認する
leava@ubuntu:/work/$ go version leava@ubuntu:/work/$ go version go1.21.4 linux/amd64
syzkaller のソースコードを取得する
leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git leava@ubuntu:/work/$ cd syzkaller
syzkaller をビルドする
leava@ubuntu:/work/syzkaller/$ make
syzkaller の設定ファイルの準備
syzkaller の設定ファイル (my.cfg) を用意する。
// 1: { "target": "linux/amd64", "http": "127.0.0.1:56741", "workdir": "workdir", "kernel_obj": "../linux-6.6.5", "image": "../image/bullseye.img", "sshkey": "../image/bullseye.id_rsa", "syzkaller": "../syzkaller", "procs": 8, "type": "qemu", "vm": { "count": 4, "kernel": "../linux-6.6.5/arch/x86/boot/bzImage", "cpu": 2, "mem": 2048 } }
設定できるパラメータは、公式ドキュメント syzkaller/docs/configuration.md から参照することができる。
変数 | 概要 |
---|---|
target |
ファジングする対象のOS |
http |
syzkallerのビューワとして出力する URL |
workdir |
実行に使用する一時ファイル置き場 |
kernel_obj |
カーネルソースツリー |
image |
rootfsディスクイメージ |
sshkey |
rootfsのSSH秘密鍵 |
syzkaller |
syzkallerのトップディレクトリ |
procs |
各VM内の並列プロセス数 |
type |
VMの種類 |
vm |
VMのパラメータ |
syzkallerの実行
用意したコンフィグファイルを入力として、syz-manager を実行することで、ファジングテストが開始する。
leava@ubuntu:/work/syzkaller/$ sudo bin/syz-manager -config=my.cfg
syzkaller の起動に成功すると、ブラウザ (http://127.0.0.1:56700/
)からダッシュボードにアクセスすることができる。
下記は syzkaller 実行から1日経過したときの様子をスクリーンショットしたものである。
syzkallerの トップ画面には "Status" "Crashes", "Logs"の3つのセクションに分かれている。
- Status: ツールの実行環境や実行結果を確認することができる
- Crashes: 実行中にシステムがクラッシュしたときにその詳細や再現方法を確認することができる
- Log: ツール実行中のログを確認することができる
トップページにある "corpus" をクリック/タップすると実施されたコーパス (システムコールの入力データセット) の詳細を確認することができる。
一方で、トップページにある "coverage" をクリック/タップするとコードカバレッジを確認することができる。
また、"Crashes" セクションには、syzkaller 実行中に VM で発生したカーネルクラッシュが記録されている。 記録されたカーネルクラッシュごとにログやレポートを確認することができ、再現プログラムも自動生成されることもある。
カスタマイズ
Syscall descriptionの追加
syzkaller では、システムコール記述言語syzlang を使用して入力データ(Syscall description)を定義する。
ここで、システムコールにどのようなパラメータを与えるかの情報を付加する。 syzkaller はこの情報を基に、意味のあるインプットをプログラムに与えていく。
そこで、既存の構成ファイルを更新し、入力データを追加する手法を確認していく。
今回は、既存の ptrace
に対して固定値 0xdeadbeaf
を入力するような入力データを追加する。
これを実現するために次の3つの作業を実施することになる。
- syscall descriptionの作成
- syscall descriptionを Goコードに変換する (
.const
中間ファイルを介して) - syskaller を再ビルドする
syscall description については、既存 sys/linux/sys.txt
の内容を流用する。
また、期待通りに syzkaller が追加された情報を使ったどうかを判断するために、カーネル側に手を加えておく。
// 1: diff --git a/kernel/ptrace.c b/kernel/ptrace.c index 443057b..66505f7 100644 --- a/kernel/ptrace.c +++ b/kernel/ptrace.c @@ -1281,6 +1281,9 @@ SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, struct task_struct *child; long ret; + if (pid == 0xdeadbeaf) + panic("Invalid process ID\n"); + if (request == PTRACE_TRACEME) { ret = ptrace_traceme(); goto out;
上記のパッチを当てたカーネルをビルドしておくことで、syzkaller が "Invalid Process ID"パニックが観測できるかどうかを確認していく。 まずは、syscall description の作成から手掛けていく。
必要なパッケージをインストールする
leava@ubuntu:/work/$ sudo apt update leava@ubuntu:/work/$ sudo apt install clang-format
最小限の syscall description を生成する。
leava@ubuntu:/work/syzkaller$ echo "ptrace$panic(req int32, pid const[0xdeadbeaf])" > sys/linux/ptrace_panic.txt
syscall description とカーネルソースから amd64向けの中間ファイル
.const
を生成するleava@ubuntu:/work/syzkaller$ make bin/syz-extract leava@ubuntu:/work/syzkaller$ bin/syz-extract -os linux -arch amd64 -sourcedir ../${KERNEL} -builddir ../${KERNEL} ptrace_panic.txt
syzkaller から使えるように Go コードに変換する
leava@ubuntu:/work/syzkaller$ make generate
syzkaller を再ビルドする
leava@ubuntu:/work/syzkaller$ make
これで、syzkaller を実行すると追加された ptrace$panic
が実行されるようになる。
このままでも問題ないが、確認を容易にできるするために ptrace$panic
のみシステムコールを有効にする。
enable_syscalls
, disable_syscalls
オプションを使用することで、実行するシステムコールのセットを有効/無効にすることができる。
leava@ubuntu:/work/syzkaller$ sed "1a \ \"enable_syscalls\": [\"ptrace$panic\"],"
このコンフィグファイルを入力として、syz-manager を実行する。
下記は syzkaller実行から20分経過したときの様子をスクリーンショットしたものである。
syzkaller のトップ画面から kernel panic: Invalid proceed ID
が確認できる。
また、syzkaller による再現プログラムも確認することができる。
Syzkaller hit 'kernel panic: Invalid process ID' bug. audit: type=1400 audit(1702720428.078:6): avc: denied { execmem } for pid=220 comm="syz-executor250" scontext=system_u:system_r:kernel_t:s0 tcontext=system_u:system_r:kernel_t:s0 tclass=process permissive=1 Kernel panic - not syncing: Invalid process ID CPU: 1 PID: 221 Comm: syz-executor250 Not tainted 6.6.5 #7 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014 Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0x50/0x70 lib/dump_stack.c:106 panic+0x53e/0x5c0 kernel/panic.c:340 __do_sys_ptrace kernel/ptrace.c:1285 [inline] __se_sys_ptrace kernel/ptrace.c:1278 [inline] __x64_sys_ptrace+0x24e/0x250 kernel/ptrace.c:1278 do_syscall_x64 arch/x86/entry/common.c:50 [inline] do_syscall_64+0x3f/0x90 arch/x86/entry/common.c:80 entry_SYSCALL_64_after_hwframe+0x6e/0xd8 RIP: 0033:0x7fa24ee5464d Code: c3 e8 a7 1f 00 00 0f 1f 80 00 00 00 00 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b8 ff ff ff f7 d8 64 89 01 48 RSP: 002b:00007fffa2c5da18 EFLAGS: 00000246 ORIG_RAX: 0000000000000065 RAX: ffffffffffffffda RBX: 00007fffa2c5dc78 RCX: 00007fa24ee5464d RDX: 0000000000000000 RSI: 00000000deadbeaf RDI: 0000000000000006 RBP: 0000000000000000 R08: 00007fffa2c5d480 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000001 R13: 431bde82d7b634db R14: 00007fa24eed14f0 R15: 0000000000000001 </TASK> Kernel Offset: 0x15200000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff) Syzkaller reproducer: # {Threaded:false Repeat:true RepeatTimes:0 Procs:1 Slowdown:1 Sandbox: SandboxArg:0 Leak:false NetInjection:false NetDevices:false NetReset:false Cgroups:false BinfmtMisc:false CloseFDs:false KCSAN:false DevlinkPCI:false NicVF:false USB:false VhciInjection:false Wifi:false IEEE802154:false Sysctl:false Swap:false UseTmpDir:false HandleSegv:false Repro:false Trace:false LegacyOptions:{Collide:false Fault:false FaultCall:0 FaultNth:0}} ptrace$panic(0x6, 0xdeadbeaf) C reproducer: // autogenerated by syzkaller (https://github.com/google/syzkaller) #define _GNU_SOURCE #include <dirent.h> #include <endian.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/prctl.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> static void sleep_ms(uint64_t ms) { usleep(ms * 1000); } static uint64_t current_time_ms(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts)) exit(1); return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; } static bool write_file(const char* file, const char* what, ...) { char buf[1024]; va_list args; va_start(args, what); vsnprintf(buf, sizeof(buf), what, args); va_end(args); buf[sizeof(buf) - 1] = 0; int len = strlen(buf); int fd = open(file, O_WRONLY | O_CLOEXEC); if (fd == -1) return false; if (write(fd, buf, len) != len) { int err = errno; close(fd); errno = err; return false; } close(fd); return true; } static void kill_and_wait(int pid, int* status) { kill(-pid, SIGKILL); kill(pid, SIGKILL); for (int i = 0; i < 100; i++) { if (waitpid(-1, status, WNOHANG | __WALL) == pid) return; usleep(1000); } DIR* dir = opendir("/sys/fs/fuse/connections"); if (dir) { for (;;) { struct dirent* ent = readdir(dir); if (!ent) break; if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue; char abort[300]; snprintf(abort, sizeof(abort), "/sys/fs/fuse/connections/%s/abort", ent->d_name); int fd = open(abort, O_WRONLY); if (fd == -1) { continue; } if (write(fd, abort, 1) < 0) { } close(fd); } closedir(dir); } else { } while (waitpid(-1, status, __WALL) != pid) { } } static void setup_test() { prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); setpgrp(); write_file("/proc/self/oom_score_adj", "1000"); } static void execute_one(void); #define WAIT_FLAGS __WALL static void loop(void) { int iter = 0; for (;; iter++) { int pid = fork(); if (pid < 0) exit(1); if (pid == 0) { setup_test(); execute_one(); exit(0); } int status = 0; uint64_t start = current_time_ms(); for (;;) { if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid) break; sleep_ms(1); if (current_time_ms() - start < 5000) continue; kill_and_wait(pid, &status); break; } } } void execute_one(void) { syscall(__NR_ptrace, /*req=*/6, /*pid=*/0xdeadbeaful, 0, 0); } int main(void) { syscall(__NR_mmap, /*addr=*/0x1ffff000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul); syscall(__NR_mmap, /*addr=*/0x20000000ul, /*len=*/0x1000000ul, /*prot=*/7ul, /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul); syscall(__NR_mmap, /*addr=*/0x21000000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=*/0x32ul, /*fd=*/-1, /*offset=*/0ul); loop(); return 0; }
Pseudo Syscallの追加
syzkaller は(基本的に)システムコールに対するファジングをすることで、バグのレポートや網羅率の集計なども取得することできる。 ただし、システムコール単位ではファイルシステムのようなサブシステムには向かないケースもある。
syzkaller では、通常のシステムコールのほかに、疑似システムコール(Pseudo syscall)と呼ばれる独自のアクションを追加することができる。
例えば、疑似システムコールの一つsyz_image_mount
は指定したファイルシステムをループバックマウントし、カレントディレクトリを変更する。
そこで、Pseudo Syscall syz_mycall
を追加する手法を確認していく。ただし、意味もなく Pseudo Syscall を追加することは推奨されていない。
Use of pseudo-syscalls is generally discouraged because they ruin all advantages of the declarative descriptions (declarativeness, conciseness, fuzzer control over all aspects, possibility of global improvements to the logic, static checking, fewer bugs, etc), increase maintenance burden, are non-reusable and make C reproducers longer.
syzkallerからPseudo Syscallを実行できるようにするためには
- executor に Pseudo Syscall を追加する
- 該当の Pseudo Syscall が該当OSでサポートされていることを明記する
- Syscall description の追加
- テストの追加 [任意]
テストの追加については、今回の確認作業では省略する。
Pseudo Syscallを追加
leava@ubuntu:/work/syzkaller$ cat << EOF >> executor/common_linux.h #if SYZ_EXECUTOR static long syz_mycall(volatile long x, volatile long y) { return x + y; } #endif EOF
Linux で syz_mycall をサポートしていることを明記する
leava@ubuntu:/work/syzkaller$ sed -i '/syz_socket_connect_nvme_tcp/a\ \"syz_mycall\": isSyzmycallSupported,' pkg/host/syscalls_linux.go leava@ubuntu:/work/syzkaller$ cat << EOF >> pkg/host/syscalls_linux.go func isSyzmycallSupported(c *prog.Syscall, target *prog.Target, sandbox string) (bool, string) { return true, "" } EOF
Syscall description の追加
leava@ubuntu:/work/syzkaller$ echo "syz_mycall(x int32[0:10], y int32[0:10])" > sys/linux/mysyscall.txt
syzkaller を再ビルド (pseudo syscall 含む) する
leava@ubuntu:/work/syzkaller$ make bin/syz-extract leava@ubuntu:/work/syzkaller$ bin/syz-extract -os linux -arch amd64 -sourcedir ../${KERNEL} -builddir ../${KERNEL} mysyscall.txt leava@ubuntu:/work/syzkaller$ make generate leava@ubuntu:/work/syzkaller$ make
おわりに
本記事では、ファジングツールの一つ syzkaller で、ファジングテストの結果におけるカバレッジの取得方法について確認した。
変更履歴
- 2023/12/18: 記事公開
参考文献
- ファジング実践資料
- syzkallerを実際に使っている記事
- syzkallerを調査している記事
- syzkaller の概要紹介
- syzkaller の実施手順
- syzkaller の既存テストの書き換え
- syzkaller のドライバのファジング
- syzkallerの使い方と実践例