前言

Pawnyable是一个由ptrYudai开发的Linux内核漏洞利用的入门教程。在这个教程中,作者设计了存在漏洞的Linux内核模块,并结合这些内核模块依次介绍了内核漏洞研究的环境搭建及调试方法、堆栈溢出、释放后重用(use after free,简称UAF)、竞态条件(race condition)、空指针解引用(NULL pointer dereference)、双重取回(double fetch)等多种漏洞类型,和滥用userfaultfd、滥用FUSE等漏洞利用方法。除此之外,作者还介绍了针对内核eBPF验证器的攻击。另外,还有一个关于脆弱mmap实现的部分,作者尚未发布。

Pawnyable项目的首页目录可以看出,除了Linux内核漏洞利用外教程,作者还计划了Linux用户态漏洞利用、Windows用户态漏洞利用、Windows内核漏洞利用、浏览器漏洞利用、虚拟机逃逸和Android/iOS漏洞利用等多个系列,覆盖了相当一部分系统安全和二进制安全的研究领域,但目前尚未发布。

感谢ptrYudai的无私分享!虽然该系列教程全部使用日语,我们可以使用Google翻译提供的网站动态翻译功能来阅读该教程的中文版本英文版本

“Linux Kernel PWN | 04”系列文章是我针对此教程的学习笔记,文章结构与原教程基本保持一致,也会补充一些学习过程中获得的额外知识。我们将该课程涉及到的题目文件及介绍汇总如下:

漏洞虚拟机环境 覆盖范围
LK01: Holstein v1 栈溢出
LK01-2: Holstein v2 堆溢出
LK01-3: Holstein v3 UAF
LK01-4: Holstein v4 竞态条件
LK02: Angus NULL Pointer Dereference
LK03: Dexter Double Fetch
LK04: Fleckvieh 滥用userfaultfd和FUSE
LK05: Highland 作者尚未发布,本系列文章暂不涉及
LK06: Brahman eBPF相关漏洞利用

本文介绍了Linux内核漏洞利用的基础知识、环境搭建和调试的方法。其中的部分内容我曾在其他文章中有过介绍和总结,因此将不再详细记录这些部分,并给出相关的博文链接供参考。

1. 内核漏洞利用基础知识与环境搭建

我曾在《Linux Kernel PWN | 01 From Zero to One》《关于漏洞研究的一些想法》中对Linux内核漏洞利用以及它与Linux用户态漏洞利用的异同做过介绍。Linux内核中的攻击面可以粗略分为两大类:内核本身和内核模块(如设备驱动程序)。总体来看,内核态漏洞利用与用户态漏洞利用的最大区别是目的不同:前者的目的通常是权限提升,后者的目的通常是命令执行。但这不是绝对的,部分内核漏洞能够被远程触发,实现RCE;部分高权限用户态程序的漏洞也可以用于权限提升。

除此之外,作者提到了另一个区别:内核态的资源和堆是内核和所有模块共享的,用户态进程则拥有自己的资源和堆栈空间。内核态的共享特性对于攻击者来说有利有弊,好处在于拥有相当多的可利用对象,坏处在于堆状态会受到所有程序影响,更难于控制和预测。因此,堆喷(heap spraying)是一个非常重要的内核漏洞利用技术。

在真实的内核漏洞利用场景中,我们需要在目标系统上直接运行漏洞利用程序(后文简称ExP)来实现权限提升。然而,在漏洞研究的过程中,使用QEMU启动一个调试环境比较方便。我在《Linux Kernel PWN | 03 Debugging Env. Setup》中讲解了如何搭建内核调试环境,与ptrYudai介绍的方法差不多,不再详细介绍。

在本小节的最后,我们尝试启动并探索一下本系列教程的第一个漏洞环境LK01

[ Holstein v1 (LK01) - Pawnyable ]
/ $ id
uid=1337 gid=1337 groups=1337
/ $ uname -a
Linux zer0pts 5.10.7 #1 SMP PREEMPT Wed Oct 6 21:20:36 JST 2021 x86_64 GNU/Linux

