はじめに
こんにちは、OUCCアドベントカレンダー15日目を担当するぐれいしゃです。
皆さんはCTFをご存じでしょうか?CTFとは、競技形式のセキュリティコンテストやイベントのことで、毎週さまざまなセキュリティ会社やCTFサークルから複数の大会が開催されています。(これらの大会はCTF Timeから見ることが出来ます。)
CTFでは、さまざまなジャンルの問題が出題されます。問題の範囲は、暗号、Web、リバースエンジニアリングなど多岐にわたります。特にreversingジャンルでは、与えられた実行ファイルなどを高級言語のコードに変換し、秘密の文字列であるFLAGを入手することが目的となります。そのためには、Ghidra(ギドラ)というデコンパイルツールを使用します。
この記事では、Ghidraの使い方と簡単な使用例について説明します。本記事を通じて、CTFやGhidra/reversingに興味を持っていただけると幸いです。
【注意】本記事で紹介する方法は、CTFなど正当な許可を得た場合に限定してご利用ください。
Ghidraについて
概要
[画像参照元:Wikipedia]
Ghidra(ギドラ)は米国家安全保障局(NSA)が開発した逆アセンブル及びデコンパイルツールです。
平たく言うと、通常、0と1からなる機械語で表現されている実行ファイルをC言語など人間にとって理解しやすい高級言語で表現されたファイルに自動的に変換するツールです。
マルウェアの解析などでは中身の分からない実行ファイルを実際に実行することで挙動を確認する(動的解析といいます)のは大きなリスクがあるため、デコンパイルという実行を伴わない手段でソースコードを解析することがあります。Ghidraはそのような静的な解析のためのツールとして用いられています。(CTFでは危険なファイルは渡されないので安心してください!)
インストール
というわけで、早速インストールしていきます。
まずは、Ghidraの実行に必要なJavaをインストールします。今回はJDK17とします。
(サポートされたバージョンのJavaが既に入っている場合はこの手順は不要です。)
Windowsの場合
次のサイトからJDK17の拡張子が.msiのファイルをダウンロードして実行して下さい。
特にこだわりのない場合はデフォルトの選択で大丈夫だと思います。
https://adoptium.net/temurin/releases/?os=windows&version=17
Linuxの場合
以下のコマンドを実行してJDKをインストールしてください。
$ sudo apt update
$ sudo apt install openjdk-17-jdk
実行し終わったら以下のコマンドでバージョンを確認して下さい。
$ java -version
OpenJDK Runtime Environment (build 17.0.6+10-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.6+10-Ubuntu-0ubuntu122.04, mixed mode, sharing)
細かいところが違っていてもbuild 17.~と書いてあればおそらく大丈夫です。
ここでjavaなんてないよと出力された場合はそもそもsudo apt install openjdk-17-jdk
がうまくいっていない可能性があります。ターミナルを再起動したり、もう一度コマンドを打ったりして試してみてください。
次に、Ghidraのリポジトリからソースをダウンロードします。
赤丸が付いているものをダウンロードしてください。
このghidra_10.4_PUBLIC_20230928.zipというファイルを適当なディレクトリ上で解凍します。
$ unzip ghidra_10.4_PUBLIC_20230928.zip
すると、中身は次のようになっていると思います。
$ ls
Extensions GPL Ghidra LICENSE bom.json docs ghidraRun ghidraRun.bat licenses server support
WindowsではghidraRun.bat、Linux/macOSではghidraRunを実行すればghidraが起動します。
小ネタですが、.bashrcなどにghidraRunのパスを追記しておくと、どのディレクトリ上でも
$ ghidraRun
だけで起動できて楽です。
使い方
ghidraRunやghidraRun.batを実行すると以下のような画面が出てきます。
I Agreeを選んでFile→New Projectを選択します。
Non-Shared ProjectのままでNextを押すとProject DirectoryとProject Nameを入力する画面が出てくるので、適当な名前を付けてfinishを押します。(私はghidraProjectという名前のプロジェクトを作りました)
すると、以下のような画面が現れるので緑色のドラゴンをクリックします。
これがGhidraの基本的な画面になります。
まだ何もデコンパイルするファイルがないので作りましょう。
次のmain.cをgccなどでコンパイルしてみます。
#include <stdio.h>
int main(){
printf("Hello World!\n");
return 0;
}
$ gcc -o main main.c
標準出力にHello World!と出力するだけのファイルですが、デコンパイルするとどうなるのでしょうか。
Code Browser(基本の画面)のFile→Import fileで先ほどのmainファイルをインポートします。
色々ウィンドウが出てくるのでYesやOKやAnalyzeを押してください。
上手くデコンパイルされるとこのような画面になります。
このままだと見づらいので左側にあるSymbol TreeからFunctionsを押してmainという関数を選択します。
右側にmain関数の中身が表示されたと思います。
これで、ようやく実行ファイルの中身を見ることが出来ました。
使用例
Ghidraの使い方がなんとなく分かってきたので、もう少し複雑な場合を考えます。
問題:ある実行ファイルとそれを用いて暗号化したFlagであるV\QWkwx!t!#qm
が与えられたとします。ここから元のFlagを求めてください。
実際には次のようなC言語のファイルをコンパイルした実行ファイルが渡されているものとします。(Cのファイルは与えられないものとします)
#include <stdio.h>
void encrypt(char* flag){
for(int i = 0; flag[i] != '\0'; i++){
flag[i] = flag[i] ^ 0x10;
}
}
int main(){
char flag[100];
printf("Input Flag: ");
scanf("%s", flag);
encrypt(flag);
printf("Encrypted Flag: %s\n", flag);
return 0;
}
上記のコードは入力として文字列を受け取り、一文字ずつ0x10とのxorを取った文字に置き換えて、標準出力に出力するという処理を行っています。
それでは与えられた実行ファイルをインポートしてGhidraで解析します。
まず、Functionsからmain関数を選択して見ると、次のようになっています。
無機質な変数名や普段は見ない関数名が並んでいて驚きますが、ひとまず処理を読んでみます。
15行目の処理を見るとlocal_78はchar型の配列であることが判明します。
そして、12、14行目でも使われているので可読性向上のために変数名を変更します。変数名にカーソルを当てて右クリック→Rename VariableでFlagなどと変えておきましょう。(もちろんFlagと関係ない変数の場合もありますが、その場合は都度直してください)
13行目でFlagを受け取り、14行目でencrypt関数を呼んでいるのでencrypt関数の中身も見てみます。
local_cはループカウンタで、encrypt関数はchar型配列__blockの文字を一文字ずつ0x10とxorを取ったものに置き換える処理を行っていることが分かります。
よって、main関数とencrypt関数を組み合わせて書き直したら、次のコードのようになります。
#include <stdio.h>
void encrypt(char *Flag,int __edflag){
int i;
for (i = 0; Flag[i] != '\0'; i = i + 1) {
Flag[i] = Flag[i] ^ 0x10;
}
return;
}
int main(void){
int __edflag;
long in_FS_OFFSET;
char Flag [104];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Input Flag: ");
__edflag = (int)Flag;
__isoc99_scanf(&DAT_00102011);
encrypt(Flag,__edflag);
printf("Encrypted Flag: %s\n",Flag);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
まだ少し分かりづらいですが、元のコードに結構近づいてきたと思います。
ここからコンパイル可能なコードに修正すると次のようになります。
#include <stdio.h>
void encrypt(char *Flag){
for (int i = 0; Flag[i] != '\0'; i++) {
Flag[i] = Flag[i] ^ 0x10;
}
return;
}
int main(){
char Flag [104];
printf("Input Flag: ");
scanf("%s", Flag);
encrypt(Flag);
printf("Encrypted Flag: %s\n",Flag);
return 0;
}
ほぼ元通りのコードになりました。あとは復号の処理を実装するところなんですが、実はxorをするだけの暗号はxorの性質上もう一度暗号化するだけで元の文字列が得られます。
実際、あるアルファベットをM、暗号化したアルファベットをC、鍵をKとすると
- $C = M\ xor\ K$
- $C\ xor\ K = M$
が成立します。(char型を想定しています)
そのため、二回暗号化すると元の文字列に戻ります。
ということで、実行ファイルに暗号文を投げてFLAG{gh1d13a}
を求めることが出来ました。
暗号化の処理が分かったら復号は一瞬という問題でした。
おわりに
長くなってしまいましたが、本記事ではCTFやGhidraの使い方について簡単に触れてみました。
今回は単純なC言語のコードを題材にしましたが、オブジェクトファイルやライブラリファイルなどをデコンパイルして問題を解くこともあります。reversingはとても奥深く、面白いジャンルなので皆さんも挑戦してもらえたらと思います。
また、CTFでなくとも自分で作ったバイナリを解析するだけでも意外と楽しかったりするので是非やってみてください。