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

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

AlpacaHack Round 13 (Crypto) write-up

AlpacaHack Round 13 (Crypto)へ参加しました。そのwrite-up記事です。

コンテスト概要

2025/07/20(日) 12:00 +09:00 - 07/20(日) 18:00 +09:00の6時間開催でした。他ルールはコンテストページから引用します:

AlpacaHack Round 13 (Crypto) へようこそ!
AlpacaHack は個人戦の CTF を継続して開催する新しいプラットフォームです。

AlpacaHack Round 13 は AlpacaHack で行われる 13 回目の CTF で、Crypto カテゴリから 5 問が出題されます。 幅広い難易度の問題が出題されるため、初心者を含め様々なレベルの方に楽しんでいただけるようになっています。 問題作成者は soon-haari です!

参加方法
1. 右上の「Sign Up」ボタンから AlpacaHack のユーザー登録をしてください。
2. 登録完了後、このページの「Register」ボタンを押して CTF の参加登録をしてください。

注意事項
- AlpacaHack は個人戦のCTFプラットフォームであるため、チームでの登録は禁止しています。
- 問題は運営が想定した難易度の順に並んでいます。
- 問題の配点は解いた人数に応じて変動します。
- フラグフォーマットは Alpaca{...} です。
- 全てのアナウンスは AlpacaHack の Discord サーバー で行われます。
  - アナウンスは本サービス上でも行うことがありますが、Discord サーバーが主な連絡手段となります。
  - 問題が発生した場合、#ticket チャンネルから連絡してください。ただし、問題のヒントは提供しません。
- 競技システム自体への攻撃は行わないでください。なお、偶然発見したバグの報告は歓迎します。

これまでのRound同様に問題は運営が想定した難易度の順に並んでいますと明記されており、並び順で想定難易度が示されました。

結果

正の得点を得ている22人中、200点で13位でした:

順位と得点

チェック印: 解けた問題

また、Certificate箇所から順位の証明書も表示できます:

環境

WindowsのWSL2(Ubuntu 24.04)を使って取り組みました。

Windows(ホスト)

c:\>ver

Microsoft Windows [Version 10.0.19045.6093]

c:\>wsl -l -v
  NAME              STATE           VERSION
* Ubuntu-24.04      Running         2
  docker-desktop    Running         2
  Ubuntu-22.04      Running         2

c:\>

WSL2(Ubuntu 24.04)

$ cat /proc/version
Linux version 6.6.87.2-microsoft-standard-WSL2 (root@439a258ad544) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
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"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
$ python3 --version
Python 3.12.3
$ python3 -m pip show pip | grep Version:
Version: 24.0
$ python3 -m pip show pycryptodome | grep Version:
Version: 3.20.0
$ python3 -m pip show pwntools | grep Version:
Version: 4.14.1
$

解けた問題

[Crypto] Jerry's flag checker (21 solves, 200 points)

Jerry will verify your flag for you!

(nc接続先省略)

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

import os
from Crypto.Util.number import bytes_to_long, long_to_bytes

FLAG = os.environ.get("FLAG", "Alpaca{***** REDACTED *****}").encode()
assert len(FLAG) <= 30 and FLAG.startswith(b"Alpaca{") and FLAG.endswith(b"}") and all(0x20 <= c <= 0x7f for c in FLAG)

while True:
    try:
        if long_to_bytes(int(input("Guess the flag in integer: ")) - bytes_to_long(FLAG)).decode():
            print("Wrong flag. :P")
        else:
            print("Yay, you found the flag! :3")
    except:
        print("Weird... :/")

long_to_bytesを条件判定にわざわざ使っている、奇っ怪な見た目です!except箇所に例外メッセージを出力するよう追加して色々試していると、次のことが分かりました:

  • int(input())bytes_to_long(FLAG) よりも小さい場合、long_to_bytesへ負の数が渡されます。この場合はValueError: Values must be non-negative例外が送出され、print("Weird... :/")ルートを通ります。
  • int(input())bytes_to_long(FLAG) よりも「ある程度の範囲を超えて、非常に」大きい場合は、.decode()箇所でUnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byteのような例外が送出され、これまたprint("Weird... :/")ルートを通ります。
  • int(input())bytes_to_long(FLAG) よりも「ある程度の範囲で」大きい場合は、処理全体が成功し、print("Wrong flag. :P")ルートを通ります。
  • なおint(input())bytes_to_long(FLAG)と一致する場合であっても、long_to_bytes(0)b'\x00'を返します。PythonとしてはTruthyです。そのためprint("Yay, you found the flag! :3")ルートは絶対に通りません!

最初は二分探索と考えていましたが、UnicodeDecodeErrorルートに阻まれてできませんでした。そのため、未探索文字は候補文字のどれよりも大きい0x80としつつ、フラグの先頭から1文字ずつ確定させる方針を取りました。

なお注意点として、動作検証用フラグ"Alpaca{***** REDACTED *****}"は28文字ですが、その後のフラグ検証条件でlen(FLAG) <= 30と30文字以下まで許容されている点に注意が必要です。そのことをしばらく見落としており、「ローカルでは28文字の任意フラグを解読できるのに、リモートでは1文字も求められない!」と迷走していました。

最終的に次のソルバーになりました:

#!/usr/bin/env python3

import pwn
from Crypto.Util.number import bytes_to_long

# pwn.context.log_level = "DEBUG"


def solve(io: pwn.tube):
    candidate = bytearray(b"Alpaca{" + bytes([0x80 for _ in range(20)]) + b"}")
    candidate = bytearray([0x80 for _ in range(30)])
    print(len(candidate))
    for index in range(len(candidate)):
        for c in range(0x20, 0x80):
            candidate[index] = c
            current = bytes_to_long(candidate)
            print(f"{candidate = }")
            # print(f"{hex(current)}")
            io.sendlineafter(b"Guess the flag in integer: ", str(current).encode())
            result = io.recvline()
            print(result)
            if b"Wrong flag. :P" in result:
                break
        else:
            raise Exception("Not found...")
    print(candidate.decode())


# fmt: off
# with pwn.process(["python3", "chall.py"]) as io: solve(io)
# with pwn.process(["python3", "chall.py.original"]) as io: solve(io)
with pwn.remote("198.51.100.1", 50953) as io: solve(io)

実行しました:

$ time ./solve.py
[+] Opening connection to 198.51.100.1 on port 50953: Done
30
candidate = bytearray(b' \x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80')
b'Wrong flag. :P\n'
candidate = bytearray(b'  \x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80')
b'Weird... :/\n'
(中略)
candidate = bytearray(b' Alpaca{ASCII_oracle_attack!!{')
b'Weird... :/\n'
candidate = bytearray(b' Alpaca{ASCII_oracle_attack!!|')
b'Weird... :/\n'
candidate = bytearray(b' Alpaca{ASCII_oracle_attack!!}')
b'Wrong flag. :P\n'
 Alpaca{ASCII_oracle_attack!!}
[*] Closed connection to 198.51.100.1 port 50953
./solve.py  1.44s user 0.35s system 0% cpu 4:55.05 total

フラグの前に半角スペースが1個入っているので、探索文字範囲を間違っているとそこでも詰まっていた可能性がありそうです。ともかくフラグを入手できました: Alpaca{ASCII_oracle_attack!!}

感想

  • 1回前のRound(Crypto)は真っ当にやって0完だったので、今回は1問解けて満足です!
  • それでも2問目以降は全く分かりませんでした。Cryptoジャンルへ取り組む筋が未だに分かっていない節はあります。