gdb 可以连接到 qemu 提供的调试端口,对 qemu 中的整个系统进行调试,在用户空间和内核中下地址都可以!(target remote :1234。不能用 ni) 如果要在内核中下断点的话,获取符号地址有两种方式:

  • 如果有 root 权限,可以打开 /proc/kallsyms
  • 如果有系统镜像且没有打开 ASLR,可以直接 objdump 或 nm 等。

自制环境搭建(使用 buildroot)

buildroot 是一个制作嵌入式 linux 环境的工具,会首先编译出一个目标环境的工具链,然后用这个工具链交叉编译出一个内核+小型文件系统出来。

使用 buildroot 的总体流程如下:

# 首先需要下载 buildroot: http://buildroot.org/downloads/
make list-defconfigs # 列出一些预定义(predefined)的配置
make qemu_x86_64_defconfig # 使用预定义的 QEMU x86_64 配置(这一步会覆盖用户自定义的配置,所以请小心!)
make menuconfig # buildroot 自己的配置项
make linux-menuconfig # linux 的配置项
make busybox-menuconfig # busybox 的配置项
make -j$(nproc)

其中,一个 kernel pwn 环境需要修改的地方包括:

  • buildroot 配置
    • Filesystem Image -> cpio the root filesystem
    • Filesystem images -> ext2/3/4 root file system -> choose ext3
  • linux 配置
    • General Settings -> Initial RAM filesystem and RAM disk (initramfs/initrd) support
    • Kernel Hacking -> Kernel debugging
    • Kernel Hacking -> Compile-time checks and compiler options -> Debug information -> Rely on the toolchain's implicit default DWARF version
  • busybox 配置
    • Shells -> cttyhack
    • Runit Utilities -> setuidgid

编译完以后,把 <PATH_TO_BUILDROOT>/output/images/*<PATH_TO_BUILDROOT>/output/linux-<VERSION>/vmlinux 拿出来。

附带一个启动脚本:

#!/bin/sh
#
# run.sh
# Copyright (C) 2025 y2 <cameudis@gmail.com>

# decompress rootfs.cpio
if [ ! -d rootfs ]; then
	mkdir rootfs && cd rootfs
	if file "../rootfs.cpio" | grep -q 'gzip compressed'; then
		cp ../rootfs.cpio ./rootfs.cpio.gz
		gunzip ./rootfs.cpio.gz
	else
		cp ../rootfs.cpio .
	fi
	cpio -idmv < rootfs.cpio
	rm rootfs.cpio
	cd ..
fi

# compile exp
gcc -masm=intel --static -fno-pie -o exp ./exp.c
mv exp rootfs

# repack rootfs.cpio
cd rootfs
find . -print0 \
| cpio --null -ov --owner root:root --format=newc \
| gzip -9 > ../rootfs.cpio
cd ..

# start vm
qemu-system-x86_64 \
	-m 256M \
	-cpu kvm64,+smep,+smap \
	-smp cores=2,threads=2 \
	-kernel ./bzImage \
	-initrd ./rootfs.cpio \
	-nographic \
	-monitor /dev/null \
	-snapshot \
	-append "console=ttyS0 rdinit=/init nokaslr pti=on quiet oops=panic panic=1" \
	-no-reboot -s

再附带一个 CTF /init 脚本:

#!/bin/sh
chown -R 0:0 /
mount -t tmpfs tmpfs /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

# echo 1 > /proc/sys/kernel/dmesg_restrict
# echo 1 > /proc/sys/kernel/kptr_restrict

chown 0:0 /flag
chmod 400 /flag
chmod 777 /tmp

# insmod kgadget.ko
# chmod 777 /dev/kgadget

#setsid cttyhack setuidgid 1000 sh
setsid cttyhack setuidgid 0 sh
poweroff -d 0 -f

内核版本指定

  • buildroot 配置
    • Toolchain -> Custom kernel headers series -> choose xxx
    • Kernel -> Kernel version

内核修改(patch)

buildroot 每次重新 build 会重新拉下来一个指定版本的 linux 源码进行编译,因此如果想要对内核源码进行修改,需要手动制作 patch 文件并在 buildroot 中指定。

  • 方便起见,最好先整一个带 git 版本控制的完整 linux 源码(git clone https://mirror.sjtu.edu.cn/git/linux.git)。
  • 切换到目标版本 git checkout xxx
  • 进行修改
  • 将修改提交为一个 commit
  • git format-patch -1 HEAD 将修改导出为一个 patch 文件
  • 将 patch 文件保存下来,并修改 buildroot 配置 Kernel -> Custom kernel patches ,将其修改为 patch 文件的绝对路径或其目录(后者可用于加载多个 patch)

内核模块

如果需要编译内核模块,需要在 buildroot 的 package 目录下新建如下结构:

pwn_kmod
├── Config.in
├── pwn_kmod.mk
└── src
    ├── Makefile
    └── pwn.c

Config.in:

config BR2_PACKAGE_PWN_KMOD
    bool "pwn_kmod (out-of-tree)"
    depends on BR2_LINUX_KERNEL
    help
      Kernel module with a controlled bug for practice.

pwn_kmod.mk:

PWN_KMOD_VERSION = 1.0
PWN_KMOD_SITE = package/pwn_kmod/src
PWN_KMOD_SITE_METHOD = local
PWN_KMOD_LICENSE = MIT

define PWN_KMOD_BUILD_CMDS
        $(LINUX_MAKE_ENV) $(MAKE) -C $(LINUX_DIR) \
                M=$(@D) \
                ARCH=$(KERNEL_ARCH) \
                CROSS_COMPILE="$(KERNEL_CROSS)" \
                HOSTCC="$(HOSTCC)" \
                modules
endef

define PWN_KMOD_INSTALL_TARGET_CMDS
    $(INSTALL) -D -m 0644 $(@D)/pwn.ko $(TARGET_DIR)/lib/modules/pwn.ko
endef

$(eval $(generic-package))

src/Makefile:

obj-m := pwn.o

src/pwn.c:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEV "pwn"

static int __init p_init(void){
    pr_info("pwn loaded\n");
    return 0;
}
static void __exit p_exit(void){
    pr_info("pwn unloaded\n");
}
module_init(p_init);
module_exit(p_exit);
MODULE_LICENSE("GPL");

最后,还需要在 package/Config.in 中添加一行 source "package/pwn_kmod/Config.in"

然后,编译后的 kmod 文件就会位于 /lib/modules/ 目录下,可以在 /init 里自行加载。

注意,有时候编译内核模块 package 时,还没有编译好 linux,会导致编译失败。由于我懒得研究怎么添加依赖,所以遇到这个错直接 make linux 就行。

Buildroot 使用技巧

因为 buildroot 一个主要用途是异架构环境配置,他会 build 一整套编译工具链,耗时非常长。所以,当修改了某个部分的配置想要重新编译时,千万不要执行 make clean

  • 当想要完全重新 build 一个包 from scratch,最好的做法是删除 output/build 下对应的目录(或者使用 make <package>-dirclean 指令),然后重新 make
  • 当想要重新编译,可以执行 make <package>-rebuild。比如说仅仅通过 make linux-menuconfig 修改了 linux 的配置时,就可以通过 make linux-rebuild 重新构建 linux。
  • 具体细节可以参考 The Buildroot user manual

另外,如果移动了 buildroot 目录,似乎需要完全重新编译,否则会有奇怪的问题!


更多参考: