スマートフォン解析 top

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

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

Linux Kernel n_tty_writeの脆弱性(CVE-2014-0196)

今回もLinux Kernelの脆弱性を検証しました。今回はn_tty_writeのheap overflowの脆弱性(CVE-2014-0196)です。


-影響と対策-

3.14.3以前のLinux Kernelではtermiosの設定がLECHO & ~OPOSTの場合にn_tty_write()で競合状態(race condition)が発生する脆弱性があります。この脆弱性を悪用すると、ローカルで一般ユーザがDoS攻撃を実行または管理者権限を取得することが可能です。

Linux Kernelのバージョンアップで対策が可能です。


-exploit-

Mathew DaleyによるPOCを検証しました。環境はUbuntu 14.04で、Kernelのバージョンは3.14.0-031400-genericです。

競合状態を発生させやすくするために、マルチプロセッサ環境でexploitを実行しています。

acruel@trusty:~/work/cve-2014-0196$ uname -r
3.14.0-031400-generic
acruel@trusty:~/work/cve-2014-0196$ ./cve-2014-0196-md 
[+] Resolving symbols
[+] Resolved commit_creds: 0xffffffff810955f0
[+] Resolved prepare_kernel_cred: 0xffffffff81095890
[+] Doing once-off allocations
[+] Attempting to overflow into a tty_struct........
[+] Got it :)
root@trusty:~/work/cve-2014-0196# whoami
root
root@trusty:~/work/cve-2014-0196# 

Pwned! 実行時のheapの状態に左右されるため要する時間は毎回違いますが、数分程度でroot権限が取れました。しかしながら、手元の検証環境ではたまに(10回に1回か2回ほど)システムがフリーズしてしまうこともありました。exploitの安定性はイマイチな感じです


-問題の箇所-

脆弱性となるバグの箇所を確認してみます。以下は、n_tty_write()のソースコードです。

2307 static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
2308                            const unsigned char *buf, size_t nr)
2309 {
2310         const unsigned char *b = buf;
2311         DECLARE_WAITQUEUE(wait, current);
2312         int c;
2313         ssize_t retval = 0;
2314 
2315         /* Job control check -- must be done at start (POSIX.1 7.1.1.4). */
2316         if (L_TOSTOP(tty) && file->f_op->write != redirected_tty_write) {
2317                 retval = tty_check_change(tty);
2318                 if (retval)
2319                         return retval;
2320         }
2321 
2322         down_read(&tty->termios_rwsem);
2323 
2324         /* Write out any echoed characters that are still pending */
2325         process_echoes(tty);
2326 
2327         add_wait_queue(&tty->write_wait, &wait);
2328         while (1) {
2329                 set_current_state(TASK_INTERRUPTIBLE);
2330                 if (signal_pending(current)) {
2331                         retval = -ERESTARTSYS;
2332                         break;
2333                 }
2334                 if (tty_hung_up_p(file) || (tty->link && !tty->link->count)     ) {
2335                         retval = -EIO;
2336                         break;
2337                 }
2338                 if (O_OPOST(tty)) {
2339                         while (nr > 0) {
2340                                 ssize_t num = process_output_block(tty, b,      nr);
2341                                 if (num < 0) {
2342                                         if (num == -EAGAIN)
2343                                                 break;
2344                                         retval = num;
2345                                         goto break_out;
2346                                 }
2347                                 b += num;
2348                                 nr -= num;
2349                                 if (nr == 0)
2350                                         break;
2351                                 c = *b;
2352                                 if (process_output(c, tty) < 0)
2353                                         break;
2354                                 b++; nr--;
2355                         }
2356                         if (tty->ops->flush_chars)
2357                                 tty->ops->flush_chars(tty);
2358                 } else {
2359                         while (nr > 0) {
2360                                 c = tty->ops->write(tty, b, nr);
2361                                 if (c < 0) {
2362                                         retval = c;
2363                                         goto break_out;
2364                                 }
2365                                 if (!c)
2366                                         break;
2367                                 b += c;
2368                                 nr -= c;
2369                         }
2370                 }
2371                 if (!nr)
2372                         break;
2373                 if (file->f_flags & O_NONBLOCK) {
2374                         retval = -EAGAIN;
2375                         break;
2376                 }
2377                 up_read(&tty->termios_rwsem);
2378 
2379                 schedule();
2380 
2381                 down_read(&tty->termios_rwsem);
2382         }
drivers/tty/n_tty.c

2325行目と2360行目に注目すると、process_echoes()が書き込みの前後にmutex lockを取得しているのに対して2360行目のtty->opts->write()ではlockを取得していません。

以下でみるように競合状態で2つのプロセスがこれらの関数を同時に実行すると問題が発生します。

 815 static void process_echoes(struct tty_struct *tty)
 816 {
 817         struct n_tty_data *ldata = tty->disc_data;
 818         size_t echoed;
 819 
 820         if (ldata->echo_mark == ldata->echo_tail)
 821                 return;
 822 
 823         mutex_lock(&ldata->output_lock);
 824         ldata->echo_commit = ldata->echo_mark;
 825         echoed = __process_echoes(tty);
 826         mutex_unlock(&ldata->output_lock);
 827 
 828         if (echoed && tty->ops->flush_chars)
 829                 tty->ops->flush_chars(tty);
 830 }
drivers/tty/n_tty.c

この2つの関数はいずれもpty_write()を経由してtty_insert_flip_string_fixed_flag()をcallします。これはtty bufferにデータをコピーする関数です。

288 int tty_insert_flip_string_fixed_flag(struct tty_port *port,
289                 const unsigned char *chars, char flag, size_t size)
290 {
291         int copied = 0;
292         do {
293                 int goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE);
294                 int flags = (flag == TTY_NORMAL) ? TTYB_NORMAL : 0;
295                 int space = __tty_buffer_request_room(port, goal, flags); 
296                 struct tty_buffer *tb = port->buf.tail;
297                 if (unlikely(space == 0))
298                         break;
299                 memcpy(char_buf_ptr(tb, tb->used), chars, space);
300                 if (~tb->flags & TTYB_NORMAL)
301                         memset(flag_buf_ptr(tb, tb->used), flag, space);
302                 tb->used += space;
303                 copied += space;
304                 chars += space;
305                 /* There is a small chance that we need to split the data ov    er
306                    several buffers. If this is the case we must loop */
307         } while (unlikely(size > copied));
308         return copied;
309 }
drivers/tty/tty_buffer.c

