株式会社Ninjastars 技術系ブログ

「本質的安全を提供し、デジタル社会を進化させる!!!」

リバースエンジニアリング対策 -難読化編パート2-

株式会社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」で逆アセンブルして比較してみましょう。

f:id:Ninjastars:20190415161205p:plainf:id:Ninjastars:20190415161209p:plain
UPXによるパッキング施行前
f:id:Ninjastars:20190415161336p:plainf:id:Ninjastars:20190415161339p:plain
UPXによるパッキング施行後
画像のようにパッキングするとどこでprintf関数がcallされているのかという情報が抜け落ちてしまいます。
また、各セクションの情報も符号化されておりソフトウェアの動作を解析することが困難になります。
次にUPXでパッキングされたプログラムの構造と挙動がどのようになっているのかをご紹介します。
f:id:Ninjastars:20190426172201p:plain
プログラムの構造
UPXでパッキングされたプログラムはアンパッキングルーチンが先に実行され、アンパッキングしたオリジナルコードをメモリ上に展開し実行しています。
パッカーの種類によってプログラムの動きは異なってきますが、パッキングされた元のプログラムは必ずメモリ上のどこかでアンパッキングされた後、実行されています。
マルウェアの場合、ハッカーは容易にプログラムを解析されないように独自でパッカーを実装していることが多いです。
そのため、我々セキュリティエンジニアは独自に実装されているパッカーを回避し、オリジナルコードを発見しなければいけません。
プログラムを実行するときに必ず特定のメモリ領域上に書き込みされてから実行しているため、メモリ上で書き込みを行っている部分を列挙すればどこかにOEP(オリジナルエントリーポイント)を発見することができます。

OEPの発見とメモリダンプ

パッカーでは大抵、エントリーポイントの近くにPUSH命令(例:PUSHAD)があります。
これによりレジスタの情報をバックアップしてからアンパッキングルーチンを実行できます。
したがってアンパッキング中に好きなようにレジスタの値を変更して使えるようになります。
アンパッキングルーチンが完了し、プログラム実行する段階でPOPAD命令で元のレジスタ値をスタックから取得します。
POPAD命令の後にjmp系命令(ret命令や関数のリターンアドレスを使用している場合がある)を探してOEPの場所を特定していきます。
実際にOllyDbgで先ほどのhello.exeをメモリダンプをしていきましょう。
手順は以下の通りです。
1.PUSHADを実行
2.ESPの情報がPUSHされたスタックのアドレスにハードウェア・ブレークポイントを設定
3.実行しブレークポイントからjmp系命令を探す
4.jmpした先でメモリダンプをする

f:id:Ninjastars:20190520130031p:plain
EntryPointにはPUSHAD命令がある
f:id:Ninjastars:20190520130117p:plain
ハードウェア・ブレークポイント
f:id:Ninjastars:20190520130231p:plain
jmp命令実行後
f:id:Ninjastars:20190520163959p:plain
OllyDump
OllyDumpでdump.exeを作成していきます。
今回はOEPが4012D0であったのでModifyを12D0に設定し、Rebuild Importのチェックを外します。
dump.exeという名前でメモリダンプしましょう。
作成されたdump.exeを実行すると以下のようなエラーが発生すると思います。
f:id:Ninjastars:20190520164345p:plain
エラー
実はパッキングされた際、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されている関数が全て表示されていることと、きちんと元のプログラムの処理が見れていることです。

f:id:Ninjastars:20190520170034p:plain
シンボル情報
f:id:Ninjastars:20190520170050p:plain
元通り!
これで心置きなく解析することができます!

注意事項

本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:saito@ninjastars-net.com

株式会社Ninjastars
取締役:齊藤和輝