Linux Kernel là một phần quan trọng của hệ thống Linux, và việc debug Kernel là một kỹ năng quan trọng đối với các nhà phát triển hệ thống. Trong bài viết này, chúng ta sẽ tìm hiểu cách sử dụng QEMU, một trình ảo hóa hệ thống, để debug Linux Kernel một cách hiệu quả.

Khi các pwner chuyển từ việc chơi trên userland sang tìm hiểu về khai thác lỗ hổng trên Linux Kernel, một trong những thách thức khó nhất mà họ phải đối mặt ban đầu là không biết cách gỡ lỗi Kernel.

Làm thế nào để khởi chạy Kernel, làm sao để đặt breakpoint và các câu hỏi tương tự là những điều đầu tiên mà người mới muốn tiếp cận với lĩnh vực này thường đặt ra.

Bài viết này sẽ hướng dẫn cho những người mới muốn thử sức với khai thác lỗ hổng trên Linux Kernel khi tham gia các hoạt động CTF hoặc nghiên cứu CVE.

  • Môi trường: kali-linux-2024.1
  • Công cụ sử dụng: QEMU

Build Kernel

Việc biết cách tự xây dựng kernel là một yếu tố quan trọng vì có một số cài đặt quan trọng chỉ có thể thay đổi được trong quá trình xây dựng kernel. Những cài đặt này có thể ảnh hưởng đến các cơ chế phòng thủ hoặc liên quan đến các module.

Khi nghiên cứu CVE, ta cũng cần có khả năng tự xây dựng kernel chính xác chứa lỗ hổng tương ứng.

Các bước để build kernel bao gồm:

Bước 1: Kiếm source

Để build được kernel thì đầu tiên ta cần phải kiếm source code về.

Bạn có thể tải source code kernel theo phiên bản tại đây: Tags · torvalds/linux (github.com)

Trong quá trình nghiên cứu CVE, nếu chúng ta biết commit nào đã tạo ra lỗ hổng, chúng ta có thể sử dụng mã nguồn theo commit đó. Phương pháp này giúp chúng ta xây dựng kernel chính xác chứa lỗ hổng tương ứng.

Sử dụng lệnh git fetch sẽ giúp tránh việc phải sao chép toàn bộ repo của Linux Kernel về máy, vì quá trình này tốn nhiều dung lượng lưu trữ.

git init
git fetch --depth=2 <https://github.com/torvalds/linux.git> <full-length SHA1>
git checkout FETCH_HEAD
  • --depth=2: Giới hạn số lượng commit được fetch. với depth = 2 thì sẽ lấy thêm một commit trước đó, nhờ vậy có thể so sánh được 2 commit xem đã có gì thay đổi khiến xuất hiện lỗ hổng.

Bước 2: Tiến hành build

Quá trình build kernel mình có tham khảo bài blog: Linux kernel QEMU setup - Víctor Colombo (vccolombo.github.io)

Đầu tiên, cài đặt QEMU và các công cụ cần thiết để build kernel:

sudo apt-get update
sudo apt-get install -y git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison qemu-system-x86

Tiếp theo, thiết lập file .config thông qua make:

# tại folder source code kernel: [kernel_dir]
make defconfig # tạo file .config
make kvm_guest.config # chỉnh sửa file .config để thiết lập mọi thứ cần thiết để chạy trên qemu 
#  hoặc sử dụng make kvmconfig cho các phiên bản kernels cũ

Sau đó, kích hoạt một số config cần thiết cho việc fuzzing, debug, và phát hiện lỗi. ta ghi thêm các dòng sau vào cuối file .config:

# thu thập độ phủ cho fuzzing
CONFIG_KCOV=y

# thêm thông tin cho debug
CONFIG_DEBUG_INFO=y

# phát hiện memory bug
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y

# Required for Debian Stretch
CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y

Trong quá trình build kernel, bạn có thể gặp phải các warning, và thường chúng bị coi là error do treat warning as error được bật. Để tắt tính năng này đi thì bạn có thể chỉnh sửa file .config:

# CONFIG_WERROR=y
CONFIG_WERROR=n

