BioErrorLog Tech Blog

試行錯誤の記録

Ghidraの使い方 | リバースエンジニアリングツールGhidraを使ってみた

Ghidraの環境構築/インストールから、Ghidraの使い方とリバースエンジニアリングの実践までの記録を残します。


はじめに

こんにちは、@bioerrorlogです。

2019年3月5日、NSA(アメリカ国家安全保障局)が、リバースエンジニアリングツール"Ghidra"を公開しました。 公開当日はえらいお祭り騒ぎで、YoutubeやTwitchではたくさんのセキュリティ系配信者がGhidraを使った生放送を行っていました。

そこで今回、私もGhidraを使ってみたいと思います。

目標は、Ghidraを使ってcrackmes.one1の簡単な問題をひとつ破ることです。

それではやっていきます。

作業環境

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


Ghidraの使い方

環境構築

Ghidraをインストールする

まずはサイトからGhidraをダウンロードします。

Ghidra

今回私がダウンロードした時点でのバージョンは、"ghidra_9.0"でした。 ダウンロードしたらzipを解凍して、さっそくインストール方法を調べました。

docsファイルのなかにInstallationGuide.htmlというものがあったので読んでみると、

To install Ghidra, simply extract the Ghidra distribution file to the desired filesystem destination using any unzip program

Ghidraをインストールするには、単にzipを解凍するだけで十分のようです。 これはとても簡単で良いですね。

多くのソフトウェアがインストール処理をする必要がある中、解凍だけで終わる方法は珍しい気がします。 Ghidraもこの方法のメリット、デメリットを認めていました。 メリットとしては、

On the up side, administrative privilege is not required to install Ghidra for personal use. Also, because installing Ghidra does not update any OS configurations such as the registry on Windows, removing Ghidra is as simple as deleting the Ghidra installation directory.

インストールに管理者権限が必要ないこと、OS設定を変更する必要がないこと、アンインストールが簡単なことが挙げられています。

一方デメリットとしては、

On the down side, Ghidra will not automatically create a shortcut on the desktop or appear in application start menus.

デスクトップやアプリケーションメニューにショートカットが自動で作成されないことが挙げられていました。


JDK(Java Development Kit)をインストールする

さて、InstallationGuide.htmlの先を読み進めると、JDK(Java Development Kit)のインストールが必要と書かれています。

Ghidra requires a supported version of a Java Runtime and Development Kit on the PATH to run.


JDKのインストール方法とPATHへの通し方も、丁寧に記載されていました。

Linux and macOS (OS X): Extract the JDK distribution (.tar.gz file) to your desired location, and add the JDK's bin directory to your PATH:

1. Extract the JDK:
        tar xvf <JDK distribution .tar.gz>  
2. Open ~/.bashrc with an editor of your choice. For example:  
        vi ~/.bashrc  
3. At the very end of the file, add the JDK bin directory to the PATH variable:  
        export PATH=<path of extracted JDK dir>/bin:$PATH  
4. Save file  
5. Restart any open terminal windows for changes to take effect  

このインストールガイドに従ってやっていきます。

まずは、JDKをダウンロードします。 GhidraがサポートされているJDKバージョンは、

Java 11 Runtime and Development Kit (JDK).OpenJDK distributed from jdk.java.net is suggested

JDK11がサポートしているようです。 jdk.javaから配布されているOpenJDK 11が推奨されていましたので、それをダウンロードしました。

JDK 11 Releases

それでは上記インストールガイドの手順1. JDKを解凍します。 インストールガイドに従い、tar xvfコマンドで解凍します。

$ tar xvf ./Downloads/openjdk-11.0.2_linux-x64_bin.tar.gz

これで、jdk-11.0.2がホームディレクトリに展開されました。

次は手順2~4. 環境PATHにJDKを通します。

やり方としては上記ガイドにならい、~/.bashrcファイルの最後の行にJDKのディレクトリを与えます。

#vimで~/.bashrcを開く

$ vim ~/.bashrc


#最後の行につぎの一行を書き加える

export PATH=~/jdk-11.0.2/bin:$PATH

これでファイルを保存すれば大丈夫です。

最後に手順5. 開いているターミナルをすべて閉じて、JDKのインストールは完了です。


[関連記事] 逆アセンブル解析 - gdb | リバースエンジニアリング入門#2


Ghidraを起動する

いよいよGhidraを起動します。 起動の仕方は、

Run ghidraRun.bat (Windows) or ghidraRun (Linux or macOS)

私はLinuxですのでghidraRunファイルを実行すればいいようです。 さっそく、ghidraの展開されたディレクトリに行き、実行してみます。

$ cd ghidra_9.0_PUBLIC_20190228/ghidra_9.0/
$ ./ghidraRun 

すると、見事Ghidraが起動しました。


Ghidraでファイルを開く

