セキュリティ
アセンブラ
マルウェア

マルウェア解析での逆アセンブルとその攻防(後編)

では、マルウェア解析での逆アセンブルとその攻防(前編)の続きを行きます。


前回までのあらすじ

前編ではタイトルでマルウェア解析の攻防と称しておきながらアセンブリ言語の解析と逆アセンブルの仕方について述べただけでした。

具体的に、"Hello World"と出力するコードをC言語で書き、それをコンパイルして実行ファイルを作成、それを逆アセンブルするという作業を行いました。

しかし、実際に逆アセンブルとはどうやって行われるのか。

後編ではそれに着目し、マルウェア解析ではどのような攻防戦になるのかということを見ていきます。


  • 逆アセンブルのアルゴリズム

逆アセンブルにおいて、前編にて線形逆アセンブルフロー志向型逆アセンブルがあるということを述べました。

これらについて紹介していきます。


線形逆アセンブル

アセンブリのファイルを見たように、コンピュータへ命令を実行させるには細かくメモリにデータを割り当ててそれらをうまいこと入出力させて....というものでした。

例えば以下のアセンブリ言語のファイルを見てみましょう。

Disassembly of section .text:

0000000100000f60 <_main>:
100000f60: 55 push rbp
100000f61: 48 89 e5 mov rbp,rsp
100000f64: 48 83 ec 10 sub rsp,0x10
100000f68: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b] # 100000faa <_main+0x4a>
100000f6f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
100000f76: b0 00 mov al,0x0
100000f78: e8 0d 00 00 00 call 100000f8a <_main+0x2a>
100000f7d: 31 c9 xor ecx,ecx
100000f7f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
100000f82: 89 c8 mov eax,ecx
100000f84: 48 83 c4 10 add rsp,0x10
100000f88: 5d pop rbp
100000f89: c3 ret

アセンブリの読み方としては左の数字列がメモリ番号だと言いましたが、これを見ると100000f6fの次が100000f76となっているように各命令ごとにある程度ブロックがあるわけです。

その特性を利用して逆アセンブルするのが線形逆アセンブルというアルゴリズムになります。

つまりどういうことかというと、線形逆アセンブルでは「逆アセンブルされた命令が次に何バイト逆アセンブルするべきかを決定する」とします。

この線形逆アセンブラをテキトーに実装すると以下のようになります。


LinearDissassemble.c

char buffer[BUF_SIZE];

int position = 0;

while (position < BUF_SIZE) {
x86_insn_t insn;
int size = x86_disasm(buf, BUF_SIZE, 0, position, &insn);
if (size != 0) {
char disassembly_line[1024];
x86_format_insn(&insn, disassembly_line, 1024, intel_syntax);
printf("%s\n", disassembly_line); position += size;
} else {
/* invalid/unrecognized instruction */
position++;
}
}

x86_cleanup();


まず最初に命令ブロックを発見してそれを逆アセンブル、その後その結果が次に何バイト分を逆アセンブルするのがいいのか決定するわけです。


フロー志向型逆アセンブル

さて、線形逆アセンブルのアルゴリズムはちゃんと逆アセンブルできるし一見良さげです。

しかし、これには落とし穴が存在します。

その落とし穴とは、線形逆アセンブルでは命令とデータがきちんと順序よく並んでいるのを前提としているという点です。

これがなぜ落とし穴になりうるかを説明するために以下のようなシチュエーションを考えましょう。

                test    eax, eax

jz short near ptr loc_15+5
push Failed_string
call printf
jmp short loc_15+9
Failed_string:
inc esi
popa
loc_15:
imul ebp, [ebp+64h], 0C3C03100h

線形逆アセンブルではjmp命令があるとき、jmpの飛ぶ先を先に逆アセンブルするのではなくデータの並んでる順に逆アセンブルします。

これでも逆アセンブルできるのですが、エラー除外やポインター、条件分岐など複雑なコードではゴチャゴチャになって逆アセンブル結果が妙なことになるのは想像がつくでしょう。

そこで、ブロック単位で逆アセンブルしていく線形逆アセンブルの改良として提案されたのがフロー志向型逆アセンブルです。

ここまで説明を読んだらだいたい察しがつくと思いますが、フロー志向型逆アセンブルのアルゴリズムとしては基本的にはブロック単位で見ていきつつjmpcallなどの命令などがある場合は飛ぶ先を先に逆アセンブルするというものです。

これを絵として表すと以下の通りです。

Screen Shot 2018-11-09 at 13.42.04.png