Sau khi tự sửa file .config thủ công, chạy make olddefconfig để cấu hình lại các sửa đổi mới được thêm vào.

# [kernel_dir]
make olddefconfig

Cuối cùng, chạy lệnh make để build kernel:

# [kernel_dir]
make -j`nproc`

Sau khi build xong thì ta sẽ có file kernel nằm ở:

[kernel_dir]/arch/x86_64/boot/bzImage 

Quá trình build sẽ tốn khá nhiều thời gian, vì vậy trong lúc chờ đợi ta sẽ đi tới bước tiếp theo.

Build image

Lúc này, nếu boot kernel luôn thì bạn sẽ gặp phải lỗi Kernel panic - not syncing: VFS: Unable to mount root.

Lý do là không thể boot kernel mà không có root filesytem.

Có nhiều cách để tạo một root filesystem như là sử dụng công cụ initramfs .

Trong bài này thì ta sẽ làm theo hướng dẫn tại syzkaller guide và sử dụng script có sẵn của họ để tạo file system:

# [kernel_dir]
sudo apt-get install -y debootstrap
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

Bước này cũng đòi hỏi nhiều thời gian, vì vậy thường ta sẽ kết hợp việc xây dựng kernel và tạo image cùng một lúc.

Sau khi quá trình tạo image hoàn thành, chúng ta sẽ có một thư mục chroot được tạo ra, bên cạnh đó còn có cặp khóa RSA được sử dụng để kết nối SSH vào QEMU, cùng với tệp hệ thống bullseye.img.

Khởi chạy kernel

Để khởi chạy Kernel với QEMU, chúng ta sẽ sử dụng lệnh dưới đây. Hãy chắc chắn rằng bạn đã thay đổi đường dẫn và tùy chọn theo cấu hình của mình:

qemu-system-x86_64 \\
        -m 2G \\
        -smp 2 \\
        -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,hostfwd=tcp::10022-:22 \\
        -net nic,model=e1000 \\
        -enable-kvm \\
        -nographic \\
        -pidfile vm.pid \\
        2>&1 | tee vm.log

Giải thích một số flag quan trọng:

Một số cơ chế kernel security có thể được bật, tắt lúc khởi chạy kernel:

  • Kích hoạt SMEP (cơ chế ngăn user space code khi kernel space code đang chạy): -cpu kvm64,+smep
  • Kích hoạt SMAP (cơ chế ngăn dữ liệu tại user space được đọc bởi kernel space): -cpu kvm64,+smap
  • Tắt KASLR (cơ chế ngẫu nhiên giống ASLR trên user space): -append "... nokaslr ..."
  • Kích hoạt KPTI (cơ chế chống side-channel attack): -append "... pti=on ..."

Sau khi khởi động xong thì ta có thể đăng nhập với root và không cần mật khẩu.

...
[  OK  ] Reached target Graphical Interface.
         Starting Update UTMP about System Runlevel Changes...
[  OK  ] Finished Update UTMP about System Runlevel Changes.

Debian GNU/Linux 11 syzkaller ttyS0

syzkaller login: root
Linux syzkaller 6.3.0-rc5-gc56e022c0a27 #1 SMP PREEMPT_DYNAMIC Tue Apr  9 02:11:54 EDT 2024 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
A valid context for root could not be obtained.
Last login: Tue Apr  9 08:38:30 UTC 2024 on ttyS0
root@syzkaller:~# 

Để kết nối với máy QEMU, ta sử dụng:

# [kernel_dir]
ssh -i image/bullseye.id_rsa -p 10022 -o "StrictHostKeyChecking no" root@localhost

Khi muốn tắt máy ảo QEMU, truy cập vào QEMU console bằng tổ hợp phím ctrl a + c và gõ q

Debug

Trong phần này, chúng ta sẽ sử dụng công cụ gdb kết hợp với plugin pwndbg để thực hiện một demo về quá trình gỡ lỗi (debug).

Attach debugger

Để có thể debug kernel, ta cần bổ sung thêm 2 flag vào câu lệnh khởi chạy máy ảo như sau:

