マルウェア

Vice Societyによる現地調達型攻撃: PowerShellを使った被害者データの漏出手法

Clock Icon 7 min read

This post is also available in: English (英語)

概要

Unit 42チームは、最近行ったインシデント対応のなかで、Vice Societyランサムウェア攻撃グループによる被害者ネットワークからのデータ漏出を特定しました。このさい同グループは、カスタム作成したMicrosoft PowerShell (PS)スクリプトを使っていました。本稿はこのカスタム スクリプトを分解して各関数の機能を解説し、データ漏出手法を明らかにします。

ランサムウェア攻撃グループはさまざまな手口で被害者のネットワークからデータを窃取します。外部からFileZilla、WinSCP、rcloneなどのツールを持ち込むグループもあれば、いわゆる「LOLBAS方式(living off the land binaries and scripts: 被害環境に元からあるバイナリーやスクリプトを使う現地調達型の攻撃手法)」を使うグループもあります。たとえばPowerShell (PS)スクリプトやリモート デスクトップ プロトコル(RDP)経由のコピー/ペースト、Microsoft Win32 API (例: Wininet.dllの呼び出し)などがLOLBAS方式の例です。ここでは、PowerShellスクリプトでランサムウェア攻撃のデータ漏出ステージを自動化した場合どのようなものになるかを検証していきます。

パロアルトネットワークスのお客様は、次の方法で、これ以下で説明するスクリプトに対する保護と緩和を受けています。

  • 本稿末に記載したXQLクエリーはCortex XDRによる同スクリプトの追跡に役立ちます。
  • Unit 42のインシデント レスポンス チームによる個別対応もご提供しています。

さらに、本稿の末尾に記載したYARAルールでこのスクリプトを検出できます。

関連するUnit 42のトピック Vice Society, Ransomware

概要

LOLBASのような現地調達型のデータ漏出手法を使うと外部からツールを持ち込む必要がなくなます。したがって、これらのツールがセキュリティ ソフトウェアやアナリストなどに検出フラグを立てられることも避けられますし、通常のオペレーションに紛れ込んでシステムを侵害できます。

たとえば一般的なWindows環境ではPowerShellスクリプトがよく使われていますので脅威アクターが目立たずに行動したいときにはこれが頼れる味方になります。

2023年の初め、Unit 42のインシデント レスポンス チームは、被害者ネットワークからのデータ漏出にw1.ps1というスクリプトを使うVice Societyランサムウェア攻撃グループを発見しました。この事例で私たちはWindowsイベント ログ(WEL)からスクリプトを復元しました。具体的には、Microsoft-Windows-PowerShell/Operationalというログ内のEvent ID 4104: Script Block Loggingというイベントからスクリプトを復元しました。

すべてのスクリプト ブロックを記録するにはWindowsでスクリプト ブロック ログの有効化をしておく必要があります。ただし、Microsoftは「悪意がある」と判断されるイベントについては、文書化されていないバックエンド側のしくみを使って、デフォルトでログを記録しています。このため、スクリプト ブロック ログが完全に有効化されていない環境でも、イベントID 4104のイベントが分析に役立つことがあります。

Unit 42のリサーチャーは、以下のスクリプトがpowershellコマンドを使って実行されていたことを確認しました。

このスクリプトの呼び出しでは、URNパス(上記で[redacted_ip]で示した部分)内のローカル ドメイン コントローラー(DC)のIPアドレスが使われ、DC上のs$という管理共有が指定されています。なお、このスクリプトは被害者環境のDCの1つを介して展開されるので、標的のコンピューターに対して脅威アクターがこの時点では直接的なアクセスを確保していない可能性がある点に留意してください。つまり被害者ネットワーク内のエンドポイントはすべて、同スクリプトの標的となる可能性があります。powershellの実行ファイルには、実行ポリシーの制限を回避するため、-ExecutionPolicyパラメーターとポリシー値「Bypass」が付与されます。

このスクリプトは引数を必要としません。どのファイルをネットワークからコピーするかはスクリプト自身に任されています。興味深いことに、このスクリプトはどのデータを漏出させるかを自動的に選択 できるようになっています。

スクリプトの分析

このスクリプトは最初に定数的に利用される2つの変数、$id$tokenを定義します。これらは被害者の識別に使われます。今回確認したスクリプトでは、これらの値はそれぞれ「TEST」と「TEST_1」という値にハードコードされていました。

論理上、これらの変数にもっと具体的な値を設定すれば、実際の被害者の特定に使えるはずなので、これが本当にテスト段階だったのか、うっかりテスト状態のままになっているのかはわかっていません。

