エグゼクティブサマリー
本記事では、VVS Stealer(「VVS $tealer」とも表記される)の技術的分析について詳述します。特に、配布者がどのように難読化や検知回避を行っているかに焦点を当てます。
このスティーラーはPythonで記述されており、Discordユーザーを標的として、Discordアカウントに保存された認証情報やトークンなどの機密情報を窃取します。このスティーラーはかつて活発に開発されており、2025年4月と早くからTelegram上で販売が確認されていました。
VVS Stealerのコードは、Pyarmorによって難読化されています。Pyarmorは、静的解析やシグネチャベースの検知を妨害するためにPythonスクリプトを難読化するツールです。Pyarmorは正当な目的で使用されることもありますが、ステルス性の高いマルウェアの構築に悪用されることもあります。
マルウェア作者は、セキュリティツールによる検知を回避するために、高度な難読化技術をますます活用しており、悪意のあるソフトウェアの解析やリバースエンジニアリングを困難にしています。本記事では、VVS Stealerの動作をより深く理解するために、私たちがどのように検体の難読化を解除したかを示します。
Pythonはマルウェア作者にとって扱いやすく、かつ本脅威では複雑な難読化が用いられているため、結果として極めて効果的でステルス性の高いマルウェアファミリとなっています。
パロアルトネットワークスのお客様は、以下の製品とサービスを通じて保護されています。
- Advanced WildFire
- Advanced URL Filtering および Advanced DNS Security
- Cortex XDR および XSIAM は、Malware Prevention Engine(マルウェア対策エンジン)を採用することで、本記事で解説する脅威を防ぎます。
侵害の懸念がある場合や緊急の事案がある場合は、Unit 42 インシデントレスポンスチームまでご連絡ください。
| 関連する Unit 42 トピック | Infostealer, Anti-analysis, Discord, Pyarmor |
はじめに
Discordは、ソーシャルメッセージングおよびコミュニケーションプラットフォームであり、VVS Stealerのようなマルウェアの標的として人気が高まっています。VVS Stealerは、被害者のDiscord情報やブラウザデータを窃取するように設計されています。
図1は、VVS Stealerが宣伝している機能を示しています。これには以下が含まれます。
- Discordデータ(トークンおよびアカウント情報)の窃取
- インジェクションによるアクティブなDiscordセッションの乗っ取り
- Webブラウザデータ(Cookie、パスワード、閲覧履歴、オートフィル詳細)の抽出

このスティーラーは、スタートアップ時に自身を自動的にインストールすることで永続性を確保します。また、偽のエラーメッセージを表示したりスクリーンショットをキャプチャしたりするなど、ステルス的に動作します。この活動のより詳細な調査については、DeepCodeの記事『Investigating VVS $tealer: A Python-Based Discord Malware』を参照してください。
技術的分析
このセクションでは、以下のSHA-256ハッシュを持つ、Pyarmorで保護されたVVS Stealerのマルウェア検体を分析します。
- c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07
図2は、検体分析ワークフロー全体を示す要約図です。

ステップ 1:PyInstallerバイナリからの抽出
今回分析した検体は、PyInstallerパッケージとして配布されています。PyInstallerは、Pythonアプリケーションとその依存関係を1つのパッケージにバンドルし、追加のモジュールをインストールすることなくパッケージ化されたアプリを実行できるようにするツールです。
標準的なPyInstallerのインストールには、組み込みユーティリティであるpyi-archive_viewerが付属しています。私たちはこのユーティリティを使用して、検体から以下のファイルを抽出・検査しました。
- vvs という名前のPythonバイトコードファイル
- pyarmor_runtime_007444 サブフォルダ配下にある pyarmor_runtime.pyd という名前のPyarmorランタイムダイナミックリンクライブラリ(DLL)ファイル
- 同じサブフォルダ内の __init__.py ファイル。ここには以下の情報が含まれています。
- Pyarmorバージョン:9.1.4 (Pro)
- 固有ライセンス番号:007444
- タイムスタンプ:2025-04-27T11:04:52.523525
- 製品名:vvs
- 同じサブフォルダ内の __init__.py ファイル。ここには以下の情報が含まれています。
- python311.dll という名前のPython 3.11 DLLファイル
- ファイルバージョン情報は、Pythonのバージョンが3.11.5であることを示しています。
PyInstallerは、Pythonバイトコード(上記1.のファイル)を生(raw)の形式で保存します。この生の形式とは、値 e3 で始まるバイトコードシーケンスを指します。値 e3 は、フラグとタイプの両方を定数 FLAG_REF を介して組み合わせたものです。
値 e3 が表すタイプは、type = e3 & ~FLAG_REF として計算されます。つまり、値 e3 は実際にはタイプ 0x63(文字 c)であり、列挙定数 TYPE_CODE としても知られています。この導出の完全な実装は、CPython 3.11のコードベースで確認できます。
以下の図3は、marshalモジュールによってシリアル化されたこのコードオブジェクトが、付随する16バイトのヘッダー(青色でマーク)を欠いた状態であることを示しています。デコンパイラがファイルを拒否しないように十分なPython情報を提供するには、デコンパイルの前にヘッダー値の少なくとも1つ(4バイトのリトルエンディアン形式のPython 3.11.5のマジックナンバー)を復元する必要があります。これは、Pythonデコンパイラが入力として有効なPythonバイトコード(.pyc)ファイルを期待するためです。

