株式会社Ninjastars
取締役:齊藤和輝
今回の記事では、難読化方式の1つであるパッカー方式についてより詳細な部分までご紹介いたします。
難読化方式の概要は前回の記事をご参考に。
www.ninjastars-net.com
動作環境
Windows10 64bit版
パッカー方式について
多くのマルウェアはリバースエンジニアリング対策としてパッカーでパッキングされています。
パッカーはリバースエンジニアリング対策として有効な手段であるが、必ずしも万能な対策とは言い難いと言えます。
強度の高いパッカーもあれば強度の低いパッカーも存在します。
有名なパッカーを使用しているのであれば専用のアンパッキングツールも存在する可能性が高く、自然に強度は低くなります。
また、パッカーはアンパッキングルーチンの過程で多くのAPIの呼び出しが必要になり、プログラムの本処理よりもパッカーの処理の方が重くなってしまいます。
APIの呼び出しが多い構造で実装すると、攻撃起点も増え結果として強度の低いパッカーが出来上がってしまいます。
このような理由からパッカーは、できる限り最小限の実装コードで最大限の効果を得られるものでなくてはいけません。
UPXについて
UPXは様々な実行ファイルフォーマットに対応したフリーのパッカーで、設計は非常にシンプルでありアンパック妨害機能も備わっていません。
そのため、アンパッキング技術の基礎を理解する教材として非常に優れています。
実際に「Hello,World!」を出力するだけのコードをコンパイルしてUPXでパッキングしていきましょう。
#include<stdio.h> int main(int argc, char* argv[]) { printf("Hello,World!"); return 0; }
下記のサイトにてUPXをダウンロードしてください。
upx.github.io
コンパイルされた実行ファイルをupx.exeの引数にすることでパッキングが施されます。
42KBあった実行ファイルが26KBまで圧縮されていたらパッキングされている状態になっています。
では、パッキングが施される前と施された後の実行ファイルをそれぞれ「Ghidra」で逆アセンブルして比較してみましょう。画像のようにパッキングするとどこでprintf関数がcallされているのかという情報が抜け落ちてしまいます。
また、各セクションの情報も符号化されておりソフトウェアの動作を解析することが困難になります。
次にUPXでパッキングされたプログラムの構造と挙動がどのようになっているのかをご紹介します。UPXでパッキングされたプログラムはアンパッキングルーチンが先に実行され、アンパッキングしたオリジナルコードをメモリ上に展開し実行しています。
パッカーの種類によってプログラムの動きは異なってきますが、パッキングされた元のプログラムは必ずメモリ上のどこかでアンパッキングされた後、実行されています。
マルウェアの場合、ハッカーは容易にプログラムを解析されないように独自でパッカーを実装していることが多いです。
そのため、我々セキュリティエンジニアは独自に実装されているパッカーを回避し、オリジナルコードを発見しなければいけません。
プログラムを実行するときに必ず特定のメモリ領域上に書き込みされてから実行しているため、メモリ上で書き込みを行っている部分を列挙すればどこかにOEP(オリジナルエントリーポイント)を発見することができます。
OEPの発見とメモリダンプ
パッカーでは大抵、エントリーポイントの近くにPUSH命令(例:PUSHAD)があります。
これによりレジスタの情報をバックアップしてからアンパッキングルーチンを実行できます。
したがってアンパッキング中に好きなようにレジスタの値を変更して使えるようになります。
アンパッキングルーチンが完了し、プログラム実行する段階でPOPAD命令で元のレジスタ値をスタックから取得します。
POPAD命令の後にjmp系命令(ret命令や関数のリターンアドレスを使用している場合がある)を探してOEPの場所を特定していきます。
実際にOllyDbgで先ほどのhello.exeをメモリダンプをしていきましょう。
手順は以下の通りです。
1.PUSHADを実行
2.ESPの情報がPUSHされたスタックのアドレスにハードウェア・ブレークポイントを設定
3.実行しブレークポイントからjmp系命令を探す
4.jmpした先でメモリダンプをする
OllyDumpでdump.exeを作成していきます。
今回はOEPが4012D0であったのでModifyを12D0に設定し、Rebuild Importのチェックを外します。
dump.exeという名前でメモリダンプしましょう。
作成されたdump.exeを実行すると以下のようなエラーが発生すると思います。実はパッキングされた際、API呼び出しのために必要なテーブルであるIATが破壊され独自のIATが生成されます。
そのためIATを自力で復元しなければいけません。
IATの復元
IATを復元するためにはバイナリで使用しているAPIがどのアドレスにどう繋がっているのかをすべて探し出す必要があります。
プログラムが複雑であればあるほど呼び出されているAPIの数が多くなり、非常に根気のいる作業となってしまいます。
それを補助するツールが「Import REC」です。
パッキングされたプログラムを実行することで呼び出されているAPIとそのアドレスの情報を一気に取得してくれます。
今回はOllyDbgにてhello.exeのOEPで止めた状態のままこのツールを使用します。
まず、Attach to an Active Processにてhello.exeを選択します。
OEPには12D0を入力して、IAT AutoSearchを実行してください。そのあと、Get Importsを実行することで先ほどの情報を取得していきます。
Fix Dumpにてメモリダンプをしたdump.exeを選択し、IATの情報を入れ込みます。
これでIATが復元され、正常にプログラムが動作するようになりました。
Ghidraで解析
UPXでパッキングされている時と違うのはImportされている関数が全て表示されていることと、きちんと元のプログラムの処理が見れていることです。これで心置きなく解析することができます!
注意事項
本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。
本レポートについて
お問い合せ
E-mail:saito@ninjastars-net.com
株式会社Ninjastars
取締役:齊藤和輝