Crackmes.oneから問題をダウンロード/解凍

それでは、起動したGhidraを使ってCrackmes.oneの簡単な問題を解いていきます。 Crackmes.oneとは、リバースエンジニアリングするためのプログラムを投稿・ダウンロードできるサイトのようです。 合法的にリバースエンジニアリングで遊ぶことができます。

crackmes.one

ここから、次の簡単な問題"easy_reverse"をひとつダウンロードしました。

cbm-hackers's easy_reverse - Crackmes.one

そして、ダウンロードしたファイルを解凍したいのですが、ここでパスワードが要求されました。 焦らずに公式FAQ2を見てみると、

The password for the files is "crackmes.one". If it does not work, this is probably because the crackme has been imported from crackmes.de, so use the password "crackmes.de" instead.

パスワードは"crackmes.one"のようです。 これで、ファイルの解凍に成功しました。


Ghidraでバイナリファイルを開く

それでは、Ghidraでこのファイルを開きます。

はじめてGhidraを起動したときは3つのウィンドウ3が立ち上がりますが、いま重要なのは中くらいのウィンドウ:プロジェクトマネージャーだけです。 このプロジェクトマネージャーから、

File -> New Project

として、新しいプロジェクトを立ち上げます。

Non-Shared Project -> DirectoryとNameを記入 -> Finish

これで、新しいプロジェクトが立ち上がりました。

つぎに、先程ダウンロード/解凍したバイナリファイルをプロジェクトにインポートします。 やり方は簡単で、ウィンドウにバイナリファイルをドラッグするだけです。 ドラッグすると設定ウィンドウが開きますので、それぞれ適当な設定をして"OK"を押せばインポート完了です。 今回の場合ですと、"Format"は"Executable and Linking Format (ELF)"、"Language"は"x86:LE:64:dafault:gcc"としました。

インポートが完了したら、そのファイルをダブルクリックすることで解析ウィンドウを開くことができます。 解析ウィンドウを開くと、<ファイル名> has not been analyzed. Would you like to analyze it now?と聞かれるので、Yesとします。 すると、Analysis Optionsウィンドウが開いて多くのオプションが提示されますが、今回はとりあえずデフォルトのまま"Analyze"を押して先へ進みました。 これで、解析ができます(Fig. 1)。

ワクワクが高まってきました。

Fig. 1 Ghidraの解析ウィンドウ


リバースエンジニアリング実践

バイナリファイルの挙動を確認する

それではここから、さきほどCrackmes.oneからダウンロードした簡単な問題"rev50_linux64-bit"を破っていきます。

まずは、ターミナル上でこのバイナリファイルを実行し、挙動を見てみました。

$ ./rev50_linux64-bit 
USAGE: ./rev50_linux64-bit <password>
try again!

どうやら正しいパスワードを暴いて、このファイルを開かせるのが目的のようです。 さっそくGhidraで解析していきます。


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


Ghidraでmain関数をデコンパイルする

Ghidraでmain関数をデコンパイルして、中身を見ていきます。 この操作は簡単にできるようです。

Ghidraの左側にある小さなウィンドウ"Symbol Tree"の"Filter: "に"main"と打つと、Symbol Treeから"main"が抽出されます。 このなかで、"Functions"の中にある"main"をクリックすると、main関数の部分に移動します。 と同時に、右側にある"Decompile"ウィンドウには、C言語としてデコンパイルされたmain関数が表示されました。

undefined8 main(int iParm1,undefined8 *puParm2)

{
  size_t sVar1;
  
  if (iParm1 == 2) {
    sVar1 = strlen((char *)puParm2[1]);
    if (sVar1 == 10) {
      if (*(char *)(puParm2[1] + 4) == '@') {
        puts("Nice Job!!");
        printf("flag{%s}\n",puParm2[1]);
      }
      else {
        usage(*puParm2);
      }
    }
    else {
      usage(*puParm2);
    }
  }
  else {
    usage(*puParm2);
  }
  return 0;
}

頑張ればこのままでもアルゴリズムを読み出せそうですが、なかなか見慣れない表記が目立ちますので、一つずつ改変していきます。


コード表記を修正する

C言語のmain関数といえば、次のような形が普通でしょう。

int main(int argc,char *argv[])
{
~
}

それが、デコンパイルされたコードでは次のようになっています。

undefined8 main(int iParm1,undefined8 *puParm2)
{
~
}

これを修正します。

やり方は簡単です。 Ghidra上で修正したい部分を右クリックし、"Edit Function Signature"から書き換えます。 上に示したmain関数の冒頭部分を修正すると、main関数全体は次のように改変されました。

int main(int argc,char *argv[])

