脆弱性 「SMBGhost」 の発生原因および攻撃手法に関する分析
本内容は、アンラボコリアより発表された内容を翻訳したものです。
--------------------------------------------------------------------------------------------
2020年3月12日、Microsoft が緊急アップデートを行い、脆弱性の修正プログラムを公開した。
対処された脆弱性は、CoronaBlue または SMBGHOST と呼ばれる 「CVE-2020-0796」 である。発表された当時、致命的な脅威とみなされ、「第二の WannaCry」 になり得ると懸念する声もあった。
2020年4月にマルウェア 「Lemonduck」 にこの脆弱性をスキャンするコードが追加され、現在もマルウェアの流布に利用されている。
本文では、脆弱性 「CVE-2020-0796」 の発生原因および攻撃手法、セキュリティパッチ、検知、対応方法について説明する。
CVE-2020-0796 は、Microsoft SMB 3.1.1 (SMBV3) プロトコルで圧縮されたメッセージを処理する Srv2!Srv2DecompressData 関数内部で OriginalSize と Offset を処理する過程で Integer Overflow が発生する脆弱性である。この脆弱性により、システム障害や権限の昇格、リモートコードの実行が可能になる。2020年3月12日にセキュリティパッチの適用により対処された。
< 影響を受けるバージョン >
● Windows 10 Version 1903 for 32-bit Systems
● Windows 10 Version 1903 for ARM64-based Systems
● Windows 10 Version 1903 for x64-based Systems
● Windows 10 Version 1909 for 32-bit Systems
● Windows 10 Version 1909 for ARM64-based Systems
● Windows 10 Version 19039 for x64-based Systems
● Windows Server, version 1903 (Server Core installation)
● Windows Server, version 1909 (Server Core installation)
脆弱性の発生原因
SMB2 COMPRESSION_TRANSFORM_HEADER は、通信時に圧縮されたメッセージを送信する際に使用されるヘッダーである。
このヘッダーは、Windows10 1809 以降のバージョンである SMB 3.1.1より適用される。
[図 1] SMB2 COMPRESSION_TRANSFORM_HEADER
CVE-2020-0796 は、改ざんされた SMB2_COMPRESSION_TRANSFORM_HEADER パケットの処理時に srv2!Srv2DecompressData 関数で圧縮解除ルーチンを処理するが、ヘッダーに存在する Originalsize + Offset 値が 0Xffffffff を超えると、Integer Overflow 脆弱性により異常なメモリ空間が割り当てられる脆弱性である。
サーバーと Negotiate 通信を通じてセッションを生成し、SMB2 COMPRESSION_TRANSFORM_HEADER の OriginalSize + Offset が 0Xffffffff より大きいデータになるように改ざんした後、改ざんしたパケットをサーバーに転送する。
[図 2] 改ざんしたパケットの転送
Srv2!Srv2DecompressData 関数内部で圧縮解除ルーチンを行うが、圧縮解除したデータを挿入するためのメモリ空間を srvnet!SrvNetAllocateBuffer 関数を通じて割り当てる。
引数として OriginalSize+Offset 値を使用するが、この際、Integer Overflow により割り当てられるべきサイズより小さいサイズのメモリ空間が割り当てられる。
メモリ空間の割り当てについては、[攻撃手法] で詳しく説明する。
[図 3] SrvNetAllocateBufferを通じたメモリ空間の割り当て
[図 4] Integer Overflow 脆弱性の発生
メモリ空間の割り当て後に srv2!SmbCompressionDecompress 関数を用いて圧縮解除を行う。圧縮解除は、クライアントから受け取ったパケットに設定されている圧縮アルゴリズムに沿って行う。
[図 5] 圧縮解除関数の呼び出しコード
圧縮解除の過程は、以下のとおりである。
OriginalSize+Offset 引数に応じてメモリ空間を割り当てる。Header+Offset に位置する圧縮されたデータを CompressionAlgorithm により設定されたアルゴリズムに沿って圧縮解除後、割り当てられたメモリの Buffer 領域にコピーする。

