【保護機構と回避編】Canary・PIE・ASLR・RELROの戦い方|CTF思考フレームワーク #65
こんにちは、アンペンです!
今回は保護機構と回避編。これまでBoF・ROP・FSB・Heap攻撃と扱ってきましたが、攻撃側の発展に伴いOS・コンパイラ・CPUは多層の保護機構を積み上げてきました。checksec1コマンドで4項目(Canary・NX・PIE/ASLR・RELRO)を確認できる現代では、各保護の『何を守るか』『どう突破されるか』を覚えるのが攻防両面で必須です。
さらにIntel CET(Shadow Stack/IBT)、ARM PAC/MTE、Hardened allocator(Scudo/GWP-ASan)など新世代のHW・OS保護も実用フェーズに入っており、古典攻撃は『そもそも成立しない』未来が見えてきました。
保護機構の話は、つい『専門用語の暗記大会』になりがちですが、見方を変えると面白いんです。攻める側にとっては『どの壁が立っているか』を最初に確認する偵察、守る側にとっては『どの壁を立てるか』の設計図。同じ4項目を、攻守どちらの目でも読める——それがこの回のテーマです。checksec の出力を“作戦ボード”として読めるようになりましょう。

checksecで全部緑なら絶対安全?

絶対ではない。各保護は『リーク→計算→上書き』を1段ずつ重くする壁。Canaryはリーク経路、PIE/ASLRはアドレスリーク、RELRO Fullなら__free_hook等のhookか_IO_FILE系を狙う応用へ。1層ずつ崩していく発想を持とう。
4大保護機構の役割と典型突破口:(1)Stack Canary(BoFでretを書き換えると検証値ズレで検出→%p等でcanary値リークすれば回避)、(2)NX/DEP(データ領域実行禁止→ROPで既存命令の継ぎ接ぎ)、(3)ASLR/PIE(コード/libcアドレスをランダム化→libcリーク+offset計算 or 部分上書き(下位12bit固定の性質)で回避)、(4)RELRO Full(GOT書込禁止→__free_hook(glibc <2.34)/_IO_FILE系を狙う応用へ)。すべてON+CET-IBT+Shadow Stackが現代のベストプラクティス。
この記事で分かること
- 4大保護機構それぞれの仕組みと守る範囲
- checksec出力の正しい読み方
- 各保護の典型的な回避手筋
- 新世代保護(CET / PAC / MTE / Scudo)の役割
- ビルド時推奨フラグまとめ
📖 はじめてのWebセキュリティ #65|保護機構と回避編
Canary・PIE・ASLR・RELROの戦い方。 シリーズ一覧を見る →
⚠️ 大事なお約束
本番システムへのexploitは違法。CTF・自作バイナリのみで確認してください。
4大保護機構の役割を1枚で把握
- Stack Canary:関数entry時にスタックに『見張り番』値(
fs:0x28から読む)を置き、ret前に検証。BoFで戻りアドレスまで上書きするとcanaryもズレるためabortできる。-fstack-protector-strongで有効化 - NX/DEP(W^X):同じページに書き込み権限と実行権限を同時付与しない。スタック・ヒープ上のshellcode実行を不可能にする
- ASLR/PIE:カーネルが各実行でlibc/コード/スタック/ヒープのベースアドレスをランダム化。アドレス推測攻撃を困難に。PIE(Position Independent Executable)で本体バイナリもランダム化対象に
- RELRO(Partial/Full):Partialは『起動時にGOTを書込禁止だが遅延解決領域は書込可』、Fullは『起動時に全関数を解決し全GOTを読取専用化』。GOT Overwriteの根を断つ
ここで大事な考え方を一つ。これらの保護は、単体で『絶対に破られない壁』ではありません。それぞれは『攻撃を一段ずつ重くする壁』です。Canaryがあればリークの一手間が増え、ASLRがあればアドレス特定の一手間が増える。一枚一枚は乗り越えられても、重ねるほど攻撃の手数とコストが跳ね上がる。“完璧な壁”ではなく“面倒くささの積み増し”——この発想が、多層防御の本質なんです。
図解:checksecの読み方と各層の突破口
checksec --file=./vulnで一目で確認できる4項目について、『どの状態でどう突破できるか』を頭に入れておくと、CTF問題開いた瞬間に作戦が立ちます。

Canaryは『城門の見張り番』、NXは『城内の武器庫封印』、ASLR/PIEは『城内地図のランダム配置』、RELRO Fullは『金庫を起動後に固定して触らせない』。城を落とすには見張りを買収(canaryリーク)→武器庫を諦めて既存武器を使う(ROP)→ランダム地図を入手(アドレスリーク)→金庫が無理なら使用人を脅す(__free_hook書換)と層を1つずつ崩していくのが定石。すべての層が完璧に機能していれば、攻撃者には『不可能ではないが現実的に困難』な状況を作れます。
ここで覚える用語:部分上書き(Partial Overwrite)
ASLRはページ境界(0x1000バイト=12bit)単位でランダム化されるため、下位12ビットはASLRに影響されない固定値です。これを使い、戻りアドレスや関数ポインタの下位2バイトだけを書き換えると、ASLR下でも別の固定関数オフセットに飛ばせます。例:win関数が呼出元と同一page内にあれば、下位2バイトの書換だけで届くため、リーク不要でASLRを部分的に迂回できます。CTF頻出の小技。
保護別 定番回避マップ
- Canary回避:FSBやformat string経由でcanary値を盗む→ペイロード内で同値に再現してret書換。マルチスレッドではfork後にcanaryが継承される性質を利用してbrute-force回避もある
- NX回避:shellcode諦め→ROP / ret2libc / ret2csu / SROP(Sigreturn-Oriented Programming)
- ASLR/PIE回避:libc関数アドレスをリーク(
puts(puts@got)等)→libc_base = leak - libc.symbols['puts'] - RELRO Full回避:GOT書換は無理→
__free_hook/__malloc_hook(glibc < 2.34)、または_IO_2_1_stdout_のvtableを書く - すべて緑+CET+メモリ安全言語:古典攻撃ほぼ無効。ロジック脆弱性や認証バグなど、別の入り口を探す段階に移行

