Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ImaginaryCTF 2025 stillerer-printf (魔女のお茶会 #8)

Avatar for r1ru r1ru
September 23, 2025
330

ImaginaryCTF 2025 stillerer-printf (魔女のお茶会 #8)

Avatar for r1ru

r1ru

September 23, 2025
Tweet

Transcript

  1. 自己紹介 2 • 学部4年のlow-level programmer • セキュリティ・キャンプ’22-25 • CTF (pwn)

    • 最近はunsafe RustのBug findingに興味あり Riru Oda (@ri5255)
  2. 解析: vuln.c • win関数がある (win関数に飛ばせばいいのかな?) • サイズチェックがある(えらい)ので、Buffer Overflowはない • 自明なFormat

    String Bugがある (簡単そう) • printf(buf)の後にすぐexitする • _exit関数なので__run_exit_handlersの処理は使えない 12
  3. 解析: run.py • 長さとpayloadを受け取る • asciiしか使えない (アドレス指定できない ) • secret.txtにtokenを書き込む

    • vulnを実行 (出力は見れない ) • win.txtがsecret.txtと同じなら1 (win関数を実行していれば1になる) 18
  4. 解析: run.py • 長さとpayloadを受け取る • asciiしか使えない (アドレス指定できない ) • secret.txtにtokenを書き込む

    • vulnを実行 (出力は見れない ) • win.txtがsecret.txtと同じなら1 (win関数を実行していれば1になる) • 200回実行して条件を満たせばflagを出力 (196回以上(98%)成功しないとだめ ) 19
  5. 解法 • 16倍は4bit左シフトと等価であることと、分配法則より: 0x?8 * 16 = (0x?0 + 0x08)

    * 16 = 0x?0 * 16 + 0x08 * 16 = 0x?00 + 0x80 = 0x?80 57 • 16倍すれば必ず0x80になるので、0x89を足せば0x09が作れる!
  6. まとめ Ascii文字だけで leaklessかつ 高確率(98%以上)で win関数を実行するformat stringは書ける! 64 今まで解いてきたCTFの中で最高難易度のformat string attackでした…

    他にも面白いpwn問題があったのでぜひupsolveしてみてください! (2025/09/23 11:00(JST)時点ではまだサーバーは動いている)
  7. 細かい補足 66 • Q: p.33とp.34でindex 0x38の値がしれっと変わっているのはなぜ? (0xffffea80→0x00ffea80) • A: 出力幅に負の値を指定すると、-フラグと幅として解釈されるのでうまくいかない:

    A negative field width is taken as a ‘-’ flag followed by a positive field width. (https://man7.org/linux/man-pages/man3/printf.3.html) そこで、run.pyのbugを使って、最上位byteを00にしている。(負の値にならないことを祈ることもできるが、 98%成功する必要があるので厳しい。仮に負の値にならなくても、幅として大きな値が指定され、今回出力 はstdoutに書かれる(/dev/nullじゃない)ので、タイムアウトする可能性が高い) Bug: len(payload)==lengthであることは保証されない
  8. 細かい補足 67 • Q: ASLRで変わるのはベースアドレスだけで、かつベースアドレスはpage size alignなんだから、リターン アドレスが置かれるアドレスの下位3nibbleは常に固定なのでは? • A:

    arch_align_stack関数がランダムな値を引くので固定にならない https://elixir.bootlin.com/linux/v6.16.8/source/arch/x86/kernel/process.c#L1013-L1018
  9. 細かい補足 68 • Q: %hnでリターンアドレスが置かれるアドレスを作るときに、%Nc%M$hhnのようにしないのはなぜ? %c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c %c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64914c%*c%c%c%c%c%c%c%c %c%c%c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn%*97$c%*97$c%*97$c%*97$c%*9 7$c%*97$c%*97$c%*97$c%*97$c%*97$c%*97$c%*97$c%*97$c%*97$c%*97$c%137c%106 $hhn

    • A: Xprintf_buffer関数は、$を見た瞬間にprintf_positional関数に切り替えて処理をする。 この関数はすべての引数をcopyしてから処理するので、partial overwriteで作ったアドレスを参照 することができない。そのため、%cを繰り返して調整している https://elixir.bootlin.com/glibc/glibc-2.37/source/stdio-common/vfprintf-internal.c#L990-L1340