AhnLab ASEC 分析チームは、ランサムウェアを含む国内でのマルウェア拡散に広く使用されているIEの脆弱性、CVE-2018-8174 に対する分析を行った。この脆弱性はマグニバー(Magniber)ランサムウェアの拡散にも使用されており、セキュリティパッチの適用によって被害を予防する作業が必要である。
MS セキュリティアップデートページ (CVE-2018-8174)
– https://portal.msrc.microsoft.com/ko-kr/security-guidance/advisory/CVE-2018-8174
01.要約
1) CVE-2018-8174の概要
CVE-2018-8174の脆弱性は VBScript エンジンの Use After Free 発生によるオブジェクトの再使用によって発生する。
リモートでの実行が可能な脆弱性であり、影響を受けるバージョンは、Internet explorer8、Internet explorer9、Internet explorer10、Internet explorer11(1803以下のバージョン)、Windows 10(1803以下)、Windows 7、Windows 8、Windows Serverである。
02.事前知識
1) VBScript エンジンがスクリプトを実行する方法
イ) 定義
VBScript エンジンのスクリプトは、エンジンが解析可能な Precompiled Code (以下、P-Code)に変換されて実行される。変換された P-Code は、0x00~0x6Fの112個の値を持ち、VBScript エンジンの内部関数である RunNoEH で解析した後、P-Code に適した VBScript 関数を呼び出す。RunNoEH 関数は [図1] の通りである。

ロ) 実行方法
P-Code は RunNoEH 関数で実行され、[図1] の最初のパラメータである CScriptRuntime クラスの内部に位置する。CScriptRuntime クラスは [図2] の通りで、0xC0に位置するメンバ変数 Compiled Script によって P-Code の位置が分かる。[図2] の0xB4に位置するメンバ変数 Position Counter は、次に実行する P-Code コマンドを指している。これは、EIP レジスタに類似した役割を実行する。

VBScript エンジンにおいて、P-Codeは [図3] のように Call-Return 方式で実行される。Call-Return 方式とは、Global Code の P-Code を実行しながら、その内部で Call を呼び出す P-Code に遭遇したとき、次に実行する P-Code を保存して当該関数の P-Code コマンドを実行する方式である。

ハ) ツールの紹介
Kaspersky 社は、P-Code を抽出するスクリプトを github に公開した。これを利用すれば、上で説明した CScriptRuntime クラスの Compiled Script を抽出して分析に利用できる。
03.詳細分析
1) CVE-2018-8174
イ) 脆弱性の発生原理
CVE-2018-8174の脆弱性は VBScript エンジンの Use After Free 発生によるオブジェクトの再使用問題を命名したものである。
以下の [図4]、[図5] は、CVE-2018-8174の脆弱性において Use After Free の発生原理を整理した図である。図のスクリプトは、CVE-2018-1874 の PoC(Proof of Concept)スクリプトである。図で定義された数字の順に実行され、[図4]、[図5] が順番に続く。

[図4] のスクリプト内容のうち、1番の Dim は変数の宣言であり、グローバル変数 o を宣言する。2番の Redim の意味は配列の宣言であり、arr で命名した2個の配列が宣言される。3番のプロセスによって cla1 クラスオブジェクトを、宣言された最初の配列 arr(0) に割り当てる。このプロセスによって、最初の配列 arr(0) に cla1 クラスオブジェクトのアドレスが保存され、クラスオブジェクトに対する参照回数(参照カウント)は1となる。

[図4] のプロセス以降、[図5] の4番のメモリ解放プロセスを実行する。Erase ステートメントによって宣言された配列 arr の削除を試みる。arr 配列の最初の要素である arr(0) を削除すると、その配列に保存された cla1 クラスのClass_Terminate 関数がコールバック関数として呼び出され、5番のプロセスを実行する。5番のプロセスでは、arr(0) に保存された cla1 クラスオブジェクトのアドレス値を、1番のプロセスで宣言されたグローバル変数 o に保存する。自身を参照する変数が増加したため、cla1 クラスの参照回数は1増加する。6番で arr(0) に保存されたクラスオブジェクトのアドレスが異なる値に変更され、これによって cla1 クラスの参照回数は1減少する。以降、7番でコールバック関数は終了し、参照回数の増加・減少が一定になり、クラスオブジェクトのアドレスに対する free() 関数を呼び出す。最後の8番によってグローバル変数 o の値を出力する場合、すでに解放されたメモリアドレスを指している。

