0x2 内核的简单食用异闻录

0x0 依赖

1
2
sudo apt-get update
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev

虽然但是,我记得即使这里装了qemu,后面还要自己装个x86-64的来着

pwn相关:

hxd写了个一键安装脚本

不过好像有bug,要自己手动装一下pwndbg

好像是因为太新了

0x1 获取内核镜像

大概有如下三种方式:

  • 下载内核源码后编译
  • 直接下载现成的的内核镜像,不过这样我们就不能自己魔改内核了2333
  • 直接使用自己系统的镜像

arttnba3师傅介绍了三种,但是我只用了第一种

I.获取内核源码

前往Linux Kernel Archive下载对应版本的内核源码

II.配置编译选项

解压我们下载来的内核源码

1
$ tar -xvf linux-5.11.tar.xz

完成后进入文件夹内,执行如下命令开始配置编译选项

1
$ make menuconfig

进入如下配置界面

image-20240924150028795

保证勾选如下配置(默认都是勾选了的):

  • Kernel hacking —> Kernel debugging
  • Kernel hacking —> Compile-time checks and compiler options —> Compile the kernel with debug info
  • Kernel hacking —> Generic Kernel Debugging Instruments –> KGDB: kernel debugger
  • kernel hacking —> Compile the kernel with frame pointers

最后这个 Compile the kernel with frame pointers,可能会找不到,需要修改配置文件lib/Kconfig.debug

1
2
3
Symbol: ARCH_WANT_FRAME_POINTERS [=y]
Type : bool
│ Defined at lib/Kconfig.debug:383

把这个符号给添加上一行(默认是没有这行的):

image-20240924150422254

添加上了之后,就能找到:kernel hacking —> Compile the kernel with frame pointers,应该会是开启的状态

通常保存的路径在当前目录下的 .config 文件中,如果你在生成配置文件后才想起来忘了改某个选项也可以直接编辑这个文件

III.开始编译

运行如下命令开始编译,生成内核镜像

1
$ make -j$(nproc) bzImage

IV.可能出现的错误

我编译时也出现过这个错误:

1
make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'.  Stop

只需要在 .config 文件中找到 CONFIG_SYSTEM_TRUSTED_KEYS,等于号后面的值改为 ""

V.编译结果

完成之后会出现如下信息:

