SECCON CTF 2021作問者writeup+作問した感想

CTF Advent Calendar 2021 22日目の記事です。

前日の記事は、Xornetさんによる「SECCON CTF 2021 作問者Writeup + 運営参加記」でした。 CTF Advent Calendarを眺めていたら、Xornetさんの投稿予定を見つけて、「その手があったな」と私も登録した。 作問ではお世話になりました。

明日の記事は、Satokiさんによる「【文学】 Flagを読む」です。 え、Advent Calendarは何か気の利いたことを言いながらパスを回すっぽいけど、タイトルを見ても何も分からない……。 IMCTF、楽しかったです。 ありがとうございました。

SECCON CTFは解く側でだいたいいつも参加している。 誘ってもらって、今年は作問側に回った。 チームで作問するのは初めての経験だったし、問題の解法はそこそこに、裏話とかを書いていこうと思う。 詳細な解法は英語版のほうを見てほしい。 英語、APIのドキュメントなどはあまり困らないけど、ニュース記事や日記は読めない。 書くのも同じで、裏話みたいなのを英語のほうに書くのは諦めた。

なお、Discordでアナウンスされたように、SECCON CTF 2021のスコアサーバーと問題サーバーは少なくとも年内いっぱいは動いている予定。 まだ見ていなくて年末に時間がある人は挑戦してほしい。

score.azure.noc.seccon.jp

s/<script>//gi (misc, 115 teams solved)

英語版: s///gi (misc, 115 teams solved) - HackMD

危険な文字列を除去したいというときにやりがちなミスとして、削除処理を1回しかしないというものがある。 例えば、 <SCR<script>IPT> から <script> を1回削除すると <SCRIPT> になり、危険な文字列が残ってしまう。 「ちゃんと <script> が無くなるまで繰り返してください」という問題。 ただし、入力は64 MiB。

O(n2) では24時間のコンテスト中に終わらない(はず)。 入力をスタックに順番に入れていって、 <script> になったら取り出すということを繰り返す。 1回の削除が O(1) になり、全体が O(n) で、適当に書いても1分も掛からずに終わる。

data = open("flag.txt").read()[:-1]

stack = []
for c in data:
  stack += [c]

  if "".join(stack[-8:]).lower()=="<script>":
    for i in range(8):
      stack.pop()

print("".join(stack))

CTFのジャンルのひとつにPPC(Professional Programming and Coding)というのがあり、その問題。 「PPCを出題するのはどうなのか?」ということで揉めた。 申し訳ない。 「セキュリティ何も関係無いだろ」「競技プログラミングでやれ」的な。 「出すなら、せめてPPC脆弱性を探す問題ではない)と分かるようにするべき」ということで、この問題だけ作問者でもwarmupでもない「ppc」というタグが付いている。

競技プログラミングをやっている人(と一部のアルゴリズム研究者くらい)しか知らないアルゴリズムは色々とあるだろうけど、この問題はそうではないと思っている。 ただ、まあ、競技プログラミングをやっていない人でも頑張れば解けるくらいの難易度になっている一方で、やっている人には簡単すぎるよな……。

競技プログラミングPPCよりもCryptoのほうが近いのではないかという気がしている。 中国剰余定理とかナップサック問題とかどちらでも見る。 この競技プログラミングの問題は、最初の文章の解読だけできれば、CTFのCryptoを解いている人はあっさり解けそう。

atcoder.jp

極論、Cryptoも競技プログラミングも、愚直には指数時間掛かる処理を多項式時間に落とせば解ける。 Cryptoを解いている人が競技プログラミングに手を出したり、逆のことをしてみると、楽しいのではなかろうか。

qchecker (misc, 14 teams solved)

英語版: qchecker (misc, 14 teams solved) - HackMD

f:id:kusano_k:20211222054558p:plain

