チームZuckerwatteとして参加し、最終的に277/1009位でした。ひとまずCpawCTFとpicoCTFをやってから臨んだけど、結構難しかった。もっと勉強しないとなぁ。(以下の見出しのカッコ内にあるのは、問題を解いた人の名前です)
R&B (benzenehat)
r_and_b.zip
を解凍し、problem.py
とencoded_flag
を見る。
problem.py
from os import getenv
FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")
def rot13(s):
# snipped
def base64(s):
# snipped
for t in FORMAT:
if t == "R":
FLAG = "R" + rot13(FLAG)
if t == "B":
FLAG = "B" + base64(FLAG)
print(FLAG)
encoded_flag
BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
problem.py
の中身から、flagに対して
・rot13(アルファベットを13文字ずらす暗号)で暗号化し、先頭に”R”をつける
・base64エンコードを行い、先頭に”B”をつける
の2種類の操作を繰り返し行っているのでは?と予想できる。つまり、encoded_flag
の文字列に対して
・先頭に”R”がついていたら、2文字目以降をrot13で復号する
・先頭に”B”がついていたら、2文字目以降をbase64デコードする
の2種類の操作を行えば元のflagが現れるはず…!
というわけで、2つのWebページ(こことここ)をいったりきたりして手動でデコードした。合計10回の操作でflagが現れた。
ctf4b{rot_base_rot_base_rot_base_base}
Spy (blaclear)
英語力が乏しくて、そもそもの問題文の意味を理解するのに時間がかかってしまった。とりあえずURLを開くと、NameとPasswordの入力が要求されている感じ。下の「Go to challenge page」を押すとチェックリストが表示される。なるほど・・・
次にヒントとして公開された2つのファイルも開いてみる。
$ cat employees.txt
Arthur
Barbara
(中略)
Ximena
Yvonne
Zalmon
こちらはユーザー名が記されたファイル。XとかZから名前が始まる人もいるんですね。
もう一つのapp.py
を開いてみると、
(省略)
# ====================
@app.route("/", methods=["GET", "POST"])
def index():
t = time.perf_counter()
if request.method == "GET":
return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t))
if request.method == "POST":
name = request.form["name"]
password = request.form["password"]
exists, account = db.get_account(name)
if not exists:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
# You know, it's really secure... isn't it? :-)
hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
session["name"] = name
return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))
# ====================
(省略)
このファイルは、先ほどのwebsiteにてどのように認証が行われているのか教えてくれている。糸口となりそうなファイルはこれしかなさそうなので、中身もしっかりと読んでみよう。パスワードのhash化にはsaltを利用してるみたいで、# You know, it's really secure... isn't it? :-)
とあるようにこれを破るのは難しそう。一方でどのような"手順"で認証しているのか見てみると、まずnameがヒットするか調べた後にpasswordを調べる2段構えになってる。ホーン、これは使えそう。しかも親切にも、計算時間を表示してくれてるじゃん。入力したnameがヒットしたかどうかは、passwordを調べるプロセスによって計算時間が長くなったかどうか見れば良いということかな?。
実際にemployeesの名前を入れてみる。Arthurでは0.00056秒、Barbaraでは0.00042秒、・・・Elbertで0.65秒!これをZalmonまで繰り返せば良いので、頑張って全員分入力してみる。最終的に計算時間が長かったのはElbert、George、Lazarus、Marc、Tony、Ximena、Yvonneの7名。これらを先ほどの"Challenge"の画面で入力すると...
やったね!
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
mask (benzenehat)
とりあえずzipファイルをダウンロード・解凍した。maskファイルが何か調べる。
$ file mask
mask: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=49166a467aee16fbfe167daf372d3263837b4887, for GNU/Linux 3.2.0, not stripped
「ELF 64bit」と書かれていることから、そのまま手元のUbuntu 64bit環境でファイルを実行した。
$ ./mask
Usage: ./mask [FLAG]
どうやら、./mask
の直後に何か単語を入れると良いらしい。いろいろ試してみるか。
$ ./mask aaaaaaaaaa
Putting on masks...
aaaaaaaaaa
aaaaaaaaaa
Wrong FLAG. Try again.
$ ./mask ctf4b{}
Putting on masks...
atd4`qu
c`b bki
Wrong FLAG. Try again.
$ ./mask abcdefghijklmnopqrstuvwxyz0123456789_{}
Putting on masks...
a`adede`a`adedepqpqtutupqp0101454501Uqu
abc`abchijkhijk`abc`abchij !"# !"#()Kki
Wrong FLAG. Try again.
入力を1文字入れると特定の2文字が返ってくる仕組みらしい。正しいflagを入れない限り答えは分からないみたいだが…
というわけで、次にファイルの中に意味のありそうな文字列がないかどうかstringsコマンドで調べてみる。もしかしたらflagがそのまま書かれているかも…!!
$ strings mask
/lib64/ld-linux-x86-64.so.2
7-2c
(中略)
Usage: ./mask [FLAG]
Putting on masks...
atd4`qdedtUpetepqeUdaaeUeaqau
c`b bk`kj`KbababcaKbacaKiacki
Correct! Submit your FLAG.
Wrong FLAG. Try again.
(中略)
.data
.bss
.comment
そんなには簡単ではなかったが、重要な情報が得られた。途中の2行に注目する。
atd4`qdedtUpetepqeUdaaeUeaqau
c`b bk`kj`KbababcaKbacaKiacki
このうちの最初の数文字が$ ./mask ctf4b{}
と打ち込んだ時の出力に一致している。つまり、**この2行こそがflagをmaskファイルの変換規則に基づいて変換した文字列ではないか…!!!**と予想できる。
とりあえずabcdefghijklmnopqrstuvwxyz0123456789_{}
に対して変換した文字列は得られているので力技で逆変換しても良いが、ちょっと大変なので逆変換のコードを書くことにする。
mask.cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
vector<string> S(3), F(2);
S[0] = "a`adede`a`adedepqpqtutupqp0101454501Uqu"; // 変換後の文字列1
S[1] = "abc`abchijkhijk`abc`abchij !\"# !\"#()Kki"; // 変換後の文字列2
S[2] = "abcdefghijklmnopqrstuvwxyz0123456789_{}"; // 変換前の文字列
F[0] = "atd4`qdedtUpetepqeUdaaeUeaqau"; // 逆変換したい文字列1
F[1] = "c`b bk`kj`KbababcaKbacaKiacki"; // 逆変換したい文字列2
for(int i = 0; i < F[0].size(); i++){
for(int j = 0; j < S[0].size(); j++){
if(F[0][i] == S[0][j] && F[1][i] == S[1][j]){
cout << S[2][j];
break;
}
}
}
}
C++では、”
を文字列の一部として扱う場合には、\
をつけてエスケープシーケンスとして扱う必要があることに注意。
$ g++ mask.cpp
$ ./a.out
ctf4b{dont_reverse_face_mask}
Flagが得られたよ。やったね!!
ctf4b{dont_reverse_face_mask}
Welcome (blalcear)
warm upみたいな位置づけか。Rulesの中にDiscordへのリンクがあったので、ひとまずそれを踏んだ。”announcement”にFlagを発見。
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode (kanau)
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲
🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰
🌰🍪🍩🍽
絵文字が出てきた。
「UnicodeのEmojiの一覧」を確認すると、U+1F330 から U+1F37D までの絵文字が使われていることがわかった。
U+1F363 U+1F374 U+1F366 U+1F334 U+1F362 U+1F37B U+1F373 U+1F374 U+1F365 U+1F367 U+1F361 U+1F36E U+1F330 U+1F367 U+1F372 U+1F361 U+1F370 U+1F368 U+1F379 U+1F35F U+1F362 U+1F379 U+1F35F U+1F365 U+1F36D U+1F330 U+1F330 U+1F330 U+1F330 U+1F330 U+1F330 U+1F36A U+1F369 U+1F37D
シーザー暗号のような気がする。
試しに🍟が'a'に対応するようにずらしてみる。
evh6d}uvgicp2itcrj{ad{ago222222lk
になる。2つ前にずらすとctf4b…
になりそう。
2つ前にずらす。
ctf4b{stegan0graphy_by_em000000ji}
flagが出てくる。
🌰が'0'に対応するようにずらせばよかったらしい。
steganography by emojiでした。