Edited at

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

現在京大からスイスに留学している@komi_edtr_1230です。

最近セキュリティの勉強をしてて、先ほどPractical Malware Analysisという本を読み終わりました。

Screen Shot 2018-11-08 at 20.28.01.png

....禍々しい表紙ですね。

とても面白く勉強になった本だったんですが、今回読んでて勉強になったなぁという箇所がいくつかあったのでまとめておこうと思います。


アセンブリ言語とは

この記事にたどり着いた人には不要かと思うんですけど、一応アセンブリ言語とはどんなものか説明しておきます。

一般に、ぼくらがコードを書くとき、そのコードは人に読めるものです。

しかし実際そのコードそのままではコンピュータは理解できないわけです。

「コンピュータは0と1の塊である」言われるように、人が読めるコードをコンピュータに実行させるにはコンピュータが理解できるようにコードを変換しなければいけません。

これがいわゆるコンパイルという作業ですね。

図にすると以下の通りです。

20181104040644.png

例えばC言語で"Hello World"と出力させるには以下のようなコードです。


HelloWorld.c

#include <stdio.h>


int main(void){
printf("Hello World\n");

return 0;
}


これはぼくらでも読めるコードですね。

ではこれをアセンブリ言語に変換します。

Cのコンパイルをアセンブリで止めるにはgccのコマンドに-Sのオプションをつけます。

$ gcc -S HelloWorld.c

そうするとHelloWorld.sというのができますね。

この中身は以下の通りです。

    .section    __TEXT,__text,regular,pure_instructions

