名もなき未知

エンジニアリングとか、日常とかそういうのをまとめる場所。

PHPerKaigi 2023のPHPerチャレンジ・成瀬の挑戦状ふりかえり

iwillblog ってやつなのでふりかえってみる。

PHPerKaigiではPHPerチャレンジを楽しんでいたので、その感想を書きます。 (久々にこういうのをやっていたので楽しかったっすね)

逆にセッションは記憶があいまいになってしまったので、ゆっくりタイムシフトで見ます…。

PHPerチャレンジ編

サイボウズ

https://blog.cybozu.io/entry/phperkaigi2023-sponser

解説セッションはこちら https://fortee.jp/phperkaigi-2023/proposal/07411094-2fc1-4abd-904f-9470454531e6

  • 見た瞬間にバージョンによる挙動の違いだなーと思った
    • 頭でトレースしたけどうまくいかず
  • asdf で3環境作って実行した
    • asdf で環境構築するのが意外と疲れる話、なんかどこかで話したい。ビルドに必要なものがいろいろ必要だったり)
  • コードが失敗している理由を調べるの、勘が働きずらかった
    • 結局、でかい乗数同士の掛け算でしたね(昔、競プロで見た覚えがあるかもしれない)
      • 類題探せば出てきそう(探す気なし)
    • 2のべき乗をもってビットでアレみたいな感じじゃなくてよかったかも(桁が大きくなると線形実行が難しくなるやつ
  • PHPバージョンによって出るエラーが変わったような気がする
    • 実行結果が変わっているのか

結局変更部分は下記のような形

$v = $value;
for($i = 1; $i < $secret_key; $i++) $v = ($v * $value) % $public_key;
$hex .= dechex($v);

リピストX

https://rpst.jp/phperchallange2023-question/

  • 2番目のトークンはすぐわかった
  • 3番目のトークンは、後半が peat -> repeat っぽかったので、PHPのドキュメント検索して str_repeat で確認した
  • 4番目のトークンは正直よく知らなかったんですが、 DNS_TXT で検索したら、 dns_get_record が出てきたのでそのまま入れた形
  • 5番目のトークンはここまで解けていれば出ました!

今回解いた中だと一番易しかったように思います。

デジタルサーカス

https://www.dgcircus.com/news/594

めっちゃ大変だったので小分けにする(解説スライドは公開を確認できてないかも)。 Q2が一番面白かったです。

Q1

これだけ最後まで解けませんでした。原因は # を最初に入れ忘れていたこと(ヒントを見て i から始まって g で終わる語か~となっていて、 # を入れていませんでした、悲しいね)

ただ今回やっているうちに次の2点を知れたので良かったです。正解はできませんでしたが…

  • PNGのフォーマットの仕様上、ヘッダ、コンテンツ、フッタからなり、フッタ以降(イメージ終端)以降は表示錠は無視されるがデータを埋め込めてしまうこと
  • 難解プログラミング言語 Piet の存在の認識
    • 画像でプログラミングできる言語があるなーって噂は聞いたことがあった

また辞書的に解けないかも健闘してみて、ワードクロスのサイトも回って回答を考えていました。 こういうのも使えるねーっていうのは一つ発見かもしれません。

Q2

置換スクリプトを書いて対応です。Pythonで実装したほうが早かったので下記のコードをPythonで書きました(筆者はPythonのほうが得意なため)。 false がヒットする直前までと、直後の文字列を入れて、 直前+"true"+直後 となるような文字列を作って、ひたすら実行して探します。

あとPHP 8.2.2で実行しましたが、PHP 8.2以上?じゃないとうまく動かない気がします(中身若干いじったのも関係してるかも? GOTOだけ消したので…。ちなみにサイボウズさんの問題で先に環境入れておいたのが幸いしました)

import subprocess
import re
import time

def find_answer(content):
    pattern = re.compile(r"(false)", re.IGNORECASE)
    right_content = ""
    left_content = content
    while True:
        m = pattern.search(left_content)
        if m:
            right_content = right_content + left_content[:left_content.find(m.group(1))]
            left_content = left_content[left_content.find(m.group(1)) + len(m.group(1)):]
            filename = "Q2_temp.php"
            write_data = right_content + "true" + left_content
            with open(filename, "w") as f:
                f.write(write_data)
            try:
                output = subprocess.run(["php", filename], capture_output=True, text=True).stdout
                if not output.find("Fatal") >= 0:
                    print(output)
            except Exception as e:
                print(e)

            right_content += m.group(1)
        else:
            return

def run():
    with open("Q2.php") as f:
        content = f.read()
    find_answer(content)

if __name__ == '__main__':
    run()

結果はいい感じの表示になるのでわかりやすかったです。

Q3

なんかいい感じに実行していったらうまく言った記憶しかない(なんかコンテナ内でガチャガチャ Q3.php を実行したような…)

Q4

ファイル内を見ると 4言語で実行できる~とあるので、それぞれ実行していって key を集めていく感じ

Q5

実行していく感じ…(ちなみに答えは問題文から何となく察せる) なお、表示されているページに書かれている console.log を見ると面白かったです。

成瀬の挑戦状

解説スライドはこちら https://speakerdeck.com/nrslib/explanation-of-hidden-tokens-or-cryptography

やったこと・考えてたことをつらつらと箇条書きで。一応最後まで解けた。

  • 最初、与えられた文字列からGitHubリポジトリだと思って、そっちを探してしまった
    • DockerHubだった
  • first.php 開いて最初に考えたのは、英単語頻度の話
    • E が多いはず、だと思って多い文字を E に置き換えてみる
    • いろいろ考えるも TEA とか意味のなさそうな英単語にしかならない…
  • そこそこたってからシーザー暗号だと気づく
    • ChatGPTに復号コードを書いてもらった
  • ファイル名と、ファイルの中身に対してひたすらシーザー暗号による復号を実施
    • 意味ありげな文字列と、そうでないものがたくさん出てくる
    • SECONDHINTを見つけて、読む
  • タイトルや中身について、 base64 っぽいものと、何らかのハッシュ値っぽいものが出てきた
    • base64 になっているものは3回、 base64 --decode したら出てきた
      • 人生で一番 base64 --decode を打ち込んだかも
    • ハッシュは64bitであることが分かったので、 sha256 であることが分かった
    • このあたりで4問目は嘘という情報を得る
  • SECONDHINTを読んだ結果、誤読する
    • 関係ある~ではなく、相対的な~みたいな読み方をして、1問目で動かした文字数分あれこれするのかなとか勘違いした
    • そのあと、1文字目のモノとの差分がずらす距離だと勘違いした
    • 結構この勘違いの修正に時間がかかった
  • 2つ目の暗号方法が分からず苦戦
    • Wikipediaのシーザー暗号を眺めていたら、下のほうに 古典的な暗号 の項目に気付く
    • https://ja.wikipedia.org/wiki/ワンタイムパッド を読んでそれっぽいなとなる
      • ちなみに正答としては ヴィジュネル暗号 なので微妙に間違っている
      • が、今回に関してはほぼ同じロジックの適用で対応できた
    • ChatGPTにワンタイムパッドの復号コードを書かせる
  • 後は慎重に回答していく…
    • 鍵が GMO でめっちゃあくことに気付かなかったのでロス
    • どこかでHOMEを見るべしと書かれていたので、見に行ったらレインボーテーブルを見つける
    • それを使ってゴリゴリと…(レインボーテーブル作るところからじゃなくてよかった)
  • その結果、多分できた~となった(合計3、4時間くらいかかったかも?)

最終的に回答で使っていたコードは下記の通り。結局これもPythonで書いた(筆者はPythonのほうが得意なため)。

def decrypt_caesar(ciphertext):
    print(f"++++++++++ decrypt_caesar {ciphertext} +++++++++")
    for shift in range(26):
        plaintext = ""
        for c in ciphertext:
            if c.isalpha():
                # アルファベットの場合はシフトを適用する
                ascii_offset = 65 if c.isupper() else 97
                shifted = (ord(c) - ascii_offset + shift) % 26 + ascii_offset
                plaintext += chr(shifted)
            else:
                # アルファベット以外はそのまま追加する
                plaintext += c
        print(plaintext)
    print()

def decrypt_onetime_pad(text, key):
    print(f"++++++++++ decrypt_onetime_pad {text, key} +++++++++")
    ans = ""
    for t in text:
        plaintext = ""
        for i, c in enumerate(t):
            if c.isalpha():
                # アルファベットの場合はシフトを適用する
                ascii_offset = 65 if c.isupper() else 97
                shift = ord(key[i % len(key)]) - (65 if c.isupper() else 97)
                shifted = (ord(c) - ascii_offset - shift) % 26 + ascii_offset
                plaintext += chr(shifted)
            else:
                # アルファベット以外はそのまま追加する
                plaintext += c
        ans += plaintext + " "
    print(ans)
    print()

data = [
    "BHJDIO",
    "DHIBYR",
    "EVNNZ",
    "FRPBAQUVAG",
    "FXFUHSXQBBUDWUYDVE",
    "JABZNSLACRQR",
    "JBJYL",
    "JMTNFN",
    "KZXUK",
    "MYCOZHKDBKF",
    "OIWRXPRAU",
    "TMFAESTAQNAGKZXUFVODR",
    "V20xc2RWbFhkejA9",
    "VTDIABLQFKZQK",
    "VTDKD",
    "VTDKDYGUUO",
    "YKIUTJ",
]

i = input()
l = i.split(" ")
if len(l) == 1:
    if len(i) == 0:
        for s in data:
            decrypt_caesar(s)
    else:
        decrypt_caesar(i)
else:
    decrypt_onetime_pad(l[:-1], l[-1])

まとめ

大満足だったので来年も参加したくなりました(私自身がPython慣れしすぎていて、あんまりPHP書いてないんですが…)。

反省としては遅くまで起きすぎて(4時ごろまで)翌朝のスケジューリングに失敗してしまったことです。大反省…。 来年以降は練馬付近にホテルをとって、じっくりやる人になるのもいいなと思いました。

とはいえ、どの問題も個性的で解くのが非常に楽しかったので、いい思い出になりました。

追記

最後なんとなく 42 ではなく 47 って書いてしまった可能性があり( 57 と混ざった)、ちょっと自信がなくなっています。 深夜帯に回答をしていたので自信はないんだよなぁ。まあ連絡来なかったら来なかったでその時はその時ということで。

追記2

正解してました

https://twitter.com/gmodev/status/1646372234175590400