内核模块开发环境及调试

2025-02-24

Qemu虚拟机

  qemu下载

   直接apt下一个即可, 建议使用模拟x86的qemu, 这样可以避免使用交叉编译器

  Direct Linux Boot

   一方面, 可以使用qemu-img制作一个镜像, 然后用qemu模拟运行
   qemu支持所谓”直接引导Linux内核(Direct Linux Boot)”的方式启动虚拟机, 更适合内核的测试 https://www.qemu.org/docs/master/system/linuxboot.html
   此种方式需要准备三个部分:
    1. 一个压缩的Linux内核镜像, 俗称bzImage
    2. 一个临时根文件系统initrd
    3.指定根文件设备的挂载, 如果initrd已经是一个可用的文件系统, 则此处省略
    此外这种方式还支持gdb-attach, 是之后主要的调试方法

准备开发环境

 linux-headers

   内核模块的编译和构建需要一些特定的宏, 数据结构以及函数, 这些东西常用的头文件中没有, 需要下载专门的linux-headers
   如果你的apt或者别的什么包管理器可以直接下载对应版本的linux-headers, 但极有可能是找不到的
   首先从https://www.kernel.org/, 下一个linux-6.1.129.tar.xz; 或者curl -O -L https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-6.1.129.tar.xz
$ unxz linux-6.1.129.tar.xz
$ tar -xf linux-6.1.129.tar
   进入源码目录之后, make menuconfig进入图形化配置界面(需要全屏), 勾选以下内容
# 便于调试
Kernel hacking -> Compile-time checks and compiler options -> Debug information (Disable debug information) -> Rely on the toolchain's implicit default DWARF version
Kernel hacking -> Generic Kernel Debugging Instruments -> KGDB: kernel debugger
# 兼容initrd
File systems -> Second extended fs support -> Ext2 extended attributes
   然后执行make preparemake modules_prepare, 理论上只用make modules_prepare即可, 但是没试过
   然后执行make modules -j$(nproc), 这一步是主要为了生成Module.symvers, 这样才能使用一些内核函数

 编译与构建举例

   以下是一个字符型内核模块的示例, 模块名为holstein
   来自https://pawnyable.cafe/linux-kernel/
   警告: 这是一个有严重漏洞的模块, 请不要尝试挂载它.
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("Holstein v1 - Vulnerable Kernel Driver for Pawnyable");

#define DEVICE_NAME "holstein"
#define BUFFER_SIZE 0x400

char *g_buf = NULL;

static int module_open(struct inode *inode, struct file *file) {
  printk(KERN_INFO "module_open called\n");

  g_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
  if (!g_buf) {
    printk(KERN_INFO "kmalloc failed");
    return -ENOMEM;
  }

  return 0;
}

static ssize_t module_read(struct file *file, char __user *buf, size_t count,
                           loff_t *f_pos) {
  char kbuf[BUFFER_SIZE] = {0};

  printk(KERN_INFO "module_read called\n");

  memcpy(kbuf, g_buf, BUFFER_SIZE);
  if (_copy_to_user(buf, kbuf, count)) {
    printk(KERN_INFO "copy_to_user failed\n");
    return -EINVAL;
  }

  return count;
}

static ssize_t module_write(struct file *file, const char __user *buf,
                            size_t count, loff_t *f_pos) {
  char kbuf[BUFFER_SIZE] = {0};

  printk(KERN_INFO "module_write called\n");

  if (_copy_from_user(kbuf, buf, count)) {
    printk(KERN_INFO "copy_from_user failed\n");
    return -EINVAL;
  }
  memcpy(g_buf, kbuf, BUFFER_SIZE);

  return count;
}

static int module_close(struct inode *inode, struct file *file) {
  printk(KERN_INFO "module_close called\n");
  kfree(g_buf);
  return 0;
}

static struct file_operations module_fops = {
    .owner = THIS_MODULE,
    .read = module_read,
    .write = module_write,
    .open = module_open,
    .release = module_close,
};

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void) {
  if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME)) {
    printk(KERN_WARNING "Failed to register device\n");
    return -EBUSY;
  }

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    printk(KERN_WARNING "Failed to add cdev\n");
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void) {
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);
   首先, 为了简写include, 需要配置includePath, 具体因IDE而异
   然后, 编写Makefile, kernel Module的Makefile有特殊语法
