Python バイトコードの難読化解除

Uncompyle2 exception trace news

序章

FLARE チームは調査中に、 py2exeを使用してパッケージ化された興味深い Python マルウェア サンプル (MD5: 61a9f80612d3f7566db5bdf37bbf22cf ) を発見しました。 Py2exe は、Python スクリプトをコンパイルして実行可能ファイルにパッケージ化する一般的な方法です。この種のマルウェアに遭遇すると、通常は Python のソース コードを逆コンパイルして読み取るだけです。しかし、このマルウェアは異なり、簡単に逆コンパイルできないようにバイトコードが操作されていました。

このブログでは、マルウェアを分析し、難読化をどのように削除したかを示します。これにより、クリーンな逆コンパイルを作成できました。ここでは、難読化された Python バイトコード (https://github.com/fireeye/flare-bytecode_graph) を分析するのに役立つソース コードを bytecode_graph モジュールにリリースします。このモジュールを使用すると、バイトコード ストリームから命令を削除し、オフセットをリファクタリングし、さらに分析できる新しいコード オブジェクトを生成できます。

バックグラウンド

Py2exe は、Python スクリプトを実行可能ファイルに変換するユーティリティです。これにより、Python インタープリターがインストールされていないシステムでスクリプトを実行できます。 Py2exe バイナリの分析は、通常、Python バイトコードの抽出から始まり、続いてmetauncompyle2などのモジュールを使用してコード オブジェクトを逆コンパイルする簡単なプロセスです。付録 A には、Py2exe バイナリからコード オブジェクトを抽出する方法を示すサンプル スクリプトが含まれています。

uncompyle2 を使用してこのサンプルを逆コンパイルしようとすると、図 1 に示す例外が生成されます。この例外は、逆コンパイラが想定していないコード シーケンスがバイトコード ストリームに含まれていることを示しています。

Uncompyle2 exception trace
図 1: Uncompyle2 例外トレース

逆コンパイラを破壊する難読化

逆コンパイラが失敗する理由を理解するには、まずバイトコードの逆アセンブリを詳しく調べる必要があります。 Python バイトコードを逆アセンブルする簡単な方法は、組み込みモジュールdisを使用することです。 disモジュールを使用する場合、バイトコードと同じバージョンの Python を使用して正確な逆アセンブリを行うことが重要です。図 2 には、スクリプト「import sys」を逆アセンブルする対話型セッションの例が含まれています。逆アセンブリ出力の各行には、オプションの行番号が含まれ、その後にバイトコード オフセットが続き、最後にバイトコード命令ニーモニックと任意の引数が続きます。

バイトコードの逆アセンブルの例
図 2: バイトコードの逆アセンブリの例

付録 A のサンプル スクリプトを使用すると、コード オブジェクトの逆アセンブリを表示して、逆コンパイラが失敗する原因をよりよく理解できます。図 3 には、このサンプルでスクリプトを実行することによって生成された逆アセンブリの一部が含まれています。

バイトコードの分解
図 3: バイトコードの逆アセンブル

逆アセンブリをよく見ると、コードのロジックに影響を与えない不要なバイトコード シーケンスがいくつかあることに注意してください。これは、標準のコンパイラがバイトコードを生成しなかったことを示唆しています。最初の驚くべきバイトコード構造は、たとえば、バイトコード オフセット 0 にあるNOPの使用です。インタプリタはパイプラインの問題に対処する必要がないため、 NOP命令は通常、コンパイルされた Python コードには含まれません。 2 つ目の驚くべきバイトコード構造は、一連のROT_TWOおよびROT_THREE命令です。 ROT_TWO命令は上位 2 つのスタック項目をローテーションし、 ROT_THREE命令は上位 3 つのスタック項目をローテーションします。 2 回連続してROT_TWO命令または 3 回ROT_THREE命令を呼び出すと、スタックは命令シーケンスの前と同じ状態に戻ります。したがって、これらのシーケンスはコードのロジックには影響しませんが、逆コンパイラを混乱させる可能性があります。最後に、 LOAD_CONSTPOP_TOPの組み合わせは不要です。 LOAD_CONST命令は定数をスタックにプッシュし、 POP_TOP命令は定数を削除します。これにより、スタックは再び元の状態のままになります。

これらの不要なコード シーケンスは、 metauncompyle2などのモジュールを使用したバイトコードの逆コンパイルを防ぎます。 ROT_TWOおよびROT_THREEシーケンスの多くは、検査時にエラーを生成する空のスタックで動作します。これは、両方のモジュールが Python List オブジェクトを使用してランタイム スタックをシミュレートするためです。空のリストでpop操作を行うと、逆コンパイル プロセスを停止する例外が生成されます。対照的に、Python インタープリターがバイトコードを実行すると、操作を実行する前にスタックでチェックが行われません。たとえば、図 4 のceval.cROT_TWOを取り上げます。

ROT_TWO ソース
図 4: ROT_TWO ソース

図 5 のceval.cTOP、SECOND、SET_TOP 、およびSET_SECONDのマクロ定義を見ると、サニティ チェックがないため、これらのコード シーケンスが停止することなく実行されています。

マクロ定義
図 5: マクロ定義

NOPおよびLOAD_CONST/POP_TOPシーケンスは、次または前の命令が特定の値であると予想される状況で、逆コンパイル プロセスを停止します。 uncompyle2のデバッグ トレースの例を図 6 に示します。ここでは、前の命令がジャンプまたはリターンであると予想されます。

難読化の削除

難読化の種類が特定されたので、次のステップは、逆コンパイルが成功することを期待してバイトコードをクリーンアップすることです。 disモジュールのopmapディクショナリは、バイトコード ストリームを操作するときに非常に役立ちます。 opmapを使用すると、命令は特定のバイトコード値ではなく名前で参照できます。たとえば、 NOPバイトコード バイナリ値はdis.opmap[‘NOP’]で使用できます。

付録 B には、 ROT_TWO、ROT_THREE 、およびLOAD_CONST/POP_TOPシーケンスをNOP命令に置き換え、新しいコード オブジェクトを作成するサンプル スクリプトが含まれています。マルウェアで付録 A のスクリプトを実行して生成された逆アセンブリを図 6 に示します。

きれいな分解
図 6: クリーンな分解

この時点で、不要な命令シーケンスが NOP に置き換えられ、逆アセンブリが多少読みやすくなっていますが、バイトコードは依然として逆コンパイルに失敗しています。失敗は、 uncompyle2metaが例外を処理する方法によるものです。この問題は、例外ハンドラーを含む単純なスクリプトを使用して図 7 に示されています。

例外ハンドラ
図 7: 例外ハンドラー

図 7 では、例外ハンドラはオフセット 0 でSETUP_EXCEPT命令を使用して作成され、ハンドラ コードはオフセット13で始まり、3 つのPOP_TOP命令を使用しています。 metaモジュールとuncompyle2モジュールの両方が、例外ハンドラーの前に命令を調べて、それがジャンプ命令であることを確認します。命令がジャンプでない場合、逆コンパイル プロセスは停止します。このマルウェアの場合、難読化命令が削除されているため、命令はNOPです。

この時点で、逆コンパイルを成功させるには、2 つのオプションがあります。まず、命令の順序を並べ替えて、逆コンパイラが期待する場所にあることを確認できます。または、すべての NOP 命令を削除することもできます。ジャンプ命令の絶対アドレスと相対アドレスも更新する必要があるため、どちらの方法も複雑で面倒です。ここでbytecode_graphモジュールの出番です。bytecode_graphモジュールを使用すると、バイトコード ストリームから命令を簡単に置き換えたり削除したり、それに応じてオフセットが自動的に更新された新しいストリームを生成したりできます。図 8 は、 bytecode_graphモジュールを使用してコード オブジェクトからすべての NOP 命令を削除する関数の例を示しています。

NOP 命令を削除する bytecode_graph の例
図 8: NOP 命令を削除した bytecode_graph の例

概要

このブログでは、 bytecode_graphモジュールを使用して Python コード オブジェクトから単純な難読化を削除する方法を示しました。使いやすく、トリッキーな py2exe サンプルを処理するのに最適なツールであることがわかると思います。 bytecode_graphは、pip ( pip install bytecode-graph ) を介して、または FLARE チームの Github ページ (https://github.com/fireeye/flare-bytecode_graph) からダウンロードできます。

このブログで説明されている難読化を削除するサンプル スクリプトは、https://github.com/fireeye/flare-bytecode_graph/blob/master/examples/bytecode_deobf_blog.py にあります。

61a9f80612d3f7566db5bdf37bbf22cf
ff720db99531767907c943b62d39c06d
aad6c679b7046e568d6591ab2bc76360
ba7d3868cb7350fddb903c7f5f07af85

付録 A: Py2exe リソースを抽出して逆アセンブルする Python スクリプト

難読化解除付録 A

付録 B: 難読化を解除するサンプル スクリプト

難読化解除-付録-b

参照: https://www.mandiant.com/resources/blog/deobfuscating-python

Comments

タイトルとURLをコピーしました