マルウェア解析での攻防

さて、ようやくマルウェア解析の攻防を話すための下準備が整いました。

セキュリティを勉強する上での1番の醍醐味は知的な攻防戦にあると思うんですよね。

その攻防戦がこのマルウェア解析においても十分見えてきます。


マルウェア作者の気持ち

マルウェアを作る側にとって、せっかくコードを書いて実行ファイルを作成し、ターゲットのコンピュータに忍ばせてパスワードをクラックしたりしてバックドアを仕掛けたりするわけですが、バレて欲しくないわけですよね。

つまりマルウェア作者側はマルウェア解析へ対策をするわけです。

ではどうするか?

この逆アセンブルのアルゴリズムの脆弱性を狙ったコードを作成すればいいんです。


アンチ逆アセンブル

フロー志向型逆アセンブルで利用可能な脆弱性は何かというと条件分岐においてはfalseの方から逆アセンブルを行うという点です。

実際、if文やwhile文は条件が満たされなければ中のコードを実行するという性質を持っていますね。

これを利用します。


同一ターゲットへのジャンプ命令

jzという命令は条件がTrueの時にジャンプし、jnzは条件がFalseの時にジャンプするというものです。

この同一ターゲットへのジャンプ命令jzjnzを重ねがけして同じターゲットに飛ばさせる、というものです。

図に表すと以下のようになります。

Screen Shot 2018-11-09 at 14.09.49.png

これは実質的にjmp命令と同義です。

しかし逆アセンブラは一度に一つの命令しか逆アセンブルしないのでこれが実質的なjmp命令であることを認識しません。

逆アセンブラはjnzに辿りついた時、Falseから逆アセンブルを行うので次にセットしたcall命令を逆アセンブルしますね。

このcall命令の呼ぶ関数を存在しないテキトーなものにセットしておくと逆アセンブラは読めないデータを読もうとして失敗し、最終的に逆アセンブルに失敗します。


定常条件におけるジャンプ命令

この手法も先ほどと同様にジャンプ命令を利用したアンチ逆アセンブルのテクニックです。

先ほどの同一ターゲットへのジャンプ命令は逆アセンブラは一度に一つの命令しか逆アセンブルできないというところを利用して実質的なjmp命令を他の組み合わせで作るというものでした。

この定常条件におけるジャンプ命令も同様です。

Screen Shot 2018-11-09 at 14.27.06.png

xor命令で常に真となる条件を用意します。

そこで逆アセンブラはfalseの方を先に逆アセンブルするので、jmp命令の方を先に読みます。

そこでjmp命令をテキトーにセットしておくことで逆アセンブルが失敗する、という構図です。


不正なバイト使用による逆アセンブル不可能化

今まで見たのはアンチ逆アセンブルのテクニックとしてはかわいいものです。

というのも、逆アセンブラ側に優秀な条件分岐をつければこれらの問題は回避することができるからです。

アンチ逆アセンブルのテクニックは非常に奥が深く、非常に巧妙な手法が数多くあります。

その中でも不正なバイト使用を利用したこのテクニックは非常に秀逸です。

Screen Shot 2018-11-09 at 14.41.27.png

頭からこのバイト列を見たとき、最初の4バイトは2バイトのjmp命令です。

そしてこのjmp命令の飛ぶ先はjmp命令自身です。

FFは次の2バイトの命令inc eaxという命令の最初の2バイトになり、このバイト列はエラーを引き起こすことはありません。

逆アセンブラが頭から逆アセンブルしようとしたとき、この命令列はjmp命令として解釈され正しい逆アセンブル結果が得られないのです。

こうしたバイトの不正使用を利用した応用例を見てみましょう。

Screen Shot 2018-11-09 at 15.11.21.png

この図の上の方では最初の8バイトはaxに05EBh(リトルエンディアンに注意)を代入するmov命令として見ることができます。

しかし最初の4バイトを無視すればjmp命令として見ることができ、本当のコードが実行されるわけです。

実際、このような最初の数バイトはプログラムの一部ではないので実行時には無視されるのですが、逆アセンブラはこれらを評価をしてしまいます。

この手法は隙間をついた面白い手法と言えるのではないのでしょうか?


まとめ

以上、マルウェア解析と解析対策における攻防を見ました。

こういう見るとめちゃくちゃワクワクしませんか?

今回Practical Malware Analysisを勉強して面白かったので、これからもセキュリティの勉強をがんばろうと思います。

ではお疲れ様でした!