Contents

First step to linux kernel pwn

First step to linux kernel pwn

将根据笔者学习进度逐步更新,作为记录和总结。

update:这段时间在搞其他的,可能会鸽一阵

前置知识

linux kernel binary

linux内核也是一个程序,其二进制文件通常以两种形式呈现:vmlinux和bzImage。使用file命令可以快捷地区分:

1
2
❯ file ./bzImage
./bzImage: Linux kernel x86 boot executable bzImage, version 4.15.8 (simple@vps-simple) #19 SMP Mon Mar 19 18:50:28 CST 2018, RO-rootFS, swap_dev 0X6, Normal VGA
1
2
❯ file ./vmlinux
./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1d8344e71a82bc43821029796ef65bebfe8e65c3, not stripped

vmlinux

普通ELF文件,也是内核源码编译出来的原始文件。在分析时可以直接载入gdb获取符号信息、函数地址信息等等。

bzImage

vmlinux原始文件经过objcopy和再次链接等步骤,最终进行压缩得到的文件。这里b是big的首字母,所以其实也有zImage,适用于小内核,bzImage即大内核。

在分析时无法直接使用,需要先解压提取出vmlinux,这里可以使用shell脚本extract-linux,缺点是丢失了符号信息。更好的替代品是vmlinux-to-elf,大多数情况下都可以完美还原符号信息。

另外在/boot 目录下,还能找到一个vmlinuz-* 文件,实际是bzImage格式,例如在笔者机器上如下:

1
2
❯ file /boot/vmlinuz-linux-zen
/boot/vmlinuz-linux-zen: Linux kernel x86 boot executable bzImage, version 6.7.7-zen1-1-zen (linux-zen@archlinux) #1 ZEN SMP PREEMPT_DYNAMIC Fri, 01 Mar 2024 15:07:23 +0000, RO-rootFS, swap_dev 0XD, Normal VGA

qemu基本使用

qemu是一个通用开源模拟器和虚拟机软件,通俗来说,和通常运行虚拟机所用的vmware、virtualbox是差不多的东西。qemu的突出特点就是在不需要图形界面场景中的方便易用,且可以纯软件模拟执行不同CPU架构的指令代码。在kernel pwn题目中,基本都会使用qemu加载特定内核部署题目环境。

user mod & system mod

qemu的使用通常有两种模式,即user mode和system mode。

kernel pwn题目中一般使用system mode,会模拟整个操作系统,使用的命令一般是qemu-system-x86_64,即x64架构的qemu虚拟机。其他架构的还有qemu-system-mips、qemu-system-riscv64等等。

user mode不会模拟整个操作系统,只会进行相应指令集的指令模拟,所以一般被用来执行二进制程序。比如下面是一个在x86机器上运行riscv程序的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
❯ file ./chall
./chall: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, for GNU/Linux 4.15.0, with debug_info, not stripped

❯ ./chall
exec: Failed to execute process: './chall' the file could not be run by the operating system.

❯ qemu-riscv64 ./chall
Welcome to moeCTF 2022
Plz input your flag:
flag{test}
ops, wrong input!
Please try again⏎ 

qemu启动脚本

一个典型的启动脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

qemu-system-x86_64 \
-initrd core.cpio \
-kernel bzImage \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 kaslr' \
-enable-kvm \
-monitor /dev/null \
-m 128M \
--nographic  \
-smp cores=1,threads=1 \
-cpu kvm64,+smep \
-s

-initrd:指定init ram disk,一般就是文件系统

-kernel:指定内核

-append:指定内核命令行参数,需要关注启用的内核保护,比如这里只启用了kaslr保护

-enable-kvm:使用kvm虚拟化

-monitor /dev/null:把monitor重定向到/dev/null

-m 128M:分配128M内存

–nographic:不使用图形界面

-smp cores=1, threads=1:设置CPU为单核单线程

-cpu kvm64, +smep:使用kvm64虚拟化cpu,开启smep保护

-s:开启gdb server,默认监听1234端口,方便调试

文件系统

qemu虚拟机加载了内核,还需要文件系统才能实现完整的操作系统功能。linux通常有两类使用文件系统的方式。

initrd & hda

一种是init ram disk(initrd),实际本身是用于在真正的硬盘文件系统挂载前临时使用的,作为内核引导过程的一部分,所有文件内容都会被放入内存,在qemu中可以使用-initrd参数指定。另一种方式则是挂载一个硬盘/软盘等存储设备,使用qemu模拟时使用-hda参数指定模拟一个硬盘设备作为文件系统。

cpio archive

cpio是一种压缩文件格式,常用来作文件备份,也可以拿来打包文件系统,使用cpio相关命令即可方便地打包/解包。cpio文件一般是-initrd的参数

常用打包解包命令:

1
2
3
4
5
6
# 打包
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
# 解包
cpio -idv < ./rootfs.cpio

ext4 image

ext4是一种文件系统格式,目前已被linux系统广泛应用。使用ext4更像是使用一个磁盘,可以使用mount/umount命令方便地挂载/取消挂载,向其中添加/更改文件。ext4 image一般是-hda的参数。

可装载内核模块

可装载内核模块即Loadable Kernel Module(LKM),一般简称为内核模块,能够运行在内核态为用户态提供服务。模块是可插拔的,使用insmod、rmmod、lsmod可以增、删、查询模块。在未开启kptr_restrict保护时,lsmod可以查看到模块的内存加载地址,方便调试时手动指定基址。

