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

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

現在京大からスイスに留学している@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つアルゴリズムがあり、今回取り上げるのは線形逆アセンブルフロー志向型逆アセンブルがあります。

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

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

ではお疲れ様でした!

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