私たちは分析を開始するにあたって、vvs という名前のPythonバイトコード(.pyc)ファイルをデコンパイルし、同等のPythonソースコード(.py)を復元しました。
ステップ 2:Pythonソースコードへのデコンパイル
Pycdcは、C++で記述されたPythonバイトコードデコンパイラです。これはDecompyle++プロジェクトの一部です。Python 3.11のバイトコードを「有効で人間が読めるPythonソースコードに戻す」ことをサポートしています。(出典:GitHub)。PyLingualもまた別のPythonバイトコードデコンパイラです。
コードリポジトリのクローンとコードベースのコンパイルを行った後、生成された実行可能ファイルを以下のように呼び出すことで、Pycdcを介してPythonバイトコードをPythonソースコードにデコンパイルできます。
- pycdc.exe -c -v "3.11.5" "vvs.pyc" > "vvs.py"
これにより、図4に示すデコンパイルされたPythonソースコードが生成されます。

その後、Python 3の ast.NodeVisitor を介して抽出できる最後の関数引数を分析します。
ステップ 3:Pyarmor難読化の解読
ペイロードは、図5に示すPyarmorヘッダーで始まります。

暗号化は全体を通して、128ビット鍵を使用したAdvanced Encryption Standard(AES)アルゴリズムで行われ、初期値2のカウンター(CTR)モードで動作します(すなわち、AES-128-CTR)。表1にフィールドの内訳を示します。
| オフセット | 値 | 説明 |
| 0x00 … 0x07 | PY007444 | 固有ライセンス番号を含むファイルシグネチャ |
| 0x09 | 03 | Pythonメジャーバージョン |
| 0x0a | 0b | Pythonマイナーバージョン |
| 0x14 | 09 | 保護タイプ:
|
| 0x1c … 0x1f | 40 00 00 00 | ELFペイロードの開始位置(リトルエンディアン形式) |
| 0x24 … 0x27 | 12 c9 06 00 | AES-128-CTRノンスの最初の4バイト |
| 0x2c … 0x33 | dc d2 98 a1 ea 11 fd f4 | AES-128-CTRノンスの残りの8バイト |
| 0x38 … 0x3b | a0 7f 02 00 | ELFペイロードの終了位置(リトルエンディアン形式 |
|---|
表1. Pyarmorヘッダーに存在するフィールドの内訳。
この同じパターン(黄色で強調表示)は、Pyarmorバイトコードペイロードの抽出と復号のために、ELFペイロードの終了後にもう一度繰り返されます。
BCCモード
BCC(おそらくByteCode-to-Compilationの略)モードは、スクリプト内の「ほとんどの関数やメソッドを同等のC関数に変換します。これらのC関数は直接マシン語命令にコンパイルされ、その後、難読化されたスクリプトによって呼び出されます。」(出典:Pyarmorドキュメント)
BCCモードは以下のように呼び出されます: pyarmor gen --enable-bcc script.py。
これらの変換されたC関数は、Pyarmorによってマーシャル化されたバイトコードとともに生成される、別のELFファイルに格納されます。
Python定数とBCC関数のマッピングは、この実装を使用して取得できます。例えば、Pythonメソッド get_encryption_key(browser_path) において、定数 __pyarmor_bcc_58580__ はBCC関数 bcc_180 にマップされ、その関数本体はELFファイルのオフセット 0x4e70 に位置しています。
ELFファイルの内容、特に bcc_ftable 構造体に関するこの分析を参照すると、図6はデコンパイルされたBCC関数 bcc_180 の一部を示しています。

図7に示すように、Pythonメソッド get_encryption_key の元のコードと同等のものを概ね復元できます。

マーシャル化されたバイトコード形式
Pyarmor 9のマーシャル化されたバイトコードは、標準のPython 3.11バイトコードとはいくつかの点で異なります。まず、co_flags フィールドの 0x20000000 ビットがセットされており、これがPyarmorによって難読化されていることを示しています。次に、追加のデータフィールドがあり、その長さは最初のバイトの値によって示されます。
さらに、バイトコードシーケンスを正常に復号するには、deopt_code() を無効にする必要があります。暗号化パラメータについては、本記事の後半で説明します。
コードオブジェクトの構造
Pyarmorのコードオブジェクトは特別に細工されており、特定のアーティファクトが含まれていることが予想されます。逆アセンブル結果のプリアンブル(前文)に LOAD_CONST __pyarmor_enter_*__ 命令が、トレーラー(後文)に LOAD_CONST __pyarmor_exit_*__ 命令が見られるのが一般的です。表2に示すように、これら2つの命令が暗号化されたバイトコードをラップします。
| 操作 | 議論 |
| LOAD_CONST | __pyarmor_enter_58592__ |
| LOAD_CONST | \x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20\x16\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 |
| … 暗号化されたバイトコード列(次節で検討する)… | |
| LOAD_CONST | __pyarmor_exit_58593__ |
| LOAD_CONST | \x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x20\x16\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 |
表2. <module> の逆アセンブルリストにおけるPyarmor関連の命令。
暗号化されたバイトコードシーケンスが復号されると、暗号化された文字列やBCC関数の呼び出しが明らかになる可能性があります。暗号化された文字列(本記事の後半で確認)の前には LOAD_CONST __pyarmor_assert_*__ 命令があります。また、BCC関数(本記事の前半で確認)を呼び出すための LOAD_CONST __pyarmor_bcc_*__ 命令もあります。
コードオブジェクトの暗号化
開始マーカー(__pyarmor_enter_*__)と終了マーカー(__pyarmor_exit_*__)の間のバイトコードシーケンスは、AES-128-CTRで暗号化されています。関連するAES鍵(273b1b1373cf25e054a61e2cb8a947b8)は、固有ライセンス番号にリンクされたPyarmorランタイムDLLから抽出されます。
一方、対応するAESノンス排他的論理和(XOR)キー(2db99d18a0763ed70bbd6b3c)はPyarmorバイトコードペイロードに固有のものであり、この値を抽出するロジックの実装が存在します。このキーは、終了マーカー(__pyarmor_exit_*__)の12バイトとXOR演算され、復号に使用される正しいAESノンスが生成されます。
文字列の暗号化
同様に、8文字を超える文字列定数はAES-128-CTRで暗号化されています(Pyarmorの用語では「mixed」として知られています)。関連するAES鍵も 273b1b1373cf25e054a61e2cb8a947b8 ですが、今回は対応するAESノンス(692e767673e95c45a1e6876d)が、固有ライセンス番号にリンクされたPyarmorランタイムDLLから計算されます。
さらに、0x81 のプレフィックス値は文字列定数が暗号化されていることを示します。そうでない場合は、代わりに 0x01 のプレフィックス値が使用されます。
Pyarmorによる保護が解除されたので、次のセクションではVVS Stealerの主要な機能のいくつかについて解説を進めます。
マルウェアの機能
BCCモードやAES-128-CTR文字列暗号化を含むPyarmorの難読化層が正常に取り除かれたことで、私たちは根底にあるPythonロジックを明らかにすることができました。この難読化解除されたコードにより、単なるデータ窃取だけでなく、アクティブなセッションハイジャックや永続化のために設計されたスティーラーであることが判明しました。以下のセクションでは、この分析中に発見されたVVS Stealerの具体的な動作機能について詳述します。
このマルウェア検体は、2026-10-31 23:59:59 以降に有効期限が切れます。それ以降は、自身を早期終了させることで動作を停止します。
マルウェア検体は、すべてのHTTPリクエストを固定のUser-Agent文字列 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 を送信して実行します。
それでは、Telegramで宣伝されている主要なマルウェア機能の概要を説明します。
Discordデータ
マルウェア検体はまず、暗号化されたDiscordトークンの可能性のあるものを検索します。暗号化されたDiscordトークンは、プレフィックス dQw4w9WgXcQ: で始まる文字列です。マルウェア検体は正規表現を使用して、この文字列プレフィックスからパターンを形成します。そして、LevelDBディレクトリ内に保存されている .ldb または .log 拡張子を持つファイルの内容に対して、このパターンを使用して検索を行います。
次に、マルウェア検体は、Data Protection Application Programming Interface (DPAPI) を介して Local State ファイル内の encrypted_key 値を復号します。この復号された encrypted_key 値をAES鍵パラメータとして、マルウェア検体はGalois/Counter Mode (GCM) モードで動作するAESアルゴリズムを暗号化されたDiscordトークンに適用し、それらを復号します。
その後、マルウェア検体は復号されたDiscordトークンを使用して、以下のユーザー情報について様々なDiscordアプリケーションプログラミングインターフェース(API)エンドポイントにクエリを送信します。
- Nitro サブスクリプション(Discordプレミアム機能)
- 支払い方法
- ユーザーID
- ユーザー名
- メールアドレス
- 電話番号
- フレンド
- ギルド(サーバー)
- 多要素認証(MFA)ステータス
- ロケール
- 検証ステータス
- アバター画像
- IPアドレス(ipifyサービス経由)
- コンピュータ名
これらすべての情報を収集した後、マルウェア検体はJavaScript Object Notation (JSON) 形式で外部への送信(エクスフィルドレーション)を進めます。送信は、事前に定義されたWebhookエンドポイント(%WEBHOOK% 環境変数およびハードコードされたフォールバックURL)へのHTTP POSTリクエストを介して行われます。
Webhookは「Discordチャンネルにメッセージを投稿するための手間のかからない方法です。ボットユーザーや認証を必要としません。」(出典:Discord Developer Portal)。
Discordへのインジェクション
この機能を担うコードは Inj クラス(おそらくInjectionの略)にあります。
このクラスでは、マルウェア検体はまず、実行中のDiscordアプリケーションプロセスがあればそれを強制終了します。次に、リモートファイル injection-obf.js(-obf サフィックスはおそらくスクリプトの難読化バージョンを表す)からJavaScript (JS) ペイロードをダウンロードし、WebhookエンドポイントURLを置き換えて、discord_desktop_core としてDiscordアプリケーションディレクトリに配置します。このJSファイルは JavaScript Obfuscator Tool によって難読化されており、Obfuscator.io Deobfuscator を介して難読化を解除できます。
注入されたJSコードの主な機能の一部を以下のスクリーンショットで強調表示します。図8は、設定とデータ送信のコードスニペットです。

図8は、Electron フレームワークに基づいて、Discordアプリケーション内で永続性を確立する役割を持つ注入されたJSコードを示しています。このフレームワークは、Atom Shell Archive Format (ASAR) アーカイブを使用して、アプリケーションのコードベース全体を単一のファイルにバンドルします(図9参照)。

図10は、Chrome DevTools Protocol (CDP) を介してネットワークトラフィックを監視する役割を持つ注入されたJSコードを示しています。

図11は、注入されたJSコード内のサポートユーティリティ関数とイベントフックを示しています。イベントフックは、Discordアプリケーションユーザーが特定のアクションを実行したときに実行されるコールバック関数です。対象となるアクションは、ユーザーがバックアップコードを表示したとき、パスワードを変更したとき、または支払い方法を追加したときです。これらのアクションにリンクされたコールバック関数は、Discordユーザーのアカウント情報や請求情報を収集することができます。

その後、マルウェア検体は Update.exe を介して侵害されたDiscordアプリケーションプロセスを再起動します。これはコマンドラインスイッチ --processStart を使用して行われます。
Webブラウザデータ
マルウェア検体は、以下のWebブラウザアプリケーションのリストを標的とします。
- Chrome
- Edge
- 7Star
- Amigo
- Brave
- CentBrowser
- Discord
- Epic Privacy Browser
- Iridium
- Kometa
- Lightcord
- Mozilla Firefox
- Opera
- Orbitum
- Sputnik
- Torch
- Uran
- Vivaldi
- Yandex
これらのターゲットに対して、マルウェア検体は以下のデータが存在する場合に抽出します。
- オートフィル
- Cookie
- 履歴
- パスワード
これらのデータが抽出されると、マルウェア検体はそれを <USERNAME>_vault.zip という名前の単一のZIPアーカイブファイルに圧縮して送信の準備をします。その後、Discordデータの送信プロセスと同様に、事前に定義されたWebhookエンドポイントへのHTTP POSTリクエストを介してこのファイルを外部へ送信します。
スタートアップ時の永続化
マルウェア検体は、自身を %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup フォルダにコピーして、スタートアップ時の永続性を実現します。マルウェアはユーザーのデバイスに留まり、例えばユーザーがDiscordアプリケーションの新しいコピーをインストールしようとした場合でも、データの送信を継続できるようにします。
偽のエラーメッセージ
マルウェア検体はWin32 API、具体的には User32.dll ライブラリの MessageBoxW 関数を使用して、コンピュータの再起動が必要な偽の致命的なエラーに関するモーダルメッセージボックスを表示します。モーダルメッセージボックスとは、アプリケーションが続行する前にユーザーの操作を必要とする小さなダイアログウィンドウのことです(図12参照)。

結論
VVS Stealerは、Pyarmorのような正当な目的で使用できるツールが、Discordなどの人気プラットフォームの認証情報を乗っ取ることを目的としたステルス性の高いマルウェアの構築にも悪用され得ることを示しています。この出現は、防御側が認証情報の窃取やアカウントの悪用に対する監視を強化する必要があることを示唆しています。
パロアルトネットワークスによる保護と緩和策
パロアルトネットワークスのお客様は、以下の製品を通じて、上記で議論された脅威から保護されています。
Advanced WildFire の機械学習モデルと分析技術は、本調査で共有された指標に照らして見直しおよび更新されています。
Advanced URL Filtering および Advanced DNS Security は、この活動に関連する既知のドメインとURLを悪意のあるものとして識別します。
Cortex XDR および XSIAM は、Malware Prevention Engine(マルウェア対策エンジン)を採用することで、本記事で解説する脅威を防ぎます。このアプローチは、Advanced WildFire、Behavioral Threat Protection(振る舞い検知による脅威防御)、およびLocal Analysis(ローカル分析)モジュールを含む複数の保護層を組み合わせ、既知および未知のマルウェアがエンドポイントに害を及ぼすのを防ぎます。
侵害された可能性があると思われる場合や緊急の事案がある場合は、Unit 42 インシデントレスポンスチームにご連絡いただくか、以下にお電話ください。
- 北米フリーダイヤル: +1 (866) 486-4842 (866.4.UNIT42)
- 英国: +44.20.3743.3660
- 欧州および中東: +31.20.299.3130
- アジア: +65.6983.8730
- 日本: +81.50.1790.0200
- オーストラリア: +61.2.4062.7950
- インド: 000 800 050 45107
- 韓国: +82.080.467.8774
パロアルトネットワークスは、これらの調査結果をCyber Threat Alliance (CTA) のメンバーと共有しています。CTAのメンバーはこのインテリジェンスを使用して、顧客に迅速に保護を展開し、悪意のあるサイバー攻撃者を組織的に阻止します。Cyber Threat Alliance の詳細については、こちらをご覧ください。
侵害の痕跡(IoC)
マルウェア検体のSHA-256ハッシュ:
- 307d9cefa7a3147eb78c69eded273e47c08df44c2004f839548963268d19dd87
- 7a1554383345f31f3482ba3729c1126af7c1d9376abb07ad3ee189660c166a2b
- c7e6591e5e021daa30f949a6f6e0699ef2935d2d7c06ea006e3b201c52666e07
Discord Webhook URL
- hxxps[://]ptb.discord[.]com/api/webhooks/1360401843963826236/TkFvXfHFXrBIKT3EaqekJefvdvt39XTAxeOIWECeSrBbNLKDR5yPcn75uIqKEzdfs9o2
- hxxps[://]ptb.discord[.]com/api/webhooks/1360259628440621087/YCo9eVnIBOYSMn8Xr6zX5C7AJF22z26WljaJk4zr6IiThnUrVyfWCZYs6JjSC12IC8c0
追加リソース
- Unpacking Pyarmor v8+ scripts – Leonard Rapp and Hendrik Eckardt, G DATA Advanced Analytics GmbH
- Obfuscated Malicious Python Scripts with PyArmor – Xavier Mertens, SANS Internet Storm Center