この記事で扱う内容
- Kali Linux
- Ghidra
- 実際のCTF問題
この記事はマルウェア解析に興味を持った筆者が入門1週間程度で綴る、初心者による初心者向けのバイナリ解析入門となります。
実際に手を動かして解析を行う一連の流れは体感頂けると思いますので、かつての私のように壁が高そうなバイナリ解析の門を叩けずにいる方はぜひ最後までお付き合いください。
基礎知識をみっちり解説、みたいな事は最小限にとどめます。「手を動かして体感すること」を重視し、必要な用語に絞り解説を行います。
この記事はpicoCTFの特定の問題に対するネタバレ・解説を含みます。また、バイナリ解析はプロセス、マルウェア解析は目的だと筆者は捉えていますが、分かりにくい場合は同義と解釈してください。
筆者は誰?
とある企業でCSIRT(非技術系)を務める社会人3年目です。
つい先日まではTryHackMeでハッキングを楽しんでいましたが、HackTheBoxに手を出した途端にあまりの難易度に挫けそうになり、気分転換も兼ねてマルウェア解析の書籍を読んだところ興味が出てしまいこのような記事を書くに至ります。
それでは以下本編。
1. 事前準備
ここでは以下の用意をします。
Kali Linux
筆者は仮想環境にKali Linuxを構築して使用しています。ディストリによっては紹介するツールを別途インストールする必要があるかも知れません。そもそも仮想環境とかLinux環境なんてないよ!って方はChatGPTに聞きながら頑張って準備してください。
Ghidra
CTF分野でよく使われる米国製の静的解析ツールです。 KaliといいGhidraといい、セキュリティ界隈では神話の動物が流行りなのでしょうか?
上記のリンクからZIPファイルをダウンロードし、中身の./ghidraRunを実行するだけで使えます。JDKのバージョンによってはエラーが出るので、指定されたバージョンのJDKをインストールしてください。
2. 解析対象の用意
バイナリ解析においては主に実行ファイルが解析対象となります。Windowsならexe、LinuxならELF、もっというとC言語などで作成したプログラムをコンパイルしたものです。
CTFのreversingというカテゴリでは正にバイナリ解析を行う問題がありますので、今回はその問題を解きながらバイナリ解析を体感していこうと思います。
今回はpicoCTFという常設CTFの問題を解いてみたいので、上記リンクにアクセスしてアカウントを作成します。
この記事で解く問題は難易度MiduimのClassic Crackme 0x100です。

準備が整ったら早速バイナリ解析を始めていきましょう。
3. 実践
Classic Crackme 0x100はNetCatで接続するプログラムに正しいパスワードを入力することで答えのFlagが得られる問題です。サーバーで実行されているプログラムのバイナリが渡されるので、バイナリを解析して正しいパスワードを特定することがゴールとなります。
┌──(kali㉿kali)-[~]
└─$ nc titan.picoctf.net 58162
Enter the secret password: hello
FAILED!
ためしにhelloと入力してみましたが、FAILED!と言われてしまいました。
早速バイナリを見ていきます。
静的解析の前に
渡されたファイルはcrackme100というバイナリです。早速Ghidraで解析を始めたいところですが、まずはバイナリの概要を掴んでいきます。
fileコマンド
- ファイルの形式を特定するためのコマンド
- 先頭ビット列(マジックナンバー)を見ることでファイル種別を判定
┌──(kali㉿kali)-[~/Downloads]
└─$ file crackme100
crackme100: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f680c44f890f619e9d88949f9048709d008b18f1, for GNU/Linux 3.2.0, with debug_info, not stripped
ELF 64-bitと出ました。つまりこれは64bitのLinux環境で動作する実行ファイルと分かります。
実行してみる
実際にバイナリを実行してみます。先ほどNetCatで試したのと同じ結果ですね。
┌──(kali㉿kali)-[~/Downloads]
└─$ chmod +777 crackme100
┌──(kali㉿kali)-[~/Downloads]
└─$ ./crackme100
Enter the secret password: hello
FAILED!
ここでcrackme100がどのようなプログラムか推測してみます。
- ユーザーからの入力を受け付ける
- 入力を「正しいパスワード」と比較する
- 間違っていれば「FAILED!」を出力し、合っていれば恐らくflagを出力する
こう見るとそこまで難しいプログラムではありませんね。また、「正しいパスワード」を特定することができればflagが手に入る可能性が高そうです。
Ghidraへの読み込み
それではいよいよバイナリを解析していきます。Ghidraはディスアセンブル機能とデコンパイル機能の両方を兼ね備える静的解析ツールです。
- ディスアセンブル:機械語をアセンブリ言語へ変換
- デコンパイル:機械語をプログラム言語(C)へ変換
要するに機械語のままでは読んでも分からないので、最低限人間が理解できる形式へと変換するわけですね。早速Ghidraを起動します。
┌──(kali㉿kali)-[~/Downloads]
└─$ cd ghidra_11.3.2_PUBLIC
┌──(kali㉿kali)-[~/Downloads/ghidra_11.3.2_PUBLIC]
└─$ ./ghidraRun
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
起動しました。
続いて適当にプロジェクトを作成し、Tool Chestから緑のアイコンを押下します。
続いてFile > Import Filesから解析対象のバイナリを選択します。
OKを選択。続いてインポートしたバイナリを解析するか聞かれるのでYesを選択し、その後も適当に進めると解析完了後の画面に切り替わります。
ディスアセンブル、デコンパイルの完了です。この辺りについては詳しい記事が他にあるため、つまづいたら調べてみてください。
ここからはプログラムの中身を理解していきます。
main関数の確認
処理の流れを追っていくにあたって、まずはmain関数を確認します。CやC++はmainという名前の関数がプログラムのスタート地点、つまりエントリーポイントとなります。
Ghidraの画面左側にSymbol Treeというセクションがあると思いますが、この中のFunctionsを開きます。ここにはGhidraによって解析された関数の一覧が表示されます。といっても人間がプログラミングの際に定義するような関数の他に、OSが読み取るような裏側の関数なんかも表示されるので全てを見ることはありません。
ひとまずはここでmain関数が確認できると思うのでクリックします。