[図 6] SmbCompressionDecompress の過程
脆弱性攻撃を行うため OriginalSize+Offset が 0xffffffff 値より大きいデータで作成されたパケットを処理すると、内部で Integer Overflow 脆弱性が発生し、圧縮解除されたデータより小さいサイズの空間が割り当てられる。
その後、srv2!SmbCompressionDecompress を通じて圧縮解除を行い、Offset 圧縮されたデータの位置を確認する。しかし、異常な Offset によって割り当てられていないメモリを指すようになり、Access Violation が発生する。
[図 7] Integer Overflow フロー図
[図 8] Access Violation の発生
攻撃手法
CVE-2020-0796 を利用して、以下の攻撃ができる。
● LPE (Local Privilege Escalation):ローカル環境で脆弱性攻撃を行い、トークンを改ざんして管理者権限を取得。
● RCE (Remote Command Execution):改ざんされたパケットを送信して攻撃対象システムで任意のコマンドを実行。
LPE (Local Privilege Escalation)
ローカル環境で脆弱性攻撃を行って管理者権限を取得できる LPE は、次の方式で行われる。
現在実行中のプロセスのトークンアドレスを取得し、そのアドレスを改ざんされたパケットに埋め込んで転送する。転送されたパケットにより脆弱性を突いた攻撃を行い、現在実行中のプロセスのトークンをシステム (SYSTEM) プロセスのトークンに変更して管理者権限を得る。その後、インジェクションによりシェルコードを実行する。
[図 9] LPE 攻撃の過程
脆弱性攻撃のためのパケットを作成する。SMB2 COMPRESSION_TRANSFORM_HEADER で Offset の次の部分に SYSTEM プロセストークン + 0x40 のデータを挿入し、ダミーデータ + 現在実行中のプロセスのトークン + 0x40 のアドレスを圧縮したデータを挿入して作成したパケットを転送する。
[図 10] 改ざんされたパケットの転送
Srv2!Srv2DecompressData 関数で OriginalSize+Offset を引数としてメモリ空間を割り当てるための srvnet!SrvNetAllocateBuffer 関数を呼び出す。この際、Integer Overflow を通じて異常な値が引数として使用される。
[図 11] Integer Overflow を通じたメモリ空間の割り当て
srvnet!SrvNetAllocateBuffer 関数は次のように空間を割り当てる。
引数として使用されたパケットの OriginalSize+Offset 値に応じてSrvNetBufferLookasides を参照してサイズ (Size) を取得する。
この際、Integer Overflow によって、本来のサイズより小さい値を引数として使用し、最小サイズである 「0x1100」 を取得する。
その後、srvnet!SrvNetAllocateBufferFromPool 関数を通じて Pool メモリを割り当てる。

割り当てられた Pool メモリは次のような構造を持つ。
圧縮解除されたデータがコピーされる Buffer と、メモリの割り当てに関連するヘッダー部分で構成されている。
BufferSize+0x68 部分に存在する PBuffer は、Buffer のアドレス値を持っている。

改ざんされたパケットを通じて Integer Overflow 脆弱性を発生させ、圧縮されたデータより小さいメモリ空間を割り当てる。
その後、圧縮解除して Buffer Overflow を発生させ、PBuffer 部分を現在実行中のプロセスの Token+0x40 値で上書きする。