メモリ解放は VBScript.dll で定義された [図6] の関数によって実行される。コードからわかるとおり、InterlockedDecrement の呼び出し後に参照回数が0の場合、VBScriptClass::TerminateClass 関数が呼び出され、その後、解放対象であるクラス(v1変数)の参照回数が1の場合、free() 関数が呼び出される。Class_Terminate コールバック関数を呼び出す時、コールバック関数内でグローバル変数 o に、解放する前のクラスオブジェクトのアドレス値を保存し、参照回数のバランスを合わせることにより free() 関数を呼び出す。したがって、実際のメモリは解放されるため、このアドレス値を Dangling pointer として使用できる。
ロ) Use After Free
前述した Use After Free の発生原理を、以下の [図7] のコードを利用して詳細に説明する。

[図7] の1番の部分で、Redim は内部的に [図8] のような関数を呼び出す。

SafeArrayAllocData 関数までの呼び出しが終了すると、tagSAFEARRAY 構造体が生成される。

[図7] の1番の redim 部分に割り当てられて初期化されたメモリ領域が生成され、Set コマンドによって値が割り当てられる。[図9] は、このプロセスによって割り当てられた値である。この時、保存される値の形式は Variant 構造体である。Variant 構造体は、複数のタイプのデータを伝達する構造体であり、最初のメンバである VT(Variant Type) 値に基づき、有効なデータタイプが決定される。現在割り当てられた Variant 構造体の VT 値が0x09(VT_DISPATCH)であることが確認できる。

Variant 構造体の現在の VT 値である VT_DISPATCH のオブジェクトポインタが保存され、0x021C6AB8が保存されていることが確認できる。このアドレスに保存された値は、VBScriptClass に割り当てられた cla1 のオブジェクト情報が保存されている空間である。Set によって cla1 の参照値が増加し、参照回数が2になったことが確認できる。

[図7] の1番の Erase 部分は、内部的に [図11] のような関数を呼び出す。oleaut32!ReleaseResources 関数はFreedObjectArray 配列を巡回し、保存された値を初期化する機能を実行する。そして、oleaut32!VariantClear 関数は Variant 構造体を初期化する関数である。VariantClear 関数は Variant 構造体の VT 値に応じて Clear するルーティンが異なる。Variant 構造体の VT 値が DISPATCH であるため、VBScriptClass::TerminateClass が呼び出される。[図7]の Erase 関数が呼び出されるタイミングは、メモリが割り当てられてすぐに解放される時であり、参照回数は [図11] のように1となる。その後 TerminateClass が呼び出され、初期化関数を経て、[図7] の2番のコールバック関数に移動する。

初期化された cla1 配列の参照回数は4であり、[図7] の2番の部分で Set 部分は cla1 オブジェクトのアドレス情報を持っている FreedObjectArray 配列を UafArrayA に割り当てて参照回数を1増加させる。UafArrayA に保存されたアドレス値が Dangling pointer に使用される [図13]。


以降、Redim で宣言された FreedObjectArray(1) 配列に新たな値を割り当てて、vbscript 内部で AssginVar が呼び出される。この時、1は Variant 構造体の VT 値のうち0x02(VT_I2)に該当するため、既存の0x09(VT_Dispatch)値は0x02(VT_I2)で上の [図14] のように割り当てられる。FreedObjectArray(1) 配列のオブジェクト参照によって、cla1 の参照回数は1減少する。コールバック関数において、開始値である4と同じくされた状態でコールバック関数の呼び出しが終了し、参照回数は0となり、メモリが解放される。
結果的に、クラス構造体の参照回数の検証が正常に実行されず、Use After Free が発生した。ここで発生した Dangling pointer が UafArrayA 配列に保存され、[図7] の3番のプロセスによって ReuseClass に再割り当てされ、以降 TypeConfusion の脆弱性に使用される。
ハ) TypeConfusion
TypeConfusion とは、宣言された Type に混乱を与えることを意味する。以下の [図15] のように、割り当てられた Type A 形式のデータがあると仮定する場合、構成されたシェルコードによって割り当てられたデータの領域に上書きされると、上書きされたシェルコードによってデータのタイプが変形する。変形した Type A の領域を読み込むと、割り当てられた Type A ではなく、変形した Type B として認識される。この方式が TypeConfusion の原理である。

