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 prepare
和make 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 \
看看模块是否挂上了
由于权限不够, 所以这里显示不了挂载到的地址
调试
在内核源码文件夹下有一个名为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
结果差不多是下面这种
调试模块:
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
对照一下地址是否一致
# grep module_read /proc/kallsyms