このスクリプトは次に、コード ベース内で実務を担う関数を複数定義します。表1にこのスクリプトの関数の概要を示します。ここでは関数の定義の順ではなく呼び出し順で記載しています。

関数 説明
Work( $disk ) マウントされたボリュームごとに呼び出される。ハードコードされたディレクトリー名リストにもとづいて特定のディレクトリーを除外し、漏出候補のディレクトリーを絞り込む。

Show()関数を呼び出し、除外リストに一致しなかったディレクトリーのディレクトリー名をすべて渡す。

Show( $name ) Work()関数からディレクトリー名を受け取る。5つのディレクトリーを1単位としてグループに分割し、このディレクトリー グループをCreateJobLocal()関数に渡し、さらに処理をすすめる。
CreateJobLocal( $folders ) ディレクトリー グループ(5つのディレクトリーを1単位とするグループに分かれていることが多い)を受け取り、PowerShellのスクリプト ブロックを作成し、作成したスクリプト ブロックをStart-Jobコマンドレット経由でジョブとして実行する。

提供されたディレクトリー名に対し、キーワードを使って、処理対象に含めるかそれとも除外するかという選択処理を行う。これによりfill()という外部への漏出を行う関数にどのディレクトリーを渡すかを決める。

fill( [string]$filename ) CreateJobLocal()関数によって呼び出される。この関数は、脅威アクターのWebサーバーに対するHTTP POSTリクエストを経由して実際にデータを漏出させる。

表1. スクリプトの関数の概要

図1は関数間の処理フローの概要で、スクリプトがどのように動作するのかを理解しやすくするものです。

画像1はw1.ps1スクリプトの処理フロー図です。フローはスクリプト ファイルから始まって、HTTP POSTイベントによる脅威アクターへのファイルのアップロードで終わります。
図1. w1.ps1スクリプトの処理フロー

開始部

このスクリプトはまず、Windows Management Instrumentation(WMI)経由でシステム上にマウントされているドライブを特定します。次に、定義された関数を呼び出します。簡単なフィルタリングを指定してget-wmiobject win32_volumeを呼び出し、$drivesという名前の配列を作成します。この配列には、コンピューターにマウントされているドライブのリストが格納されます。この後、見つかった各ドライブのパスを個別にWork()関数に渡します。図2に関連するコード スニペットを示します。

画像2はスクリプトのスクリーンショットです。1で示しているのはForEach-Objectの行、2で示しているのがWork()関数の始まる行です。
図2. スクリプト開始部のコード。マウントされている各ボリュームを特定して処理する

この開始部のコードは以下を行っています。

  1. $drivesという名前の配列を作成し、コンピューター上にマウントされているボリュームのリストをこの配列に格納する。
    1. win32_volumeDriveTypeのenumは「
      Local Disk (ローカル ディスク)」を意味する。詳しくはMicrosoftのWin32_Volume Classのドキュメントを参照のこと。
  2. コンピューター上で特定されたドライブ($drive)を繰り返し処理し、特定された各ドライブ パスをWork()関数に渡す。

図3は1台のドライブしかマウントされていない平均的なWindowsホストでこのコードがどのように動くかを示した例です。

画像3はコードのスクリーンショットです。変数$driveと配列$drivesを赤枠で強調しています。
図3. ドライブが1台マウントされているコンピューターの$drives配列と$drive変数の値の例

特定された各ドライブ名に対し、この開始部はWork()関数を呼び出してドライブ上のディレクトリーを処理します。

Work()関数

Work()関数は呼び出しのつどディレクトリーの検索と処理に使うドライブ パスを ($diskとして) 受け取ります。図4はWork()関数の開始部分です。

画像4はWork()関数の開始部分を表示した何行ものコードのスクリーンショットです。ここでは配列$folders、変数$store、Show()関数の3行がそれぞれ1、2、3の番号つきで強調されています。
図4. Work()関数の開始部分

上のコードは以下を行っています。

  1. $foldersという配列と$jobsという配列を作成する。
  2. $storeというTupleを作成し、さきほど作成した2つの配列を格納する。
  3. Show()関数を定義する。

図5はShow()関数のすぐ下にあるWork()関数のコードの残りの部分です。

画像5はWork()関数の終了部分を示す何行ものコードのスクリーンショットです。ハイライトは3つあります。
図5. Work()関数の残りの部分