在kernel pwn题目中,通常都会提供一个有漏洞的LKM,用户程序通过i/o设备或者procfs与LKM通信,攻击者构造恶意数据触发漏洞,从而达到提权或者其他恶意目的。

分析LKM时,应首先定位init_modulecleanup_module函数,二者是LKM的入口函数和退出函数,相当于类的构造和析构函数。

I/O设备

linux哲学强调一切皆文件,i/o设备也不例外。使用i/o设备的通信,简单说只是对i/o设备文件使用open、read、write三个系统调用完成的, 这些i/o设备文件都放在/dev下。

开发者可以创建自己的io设备,同时对于新创建的设备,实现对应的打开、读、写、关闭等回调函数,预定义的这些函数不够用时,linux还设计了ioctl系统调用,可以针对自定义的行为定义回调函数,以ioctl的调用号做区分,实现高定制性的功能。

要实现回调的能力,势必需要有个结构体存储各种回调函数指针,这里就使用了file_operations这个或类似的结构体。这个逻辑就跟用户态FILE结构体中的指针、__free_hook、__malloc_hook这些东西别无二致了,这也就带来了潜在的攻击面。

procfs

进程文件系统(process file system),即/proc文件夹下的内容,描述了一个进程的运行状态,包括命令行参数、环境变量、进程内存布局等很多信息。这些内容实际存储在内存中,并不占用硬盘空间,故为伪文件系统。

开发者也可以使用API自行创建proc文件,定义相应回调函数,这就和i/o设备相差无几。

攻击面与利用手法

kernel pwn与用户态攻击面基本一致,主要还是针对栈、堆的攻击,比较有特色的是条件竞争(race condition)的攻击可能性增加,其余像整数溢出、数组越界依旧容易被利用。

攻击思路

针对kernel的攻击大多还是本地提权,各种提权手段,大致可以归结为以下三种思路:

修改cred结构体直接提权

kernel v4.5 cred结构体

 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
struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC    0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    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 __rcu *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;    /* subjective 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 group_info *group_info;    /* supplementary groups for euid/fsgid */
    struct rcu_head    rcu;        /* RCU deletion hook */
};

只要将前28位置0,即覆盖前7项即可完成提权。具体做法一般是UAF释放和cred结构体相同大小的堆块,然后使用fork函数分配一个cred结构体刚好到这个堆块上,然后改值即可。

但是这种利用手法在kernel v4.5及以上版本中,由于引入了SLAB_ACCOUNT,cred结构体不可能分配到我们释放的堆块,故这条路基本堵死,关于这一点会在下面说内核保护时详细介绍。

构造ROP链提权并着陆用户态

由于linux kernel足够大足够复杂,比较容易找到各种gadget,这就有了构造ROP链的基础,下一步就是如何控制程序执行流的问题。

如果有栈溢出漏洞,自然直接覆盖返回地址即可。如果有堆上漏洞,就要考虑伪造特殊结构体,覆盖其中的函数指针,然后布置gadget进行栈迁移即可执行ROP链,这根用户态堆漏洞很多house of利用手法思路一致。常利用的结构体可以看【PWN.0x02】Linux Kernel Pwn II:常用结构体集合 - arttnba3‘s blog

ROP链中一般使用commit_creds(prepare_kernel_cred(NULL))进行提权,本质上也是使用gadget修改了本进程的cred结构体。如果没有SMEP、SMAP保护或者可以修改cr4寄存器绕过,也可以直接ret2usr,即通过ROP执行用户代码更方便地提权,但在开启KPTI保护之后,ret2usr基本失效。

提权之后为了更好地利用,一般会返回用户态执行system("/bin/sh")。从内核返回到用户态一般需要先使用swapgs恢复GS寄存器,然后使用iret或sysret返回用户态,使用这两个指令同时需要布置栈帧,比如iret返回的同时还会从栈上pop出4个值恢复用户态寄存器cs、eflags、sp、ss。为了防止非法寄存器值导致程序崩溃,最好在用户态执行一开始保存一份寄存器值方便后面使用。参考代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 保存寄存器值到变量中
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
// 返回用户态时布置栈帧:
   swapgs
    iretq
    user_shell_addr
    user_cs
    user_eflags //64bit user_rflags
    user_sp
    user_ss

以上代码截取自Kernel ROP - CTF Wiki

任意地址写劫持全局静态指针

如果能直接获得任意地址读写的能力,那么可以直接泄漏内核基址、劫持全局静态指针一套带走了,当然如果有额外的内核保护无法修改那就没办法了。

常用的可劫持的是modprobe_path,正常来说这个变量存着字符串/sbin/modprobe

动态内存分配 — slub系统

动态内存分配 — buddy系统

保护措施与绕过

地址随机化

kaslr

fgkaslr

隔离

SMEP

SMAP

KPTI

信息限制

dmesg_restrict

kptr_restrict

栈保护

cookie(canary)

堆保护

堆块分配隔离

GFP_KERNEL & GFP_KERNEL_ACCOUNT
SLAB_ACCOUNT

[PATCH v2 4/6] slab: add SLAB_ACCOUNT flag - Vladimir Davydov

Hardened Usercopy

Hardened freelist

Random freelist

CONFIG_INIT_ON_ALLOC_DEFAULT_ON

Reference

【OS.0x01】Linux Kernel II:内核简易食用指北 - arttnba3’s blog

【OS.0x00】Linux Kernel I:Basic Knowledge - arttnba3‘s blog

【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3‘s blog

Kernel再入门 | A1ex‘s Blog

Linux内存管理(一) · Linux kernel脉络和主干总结 · 看云