6/21 - 6/22という日程で開催された。BunkyoWesternsで参加して23位。いつも出ているメンバーがフランスへ行ってしまっており、鳥さんとふたりでのんびり出ていた。
Webが解けず涙。canvasboxは通したかったねえ。まっさらな Window
を得ようとしてCustom Elementsで iframe
の錬成はできていたものの、contentWindow
がやはり消されているということで諦めてしまっていた。Node
や DOMParser
が絡んでいるはずのない frames
を見るべきだった。
[Rev 165] rot rot rot (77 solves)
Rotate it.
添付ファイル: firectf_ierae-ctf-2025-prod-93zb0_distfiles_rot-rot-rot.tar.gz
展開すると rot_rot_rot
と flag.enc
という2つのファイルが出てくる。前者のamd64のバイナリを使ってフラグをエンコードした結果を後者に保存しているのだろう。試しにバイナリを実行してみると、ご丁寧に次のように使い方を教えてくれた。
$ ./rot_rot_rot Usage: ./rot_rot_rot <input> <output>
適当なテキストファイルを投げてみると、確かにエンコードされている。よく見ると大部分が似ている。1文字ずつ総当たりをして、エンコードしたものと flag.enc
とがビット単位でもっとも一致しているものを採用していくということをやってみよう。
$ echo testdayo > plain; ./rot_rot_rot plain test_out; xxd test_out 00000000: aaac a09f 4cdf d824 b56a 06eb a5de 0e50 ....L..$.j.....P $ echo testdaaa > plain; ./rot_rot_rot plain test_out; xxd test_out 00000000: aaac a1ff 4cdf d824 b56a 06eb a5de 0e50 ....L..$.j.....P
スクリプトを書く。
import subprocess def bin2bits(b): return ''.join(bin(c)[2:].zfill(8) for c in b) def encode(s): with open('plain', 'wb') as f: f.write(s) subprocess.run('../rot_rot_rot plain encrypted', shell=True) with open('encrypted', 'rb') as f: return f.read() def diff(s, t): return sum(c != d for c, d in zip(bin2bits(s), bin2bits(t))) with open('../flag.enc', 'rb') as f: target = f.read() s = list(b'IERA???????????????????????????????????????????????????????????????????????????????????') for i in range(4, len(s)): mc, md = None, 1000000 for c in range(0x20, 0x7f): s[i] = c d = diff(encode(bytes(s)), target) if d < md: mc, md = c, d s[i] = mc print(bytes(s))
実行するとフラグが得られた。
IERAE{Rot!Rot!Rot!_91961c2b05ff7eb8b940011210731f298509d87a4c2f33b294322688d931a852}
[Misc 361] Fault Tolerance (7 solves)
I have heard that in some industries it is required to write programs that will work even if some data is corrupted! Let's try that this time!
(問題サーバへの接続情報)
添付ファイル: firectf_ierae-ctf-2025-prod-93zb0_distfiles_fault-tolerance-javascript.tar.gz
次のようなサーバのコードが与えられている。要はどの1文字を消しても hello
と出力するようなNode.js向けのコードを書けばよいらしい。
#!/usr/bin/env python3 """ Your task is to write a JavaScript code that works even if any one character of it is removed. Specifically, you must give to this script a string `prog` that meets the following conditions: * For any integer i (0 <= i < `len(prog)`), `prog[:i] + prog[i+1:]` is recognized as a valid JavaScript code and outputs `"hello\n"`. * `len(prog)` must be smaller than or equal to 1500. Do not try to save something to files and reuse it, as it would fail. """ import sys import string import random import tempfile import subprocess def myassert(cond, msg): if not cond: print(msg) sys.exit(1) def main(): sys.stdout.write('Input length: ') prog_len = int(sys.stdin.readline()[:-1]) myassert(1 <= prog_len, "Don't hack!") myassert(prog_len <= 1500, "len(prog) must be smaller than or equal to 1500") sys.stdout.write('Input prog: ') prog = '' for i in range(prog_len): prog += sys.stdin.read(1) myassert(prog_len == len(prog), "EOF detected") verified = False # fault tolerance for i in range(prog_len): print('\n# i={}'.format(i)) code = prog[:i] + prog[i+1:] with tempfile.NamedTemporaryFile(mode='w') as f: f.write(code) f.flush() # check if the program outputs "hello\n" subprocess.run(['chmod', 'o+r', f.name]) result = subprocess.run(['sudo', '-u', 'nobody', 'node', f.name], capture_output=True) myassert(result.returncode == 0, "node should exit normally") output = result.stdout print('output: {}'.format(output)) myassert(output == b'hello\n', 'the program should output "hello\\n"') verified = True myassert(verified, 'How did you fool it?') with open('./flag.txt') as f: flag = f.read() print('Well done!') print('The flag is {}'.format(flag)) if __name__ == '__main__': main()
この条件を聞いて思い出したのは放射線耐性Quineだった*1。1文字消しても動き、かつそれがQuineとして動くコードをRubyで書くというとんでもない試みだ。ただ、Rubyでなければできないような、放射線耐性Quineの実現を支えているテクニックが多く、JavaScriptで書くのはしんどそう。考えることが多いということで楽しそうだけれども。
hello
さえ出力できればよいので、別の文字が消されたときのために必要な処理ではあるけれども今は実行してほしくない、というときに process.exit(0)
すればよいだろうとか、//* */
や ///
のようにするとどこが消されてもコメントアウトされるなあとか、val
, eal
, evl
, val
にそれぞれ eval
を入れておけば、eval
からどの文字が消されてもどのみち同じ関数を指すようにできるなあとか、しばらく使えそうなテクニックを考えていた。
ふと、すでにこれを実現している人はいないかと考える。「javascript 放射線耐性」でXを検索してみると、あった。構造を見てみると、変数宣言のhoistingだとかテンプレートリテラルだとか、JavaScriptならではのテクニックを最大限活用していて美しい。
ただ、このままだと aa=a=unction=
が1文字消されて aa==unction=
という構造になったときに "Invalid left-hand side in assignment" というエラーが発生する、実行されるのはQuineであるという2つの問題がある。これらを解決する必要がある。
まずQuineでなく hello
が出力されるようにする点については、aa
という変数に代入されている文字列の中身を hello
と出力するコードに変えてしまえばよい。文字列リテラルの始まりを示すダブルクォートが消されたときに備えてコメントアウトも忘れず、(()=>{console.log(String.fromCharCode(104,101,108,108,111));process.exit(0)})()//
とした。
"Invalid left-hand side in assignment" というエラーが吐かれてしまう点については、a
と aa
をそれぞれ aa
と aaa
とリネームしてしまえばよい。これならば、1文字が消されても =
が ==
に変わってしまうことはない。
ということで、次のようなコードができあがった。ほとんど元のコードと変わらない。
aaa=aa=unction=Fnction=Fuction=Funtion=Funcion=Functon=Functin=Functio= Function aaa=aa= "(()=>{console.log(String.fromCharCode(104,101,108,108,111));process.exit(0)})()//"///" aaa.length==81&&Function`///${aaa}}```///` aaa= "(()=>{console.log(String.fromCharCode(104,101,108,108,111));process.exit(0)})()//"///" Function`///${aaa}}```///` var ar,vr,va,aaalength,unction,Fnction,Fuction,Funtion,Funcion,Functon,Functin,Functio var ar,vr,va
これを投げると、フラグが得られた。
$ (wc -c rquine.js | tr ' ' '\n' | head -n 1; cat rquine.js) | nc (省略) … # i=441 output: b'hello\n' Well done! The flag is IERAE{h3y_br0_n0t_th47_c0rrupt10n_10l_cfd1542e}
IERAE{h3y_br0_n0t_th47_c0rrupt10n_10l_cfd1542e}
スクリプトキディをしてしまった。自力でやるのが楽しいのだろうけれども、気づいてしまったので押し通すしかなかった。去年の[Misc] gnalangや[Misc] 5のような問題が好きなのでほかにもあると嬉しいなと思ったけれども、今年はその系統の問題はこれだけだったように見える。次回が楽しみだ。
*1:これ11年前の記事なんだという驚きがまずあった