Misc
git_echo
从完整 .git 找回不可达提交
当前 HEAD 位于 refs/heads/main,可见提交是 89316b3。先扫描对象库:
Plain
git -C project_backup status --short --branch
git -C project_backup show-ref
git -C project_backup fsck --full --no-reflogs --unreachable --no-progress
fsck 报出三个 dangling commit 以及三个对应的 banner blob:
Plain
2bfddca0a56f7f5f6aaac60b06ac58b41de6d004
b31a305c11f473991a7c326e8bd0e769e384cb4f
be7aaf7e82a116eacb436b14835541dfe9fd5778

沿 parent 指针读取历史:
Plain
git -C project_backup log --oneline --reverse be7aaf7
得到:
Plain
89316b3 initial production snapshot
2bfddca automated banner render 1
b31a305 automated banner render 2
be7aaf7 automated banner render 3
| 提交 | 父提交 | 作用 |
|---|---|---|
89316b336e58ddc69d8179c4b602891615d51054 |
--- | 当前 production snapshot |
2bfddca0a56f7f5f6aaac60b06ac58b41de6d004 |
89316b3 |
automated banner render 1 |
b31a305c11f473991a7c326e8bd0e769e384cb4f |
2bfddca |
automated banner render 2 |
be7aaf7e82a116eacb436b14835541dfe9fd5778 |
b31a305 |
automated banner render 3 |
用 ls-tree 检查三棵树可见的文件,确认后续提交只替换 banner.png:
Plain
git -C project_backup ls-tree -r 2bfddca
git -C project_backup ls-tree -r b31a305
git -C project_backup ls-tree -r be7aaf7
导出并检查三版 banner
以二进制方式导出,避免 PowerShell 管道对 PNG 字节做编码转换:
Plain
git --git-dir=project_backup/.git show 2bfddca:banner.png > render1.png
git --git-dir=project_backup/.git show b31a305:banner.png > render2.png
git --git-dir=project_backup/.git show be7aaf7:banner.png > render3.png
| 版本 | Blob SHA-1 | 大小 | 属性 |
|---|---|---|---|
| render 1 | b41e22bbce73d5214f28363932b2418def0be767 |
126,572 B | 720×720 RGB;通道值仅 0/255 |
| render 2 | 383ab1ff7a7936407ee08a162fdeaa21ec367e7b |
126,576 B | 720×720 RGB;通道值仅 0/255 |
| render 3 | 1c068d05031d3943cbc95853c8ac4aa5c21d33ff |
126,687 B | 720×720 RGB;通道值仅 0/255 |

每个 PNG 的结构只有 IHDR / IDAT / IEND,没有 tEXt/iTXt/zTXt 或尾随数据;单独查看只能看到二值噪声,因此转向跨版本像素重组。
三通道异或复原二维码
按历史顺序测试颜色通道组合,唯一能形成完整二维码定位角并被解码器识别的组合是:
-
render 1 的 R 通道
-
render 2 的 G 通道
-
render 3 的 B 通道
核心公式:
Plain
raw_xor = r1[:, :, 0] ^ r2[:, :, 1] ^ r3[:, :, 2]
qr = 255 - raw_xor
先 XOR 得到"白色模块 / 黑色背景",再反相为标准"黑色模块 / 白色背景"。二维码有效区域约为 (64,64)--(655,655),即 37×37 modules、每个 module 16 px,外侧保留 quiet zone。