qemu-system-x86_64 \\
        -m 2G \\
        -smp 2 \\
        -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,hostfwd=tcp::10022-:22 \\
        -net nic,model=e1000 \\
        -enable-kvm \\
        -nographic \\
        -pidfile vm.pid \\
        **-s \\
        -S \\**
        2>&1 | tee vm.log
  • -s: Viết tắt của -gdb tcp::1234, flag này sẽ chạy gdbserver tại port 1234
  • -S: Freeze CPU at startup, cho phép ta đặt breakpoint trước khi kernel chạy.

Sau khi chạy lệnh trên, màn hình sẽ không hiển thị bất kỳ thông tin nào. Điều này xảy ra vì gdbserver đang chờ đợi để được kết nối bởi gdb. Trong một cửa sổ terminal khác, hãy khởi động gdb và nhập lệnh sau để thực hiện kết nối (attach):

# attach
pwndbg> target remote :1234
Remote debugging using :1234
# continue
pwndbg> c

QEMU sẽ tiếp tục khởi chạy kernel

Debug kernel

File /proc/kallsyms là một file chứa danh sách các symbols của kernel.

Thông thường, khi cơ chế bảo mật KADR (Kernel Address Display Restriction) được kích hoạt, bạn sẽ không thể đọc được file này cho dù có quyền root. Tuy nhiên, may mắn là khi sử dụng hình ảnh được xây dựng bằng script của syskaller, cơ chế KADR đã được tắt mặc định, vì vậy bạn không cần cài đặt thêm bất kỳ điều gì.

Hãy cùng xem thử xem trong file này có gì:

root@syzkaller:~# cat /proc/kallsyms
...
ffffffff81000000 T startup_64
ffffffff81000000 T _stext
ffffffff81000000 T _text
ffffffff81000060 T secondary_startup_64
ffffffff81000065 T secondary_startup_64_no_verify
ffffffff81000150 t __pfx_verify_cpu
ffffffff81000160 t verify_cpu
ffffffff81000260 T __pfx_sev_verify_cbit
ffffffff81000270 T sev_verify_cbit
ffffffff81000280 T start_cpu0
ffffffff81000290 T __pfx___startup_64
ffffffff810002a0 T __startup_64
ffffffff810006b0 T __pfx_startup_64_setup_env
  • Cột đầu tiên là địa chỉ của symbol
  • Cột thứ hai là kiểu của section, T là text section, D là data section. Chi tiết, có thể gõ lệnh man nm để xem thêm.
  • Cuối cùng là symbol.

Thông qua file này, ta có thể tìm được địa chỉ các hàm trong kernel, ví dụ ta muốn tìm địa chỉ hàm commit_creds, ta chạy kết hợp với lệnh grep :

root@syzkaller:~# cat /proc/kallsyms | grep 'commit_creds'
ffffffff811e2330 T __pfx_commit_creds
**ffffffff811e2340 T commit_creds**
ffffffff8494a7dc r __ksymtab_commit_creds

Đặt breakpoint tại địa chỉ đã tìm được:

pwndbg> break *0xffffffff811e2340
Breakpoint 1 at 0xffffffff811e2340
pwndbg> c

Hàm commit_creds được gọi khi mà kernel tạo process mới. Chạy thử lệnh ls trong máy ảo và quan sát gdb. Ta có thể thấy được rằng chương trình đã dừng tại breakpoint:

Debug driver

Bây giờ, ta sẽ tự build một driver đơn giản, thử đặt breakpoint và tiến hành debug trên đó.

Tại folder [kernel_dir]/drivers, ta tạo thêm một folder khác tên là mymodule. Trong folder này ta tạo 2 file với nội dung như sau:

/mymodule.c - nguồn Linux_Driver_Tutorial/01_simple_LKM/mymodule.c at main · Johannes4Linux/Linux_Driver_Tutorial (github.com):

#include <linux/module.h>
#include <linux/init.h>

/* Meta Information */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Johannes 4 GNU/Linux");
MODULE_DESCRIPTION("A hello world LKM");

/**
 * @brief This function is called, when the module is loaded into the kernel
 */