上のコードは以下を行っています。

  1. Get-ChildItemコマンドレットに処理中のボリューム文字列を渡す。システム関連ファイルやアプリケーション関連ファイルの処理を避けるため、対象外にする31個のディレクトリー パスをフィルタリングする。その後それぞれのルートのディレクトリー名をShow()関数に渡して処理を継続する。
  2. Show()関数にルート ディレクトリーのディレクトリーを渡してから、Work()関数はルート ディレクトリー内のサブディレクトリーを再帰的に検索する。先のフィルタリング同様、除外リストに一致しないサブディレクトリーがShow()関数に送られて処理される。
  3. Show()関数はPowerShellジョブを作成してデータを漏出できるようにする。この関数はディレクトリーを5つ含むグループを1単位として処理を行う。このコード部分はグループ化されたフォルダーのあまりを確実に処理するためのフェイル セーフとして機能する。
    • たとえば合計212個のディレクトリーが特定された場合はこのコードの部分であまりの2つのディレクトリー処理を保証する。

Show()関数

Show()関数はディレクトリー名を受け取って処理します。図6はShow()関数の概要を示したものです。

画像6は、Show()関数の概要を示す何行ものコードのスクリーンショットです。ifで始まる行とwhileで始まる行の2箇所が強調されています。
図6. Show()関数の概要

上のコードは以下を行っています。

  1. 提供されたディレクトリー名を集めてディレクトリー名が5つ含まれるグループを作る。ディレクトリー名が5つ集まったらそれをCreateJobLocal()関数に渡してPowerShellジョブを作成させ、そのディレクトリー グループからデータの漏出を行う。
  2. このスクリプトは、1回の処理では5つのディレクトリーを含むグループを最大10ジョブまでしか処理しないというレート制限を設けている。10個以上のジョブが実行されている場合このスクリプトは5秒間スリープして実行中のジョブ数を再確認する。
    • 注: 全体的なスクリプト設計ではプロレベルのコーディングが行われていることがわかる。このスクリプトはコンピューター リソースに影響しないような工夫がしてある。正確な理由は作者にしかわからないが、この手法は一般的なコーディングのベストプラクティスにそったもの。

CreateJobLocal()関数

CreateJobLocal()関数は、データ漏出用のマルチプロセッシング キューを設定します。図7はCreateJobLocal()関数の開始部分です。

画像7はCreateJobLocal()関数の概要を示す何行ものコードのスクリーンショットです。ifで始まる行とwhileで始まる行の2箇所が強調されています。
図7 CreateJobLocal()関数の概要

上のコードは以下を行っています。

  1. 作成するジョブの名前を擬似乱数を使って生成する。ジョブ名はアルファベット5文字(小文字、大文字を含む)で構成される。
    • たとえば、あるデバッグ セッション中、このスクリプトはiZUIbdlHxFVCHYuFyrCbGVILAという5つのジョブ名を生成した。
  2. PowerShellジョブを1つセットアップする。スクリプトのこの時点で作成されるこのジョブは、のちにスクリプト ブロックとなるコード構造を持つ。

CreateJobLocal()内のこの時点でfill()関数が定義されます(後述)が、ここでは先にCreateJobLocal()関数の残りを続けて説明します。図8はコードの続きの部分です。

画像8は、CreateJobLocal()関数の残りを示す何行ものコードのスクリーンショットです。foreach、$include/$excludes、ifの部分を3、4、5で強調しています。
図8. CreateJobLocal()関数の残りのコード

以下は、上のCreateJobLocal()のコードベースについての説明です。

  1. 漏出対象ファイルのための$fileListという配列を作成する。次に、現在のグループ内のディレクトリーをループ処理する。前述のとおり、通常はディレクトリーを5つ含むグループ単位でディレクトリーを処理する。
  2. 包含用に$include、除外用に$excludesという名前の配列をセットアップする。
  3. 与えられたディレクトリー グループ内のディレクトリーをループし、$include配列にハードコードされた値にもとづいて、正規表現を使い、漏出対象に含めるフォルダーを絞り込む。

この関数はさらにここで$excludesを使って漏出対象から除外するファイルを絞り込みます。

画像9は、CreateJobLocal()関数の残りを示す、何行ものコードのスクリーンショットです。if節内の$filesの処理と、else節内の$filesの処理、そしてforeachのセクションが6、7、8で強調されています。
図9. CreateJobLocal()関数の残りの部分

