# Syzkaller 101: Cách hoạt động, kiến trúc, và áp dụng với một Custom Vulnerable Driver. ###### tags: `syzkaller` --------------------------------- ![syzkaller console](https://hackmd.io/_uploads/ryb9dJc-ge.png) ## I. General Knowledge ### 1. What is Syzkaller? - Syzkaller là một `Coverage-Guided kernel Fuzzer`, được viết bởi Google nhằm tự động tìm kiếm các lỗ hổng trong Linux Kernel, ngoài Linux Kernel ra thì Syzkaller còn được áp dụng cho một số hệ điều hành tương thích khác như Fuschia, FreeBSD, Windows. - Không giống như các fuzzer truyền thống khác, chẳng hạn như AFL++, nơi chế độ fuzzing `Fork Server` tạo ra mỗi process và truyền vào các process đó các input ngẫu nhiên, chế độ `Persistent Mode` cho phép tập trung fuzzing vào một đoạn code cụ thể. Syzkaller áp dụng một cách tiếp cận thông minh hơn, dựa trên `coverage feedback`, để tối ưu hóa việc khám phá các `đường đi` tiềm ẩn nguy hiểm trong kernel. - Từ năm 2016 đến nay, Syzkaller đã phát hiện `hàng ngàn` lỗ hổng trong linux kernel, giúp cho cộng đồng kernel hardening phát triển tốt hơn bằng cách tạo ra [Syzbot](https://syzkaller.appspot.com/upstream) (Một hệ thống Syzkaller tự động, chạy liên tục với các bản linux kernel mới, chủ yếu họ sẽ tìm bug dựa trên hệ thống này). ![image-1](https://hackmd.io/_uploads/ByAj_y5Wgl.png) - Linux Kernel Researcher chỉ cần dựa trên Syzbot để có thể tìm được các lỗ hổng, tuy nhiên có một vấn đề của Syzbot, đó là nó chỉ dựa trên một số [template fuzzer](https://github.com/google/syzkaller/tree/master/sys/linux) có sẵn do các developer của Syzkaller viết. Vì source của linux kernel rất rất rộng, syzbot không thể bao quát hết được toàn bộ, sẽ có một số `đường đi` không được cover hết. - Vì thế bài viết này và [Part 2](https://hackmd.io/HlQTdBOLRc-sX__Y0ndeGA) sẽ hướng dẫn bạn cách tùy chỉnh Syzkaller để fuzz chính xác các target mong muốn, từ đó mở rộng độ phủ kiểm thử và tìm thêm các lỗ hổng mà Syzbot mặc định có thể bỏ sót. ### 2. Syzkaller Knowledge #### a. Syscall description aka "syzlang" - Syzkaller cần một mô tả chi tiết về các syscall hoặc interface của target, định nghĩa đầu vào/đầu ra, kiểu dữ liệu và cách chúng tương tác. Các mô tả này thường nằm trong thư mục [sys/linux](https://github.com/google/syzkaller/tree/master/sys/linux). - Linux kernel có khoảng cỡ 450 syscall, nghe có vẻ là không nhiều lắm để làm attack surface, tuy nhiên với mỗi syscall thì linux kernel lại implement hàng trăm hướng để giao tiếp/tương tác với các subsystem. Vì thế syzkaller bao phủ tốt nhưng vẫn không đủ với toàn bộ, một số subsystem vẫn phải cần manual auditting. - Syzkaller sử dụng một ngôn ngữ mô tả syscall riêng gọi là syzlang để định nghĩa: + Tên syscall, các argument (kiểu cơ bản như int, pointer, hoặc struct phức tạp) + Quan hệ giữa các resources (ví dụ mở descriptor rồi truyền vào syscall khác) + Ràng buộc giá trị (range, flags, special values) Format của `Syzlang` ``` syscallname "(" [arg ["," arg]*] ")" [type] ["(" attribute* ")"] arg = argname type argname = identifier type = typename [ "[" type-options "]" ] typename = "const" | "intN" | "intptr" | "flags" | "array" | "ptr" | "string" | "filename" | "glob" | "len" | "bytesize" | "bytesizeN" | "bitsize" | "vma" | "proc" | "compressed_image" type-options = [type-opt ["," type-opt]] ``` Ví dụ với một số syscall cơ bản: `open`, `read`, `close` ``` open(file filename, flags flags[open_flags], mode flags[open_mode]) fd read(fd fd, buf buffer[out], count len[buf]) close(fd fd) open_mode = S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH ``` Nhờ ngôn ngữ này, Syzkaller có thể sinh các sequence syscall hợp lệ và đa dạng, thay vì chỉ fuzz ngẫu nhiên mã nhị phân. #### b. Các kiểu dữ liệu cơ bản (Basic Types) - Ints: Hỗ trợ int8/16/32/64 (có thể thêm hậu tố be cho big-endian), cận giá trị [min:max] hoặc bitfield int64:N - const: Kiểu hằng số kèm giá trị và kiểu nền (mặc định cùng kích thước với intN/intptr) - flags: Tập hợp các hằng số (tham chiếu đến phần mô tả flags) #### c. Syzlang struct - Syzlang define struct như sau: ``` structname "{" "\n" (fieldname type ("(" fieldattribute* ")")? (if[expression])? "\n")+ "}" ("[" attribute* "]")? ``` Ví dụ với struct sau: ```c struct foo { int8_t f0; const uint16_t f1; int32_t f2; int64_t f3: 20; } ``` Convert sang syzlang sẽ như sau: ``` example_struct { f0 int8 f1 const[0x42, int16be] f2 int32[0:100] f3 int64:20 } ``` #### d. Resources - Định nghĩa custom resource (ví dụ file descriptor, socket): ``` resource fd[int32]: 0xffffffffffffffff, AT_FDCWD, 1000000 resource sock[fd] resource sock_unix[sock] ``` - Sau đó có thể dùng `fd` hoặc `sock` làm kiểu cho syscall: ``` open(... ) fd accept(fd sock, ...) sock listen(fd sock, backlog int32) ``` #### e. Alias và Templates - Type Aliases ``` type identifier underlying_type ``` Ví dụ: ``` type signalno int32[0:65] type net_port proc[20000,4,int16be] ``` - Alias này có thể dùng như kiểu gốc trong mọi ngữ cảnh 2. Coverage feedback Sử dụng kernel với tính năng kcov, Syzkaller theo dõi coverage của từng testcase. Những testcase giúp tăng độ phủ code sẽ được ưu tiên và nhân bản để tiếp tục fuzz. 3. Executor và VM Mỗi testcase được chạy trên một instance (VM hoặc QEMU), thông qua một chương trình gọi là syz-executor. Khi testcase gây ra lỗi (crash, hang, timeout), kết quả sẽ được ghi nhận và lưu log chi tiết. ### 3. How Syzkaller works? ##### a. Kiến trúc tổng thể ![image-2](https://hackmd.io/_uploads/HyPTu15-lx.png) - Syzkaller là một `coverage-guided kernel fuzzer` cho Linux (và các hệ thống tương tự), bao gồm hai thành phần chính: `syz-fuzzer (manager + fuzzer logic)` và `syz-executor (thực thi testcases trong VM)`. + syz-fuzzer (manager/logic) khởi tạo nhiều máy ảo (VM) và quản lý quá trình fuzzing. Nó nạp các seed testcase (corpus) ban đầu, sinh thêm testcase mới bằng cách biến đổi (mutation) dựa trên mô tả syscall (syzlang). + syz-executor chạy các testcase này bên trong VM, thực thi các syscall lên kernel mục tiêu. + Kernel được build với hỗ trợ kcov để thu thập thông tin coverage (đường đi mã lệnh đã thực thi). + Coverage data được gửi ngược về syz-fuzzer. Nếu testcase nào giúp tăng coverage, nó sẽ được lưu lại vào corpus để tiếp tục fuzzing. + Khi phát hiện lỗi/crash, thông tin sẽ được gửi về crash/error handler để lưu trữ, tối ưu hóa (minimize testcase) và tái sinh (repro) nhằm phục vụ phân tích sau này. + Quá trình này diễn ra song song trên nhiều VM, giúp Syzkaller có thể kiểm thử kernel ở quy mô lớn và hiệu quả. ![image-3](https://hackmd.io/_uploads/H1F0uJqbel.png) - `syz-manager`: khởi tạo và điều phối các VM, quản lý cấu hình, corpus, thống kê và logs - `syz-fuzzer`: chịu trách nhiệm sinh mutation từ corpus ban đầu và ongoing, đánh giá testcase mới, ưu tiên testcase tạo coverage mới - `syz-executor`: tiến trình chạy bên trong VM, nhận testcases qua shared memory hoặc pipes, thực thi syscall sequence, thu thập coverage qua kcov và báo cáo lỗi/crash #### b. Corpus & Mutation 1. Seed corpus: Ban đầu Syzkaller khởi tạo một tập testcases cơ bản 2. Mutation strategies: - Bit-flips, arithmetic mutations trên giá trị argument - Truncate/extend pointer, buffer - Insert/Delete syscall trong sequence - Cross-over giữa các testcase 3. Prioritization: Mỗi testcase được gán trọng số dựa trên coverage gây ra; các testcase “interesting” (tăng coverage) được giữ lại và ưu tiên mutation tiếp theo #### c. Feedback dựa trên coverage Syzkaller tích hợp `Linux KCOV (hoặc SanitizerCoverage)` để thu thập coverage basic-block mỗi khi `syz-executor` thực thi syscall sequence. - Shared memory: vùng memory share giữa `syz-fuzzer` và `syz-executor` chứa `bitmap coverage`. - Filter: chỉ những `testcase` làm `tăng tổng số bits coverage` mới được xem là thành công và được thêm vào corpus. - Statistical analysis: Syzkaller ghi nhận số lượng coverage mới thu được, tốc độ tạo coverage và tự điều chỉnh mutation rate. #### d. Syz-executor & quản lý VM (Executor & VM Management) - Executor: một binary nhỏ (đều compile theo GOARCH tương ứng) chịu trách nhiệm đọc testcases, thực thi syscalls và trả kết quả/crash logs. - Fork server + SHMEM: để giảm overhead, executor thường dùng fork-server pattern và shared-memory, tránh khởi tạo quá nhiều process nặng. - VM orchestration: syz-manager khởi tạo số lượng VM (QEMU) mà mình muốn dựa trên config của manager, mỗi VM chạy executor, giao tiếp qua gRPC hoặc pipes. Khi VM crash/hang, manager tự động reboot và reload kernel mới. #### e. Xử lý crash (Crash Handling) - Crash detection: executor giám sát kernel panic, oops, hoặc timeout. - Crash report: Syzkaller lưu toàn bộ console log, call trace, VM state vào workdir/crashes/<type>/. - Minimization: sử dụng tool syz-minimize để tự động loại bỏ các syscalls dư thừa, tạo repro case nhỏ nhất vẫn trigger crash. - Reproducer: `syz-repro` biến repro case thành C program hoặc syzkaller script để dễ test lại và patch. Tuy nhiên, không phải bug nào cũng reproduce thành công, đôi lúc phải tự làm bằng tay hoặc không thể reproduce, chỉ có crash report (đa phần là như vậy, và đây là điểm yếu của syzkaller, bắt buộc vulnerability researcher phải có kỹ năng đọc crash report và tự tạo ra reproducer). ## II. Build Syzkaller ### 1. Cách build syzkaller - Về cách build của syzkaller thì trong [repo](https://github.com/google/syzkaller/blob/master/docs/linux/setup.md) đã mô tả khá chi tiết cách build rồi nên mình sẽ không nói lại cách build nữa. - Ngoài ra trong series syzkaller này mình chỉ xoay quanh việc fuzzing linux kernel os, mình dựng trên Ubuntu host, QEMU vm, X86_64 kernel. Cũng như trên, repo cũng mô tả khá chi tiết việc build chúng [build kernel](https://github.com/google/syzkaller/blob/master/docs/linux/setup_ubuntu-host_qemu-vm_x86-64-kernel.md) - Sau khi mọi thứ đã xong hết thì sẽ bao gồm các file như sau ![image-4](https://hackmd.io/_uploads/r1cyYy5-ge.png) - Viết 1 file config đơn giản cho syz-manager ```c { "target": "linux/amd64", "http": "127.0.0.1:56741", "workdir": "/home/cpu12462/p2o_2025/workdir", "kernel_obj": "/home/cpu12462/p2o_2025/linux", "image": "/home/cpu12462/p2o_2025/image/bullseye.img", "sshkey": "/home/cpu12462/p2o_2025/image/bullseye.id_rsa", "syzkaller": "/home/cpu12462/syzkaller_/", "procs": 4, "type": "qemu", "vm": { "count": 2, "kernel": "/home/cpu12462/p2o_2025/linux/arch/x86/boot/bzImage", "cpu": 2, "mem": 2048 } } ``` - Và sau đó chạy syz-manager sẽ được kết quả như thế này ![image-5](https://hackmd.io/_uploads/rk7lYkcZxg.png) ## III. Fuzzing a Vulnerable Driver Bây giờ mới là phần chính của bài viết này. Trước khi đến với việc fuzzing một real world target, hãy thử với những thứ nhỏ hơn. Để có thể hiểu rõ cách syzkaller hoạt động, thì mình sẽ bắt đầu với việc viết một module chứa lỗ hổng trong kernel để test (và chỉ có thể tương tác thông qua một `kênh truyền duy nhất`) ### 1. Build module để test ```c #include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/cdev.h> #include <linux/slab.h> #include <linux/ioctl.h> #define DEVICE_NAME "vulnio" #define IOCTL_CMD _IOW('m', 1, struct ioctl_arg) #define MAX_PAYLOAD 512 static int major; static struct cdev cdev; struct ioctl_arg { uint32_t size; char data[]; /* flexible array member */ }; static long ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct ioctl_arg __user *uarg = (void __user *)arg; struct ioctl_arg *karg; uint32_t size; long ret = 0; if (cmd != IOCTL_CMD) return -EINVAL; if (copy_from_user(&size, &uarg->size, sizeof(size))) return -EFAULT; /* Intentionally vulnerable: allow size up to MAX_PAYLOAD * 2 */ if (size == 0 || size > MAX_PAYLOAD * 2) return -EINVAL; /* Allocate kernel buffer slightly smaller than user size to trigger overflow */ karg = kmalloc(sizeof(*karg) + MAX_PAYLOAD, GFP_KERNEL); if (!karg) return -ENOMEM; karg->size = size; /* Vulnerable copy without proper bounds check */ if (copy_from_user(karg->data, uarg->data, karg->size)) { ret = -EFAULT; goto out; } pr_info("ioctl: received %u bytes of data\n", karg->size); out: kfree(karg); return ret; } static const struct file_operations fops = { .owner = THIS_MODULE, .unlocked_ioctl = ioctl, }; static int __init init(void) { dev_t dev; if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0) { pr_err("Failed to allocate char device region\n"); return -1; } major = MAJOR(dev); cdev_init(&cdev, &fops); cdev.owner = THIS_MODULE; if (cdev_add(&cdev, dev, 1) < 0) { unregister_chrdev_region(dev, 1); pr_err("Failed to add cdev\n"); return -1; } pr_info(" vulnerable ioctl module loaded (major=%d)\n", major); return 0; } static void __exit vuln_exit(void) { dev_t dev = MKDEV(major, 0); cdev_del(&cdev); unregister_chrdev_region(dev, 1); pr_info(" vulnerable ioctl module unloaded\n"); } module_init(init); module_exit(vuln_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("YourName <youremail@example.com>"); MODULE_DESCRIPTION("Custom vulnerable ioctl kernel module for fuzz testing"); ``` Module này minh hoạ một vulnerable ioctl với buffer overflow chủ ý bằng cách: + Cho phép user chỉ định size lớn hơn buffer kernel. + Cấp buffer quá nhỏ (MAX_PAYLOAD). + Copy toàn bộ dữ liệu user vào buffer mà không kiểm soát đầy đủ. - Để user tương tác với module chứa lỗ hổng như trên: ```c #include <string.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <sys/ioctl.h> #define IOCTL_CMD _IOW('m', 1, struct ioctl_arg) #define MAX_PAYLOAD 512 struct ioctl_arg { uint32_t size; char data[]; }; int main() { int fd = open("/dev/vulnio", O_RDWR); if (fd < 0) { perror("Failed to open /dev/myvulnio"); return 1; } uint32_t payload_size = MAX_PAYLOAD + 128; struct ioctl_arg *arg = malloc(sizeof(*arg) + payload_size); if (!arg) { perror("malloc failed"); close(fd); return 1; } arg->size = payload_size; memset(arg->data, 'A', payload_size); printf("[+] Sending %u bytes to /dev/vulnio\n", arg->size); if (ioctl(fd, IOCTL_CMD, arg) == -1) { perror("ioctl failed"); } else { printf("[+] ioctl succeeded\n"); } free(arg); close(fd); return 0; } ``` - Compile module chứa lỗ hổng và test trigger lỗi. ![image-6](https://hackmd.io/_uploads/ByZZFkcZlg.png) - Lỗ hổng buffer overflow đã được trigger và crash report khá chi tiết với KASAN được builtin với kernel. ```bash root@syzkaller:~# cat /etc/init.d/load_vuln #!/bin/sh ### BEGIN INIT INFO # Provides: load_vuln # Required-Start: $remote_fs $syslog # Required-Stop: # Default-Start: 2 3 4 5 # Default-Stop: # Short-Description: Load vuln_ioctl module at boot ### END INIT INFO case "$1" in start) echo "[*] Loading vuln.ko..." insmod /root/vuln.ko echo "[*] Creating /dev/vulnio..." major=$(dmesg | grep "vulnerable ioctl module loaded" | tail -n1 | grep -oP 'major=\K[0-9]+') if [ -n "$major" ]; then mknod /dev/vulnio c $major 0 chmod 666 /dev/vulnio else echo "[!] Could not determine major number for vuln device" fi ;; stop) rmmod vuln rm -f /dev/vulnio ;; *) echo "Usage: $0 {start|stop}" exit 1 esac exit 0 ``` - Thêm đoạn script như trên vào `/etc/init.d/` để mỗi lần VM boot lên thì sẽ load module trên vào. ### 2. Viết Description cho Syzkaller để fuzzing module trên - Để viết được một description mới cho Syzkaller thì ta cứ dựa vào document của họ [syzlang](https://github.com/google/syzkaller/blob/master/docs/syscall_descriptions.md), [syscall_description](https://github.com/google/syzkaller/blob/master/docs/syscall_descriptions_syntax.md) Để Syzkaller hiểu và fuzz đúng ioctl của module vulnio, ta cần: 1. Đặt file description vào thư mục syzkaller/sys/linux/. 2. Include các header cần thiết để Syzkaller “hiểu” được macro và struct. 3. Khai báo resource, syscall sequence (open + ioctl), và type cho argument. Ví dụ cho description để fuzzing module trên: `syzkaller/sys/linux/cc.txt` ```c include <linux/ioctl.h> include <vuln.h> resource fd_vuln[fd] open_vuln(fd_vuln) = openat$char_dev(AT_FDCWD, "/dev/vulnio", O_RDWR, 0) ioctl_arg { size len[DATA, int32] DATA array[int8] } ioctl$VULN(fd fd_vuln, cmd const[IOCTL_CMD], arg ptr[in, ioctl_arg]) ``` - `include <linux/ioctl.h>`: Bao gồm các định nghĩa `ioctl` của Linux. - `include <vuln.h>`: Bao gồm các định nghĩa từ file header tự định nghĩa vuln.h (chứa struct, macro liên quan đến module vuln). - `resource fd_vuln[fd]`: Định nghĩa một resource mới tên là fd_vuln, dựa trên kiểu file descriptor (fd). Resource này dùng để quản lý các file descriptor mở ra từ device `/dev/vulnio`. - `open_vuln(fd_vuln) = openat$char_dev(AT_FDCWD, "/dev/vulnio", O_RDWR, 0)`: Định nghĩa một syscall mở device `/dev/vulnio` với quyền `đọc/ghi`, trả về resource `fd_vuln`. - `ioctl_arg { ... }`: Định nghĩa một struct tên `ioctl_arg` với hai trường: + `size`: là độ dài của trường DATA, kiểu int32. + `DATA`: là một mảng các số nguyên 8 bit (int8), độ dài xác định bởi trường size. - `ioctl$VULN(fd fd_vuln, cmd const[IOCTL_CMD], arg ptr[in, ioctl_arg])`: Định nghĩa một syscall ioctl đặc biệt cho device này: + `fd`: là resource fd_vuln (file descriptor của device). + `cmd`: là hằng số IOCTL_CMD (được định nghĩa trong vuln.h). + `arg`: là con trỏ tới struct ioctl_arg, truyền vào từ user. - Add file header của module chứa lỗ hổng vào `linux/include/` để description có thể lấy được giá trị macro để fuzzing. ```c #ifndef _VULN_IOCTL_H #define _VULN_IOCTL_H #include <linux/ioctl.h> #include <linux/types.h> /* * Header for the vulnerable ioctl kernel module * Provides struct definition and IOCTL command macro */ /** * struct ioctl_arg - argument structure for vuln ioctl * @size: number of bytes in @data * @data: flexible array member holding payload */ struct ioctl_arg { uint32_t size; char data[]; }; #define IOCTL_CMD _IOW('m', 1, struct ioctl_arg) #endif /* _VULN_IOCTL_H */ ``` - Bước tiếp theo dựa trên document của syzkaller thì cần phải chạy `make extract`, dựa vào mô tả thì make extract sẽ generates/updates các file *.const, có nghĩa là nó trích xuất các hằng số tất cả các kiến trúc được cross-compile và đặt nó ở trong các file .const. ![image-7](https://hackmd.io/_uploads/S1lMKkqbgx.png) - Sau đó sẽ `make generate` updates generated code và cuối cùng là `make` để rebuilt lại các file binary của syzkaller ![image-8](https://hackmd.io/_uploads/SkBGKJ5Wgl.png) ![image-9](https://hackmd.io/_uploads/HkBLKJ5-ll.png) - Trong quá trình `make generate` sẽ bị vướng một số lỗi liên quan đến tương thích version của go modules (phần này thì xem như bài tập về nhà của các bạn, hãy tự fix những phần đó). ### 3. Chạy Syzkaller - Sau khi chạy thì sẽ có bug ngay lập tức ![image-11](https://hackmd.io/_uploads/ByStYJcWge.png) - Page của Syzkaller ![image-10](https://hackmd.io/_uploads/Bk1dY15bgl.png) - Tính năng coverage ở page của syzkaller khá là hay. Nó giúp cho mình có cái nhìn tốt hơn về fuzzer mình viết có hoạt động đúng hay không hay là sai ở chỗ nào để có thể sửa lại cho tốt hơn. ## IV. Final Word - Đợi một lúc thì sẽ có reproducer hoặc là không. Như ở trên mình nói, syzkaller có một điểm yếu là nó không hoàn toàn có thể reproduce lại được bug, đôi lúc nó sẽ làm được đôi lúc thì không (thông thường thì không được). Vậy nên đôi lúc vulnerable researcher phải có kỹ năng đọc crash log để có thể trace bug, tìm root cause để có thể reproduce được bug. - Reproducer từ syzkaller: ![image-12](https://hackmd.io/_uploads/rJ3thVjbxe.png) - Ở bài viết tiếp theo mình sẽ hướng dẫn modified syzkaller để fuzzing một real world target Cảm ơn bạn đã đọc tới đây. Peace! Out