BioErrorLog Tech Blog

試行錯誤の記録

逆アセンブル解析 - gdb | リバースエンジニアリング入門#2

gdbを用いてバイナリコードを逆アセンブル (ディスアセンブル)し、Cで書かれたプログラムの全体像を解析します。


はじめに

おはよう。@bioerrorlogです。

パスワード認証プログラムをリバースエンジニアリングして、アクセス承認を引き出したいシリーズpart2です。

前回は、C言語で簡単なパスワード認証プログラムを作成しました。
www.bioerrorlog.work

今回はいよいよ、GNUデバッガgdbを用いて、パスワード認証プログラムを逆アセンブルしていきます。

wikipediaによると1、逆アセンブラとは次のようなものを指します。

実行ファイルないしオブジェクトファイルの機械語コード(とシンボルテーブルなどの付随情報)を基に、アセンブリ言語ソースコードを生成する、すなわちアセンブラの逆の作用をするものを特に指す。

Cコードでは、コンパイルによってバイナリコード(機械語コード)を生成しました。 逆アセンブラはそれとは逆方向に、バイナリコードからアセンブラコードを生成するわけです。
ただし、アセンブリ言語はC言語などよりも機械語よりの低水準言語ですので、入念な読み込みが必要です。

それでは、前回作成したソースコードについての記憶を一旦忘れ去り、バイナリコード"your_pass"のみを与えられたと想定して、逆アセンブル解析していきます。

なお、前回に引き続きLiveOverflow氏の動画2を参考にしています。


作業環境

Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work


逆アセンブル解析

gdbとは

gdbとは何たるかを知るため、まずはマニュアルを読んで概要を把握します。

$ man gdb
NAME
       gdb - The GNU Debugger
~
DESCRIPTION
       The purpose of a debugger such as GDB is to allow you to see what is going on "inside"
       another program while it executes -- or what another program was doing at the moment it
       crashed.

GNUデバッガgdbとは、実行中あるいはクラッシュしたプログラムの"中身"をみるためのもの、だそうです。

実際にgdbができることとは、

       ·   Start your program, specifying anything that might affect its behavior.

       ·   Make your program stop on specified conditions.

       ·   Examine what has happened, when your program has stopped.

       ·   Change things in your program, so you can experiment with correcting the effects of
           one bug and go on to learn about another.

意訳すると、
・プログラムを開始して、挙動に影響を与えるものを特定する。
・特定の条件で、プログラムを停止させる。
・プログラムが停止したとき、何が起こったのか確かめる。
・プログラムに変更を加えることで、バグを一つずつ検証できる。
といったことができるそうです。

なんとなく言わんとしてることはわかりますが、手を動かしてみないと正直イメージは湧いてきません。

アセンブラコードを表示する | (gdb) disassemble

それでは、gdbを使っていきます。
gdbにバイナリコード"your_pass"を渡し、解析を開始します。

$ gdb your_pass

これでgdbデバッガが開始されました。
さっそくmain関数を逆アセンブルします。