すると自動でmain関数が記述されている画面へと移動します。ちなみに真ん中のListingセクションがディスアセンブル結果、右のDecompileセクションがデコンパイル結果です。
Symbol Tree、Listing、Decompileの3セクションさえ覚えれば最低限の解析はできます。
上記3つのセクションが表示されていない場合、上部のアイコンが並んだエリアから「Display <セクション名>」とあるアイコンを探してクリックしてください。
ちなみにディスアセンブル結果を適当にスクロールしながら見ていくと、なんだかMOVとかCALLとかが並んだ表示が確認できると思いますが、それらがバイナリから復元されたアセンブラ言語です。
恐らく何を意味しているのか分からないと思うので、右のデコンパイル結果から見てみましょう。
C言語で記述されたプログラムが表示されていますが、こちらがcrackme100のmain関数です。といっても本物のマルウェアとは違い複雑なバイナリではないので、これがcrackme100のほぼ全てとも言えます。
この次は、ソースコードから目的の「正しいパスワード」を特定していきます。
ソースコードの読み解き
ここからはバイナリ解析特有の技術ではなく、単純にソースコードを読んで処理内容を確認します。上から読んでもいいのですが、筆者は近道をしたい派なのでflagを出力するプログラムをまず探します。
最下部にそれらしき箇所がありました。
iVar2 = memcmp(input,output,(long)(int)sVar3);
if (iVar2 == 0) {
printf("SUCCESS! Here is your flag: %s\n","picoCTF{sample_flag}");
}
else {
puts("FAILED!");
}
inputとoutputという変数をmemcmpで比較し、一致していたらflagを出力するようです。
inputはユーザーが入力した値だろうなと当たりを付けながらプログラム上部に戻ると、scanfでユーザー入力を読み取っている箇所がありました。
__isoc99_scanf(&DAT_00402024,input);
そしてoutputですが、アルファベットの小文字がランダムに並んだ文字列が入ってます。
builtin_strncpy(output, "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze",0x33);
上記がパスワードかと思いきや何やら処理が挟まっています。
i = 0;
sVar3 = strlen(output);
for (; i < 3; i = i + 1) {
for (i_1 = 0; i_1 < (int)sVar3; i_1 = i_1 + 1) {
uVar1 = (i_1 % 0xff >> 1 & 0x55U) + (i_1 % 0xff & 0x55U);
uVar1 = ((int)uVar1 >> 2 & 0x33U) + (uVar1 & 0x33);
iVar2 = ((int)uVar1 >> 4) + input[i_1] + -0x61 + (uVar1 & 0xf);
input[i_1] = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'a';
}
}
ユーザーが入力したinput文字列に1文字ずつ処理を行い、それを3回繰り返しているようです。そして処理後の文字列とoutputを比較しているみたいですね。
ChatGPTにPythonコードに変換させ、無理やり正解の文字列を出力するプログラムを作成してみました。
# solve.py
alf = "abcdefghijklmnopqrstuvwxyz"
ans = "kgxmwpbpuqtorzapjhfmebmccvwycyvewpxiheifvnuqsrgexl"
for i in range(len(ans)):
for a in alf:
tmp = a
for _ in range(3):
val = i % 0xff
uVar1 = ((val >> 1) & 0x55) + (val & 0x55)
uVar1 = ((uVar1 >> 2) & 0x33) + (uVar1 & 0x33)
iVar2 = (uVar1 >> 4) + ord(tmp) - ord('a') + (uVar1 & 0xf)
tmp = chr((iVar2 % 26) + ord('a'))
if tmp == ans[i]:
print(a,end='')
break
2重for文の中の処理をアルファベット26文字に対して順に実行し、正解文字列のn文字目と一致した際にprint関数で出力します。
実行することで正解の文字列が出力されました。
というわけで、バイナリ解析の完了です。
ここでは回答のflagは記載しませんので、どうか皆様の環境でflagを見つけてみてください。
総括
かなり説明を割愛した気がしますが、「バイナリ解析の流れを体感する」ことはできましたでしょうか。マルウェア解析においては表層解析→動的解析→静的解析というステップを踏みますが、実はこの記事でもまさにそのような流れになっています。
- 表層解析:fileコマンドでバイナリの情報を確認
- 動的解析:実際にバイナリを実行
- 静的解析:Ghidraでディスアセンブル&デコンパイル
実際にはそれぞれのステップで他にやる事は数多くあるのですが、とにかく全体の流れとしてはこんな感じです。
筆者自身かなりハードルの高さを感じていた分野ですが、1週間程度でもこの辺りまで習得できました。実際のマルウェアを扱うまでには知識・スキルが到底不足していますが、バイナリの動作を解き明かす達成感やプログラム言語と仲良くなれる側面にはとても魅力を感じます。
今後も気長に続けていきたいと思います。
以上






