【picoCTF】format string 0 - フォーマット文字列脆弱性で任意メモリを読み出す

問題概要

picoCTFの「format string 0」という問題の解説記事です。

  • カテゴリ: Binary Exploitation
  • 難易度: Easy

問題文

picoCTF format string 0

解説

この問題では、与えられたバイナリファイル 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_Cheeseprintf 的には
  • %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段階です。

  1. Patrick の注文で、count > 2 * BUFSIZE を満たして Bob の注文に進む
  2. 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での表示順が近い記事を優先しておすすめしています。