[図 15] 改ざんされた PBuffer メモリの比較
圧縮解除後、転送されたパケットの Offset と圧縮されたデータとの間にデータが存在する場合、memcpy を通じて Pool メモリの PBuffer を参照して Buffer の始めの部分に該当データをコピーする。
[図 16] メモリコピーコード
Pool メモリの PBuffer は現在実行中のプロセスのトークン + 0x40 アドレスに改ざんされ、Raw Data 部分にシステムプロセストークン+ 0x40 の値が挿入されているため memcpy を通じて現在のプロセストークン + 0x40 はプロセストークン + 0x40 値に改ざんされる。
[図 17] 権限昇格フロー
[図 18] 改ざんされたトークンの比較
現在実行中のプロセスのトークン + 0x40 を改ざんした後、プロセスの権限を確認すると [図 19] のようにさまざまな権限が設定されていることがわかる。また、シェルコードにより実行された cmd.exe が管理者権限で実行されることと LPE (Local Privilege Escalation) が行われたことが確認できる。
[図 19] 権限昇格後のプロセス権限の比較
[図 20] 管理者権限で cmd.exe を実行
RCE (Remote Command Execution)
改ざんされたパケットを送信してリモートで任意のコマンドを実行できる RCE は、次の方式で行われる。
まず、物理メモリを読み込んで Self-Reference PML4 Entry アドレスと HAL Heap アドレスを検索する。その後、見つかった HAL Heap アドレスを利用して HalpApicRequestInterrupt アドレスを検索する。Self-Reference PML4 Entry により PTE アドレスを見つけて NX bit を改ざんする。KUSER_SHARED_DATA アドレスにシェルコードを挿入し、挿入したシェルコードアドレスを HapApicRequestInterrrupt に変更後、シェルコードを実行する。
[図 21] RCE 攻撃の過程
RCE 攻撃の過程を説明する前に、Write Primitive および Read Primitive について説明する。
1) Write Primitive
Write Primitive は、任意のメモリアドレスに入れたい値を挿入するための方法である。LPE でトークンを改ざんする際に用いられた方法がそのまま使用される。
脆弱性を突いて Pool メモリの PBuffer 部分を改ざんする。
[図 22] PBuffer の改ざん
圧縮解除後、memcpy を通じて PBuffer に挿入されている Raw Data をコピーするようになるため、任意のアドレスに入れたいデータを挿入することができる。
[図 23] Write Primitive によるメモリ改ざん
2) Read Primitive
Read Primitive は、任意の物理メモリアドレスを読み込むための方法である。
Write Primitive により Fake MDL を挿入および Pool メモリを改ざんしてFake MDL アドレスに変更する。その後、Negotiate パケットをリクエストすると、正常なレスポンスではなく、物理メモリのデータを転送する。
[図 24] Read Primitive 過程
Fake MDL を生成して任意の物理メモリのアドレスに設定し Pool メモリの PMDL を Fake MDL アドレスに変更すると、Negotiate パケットのレスポンス時に tcp.sys を通じて Fake MDL を参照するようになり、設定した任意の物理メモリデータを転送する。
[図 25] MDL の構造
KUSER_SHARED_DATA は固定のメモリアドレスを持っているため、Write Primitive を通じて KUSER_SHARED_DATA + 0x900 アドレスに Fake MDL を挿入する。
[図 26] Fake MDL 挿入フロー
既存の Buffer Overflow により PMDL を改ざんすると PBuffer も改ざんされるため、圧縮解除後 memcpy を通じて割り当てられていないメモリにアクセスして Access Violation が発生したり、異常な値をコピーしてエラーが発生する可能性がある。
PMDL 部分のみを改ざんするために Offset を PMDL の位置に変え、圧縮データ (Compressed Data) 領域に SER_SHARED_DATA + 0x900 アドレスを圧縮してから、圧縮されていない 0x1337 データを挿入する。このパケットの圧縮解除を行うと PMDL 領域から圧縮解除されたデータがコピーされ、PMDL を KUSER_SHARED_DATA + 0x900 アドレスで上書きする。0x1337 データの影響で圧縮解除に失敗し、PMDL 領域のみを改ざんできるようになる。
[図 27] PMDL 改ざん
PMDL Overwrite 後、圧縮解除に失敗すると、割り当てられたメモリの Allocation Header のアドレスを引数にして SrvNetFreeBuffer 関数が呼び出される。
[図 28] SrvNetFreeBuffer 呼び出しコード
[図 28] は、SrvNetAllocateBuffer で Pool メモリを割り当てるために参照した SrvNetBufferLookasides の構造を表したものである。
SrvNetBufferLookasides はサイズ (Size) 別に Lookaside を管理している。Lookaside は GENERAL_LOOKASIDE_POOL の構造を持っており、Pool メモリの割り当てや解除時に ListHead 部分を参照する。Pool メモリ空間の割り当て時に ListHead に保存されているメモリが存在する場合は ExpInterlockedPopEntrySList を通じて取得する。Pool メモリ空間を解除する際には ExpInterlockedPushEntrySList を通じてメモリを ListHead に保存して管理する。
[図 29] SrvNetBufferLookasides の構造
圧縮解除に失敗すると SrvNetFreeBuffer 関数で初期化を行うが、Pool メモリの PMDL1 に対しては初期化を実行しない。Fake MDL のアドレスに改ざんされた PMDL1が初期化されていない状態で ExpInterlockedPushEntrySList を通じて Lookaside の ListHead に保存される。その後、Negotiate レスポンスのために Smb2ExecuteNegotiateReal 関数が呼び出され、メモリ空間の割り当てのために srv2AllocateResponseBuffer 関数が呼び出される。Lookaside の ListHead 部分を確認し ExpInterlockedPopEntrySList を通じて割り当て解除されたメモリ空間を呼び出すが、PMDL1 が Fake MDL アドレスになっているため、tcp.sys を通じて任意の物理メモリアドレスを読み込んで転送する。