新世代保護機構:CET・PAC・MTE・Scudo
古典4層を越えるため、現代CPUとモバイルOSは『そもそも攻撃の前提を壊す』ハードウェア保護を装備し始めています。
ここからの新世代保護は、発想がひと味違います。古典4層が『攻撃を難しくする』壁だったのに対し、CETやMTEは『そもそも攻撃の前提を成り立たせない』——たとえば“戻りアドレスは書き換えられて当たり前”という前提を、ハードウェアごとひっくり返してしまうんです。攻撃者が長年使ってきた土台そのものを崩しにかかる、いわば“ちゃぶ台返し”の防御だと言えます。
- Intel CET-Shadow Stack:HW専用の影スタックがcall時にretアドレスを保存、ret時に通常スタックと比較。BoF/ROPで戻りアドレスを書き換えると即CPU例外。Tiger Lake(2020)以降のIntel CPUとWindows 11・Linux 6.x系で実用化
- Intel CET-IBT(Indirect Branch Tracking):間接call/jmpの先頭に
endbr64命令を要求。攻撃者ガジェットの多くはendbr64を持たないため弾かれる - ARM PAC(Pointer Authentication):関数ポインタの上位ビットに暗号署名を付与、改ざんを検出。Apple M1/M2やAndroid ARMv8.3+で実装
- ARM MTE(Memory Tagging Extension):16バイト粒度でメモリにタグを付与、アクセス時にポインタのタグと比較。UAF/Heap-BoFを確率的に検出。Pixel 8+で本番採用
- Hardened allocator:
Scudo(Android標準)、GWP-ASan(確率的ASan)、mimalloc-secureでヒープ攻撃を本番環境でも低オーバーヘッドで検出

CTFでやってみよう:checksec読み→突破口決定
checksec出力から exploit戦略を組み立てる
目的は『checksecの4項目を見ただけで作戦が立つ』状態にすることです。CTF本番では時間が命なので、ルーチン化が効きます。
checksec --file=./vulnで4項目(Canary/NX/PIE/RELRO)を確認- Canary=Yes → リーク手段(
%p等のFSB or fork後brute-force)を探す - NX=Yes(=ほぼ常時) → shellcode諦め、ROPで構築
- PIE=Yes → libc関数アドレスのリーク経路を確保(
puts等) - RELRO=Full → GOT書換は諦め、
__free_hook(glibc < 2.34)/_IO_FILE系を狙う - pwntoolsで
elf.symbols['main']/libc.address = leak - libc.symbols['puts']等を使って自動計算 - すべてONなら、Pwn以外(認証バグ・ロジック脆弱性)を探すフェーズに切り替える判断
- 余裕があればCETが有効な環境で過去のexploitが動かないことを確認
守る側:『すべてON + 新世代を順次追加』
-fstack-protector-strong(Canary)、-fstack-clash-protection(大きなスタック割り当て検出)-fPIE -pie(PIE/ASLR有効化)、リンクも対応必要-Wl,-z,relro -Wl,-z,now(Full RELRO)、ライブラリ含む全プロジェクトで-D_FORTIFY_SOURCE=2 -O2(libc強化チェック、buffer overflow検出)-fcf-protection=full(CET-IBT+Shadow Stack有効化、対応コンパイラ必須)-fzero-call-used-regs=used-gpr(関数呼出後にレジスタゼロクリア、ROP情報リーク削減)- CIでchecksecを全成果物に対して回し、退化を検出。1項目でも欠けたらビルド失敗にする
- 本番にはScudo/GWP-ASanのHardened allocatorを採用
- 新規プロダクトはRust/Go等のメモリ安全言語でこれら保護の必要性そのものを下げる

『城を1層ずつ崩す』ってイメージで分かりやすいね。

次回はPwn章の総まとめ。実戦シナリオ+チートシートで完走するよ。
まとめ:『1層ずつリークして崩す』
- 4大保護:Canary / NX / ASLR/PIE / RELRO、それぞれ守る範囲が違う
- 突破はリーク→計算→上書きの繰り返し、層が多いほど手数が増える
- 新世代:CET-Shadow Stack/IBT, ARM PAC/MTE, Scudo/GWP-ASan
- 守りはすべてON+新世代+メモリ安全言語の組合せ、CIで退化を検出
今日の持ち帰りは『保護機構は“偵察対象”であり“設計図”でもある』。攻める人は checksec で立っている壁を見て手数を読み、守る人は全部の壁を立ててCIで退化を見張る。そして時代は、壁を高くする発想から“前提を壊す”新世代保護へ。攻防の最前線がいちばん速く動く、それがこの保護機構の領域です。
次回はPwn章の総まとめ。実戦シナリオとチートシートで全Pwn技術を統合して完走します。
