syzkallerでLinux KernelをFuzzingしてみる
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/dl/go1.22.1.linux-amd64.tar.gz
tar -xf go1.22.1.linux-amd64.tar.gz
Goのパスを環境変数に追加します。
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にファジングに必要な機能を有効にします。
# 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
は手元の環境に合わせて調整してください。
{
"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をいじっている方のブログの再現をしてみます。
ここでは、 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_NEWUTS
とCLONE_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に記述されています。
#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