如果想要以root身份启动,可以将rootfs/etc/init.d/S99pawnyablesetsid命令的参数1337改为0,然后再次打包rootfs启动即可。

2. 使用GDB进行内核调试

我在《Linux Kernel PWN | 03 Debugging Env. Setup》中曾简单介绍了如何使用GDB进行内核调试,ptrYudai介绍得更为详细。下面我们按照ptrYudai的步骤复现一遍。

首先将rootfs/etc/init.d/S99pawnyable中的kptr_restrict所在行注释掉并重新打包rootfs,从而允许root用户通过/proc/kallsyms查看内核导出符号地址。

接着在run.sh中添加-s选项开启GDB调试服务,然后启动虚拟机。我们先看一下内核加载基址,并获得commit_creds函数地址:

/ # head -n 3 /proc/kallsyms
ffffffff81000000 T startup_64
ffffffff81000000 T _stext
ffffffff81000000 T _text
/ # grep "commit_creds" /proc/kallsyms
ffffffff8106e390 T commit_creds

在虚拟机外启动GDB,连接并进行配置。需要注意的是,如果使用了GEF,上述命令可能会导致报错,可参考Kiprey的文章来处理。与原教程相同,本文使用pwndbg作为GDB的增强框架。

pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
...
pwndbg> set arch i386:x86-64:intel
The target architecture is set to "i386:x86-64:intel".

此时,我们可以在GDB中为commit_creds函数设置断点:b *0xffffffff8106e390,然后输入c让系统继续运行。接着在虚拟机终端中执行一个命令,如ls。自然,控制流被GDB中断,可以进行调试了:

pwndbg> b *0xffffffff8106e390
Breakpoint 1 at 0xffffffff8106e390
pwndbg> c
Continuing.

Breakpoint 1, 0xffffffff8106e390 in ?? ()

以上即为基本的内核调试步骤。下面我们调试一下LK01中的漏洞内核模块。可以看到,该模块已经被加载到0xfffffffc0000000处:

/ # cat /proc/modules
vuln 16384 0 - Live 0xffffffffc0000000 (O)

我们可以在GDB中将内核模块文件vuln.ko作为符号文件加载,然后使用符号(如下面的module_close)作为断点。在虚拟机终端中执行cat /dev/holstein后控制流将会运行到断点处,触发断点:

pwndbg> add-symbol-file vuln.ko 0xffffffffc0000000
add symbol table from file "vuln.ko" at
	.text_addr = 0xffffffffc0000000
Reading symbols from vuln.ko...
warning: remote target does not support file transfer, attempting to access files from local filesystem.
(No debugging symbols found in vuln.ko)
pwndbg> b module_close
Breakpoint 1 at 0xffffffffc0000213
pwndbg> c
Continuing.

Breakpoint 1, 0xffffffffc0000213 in module_close ()

至此,我们分别介绍了如何调试内核及内核模块,其他操作与用户态调试没有太大区别,基本的漏洞调试方法介绍完毕。

3. 内核常见安全机制简介

我们在《Linux Kernel PWN | 01 From Zero to One》已经简单介绍过SMEP/SMAP、KPTI、KASLR/FG-KASLR等缓解措施的相应绕过技术,但对缓解措施本身并没有过多关注。不过,在更早的《容器环境相关的内核漏洞缓解技术》中,我们介绍了五种内核漏洞缓解措施。这两篇文章刚好覆盖了原教程提到的安全机制,因此这里不再过多阐述。

4. 编译和传输漏洞ExP

与原教程类似,我也推荐使用脚本来完成ExP的编译与打包操作,具体可见《Linux Kernel PWN | 01 From Zero to One》的相关部分。另外,ptrYudai提到,远程上传ExP(如CTF场景)有时需要先将其进行Base64编码,然后在远端环境解码运行。在这种情况下,我们不希望ExP过大,此时可以使用musl项目对ExP进行编译。

总结

本文对Linux内核漏洞利用环境搭建、安全机制和调试方法进行了简单回顾。本系列后续文章将按照Pawnyable课程设置逐步完成。