😺

syzkallerでLinux KernelをFuzzingしてみる

2024/03/31に公開

syzkallerと呼ばれる、Linux Kernelを中心としたOSをファジングするツールを使ってバグの検出を試してみました。

syzkallerは、KCOV(カバレッジ収集)、KASAN(メモリ破壊検出)といった機能を使って不正なシステムコールに対する入力を発見することができます。公式リポジトリのページにも、syzkallerを使って発見されたバグの一覧が紹介されています。

この記事ではドキュメントに従ってsyzkallerの環境構築をした後、故意にソースコードに仕込んだバグを検出させるデモをします。

1. 準備

公式ドキュメントsyzkaller/docs/linux/setup_ubuntu-host_qemu-vm_x86-64-kernel.md at master · google/syzkallerに従って進めます。

Goを導入します。手順はGoの公式ドキュメントに従います。

https://go.dev/doc/install

https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -xf go1.22.1.linux-amd64.tar.gz

Goのパスを環境変数に追加します。

~/.zshrc
export GOROOT=$HOME/go
export PATH=$GOROOT/bin:$PATH

syzkallerをcloneします。

git clone https://github.com/google/syzkaller
cd syzkaller
make

Linux KernelのソースコードはThe Linux Kernel Archivesから入手できます。gitは git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git から入手できます。

wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.83.tar.xz
tar -xvf linux-6.1.83.tar.xz
mv linux-6.1.83 linux
cd linux
make defconfig
make kvm_guest.config

Linux Kernelのconfigにファジングに必要な機能を有効にします。

.config
# Coverage collection.
CONFIG_KCOV=y

# Debug info for symbolization.
CONFIG_DEBUG_INFO_DWARF4=y

# Memory bug detector
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y

# Required for Debian Stretch and later
CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y

カーネルビルドします。ミルクティーでも飲んで待ちましょう。

make olddefconfig
make -j`nproc`

syzkallerが配布しているシェルスクリプトを実行してQEMU用のrootfsを作ります。QEMUで仮想マシンを立ち上げられたらOKです。

sudo apt install debootstrap
export IMAGE="img"
export KERNEL="linux"
mkdir $IMAGE
cd $IMAGE/
wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
chmod +x create-image.sh
./create-image.sh
sudo apt install qemu-system-x86

qemu-system-x86_64 \
	-m 2G \
	-smp 2 \
	-kernel $KERNEL/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

マシンに入ったらsshdデーモンが立ち上がっていることを確認します。でないとファジングできません >.<

root@syzkaller:~# systemctl status sshd
● ssh.service - OpenBSD Secure Shell server
     Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: e>
     Active: active (running) since Thu 2024-03-28 11:54:57 UTC; 1min 21s ago
       Docs: man:sshd(8)
             man:sshd_config(5)
    Process: 201 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
   Main PID: 210 (sshd)
      Tasks: 1 (limit: 994)
        CPU: 44ms
     CGroup: /system.slice/ssh.service
             └─210 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups

Mar 28 11:54:57 syzkaller systemd[1]: Starting OpenBSD Secure Shell server...
Mar 28 11:54:57 syzkaller sshd[210]: Server listening on 0.0.0.0 port 22.
Mar 28 11:54:57 syzkaller sshd[210]: Server listening on :: port 22.
Mar 28 11:54:57 syzkaller systemd[1]: Started OpenBSD Secure Shell server.

念の為、別のターミナルからsshで仮想マシンのシェルに入れることを確認します。

ssh -i $IMAGE/bullseye.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost

ファジングに関する設定を syz.conf に記述します。 workdir, kernel_obj, image, sshkey, syzkaller は手元の環境に合わせて調整してください。

syz.conf
{
  "target": "linux/amd64",
  "http": "127.0.0.1:56741",
  "workdir": "./workdir",
  "kernel_obj": "linux",
  "image": "img/bullseye.img",
  "sshkey": "img/bullseye.id_rsa",
  "syzkaller": "./syzkaller",
  "procs": 8,
  "type": "qemu",
  "vm": {
    "count": 4,
    "kernel": "linux/arch/x86/boot/bzImage",
    "cmdline": "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0"
    "cpu": 2,
    "mem": 2048
  }
}

動かしてみます! 仮想マシンが何度も立ち上がってログが出てきます。 http://localhost:56741/ にアクセスするとブラウザからログが見ることができます。

./syzkaller/bin/syz-manager -config=syz.conf 

Could not access KVM kernel module: Permission denied で怒られるときは、これを実行します。参考

sudo chmod 666 /dev/kvm

2. バグを仕込んで検出させてみる

どこから手を付けたら良いのかわからなかったので、Linux Kernelをいじっている方のブログの再現をしてみます。

https://kernhack.hatenablog.com/entry/2017/06/18/192850

ここでは、 unshare と呼ばれるシステムコールに焦点を当てます。unshare(2)は名前空間を分離するための機能です。この機能が実装されている関数内に特定の条件の場合にのみKernel Panicを起こすコードを仕込みました。

--- kernel/fork.original.c      2024-03-31 13:20:28.236480395 +0900
+++ kernel/fork.c       2024-03-31 13:15:42.929204728 +0900
@@ -3159,6 +3159,9 @@
        int do_sysvsem = 0;
        int err;
 