{
  size_t sVar1;
  
  if (argc == 2) {
    sVar1 = strlen(*(char **)(argv[] + 8));
    if (sVar1 == 10) {
      if (*(char *)(*(long *)(argv[] + 8) + 4) == '@') {
        puts("Nice Job!!");
        printf("flag{%s}\n",*(undefined8 *)(argv[] + 8));
      }
      else {
        usage(*(undefined8 *)argv[]);
      }
    }
    else {
      usage(*(undefined8 *)argv[]);
    }
  }
  else {
    usage(*(undefined8 *)argv[]);
  }
  return 0;
}

だいぶ見やすくなりましたが、まだ少し変な部分があります。

どうやらargv[]周辺の表記がおかしいようです4。 これを解決するため、配列表記*argv[]を、ポインタ表記**argvで代替します。

なぜこのように表記を代替できるのかについては、配列とポインタについての複雑な説明が必要になるので、割愛します。 次のサイトなどで分かりやすく解説されていました。
「ポインタのポインタ」と「ポインタの配列」の関係 - マイナビニュース

それでは、main関数冒頭部分int main(int argc,char * argv[])int main(int argc,char** argv)に変更し、再びmain関数全体を見渡します。

int main(int argc,char **argv)

{
  size_t sVar1;
  
  if (argc == 2) {
    sVar1 = strlen(argv[1]);
    if (sVar1 == 10) {
      if (argv[1][4] == '@') {
        puts("Nice Job!!");
        printf("flag{%s}\n",argv[1]);
      }
      else {
        usage(*argv);
      }
    }
    else {
      usage(*argv);
    }
  }
  else {
    usage(*argv);
  }
  return 0;
}

とても見やすくなりました。 このコードから、アルゴリズムを読み解いていきます。


アルゴリズムを解析する

どうやらmain関数の前半部分が重要に見えます。

int main(int argc,char **argv)

{
  size_t sVar1;
  
  if (argc == 2) {
    sVar1 = strlen(argv[1]);
    if (sVar1 == 10) {
      if (argv[1][4] == '@') {
        puts("Nice Job!!");
        printf("flag{%s}\n",argv[1]);
      }

まず、if (argc == 2)では、引数が一つ渡されているかどうかがチェックされています。 Trueだとその先の処理へ、Falseだと他の分岐でusage(*argv)へと飛ばされてしまいます。

よって、一つ目の条件は、

1. 引数がひとつ渡される

となるでしょう。


先にいきます。 次の認証は、

    sVar1 = strlen(argv[1]);
    if (sVar1 == 10) {
~
      }

となっています。 これは、sVar1 = strlen(argv[1])で引数の文字数を取得したあと、それが10であるかif (sVar1 == 10)がチェックされています。 つまり、2つめの条件は、

2. 引数は10文字の文字列

です。


最後の認証は、次の部分です。

      if (argv[1][4] == '@') {
        puts("Nice Job!!");
        printf("flag{%s}\n",argv[1]);

if (argv[1][4] == '@')の部分で、引数の5文字目5@であるかどうかをチェックしています。 この条件をクリアすれば、ついにflagが表示されるようです。


読み解いた認証アルゴリズムをまとめます。

1. 引数がひとつ渡される
2. 引数は10文字の文字列
3. 引数の4文字目は"@"

これで、このプログラムを破れるはずです。


[関連記事] 初心者が挑むCTF入門 | OverTheWire: Bandit Level 0~20


解いたパスワードでプログラムを破る

それでは、条件を満たすパスワード0123@56789で、ファイルを開いてみます。

$ ./rev50_linux64-bit 0123@56789
Nice Job!!
flag{0123@56789}

見事ファイルが開かれ、フラッグflag{0123@56789}を取ることができました。 Nice Job!!


おわりに

今回は、Ghidraのインストールと使い方、簡単なリバースエンジニアリングの実践方法を記しました。

使用したGhidraの機能はごく限定したもので、Ghidraには他にもたくさんの強力な機能が搭載されています。 とりあえず、Ghidraで何かをリバースしたいという目標は達成できました。

なお、今回扱ったCrackmes.oneの問題も、もっとも簡単なレベルのものです。 たくさんの問題が投稿されているので、色々挑戦していきたいと思っています。


参考

Ghidra quickstart & tutorial: Solving a simple crackme - YouTube

Ghidra

Crackmes

FAQ - Crackmes

cbm-hackers's easy_reverse - Crackmes.one

今こそ再考察! C言語ポインタ徹底解説 (3) | TECH+


  1. Crackmes-リバースエンジニアリングするためのプログラムを投稿・ダウンロードできるサイト。もちろん合法的に、リバースエンジニアリングで遊ぶことができる。

  2. FAQ - Crackmes.one

  3. 小ウィンドウ: Tips、中ウィンドウ: プロジェクトマネージャー、大ウィンドウ: Help

  4. Ghidraでは配列表記[]での初期化が認識されないようです。

  5. 配列は0からはじまることに注意