CVE-2018-8174の脆弱性において、Use After Free は2回実行され、2つの Dangling Pointer が発生し、また ReuseClass に2回割り当てられる。Use After Free の部分では方法と原理に差異がないため1つだけ説明したが、TypeConfusion では2つそれぞれの使用方法が異なる。まず最初の方法を説明し、続いて相違点を後述する。
以下の [図16] は、CVE-2018-8174コードの TypeConfusion 部分である。[図7] のプロセス3の Use After Free で求められた Dangling pointer に割り当てられた ReuseClass のアドレス、resueObjectA_arr に TypeConfusion のためのメモリを構成するコードである。resueObjectA_arr クラスの SetProp 関数が呼び出された場合、ReuseClass に宣言された規定のプロパティを実行する。規定のプロパティ関数内に構成されたQは0x0C、0x20の値で Variant 構造体の VT 値が0x200Cとなり、変数型は VT_ARRAY、VT_VARIANT である。UafArrayA に0を代入すると VBScript Terminate Class を呼び出し、参照回数が0となり、ReuseClass が free となる。以降、objectImitatingArray に FakeReuseClass オブジェクトを割り当てるが、この時 ReuseClass オブジェクトアドレスと同じアドレスを割り当てる。

割り当てられた FakeReuseClass オブジェクトは、以下の [図17] の通りである。以下の [図17] の上の部分は ReuseClass オブジェクトを割り当てた空間で、下の部分は FakeReuseClass オブジェクトを割り当てた空間である。白い枠線の部分が関数名、変数名を表しており、白い枠線の前の空間が割り当てられたデータである。Function は0x4Cの値で始まり、dim で宣言された変数は0x00で始まる。
[図17] の ReuseClass オブジェクトを割り当てた空間に FakeReuseClass オブジェクトを割り当てるが、最初の関数名が p で10個の文字列が追加され、16byte 増加して SPP 関数がその後割り当てられる。

FakeReuseClass オブジェクトに割り当てた objectImitatingArray の mem に、あらかじめ構成された FakeArrayString で上書きする。FakeArrayString は String で構成された値(0x08,BSTR)で、正常でないサイズの tagSAFEARRAY を作成するために構成された値である。書き込まれた値である FakeArrayString は、BSTR であるため、mem の VT 値が0x08に変更される。この中で0xFFFF、0x7FFFの値があるが、この値は TypeConfusion を実行する時に SafeArray 配列のサイズを0x7FFFFFFFサイズで作成するために構成された値である。また、0x0001と0x0880、0x0001の値があるが、この値は tagSAFEARRAY 構造体の1番目、2番目、3番目の引数値として設定され、それぞれ配列の次元情報、配列のフィーチャー情報、配列の要素のサイズ値となる。このようにして構成された値を objectImitatingArray の mem 変数に上書きする。このように上書きを行うと、以下の [図18] のように変更された tagSAFEARRAY 構造を確認できる。したがって、変更された tagSAFEARRAY は1次元配列の0x880( FADF_VARIANT | FADF_HAVEVARTYPE )属性と配列要素のサイズが1である0x7FFFFFFF個の配列を持つようになる。規定のプロパティ関数が終了し、SetProp に戻って定義された mem を構成された Q の値に ReuseClass の mem の位置に上書きし、タイプが0x08(BSTR)から0x200C(ARRAY, VARIANT)に変更される。その後、reuseObjectA_arr の mem を呼び出すと0x7FFFFFFFサイズの配列を使用できるようになる。正常でないサイズのこの配列は、スクリプトがロードされた iexplore.exe のヒープ領域と同じサイズのメモリにアクセスすることができる。

resueObjectB_int は resueObjectA_arr と同じ方式で TypeConfusion となる。上の説明と異なる点は、規定のプロパティ関数内に構成されたPの値とあらかじめ構成された FakeArrayString の値である。規定のプロパティ関数内に構成された P の値の VT 値が BSTR(0x08) から LONG(0x03) に変更され、空白の 16byte に満たされる。その空間にあるreuseObjectB_int の mem のグローバル変数アドレスを some_memory に挿入する。空白の値は、スクリプトがロードされた iexplore.exe のヒープ領域に、以降メモリの特定空間を保存するときに使用される。


ニ) LeakVBAddr
LeakVBAddr 部分では、先に実行した TypeConfusion で作成された配列(resueObjectA_arr)とヒープ領域のアドレス(resueObjectB_int)を利用して VBScript.dll の base addresss を取得する。