1
Kernel: arch/x86/boot/bzImage is ready  (#1)

我们主要关注生成的这两个文件:

vmlinux:原始内核文件

在当前目录下提取到vmlinux,为编译出来的原始内核文件

1
2
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=f1fc85f87a5e6f3b5714dad93a8ac55fa7450e06, with debug_info, not stripped

bzImage:压缩内核镜像

在当前目录下的arch/x86/boot/目录下提取到bzImage,为压缩后的内核文件,适用于大内核

1
2
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 5.11.0 (root@iZf3ye3at4zthpZ) #1 SMP Sun Feb

顺带一提,我是在wsl里面编译的,但是在/mnt/的挂载路径中好像会出现编译问题,但是换到用户路径下就一起正常了。

↓看起来是有用的情报

zImage && bzImage

zImage–是vmlinux经过gzip压缩后的文件。
bzImage–bz表示“big zImage”,不是用bzip2压缩的,而是要偏移到一个位置,使用gzip压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个 640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。

https://blog.csdn.net/xiaotengyi2012/article/details/8582886

EXTRA.添加系统调用

1.分配系统调用号

arch/x86/entry/syscalls/syscall_64.tbl中添加我们自己的系统调用号,这里用CTF常见数字114514

1
114514    64    3zureus_test        sys_3zureus_test

2.声明系统调用

include/linux/syscalls.h中添加如下函数声明:

1
2
/* for 3zureus's personal syscall test */
asmlinkage long sys_3zureus_test(void);

3.添加系统调用函数定义

kernel/sys.c中添加如下代码(放置于最后一行的#endif /* CONFIG_COMPAT */之前):

1
2
3
4
5
SYSCALL_DEFINE0(3zureus_test)
{
printk("3zureus\' syscall has been called!\n");
return 114514;
}

这里的SYSCALL_DEFINE0()本质上是一个宏,意为接收0个参数的系统调用,其第一个参数为系统调用名

这里定义了一个简单的输出一句话的系统调用,在这里使用了内核态的printk()函数,输出的信息可以使用dmesg进行查看

4.重新编译内核

这一步参照之前的步骤即可,通过这一步我们要将我们自己的系统调用编译到内核当中

5.测试系统调用

我们使用如下的例程测试我们的新系统调用

1
2
3
4
5
6
#include <unistd.h>
int main(void)
{
syscall(114514);
return 0;
}

编译,放入磁盘镜像中后重新打包,qemu起内核后尝试运行我们的例程,结果如下:

因为dmesg输出的东西太多,这里还附加用了grep命令过滤

。。。?为啥会no found

9d031f4ef6497f8cc421211e4444517

破案了,因为缺少libc的库,在编译的时候补上-static参数即可

image-20240924201607257

0x2 使用 busybox 构建文件系统

BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件,包含了例如ls、cat和echo等一些简单的工具,我们将用 busybox 为我们的内核提供一个基本的用户环境

没啥好说的,全抄!

一、编译busybox

I.获取busybox源码

busybox.net下载自己想要的版本,笔者这里选用busybox-1.33.0.tar.bz2这个版本

1
$ wget https://busybox.net/downloads/busybox-1.33.0.tar.bz2

外网下载的速度可能会比较慢,可以在前面下载Linux源码的时候一起下载,也可以选择去国内的镜像站下载

解压

1
$ tar -jxvf busybox-1.33.0.tar.bz2

II.编译busybox源码

进入配置界面

1
$ make menuconfig

勾选 Settings —> Build static binary file (no shared lib)

若是不勾选则需要单独配置 libc,比较麻烦

接下来就是编译了,速度会比编译内核快很多

1
$ make install

编译完成后会生成一个_install目录,接下来我们将会用它来构建我们的磁盘镜像

二、建立文件系统

I.初始化文件系统

一些简单的初始化操作…

1
2
3
4
5
6
$ cd _install
$ mkdir -pv {bin,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
$ touch etc/inittab
$ mkdir etc/init.d
$ touch etc/init.d/rcS
$ chmod +x ./etc/init.d/rcS

II.配置初始化脚本

首先配置 etc/inttab ,写入如下内容:

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

在上面的文件中指定了系统初始化脚本,因此接下来配置 etc/init.d/rcS,写入如下内容,主要是挂载各种文件系统:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

poweroff -d 0 -f

也可以在根目录下创建 init 文件,写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

别忘了添加可执行权限:

1
$ chmod +x ./init

III.配置用户组

1
2
3
4
5
$ echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
$ echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
$ echo "root:x:0:" > etc/group
$ echo "ctf:x:1000:" >> etc/group
$ echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab

在这里建立了两个用户组rootctf,以及两个用户rootctf

IV.配置glibc库

将需要的动态链接库拷到相应位置即可

为了方便笔者这里就先不弄了,直接快进到下一步,以后有时间再补充(咕咕咕

听hxd说有个配好的,看看,等他有点时间发出来

三、打包文件系统为镜像文件

I. 打包为 cpio 文件

使用如下命令打包文件系统为 cpio 格式

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

也可以这么写

1
$ find . | cpio -o -H newc > ../core.cpio

II. 打包为 ext4 镜像

这里也可以将文件系统打包为 ext4 镜像格式,首先创建空白 ext4 镜像文件,这里 bs 表示块大小,count 表示块的数量:

1
$ dd if=/dev/zero of=rootfs.img bs=1M count=32

之后将其格式化为 ext4 格式:

1
$ mkfs.ext4 rootfs.img 

挂载镜像,将文件拷贝进去即可:

1
2
3
4
$ mkdir tmp
$ sudo mount rootfs.img ./tmp/
$ sudo cp -rfp _install/* ./tmp/
$ sudo umount ./tmp

四、向文件系统中添加文件

若是我们后续需要向文件系统中补充一些其他的文件,可以选择在原先的_install文件夹中添加(不过这样的话若是配置多个文件系统则会变得很混乱),也可以解压文件系统镜像后添加文件再重新进行打包

cpio 文件

I.解压磁盘镜像

1
$ cpio -idv < ./rootfs.cpio

该命令会将磁盘镜像中的所有文件解压到当前目录下

II.重打包磁盘镜像

和打包磁盘镜像的命令一样

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

ext4 镜像

直接 mount 后再 umount jike:

1
2
3
$ sudo mount rootfs.img ./tmp/
$ # do something
$ sudo umount ./tmp

0x3 使用qemu运行内核

终于到了最激动人心的时候了:我们即将要将这个Linux内核跑起来——用我们自己配置的文件系统与内核

安全起见,我们并不直接在真机上运行这个内核,而是使用qemu在虚拟机里运行

配置启动脚本

I. 使用 cpio 文件作为文件系统

首先将先前的bzImagerootfs.cpio放到同一个目录下

接下来编写启动脚本

1
$ touch boot.sh

写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s

部分参数说明如下:

  • -m:虚拟机内存大小

  • -kernel:内存镜像路径

  • -initrd:磁盘镜像路径

  • append:附加参数选项

    • nokalsr:关闭内核地址随机化,方便我们进行调试
    • rdinit:指定初始启动进程,/sbin/init进程会默认以 /etc/init.d/rcS 作为启动脚本
    • loglevel=3 & quiet:不输出log
    • console=ttyS0:指定终端为/dev/ttyS0,这样一启动就能进入终端界面
  • -monitor:将监视器重定向到主机设备/dev/null,这里重定向至null主要是防止CTF中被人给偷了qemu拿flag

  • -cpu:设置CPU安全选项,在这里开启了smep保护

  • -s:相当于-gdb tcp::1234的简写(也可以直接这么写),后续我们可以通过gdb连接本地端口进行调试

运行boot.sh,成功启动~撒花~🌸🌸🌸

II. 使用 ext4 镜像作为文件系统

编写如下启动脚本即可,实际上只是将 -initrd 换成了 -hda,这里也可以写成 -drive file=./rootfs.img,format=raw

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-hda ./rootfs.img \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr pti=on quiet oops=panic panic=1" \
-no-reboot \