eval$uate=%w(a=%(eval$uate=%w(#{$uate})*"");Bftjarzs=b=->a{a.split(?+).map{|b|b.to_i(36)}};c=b["awyiv4fjfkuu2pkv+awyiv4f
v                  ut                  71                  6g                  3j                  +a                  x
c  e5e4pxrogszr3+5i0o  mfd5dm9xf9q7+axce5  e4khrz21ypr+5htqqi  9iasvmjri7+axcc76i  03zrn7gu7+cbt4  m8  xybr3cb27+1ge6  s
n  jex10w3si9+1k8vdb4  fzcys2yo0"];d,e,f,  g,h,i=b["0+0+zeexa  xq012eg+k2htkr1ola  j6+3cbp5mnkzll  t3  +2qpvamo605t7j  "
]  ;(j=eval(?A<<82<<7  1<<86)[0])&&d==0&&  (e+=1;k=2**64;l=->  (a,b){(a-j.ord)*25  6.pow(b-2,b)%b  };  f=l[f,k+13];g=  l
[                  g,                  k+  37];h=l[h,k+51];i=  l[i,k+81];j==?}&&(  d=e==32&&f+g+h  +i  ==0?2:1);a.sub  !
(/"0.*?"/,'"0'+[d  ,e  ,f,g,h,i].map{|x|x  .to_s(36)}*?+<<34)  );srand(f);k=b["7a  cw+jsjm+46d84"  ];  l=d==2?7:6;m=[  ?
#*(l*20)<<10]*11*  ""  ;l.times{|a|b=d==0  &&e!=0?rand(4):0;9  .times{|e|9.times{  |f|(c[k[d]/10*  *a  %10]>>(e*9+f)&  1
)!=0&&(g=f;h=e;b.  ti  mes{g,h=h,8-g};t=(  h*l+l+a)*20+h+g*2+  2;m[t]=m[t+1]=""<<  32)}}};a.sub!(  /B  .*?=/,"B=");n=  m
.                  co                  un                  t(                  ?#                  )-  a.length;a.sub  !
("B=","B#{(1..n).map{(rand(26)+97).chr}*""}=");o=0;m.length.times{|b|m[b]==?#&&o<a.length&&(m[b]=a[o];o+=1)};puts(m))*""

Quineで実装したフラグチェッカー。 フラグを1文字与えるごとに状態が変化していくので、CORRECTと表示されるようなフラグを見つければクリア。

上の色はwriteup用に画像処理ソフトで塗っている。 本当はカラーで出力するようにしたかったけれど、横幅120文字に詰め込むことができなくて諦めた。

Reversingでは、何かのVM用のバイトコードとか、謎アーキテクチャのバイナリを解析する問題を良く見るけれど、ソースコードを解析する問題があっても良いのでは? という主張。

状態を保存する変数を先頭ではなく中央あたりに持ってきたり、単純には差分が取れないように SECCON の文字を回転させたりはしているけれど、まあ、そんなに難しくはない。 結局、フラグを整数として見たときの剰余がある値になっているかどうかを調べているだけなので、ソースコードを解読したら、あとは中国剰余定理。

で、なぜこれがreversingではなくmiscなのかというと、レビューで「reversing要素が少ないからすぐに解けた」と指摘されたから。 たしかに、半分reversingで半分cryto+一発ネタ感はある。 同時に「簡単だけど、見た目のとっつきづらさと暗号要素もあるから、正解者数は伸びないかも」とも言っていて、実際にその通りですごい。

Vulnerabilities (web, 94 teams solved)

英語版: Vulnerabilities (web, 94 teams solved) - HackMD

f:id:kusano_k:20211222235528p:plain

Go製のウェブアプリ。

{
  "Name": "x",
  "name": "",
  "ID": 14
}

で解ける。

当初はWebの問題が少なく。Warmup枠が無かった(いや、正確にはCookie Spinnerが当初はwarmup枠だった。warmup……? 🤔)ので出題してみた。 最初は Name のチェックが無かったけど、「warmupとはいえ、こんなやるだけの問題はダメ」と言われた。 ボツは悲しいので、「何かもう一ひねりできないかな~」とGinやGORMのソースコードを漁ったが、Goはしっかりしていてつらかった。 JavaScriptなら、適当に違う型の値でも突っ込めば何とでもなりそうなのに。 静的型付け言語強い。 Gin(が中で使っているencoding/json)がなぜか大文字小文字を無視していたので、これで。 別に区別する必要性は無さそうなものだけど……JSONはキー名に小文字を使うことが多く、Goの構造体のフィールド名は先頭を大文字にしないといけないからだろうか。

フロント側が適当でも問題としては成立するけれど、やはり良い見た目にしたい。 最初はときどき撮っていた花の写真でも使おうかなと思ったけど、写真だけ撮っていて名前が分からないのでダメだった。 ふと脆弱性のロゴを見てみたらたいていCC0だったので、テーマを脆弱性にして、ありがたく使わせてもらった。 ちなみにデザインはPure CSS

https://purecss.io/layouts/blog/

sed programming (reversing, 14 teams solved)

英語版: sed programming (reversing, 14 teams solved) - HackMD

$ echo "SECCON{dummy}" | sed -f checker.sed
WRONG
#!/bin/sed -f

# Check flag format
# Some characters are used internally
/^SECCON{[02-9A-HJ-Z_a-km-z]*}$/!{
  cINVALID FORMAT
  b
}

:t
s/1Illl11IlIl1/1IlIl11Illl1/;tt
s/1Illl11III1/1III11Illl1/;tt
s/1Ill11IlIl1/1IlIl11Ill1/;tt
s/1Illl11l1/1l11Illl1/;tt
 :
s/o/1IlIl11IIll11IIll11IlIl11IIll11IIll11IIll11IIll1/;tt
s/O/1IlIl11IIll11IlIl11IlIl11IIll11IIll11IIll11IIll1/;tt
s/j/1IlIl11IIll11IIll11IlIl11IIll11IlIl11IIll11IlIl1/;tt
s/^/1IIll11IIll11IlIl11IIll11IIlI11l1/;tt

sedスクリプト

sedは単に正規表現で置換をするだけだと思っている人も多い……というか私もそう思っていて、「s/<script>//giにnaive解を付けたほうが良いかな? ワンライナーで書けないかな?」と調べていて、ちょっとしたプログラミング言語くらいの機能があることを知った。 info sed と打ってみると分量がすごい。

もっとも、 sed の機能はほとんど使っておらず、(空文字列を置換対象にできなかったので使った ^ 以外は)正規表現すら無しで、ひたすら置換を繰り返しているだけ。 マルコフアルゴリズム

マルコフアルゴリズムチューリング完全性をアピールしたいと思い、1次元セルオートマトンを実装している。 あるシステムAを使ってチューリング完全なシステムBをシミュレートできるならば、システムAはチューリング完全である。 入力を13世代進めて、特定の文字列になっていれば、正解。

作るのも面倒だったけど、解くのはもっと面倒だろうに、解いているチームはすごい。

constellations (reversing, 17 teams solved)

英語版: constellations (reversing, 17 teams solved) - HackMD

Goアプリの解析。 実行するとフラグが出てくるけど遅い。 高速化すればOK。

Goはネイティブコードに変換される。 綺麗に戻してくれる逆コンパイラは無いはず(あったら教えてほしい)。 Goのランタイムも入ってサイズは大きいけれど、シンボルは残しているので、 main.main から読んでいけば読めるはず。

……ということを改めて確認していたら、デバッグ情報としてソースコードが残っていることに気が付いた😩 GDB+PEDAで見ていると普通に表示されていた。 作問時に確認したときはコンソールを小さくでもしていたのだろうか?

追記: ソースコードは、バイナリの中にあるのではなく、単に私の手元にあるソースコードが表示されているだけだった。 良かった。

この問題だけフラグが妙に長いのは、フラグの各文字に対応する文字列をデバッガから1個ずつコピペするのではなく、そこも自動化してくれということである。 たとえフラグが普通の長さでも、結果的にはそのほうが早いと思う。

Average Calculator (pwnable, 56 teams solved)

英語版: Average Calculator (pwnable, 56 teams solved) - HackMD

良くあるスタックバッファオーバーフローpwnable。 ただし、スタックの各値が64bitのところ、32bitちょっとしか書き込めないので、そこを何とかしてくださいという問題。

ROPで scanf("%lld") を呼び出して、GOT overwriteをすれば良い。

平均を取っているのは、「オーバーフローを防ぐために32bit程度に制限しているんですよ」と言いたいだけであって、何の意味も無い。 何の意味も無かったのだけれど…… @moratorium08さんが、「これ、FullRELROでも解けない? i を書き換えて __libc_main のアドレスのところを飛ばすようにすれば、 sum__libc_main ±任意の値にできるから、それで system にして、あとはスタックを上手く調節すれば~」と言っていて、「平均にちゃんと意味が出てきた!」と色めき立った。まあ、冷静に考えると、__libc_main を書き換えないとROPができないので無理だったのだが。この手法では無いけれど、@moratorium08さんは結局FullRELROでも解いていたので、腕に自信のある人は挑戦してみてほしい。

感想

振り返ってみると、簡単な問題か変な問題しか作っていないな……。 作問をしてみて分かったけど、そのジャンルを極めるくらいじゃないと、良い問題は作れませんね。 「良い問題とは?」という話がそもそもあるけれど……後から「○○CTFの××の手法で解ける」みたいに語られるのは良い問題だろうか。 いつかはそういう問題を作りたい。

作問を引き受けた理由で一番大きいのは、どんな風に作問をするのか知りたかったから。

テンプレートに「 docker-compose up で問題サーバーが立ち上がるようになっていること」みたいなことが書かれていて、「は~、今どきはプルリクがマージされたら自動で問題サーバーが立ち上がるのかな。すげ~」と思っていたけれど、そこは作問者にサーバーが割り当てられて、手動だった😇 私の手元のTera Termでは、今も問題サーバーのログが流れている。 まあ、問題によっては自動化できない部分もあるだろうし、どの問題でも緊急対応をしないといけない可能性もあるし。

もっとも、作問者の手作業が発生するのはサーバーの管理くらいで、あとのソルバーによるチェックとか配布ファイルを固めるのとかは自動化(もしくはインフラチームの頑張り)されていた。 問題のレビューと合わせてそこら辺もチェックされるだろうから、CTFでときどき見かける「配布ファイル間違えてました。ごめんね」というミスも発生しづらそう。

SECCONのカンファレンスにCTFのインフラチームの発表もあって、ちょっと雰囲気が分かるかもしれない(事前に申し込んだ人だけの限定公開だったけど、なぜか公式Twitterがツイートしているので見られる)。 ↓の15分くらいから。

相互にレビューをするので、未公表の問題を何日も掛けてじっくりと解けるのも、人によっては嬉しい特権だろうか。 今回のCryptoはなぜかやたらとLLLが出題されていたけれど、そのおかげでLLLがだいぶ理解できた。 最初は非想定解法もけっこう残っているので、非想定解法を探すのが好きな人も楽しいかもしれない。