# 2025-12-09_"hit-and-miss": 251218
{%preview https://alpacahack.com/daily/challenges/hit-and-miss %}
Level: Easy
Type: Misc
solved day: 2025-12-18
## solution
```sh
python try1.py
```
出力:
```
progress: R
progress: Re
progress: Reg
progress: Reg3
progress: Reg3x
progress: Reg3x_
progress: Reg3x_C
progress: Reg3x_Cr
progress: Reg3x_Cro
progress: Reg3x_Cros
progress: Reg3x_Cross
progress: Reg3x_Crossw
progress: Reg3x_Crossw0
progress: Reg3x_Crossw0r
progress: Reg3x_Crossw0rd
FLAG = Alpaca{Reg3x_Crossw0rd}
```
Flag: `Alpaca{Reg3x_Crossw0rd}`
## How I thought.
### 1. ファイルの確認
```sh
ls -la
```
```
.rw-r--r--@ 234 s12810162 7 Dec 03:57 app.py
.rw-r--r--@ 164 s12810162 7 Dec 10:17 compose.yaml
.rw-r--r--@ 185 s12810162 6 Dec 06:08 Dockerfile
```
### 2. サーバーコードの解析
`app.py` の内容:
```python
import os, re
FLAG = os.environ.get("FLAG", "Alpaca{REDACTED}")
assert re.fullmatch(r"Alpaca\{\w+\}", FLAG)
while pattern := input("regex> "):
if re.match(pattern, FLAG):
print("Hit!")
else:
print("Miss...")
```
#### 分析
* サーバーは正規表現パターンを受け取り、フラグとマッチするか判定
* マッチすれば `Hit!`、しなければ `Miss...` を返す
* フラグの形式は `Alpaca{\w+}` (英数字とアンダースコアのみ)
* **Regex Oracle Attack** が可能:正規表現を使ってフラグを1文字ずつ特定できる
### 3. 攻撃手法の設計
#### 基本方針
正規表現の前方一致を利用して、フラグを1文字ずつ推測する。
例:
* `^Alpaca\{` → Hit! (フラグの先頭は `Alpaca{`)
* `^Alpaca\{R` → Hit! (最初の文字は `R`)
* `^Alpaca\{Re` → Hit! (2文字目は `e`)
#### 効率化:二分探索
候補文字が63文字(`[a-zA-Z0-9_]`)ある場合、総当たりでは最悪63回のクエリが必要。
文字クラスを使った二分探索で効率化:
1. 候補を半分に分割:`[a-z0-9_]` と `[A-Z]` など
2. `^Alpaca\{prefix[a-z0-9_]` でクエリ
3. Hit なら左半分、Miss なら右半分に絞り込む
4. 候補が1文字になるまで繰り返す
この方法なら、1文字あたり約 log₂(63) ≈ 6回のクエリで特定できる。
### 4. 実装上の注意点
#### プロトコルの理解
サーバーとの通信フロー:
1. 接続後、サーバーが `regex> ` プロンプトを送信
2. クライアントが正規表現パターンを送信
3. サーバーが `Hit!` または `Miss...` + 次の `regex> ` を送信
#### 初期実装の問題
最初のコードではタイムアウトエラーが発生:
```python
def query(sock, pattern: str) -> bool:
recv_until(sock, PROMPT) # ← これが原因!
sendline(sock, pattern)
resp = recv_until(sock, PROMPT)
return b"Hit!" in resp
```
問題点:
* `query()` を呼ぶ前に既にプロンプトは受信済み
* もう一度 `recv_until(sock, PROMPT)` を呼ぶと、データが来ないためタイムアウト
#### 修正
```python
def query(sock, pattern: str) -> bool:
sendline(sock, pattern) # すぐにパターンを送信
resp = recv_until(sock, PROMPT) # 応答を待つ
return b"Hit!" in resp
```
そして、接続直後に初期プロンプトを読み取る:
```python
def solve_fast():
s = socket.socket()
s.connect((HOST, PORT))
recv_until(s, PROMPT) # 初期プロンプトを消費
assert query(s, r"^Alpaca\{")
# ...
```
### 5. 解答スクリプト (`try1.py`)
```python
import socket
HOST = "34.170.146.252"
PORT = 60263
PROMPT = b"regex> "
# ASCIIの\wに対応する集合
ALPHABET = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_")
def recv_until(sock, marker=PROMPT, timeout=5.0):
sock.settimeout(timeout)
data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
if marker in data:
break
return data
def sendline(sock, s: str):
sock.sendall(s.encode() + b"\n")
def query(sock, pattern: str) -> bool:
sendline(sock, pattern)
resp = recv_until(sock, PROMPT)
return b"Hit!" in resp
def classpat(prefix: str, chars: list[str]) -> str:
# 文字クラスを使ったパターン: ^Alpaca\{prefix[charset]
charset = "".join(chars)
return rf"^Alpaca\{{{prefix}[{charset}]"
def solve_fast():
s = socket.socket()
s.connect((HOST, PORT))
# Read initial prompt after connection
recv_until(s, PROMPT)
assert query(s, r"^Alpaca\{")
prefix = ""
while True:
# 終端(})の可能性を先にテスト
if query(s, rf"^Alpaca\{{{prefix}\}}$"):
print("FLAG =", f"Alpaca{{{prefix}}}")
break
# 二分探索:集合を半分にして所属を判定
candidates = ALPHABET[:]
while len(candidates) > 1:
mid = len(candidates) // 2
left, right = candidates[:mid], candidates[mid:]
# 次の文字が left 側にあるか?
if query(s, classpat(prefix, left)):
candidates = left
else:
candidates = right
# 1文字に絞れたはず
c = candidates[0]
# 念のため確認
if query(s, rf"^Alpaca\{{{prefix}{c}"):
prefix += c
print("progress:", prefix)
else:
# ありえないが、落ちたら総当たりにフォールバック
for c2 in ALPHABET:
if query(s, rf"^Alpaca\{{{prefix}{c2}"):
prefix += c2
print("progress:", prefix)
break
else:
# どれも当たらない→終端確認
if query(s, rf"^Alpaca\{{{prefix}\}}$"):
print("FLAG =", f"Alpaca{{{prefix}}}")
break
print("Unexpected failure.")
break
s.close()
if __name__ == "__main__":
solve_fast()
```
### 6. 動作の流れ
1. サーバーに接続し、初期プロンプトを受信
2. `^Alpaca\{` でフラグの先頭を確認
3. 各文字位置で二分探索:
* 候補文字集合を2分割
* `^Alpaca\{prefix[left_half]` でクエリ
* Hit/Miss で候補を絞り込み
* 1文字に絞れたら確定
4. `^Alpaca\{prefix\}$` で終端を検出
5. フラグを出力
### 7. 実行結果
```sh
python try1.py
```
約6秒でフラグを取得:
```
progress: R
progress: Re
progress: Reg
progress: Reg3
progress: Reg3x
progress: Reg3x_
progress: Reg3x_C
progress: Reg3x_Cr
progress: Reg3x_Cro
progress: Reg3x_Cros
progress: Reg3x_Cross
progress: Reg3x_Crossw
progress: Reg3x_Crossw0
progress: Reg3x_Crossw0r
progress: Reg3x_Crossw0rd
FLAG = Alpaca{Reg3x_Crossw0rd}
```
## まとめ
* **Regex Oracle Attack**: 正規表現の判定結果を利用してフラグを抽出
* **二分探索**: 文字クラスを使って効率的に候補を絞り込み
* **プロトコル理解が重要**: 初期プロンプトの処理と、query関数内での recv_until の順序に注意
* 総クエリ数: 約 15文字 × 6回 = 90回程度で効率的にフラグを取得
フラグ名 `Reg3x_Crossw0rd` は正規表現(Regex)とクロスワード(Crossword)を掛けた言葉遊び。