static int __init my_init(void) {
        printk("Hello, Kernel!\\n");
        return 0;
}

/**
 * @brief This function is called, when the module is removed from the kernel
 */
static void __exit my_exit(void) {
        printk("Goodbye, Kernel\\n");
}

module_init(my_init);
module_exit(my_exit);

Makefile:

obj-m += mymodule.o
CC += -g -DDEBUG

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Quay lại [kernel_dir] và chạy make để build module:

#[kernel_dir]
make -C . M=drivers/mymodule

Kết quả file mymodule.ko đã được tạo ra:

#[kernel_dir]/drivers/mymodule
$ ls
Makefile mymodule.c  **mymodule.ko** ...

Module này có cấu trúc đơn giản, khi nạp vào kernel, hàm my_init sẽ được gọi và in ra dòng chữ "Hello, Kernel!". Khi module bị xóa khỏi kernel, hàm my_exit sẽ được gọi và in ra dòng chữ "Goodbye, Kernel!".

Vậy là module đã được build thành công. Tiếp theo, ta sẽ load module vào kernel và kiểm tra xem nó được load vào địa chỉ nào.

Chuyển file .ko này vào trong máy ảo thông qua SSH:

#[kernel_dir]
scp -P 10022 -i image/bullseye.id_rsa drivers/mymodule/mymodule.ko root@localhost:/home

Load module vào kernel bằng lệnh insmod:

root@syzkaller:/home# insmod mymodule.ko
[  117.409434] Hello, Kernel!

Lấy địa chỉ mà module được load vào thông qua file /proc/modules:

root@syzkaller:/home# cat /proc/modules
mymodule 16384 0 - Live 0xffffffffc0000000 (O)

Sau khi đã load xong module và có được địa chỉ gốc của nó, ta quay lại với gdb để đặt break point. Ta sẽ thử đặt breakpoint tại hàm my_exit.

Load symbol vào gdb với module base address bằng lệnh add-symbol-file và đặt breakpoint thông qua symbol:

pwndbg> add-symbol-file ./drivers/mymodule/mymodule.ko 0xffffffffc0000000
add symbol table from file "./drivers/mymodule/mymodule.ko" at
        .text_addr = 0xffffffffc0000000
Reading symbols from ./drivers/mymodule/mymodule.ko...
pwndbg> break my_exit
Breakpoint 1 at 0x10 (2 locations)
pwndbg> bl
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   <MULTIPLE>
1.1                         y   0x0000000000000010 <my_init>
1.2                         y   0xffffffffc0000010 in my_exit at drivers/mymodule/mymodule.c:25

Để kích hoạt my_exit ta sẽ xóa module ra khỏi kernel bằng lệnh rmmod:

root@syzkaller:/home# rmmod mymodule

Kết quả, chương trình đã dừng tại breakpoint module_exit:

pwndbg> c
Continuing.
[Switching to Thread 1.2]

Thread 2 hit Breakpoint 1.2, my_exit () at drivers/mymodule/mymodule.c:25
25      module_exit(my_exit);

Kết quả là chúng ta đã thành công trong việc chạy và gỡ lỗi (debug) Linux kernel và module. Bài viết của tôi đã đến đây là kết thúc. Nếu có bất kỳ sai sót nào, xin vui lòng để lại nhận xét của bạn ở phần bình luận bên dưới.

Tài liệu tham khảo

  1. Keith Makan (2020), [Linux Kernel Exploitation 0x0] Debugging the Kernel with QEMU (k3170makan.com)
  2. Víctor Cora Colombo (2021), Linux kernel QEMU setup - Víctor Colombo (vccolombo.github.io)
  3. Nick Desaulniers (2018), Booting a Custom Linux Kernel in QEMU and Debugging It With GDB (nickdesaulniers.github.io)
  4. syzkaller/docs/linux/setup_ubuntu-host_qemu-vm_x86-64-kernel.md at master · google/syzkaller · GitHub
  5. Kernel parameter: linux/Documentation/admin-guide/kernel-parameters.txt at master · torvalds/linux (github.com)
Chia sẻ bài viết này