kernel与Qemu模拟环境

ctf-wiki上学习一下kernel。内核主要有以下几个类别

  • Prepatch
  • Mainline
  • Stable
  • Longterm

kernel

内核的下载

更好的学习kernel pwn选择长期支持版。选择 5.4 的版本 5.4.98来学习下载链接:https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/[linux-5.4.98.tar.sign](https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.4.98.tar.sign)

1
2
3
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.4.98.tar.xz
$ unxz linux-5.4.98.tar.xz
$ tar -xf linux-5.4.98.tar

wiki上还有验证内核签名,这里就不进行验证了。解压成功之后我们就可以得到内核源码了

内核的编译

在编译之前我们需要配置内核编译选项。make menuconfig这里我们主要关注调试方面的选项,依次进入到 Kernel hacking -> Compile-time checks and compiler options,然后勾选如下选项Compile the kernel with debug info,以便于调试。不过似乎现在是默认开启的。

如果要使用 kgdb 调试内核,则需要选中 KGDB: kernel debugger,并选中 KGDB 下的所有选项。

1
$ sudo make -j 4  bzImage

这个中间出了好多错,百度一下就行:)。最后成功的界面是这样子的。

在编译成功后我们需要注意的是:

  • bzImage:arch/x86/boot/bzImage
  • vmlinux:源码所在的根目录下
  • bzImage:目前主流的 kernel 镜像格式,即 big zImage(即 bz 不是指 bzip2),适用于较大的(大于 512 KB) Kernel。这个镜像会被加载到内存的高地址,高于 1MB。bzImage 是用 gzip 压缩的,文件的开头部分有 gzip 解压缩的代码,所以我们不能用 gunzip 来解压缩。
  • zImage:比较老的 kernel 镜像格式,适用于较小的(不大于 512KB) Kernel。启动时,这个镜像会被加载到内存的低地址,即内存的前 640 KB。zImage 也不能用 gunzip 来解压缩。
  • vmlinuz:vmlinuz 不仅包含了压缩后的 vmlinux,还包含了 gzip 解压缩的代码。实际上就是 zImage 或者 bzImage 文件。该文件是 bootable 的。 bootable 是指它能够把内核加载到内存中。对于 Linux 系统而言,该文件位于 /boot 目录下。该目录包含了启动系统时所需要的文件。
  • vmlinux:静态链接的 Linux kernel,以可执行文件的形式存在,尚未经过压缩。该文件往往是在生成 vmlinuz 的过程中产生的。该文件适合于调试。但是该文件不是 bootable 的。
  • vmlinux.bin:也是静态链接的 Linux kernel,只是以一个可启动的 (bootable) 二进制文件存在。所有的符号信息和重定位信息都被删除了。生成命令为:objcopy -O binary vmlinux vmlinux.bin
  • uImage:uImage 是 U-boot 专用的镜像文件,它是在 zImage 之前加上了一个长度为 0x40 的 tag 而构成的。这个 tag 说明了这个镜像文件的类型、加载位置、生成时间、大小等信息。

编译内核驱动

这里尝试编译一个内核驱动。源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("Dual BSD/GPL");
static int ko_test_init(void) {
printk("This is a test ko!\n");
return 0;
}
static void ko_test_exit(void) {
printk("Bye Bye~\n");
}
module_init(ko_test_init);
module_exit(ko_test_exit);

Makefile文件如下:

1
2
3
4
5
6
7
8
9
obj-m += 1.o

KDIR =/home/xuxu/stu_ha/ctf_range/kernel_pwn/linux-5.4.98

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order

这里对table等字符非常严格,说一下makefile吧:

obj-m 指定要声称哪些模块

KDIR 用来标识内核源码目录,提供驱动编译所需环境

$(MAKE) -C $(KDIR) M=$(PWD) modules

-C 表示进入到指定的内核目录

M 指定驱动源码的环境,M 并不是 Makefile 的选项,而是内核根目录下 Makefile 中使用的变量。这会使得该 Makefile 在构造模块之前返回到 M 指定的目录,并在指定的目录中生成驱动模块

Qemu 模拟环境

使用 QEMU 来搭建调试分析环境。为了使用 qemu 启动和调试内核,我们需要内核、qemu、文件系统

qemu 是一款由 Fabrice Bellard 等人编写的可以执行硬件虚拟化的开源托管虚拟机,具有运行速度快(配合 kvm),跨平台等优点。qemu 通过动态的二进制转化模拟 CPU,并且提供一组设备模型,使其能够运行多种未修改的客户机OS。

在 CTF 比赛中,qemu 多用于启动异架构的程序(mips, arm 等)、kernel 及 bootloader 等二进制程序,有时也会作为要 pwn 掉的程序出现。

准备

首先是内核,已经完成了。接下来是qemu

1
$ sudo apt install qemu

qemu也搞定之后最后是文件系统。使用 busybox 来构建一个简单的文件系统。

1
2
$ wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
$ tar -jxf busybox-1.32.1.tar.bz2

接下来进行配置。

1
$ make menuconfig

在 Setttings 选中 Build static binary (no shared libs),将 busybox 编译为静态链接的文件;在 Linux System Utilities 中取消选中 Support mounting NFS file systems on Linux < 2.6.23 (NEW);在 Networking Utilities 中取消选中 inetd。

编译

1
$ make -j3

配置文件系统

在编译完成后,我们在 _install 目录下创建以下文件夹

1
$ mkdir -p  proc sys dev etc/init.d

并创建 init 作为 linux 的启动脚本,内容为下面。其中setsid我给的权限是root

1
2
3
4
5
6
7
8
9
10
#!/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"
setsid /bin/cttyhack setuidgid 0 /bin/sh

打包文件系统

1
$ find . | cpio -o --format=newc > ../rootfs.img

启动内核

使用脚本来启动,这里面的bzImage也就是之前内核里的bzImage。rootfs.img是打包文件系统时创建的。

1
2
3
4
5
6
7
8
9
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kalsr" \
-smp cores=2,threads=1 \
-cpu kvm64

接下来运行这个脚本就可以成功启动了。

加载驱动

之前写了一个驱动,我们将那个驱动放到_install目录下。改一下init脚本

1
2
3
4
5
6
7
8
9
10
11
#!/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
insmod /1.ko
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 0 /bin/sh

再次启动一下内核吧。可以看到成功加载驱动了。

调试分析

基本操作

查看装载的驱动

1
lsmod

获取驱动加载的基地址

1
2
3
4
# method 1
grep target_module_name /proc/modules
# method 2
cat /sys/module/target_module_name/sections/.text

启动调试

想要调试需要在启动脚本里加上一个-s

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kalsr" \
-smp cores=2,threads=1 \
-cpu kvm64 \
-s

qemu 其实提供了调试内核的接口,我们可以在启动参数中添加 -gdb dev 来启动调试服务。最常见的操作为在一个端口监听一个 tcp 连接。 QEMU 同时提供了一个简写的方式 -s,表示 -gdb tcp::1234,即在 1234 端口开启一个 gdbserver。

1
$ gdb -q -ex "target remote localhost:1234"