.build_version macos, 10, 14
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
leaq L_.str(%rip), %rdi
movl $0, -4(%rbp)
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -8(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World\n"

.subsections_via_symbols

ぼくの環境はMacbookでアセンブリの命令セットはx86_64です。

アセンブリ言語はまだ読めるんですけど、C言語とかに比べたらだいぶ可読性は落ちますね。

あまり長くなっても仕方ないのでここではアセンブリ言語で出てくるripとかecxとかのメモリの説明は割愛しますが、今回話す逆アセンブルと攻防について強く関連はしないので知らない人はデータの格納場所というように捉えておいてください。

さて、このアセンブリ言語をアセンブルすると実行ファイル(Unixではa.out、Windowsだとa.exe)ができ、これを実行することで晴れてようやくコンピュータに"Hello World"と言わせることができるわけですね。


逆アセンブルとは

コンピュータに命令を実行させる際、C言語 → アセンブリ言語 → 実行ファイルという順番でした。

しかしアセンブリ言語 ← 実行ファイルというようなこともしたいときがあるでしょう。

例えば正体不明の実行ファイル(まさにマルウェアとか)を解析したいときは尚更この作業は重要です。

この実行ファイルからアセンブリ言語に戻す作業を逆アセンブルといい、逆アセンブルしたコードを解析するのをリバースエンジニアリングと言います。

実際に逆アセンブルするにはobjdumpを使うことで実践できます。

$ objdump -d -M intel 実行ファイル

この-dオプションはdisassembleのことで逆アセンブルし、-M intelというのは出力するアセンブリ言語をintelの文法にするという意味です。

この-MはintelとAT&Tがあるのですが、ぼくはintelの方が読みやすいのでintelをよく選択しています。

さて、先ほどの"Hello World"と出力する実行ファイルをobjdumpを実際に使って解析すると以下の通りの出力されます。

a.out:     file format mach-o-x86-64

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

Disassembly of section __TEXT.__stubs:

0000000100000f8a <__TEXT.__stubs>:
100000f8a: ff 25 80 00 00 00 jmp QWORD PTR [rip+0x80] # 100001010 <_main+0xb0>

Disassembly of section __TEXT.__stub_helper:

0000000100000f90 <__TEXT.__stub_helper>:
100000f90: 4c 8d 1d 71 00 00 00 lea r11,[rip+0x71] # 100001008 <_main+0xa8>
100000f97: 41 53 push r11
100000f99: ff 25 61 00 00 00 jmp QWORD PTR [rip+0x61] # 100001000 <_main+0xa0>
100000f9f: 90 nop
100000fa0: 68 00 00 00 00 push 0x0
100000fa5: e9 e6 ff ff ff jmp 100000f90 <_main+0x30>

Disassembly of section __TEXT.__unwind_info:

0000000100000fb8 <__TEXT.__unwind_info>:
100000fb8: 01 00 add DWORD PTR [rax],eax
100000fba: 00 00 add BYTE PTR [rax],al
100000fbc: 1c 00 sbb al,0x0
100000fbe: 00 00 add BYTE PTR [rax],al
100000fc0: 00 00 add BYTE PTR [rax],al
100000fc2: 00 00 add BYTE PTR [rax],al
100000fc4: 1c 00 sbb al,0x0
100000fc6: 00 00 add BYTE PTR [rax],al
100000fc8: 00 00 add BYTE PTR [rax],al
100000fca: 00 00 add BYTE PTR [rax],al
100000fcc: 1c 00 sbb al,0x0
100000fce: 00 00 add BYTE PTR [rax],al
100000fd0: 02 00 add al,BYTE PTR [rax]
100000fd2: 00 00 add BYTE PTR [rax],al
100000fd4: 60 (bad)
100000fd5: 0f 00 00 sldt WORD PTR [rax]
100000fd8: 34 00 xor al,0x0
100000fda: 00 00 add BYTE PTR [rax],al
100000fdc: 34 00 xor al,0x0
100000fde: 00 00 add BYTE PTR [rax],al
100000fe0: 8b 0f mov ecx,DWORD PTR [rdi]
100000fe2: 00 00 add BYTE PTR [rax],al
100000fe4: 00 00 add BYTE PTR [rax],al
100000fe6: 00 00 add BYTE PTR [rax],al
100000fe8: 34 00 xor al,0x0
100000fea: 00 00 add BYTE PTR [rax],al
100000fec: 03 00 add eax,DWORD PTR [rax]
100000fee: 00 00 add BYTE PTR [rax],al
100000ff0: 0c 00 or al,0x0
100000ff2: 01 00 add DWORD PTR [rax],eax
100000ff4: 10 00 adc BYTE PTR [rax],al
100000ff6: 01 00 add DWORD PTR [rax],eax
100000ff8: 00 00 add BYTE PTR [rax],al
100000ffa: 00 00 add BYTE PTR [rax],al
100000ffc: 00 00 add BYTE PTR [rax],al
100000ffe: 00 01 add BYTE PTR [rcx],al

....なんともおぞましいですね。

これの読み方として、左の数字列がメモリの番号、addとかmovが命令、その右にあるraxなどが命令への引数という風に解釈すれば解析することができます。

さて、C言語ではmain関数が実行される部分ですので、そのmain部分を注目しましょう。

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

このobjdumpの逆アセンブルの結果を読むと100000f78のところで命令callという命令をしています。

callというのは関数の呼び出し命令なのですが、ぼくらが今回使ってる関数はprintf()だけですので、恐らくこれはprintf()関数のことを指しているのだろうと推測できます。

さて、呼び出している関数100000f8aを見てみると以下の通りです。

Disassembly of section __TEXT.__stubs:

0000000100000f8a <__TEXT.__stubs>:
100000f8a: ff 25 80 00 00 00 jmp QWORD PTR [rip+0x80] # 100001010 <_main+0xb0>

ここで出てくるのはjmpという命令です。

jmpというのはジャンプで、QWORDというのはQuad wordのことです。

また、アセンブリ言語上での[rip]というのはripのポインタを意味します。

つまりここから推測するにripに入っている文字列を出していることがわかります。

jmp命令でrip+0x80のアドレスにジャンプ、動的にリンクされてるprintf()関数にアクセスして"Hello World"を出力する、ということです。

(@fujitanozomuさんに指摘していただきました。ありがとうございました。)


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

以上のような感じでアセンブリ言語は読めるんですけど、ここで一つ疑問。

どうやって逆アセンブルしてるんだろう?

技術を理解する上でブラックボックス化というのは1番よくないことなのですが、今回は逆アセンブリという作業をするにあたってobjdumpというツールを使いました。

実を言うと逆アセンブルのアルゴリズムはぼくが知っているだけでも2つアルゴリズムがあり、今回取り上げるのは線形逆アセンブルフロー志向型逆アセンブルがあります。

逆アセンブルがどのように行われるか、これがマルウェア解析においてどのような攻防戦となるのか。

すでに長くなってしまった感があるので、続きについては後編にて紹介しようと思います。

ではお疲れ様でした!

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