[図 31] Fake MDL 改ざんによる物理メモリデータの転送
3) RCE 攻撃過程
RCE (Remote Command Execution) 攻撃過程で使用される Self-Reference PML4 Entry と HAL Heap の Base Address を取得するための動作を行う。HAL Heap に存在する HalPLowStub は _PROCESSOR_START_BLOCK 構造を持っている。この構造の KPROCESSOR_STATE の SpecialRegisters 部分に CR3 レジスタが存在する。
[図 32] _PROCESSOR_START_BLOCK
Read Primitive を通じて 0x10000から物理メモリを読み込み、JMP Opcode を見つけた後、0x78 Offset に存在する HalPLowStub 仮想アドレスを求める。HalPLowStub 仮想アドレス & 0xFFFFFFFFF0 000000 を通じて HAL Heap Base Address を検索する。 0xA0 Offset に CR3 レジスタが存在し PML4 値を検索する。

[図 34] HAL Heap Base Address PML4 データの確認
Self-Reference PML4 Entry を検索するために、次の動作を行う。
PML4 アドレスの物理メモリを読み込み、PML4 Entry を検索する。PML4 Entry & 0xffff00 値と PML4 値が同じアドレスを求める。求めたアドレスを 0xfff と And 演算を行い、>>3 演算を行うと、Self-Reference PML4 Entry を求めることができる。
[図 35] Self-Reference PML4 Entry の検索
Hal!HalpApicrequestInterrupt 関数のアドレスが保存されているアドレスを検索するために、次の動作を行う。
HAL Heap Base Address の物理メモリアドレスデータを読み込み、HalInterruptControler Table を見つけるため、HAL 関数アドレスである 0xfffff80000000000 領域のアドレスが4つ連続で存在するアドレスを検索する。5つ目の HAL 関数アドレスが存在する Index + 0x38 + HAL Heap Base Address が hal!HalApicRequestInterrupt 関数のアドレスが保存されているアドレスである。
[図 36] HalApicRequestInterrupt アドレスの検索
KUSER_SHARED_DATA にシェルコードをコピーして実行するために、KUSER_SHARED_DATA の PTE を改ざんする。PTE のNX Bit を設定するため、64 Bit の値を 0に変更する。Self-Reference PML4 Entry と KUSER_SHARED_DATA を演算により PTE アドレスを求めることができる。
[図 37] PTE Overwrite の過程
MiGetPteAddress 関数により PTE 仮想アドレスを求めるコードを確認できる。
[図 37] は、メモリアドレスと PTE Base Address を通じて PTE アドレスを求めるルーチンである。PTE Base Address は Self Reference PML4E 演算により求められる。KUSER_SHARED_DATA の PTE アドレスを求めて Read Primitive を通じてメモリを読み込む。PTE を読み込んだ後 NX Bit を設定した値を上書きする。
[図 38] MiGetPteAddress
[図 39] PTE アドレスの計算コード
[図 40] 改ざんされた PTE の比較
Write Primitive を通じて KUSER_SHARED_DATA + 0x950 アドレスにカーネルシェルコードを挿入する。
[図 41] カーネルシェルコードの挿入
KUSER_SHARED_DATA アドレスに挿入したシェルコードを実行するため、Write Primitive を利用して hal!HalpApicRequestInterrupt 関数アドレスを KUSER_SHARED_DATA + 0x950 アドレスに変更する。その後、該当関数を呼び出すと、hal!HalpApicRequestInterrupt 関数の呼び出しではなく、シェルコードが実行される。
[図 42] HalpApicRequestInterrupt 関数アドレスの変更
実行されたシェルコードは、次のように動作する。
まず、HalpApicRequestInterrupt を正常なアドレスに復元する。その後、ユーザーモード (User Mode) APC インジェクション検知を回避するため、spoolsv.sys プロセスでカーネルモードで APC キューを登録してカーネルシェルコードを実行する。追加で実行されるカーネルシェルコードによりメモリ空間を割り当て、シェルコードをコピーする。その後、ユーザーモードで APC キューを登録してシェルコードを実行する。
[図 43] シェルコードの動作フロー
脆弱性対応
CVE-2020-0796 は、2020年3月12日に Microsoft の緊急アップデートにより対処された。
対処された srv2.sys の Srv2DecompressData 関数で RtlULongAdd、RtlULongSub 関数および追加ルーチンにより OriginalSize、Offset に対する検証部分が追加された。
[図 44] CVE-2020-0796 セキュリティパッチの内容
脅威情報
マルウェア 「Lemonduck」 は、2019年末に韓国国内で流布され、パワーシェルスクリプトで SMB、RDP、SSH、MSSQL Bruteforce、そして脆弱性を突いてモネロ(Monero)を採掘するマルウェアを流布した。
2020年4月、Lemonduck から脆弱性 「CVE-2020-0796」 をスキャンするコードが発見され、その後攻撃コードを追加しながらマルウェア流布に利用されている。
マルウェア流布コードが存在する if.bin ファイルを用いて脆弱性攻撃を行う。このコードは smgh.bin ファイルをダウンロードし、%TEMP%\smgh.exe ファイルを生成する。
生成した smgh.exe ファイルは、公開された CVE-2020-0796 RCE コードを修正した python コードを exe にコンパイルしたマルウェアである。このファイルをデコンパイルしてコードを比較すると、大抵類似したコード部分が存在する。既存のコードに [-cmd] 引数が追加され、ユーザーシェルコード (User Shellcode) 部分が修正された。引数によって任意のコードの実行が可能である。最終的にスレッド (thread) を生成して Winexec 関数によりコマンドが実行される。
[図 45] 攻撃コードの比較
Lemonduck で smgh.exe ファイルを通じて脆弱性 (CVE-2020-0796) 攻撃を行い、追加のパワーシェルスクリプトをダウンロード・実行してマルウェアを流布する。
[図 46] 脆弱性 (CVE-2020-0796) 攻撃コード
脆弱性攻撃の検知
アンラボ製品では脆弱性 「CVE-2020-0796」 を次のように検知している。
● Microsoft_Windows_SMBv3_Compression_Remote_Code_Execution(CVE-2020-0796)
● Microsoft Windows SMBv3 Compression Remote Code Execution
対策
脆弱性 「CVE-2020-0796」 に対するセキュリティパッチは2020年3月12日に Microsoft から提供されている。このセキュリティパッチを適用することで脆弱性に対応することができる。
Microsoft セキュリティパッチの適用が不可能な場合は、以下の方法で対応できる。
- SMBv3 圧縮の非アクティブ化
SMBv3 サーバーでは以下の PowerShell コマンドを使用して SMB プロトコルの圧縮機能を一時的に非アクティブ化できる。
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServe r\Parameters" DisableCompression -Type DWORD -Value 1 -Force
- TCP ポート 445 遮断
ファイル共有が不要なシステムでは、TCP ポート 445をファイアウォールで遮断する方法で対応できる。
IoC (Indicators of Compromise)
関連ファイルの MD5は以下のとおりである。
[図 47] MD5