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
は、関数の呼び出しを行います5。
jne
は”jump not equal”で、直前の判定(cmp
やtest
)にしたがった処理の分岐をしています。
普通のプログラム言語で言うところの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)。
おわりに
今回は、gdbを用いてバイナリコードを逆アセンブルし、プログラムの流れを解析しました。 いよいよプログラムの裏の仕組みが見えてきたようで、面白くなってきました。
次回はもっと詳細の機能を解析し、パスワードが認証されるステップの特定を目指します。
[関連記事]
-
Reversing and Cracking first simple Program - bin 0x05 - YouTube↩
-
“Dump"とは、dump (program) - Wikipedia↩
-
AT&Tとintelの違いはhttps://imada.sdu.dk/Employees/kslarsen-bak/Courses/dm18-2007-spring/Litteratur/IntelnATT.htmなどが詳しい。↩
-
CPUがどのようにして関数を呼び出しているのか、あるいはアセンブラについての詳しい説明はHow a CPU works and Introduction to Assembler - bin 0x04 - YouTube。↩
-
“0x2"のように、頭に"0x"がつく数字は16進数を表します。↩