Full Weak Engineer CTF 2025に参加した

投稿日: 2025-09-03

概要

8/29~8/31に開催されたFull Weak Engineer CTF 2025に参加しました。

解いた問題

Forensic/OSINT:RSA Phone Tree(240 Solve)

いわゆるプッシュ音(DTMFっていうんだね)からRSAのp, q, cを解読する。
プログラムはAIにぶん投げ。

長いので折りたたみ
import numpy as np
import scipy.io.wavfile as wav
from scipy import signal
from scipy.signal import butter, filtfilt
import matplotlib.pyplot as plt
from typing import List, Tuple, Optional
from Crypto.Util.number import long_to_bytes

class RobustDTMFDecoder:
    """堅牢なDTMF音声信号復号化クラス"""
    
    def __init__(self):
        # DTMF周波数マップ(Hz)
        self.dtmf_freqs = {
            '1': (697, 1209), '2': (697, 1336), '3': (697, 1477), 'A': (697, 1633),
            '4': (770, 1209), '5': (770, 1336), '6': (770, 1477), 'B': (770, 1633),
            '7': (852, 1209), '8': (852, 1336), '9': (852, 1477), 'C': (852, 1633),
            '*': (941, 1209), '0': (941, 1336), '#': (941, 1477), 'D': (941, 1633),
        }
        
        # 逆マッピング
        self.freq_to_digit = {v: k for k, v in self.dtmf_freqs.items()}
        
        # 周波数リスト
        self.low_frequencies = [697, 770, 852, 941]
        self.high_frequencies = [1209, 1336, 1477, 1633]
        
        # パラメータ
        self.frequency_tolerance = 30  # 周波数許容誤差
        self.min_tone_power = 0.05     # 最小音パワー
        self.min_duration_ratio = 0.8  # 最小持続時間比率
    
    def load_audio(self, filename: str) -> Tuple[np.ndarray, int]:
        """音声ファイルを読み込む"""
        try:
            sample_rate, audio_data = wav.read(filename)
            if len(audio_data.shape) > 1:
                audio_data = np.mean(audio_data, axis=1)
            
            # 正規化
            audio_data = audio_data.astype(np.float32)
            if np.max(np.abs(audio_data)) > 0:
                audio_data = audio_data / np.max(np.abs(audio_data))
            
            print(f"音声読み込み成功: {filename}")
            print(f"  サンプルレート: {sample_rate} Hz")
            print(f"  音声長: {len(audio_data)/sample_rate:.3f} 秒")
            print(f"  振幅範囲: {np.min(audio_data):.3f} ~ {np.max(audio_data):.3f}")
            
            return audio_data, sample_rate
            
        except Exception as e:
            raise Exception(f"音声ファイルの読み込みに失敗: {e}")
    
    def preprocess_audio(self, audio_data: np.ndarray, sample_rate: int) -> np.ndarray:
        """音声データの前処理"""
        # バンドパスフィルタ(600-2000 Hz)
        nyquist = sample_rate / 2
        low = 600 / nyquist
        high = 2000 / nyquist
        
        try:
            b, a = butter(4, [low, high], btype='band')
            filtered = filtfilt(b, a, audio_data)
        except:
            # フィルタリングに失敗した場合は元のデータを使用
            filtered = audio_data
        
        return filtered
    
    def detect_tone_segments_multiple_methods(self, audio_data: np.ndarray, sample_rate: int) -> List[Tuple[int, int]]:
        """複数の手法でトーンセグメントを検出"""
        
        # 方法1: エネルギーベース
        segments_energy = self.detect_segments_by_energy(audio_data, sample_rate)
        
        # 方法2: 固定ウィンドウベース  
        segments_fixed = self.detect_segments_by_fixed_window(audio_data, sample_rate)
        
        print(f"エネルギーベース検出: {len(segments_energy)} セグメント")
        print(f"固定ウィンドウ検出: {len(segments_fixed)} セグメント")
        
        # より多くのセグメントを検出した方を使用
        if len(segments_energy) >= len(segments_fixed):
            print("エネルギーベース検出を採用")
            return segments_energy
        else:
            print("固定ウィンドウ検出を採用")
            return segments_fixed
    
    def detect_segments_by_energy(self, audio_data: np.ndarray, sample_rate: int) -> List[Tuple[int, int]]:
        """エネルギーベースでセグメント検出"""
        # 短期エネルギーを計算
        window_size = int(sample_rate * 0.01)  # 10ms
        energy = []
        
        for i in range(0, len(audio_data) - window_size, window_size//2):
            window = audio_data[i:i + window_size]
            energy.append(np.sum(window ** 2))
        
        energy = np.array(energy)
        
        # 適応的閾値
        energy_threshold = np.mean(energy) + 2 * np.std(energy)
        energy_threshold = max(energy_threshold, np.max(energy) * 0.1)
        
        # セグメント検出
        is_tone = energy > energy_threshold
        changes = np.diff(is_tone.astype(int))
        
        starts = np.where(changes == 1)[0] + 1
        ends = np.where(changes == -1)[0] + 1
        
        # 境界調整
        if len(starts) == 0 or (len(ends) > 0 and starts[0] > ends[0]):
            starts = np.concatenate([[0], starts])
        if len(ends) == 0 or (len(starts) > 0 and starts[-1] > ends[-1]):
            ends = np.concatenate([ends, [len(is_tone)]])
        
        # サンプル位置に変換
        segments = []
        min_samples = int(sample_rate * 0.03)  # 最小30ms
        
        for start, end in zip(starts, ends):
            start_sample = start * window_size // 2
            end_sample = min(end * window_size // 2, len(audio_data))
            if end_sample - start_sample >= min_samples:
                segments.append((start_sample, end_sample))
        
        return segments
    
    def detect_segments_by_fixed_window(self, audio_data: np.ndarray, sample_rate: int) -> List[Tuple[int, int]]:
        """固定ウィンドウでセグメント検出(元のスクリプトのパラメータに基づく)"""
        # 元のスクリプト: tone_time=0.08, silence_time=0.10
        tone_samples = int(sample_rate * 0.08)
        silence_samples = int(sample_rate * 0.10)
        segment_samples = tone_samples + silence_samples
        
        segments = []
        total_expected = len(audio_data) // segment_samples
        
        print(f"期待されるセグメント数: {total_expected}")
        print(f"トーン長: {tone_samples} サンプル ({tone_samples/sample_rate:.3f}s)")
        
        for i in range(total_expected):
            start = i * segment_samples
            end = start + tone_samples
            
            if end <= len(audio_data):
                # セグメントにある程度のエネルギーがあることを確認
                segment = audio_data[start:end]
                if np.std(segment) > 0.01:  # 最小エネルギー閾値
                    segments.append((start, end))
        
        # 残りの部分もチェック
        if total_expected * segment_samples < len(audio_data):
            remaining_start = total_expected * segment_samples
            remaining_end = len(audio_data)
            if remaining_end - remaining_start >= tone_samples // 2:
                segment = audio_data[remaining_start:remaining_end]
                if np.std(segment) > 0.01:
                    segments.append((remaining_start, remaining_end))
        
        return segments
    
    def analyze_frequency_spectrum(self, audio_segment: np.ndarray, sample_rate: int) -> Tuple[List[Tuple[float, float]], np.ndarray, np.ndarray]:
        """周波数スペクトラムを詳細解析"""
        # ゼロパディングでFFT解像度を上げる
        padded_length = max(8192, len(audio_segment) * 4)
        
        # ウィンドウ関数適用
        windowed = audio_segment * np.hanning(len(audio_segment))
        
        # FFT実行
        fft = np.fft.fft(windowed, n=padded_length)
        freqs = np.fft.fftfreq(padded_length, 1/sample_rate)
        
        # 正の周波数のみ
        positive_freqs = freqs[:len(freqs)//2]
        magnitude = np.abs(fft[:len(fft)//2])
        
        # 正規化
        if np.max(magnitude) > 0:
            magnitude = magnitude / np.max(magnitude)
        
        # ピーク検出
        peaks = []
        
        # 各DTMF周波数でのパワーを測定
        for target_freq in self.low_frequencies + self.high_frequencies:
            # 周波数範囲内のインデックスを取得
            freq_mask = np.abs(positive_freqs - target_freq) <= self.frequency_tolerance
            
            if np.any(freq_mask):
                power = np.max(magnitude[freq_mask])
                actual_freq_idx = np.argmax(magnitude[freq_mask])
                actual_freq = positive_freqs[freq_mask][actual_freq_idx]
                
                if power > self.min_tone_power:
                    peaks.append((actual_freq, power))
        
        return peaks, positive_freqs, magnitude
    
    def decode_segment_multiple_methods(self, segment: np.ndarray, sample_rate: int, segment_idx: int) -> Optional[str]:
        """複数の手法でセグメントを解析"""
        
        print(f"\n--- セグメント {segment_idx + 1} 解析 ---")
        print(f"長さ: {len(segment)} サンプル ({len(segment)/sample_rate:.3f}s)")
        print(f"RMS: {np.sqrt(np.mean(segment**2)):.4f}")
        
        # 前処理
        processed_segment = self.preprocess_audio(segment, sample_rate)
        
        # 方法1: 全体を使った解析
        result1 = self.decode_single_segment(processed_segment, sample_rate)
        
        # 方法2: 中央部分のみ使用
        center_start = len(processed_segment) // 4
        center_end = 3 * len(processed_segment) // 4
        center_segment = processed_segment[center_start:center_end]
        result2 = self.decode_single_segment(center_segment, sample_rate)
        
        # 方法3: 複数の小ウィンドウで解析
        result3 = self.decode_with_multiple_windows(processed_segment, sample_rate)
        
        print(f"全体解析: {result1}")
        print(f"中央部解析: {result2}")  
        print(f"マルチウィンドウ: {result3}")
        
        # 結果を統合
        candidates = [r for r in [result1, result2, result3] if r is not None]
        if candidates:
            # 最も多く検出された結果を採用
            from collections import Counter
            counter = Counter(candidates)
            most_common = counter.most_common(1)[0][0]
            print(f"採用結果: {most_common}")
            return most_common
        
        print("検出失敗")
        return None
    
    def decode_single_segment(self, segment: np.ndarray, sample_rate: int) -> Optional[str]:
        """単一セグメントからDTMF解析"""
        peaks, freqs, magnitude = self.analyze_frequency_spectrum(segment, sample_rate)
        
        if len(peaks) < 2:
            return None
        
        # パワー順にソート
        peaks.sort(key=lambda x: x[1], reverse=True)
        
        # 低周波数と高周波数を分離
        low_peaks = [(f, p) for f, p in peaks if f < 1000]
        high_peaks = [(f, p) for f, p in peaks if f >= 1000]
        
        if not low_peaks or not high_peaks:
            return None
        
        # 最強のピークを選択
        best_low = low_peaks[0][0]
        best_high = high_peaks[0][0]
        
        # 最も近いDTMF周波数を検索
        closest_low = min(self.low_frequencies, key=lambda x: abs(x - best_low))
        closest_high = min(self.high_frequencies, key=lambda x: abs(x - best_high))
        
        # 許容誤差内かチェック
        if (abs(closest_low - best_low) <= self.frequency_tolerance and 
            abs(closest_high - best_high) <= self.frequency_tolerance):
            
            freq_pair = (closest_low, closest_high)
            return self.freq_to_digit.get(freq_pair)
        
        return None
    
    def decode_with_multiple_windows(self, segment: np.ndarray, sample_rate: int) -> Optional[str]:
        """複数の小ウィンドウで解析"""
        window_size = min(int(sample_rate * 0.04), len(segment) // 3)  # 40msまたはセグメントの1/3
        results = []
        
        for i in range(0, len(segment) - window_size, window_size // 2):
            window = segment[i:i + window_size]
            result = self.decode_single_segment(window, sample_rate)
            if result:
                results.append(result)
        
        if results:
            from collections import Counter
            counter = Counter(results)
            most_common = counter.most_common(1)[0][0]
            return most_common
        
        return None
    
    def decode_dtmf_file(self, filename: str, debug: bool = True) -> str:
        """DTMFファイルから数字列を復元(堅牢版)"""
        print(f"\n{'='*50}")
        print(f"ファイル解析: {filename}")
        print(f"{'='*50}")
        
        try:
            # 音声データ読み込み
            audio_data, sample_rate = self.load_audio(filename)
            
            # セグメント検出
            segments = self.detect_tone_segments_multiple_methods(audio_data, sample_rate)
            
            if not segments:
                print("音のセグメントが検出されませんでした")
                return ""
            
            # 各セグメントを解析
            detected_digits = []
            
            for i, (start, end) in enumerate(segments):
                segment = audio_data[start:end]
                digit = self.decode_segment_multiple_methods(segment, sample_rate, i)
                
                if digit:
                    detected_digits.append(digit)
                else:
                    print(f"セグメント {i+1}: 検出失敗")
            
            result = ''.join(detected_digits)
            
            print(f"\n{'='*30}")
            print(f"最終結果: {result}")
            print(f"検出数字数: {len(result)}")
            print(f"総セグメント数: {len(segments)}")
            print(f"成功率: {len(result)}/{len(segments)} ({len(result)/len(segments)*100:.1f}%)")
            print(f"{'='*30}")
            
            return result
            
        except Exception as e:
            print(f"エラー: {e}")
            import traceback
            traceback.print_exc()
            return ""
    
    def plot_detailed_analysis(self, filename: str):
        """詳細な解析結果を可視化"""
        try:
            audio_data, sample_rate = self.load_audio(filename)
            segments = self.detect_tone_segments_multiple_methods(audio_data, sample_rate)
            
            fig, axes = plt.subplots(3, 1, figsize=(15, 12))
            
            # 時間軸
            time = np.arange(len(audio_data)) / sample_rate
            
            # 1. 元の波形
            axes[0].plot(time, audio_data)
            axes[0].set_title(f'{filename} - 音声波形')
            axes[0].set_ylabel('振幅')
            axes[0].grid(True)
            
            # セグメントをハイライト
            for i, (start, end) in enumerate(segments):
                axes[0].axvspan(start/sample_rate, end/sample_rate, 
                               alpha=0.3, label=f'セグメント{i+1}')
            
            # 2. フィルタ後の波形
            filtered_audio = self.preprocess_audio(audio_data, sample_rate)
            axes[1].plot(time, filtered_audio)
            axes[1].set_title('フィルタリング後の波形')
            axes[1].set_ylabel('振幅')
            axes[1].grid(True)
            
            # 3. スペクトログラム
            f, t, Sxx = signal.spectrogram(filtered_audio, sample_rate, 
                                         nperseg=512, noverlap=256)
            im = axes[2].pcolormesh(t, f, 10 * np.log10(Sxx + 1e-10), shading='gouraud')
            axes[2].set_title('スペクトログラム')
            axes[2].set_ylabel('周波数 [Hz]')
            axes[2].set_xlabel('時間 [秒]')
            axes[2].set_ylim([0, 2000])
            
            # DTMF周波数ライン
            for freq in self.low_frequencies + self.high_frequencies:
                axes[2].axhline(y=freq, color='white', linestyle='--', 
                               alpha=0.8, linewidth=1)
            
            plt.colorbar(im, ax=axes[2], label='パワー [dB]')
            plt.tight_layout()
            plt.show()
            
        except Exception as e:
            print(f"可視化エラー: {e}")

# RSA関連の関数
def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    gcd, x1, y1 = extended_gcd(b % a, a)
    x = y1 - (b // a) * x1
    y = x1
    return gcd, x, y

def mod_inverse(e, phi):
    gcd, x, _ = extended_gcd(e, phi)
    if gcd != 1:
        raise ValueError("モジュラー逆元が存在しません")
    return (x % phi + phi) % phi

def rsa_decrypt(c, p, q, e=65537):
    n = p * q
    phi = (p - 1) * (q - 1)
    d = mod_inverse(e, phi)
    m = pow(c, d, n)
    return m

def main():
    """メイン実行関数"""
    decoder = RobustDTMFDecoder()
    
    files = ["p_dial.wav", "q_dial.wav", "message.wav"]
    results = {}
    
    # 各ファイルを解析
    for filename in files:
        try:
            result = decoder.decode_dtmf_file(filename, debug=True)
            results[filename] = result
            
            if not result:
                print(f"⚠️  {filename} から数字を復元できませんでした")
            else:
                print(f"✅ {filename}: {result}")
                
        except FileNotFoundError:
            print(f"❌ ファイルが見つかりません: {filename}")
            results[filename] = ""
        except Exception as e:
            print(f"❌ {filename} の処理中にエラー: {e}")
            results[filename] = ""
    
    # RSA復号化
    p_str = results.get("p_dial.wav", "")
    q_str = results.get("q_dial.wav", "")
    c_str = results.get("message.wav", "")
    
    if p_str and q_str and c_str:
        try:
            print(f"\n{'='*50}")
            print("RSA復号化")
            print(f"{'='*50}")
            
            p = int(p_str)
            q = int(q_str)
            c = int(c_str)
            
            print(f"p = {p}")
            print(f"q = {q}") 
            print(f"c = {c}")
            
            m = rsa_decrypt(c, p, q)
            print(f"復号化された数値: {m}")
            
            try:
                flag_bytes = long_to_bytes(m)
                flag = flag_bytes.decode('utf-8', errors='ignore')
                print(f"🎉 フラグ: {flag}")
            except Exception as e:
                print(f"フラグ変換エラー: {e}")
                print(f"バイト列: {long_to_bytes(m)}")
                
        except ValueError as e:
            print(f"数値変換エラー: {e}")
        except Exception as e:
            print(f"RSA復号化エラー: {e}")
    else:
        print(f"\n❌ RSA復号化をスキップ(数字復元不完全)")
        print(f"p復元: {'' if p_str else ''}")
        print(f"q復元: {'' if q_str else ''}")  
        print(f"c復元: {'' if c_str else ''}")
    
    # 可視化オプション
    try:
        print(f"\n詳細解析を表示しますか? (y/n): ", end="")
        user_input = input().lower()
        if user_input == 'y':
            for filename in files:
                if filename in results:
                    print(f"\n{filename} の詳細解析:")
                    decoder.plot_detailed_analysis(filename)
    except KeyboardInterrupt:
        print("\n終了します")
    except Exception as e:
        print(f"可視化エラー: {e}")

if __name__ == "__main__":
    main()

実行すると以下のような結果が得られる。

==================================================
RSA復号化
==================================================
p = 10983959977181906221944070277050249695159719065797979860198327906342517841465796251340510314543825459306549312379388467919085481804275635828255424368896277
q = 8678036199170488884780093394192150083464607667624372136417058065258513864387020799930756914928144817991697023271050010755986626888722989975295810617844267
c = 70508688516557799681651812792642651067871903715364993117923211379841452025135849618182868501557625870789695156447009505178161039560403696520152794746543040447501478286428695065976528304147698191471798860041657166559094242292315109255014028450790613496046735617040355184531712588116133880923988426223187516216
復号化された数値: 1033569629655215080636516759323215294487616677784512678076723604554220555230354161378182322072389682103401387364203904893
🎉 フラグ: fwectf{Y0ur_7e13ph0n3_Num83r_15_6700_WoW!__H3110?}

Forensic/OSINT:QR(110 Solve)

青空白猫でASCIIコードを抽出するとFlagが3文字ずつ分割されて入っているのがわかる。

{'olo', '{QR', 'd_c', 'fwe', 'ur_', '987', '6e}', 'ctf', '_an'}

気合で並び替えるとFlagになる。並び替えのヒントどこかにあったのかな……?
fwectf{QR_and_colour_9876e}

Forensic/OSINT:Sharkshop(13 Solve)

攻撃者とサーバー間のパケットキャプチャからサーバーのadminパスワードを推測する必要がある。

TLSで通信されているので直接中身を見るのは無理だが、サーバーからのレスポンスの差がTCPパケットサイズにそのまま出ることを利用すれば良い。
pcapの解析プログラムの書き方はよくわからないのでAIにぶん投げ。

サーバーからのペイロード長解析プログラム
"""
PCAPファイルからTCPストリームごとに特定のIPアドレスから送信された
TCPペイロードのサイズを計算するスクリプト
"""

import argparse
from scapy.all import *
from collections import defaultdict

def analyze_tcp_payload(pcap_file, target_ip="34.84.101.79"):
    """
    PCAPファイルを解析して、指定されたIPアドレスから送信された
    TCPペイロードサイズをストリームごとに集計する
    
    Args:
        pcap_file (str): PCAPファイルのパス
        target_ip (str): 対象となる送信元IPアドレス
    
    Returns:
        dict: ストリームIDをキー、ペイロードサイズ合計を値とする辞書
    """
    
    # ストリーム識別用の辞書 (src_ip, src_port, dst_ip, dst_port) -> stream_id
    stream_map = {}
    stream_counter = 0
    
    # ストリームごとのペイロードサイズを格納
    stream_payload_sizes = defaultdict(int)
    
    print(f"PCAPファイルを読み込み中: {pcap_file}")
    print(f"対象IP: {target_ip}")
    print("-" * 50)
    
    try:
        # PCAPファイルを読み込み
        packets = rdpcap(pcap_file)
        print(f"総パケット数: {len(packets)}")
        
        processed_packets = 0
        target_packets = 0
        
        for packet in packets:
            processed_packets += 1
            
            # TCP パケットかつ、送信元IPが対象IPの場合のみ処理
            if packet.haslayer(TCP) and packet.haslayer(IP):
                ip_layer = packet[IP]
                tcp_layer = packet[TCP]
                
                # 送信元IPが対象IPと一致するかチェック
                if ip_layer.src == target_ip:
                    target_packets += 1
                    
                    # ストリームを識別するためのタプル
                    stream_tuple = (ip_layer.src, tcp_layer.sport, 
                                  ip_layer.dst, tcp_layer.dport)
                    
                    # 新しいストリームの場合、IDを割り当て
                    if stream_tuple not in stream_map:
                        stream_map[stream_tuple] = stream_counter
                        stream_counter += 1
                    
                    stream_id = stream_map[stream_tuple]
                    
                    # TCPペイロードサイズを計算
                    # IP長 - IP ヘッダ長 - TCPヘッダ長 = ペイロード長
                    ip_header_len = ip_layer.ihl * 4
                    tcp_header_len = tcp_layer.dataofs * 4
                    payload_size = ip_layer.len - ip_header_len - tcp_header_len
                    
                    # ペイロードが存在する場合のみ加算
                    if payload_size > 0:
                        stream_payload_sizes[stream_id] += payload_size
                        
                        # デバッグ情報(最初の10個のペイロードパケットのみ表示)
                        if target_packets <= 10:
                            print(f"Stream {stream_id}: {ip_layer.src}:{tcp_layer.sport} -> {ip_layer.dst}:{tcp_layer.dport}, Payload: {payload_size} bytes")
        
        print("-" * 50)
        print(f"処理済みパケット数: {processed_packets}")
        print(f"対象IP({target_ip})からのTCPパケット数: {target_packets}")
        print(f"検出されたストリーム数: {len(stream_payload_sizes)}")
        
        return stream_payload_sizes, stream_map
    
    except Exception as e:
        print(f"エラーが発生しました: {e}")
        return {}, {}

def print_results(stream_payload_sizes, stream_map, target_ip):
    """
    結果を整理して表示する
    
    Args:
        stream_payload_sizes (dict): ストリームIDごとのペイロードサイズ
        stream_map (dict): ストリームタプルとIDのマッピング
        target_ip (str): 対象IPアドレス
    """
    
    if not stream_payload_sizes:
        print("対象となるTCPストリームが見つかりませんでした。")
        return
    
    print("\n" + "=" * 80)
    print(f"結果: {target_ip} から送信されたTCPペイロードサイズ")
    print("=" * 80)
    
    # ストリームIDでソート
    total_payload = 0
    
    # stream_mapを逆引きするための辞書を作成
    id_to_stream = {v: k for k, v in stream_map.items()}

    # 各ストリームのペイロードのサイズをリストへ
    stream_payload_list = []

    for stream_id in sorted(stream_payload_sizes.keys()):
        payload_size = stream_payload_sizes[stream_id]
        stream_info = id_to_stream[stream_id]
        src_ip, src_port, dst_ip, dst_port = stream_info
        
        print(f"ストリーム {stream_id:2d}: {src_ip}:{src_port} -> {dst_ip}:{dst_port}")
        print(f"                ペイロードサイズ: {payload_size:,} bytes ({payload_size/1024:.2f} KB)")
        print()
        
        total_payload += payload_size
        stream_payload_list.append(payload_size)
    
    print("-" * 80)
    print(f"総ペイロードサイズ: {total_payload:,} bytes ({total_payload/1024:.2f} KB, {total_payload/1024/1024:.2f} MB)")
    print(f"ストリーム数: {len(stream_payload_sizes)}")
    print(f"ペイロードサイズのパターン: {sorted(set(stream_payload_list))}")
    print(stream_payload_list)

def main():
    parser = argparse.ArgumentParser(description="PCAPファイルからTCPペイロードサイズを分析")
    parser.add_argument("pcap_file", help="分析するPCAPファイルのパス")
    parser.add_argument("--ip", default="34.84.101.79", help="対象となる送信元IPアドレス (デフォルト: 34.84.101.79)")
    parser.add_argument("--verbose", "-v", action="store_true", help="詳細な出力を表示")
    
    args = parser.parse_args()
    
    # PCAPファイルの存在確認
    if not os.path.exists(args.pcap_file):
        print(f"エラー: ファイル '{args.pcap_file}' が見つかりません。")
        return
    
    # 分析実行
    stream_payload_sizes, stream_map = analyze_tcp_payload(args.pcap_file, args.ip)
    
    # 結果表示
    print_results(stream_payload_sizes, stream_map, args.ip)

if __name__ == "__main__":
    main()

攻撃者が攻撃に用いたプログラムはpcapファイルの先頭にあるので引っ張ってきて使い回せば良い。
payload_sizes = [5071, 5071, 5037, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5071, 5071, 5037, 5071, 5071, 5071, 5037, 5071, 5071, 5071, 5071, 5071, 5037, 5071, 5071, 5071, 5037, 5071, 5071, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5037, 5071, 5037, 5071, 5037, 5037, 5037, 5071, 5071, 5071, 5071, 5037, 5037, 5037, 5037, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5071, 5037, 5037, 5037, 5071, 5037, 5037, 5071, 5037, 5037, 5037, 5037, 5037, 5071, 5037, 5071, 5071, 5071, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5037, 5071, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5071, 5071, 5037, 5071, 5037, 5071, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5071, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5071, 5071, 5071, 5071, 5037, 5037, 5037, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5037, 5037, 5071, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5037, 5037, 5037, 5037, 5071, 5037, 5071, 5037, 5037, 5071, 5071, 5071, 5037, 5071, 5071, 5037, 5071, 5071, 5037, 5037, 5071, 5071, 5071, 5071, 5071, 5071, 5071, 5071, 5037, 5037, 5071, 5071, 5037, 5037, 5037, 5037, 5037, 5037, 5037]

max_length = 32
result = ""

payload_ind = 0

for pos in range(1, max_length + 1):
    low = 32
    high = 126

    while low <= high:
        mid = (low + high) // 2
        if payload_sizes[payload_ind] == 5071:
            low = mid + 1
            char_guess = chr(mid)
        else:
            high = mid - 1
        
        payload_ind += 1

    if low == 32:
        break
    result += char_guess
    print(f"Password so far: {result}", flush=True)

print("Leaked admin password:", result)
print(payload_ind)
> python detect_passwd.py 
Password so far: k
Password so far: kG
Password so far: kGx
......
Password so far: kGxqyvTPlR7BEadMdFku6hjGEPYpN
Password so far: kGxqyvTPlR7BEadMdFku6hjGEPYpNk
Leaked admin password: kGxqyvTPlR7BEadMdFku6hjGEPYpNk
206

ログインして/adminに行けばFlagを得られる。
fwectf{pcap_f1L3_d3_c7f_914y3r_w0_k0w464r453m45h0u}

Rev:strings jacking(447 Solve)

stringsコマンドでサクッと。

> strings strings_jacking | grep fwectf
fwectf{5tr1n65_30F_p4ss937_0011}
fwectf_strings_jacking.c

Rev:No need Logical Thinking(219 Solve)

Challenge.plはFlagのASCIIコードをIndex(1スタート)分加算しているプログラム。
逆の処理を書いてサクッと。

output = ""
with open("./output.txt", "r") as f:
    output = f.read()

flag = ""
index = 1
for c in output:
    flag += chr(ord(c) - index)
    index += 1

print(flag)
> python decode.py
fwectf{the_Pr010g_10gica1_Languag3!}

Rev:Mystery Zone(97 Solve)

Unity製ゲーム。普通に遊んでもエラーが出てビックリするだけ。
dnSpyでShellmoveクラスの中身をいじいじすれば良い。

public class Shellmove : MonoBehaviour
{
	// Token: 0x06000004 RID: 4 RVA: 0x00002074 File Offset: 0x00000274
	private void Update()
	{
		Vector3 position = base.gameObject.transform.position;
		if (Input.GetKey(KeyCode.Delete))
		{
			SceneManager.LoadScene("Main");
		}
		if (this.Len(position) >= 50.0)
		{
			this.TransError();
		}
        if (position == new Vector3(65535f, 65535f))
		{
			this.TransFlag();
		}
......

Flagを表示しそうな関数の呼び出し条件がエラーの呼び出し条件より遠くに行くことなので正攻法では無理ゲーだった。
this.TransError()this.TransFlag()に変更して再度コンパイル。

ゲームを起動して適当に動くとFlagを入手。
fwectf{K494ku_no_Ch1k4r4_t7e_5u63h!}

Rev:I_HATE_DEBUGGING(25 Solve)

exeを実行するとfakeflag.txtからflag.txtが生成されるが、中身を見ると全然Flagになっていない。

Ghidraで中身を見ると、filetxtmemoryという変数が重要そうなのが分かる。
newOpen()辺りを見ると、デコンパイルされていないコードがあり、filetxtをゴニョゴニョしながらmemoryに格納している。

filetxtの中身を見るため、バイナリエディタでJZをJNZに変更する等してアンチデバッグを回避する。
回避すれば、x64dbgで最終的なfiletxtの中身を覗くことができる。

デコンパイルされていないコードは自力で読むと時間がかかりそうだったので、AIに投げた。
微修正こそ必要だったが、割といい感じのところまで解釈してくれていた。

memory = [ 0x7a, 0xca, 0xb9, 0xb1, 0x83, 0xa9, 0xca, 0x12, 0xf0, 0xf9, 0xe6, 0x81, 0x11, 0xb1, 0x29, 0x81, 0x51, 0xf1, 0x29, 0xe9, 0x51, 0xb0, 0x51, 0x11, 0x29, 0xe9, 0xf0, 0x79, 0xe0, 0x23, 0x53, 0xdb, 0xb9, 0x28, 0x90, 0x53, 0x6b, 0x60, 0x98, 0x53, 0xdb, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]

filetxt = [
    0x66, 0x77, 0x65, 0x63, 0x74, 0x66, 0x7b, 0x03, 0x6d, 0x6c,
    0x2a, 0x6f, 0x03, 0x18, 0x44, 0x65, 0x86, 0x6a, 0x78, 0x6c,
    0x7f, 0x6a, 0x70, 0x64, 0x7f, 0x67, 0x70, 0x4c, 0x70, 0x78,
    0x7f, 0x67, 0x44, 0x75, 0x46, 0x3e, 0x30, 0x21, 0x6d, 0x5f,
    0x48, 0x30, 0x37, 0x56, 0x49, 0x30, 0x21, 0x6a, 0x7d, 0x00,
    0x00, 0x00,
]

for i in range(50):
    memory[i] = filetxt[i]

for i in range(35, 48):
    if i % 3 == 0:
        memory[i] = filetxt[i] & 0xFF
    else:
        memory[i] = ((filetxt[i] ^ 0x0D) + 0x04) & 0xFF

for i in range(7, 14):
    memory[i] = (filetxt[i] ^ 0x5C) & 0xFF

for i in range(14, 35):
    if (i % 2 == 0):
        memory[i] = (filetxt[i] - 0x11) & 0xFF
    else:
        memory[i] = (filetxt[i] - 0x03) & 0xFF

for i in range(50, 6, -1):
    memory[i + 1] = memory[i]

memory[7] = 0x49

flag = "".join(chr(b) for b in memory)
print(flag)
> python decode_flag.py
fwectf{I_10v3_D3bugging_and_I_und3r5700d_IA7_H00k}