Linux上でのBinaryプログラム解析方法を記す。
<問題1>
以下のファイルの種類を答えよ(拡張子が正解とする)
※googlelogoという拡張子がないファイル
<答え1>
fileコマンドを用いて対象ファイル種別を判別する。

「PNG image data」という結果から回答はPNGとなる。
<問題2>
実行プログラム「bin1.out」を解析してフラグを取得せよ
<環境準備&答え2>
「bin1.out」を実行すると入力を求めるプロンプトが現れる。答えが合うとフラグが表示されるであろうことを推測させる。ヒントがないかstringsコマンドを実行することで、答えとなるパスワードおよびフラグそのものを発見することができる。
■プログラムのソースコード(bin1.c)
#include <stdio.h>
#include <string.h>
int main(){
char input[50];
char pass[50] = "pass1234";
char flag[50] = "FLAG is {CTF_binary}";
printf("input:");
scanf("%s", input);
if(strcmp(input,pass)==0){
printf("%s\n", flag); }
else
printf("Not Correct.\n");
return 0;
}
1.プログラムのコンパイル
gcc bin1.c -o bin1.out
2.プログラムの実行画面

stringsコマンドで文字列を抽出するとパスワードらしきものとフラグが表示される。

「pass1234」や「FLAG is {CTF_binary}」という文字があることがわかる。
プログラムを実行してInputを求められたときに「pass1234」と入力するとフラグである「FLAG is {CTF_binary}」という文字列が表示される。
このようにstringsコマンドを活用することでBinary解析プログラムを用いなくともフラグにたどり着くことができる。
<問題3>
実行プログラム「bin2.out」を解析してフラグを取得せよ
<環境準備&答え3>
「bin2.out」はプログラムコードを難読化しており、その結果、stringsコマンドでフラグを取得するための文字列を取得できないようにしている。そのため、プログラム本体を解析、変更してフラグが表示されるようにする。
■プログラムのソースコード(bin2.c)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(){
char input[50];
char flag[] = { ~'C', ~'T', ~'F', ~'b',~'i',~'n', '\0' };
int i=0;
printf("input:");
scanf("%s", input);
/* password 1234pass */
if(strcmp( crypt(input,"$1$seed"),"$1$seed$9Pv9DYRW3TuJN8QETcvoE1")==0){
for (int i = 0; i < 6; i++) flag[i] = ~flag[i]; printf("Flag is %s\n", flag); }
else
printf("Not Correct.\n"); return 0;
}
1.プログラムのコンパイル
gcc bin2.c -o bin2.out -lcrypt
2.プログラムの実行画面

CTFの回答者はこのプログラムが正しいパスワードを入力しないとFlagが表示されないことを認識する。問題2のようにstringsコマンドで文字列を表示させても難読化されているためFlagを得るためのパスワードは判明しない。そのためプログラム解析ツールを用いることになる。分析方法を以下に記す。
3.gdbでの解析方法
gdb ./bin2.out
break main
run
layout asm
nexti
上記コマンドを実行し、プログラムの流れを見る。
nextiを実行していき、inputというコマンドが出たあとに
cryptやstrcmpが実行されjne命令が実行されることを確認する。


nextiコマンドでマシン命令を実行していくとprintf関数による「input:」と表示される命令があり、そのあとにscanf関数が呼び出され入力を求められる。

ここで適当な文字を入れてさらにnextiコマンドを実行していくとcrypt関数、strcmp関数が呼び出された後、「jne」命令が現れる。これはzeroflagと呼ばれるフラグレジスタがTrueでなければmainから180番地までJumpするという命令になる。このときのzeroflagの状態を「print $eflags」コマンドで確認するとzeroflagであるZFフラグがないことがわかる。

さらに命令を進めていくとmainから180番地にJumpして「Not Correct」という表示でプログラムが終了する。

このプログラムの流れは以下であることがgdbによる解析から推測される。
・Input表示後にscanf関数で文字を読み取る。
・取得した文字をcrypt関数でhash化してパスワードとマッチするかstrcmp関数で比較する。マッチしていた場合はzeroflagがセットされる。
・マッチしていない場合はJne命令によりJumpして「Not Correct」と表示される。
正しい答えを得るためにはJne命令が実行する前にzeroflagをgdbのコマンドを使用してTrueにすることで正しい答えを得ることができると推測する。

main+112にあるjne命令を実行する前に「set $eflags |=(1<<6)」コマンドでzeroflagをセットする。「print $eflags」で確認するとZFフラグがセットされたことがわかる。この状況で「nexti」コマンドで命令を実行すると、zeroflagがTrueであるためmain+180アドレスにJumpせずに次の命令(0x400778)が実行されることがわかる。

そのままnextiコマンドを実行していくとFlagが表示される。

3.objdumpでの解析とhexeditでの書き換え
gdbによる解析ではjnz命令が実行される前のZFフラグを書き換えることでFlagを得た。ほかの方法としてはjne命令自体を無効化する方法がある。これはobjdumpとhexeditコマンドで書き換えを行うことができる。
まず、無効化したい該当のjnzのマシン語をobjdumpコマンド「objdump -d bin2.out」で確認する。すると「7542」であることがわかる。

次にhexeditでjne命令である「7542」を検索コマンド「/」で検索する。

該当部分をNOP命令である「9090」に置き換え、「Ctrl+X」で保存して終了する。

保存したbin2.outプログラムを実行すると、どんな文字を入れてもFlagが表示されるようになったことがわかる。

■他の解析方法
Flagを得るための方法として
・Jne命令直前にeflagsを書き換えて比較結果を正しくする方法
・Jnz命令そのものを無効化する方法
について記載した。
他の方法としては
・Ghidraなどのツールを使いデコンパイルしてFlagを得る方法
・strcmp関数が呼ばれる前の引数そのものを一致させてしまう方法
などがある。