競合状態では2つのプロセスAとBが上のコードを以下のようなシーケンスで実行する可能性があります。

Bがバッファをリクエストした後にAがバッファを消費し、さらにtb->usedを更新してバッファの残りがBがリクエストしたサイズよりも小さくなると、Bがコピーしたデータでバッファがあふれてしまいます。

            A                                B
__tty_buffer_request_room
                                  __tty_buffer_request_room
memcpy(buf(tb->used), ...)
tb->used += space;
                                  memcpy(buf(tb->used), ...) ->BOOM
kernel/git/gregkh/tty.git

つまり、あるpty slaveで書き込みを行うのとほとんど同時にpty masterからの書き込みをechoすると競合してheap overflowが発生する可能性があることがわかりました。


-exploitの中身-

heap overflowのexploitにはslab allocatorの性質を利用します。各slab cacheは連続して確保された固定サイズのオブジェクトのかたまりであるため、攻撃者は書き換えたいデータを含むcacheでoverflowを発生させるためにkernelに送り込むデータサイズをある程度コントロールできる必要があります。これは安定したexploitのための重要な条件です。

Mathew DaleyのPOCではtty_struct構造体のopsフィールドを攻撃者が用意したfakeのtty_operations構造体へのポインタに書き換える手法をとっています。tty_operations構造体は各種の関数ポインタを格納していますが、これを攻撃者が用意したshellcodeのアドレスにしておくことで攻撃者はkernel modeで任意のコードを実行することができます。

struct tty_struct {
        int     magic;
        struct kref kref;
        struct device *dev;
        struct tty_driver *driver;
	const struct tty_operations *ops;
        int index;

*snip*

        unsigned char closing:1;
        unsigned char *write_buf;
        int write_cnt;
        /* If the tty has a pending do_SAK, queue it here - akpm */
        struct work_struct SAK_work;
        struct tty_port *port;
};
include/linux/tty.h

以上、CVE-2014-0196の検証でした。競合状態で発生する脆弱性は発見することもexploitも難しいものが多いですが、今回も報告まで約5年かかっています。また、この脆弱性は既にリリースされているAndroidにも影響する可能性があります。


参考情報:
[1] kernel/git/gregkh/tty.git - TTY/Serial driver development tree
https://git.kernel.org/cgit/linux/kernel/git/gregkh/tty.git/commit/?h=tty-linus&id=4291086b1f081b869c6d79e5b7441633dc3ace00


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