+       if (unshare_flags & CLONE_NEWUTS && unshare_flags & CLONE_NEWPID)
+            panic("syscall fuzzer found it!");
+
        /*
         * If unsharing a user namespace must also unshare the thread group
         * and unshare the filesystem root and working directories.

ここでは、ファジングの対象をunshareのみに絞る設定をconfigに追記します。

--- syz.original.conf   2024-03-31 15:32:46.224491638 +0900
+++ syz.conf    2024-03-31 13:26:04.355573091 +0900
@@ -6,6 +6,7 @@
        "image": "img/bullseye.img",
        "sshkey": "img/bullseye.id_rsa",
        "syzkaller": "./syzkaller",
+  "enable_syscalls": ["unshare"],
        "procs": 8,
        "type": "qemu",
        "vm": {

実行すると、CrashesブロックにKernel Panicのログが出力されています。

また、has C reproの中には再現するコードが生成されていました。読むとフラグのCLONE_NEWUTSCLONE_NEWPIDを有効にしてunshareを呼び出していることがわかります。

// autogenerated by syzkaller (https://github.com/google/syzkaller)

#define _GNU_SOURCE 

#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    syscall(__NR_mmap, /*addr=*/0x1ffff000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
    syscall(__NR_mmap, /*addr=*/0x20000000ul, /*len=*/0x1000000ul, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
    syscall(__NR_mmap, /*addr=*/0x21000000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE*/0x32ul, /*fd=*/-1, /*offset=*/0ul);
    syscall(__NR_unshare, /*flags=CLONE_NEWUTS|CLONE_NEWUSER|CLONE_NEWPID*/0x34000000ul);
    return 0;
}

このバグを再現するために、新たにQEMUコマンドから仮想マシンを立ち上げ、マシンの中で再現コードを実行してみます。

vi crash.c
gcc crash.c
./a.out

すると、shellが動かなくなりKernel Panicのログが流れてきました。

[   42.603872] audit: type=1400 audit(1711863713.393:7): avc:  denied  { execmem } for  pid=238 1
[   42.604109] Kernel panic - not syncing: syscall fuzzer found it!
[   42.604114] CPU: 1 PID: 238 Comm: a.out Not tainted 6.1.83 #2
[   42.604120] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[   42.604137] Call Trace:
[   42.604139]  <TASK>
[   42.604141]  dump_stack_lvl+0x4c/0x64
[   42.604154]  panic+0x224/0x50b
[   42.604163]  ? panic_print_sys_info.part.0+0x75/0x75
[   42.604172]  ? __switch_to+0x5cc/0xe20
[   42.604181]  ? __schedule+0x8dd/0x1a20
[   42.604187]  ? ksys_unshare.cold+0x5/0x16
[   42.604195]  ksys_unshare.cold+0x16/0x16
[   42.604208]  ? kernel_fpu_begin_mask+0x240/0x240
[   42.604216]  ? unshare_fd+0x1a0/0x1a0
[   42.604225]  ? switch_fpu_return+0xfe/0x230
[   42.604231]  __x64_sys_unshare+0x2d/0x40
[   42.604239]  do_syscall_64+0x3b/0x90
[   42.604248]  entry_SYSCALL_64_after_hwframe+0x64/0xce
[   42.604254] RIP: 0033:0x7f50e1833f69
[   42.604258] Code: 00 c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 8
[   42.604264] RSP: 002b:00007ffce3715408 EFLAGS: 00000202 ORIG_RAX: 0000000000000110
[   42.604272] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00007f50e1833f69
[   42.604276] RDX: 00007f50e1833f69 RSI: 0000000000000000 RDI: 0000000034000000
[   42.604279] RBP: 00007ffce3715410 R08: 0000000000000000 R09: 00005608eb8e81f0
[   42.604283] R10: 00000000ffffffff R11: 0000000000000202 R12: 00005608eb8e8050
[   42.604286] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[   42.604291]  </TASK>
[   42.605179] Kernel Offset: 0x7e00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbf)
[   42.611937] ---[ end Kernel panic - not syncing: syscall fuzzer found it! ]---

余談ですが、フラグから CLONE_NEWUSER を除いてもKernel Panicするかを検証してみます。フラグの値はlinux/sched.hに記述されています。

linux/sched.h
#define CLONE_NEWIPC		0x08000000	/* New ipc namespace */
#define CLONE_NEWUSER		0x10000000	/* New user namespace */
#define CLONE_NEWPID		0x20000000	/* New pid namespace */
#define CLONE_NEWNET		0x40000000	/* New network names

CLONE_NEWUSERの値は0x10000000なので、 0x34000000ulから引いた0x24000000ulを入力してみます。

// autogenerated by syzkaller (https://github.com/google/syzkaller)

#define _GNU_SOURCE 

#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
                syscall(__NR_mmap, /*addr=*/0x1ffff000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_AN;
        syscall(__NR_mmap, /*addr=*/0x20000000ul, /*len=*/0x1000000ul, /*prot=PROT_WRITE|PROT_READ|PROT_EXEC*/7ul,;
        syscall(__NR_mmap, /*addr=*/0x21000000ul, /*len=*/0x1000ul, /*prot=*/0ul, /*flags=MAP_FIXED|MAP_ANONYMOUS|;
        syscall(__NR_unshare, /*flags=CLONE_NEWUTS|CLONE_NEWPID*/0x24000000ul);

        return 0;
}

これでも同様にKernel Panicが起きました!想定通りです。

時間があるときにsyzkallerを使って発見されたLinux Kernelのバグの再現をしてみようと思います。syzkallerの仕組みも、Linuxそのものも奥が深いようなのでゆっくり沼に浸かっていきたいです。

Discussion