実戦形式で学ぶホワイトハッカーの技術

初心者向けのEthical Hacking講座です。

リバースエンジニアリング超入門

最後のトピックはリバースエンジニアリングです。

リバースエンジニアリングとはいったん何なのでしょうか。
Wikipediaで調べてみると、下記の様な説明になるそうです。

リバースエンジニアリング(英: reverse engineering)とは、機械を分解したり、製品の動作を観察したり、ソフトウェアの動作を解析するなどして、製品の構造を分析し、そこから製造方法や動作原理、設計図などの仕様やソースコードなどを調査することを指す。

少し分かりにくいでしょうか。
皆さんが想像しやすいようにゲームの例で考えてみましょう。
例えば、自分のキャラクターとCPUのキャラクターが戦うゲームがあったとします。

Fighting Game

皆さんはキックやパンチを繰り出して、CPUを倒そうとします。
キックとパンチはそれぞれ与えるダメージが違います。さらにガードされると与えられるダメージ量が減ってしまいます。
このように複雑なゲームでは、一体内部ではどのような処理になっているのだろうと気になりますよね。
この内部の処理を知るための解析をリバースエンジニアリングと呼びます。

ここで一つ重要なことなのでお伝えしておくと、許可を得ていないソフトウェアやハードウェアへのリバースエンジニアリングは違法となります。必ず許可を得たプログラムに対してのみ行うようにしましょう。
ゲームに対してリバースエンジニアリングを行い、プログラムを改ざんしてしまうと、俗に言う「チート」行為にあたってしまいます。

話を戻して、プログラムの中身を知りたいのですが、私達はバイナリのソースコードを持っていません。
もちろん開発者であれば、そのプログラムがどのような処理をするのかソースコードを閲覧することで確認できますが、ユーザーはできません。
そのためリバースエンジニアリングを行うことで、ソースコードがなくともどのような処理を行っているのかを知ろうとするわけです。
オリジナルに近いソースコードに戻せることはあまりありませんが(中間言語系は割と戻せたりしますが)、時間をかければプログラムの内部を解明することも可能です。
基本的にリバースエンジニアリングには高度な知識や技術が求められますが、今回は超入門ということで、Linuxのコマンドのみにて解析したいと思います。
なので今回はリバースエンジニアリングというよりも、ファイル解析に近いかもしれません。

ではまず解析用のプログラム「crackme」をKali Linuxにダウンロードしましょう。
github.com

では解析をはじめます。
これから使用するコマンドがKali Linux上に入っていない場合は、コマンドを使用する際にインストールするかどうか聞かれるので、インストールしてください。

解析

まずこのファイルが何なのか「file」コマンドで確認します。

┌──(kali㉿kali)-[~/Downloads]
└─$ file crackme
crackme: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=75738b208b83feabd92439f81104e978710f4d19, for GNU/Linux 3.2.0, not stripped

ELFのexecutableということで、Linux上で実行できるファイルであることが分かります。
では実行してみましょう。(マルウェアの可能性もあるので、いきなり実行するのは本来危ないです。)

┌──(kali㉿kali)-[~/Downloads]
└─$ ./crackme 
Enter the password: 123456
Not Accepted

実行するとパスワードを求められ、適当なパスワードを入力すると「Not Accepted」というメッセージが出力されます。
つまり正しいパスワードを入力することが今回のゴールです。
ですが私達はパスワードを知りません。そこで正しいパスワードをリバースエンジニアリングによって取得します。

次に実行ファイルから文字列を取得してみましょう。

┌──(kali㉿kali)-[~/Downloads]
└─$ strings crackme 
/lib64/ld-linux-x86-64.so.2
puts
__libc_start_main
__cxa_finalize
printf
__isoc99_scanf
strcmp
libc.so.6
GLIBC_2.7
GLIBC_2.2.5
GLIBC_2.34
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
PTE1
u+UH
TalkToMeH
Enter the password: 
Well Done
Not Accepted
;*3$"
GCC: (Debian 12.2.0-14) 12.2.0
Scrt1.o

出力結果が長いので一部省略していますが、「Enter the password: 」や「Not Accepted」などの見慣れた文字列も見られます。
その2つの間にある「Well Done」は正解時のメッセージであると予想することができます。
「Enter the password: 」の上には、「TalkToMeH」という謎の文字列がありますが、今は何かわからないので頭の片隅に入れて、次のステップに進みましょう。

シンボル情報(関数名や変数名などの情報)を確認してみましょう。
「nm」コマンドを実行することで、オブジェクトファイルのシンボル情報を確認できます。

                                                                                                                                   
┌──(kali㉿kali)-[~/Downloads]
└─$ nm crackme
000000000000037c r __abi_tag
0000000000004030 B __bss_start
0000000000004030 b completed.0
                 w __cxa_finalize@GLIBC_2.2.5
