用户态视角理解内核ROP利用:快速从shell到root的进阶
用户态视角理解内核ROP利用:快速从shell到root的进阶
一、摘要
本文仅限于快速从用户态向内核态入门,可能会有很多不严谨的地方,存在问题请及时告知感谢!本文旨在通过对比用户态 ROP 利用和内核 ROP 利用,揭示两者在利用手法上的相似性。通过分析用户态漏洞利用的流程,结合内核漏洞利用的特点,引导读者理解内核 ROP 利用的本质,从而更快地掌握内核漏洞利用技术。
本文主要是要为了帮助大部分人去克服学习内核利用Kernel_pwn前的枯燥无聊的前置知识的学习,虽然本文很长但是只需要跟着实际操作就可以快速学习完毕,本文直接以用户态和内核态的对比利用来讲解,在实际操作中讲解知识点,其实本文可以直接从这部分开始看:五、实战以强网杯2018 - core内核Pwn题
之后再回来看基础知识!学会如何进行Linux内核调试和驱动文件的漏洞挖掘!!!
无论是在内核还是用户态中,漏洞利用的本质是通过构造特定输入,触发程序的异常执行路径。其核心逻辑包含三个必要要素:
- 输入交互:必须存在外部可控的输入通道,例如网络数据包、文件内容或用户输入界面。
- 漏洞触发点:程序中存在未经验证的内存操作(存在栈溢出漏洞),导致控制流可能被劫持。
- 权限提升路径:通过修改关键数据结构(返回地址、函数指针等),建立从漏洞触发到目标代码执行的完整链路。
攻击的终极目标是控制程序执行流。当攻击者能够通过漏洞改写EIP/RIP寄存器或劫持函数指针时,即可强制程序跳转至指定内存区域(如Shellcode或ROP链),最终实现代码执行。
当内存屏障竖起高墙,黑客如何让代码在荒漠中开花?(ROP(Return-Oriented Programming)的本质是一场精密的指令拼图游戏——从程序的废墟中挖掘代码片段(gadgets),通过栈指针的精准操控,让这些孤立指令像多米诺骨牌般连锁触发,最终突破系统的桎梏!
二、概述:漏洞利用的通用思维框架
1. 用户态和内核态ROP的核心共性
对比维度 | 用户态ROP | 内核态ROP | 核心共性 |
---|---|---|---|
控制流劫持目标 | 劫持进程控制流(如覆盖栈返回地址) | 劫持内核控制流(驱动或内核存在可以利用漏洞) | 均需通过漏洞(溢出、UAF等)劫持控制流 |
Gadget来源 | 用户态程序或动态库(如libc、libc.so) | 内核镜像或内核模块(如vmlinux、.ko文件) | 复用现有代码片段(Gadgets),绕过DEP/NX |
内存布局操控 | 覆盖用户栈/堆内存(如返回地址、函数指针) | 篡改内核栈/堆内存(如内核结构体、任务栈) | 需精确控制内存布局以链式调用Gadgets |
权限与上下文 | 用户态权限(Ring 3),遵循用户空间调用约定 | 内核态权限(Ring 0),需适配内核特权级上下文 | 上下文敏感设计(如寄存器状态、传参方式) |
对抗保护机制 | 绕过用户态ASLR、Stack Canary | 绕过内核态KASLR、SMAP/SMEP(若启用) | 依赖信息泄露获取基址,绕过随机化与检测机制 |
漏洞利用入口 | 常见于应用层漏洞(如栈溢出、格式化字符串) | 多通过系统调用接口(如ioctl、syscall)触发漏洞 | 利用程序逻辑缺陷或内存错误实现初始控制流劫持 |
攻击目标 | 执行Shellcode、劫持应用逻辑 | 实现内核代码执行、关闭安全机制、Rootkit植入、提权 | 最终目标均为实现非授权代码执行或权限提升 |
缓解措施 | DEP/NX、ASLR、Stack Canary | KASLR、SMAP/SMEP、KPTI、内核代码签名 | 均需结合硬件与软件防护机制抵御ROP攻击 |
关键差异总结:
- 权限层级:用户态受限于Ring 3,内核态拥有Ring 0特权,可执行敏感指令(如CR3修改)。
- Gadget范围:内核态需从内核镜像中提取Gadgets,且需处理特权指令(如
swapgs
、sti
)。 - 缓解机制:内核态需额外对抗SMAP/SMEP(阻止用户态内存访问)和KPTI(隔离用户/内核页表)。
- 漏洞入口:用户态漏洞多由应用逻辑引发,内核态漏洞常通过驱动或系统调用触发。
2.用户态和内核态漏洞利用环境差异对比
维度 | 用户态 | 内核态 |
---|---|---|
交互方式 | 通过开放端口、命令行参数等进程间通信 | 依赖系统调用(syscall)、驱动IOCTL接口等底层交互 |
地址空间 | 每个进程拥有独立的虚拟地址空间(受MMU隔离保护) | 所有进程,全局共享的内核地址空间(无隔离性) |
权限级别 | Ring 3,仅能访问受限资源 | Ring 0,可直接操作硬件和系统关键数据结构 |
保护机制 | ASLR、NX、Stack Canary等用户层防护 | Stack Canary、KASLR、SMEP(禁止内核执行用户页)、SMAP(禁止内核访问用户页)等硬件级防护 |
提权目标 | 获取进程权限(如启动/bin/sh) | 突破权限隔离,获取root或SYSTEM级特权 |
攻击脚本 | Python脚本(依赖进程交互接口) | 需编写C/汇编代码直接操作内核数据结构 |
- 地址空间隔离性
- 用户态下,每个进程拥有独立的虚拟地址空间,通过MMU实现内存隔离。漏洞利用通常需要先泄露内存布局(如通过信息泄露漏洞绕过ASLR)。
- 内核态中,所有进程共享同一内核地址空间。攻击者可利用该特性直接修改全局数据结构(如进程凭证cred结构),但需规避KASLR对内核符号的随机化。
- 权限提升路径
- 用户态提权通常通过执行/bin/sh等程序获取当前进程权限,若目标进程本身具有高权限(如SUID程序),则可直接完成提权。
- 内核态攻击需修改当前进程的权限标识(如Linux中的
struct cred
的uid/gid字段),或劫持系统调用表等核心结构,使攻击代码获得Ring 0的执行权限。
- 防护机制对抗
- 用户态需组合使用ROP/JOP等代码复用技术绕过NX,利用堆喷(Heap Spray)对抗ASLR。
- 内核态需处理更严格的硬件防护:例如通过
swapgs
等指令绕过KPTI隔离,或构造ROP链规避SMEP/SMAP对用户空间内存的访问限制。
三、内核态的基础文件系统和内核基础讲解
(一),基础的内核文件说明
1. 内核映像文件(如 vmlinuz)
内核映像文件是 Linux 操作系统的核心,包含内核代码和基本驱动。启动时,它被加载到内存中,解压后执行,负责初始化硬件、设置内存管理,并为用户空间准备环境。
- bzImage: 这是可启动的压缩内核格式,“bz”代表“big zip”,允许内核文件超过传统 512 KB 限制,适合现代系统。
- vmlinux: 未压缩的内核映像,通常用于调试或分析。
- 启动过程: 内核加载后,初始化硬件(如 CPU、内存)、设置中断和设备驱动,然后挂载根文件系统。
2. Initrd/Initramfs
Initrd 和 Initramfs 是启动过程中的临时根文件系统,用于在挂载真实根文件系统前加载必需的驱动和工具。
- initrd(initial ramdisk):内核启动时加载的临时根文件系统,用于在挂载真正的根文件系统前加载必需的驱动模块。
- initramfs: Initrd 的改进版,使用 cpio 格式,直接解压到内存,无需挂载,提供更高效率和灵活性。
- 用途: 内核可能需要特定驱动(如磁盘控制器)才能访问真实根文件系统,Initramfs 提供临时环境加载这些驱动。
(二),了解Qemu模拟运行Linux内核的基本执行流程
QEMU 是一个开源模拟器,广泛用于虚拟化不同架构的操作系统。在启动 Linux 内核时,QEMU 通过 qemu-system-x86 命令初始化虚拟机环境。这一过程包括设置 CPU、内存和设备模拟,确保虚拟机能够运行目标操作系统。
- 通过QEMU启动内核以及加载文件系统:
内核加载与执行: QEMU 加载内核映像文件,通常为 bzImage(压缩的内核)或 vmlinux(未压缩的内核)。根据 QEMU 文档,用户可以通过 -kernel 选项指定内核文件路径。内核加载后,会自动解压(如果为 bzImage),然后开始执行,完成硬件初始化(如设置内存结构、加载驱动)。
- Initramfs 处理过程:
Initramfs(Initial RAM Filesystem)是 Linux 启动过程中的临时根文件系统,通常以 initramfs.cpio 格式存在。根据 Gentoo Wiki,其主要目的是为内核提供一个最小化的用户空间环境,以加载必要的驱动和挂载真实根文件系统。
- 加载与挂载: 内核检测到 Initramfs 文件后,将其解压并挂载为内存中的临时根文件系统(/)。这一过程通过 cpio 归档格式实现,内容包括基本的 shell、工具(如 busybox)和配置文件。
- 作用: Initramfs 特别适用于需要特殊驱动(如 RAID、LVM 或加密文件系统)的系统。例如,Debian Wiki 指出,Initramfs 允许在挂载真实根文件系统之前加载内核模块,增强了启动的灵活性。
- 解释补充: 用户提供的图表正确描述了这一阶段,但需要强调 Initramfs 是启动过程中的关键步骤,尤其在复杂存储配置下(如 USB 启动或加密分区)。
- 用户空间初始化
在 Initramfs 设置完成后,内核会启动 Initramfs 中的 /init 脚本,这是第一个用户空间进程。/init 脚本负责加载必要的内核模块(如 .ko 文件),挂载真实根文件系统,然后启动用户空间的初始化进程(如 systemd 或 shell),完成系统的全面初始化。,根据不同的构建方式shell脚本可以在不同位置。
- 最终整合后的流程如下表
阶段 | 主要动作 | 说明 |
---|---|---|
QEMU 启动内核文件 | 加载内核映像(如 bzImage),解压并执行 | 完成硬件初始化,准备后续步骤 |
Initramfs 处理 | 加载 initramfs.cpio,挂载为临时根文件系统 | 提供最小化环境,加载驱动和工具 |
用户空间初始化 | 执行 /init 脚本,挂载真实根,启动 systemd/shell | 完成系统初始化,进入用户 |
(三),LKMs(可装载内核模块,Loadable Kernel Modules)
1.LKMs 的文件格式:
在linux里面查看该文件:
ub20@ub20:~/KernelStu/RunQemu$ file ../CodeKernelDriver/hello.ko
../CodeKernelDriver/hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=6cc23340ffa46ada1c55861237166888e5676d
文件格式:Linux:采用 ELF(Executable and Linkable Format)格式,后缀通常是 .ko(kernel object)。
LKMs 文件与ELF的区别:
- 用户态程序:ELF 文件可以独立运行(如 ./binary),由用户态加载器解析并执行。
- LKMs:ELF 文件(.ko)不能独立运行,必须通过内核的模块加载机制(如 insmod)加载到内核空间,依赖内核符号表(如 printk、kmalloc)工作。
在不同操作系统上也有类似的机制: - Windows:类似功能的文件是 .exe 或 .dll(动态链接库),但内核模块通常以驱动形式存在(如 .sys)。
- macOS:使用 Mach-O 格式,但 macOS 的内核扩展(KEXT)机制与 LKMs 略有不同。
-
LKMs(可装载内核模块)是什么?
定义:LKMs 是 Linux 内核支持的一种机制,允许在内核运行时动态加载或卸载功能模块,而无需重新编译或重启整个内核。它们是内核代码的扩展,通常用于添加设备驱动程序、文件系统支持、网络协议或其他功能。
特点:- 动态性:可以在运行时加载(load)或卸载(unload),相比之下,静态编译进内核的代码需要重启系统才能生效。
- 模块化:将内核功能分解为独立模块,便于维护和扩展。
- 依赖内核:LKMs 不能独立运行,必须嵌入内核地址空间,作为内核的一部分执行。
-
与内核相关的Linux指令:
- insmod: 讲指定模块加载到内核中
- 特点:需要提供模块文件的完整路径,且不会自动处理模块的依赖关系。通常用在需要手动加载单个模块的场景。
- rmmod: 从内核中卸载指定模块
- 特点:只需要模块名称(不需要路径或 .ko 后缀),但如果模块正在被使用或有依赖关系,卸载会失败。
- lsmod: 列出已经加载的模块
- 特点:输出包括模块名、占用内存大小以及依赖关系,简单明了,常用于检查模块状态。
- modprobe: 添加或删除模块,modprobe 在加载模块时会查找依赖关系
- 比 insmod 和 rmmod 更智能,会根据 /lib/modules/ 下的模块依赖文件(通常由内核版本管理)自动加载依赖模块,是日常使用中最推荐的工具。
(四),以强网杯2018 - core内核Pwn题的文件为例:
例题:强网杯2018 - core
依然是十分经典的kernel pwn入门题,内核入门题的老演员了!
点击下载-core.7z
WP:【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog
- 将压缩包解压出来之后可以得到这些文件
ub20@ub20:~/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player$ ls
bzImage core.cpio vmlinux start.sh
- bzImage:启动内核的压缩镜像。
- core.cpio:包含了完整的根文件系统。
- vmlinux:内核符号文件,有助于我们分析内核函数地址。
- start.sh:启动脚本,用于初始化环境。
- start.sh利用如下命令启动 QEMU 虚拟机,查看一下启动参数及保护机制:
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:开启内核地址空间布局随机化,但由于其他防护措施较弱(例如 NX 被禁用),依然为后续的 ROP 利用提供了突破口。
- 其它参数则确保虚拟机内的文件系统、网络及调试环境能正常工作。
- 解压这个文件可以看见整个文件系统的目录:
gunzip -c /home/ub20/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player/core.cpio | cpio -idmv
这就是解压出来的文件系统core.cpio,就是一个Linux操作系统的文件目录结构里面有构建好的基础工具:
ub20@ub20:~/KernelStu/KernelVuln/Kernel_ROP_basic/tmp$ tree -d
.
├── bin
├── etc # 系统配置文件(通常包含inittab、passwd等)
├── lib # 内核模块存储目录(注意权限位为755)
│ └── modules
│ └── 4.15.8 # 内核版本
│ └── kernel
│ ├── arch
│ │ └── x86
│ │ └── kvm
│ ├── drivers
│ │ ├── thermal
│ │ └── vhost
│ ├── fs
│ │ └── efivarfs
│ └── net
│ ├── ipv4
│ │ └── netfilter
│ ├── ipv6
│ │ └── netfilter
│ └── netfilter
├── lib64
├── proc # 进程信息虚拟文件系统
├── root
├── sbin
├── sys
├── tmp
└── usr
├── bin
└── sbin
解压后你会发现文件系统内的目录结构与标准 Linux 系统类似,主要目录包括:
- bin、sbin:基本命令与系统管理工具。
- etc:存放系统配置文件,如 passwd、inittab 等。
- lib/lib64:存放库文件及内核模块,其中
lib/modules/4.15.8/
目录下包含当前内核版本的模块。 - proc、sys:虚拟文件系统,分别提供进程与系统信息。
- usr:包含一些用户级工具。
- 查看解压后的
/init
脚本内容,可以看见他的初始化脚本加载了哪些内核文件,和进行了什么操作有什作用:
#!/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# 将内核符号表导出到 /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
# 加载内核模块 core.ko
insmod /core.ko
poweroff -d 120 -f & # 在 120 秒后强制关机
setsid /bin/cttyhack setuidgid 1000 /bin/sh # 启动一个新的会话,并以 UID 1000 的用户身份运行 shell
echo 'sh end!\n' # 打印 shell 结束信息
umount /proc
umount /sys
# 立即强制关机
poweroff -d 0 -f
- 内核符号泄露:通过将
/proc/kallsyms
的内容复制到/tmp/kallsyms
,攻击者可以轻松获取内核中所有函数的地址,对于构造 ROP 链至关重要。 - 安全配置调整:写入
kptr_restrict
和dmesg_restrict
,将内核符号信息暴露出来,从而降低了利用难度。 - 模块加载:
insmod /core.ko
表明题目中真正存在漏洞的模块就是core.ko
,后续的漏洞挖掘与利用将围绕此模块展开。 - 调试与网络配置:配置网络及设置延时关机方便调试,有时需要手动去除定时关机以便反复利用。
- 内核模块保护检查
在解压出来的文件系统中成功找到了core.ko内核文件:
ub20@ub20:~/KernelStu/KernelVuln/Kernel_ROP_basic/tmp$ checksec --file=./core.ko
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO Canary found NX disabled Not an ELF file No RPATH No RUNPATH 41 Symbols No 0 0 ./core.ko
分析输出可以看出:
- RELRO:未启用,模块内存保护较弱。
- Stack Canary:存在一定的栈保护,但并不足以完全抵抗溢出攻击。
- NX(不可执行位):被禁用,允许在堆栈上执行代码,这为构造 ROP 链提供了便利。
- 符号信息:包含 41 个符号,有助于定位漏洞代码和搭建攻击链。
四、驱动文件的动静逆向分析技术讲解
在深入分析驱动文件之前,掌握一些基础知识是至关重要的。本节将带领读者了解如何通过动静结合的方式对驱动文件进行逆向分析,重点讲解用户态与内核态的交互机制、关键内核函数的使用,以及如何通过调试工具对内核进行调试。
基础知识准备: 用户态与内核交互的关键系统调用 ioctl
在Linux系统中,用户态程序与内核态驱动之间的交互通常通过系统调用实现。ioctl
是一个非常重要的系统调用,用于设备控制。它允许用户态程序向内核态驱动发送特定的控制命令,从而实现设备的管理和配置。
在一般系统调用就是通过系统调用号来实现,比如64
位下read
的系统调用号为0
。
/usr/include/x86_64-linux-gnu/asm/unistd_64.h
和/usr/include/x86_64-linux-gnu/asm/unistd_32.h
可以查看 64 位和 32 位的系统调用号。
系统调用:ioctl
在 Linux
系统中,几乎所有设备都被视为文件,这使得通过标准的文件操作(如 open
、read
、write
和 close
)来访问设备变得简单。然而,某些操作超出了这些标准接口的能力,需要一个更灵活的机制来处理设备特定的功能,这就是 ioctl
的用武之地。
ioctl
** 的功能**
- 设备控制:通过
ioctl
,用户空间程序可以发送控制命令给设备驱动程序,进行设备特定的操作,比如设置设备参数、查询设备状态等。 - 扩展性:因为设备驱动是可扩展的,
ioctl
使得新设备可以通过定义新的请求码和操作接口来被支持,而不需要修改现有的系统调用。
函数原型
在 C 语言中,ioctl
的原型如下:
int ioctl(int fd, unsigned long request, ...);
fd
:文件描述符,通常是通过open()
函数获取的,用于指定要操作的设备。request
:请求的命令,通常是一个宏,定义了要执行的操作。...
:可选参数,具体取决于请求的类型,有时需要传递指向结构体的指针或其他参数。
使用ioctl
int fd = open("/dev/mydevice", 2);//在内核中攻击使用/proc/mydevice
ioctl(fd, request_code, data) ;
基础知识准备: 驱动文件中的关键函数init_module
init_module
是 Linux 内核模块加载时的入口函数,负责:
- 分配资源(内存、IO 端口等);
- 注册设备(如创建设备节点、分配设备号);
- 初始化硬件或数据结构。
了解 init_module
的实现可以帮助确定驱动的加载流程和初始化逻辑,并为后续的漏洞挖掘提供线索。
基础知识准备: Linux内核中常见的内核态函数
在内核驱动中,许多函数在系统稳定性与安全性上起着至关重要的作用。下面列举一些常见的函数:
- printk:用于内核日志输出,调试信息可通过
dmesg
查看。利用 printk 可观察到模块加载、错误提示等信息。 - copy_from_user / copy_to_user:这两个函数用于在内核与用户空间之间传递数据,往往是漏洞出现的重要接口,如缓冲区溢出、越界访问等问题可能出现在此。
- kmalloc / kfree:内核动态内存分配与释放函数,类似于用户态的 malloc/free,但因内存池和同步机制的不同,容易因错误使用引发内存泄漏或内存破坏。
- proc_create:在
/proc
文件系统中创建条目,常用于调试和状态监控,逆向过程中可借助该接口了解驱动的运行状态。
在 Linux 内核中,proc_create 是一个非常重要的函数,用于在 /proc 文件系统下创建新的文件条目。它通常用于内核模块或驱动程序开发,以便提供一种用户空间与内核空间交互的接口。下面我详细解释它的作用和用法。
struct proc_dir_entry *proc_create(
const char *name,
umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops);
返回值: 返回一个指向 struct proc_dir_entry 的指针,表示创建的 proc 文件条目。如果创建失败,返回 NULL。
参数说明:
- const char name:
- 指定要创建的 proc 文件的名称。
- 例如,如果传入 “core”,则会在指定的父目录下创建 /proc/core 文件。
- 这个文件可以通过用户空间的工具(如 cat、echo)访问。
- umode_t mode:
- 指定文件的权限模式,通常用八进制表示(例如 0666)。
- 权限值遵循 Linux 文件权限规则:
- struct proc_dir_entry parent:
- 指定新文件的父目录,是一个 proc_dir_entry 结构体的指针。
- 如果传入 NULL,文件会创建在 /proc 根目录下。
- 如果传入其他值,则可以在子目录下创建文件。例如,如果 parent 指向 /proc/sys,新文件会出现在 /proc/sys/name。
- const struct file_operations proc_fops:
- 一个指向 file_operations 结构体的指针,定义了该 proc 文件支持的操作。
- 这个结构体包含函数指针,例如:
- .read: 定义读文件时的行为。
- .write: 定义写文件时的行为。
- .open: 文件打开时的回调。
- .release: 文件关闭时的回调。
- 通过这个参数,开发者可以自定义用户空间与内核交互的逻辑。
4.基础知识准备: 内核调试环境搭建与实践
例题:强网杯2018 - core
依然是十分经典的kernel pwn入门题,用他来进行内核调试
点击下载-core.7z
WP:【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog
本部分将深入解析内核漏洞调试环境的构建方法,以强网杯2018核心赛题为例,演示完整的调试流程
编写C语言程序来调用驱动中的函数作为内核调试入口
我们使用以下C程序作为内核态数据探测工具,其核心功能是通过ioctl系统调用与内核模块交互,写了一个泄露内核Canary值的简单程序,编译好后直接传入虚拟机,之后执行即可获得,内核空间中Canary的值!仅本案例有效:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
void set_off_val(int fd, size_t off) {
ioctl(fd, 0x6677889C, off