問題概要
picoCTFの「format string 0」という問題の解説記事です。
- カテゴリ: Binary Exploitation
- 難易度: Easy
問題文
解説
この問題では、与えられたバイナリファイル
format-string-0 とソースコード format-string-0.c を解析して、フラグを表示させます。ポイントは Format String Vulnerability(フォーマット文字列脆弱性) です。
ステップ1: ソースコードを読んで全体像を掴む
まず
format-string-0.c を見て、フラグがどこで表示されるか確認します。特に注目すべきなのが、次の部分です。
char flag[FLAGSIZE];
void sigsegv_handler(int sig) {
printf("\n%s\n", flag);
fflush(stdout);
exit(1);
}
SIGSEGV(セグメンテーションフォルト)が起きると、flag を表示して終了します。
つまりこの問題は、- クラッシュさせれば勝ち
という設計です。
ステップ2: どこが脆弱か
次に、入力箇所を追います。
Patrick の注文(1人目)
char choice1[BUFSIZE];
scanf("%s", choice1);
...
int count = printf(choice1);
ここが脆弱です。
printf(choice1) により、入力文字列がそのままフォーマットとして解釈されます。ここでの「問題の本質」は、ユーザー入力を“文字列”として表示したいだけなのに、
printf の“書式”として扱ってしまっている点です。本来は次のように、フォーマットは固定で
%s にすべきです。printf("%s", choice1); // 安全な書き方
次にメニューを見ます。
char *menu1[3] = {"Breakf@st_Burger", "Gr%114d_Cheese", "Bac0n_D3luxe"};
Gr%114d_Cheese は printf 的には%114d= 「整数を幅114で出力」
という命令になります。
ただし
printf(choice1) では %d に対応する引数(int)を一切渡していません。
それでも printf は、呼び出し規約に従い「引数が渡されているはずだ」と仮定してスタック上の値などを勝手に読み取り、整数として扱います。このとき重要なのは「数字が何になるか」よりも、幅114指定によって“出力文字数を稼げる”ことです。
この問題では
printf の戻り値(出力文字数)が count に入るので、
%114d を含む選択肢を選ぶだけで count > 64 を満たしやすくなり、次の Bob パートへ進めます。if (count > 2 * BUFSIZE) {
serve_bob();
} else {
printf("%s\n%s\n",
"Patrick is still hungry!",
"Try to serve him something of larger size!");
fflush(stdout);
}
}
Bob の注文(2人目)
char choice2[BUFSIZE];
scanf("%s", choice2);
...
printf(choice2);
2人目も同様に
printf(choice2) になっています。char *menu2[3] = {"Pe%to_Portobello", "$outhwest_Burger", "Cla%sic_Che%s%steak"};
この中の
Cla%sic_Che%s%steak が、クラッシュ要因です。
printf において、%s は 「次の引数(ポインタ)は char*(文字列へのポインタ)である」 という意味を持ちます。printf は %s を見つけると、- 次の引数をポインタとして取り出し
- そのポインタが指すアドレスに移動する
- そこから
\0が出るまでメモリを読み続ける
という動作を行います。
値を表示するだけの
値を表示するだけの
%d などと違い、ポインタを辿ってメモリを読み取りに行くため、不正なアドレスを引いた瞬間にクラッシュしやすいです。Cla%sic_Che%s%steak を正しく使うなら、 printf は次のように呼ばれることを想定しています。
printf("Cla%sic_Che%s%steak", ptr1, ptr2, ptr3);
%sが3つあるので、3つのchar*ポインタが引数として渡される想定です。しかし実際のコードは
printf(choice2); なので、ptr1 などは渡されません。
それでも printf は %s を見つけるたびに、- 「次の引数(ポインタ)があるはず」と仮定して
- スタック上のゴミ値をポインタとして取り出し
- ポインタが指す先を読み取りに行きます。
スタック上のゴミ値は高確率で「読めないアドレス」なので、
不正メモリアクセスが起きて
SIGSEGV → ハンドラ発火 → フラグ表示、という流れになります。ステップ3: 攻撃方針(2段階)
この問題の流れは次の2段階です。
- Patrick の注文で、
count > 2 * BUFSIZEを満たして Bob の注文に進む - Bob の注文で、
printfを%sで暴走させてSIGSEGVを起こし、フラグを表示させる
3-1: Patrick を満腹にする(出力文字数を稼ぐ)
Patrick パートでは
printf の戻り値(出力した文字数)が count に入ります。if (count > 2 * BUFSIZE) {
serve_bob();
} else {
printf("%s\n%s\n",
"Patrick is still hungry!",
"Try to serve him something of larger size!");
fflush(stdout);
}
}
ここで
BUFSIZE=32 なので、条件は count > 64 です。メニューの
Gr%114d_Cheese に注目すると、%114d は- 整数を幅114で出力(足りない分は空白でパディング)
という意味です。
そのため、これを
printf に渡すと「スタック上のどこかの値」を整数として出しつつ、
最低でも114文字ぶんの出力が発生しやすく、count > 64 を満たせます。そのため、Patrick への入力はこれでOKです。
Gr%114d_Cheese
3-2: Bob の注文で店を破壊する(%s でセグメンテーションフォルトを狙う)
Bob のメニューには
Cla%sic_Che%s%steak が含まれています。Cla%sic_Che%s%steak
これを
printf(choice2) に渡すと、%s のたびに 「次の引数(本来は文字列ポインタ)」 をスタックから読みます。しかし実際には
printf に引数を渡していないため、
printf はスタック上のゴミ値をポインタだと思って参照しに行き、セグメンテーションフォルトになるはずです。セグメンテーションフォルトが起きた瞬間、
sigsegv_handler が動き、フラグが表示されます。ステップ4: 攻撃の実行
picoCTFの問題ページに書かれているホスト/ポートで
nc 接続します。$ nc mimas.picoctf.net xxxxx
(実際のポート番号は問題ページを参照してください)
接続後、1回目(Patrick)でこれを入力:
Enter your recommendation: Gr%114d_Cheese
続けて2回目(Bob)でこれを入力:
Enter your recommendation: Cla%sic_Che%s%steak
うまくいくとクラッシュして、フラグが表示されます。
picoCTF{xxxxx}
※フラグはマスクしています。
まとめ
format string 0 は、printf(user_input) という典型的なミスから起きる フォーマット文字列脆弱性の入門問題でした。▼ポイントは以下の通りです。
printfにユーザー入力をそのまま渡すのは危険%114dで「出力文字数」を稼いで次のステージへ進められる%sを含む文字列でprintfを暴走させ、セグメンテーションフォールト → フラグ表示
閲覧ありがとうございました!
NEXT
次におすすめ
読み終わったら、そのまま次へ
【picoCTF】WebDecode - DevToolsでHTMLに埋め込まれたBase64フラグを復号
カテゴリ: Web Exploitation難易度: Easy#picoCTF
次の記事へ →
同じカテゴリ/難易度/picoCTFでの表示順が近い記事を優先しておすすめしています。