(gdb) disassemble main
Dump of assembler code for function main:
   0x00000000000006da <+0>:  push   %rbp
   0x00000000000006db <+1>:  mov    %rsp,%rbp
   0x00000000000006de <+4>:  sub    $0x10,%rsp
   0x00000000000006e2 <+8>:  mov    %edi,-0x4(%rbp)
   0x00000000000006e5 <+11>: mov    %rsi,-0x10(%rbp)
   0x00000000000006e9 <+15>: cmpl   $0x2,-0x4(%rbp)
   0x00000000000006ed <+19>: jne    0x748 <main+110>
   0x00000000000006ef <+21>: mov    -0x10(%rbp),%rax
   0x00000000000006f3 <+25>: add    $0x8,%rax
   0x00000000000006f7 <+29>: mov    (%rax),%rax
   0x00000000000006fa <+32>: mov    %rax,%rsi
   0x00000000000006fd <+35>: lea    0xe4(%rip),%rdi        # 0x7e8
   0x0000000000000704 <+42>: mov    $0x0,%eax
   0x0000000000000709 <+47>: callq  0x5a0 <printf@plt>
   0x000000000000070e <+52>: mov    -0x10(%rbp),%rax
   0x0000000000000712 <+56>: add    $0x8,%rax
   0x0000000000000716 <+60>: mov    (%rax),%rax
   0x0000000000000719 <+63>: lea    0xe0(%rip),%rsi        # 0x800
   0x0000000000000720 <+70>: mov    %rax,%rdi
   0x0000000000000723 <+73>: callq  0x5b0 <strcmp@plt>
   0x0000000000000728 <+78>: test   %eax,%eax
   0x000000000000072a <+80>: jne    0x73a <main+96>
   0x000000000000072c <+82>: lea    0x107(%rip),%rdi        # 0x83a
   0x0000000000000733 <+89>: callq  0x590 <puts@plt>
   0x0000000000000738 <+94>: jmp    0x754 <main+122>
   0x000000000000073a <+96>: lea    0x108(%rip),%rdi        # 0x849
   0x0000000000000741 <+103>:    callq  0x590 <puts@plt>
   0x0000000000000746 <+108>:    jmp    0x754 <main+122>
   0x0000000000000748 <+110>:    lea    0xfa(%rip),%rdi        # 0x849
   0x000000000000074f <+117>:    callq  0x590 <puts@plt>
   0x0000000000000754 <+122>:    mov    $0x0,%eax
   0x0000000000000759 <+127>:    leaveq 
   0x000000000000075a <+128>:    retq   
End of assembler dump.

こいつは面白そうな文字列が表示されました。
一行目には Dump of assembler code for function main:とあります。 つまり、main関数のアセンブラコードが表示されている3わけです。

ちなみに、アセンブリ言語の表示形式は、AT&Tとintelの2つの形式があります4。 gdbのデフォルトの表示方法はAT&Tであり、ご覧のようにすごく読みにくいものです。 これをintel方式にセットします。

(gdb) set disassembly-flavor intel

これで再び逆アセンブルすると、

Dump of assembler code for function main:
   0x00000000000006da <+0>:  push   rbp
   0x00000000000006db <+1>:  mov    rbp,rsp
   0x00000000000006de <+4>:  sub    rsp,0x10
   0x00000000000006e2 <+8>:  mov    DWORD PTR [rbp-0x4],edi
   0x00000000000006e5 <+11>: mov    QWORD PTR [rbp-0x10],rsi
   0x00000000000006e9 <+15>: cmp    DWORD PTR [rbp-0x4],0x2
   0x00000000000006ed <+19>: jne    0x748 <main+110>
   0x00000000000006ef <+21>: mov    rax,QWORD PTR [rbp-0x10]
   0x00000000000006f3 <+25>: add    rax,0x8
   0x00000000000006f7 <+29>: mov    rax,QWORD PTR [rax]
   0x00000000000006fa <+32>: mov    rsi,rax
   0x00000000000006fd <+35>: lea    rdi,[rip+0xe4]        # 0x7e8
   0x0000000000000704 <+42>: mov    eax,0x0
   0x0000000000000709 <+47>: call   0x5a0 <printf@plt>
   0x000000000000070e <+52>: mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000000712 <+56>: add    rax,0x8
   0x0000000000000716 <+60>: mov    rax,QWORD PTR [rax]
   0x0000000000000719 <+63>: lea    rsi,[rip+0xe0]        # 0x800
   0x0000000000000720 <+70>: mov    rdi,rax
   0x0000000000000723 <+73>: call   0x5b0 <strcmp@plt>
   0x0000000000000728 <+78>: test   eax,eax
   0x000000000000072a <+80>: jne    0x73a <main+96>
   0x000000000000072c <+82>: lea    rdi,[rip+0x107]        # 0x83a
   0x0000000000000733 <+89>: call   0x590 <puts@plt>
   0x0000000000000738 <+94>: jmp    0x754 <main+122>
   0x000000000000073a <+96>: lea    rdi,[rip+0x108]        # 0x849
   0x0000000000000741 <+103>:    call   0x590 <puts@plt>
   0x0000000000000746 <+108>:    jmp    0x754 <main+122>
   0x0000000000000748 <+110>:    lea    rdi,[rip+0xfa]        # 0x849
   0x000000000000074f <+117>:    call   0x590 <puts@plt>
   0x0000000000000754 <+122>:    mov    eax,0x0
   0x0000000000000759 <+127>:    leave  
   0x000000000000075a <+128>:    ret    