QR 解码
Plain
flag{unreachable_does_not_mean_deleted}
完整EXP
Python
from __future__ import annotations
import argparse
import io
import subprocess
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
COMMITS = (
"2bfddca0a56f7f5f6aaac60b06ac58b41de6d004",
"b31a305c11f473991a7c326e8bd0e769e384cb4f",
"be7aaf7e82a116eacb436b14835541dfe9fd5778",
)
def read_banner(repo: Path, commit: str) -> np.ndarray:
raw = subprocess.check_output(
["git", "-C", str(repo), "show", f"{commit}:banner.png"]
)
image = np.array(Image.open(io.BytesIO(raw)).convert("RGB"))
if image.shape != (720, 720, 3):
raise ValueError(f"unexpected image shape: {image.shape}")
if not np.all(np.isin(image, (0, 255))):
raise ValueError("banner is not binary-valued")
return image
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("repo", type=Path, help="extracted project_backup directory")
parser.add_argument(
"--output",
type=Path,
default=Path("reconstructed_qr.png"),
help="where to save the reconstructed QR (default: reconstructed_qr.png)",
)
args = parser.parse_args()
r1, r2, r3 = [read_banner(args.repo, commit) for commit in COMMITS]
raw_xor = r1[:, :, 0] ^ r2[:, :, 1] ^ r3[:, :, 2]
qr = 255 - raw_xor
Image.fromarray(qr, mode="L").save(args.output)
text, points, _ = cv2.QRCodeDetector().detectAndDecode(qr)
if not text or points is None:
raise RuntimeError("QR decoder did not return a payload")
corners = [tuple(map(int, point)) for point in points.reshape(-1, 2)]
print(f"decoded = {text!r}")
print(f"shape = {qr.shape}, unique = {np.unique(qr).tolist()}")
print(f"corner points = {corners}")
print(f"saved = {args.output.resolve()}")
if __name__ == "__main__":
main()
Crypto
byte_257
加密算法分析
task.py 的核心逻辑如下:
Python
MODULUS = 257
def encrypt(data: bytes, a: int, b: int, state: int) -> bytes:
encrypted = []
for value in data:
encrypted.append((value + state) % MODULUS)
state = (a * state + b) % MODULUS
return b"".join(struct.pack("<H", value) for value in encrypted)
对第 i 个明文字节 p_i,加密状态记为 s_i,则:
ci=(pi+si) mod 257c_i=(p_i+s_i)\bmod 257ci=(pi+si)mod257
si+1=(asi+b) mod 257s_{i+1}=(a s_i+b)\bmod 257si+1=(asi+b)mod257
注意输出使用小端 uint16 保存,因此 encrypted_image.bin 的 14590 字节对应 7295 个密文数值,而不是 14590 个明文字节。
密文中确实出现了数值 256(共 31 次);这也正好对应提示语 "A byte can count past 255",说明不能把文件直接按普通单字节流读取。
利用 PNG 文件头恢复状态
PNG 的固定文件头为:
Plain
明文 p: 89 50 4E 47 0D 0A 1A 0A
密文前 8 个小端 uint16 值为:
Plain
c: 165 80 219 172 242 35 87 110
由 s_i = (c_i - p_i) mod 257 得到前 8 个状态:
Plain
s: 28 0 141 101 229 25 61 100
逐项对应关系如下(密文列是按 <H 解包后的数值):
i |
p_i****(HEX) |
c_i |
s_i=(c_i-p_i) mod 257 |
|---|---|---|---|
| 0 | 89 |
165 | 28 |
| 1 | 50 |
80 | 0 |
| 2 | 4E |
219 | 141 |
| 3 | 47 |
172 | 101 |
| 4 | 0D |
242 | 229 |
| 5 | 0A |
35 | 25 |
| 6 | 1A |
87 | 61 |
| 7 | 0A |
110 | 100 |
只使用前三个状态即可解出 LCG 参数:
Plain
s1 = a*s0 + b = 0 (mod 257)
s2 = a*s1 + b = b = 141 (mod 257)
所以:
Plain
b = 141
28*a + 141 = 0 (mod 257)
28*a = 116 (mod 257)
因为 28⁻¹ = 101 (mod 257),得到:
Plain
a = 116*101 mod 257 = 151
b = 141
s0 = 28
用参数递推得到的状态序列前几项为:
Plain
28, 0, 141, 101, 229, 25, 61, 100, ...
与 PNG 文件头计算出的状态完全一致。
完整EXP
逆运算为:
pi=(ci−si) mod 257p_i=(c_i-s_i)\bmod 257pi=(ci−si)mod257
Python
import struct
import sys
import binascii
import zlib
from pathlib import Path
MODULUS = 257
PNG_HEADER = b"\x89PNG\r\n\x1a\n"
def recover(input_path: Path, output_path: Path) -> None:
raw = input_path.read_bytes()
if len(raw) % 2:
raise ValueError("密文长度不是 2 的倍数")
values = [v for (v,) in struct.iter_unpack("<H", raw)]
# 由已知 PNG 头自动求出前几个状态,再解 LCG 参数。
first_states = [
(values[i] - PNG_HEADER[i]) % MODULUS
for i in range(len(PNG_HEADER))
]
s0, s1, s2 = first_states[:3]
delta = (s1 - s0) % MODULUS
a = ((s2 - s1) * pow(delta, -1, MODULUS)) % MODULUS
b = (s1 - a * s0) % MODULUS
# 用已知头校验求出的参数。
check = s0
for expected in first_states:
assert check == expected
check = (a * check + b) % MODULUS
plain = bytearray()
state = first_states[0]
for value in values:
decoded = (value - state) % MODULUS
if decoded > 255:
raise ValueError("解密结果不是合法字节")
plain.append(decoded)
state = (a * state + b) % MODULUS
if bytes(plain[:8]) != PNG_HEADER:
raise ValueError("PNG 头校验失败")
# 解析 PNG 块并校验 CRC/IDAT,避免只凭文件头误判。
pos = 8
chunks = []
idat = bytearray()
ihdr = None
while pos < len(plain):
if pos + 12 > len(plain):
raise ValueError("PNG 块头截断")
size = struct.unpack(">I", plain[pos:pos + 4])[0]
kind = bytes(plain[pos + 4:pos + 8])
end = pos + 12 + size
if end > len(plain):
raise ValueError("PNG 块数据截断")
payload = bytes(plain[pos + 8:pos + 8 + size])
expected_crc = struct.unpack(">I", plain[pos + 8 + size:end])[0]
actual_crc = binascii.crc32(kind + payload) & 0xffffffff
if expected_crc != actual_crc:
raise ValueError(f"PNG CRC 错误: {kind!r}")
chunks.append((kind, size))
if kind == b"IHDR":
ihdr = payload
elif kind == b"IDAT":
idat.extend(payload)
pos = end
if kind == b"IEND":
break
if pos != len(plain) or chunks[-1][0] != b"IEND":
raise ValueError("PNG 未正常结束")
if ihdr is None or len(ihdr) != 13:
raise ValueError("缺少合法 IHDR")
zlib.decompress(bytes(idat))
output_path.write_bytes(plain)
print(f"a={a}, b={b}, s0={first_states[0]}")
print(f"recovered {len(plain)} bytes -> {output_path}")
print("PNG chunks:", [(k.decode('ascii'), n) for k, n in chunks], "CRC OK")
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit(f"用法: {sys.argv[0]} encrypted_image.bin decrypted.png")
recover(Path(sys.argv[1]), Path(sys.argv[2]))
本题实际解密时得到:
Plain
a=151, b=141, s0=28
recovered 7295 bytes
打开恢复的截图后,中央文字为:
Plain
flag{f257_known_header_breaks_stream}