0000000000004020 D __data_start
0000000000004020 W data_start
00000000000010b0 t deregister_tm_clones
0000000000001120 t __do_global_dtors_aux
0000000000003dd8 d __do_global_dtors_aux_fini_array_entry
0000000000004028 D __dso_handle
0000000000003de0 d _DYNAMIC
0000000000004030 D _edata
0000000000004038 B _end
00000000000011f0 T _fini
0000000000001160 t frame_dummy
0000000000003dd0 d __frame_dummy_init_array_entry
0000000000002108 r __FRAME_END__
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002034 r __GNU_EH_FRAME_HDR
0000000000001000 T _init
0000000000002000 R _IO_stdin_used
                 U __isoc99_scanf@GLIBC_2.7
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main@GLIBC_2.34
0000000000001169 T main
                 U printf@GLIBC_2.2.5
                 U puts@GLIBC_2.2.5
00000000000010e0 t register_tm_clones
0000000000001080 T _start
                 U strcmp@GLIBC_2.2.5
0000000000004030 D __TMC_END__

下の方に「strcmp」という関数があります。
この関数は2つの文字列を比較をするものです。つまりこの関数で、パスワードのチェックをしているのではないかと予想できます。

ではこの関数の処理を見てみましょう。
今回は「ltrace」を使用します。
「ltrace」とは、ダイナミックリンクライブラリの呼び出しを追跡するプログラムで、今回であれば「strcmp」関数の引数を確認するために使用します。

┌──(kali㉿kali)-[~/Downloads]
└─$ ltrace ./crackme
printf("Enter the password: ")                                                              = 20
__isoc99_scanf(0x55e930f3f019, 0x7ffe9a991930, 0, 0Enter the password: 

ltraceを実行すると、一度ここで止まります。
「scanf」で止まっており、パスワードを聞かれています。適当なパスワードを入力しましょう。
すると正しいパスワードが判明します。

┌──(kali㉿kali)-[~/Downloads]
└─$ ltrace ./crackme
printf("Enter the password: ")                                                              = 20
__isoc99_scanf(0x55e930f3f019, 0x7ffe9a991930, 0, 0Enter the password: hello
)                                        = 1
strcmp("hello", "TalkToMe")                                                                 = 20
puts("Not Accepted"Not Accepted
)                                                                        = 13
+++ exited (status 0) +++

strcmp関数で「hello」と「TalkToMe」が比較されています。
「hello」は僕が入力したものなので、「TalkToMe」がパスワードではないかと見当がつきます。

では正しいパスワードでプログラムを動かしてみましょう。

┌──(kali㉿kali)-[~/Downloads]
└─$ ./crackme  
Enter the password: TalkToMe
Well Done

正解しました。
このようにリバースエンジニアリングを行うことで、内部の処理を把握することができ、今回であれば普通は取得できないパスワードを当てることができました。

今回は超入門でしたが、通常のリバースエンジニアリングでは「Ghidra」や「IDA」などのデコンパイル(逆アセンブリ)ツールを使うことがほとんどです。
Ghidraに少しだけ触れてみましょう。

Ghidra

まずGhidraをインストールして実行します。

┌──(kali㉿kali)-[~/Downloads]
└─$ sudo apt update && sudo apt install ghidra --fix-missing

┌──(kali㉿kali)-[~/Downloads]
└─$ ghidra

Ghidraが起動すると、「File」→「Import File」を選択し、Crackmeをインポートします。
すると、「Auto Analyze」のポップアップが出るので、全てOKを押しましょう。
すると下記の画像のような状態になるはずです。

Ghidra

そして左側の「Symbol Tree」内の「Functions」からmainを選択します。

main関数

デコンパイル結果を見ると、ソースコードのようなものが確認できます。

デコンパイル結果のコードはとてもシンプルで、入力したパスワードが合っていれば「Well Done」、間違っていれば「Not Accepted」というメッセージが出力されます。
どうやら正しいパスワードは「local81」という変数に入っているのですが、「0x654d6f546b6c6154」という16進数として格納されています。
その16進数にカーソルを当てると、char型で「eMoTklaT」と表示されます。
勘のいい方は、これが正解のパスワードである「TalkToMe」を逆から読んだ形であると気づくかもしれません。
これはリトルエンディアンであるためです。気になる方は調べてみてください。

要はGhidraを使えば、コンパイルされたプログラムをデコンパイルして、デコンパイルされたコードを追っていくことで、プログラムの処理を理解することができます。
実際は全ての処理を追っていくのは大変ですが、Ghidraによるリバースエンジニアリングの様子だけでも理解してもらえると嬉しいです。