空白の関数 EmptySub を emptySub_addr_placeholder 変数に割り当てることにより、vbscript.dll の内部アドレスを含むオブジェクトが割り当てられる。この時、空白の関数を変数に割り当てることは文法上のエラーになるが、On Error Resume Next によって以降のルーティンが実行可能である。以降、null を割り当てて VT 値が Func(0x4C) からNULL(0x01) に変更されることを確認できる。EmptySub を割り当てた後、Null を割り当てた状態で VT を Null(VT=0x01) から LONG(0x03) に変更し、メモリアドレスを変数で出力してメモリアドレスを漏えいさせる。TypeConfusion 関数によって得られた0x7FFFFFFFサイズの配列(reuseObjectA_arr.mem)で heap Address(some_memory) にアクセスし、タイプの変更を自由に実行することができる。
ホ) Execute ShellCode

シェルコードは0x4D値を割り当てて実行される。0x4D値を割り当てると AssignVar が呼び出され、Variant 構造体の VT 値を確認して値に応じた分岐を行う。VT 値が0x4Dのときは VAR::Clear が呼び出され、構成されたメモリによって NtContinue と VirtualProtect でシェルコードまで実行される。

[図25] は、VirtualProtect でシェルコードに実行権限を付与するためにメモリを構成するスクリプトである。VirtualProtect は対象アドレス、サイズ、保護オプションによって呼び出される。実行権限として64(0x40)が使用される。これは、PAGE_EXECUTE_READWRITE 権限である。

[図26] は、NtContinue を呼び出すときに使用される CONTEXT 構造体を構成するスクリプトである。レジストリの位置にはパディング値が設定され、EIP、ESP値の位置にそれぞれ VirtualProtect のアドレス値とシェルコードの EP アドレス値を使用してメモリを構成する。

上で説明したように、VTに0x4D値を割り当てると AssignVar の後に Var::Clear が呼び出され、Var::Clear で以下の [図27] の分岐を実行する。[図27] のloc_6E50089Cにおける1列目は Variant 構造体のデータに該当する位置([esi+8])で、上の [図26] の StructForNtContinue で構成したメモリを指す。以降、StructForNtContinue で構成したメモリのアドレスを push したあと call し、NtContinue を実行する。

NtContinue が呼び出される時、上記で構成された CONTEXT 構造体のアドレスと共に呼び出され、VirtualProtect 関数を呼び出す。
構成された CONTEXT 構造体により VirtualProtect でシェルコードのアドレスを取得し、実行権限を付与して return し、シェルコードに分岐してシェルコードが実行される。


ヘ) 脆弱性修正パッチ
前述した CVE-2018-8174 の Use After Free 脆弱性については、oleaut32.dll モジュールが以下のようにパッチ適用された。[図30] は、oleaut32.dll パッチ適用前後の VariantClear 関数である。図の左側がパッチ適用前、右側がパッチ適用後である。割り当てられた Variant 構造体を解放するときに呼び出される関数は、[図11] の通りである。vbscript.dll の VbsErase が呼び出され、内部呼び出しにより oleaut32.dll モジュールの VariantClear 関数が呼び出される。呼び出された VariantClear 関数で vbscript.dll モジュールの TerminateClass によって解放されるが、[図30] の右側のように解放するとき、その関数で Variant Type 値を0x00(VT_EMPTY)で設定し、解放する形式でパッチが適用された。パッチ適用前後のメモリ値を比較すると、VT 値が0x09(VT_DISPATCH)から0x00(VT_EMPTY)に変更されていることがわかる。このようにパッチが適用され、Use After Free で取得した Dangling pointer を使用すると、UafArrayA 配列にオブジェクトのアドレスをコピーするプロセスでエラーが発生し、オブジェクトの再使用が不可能となる。

04.参考文献
[1] https://github.com/KasperskyLab/VBscriptInternals , Github, KasperskyLab
[2] https://msdn.microsoft.com/ko-kr/windows/hardware/ms221482(v=vs.71) , MSDN, SAFEARRAY structure
[3] https://msdn.microsoft.com/en-us/library/cc237824.aspx , MSDN, ADVFEATUREFLAGS Advanced Feature Flags
[4] https://msdn.microsoft.com/en-us/library/ms931135.aspx , MSDN, VARIANT structure
[5] https://msdn.microsoft.com/en-us/library/cc237865.aspx , MSDN, VARIANT Type Constants
[6] https://portal.msrc.microsoft.com/ko-kr/security-guidance/advisory/CVE-2018-8174、Microsoft、セキュリティアップデートガイド
[7] https://github.com/piotrflorczyk/cve-2018-8174_analysis/blob/master/analysis.vbs , Github, piotrflorczyk
Categories:マルウェアの情報