wrapmac
算法分析
test.py 中的消息打包函数为:
Python
def pack_record(record):
return ((record["device"] << 56) |
(record["command"] << 48) |
(record["value"] << 16) |
record["counter"])
因此 64 位消息的布局是:
Plain
63 56 55 48 47 16 15 0
+-------------+------------+-----------------------+--------------+
| device | command | value | counter |
+-------------+------------+-----------------------+--------------+
标签算法非常简单:
Plain
tag = (a * message + b) & ((1 << 64) - 1)
即
t≡am+b(mod264).t \equiv a m+b \pmod {2^{64}}.t≡am+b(mod264).
其中 a 满足 1 ≤ a < 2^44(至多 44 位),b 是 64 位整数。flag 的生成规则是:将目标消息和目标标签分别按大端序打包为 8 字节,拼接后做 SHA-256,取摘要前 32 个十六进制字符。
解析公开记录
按 pack_record 重组得到以下消息整数(所有十六进制数均补足为 64 位):
| 编号 | device | command | value | counter | **message **m |
**tag **t |
|---|---|---|---|---|---|---|
| 0 | 126 | 142 | 748140490 | 20515 | 0x7e8e2c97b7ca5023 |
0xf84627af4c3598c2 |
| 1 | 93 | 44 | 1876572954 | 20515 | 0x5d2c6fda3b1a5023 |
0xa5539b3490d598c2 |
| 2 | 205 | 58 | 2828843097 | 1808 | 0xcd3aa89cb8590710 |
0xc611b4e1f1430f7c |
目标记录为:
Plain
device = 192
command = 218
value = 2425955337
counter = 63960
利用差分恢复 a
对前两条合法记录作差,常数项 b 被消掉:
Δt≡aΔm(mod264).\Delta t \equiv a\Delta m \pmod {2^{64}}.Δt≡aΔm(mod264).
注意差分必须按无符号模 2^64 计算:
Plain
Δm = (m1 - m0) mod 2^64
= 0xde9e434283500000
= 0xde9e4342835 * 2^20
Δt = (t1 - t0) mod 2^64
= 0xad0d738544a00000
= 0xad0d738544a * 2^20
生成器明确构造了一个带 20 个尾零位的消息差,其中 0xde9e4342835 是奇数。因为两边都含有 2^20,可将方程约去这一因子,但模数同时降为 2^(64-20)=2^44:
更严格地说,2^64 | 2^20 (a·d-r) 等价于 2^44 | (a·d-r),所以这里是在模 2^44 下求逆,而不是试图在模 2^64 下求一个偶数的逆。
a⋅0xde9e4342835≡0xad0d738544a(mod244).a\cdot\texttt{0xde9e4342835} \equiv \texttt{0xad0d738544a} \pmod {2^{44}}.a⋅0xde9e4342835≡0xad0d738544a(mod244).
奇数在模 2^44 下可逆,计算得到:
Plain
inv(0xde9e4342835, 2^44) = 0x9c944dbea1d
a = 0xa0fa1bb3062
该结果落在题目规定的 44 位范围内,因此解是唯一的;若没有这个约束,方程只会确定 a mod 2^44,还可能存在 a+ k·2^44 的 64 位提升。这里不能直接在模 2^64 下把 Δm 求逆,因为它是偶数;先约去 2^20 正是利用题目故意留下的结构。
恢复 b
代回第一条记录:
b≡t0−am0(mod264),b \equiv t_0-a m_0 \pmod {2^{64}},b≡t0−am0(mod264),
得到:
Plain
b = 0x341715e9752b5b5c
用恢复出的参数重新计算三条记录:
Plain
record 0: 0xf84627af4c3598c2 (match)
record 1: 0xa5539b3490d598c2 (match)
record 2: 0xc611b4e1f1430f7c (match)
第三条记录并非求解所必需,但可以用来确认参数和模运算均正确。
伪造目标标签
将目标字段按同样的位布局打包:
Plain
m_target = 0xc0da90992409f9d8
计算:
Plain
t_target = (a * m_target + b) mod 2^64
= 0x72bed45d789d800c
计算 flag
submission_flag 使用大端序,而不是把十六进制文本直接作为输入:
Plain
message = struct.pack(">Q", m_target)
tag = struct.pack(">Q", t_target)
digest = hashlib.sha256(message + tag).hexdigest()
实际拼接的 16 字节为:
Plain
c0da90992409f9d872bed45d789d800c
SHA-256 完整摘要为:
Plain
ecac2529f5bcf5960478d73d4dc7df86923a64002187dc04b0f516b921f6350b
取前 32 个十六进制字符,得到最终 flag:
Plain
flag{ecac2529f5bcf5960478d73d4dc7df86}
完整EXP
Python
import hashlib
import json
import struct
from pathlib import Path
MASK = (1 << 64) - 1
SHIFT = 20
AMOD = 1 << 44
def pack_record(r):
return ((r["device"] << 56) | (r["command"] << 48) |
(r["value"] << 16) | r["counter"])
data = json.loads(Path("records.json").read_text())
items = data["records"]
m = [pack_record(x["record"]) for x in items]
t = [int(x["tag"], 16) for x in items]
dm = (m[1] - m[0]) & MASK
dt = (t[1] - t[0]) & MASK
assert dm % (1 << SHIFT) == 0
assert dt % (1 << SHIFT) == 0
q = dm >> SHIFT
r = dt >> SHIFT
assert q & 1 # q 在模 2^44 下可逆
a = (r * pow(q, -1, AMOD)) % AMOD
b = (t[0] - a * m[0]) & MASK
assert all(((a * mi + b) & MASK) == ti for mi, ti in zip(m, t))
mt = pack_record(data["target"])
tt = (a * mt + b) & MASK
raw = struct.pack(">Q", mt) + struct.pack(">Q", tt)
flag = "flag{" + hashlib.sha256(raw).hexdigest()[:32] + "}"
print(f"a = {a:#x}")
print(f"b = {b:#x}")
print(f"target tag = {tt:016x}")
print(flag)
数据安全
shadow_route
提取目标公开打卡
读取 public_posts.json 的 target_handle,得到 blue_fox。只保留 handle == target_handle 的记录,排除所有 guest_* 打卡,并按 UTC 时间排序:
| # | published_at****(UTC) |
公开地标 | landmarks.json ** 映射的 **cell |
文本 |
|---|---|---|---|---|
| 1 | 2026-07-21T09:05:00Z |
North Library | G17-04 |
nice view |
| 2 | 2026-07-23T13:10:00Z |
Riverside Cafe | G22-11 |
arrived |
| 3 | 2026-07-26T16:45:00Z |
West Stadium | G05-19 |
nice view |
| 4 | 2026-07-29T11:00:00Z |
Innovation Hall | G31-08 |
quick stop |
时间直接按附件中的 UTC ISO 字符串比较,不做本地时区转换;这正好对应 mobility.csv 的 start_bucket_utc。
地标映射
完整映射如下(本题实际用到前四项):
| 公开地标 | 粗化 cell |
|---|---|
| North Library | G17-04 |
| Riverside Cafe | G22-11 |
| West Stadium | G05-19 |
| Innovation Hall | G31-08 |
| Central Station | G14-15 |
| Art Center | G27-02 |
mobility 关联方法
对每个公开观测点建立严格候选集合:
Plain
start_bucket_utc == published_at
AND
origin_cell == landmarks[landmark]
公开打卡发生在地标处,因此将地标网格与行程的 origin_cell 对齐。单条观测可能命中多个匿名 token,最后必须对四个集合求交集。
完整EXP
Python
import base64, csv, json
from pathlib import Path
p = Path(".")
landmarks = json.loads((p / "landmarks.json").read_text(encoding="utf-8"))
data = json.loads((p / "public_posts.json").read_text(encoding="utf-8"))
posts = sorted(
(x for x in data["posts"] if x["handle"] == data["target_handle"]),
key=lambda x: x["published_at"],
)
with (p / "mobility.csv").open(newline="", encoding="utf-8") as f:
trips = list(csv.DictReader(f))
sets = []
for post in posts:
cell = landmarks[post["landmark"]]
s = {
r["anonymous_token"]
for r in trips
if r["start_bucket_utc"] == post["published_at"]
and r["origin_cell"] == cell
}
sets.append(s)
print(post["published_at"], post["landmark"], cell, len(s), sorted(s))
common = set.intersection(*sets)
assert len(common) == 1
token = next(iter(common))
with (p / "support_tickets.csv").open(newline="", encoding="utf-8") as f:
tickets = list(csv.DictReader(f))
ticket = next(r for r in tickets if r["anonymous_token"] == token)
flag = base64.b64decode(ticket["attachment_base64"]).decode("utf-8")
print(token, ticket["ticket_id"], flag)
候选集合与唯一性交集
| 公开观测 | origin_cell |
候选数 | 候选 token |
|---|---|---|---|
| 2026-07-21 09:05Z / North Library | G17-04 |
2 | u_7fa56c8eb4519638、u_d38469e66bb02624 |
| 2026-07-23 13:10Z / Riverside Cafe | G22-11 |
3 | u_7015a96e1582ebc6、u_9b4e6ea36c0969dc、u_d38469e66bb02624 |
| 2026-07-26 16:45Z / West Stadium | G05-19 |
1 | u_d38469e66bb02624 |
| 2026-07-29 11:00Z / Innovation Hall | G31-08 |
5 | u_489861491414025c、u_7faced49baf3938a、u_c3bb67b085e5a00c、u_d38469e66bb02624、u_fb2f62cb15945f67 |
因此:
Plain
S1 ∩ S2 ∩ S3 ∩ S4
= {u_d38469e66bb02624}
四个集合的交集基数为 1,唯一 token 为:
Plain
u_d38469e66bb02624
关键 mobility 命中行
用唯一 token 回看 mobility.csv,四条公开打卡对应的关键行如下;该 token 在整个文件中共有 19 条行程,其余为噪声。
start_bucket_utc |
origin_cell |
destination_cell |
anonymous_token |
|---|---|---|---|
2026-07-21T09:05:00Z |
G17-04 |
G32-06 |
u_d38469e66bb02624 |
2026-07-23T13:10:00Z |
G22-11 |
G09-16 |
u_d38469e66bb02624 |
2026-07-26T16:45:00Z |
G05-19 |
G35-17 |
u_d38469e66bb02624 |
2026-07-29T11:00:00Z |
G31-08 |
G24-13 |
u_d38469e66bb02624 |
工单定位与 Base64 解码
在 support_tickets.csv 中按 token 精确查找,命中且仅命中一行:
anonymous_token |
ticket_id |
attachment_base64 |
|---|---|---|
u_d38469e66bb02624 |
T-100317 |
ZmxhZ3syMDlmNjYzNDU1NDI5Zjc0MjAxMjE0NzkzMjMzNGQ0M30= |
解码输出:
Plain
flag{209f663455429f742012147932334d43}