LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

syzkaller によるファジングテストの実施とテストのカスタマイズ

概要

本稿では、この QEMU による syzkallerの x86_64カーネルのファジングを実施した。

syzkaller は Google が開発したカーネルのファジングツールである。 ファジングを適切に実施することで、規模が膨大なソフトウェア Linuxカーネルの不具合・バグを検出することができる。 しかし、設定方法を誤って実施してしまうと、不具合・バグを見過ごすことになり、リソースの無駄となってしまう。

そこで、公式ドキュメントやブログなどの情報を基に、syzkaller をインストールし、テストを実施した。本稿では、その時のインストール手順やカスタマイズ方法についてまとめる。

はじめに

近年、ソフトウェアの規模が飛躍的に増大しており、潜在バグが存在する可能性が高いため、ソフトウェアテストの実施が必要不可欠になりつつある。 ソフトウェアが期待するに動作することを確認するためにソフトウェアテストを実施することが多い。
しかし、ソフトウェアの規模が大きくなっている昨今では、すべてのケースを網羅したテストを作成・実施することは現実的ではない。

「ファジング」は近年注目されているソフトウェアテストの手法の一つで、検査対象のソフトウェアに対して問題を引き起こしそうなデータを入力し、その応答や挙動を監視する手法である。 ファジングではツールを用いて自動化することが多く、非常に多くのケースを実行することができる。

Linuxカーネルは、ソースコードの行数だけ見ても 2000万行を超える大きなソフトウェアであり、セキュリティの重要性も高くなっている。 syzkaller は Google が開発したカーネルのファジングツールの一つとなっている。

github.com

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のビルド

  1. カーネルのビルドに必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt update
     leava@ubuntu:/work/$ sudo apt install make gcc flex bison libncurses-dev libelf-dev libssl-dev
    
  2. Linuxソースコードを取得する

     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}
    
  3. QEMU/KVM の実行用にデフォルト構成からコンフィグを生成する

     leava@ubuntu:/work/linux-6.6.5/$ make defconfig
     leava@ubuntu:/work/linux-6.6.5/$ make kvm_guest.config
    
  4. 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
    
  5. トラブル回避のために、ネットワークインターフェースの名前が予測可能機能を無効にする

     leava@ubuntu:/work/linux-6.6.5/$ echo "CONFIG_CMDLINE_BOOL=y\nCONFIG_CMDLINE=\"net.ifnames=0\"" >> .config
    
  6. Linuxカーネルをビルドする

     leava@ubuntu:/work/linux-6.6.5/$ make -j$(nproc)
    
  7. 成果物を確認する

     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の生成

  1. ベースイメージの生成に必要なパッケージをインストールする

     leava@ubuntu:/work/$sudo apt install debootstrap
    
  2. イメージの生成準備

     leava@ubuntu:/work/$ export IMAGE=image
     leava@ubuntu:/work/$ mkdir ${IMAGE}
     leava@ubuntu:/work/$ cd ${IMAGE}
    
  3. 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
    
  4. 成果物を確認する

     leava@ubuntu:/work/image$ ls bullseye.img
     bullseye.img
    

仮想マシンで起動確認

  1. QEMU/KVM の実行に必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt install qemu-system-x86
    
  2. 生成したカーネルQEMU/KVM を起動する

     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
    
  3. 別の端末から QEMUインスタンスssh接続できるかどうかを確認する

     leava@ubuntu:/work/$ ssh -i image/bullseye.id_rsa -p 10021 -o  "StrictHostKeyChecking no" root@localhost
    

syzkaller のビルド

  1. 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
    
  2. ホスト環境にGoの実行環境をセットアップする

     leava@ubuntu:/work/$ export PATH=$PATH:/usr/local/go/bin
     leava@ubuntu:/work/$ export PATH=$GOROOT/bin:$PATH
    
  3. Goの実行環境が正しくインストールされているか確認する

     leava@ubuntu:/work/$ go version
     leava@ubuntu:/work/$ go version go1.21.4 linux/amd64
    
  4. syzkaller のソースコードを取得する

     leava@ubuntu:/work/$ git clone https://github.com/google/syzkaller.git
     leava@ubuntu:/work/$ cd syzkaller
    
  5. 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日経過したときの様子をスクリーンショットしたものである。

main ダッシュボード

syzkallerの トップ画面には "Status" "Crashes", "Logs"の3つのセクションに分かれている。

  • Status: ツールの実行環境や実行結果を確認することができる
  • Crashes: 実行中にシステムがクラッシュしたときにその詳細や再現方法を確認することができる
  • Log: ツール実行中のログを確認することができる

トップページにある "corpus" をクリック/タップすると実施されたコーパス (システムコールの入力データセット) の詳細を確認することができる。

corpus ダッシュボード

一方で、トップページにある "coverage" をクリック/タップするとコードカバレッジを確認することができる。

coverage ダッシュボード

また、"Crashes" セクションには、syzkaller 実行中に VM で発生したカーネルクラッシュが記録されている。 記録されたカーネルクラッシュごとにログやレポートを確認することができ、再現プログラムも自動生成されることもある。

Crashes ダッシュボード

カスタマイズ

Syscall descriptionの追加

syzkaller では、システムコール記述言語syzlang を使用して入力データ(Syscall description)を定義する。

github.com

ここで、システムコールにどのようなパラメータを与えるかの情報を付加する。 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 の作成から手掛けていく。

  1. 必要なパッケージをインストールする

     leava@ubuntu:/work/$ sudo apt update
     leava@ubuntu:/work/$ sudo apt install clang-format
    
  2. 最小限の syscall description を生成する。

     leava@ubuntu:/work/syzkaller$ echo "ptrace$panic(req int32, pid const[0xdeadbeaf])" > sys/linux/ptrace_panic.txt
    
  3. 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
    
  4. syzkaller から使えるように Go コードに変換する

     leava@ubuntu:/work/syzkaller$ make generate
    
  5. 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分経過したときの様子をスクリーンショットしたものである。

ptrace$panic によるsyzkallerトップ画面

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.

github.com

syzkallerからPseudo Syscallを実行できるようにするためには

  • executor に Pseudo Syscall を追加する
  • 該当の Pseudo Syscall が該当OSでサポートされていることを明記する
  • Syscall description の追加
  • テストの追加 [任意]

テストの追加については、今回の確認作業では省略する。

  1. 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
    
  2. 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
    
  3. Syscall description の追加

     leava@ubuntu:/work/syzkaller$ echo "syz_mycall(x int32[0:10], y int32[0:10])" > sys/linux/mysyscall.txt
    
  4. 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: 記事公開

参考文献