BUILD_DIR := build
obj-m := holstein.o
KBUILD_DIR := ../_kernel/linux-6.1.129
CFLAGS_holstein.o := -O0

all:
	@mkdir -p $(BUILD_DIR)
	$(MAKE) -C $(KBUILD_DIR) M=$(shell pwd) modules
	find . -maxdepth 1 -type f ! -name '*.ko' ! -name 'Makefile' ! -name '*.c' ! -name 'build' -exec mv {} build \;

clean:
	$(MAKE) -C $(KBUILD_DIR) M=$(shell pwd) clean

	rm -rf $(BUILD_DIR)
   编译完成之后, 出现的holstein.ko就是编译后的模块

编译内核

   在上述源码文件夹中, 使用make menuconfig配置之后, 即可开始编译内核
$ make -j$(nproc) bzImage
   编译完成之后, 会通知镜像的路径
Kernel: arch/x86/boot/bzImage is ready  (#1)

编译busybox

   现在还缺少一个initrd, 使用busybox获取, 通过busybox可以获取一个带文件系统的rootfs
   从https://busybox.net/, 下载busybox源代码或者wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
   解压并配置
tar -vxf busybox-1.37.0.tar.bz2
cd busybox-1.37.0
make menuconfig
    Setttings 选中 Build static binary (no shared libs), 使其编译成静态链接的文件, 因为bzImage内核本身不提供glibc
    然后执行make install -j$(nproc), 当前文件夹会出现名为_install的文件夹
   进入文件夹, 添加一些东西, 并且把编译出的holstein.ko放进去
$ mkdir -p  proc sys dev etc/init.d
   在文件夹下新建一个init文件, 写入如下内容作为初始化脚本, 初始化系统环境, 上面的新建文件夹的操作也可以放在这个脚本里
#!/bin/sh

echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
insmod holstein.ko
setsid /bin/cttyhack setuidgid 1000 /bin/sh
   然后打包
$ cd _install
$ find . | cpio -o --format=newc > ../rootfs.cpio
   获得一个rootfs.cpio

启动Qemu

   在做完了上述工作之后, 编写一个用于启动Qemu的脚本
#!/bin/sh

qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel ./bzImage-6.1 \
    -append "root=/dev/ram console=ttyS0 loglevel=3 oops=panic panic=1 pti=on nokaslr" \
    -no-reboot \
    -cpu kvm64 \
    -S \
    -gdb tcp::1234 \
    -smp cores=2,threads=1 \
    -monitor /dev/null \
    -initrd rootfs.cpio \
    -net nic,model=virtio \
    -net user \
    -enable-kvm \
   看看模块是否挂上了

Screenshot 2025-02-24 174107.png

   由于权限不够, 所以这里显示不了挂载到的地址

调试

   在内核源码文件夹下有一个名为vmlinux的文件, 是内核的符号表, 用的上的用不上的都在里面.
   启动参数中的-gdb tcp::1234, 指的是本地的端口12345, -S参数会在qemu启动虚拟机后立即将其挂起, 方便调试
   在另一个窗口中使用: 加载符号表, 下断点
$ gdb -q -ex "target remote localhost:1234" 
(gdb) set architecture i386:x86-64 # 可选
(gdb) add-symbol-file vmlinux
(gdb) 
(gdb) b start_kernel
   结果差不多是下面这种

63b68857-6543-4031-b299-d624a29bf77e.png

   调试模块:
   init脚本改成root用户启动
setsid /bin/cttyhack setuidgid 0 /bin/sh
   命令行获取模块加载地址
# lsmod
holstein 16384 0 - Live 0xffffffffc0000000 (O)
   加载符号表, 断点
(gdb) add-symbol-file holstein.ko 0xffffffffc0000000
(gdb) b module_read

Screenshot 2025-02-24 202026.png

   对照一下地址是否一致
# grep module_read /proc/kallsyms

Screenshot 2025-02-24 202033.png

   看起来有点蠢, 但是也没找到更好的调试方法了
   据说gef插件会方便一些(