以下はCreateJobLocal()のコードベースの残りの部分の説明です。

  1. あるディレクトリーが$lincludeの包含リストと一致した場合、そのディレクトリー内にあり、$excludesの除外リストに記載された拡張子を持たず、10KBより大きく、拡張子がついているファイルをすべて見つける。
    • 注: 検証でサイズが10KB以下のファイルとファイル拡張子のないファイルの両方が除外されることを確認した。
  2. 正規表現で$includeの包含リストに合致しなかったディレクトリーであっても、そのディレクトリー内のファイルを漏出対象に含めるべきかどうかをチェックする。
    • これは、対象ファイルが包含リストに一致するかどうかを確認する2度目のチャンスを与えているものと見られる。これは、上記ステップ5では正規表現でマッチを行う-Likeで比較を行っているが、ここではGet-ChildItemコマンドレットに-Includeパラメーターを指定して比較を行っているため。
  3. 漏出対象として特定されたファイルをループし、fill()関数を呼び出して、各ファイルを漏出させる。

図10は、マルウェア解析用仮想マシン(VM)で実行したさい、このスクリプトが選択した5つのフォルダーを含む最初のグループを示しています。これらの値はスクリプトを実行するコンピューターによって変わるので、ここでは単に、この検証環境だとここからスクリプトがデータの検索を始めた、ということです。

画像10はスクリプトが漏出対象に選んだフォルダーを示すスクリーンショットです。下の紺色のウィンドウ上で、Cドライブのパスを赤枠でハイライトしています。
図10. スクリプトの実行例。漏出候補に選ばれた最初の5つのディレクトリー

fill()関数

fill()関数が実際にデータを漏出させます。この関数はファイル漏出先URLを作成する役割を担っていて、System.Net.Webclientオブジェクトと、このオブジェクトの.UploadFileメソッドを使い、HTTP POSTイベント経由で実際の漏出を行います。図11にfill()関数を示します。

画像11は、fill()関数の概要を示す何行ものコードのスクリーンショットです。スクリプトの動作の順番を2から6の赤い数字で強調表示してあります。
図11 fill()関数の概要

上のコードは以下を行っています。

  1. 全スクリプトの冒頭2行で定義されている変数$id$token(fill()関数にこれらの定義が含まれていないことに注意)が各ファイル アップロードURL内で使われる。
  2. このスクリプトから得られるもっとも重要なIoC(侵害指標)2つを含む$prefixの値を構築する。
    • IPアドレス
      1. ファイルのアップロード先となる脅威アクターのインフラ/サーバーのIPアドレス。
    • ネットワークポート番号
      1. このポート番号は80、443、または通常はエフェメラル ポートの範囲とされるカスタム ポート番号の可能性がある。

注: 本稿の執筆においてはこのIoC情報を意図的に一部削除してあります。

  1. HTTPベースのデータ漏出実行に使うWebClientオブジェクトのインスタンスを作成する。
  2. アップロード対象ファイルの完全ファイル パスとなる$fullPath変数を組み立てる。
    • 注: 各HTTP POSTイベントにはファイルの完全パスが含まれることを意味するため、ここは重要。この完全パスと送信元ホストのIPアドレスの両方が取得できれば、事後、漏出したファイルのリストを作成できる。
  3. $prefix$token$id$fullPathの各変数を組み合わせて、ファイル アップロード用の完全URLとなる$uriを組み立てる。
  4. WebClient.UploadFile()メソッドを呼び出し、ファイルをアップロードする。
    • 注: これによりHTTP POSTイベントが作成される。

HTTPアクティビティの例

このスクリプトのPOSTリクエストが脅威アクターのWebサーバー上でどのように見えるかを確認するため、ローカルのVM上にサーバーをセットアップし、マルウェア解析用マシンにこのVMをゲートウェイとして使用するように指示してスクリプトを実行しました。以下は、検証環境で実行したさいにこのスクリプトが作成した3つのPOSTリクエストの例です。

上記でいう192.168.42[.]100のIPアドレスが今回私たちが使用した検証用クライアントVMのIPアドレスです。現実のシナリオだとVice SocietyのWebサーバーはこの部分に被害者のEgress用IPアドレスを表示することになります。

以上より、このスクリプトが開始するHTTPアクティビティに関し、重要事項をいくつか集められました。

  1. POSTのパラメーターのfullpathには、ファイル送信元のドライブ レターが含まれていない
  2. このスクリプトはWebサーバーにUser-Agent文字列を提供しない。

Zeekなどのネットワーク セキュリティ監視(NSM)・侵入検知システム(IDS)がある場合や、パケット キャプチャー システムが稼働している環境であれば、社内から社外へ向かうPOSTリクエストを確認できることがあります。こうした社内から社外へのログを確認すればリクエスト長がバイト単位でわかるので、総バイト数に対する送信バイト数に注目することにより、どの版のファイルが漏出したかを特定するのに役立つかもしれません。

結論