End of assembler dump.

右の部分がだいぶスッキリしました。


[関連記事] radare2の使い方 | リバースエンジニアリング入門#5


アセンブラコードの流れを解析する

私のひとまずの目的は、上のアセンブラコードの全体的な流れを理解することです。 そのためには、ほとんどの部分は無視し、論理構造の上で重要な部分だけ読んでいきます。
その部分を抜き出すと、次のようになります。

~
   0x00000000000006e9 <+15>: cmp    DWORD PTR [rbp-0x4],0x2
   0x00000000000006ed <+19>: jne    0x748 <main+110>
~
   0x0000000000000709 <+47>: call   0x5a0 <printf@plt>
~
   0x0000000000000723 <+73>: call   0x5b0 <strcmp@plt>
   0x0000000000000728 <+78>: test   eax,eax
   0x000000000000072a <+80>: jne    0x73a <main+96>
~
   0x0000000000000733 <+89>: call   0x590 <puts@plt>
   0x0000000000000738 <+94>: jmp    0x754 <main+122>
~
   0x0000000000000741 <+103>:    call   0x590 <puts@plt>
   0x0000000000000746 <+108>:    jmp    0x754 <main+122>
~
   0x000000000000074f <+117>:    call   0x590 <puts@plt>
~

着目したのは、callと、jne(jmp)です。
callは、関数の呼び出しを行います5jneは”jump not equal”で、直前の判定(cmptest)にしたがった処理の分岐をしています。 普通のプログラム言語で言うところのif分岐のようなものでしょう。 jmpはそのまま"jump"で、条件による判定なしにjumpします。

さっそく、上から読み解いていきます。

~
   0x00000000000006e9 <+15>: cmp    DWORD PTR [rbp-0x4],0x2
   0x00000000000006ed <+19>: jne    0x748 <main+110>
~

最初のこの塊は、未知の文字列(DWORD PTR [rbp-0x4])を、2(0x2)6と比較(cmp)しています。 そしてその結果を受けた上で、アドレス0x748にジャンプ(jne)しています。 アドレス0x748の先を読んでみると、

   0x0000000000000748 <+110>:    lea    rdi,[rip+0xfa]        # 0x849
   0x000000000000074f <+117>:    call   0x590 <puts@plt>
   0x0000000000000754 <+122>:    mov    eax,0x0
   0x0000000000000759 <+127>:    leave  
   0x000000000000075a <+128>:    ret    

着目すべきはアドレス0x74fのみです。 ここでcall 0x590 <puts@plt>を実行した後、終了(ret)しています。 つまり、

   0x6e9:   cmp    ?,0x2
   0x6ed:   jne    0x748
            ↓
            ↓
   0x748:
   0x74f:   call   0x590 <puts@plt>
   おわり

これで一つの流れが分かりました。

こんな感じで、その他の分岐もすべて解析すると、次のようなプログラムの全体像が浮かび上がります(Fig. 1)。

f:id:BioErrorLog:20190202175344j:plain
Fig. 1 アセンブラコードから解析したプログラムの流れ


おわりに

今回は、gdbを用いてバイナリコードを逆アセンブルし、プログラムの流れを解析しました。 いよいよプログラムの裏の仕組みが見えてきたようで、面白くなってきました。

次回はもっと詳細の機能を解析し、パスワードが認証されるステップの特定を目指します。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work


  1. 逆アセンブラ - Wikipedia

  2. Reversing and Cracking first simple Program - bin 0x05 - YouTube

  3. “Dump"とは、dump (program) - Wikipedia

  4. AT&Tとintelの違いはhttps://imada.sdu.dk/Employees/kslarsen-bak/Courses/dm18-2007-spring/Litteratur/IntelnATT.htmなどが詳しい。

  5. CPUがどのようにして関数を呼び出しているのか、あるいはアセンブラについての詳しい説明はHow a CPU works and Introduction to Assembler - bin 0x04 - YouTube

  6. “0x2"のように、頭に"0x"がつく数字は16進数を表します。