Vice SocietyのこのPowerShellスクリプトはシンプルなデータ漏出ツールで、マルチプロセッシングやキューイングを使って、システム リソースの消費をおさえています。このスクリプトは、$includeのリストに合致するディレクトリーに存在し、サイズが10KBを超え、拡張子をもつファイルだけを対象としています。この説明に当てはまらないデータは漏出させません。

Windows環境におけるPowerShellスクリプトの性質上、この手の脅威を完全に防止するのは容易ではありませんが、検出と脅威ハントに関するヒントを「検出と脅威ハント」のセクションに記載しますので、この中でもとくにYARAルールを活用して、自社環境内の脅威の有無の識別にベストを尽くしていただければと思います。ランサムウェア攻撃グループに皆さんのデータを自動で漏出させないようご注意ください。

パロアルトネットワークスのお客様は、次の方法で、これ以下で説明するスクリプトに対する保護と緩和を受けています。

  • 本稿末に記載したXQLクエリーはCortex XDRによる同スクリプトの追跡に役立ちます。
  • Unit 42のインシデント レスポンス チームによる個別対応もご提供しています。

侵害の懸念があり弊社にインシデントレスポンスに関するご相談をなさりたい場合は、こちらのフォームからご連絡いただくか、infojapan@paloaltonetworks.comまでメールにてご連絡いただくか、下記の電話番号までお問い合わせください(ご相談は弊社製品のお客様には限定されません)。

  • 北米フリーダイヤル: 866.486.4842 (866.4.UNIT42)
  • 欧州: +31.20.299.3130
  • アジア太平洋: +65.6983.8730
  • 日本: +81.50.1790.0200

検出と脅威ハント

  • 本稿で紹介するYARAルールをお使いのセキュリティ システムに導入します。
  • PowerShellでPowerShellモジュールとスクリプト ブロック ログを有効にします。
    • Windowsイベント ログからEvent ID 400、600、800、4103、4104を確認します。
    • Event IDが4104のイベントのなかでスクリプトの関数名を検索します。
      • Work( $disk )
      • Show( $name )
      • CreateJobLocal( $folders )
      • fill( [string]$filename )
  • powershell.exe -ExecutionPolicy Bypass -file \\[internal_ip_address]\s$\w1.ps1を含むコマンドラインを監視します。
  • 不明なリモートHTTPサーバーに対してエンドポイントから/uploadを実行しているHTTP POSTイベントを探します。
  • 外部IPアドレス宛のHTTPアクティビティを確認できる場合はそれを調べます。
  • ネットワーク トラフィックのスパイクを検出します。
    • ネットワーク トラフィックのベースラインを計測済みであれば、そのベースラインから極端に逸脱しているホストやホスト グループがないかをそのベースラインから判断します。
    • HTTP POSTのサイズにもとづいてアラートを発報できるSIEMやSOARなどのログ アグリゲーション ユーティリティがある場合は、ある特定のサイト(とくにある特定のIPアドレス)に対するPOSTイベント数が基準を上回っている時点がどこかにないかを調べます。また、POSTイベントのリクエスト サイズが所定の閾値を超えた場合にアラートを発報させることも検討します。たとえば、POSTイベントのファイル サイズが10MBを超える場合はアラートを出す、などです。これを行うにはチューニングと自社環境における通常トラフィックがどのようなものかという洞察が必要です。
    • 予期しないアカウントからのネットワーク トラフィック急増がないかどうか調べます。たとえば自社のドメイン管理者アカウント、Enterprise管理者アカウント、一般的サービス アカウントは、サイズの大きなPOSTリクエストを行うことが想定されているでしょうか。そうした場合にアラートを発報できるでしょうか。

IoC

私たちはこのスクリプトをEvent ID 4104のWindowイベント ログから復元したため、ディスク上に存在していたかもしれない元ファイルのハッシュは入手できていません。そのかわりにスクリプトのファイル名とスクリプトから復元した内容を記載しておきます。

注: IPアドレスやポート番号は公開しないことにしました。リクエストがあった場合でもこれらのIoCを提供することはありません

ファイル名

  • w1.ps1

YARAルール

このスクリプトの識別用に次のYARAルールが作成されました。本稿公開日現在で、過去1年間の間に、VirusTotal IntelligenceのRetro Huntシステムにおいて、このスクリプトには1件しか誤検出がありません。このルールは、被害者のID情報を設定する2行のコードと、HTTP経由でデータを漏出するために使うURI組み立て用の文字列連結メソッドを探します。

Unit 42 マネージド スレット ハンティング チームによるクエリ

追加リソース

付録: 包含・除外リスト

Work()関数での除外

CreateLocalJob()関数での包含

CreateLocalJob()関数での除外

Enlarged Image