┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Rogue Tower — picoCTF 2026
~ Imattas aka Zemi
 Category: Forensics
 Difficulty: Medium
 Points: 300
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 A suspicious cell tower has been detected in the network. Analyze the captured
network traffic to identify the rogue tower and extract the hidden data.

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This challenge simulates a rogue cell tower (IMSI catcher / Stingray) scenario.
A rogue base station intercepts mobile communications by masquerading as a
legitimate cell tower. The challenge provides a network traffic capture (PCAP)
file containing GSM/cellular protocol traffic that we must analyze to identify
the rogue tower and extract hidden/exfiltrated data.

:: Key Concepts

1. IMSI Catchers: Devices that impersonate legitimate cell towers to intercept
mobile communications. They exploit the fact that GSM does not require the
network to authenticate itself to the phone.
2. GSM Protocol Analysis: GSM traffic in PCAPs can be analyzed using Wireshark
with the GSM dissectors (GSM A, GSM MAP, GSMTAP, etc.).
3. Cell Tower Identification: Each cell tower has identifying information
including:
  - MCC (Mobile Country Code): Identifies the country
  - MNC (Mobile Network Code): Identifies the carrier
  - LAC (Location Area Code): Identifies the location area
  - Cell ID (CID): Identifies the specific cell tower
4. Rogue Tower Indicators: A rogue tower typically has anomalous identifiers --
mismatched MCC/MNC, unusual LAC/CID, stronger signal forcing handover, or
downgrade attacks (forcing 2G instead of 4G/5G).

:: Analysis Strategy

1. Open the PCAP in Wireshark and filter for GSM/GSMTAP traffic
2. Identify all cell towers by extracting MCC, MNC, LAC, and Cell ID values
3. Spot the anomaly: The rogue tower will have identifiers that don't match the
legitimate network, or will exhibit suspicious behavior such as:
  - Identity requests (IMSI catching)
  - Cipher mode commands disabling encryption (A5/0)
  - Unusual system information broadcasts
4. Extract hidden data: The flag is likely embedded in:
  - SMS messages sent through the rogue tower (in plaintext due to disabled
encryption)
  - Custom/malformed protocol fields
  - Data exfiltrated via the rogue tower's traffic
  - Base64 or hex-encoded strings in packet payloads

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

:: Wireshark Filters

Useful display filters for this type of analysis:
- gsmtap -- All GSMTAP encapsulated traffic
- gsm_a.dtap -- GSM A-interface DTAP messages
- gsm_a.dtap.msg_rr_type == 0x3f -- System Information messages
- gsm_sms -- SMS messages
- gsm_a.dtap.msg_mm_type == 0x05 -- Identity Request (IMSI catching)
- gsm_a.dtap.msg_cc_type -- Call control messages

:: Typical Data Hiding Locations

- SMS PDU content: The rogue tower may relay or capture SMS messages containing
the flag
- BCCH System Information: Custom data embedded in broadcast control channel
messages
- Padding fields: Data hidden in protocol padding bytes
- ARFCN (Absolute Radio Frequency Channel Number): Unusual frequency assignments
that encode data
- Hex-encoded data in user payloads: After the rogue tower downgrades
encryption, plaintext data becomes visible

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

1. Download the PCAP file provided by the challenge.
2. Open in Wireshark and apply GSM-related display filters.
3. Enumerate cell towers: Extract all unique (MCC, MNC, LAC, CID) tuples from
System Information messages.
4. Identify the rogue tower: Look for mismatched or anomalous identifiers
compared to the legitimate towers.
5. Filter traffic through the rogue tower: Isolate packets associated with the
rogue tower's identifiers.
6. Extract the flag: Look for SMS content, plaintext data, or encoded payloads
in the rogue tower's traffic. The flag may be:
  - Directly in an SMS message body
  - Encoded in hex/base64 within packet fields
  - Spread across multiple packets (requiring reassembly)
  - Hidden in custom GSMTAP header fields or user data
7. Decode if necessary: Apply base64/hex decoding, or reassemble fragmented
data.

:: Method: Manual Approach with tshark
-- bash --
# List all GSMTAP packets
tshark -r capture.pcap -Y "gsmtap"

# Extract Cell IDs from the capture
tshark -r capture.pcap -Y "gsmtap" -T fields -e gsmtap.cell_id | sort -u

# Extract ARFCN values
tshark -r capture.pcap -Y "gsmtap" -T fields -e gsmtap.arfcn | sort -u

# Look for SMS content
tshark -r capture.pcap -Y "gsm_sms" -T fields -e gsm_sms.sms_text

# Extract all readable strings from specific packets
tshark -r capture.pcap -Y "gsmtap" -T fields -e data.data | xxd -r -p

# Search for the flag pattern directly
tshark -r capture.pcap -x | grep -i "pico"
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Rogue Tower - picoCTF 2026 (Forensics, 300 pts)

Analyze a PCAP containing GSM/cellular network traffic to identify a rogue
cell tower and extract the hidden flag.

Approach:
  1. Parse GSMTAP packets to identify all cell towers (by Cell ID, ARFCN, etc.)
  2. Identify the rogue tower by anomalous identifiers
  3. Extract hidden data (SMS content, encoded payloads, etc.)
  4. Decode the flag

Usage:
    python3 solve.py <capture.pcap>

Dependencies:
    pip install scapy pyshark
"""

import sys
import os
import struct
import re
import binascii
import subprocess
from collections import defaultdict, Counter

# Try to import pyshark (preferred for GSM dissection)
try:
    import pyshark
    HAS_PYSHARK = True
except ImportError:
    HAS_PYSHARK = False

# Try to import scapy as fallback
try:
    from scapy.all import rdpcap, raw, UDP
    HAS_SCAPY = True
except ImportError:
    HAS_SCAPY = False


# GSMTAP header format (from libosmocore)
# https://osmocom.org/projects/baseband/wiki/GSMTAP
GSMTAP_PORT = 4729
GSMTAP_HDR_FMT = ">BBBBHHBBxxxxI"
GSMTAP_HDR_SIZE = 16

# GSMTAP types
GSMTAP_TYPE_UM = 0x01
GSMTAP_TYPE_ABIS = 0x02
GSMTAP_TYPE_UM_BURST = 0x03
GSMTAP_TYPE_SIM = 0x04
GSMTAP_TYPE_GB_LLC = 0x08


def parse_gsmtap_header(data):
    """Parse a GSMTAP header and return a dict of fields."""
    if len(data) < GSMTAP_HDR_SIZE:
        return None

    (version, hdr_len, pdu_type, timeslot,
     arfcn, signal_dbm, snr_db, subtype,
     frame_number) = struct.unpack(GSMTAP_HDR_FMT, data[:GSMTAP_HDR_SIZE])

    actual_hdr_len = hdr_len * 4  # header length is in 32-bit words

    return {
        'version': version,
        'hdr_len': actual_hdr_len,
        'type': pdu_type,
        'timeslot': timeslot,
        'arfcn': arfcn & 0x3FFF,  # mask out uplink flag
        'uplink': bool(arfcn & 0x4000),
        'signal_dbm': signal_dbm,
        'snr_db': snr_db,
        'subtype': subtype,
        'frame_number': frame_number,
    }


def extract_strings(data, min_length=4):
    """Extract printable ASCII strings from binary data."""
    result = []
    current = []
    for byte in data:
        if 32 <= byte < 127:
            current.append(chr(byte))
        else:
            if len(current) >= min_length:
                result.append(''.join(current))
            current = []
    if len(current) >= min_length:
        result.append(''.join(current))
    return result


def try_decode_sms_7bit(data):
    """Attempt to decode GSM 7-bit encoded SMS data."""
    # GSM 7-bit default alphabet (basic set)
    gsm_alphabet = (
        "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5"
        "\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\x1b\u00c6\u00e6"
        "\u00df\u00c9 !\"#\u00a4%&'()*+,-./0123456789:;<=>?"
        "\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7"
        "\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0"
    )

    try:
        result = []
        bits = 0
        buf = 0
        for byte in data:
            buf |= (byte << bits)
            bits += 8
            while bits >= 7:
                char_idx = buf & 0x7F
                if char_idx < len(gsm_alphabet):
                    result.append(gsm_alphabet[char_idx])
                buf >>= 7
                bits -= 7
        return ''.join(result)
    except Exception:
        return None


def solve_with_pyshark(pcap_path):
    """Use pyshark (tshark backend) for full GSM protocol dissection."""
    print("[*] Analyzing with pyshark (tshark backend)...")

    # ---- Phase 1: Identify all cell towers ----
    print("\n[*] Phase 1: Enumerating cell towers...")
    towers = defaultdict(lambda: {'count': 0, 'arfcns': set(), 'types': Counter()})
    sms_texts = []
    all_payloads = []
    flag_candidates = []

    try:
        cap = pyshark.FileCapture(pcap_path, display_filter='gsmtap')
    except Exception as e:
        print(f"[!] pyshark error: {e}")
        return None

    for pkt in cap:
        try:
            if hasattr(pkt, 'gsmtap'):
                gsmtap = pkt.gsmtap
                arfcn = int(getattr(gsmtap, 'arfcn', 0))
                cell_id = getattr(gsmtap, 'cell_id', 'unknown')
                pdu_type = getattr(gsmtap, 'type', 'unknown')

                key = f"CID:{cell_id}_ARFCN:{arfcn}"
                towers[key]['count'] += 1
                towers[key]['arfcns'].add(arfcn)
                towers[key]['types'][str(pdu_type)] += 1

            # Check for SMS content
            for layer in pkt.layers:
                layer_name = layer.layer_name.lower()
                if 'sms' in layer_name:
                    for field_name in layer.field_names:
                        val = getattr(layer, field_name, '')
                        if val and 'pico' in str(val).lower():
                            flag_candidates.append(str(val))
                        sms_texts.append(f"{field_name}: {val}")

            # Extract raw payload data
            if hasattr(pkt, 'data'):
                raw_data = getattr(pkt.data, 'data', '')
                if raw_data:
                    all_payloads.append(raw_data.replace(':', ''))

        except Exception:
            continue

    cap.close()

    # Print tower summary
    print(f"\n[*] Found {len(towers)} unique cell tower identifiers:")
    for tower_id, info in sorted(towers.items(), key=lambda x: x[1]['count'], reverse=True):
        print(f"    {tower_id}: {info['count']} packets")

    # ---- Phase 2: Identify the rogue tower ----
    print("\n[*] Phase 2: Identifying the rogue tower...")

    if len(towers) > 1:
        # The rogue tower typically has fewer packets (it's intermittent)
        # or has an unusual ARFCN/Cell ID
        avg_count = sum(t['count'] for t in towers.values()) / len(towers)
        for tower_id, info in towers.items():
            if info['count'] < avg_count * 0.3 or info['count'] > avg_count * 3:
                print(f"    [!] Anomalous tower: {tower_id} ({info['count']} packets)")

    # ---- Phase 3: Extract flag ----
    print("\n[*] Phase 3: Searching for flag...")

    # Check SMS content
    if sms_texts:
        print(f"    Found {len(sms_texts)} SMS fields")
        for text in sms_texts:
            if 'pico' in text.lower():
                print(f"    [+] Potential flag in SMS: {text}")

    # Check payloads for flag pattern
    for payload_hex in all_payloads:
        try:
            payload_bytes = bytes.fromhex(payload_hex)
            decoded = payload_bytes.decode('utf-8', errors='ignore')
            if 'picoCTF' in decoded:
                flag_match = re.search(r'picoCTF\{[^}]+\}', decoded)
                if flag_match:
                    return flag_match.group(0)
        except Exception:
            continue

    # Check flag candidates
    for candidate in flag_candidates:
        flag_match = re.search(r'picoCTF\{[^}]+\}', candidate)
        if flag_match:
            return flag_match.group(0)

    return None


def solve_with_scapy(pcap_path):
    """Use scapy to parse the PCAP and extract GSMTAP data."""
    print("[*] Analyzing with scapy...")

    packets = rdpcap(pcap_path)
    print(f"[*] Loaded {len(packets)} packets")

    towers = defaultdict(lambda: {'count': 0, 'payloads': []})
    all_payloads = []

    for pkt in packets:
        # Look for GSMTAP (typically UDP port 4729)
        if pkt.haslayer(UDP):
            udp = pkt[UDP]
            if udp.dport == GSMTAP_PORT or udp.sport == GSMTAP_PORT:
                payload = bytes(udp.payload)
                gsmtap = parse_gsmtap_header(payload)
                if gsmtap:
                    key = f"ARFCN:{gsmtap['arfcn']}_TS:{gsmtap['timeslot']}"
                    towers[key]['count'] += 1
                    # Extract the payload after GSMTAP header
                    gsm_payload = payload[gsmtap['hdr_len']:]
                    towers[key]['payloads'].append(gsm_payload)
                    all_payloads.append(gsm_payload)

        # Also check raw packet data for flag strings
        raw_bytes = raw(pkt)
        if b'picoCTF' in raw_bytes:
            match = re.search(rb'picoCTF\{[^}]+\}', raw_bytes)
            if match:
                return match.group(0).decode()

    # Print tower summary
    print(f"\n[*] Found {len(towers)} unique GSMTAP sources:")
    for tower_id, info in sorted(towers.items(), key=lambda x: x[1]['count'], reverse=True):
        print(f"    {tower_id}: {info['count']} packets")

    # Search all GSMTAP payloads for the flag
    print("\n[*] Searching GSMTAP payloads for flag...")
    for tower_id, info in towers.items():
        combined_payload = b''.join(info['payloads'])

        # Direct string search
        if b'picoCTF' in combined_payload:
            match = re.search(rb'picoCTF\{[^}]+\}', combined_payload)
            if match:
                print(f"    [+] Flag found in {tower_id}!")
                return match.group(0).decode()

        # Extract and check ASCII strings
        strings = extract_strings(combined_payload)
        for s in strings:
            if 'picoCTF' in s:
                match = re.search(r'picoCTF\{[^}]+\}', s)
                if match:
                    return match.group(0)

        # Try GSM 7-bit decoding
        decoded = try_decode_sms_7bit(combined_payload)
        if decoded and 'picoCTF' in decoded:
            match = re.search(r'picoCTF\{[^}]+\}', decoded)
            if match:
                return match.group(0)

    # Try concatenating all payloads (flag might span packets)
    print("[*] Trying payload reassembly...")
    all_data = b''.join(all_payloads)

    # Check hex-encoded data
    hex_str = all_data.hex()
    try:
        decoded_hex = bytes.fromhex(hex_str).decode('utf-8', errors='ignore')
        if 'picoCTF' in decoded_hex:
            match = re.search(r'picoCTF\{[^}]+\}', decoded_hex)
            if match:
                return match.group(0)
    except Exception:
        pass

    # Check base64 patterns in the data
    b64_pattern = re.compile(rb'[A-Za-z0-9+/]{20,}={0,2}')
    for match in b64_pattern.finditer(all_data):
        try:
            import base64
            decoded = base64.b64decode(match.group(0)).decode('utf-8', errors='ignore')
            if 'picoCTF' in decoded:
                flag_match = re.search(r'picoCTF\{[^}]+\}', decoded)
                if flag_match:
                    return flag_match.group(0)
        except Exception:
            continue

    return None


def solve_with_tshark(pcap_path):
    """Use tshark directly via subprocess as a fallback."""
    print("[*] Analyzing with tshark (subprocess)...")

    # First, search for the flag pattern directly in packet hex dumps
    try:
        result = subprocess.run(
            ['tshark', '-r', pcap_path, '-x'],
            capture_output=True, text=True, timeout=60
        )
        if 'pico' in result.stdout.lower():
            # Search through hex dump for the flag
            lines = result.stdout.split('\n')
            for line in lines:
                if 'pico' in line.lower():
                    print(f"    [!] Potential flag reference: {line.strip()}")
    except Exception as e:
        print(f"    [!] tshark hex dump failed: {e}")

    # Extract GSMTAP Cell IDs
    try:
        result = subprocess.run(
            ['tshark', '-r', pcap_path, '-Y', 'gsmtap',
             '-T', 'fields', '-e', 'gsmtap.arfcn', '-e', 'gsmtap.cell_id',
             '-e', 'gsmtap.type'],
            capture_output=True, text=True, timeout=60
        )
        if result.stdout.strip():
            print("\n[*] GSMTAP fields found:")
            lines = result.stdout.strip().split('\n')
            cell_ids = set()
            for line in lines[:20]:  # Show first 20
                print(f"    {line}")
                parts = line.split('\t')
                if len(parts) >= 2:
                    cell_ids.add(parts[1])
            print(f"\n[*] Unique Cell IDs: {cell_ids}")
    except Exception as e:
        print(f"    [!] tshark field extraction failed: {e}")

    # Try to extract SMS text
    try:
        result = subprocess.run(
            ['tshark', '-r', pcap_path, '-Y', 'gsm_sms',
             '-T', 'fields', '-e', 'gsm_sms.sms_text'],
            capture_output=True, text=True, timeout=60
        )
        if result.stdout.strip():
            print(f"\n[*] SMS texts found:")
            for line in result.stdout.strip().split('\n'):
                print(f"    {line}")
                if 'picoCTF' in line:
                    match = re.search(r'picoCTF\{[^}]+\}', line)
                    if match:
                        return match.group(0)
    except Exception as e:
        print(f"    [!] SMS extraction failed: {e}")

    # Extract all UDP payloads on GSMTAP port
    try:
        result = subprocess.run(
            ['tshark', '-r', pcap_path, '-Y', f'udp.port == {GSMTAP_PORT}',
             '-T', 'fields', '-e', 'data.data'],
            capture_output=True, text=True, timeout=60
        )
        if result.stdout.strip():
            for line in result.stdout.strip().split('\n'):
                hex_data = line.replace(':', '').strip()
                if hex_data:
                    try:
                        raw_bytes = bytes.fromhex(hex_data)
                        decoded = raw_bytes.decode('utf-8', errors='ignore')
                        if 'picoCTF' in decoded:
                            match = re.search(r'picoCTF\{[^}]+\}', decoded)
                            if match:
                                return match.group(0)
                    except Exception:
                        continue
    except Exception as e:
        print(f"    [!] Payload extraction failed: {e}")

    # Brute-force: search ALL packet bytes for the flag
    try:
        result = subprocess.run(
            ['tshark', '-r', pcap_path, '-T', 'fields', '-e', 'frame.protocols',
             '-e', 'data.data'],
            capture_output=True, text=True, timeout=60
        )
        for line in result.stdout.strip().split('\n'):
            parts = line.split('\t')
            if len(parts) >= 2:
                hex_data = parts[1].replace(':', '').strip()
                if hex_data:
                    try:
                        raw_bytes = bytes.fromhex(hex_data)
                        decoded = raw_bytes.decode('utf-8', errors='ignore')
                        if 'picoCTF' in decoded:
                            match = re.search(r'picoCTF\{[^}]+\}', decoded)
                            if match:
                                return match.group(0)
                    except Exception:
                        continue
    except Exception:
        pass

    return None


def main():
    if len(sys.argv) < 2:
        print("Rogue Tower - picoCTF 2026 (Forensics, 300 pts)")
        print()
        print("Usage: python3 solve.py <capture.pcap>")
        print()
        print("This script analyzes GSM/GSMTAP network traffic to identify a")
        print("rogue cell tower and extract the hidden flag.")
        print()
        print("Dependencies (install at least one):")
        print("  pip install pyshark    # Preferred - full protocol dissection")
        print("  pip install scapy      # Fallback - manual GSMTAP parsing")
        print("  (or have tshark in PATH)")
        sys.exit(1)

    pcap_path = sys.argv[1]
    if not os.path.isfile(pcap_path):
        print(f"[!] File not found: {pcap_path}")
        sys.exit(1)

    print(f"[*] Analyzing: {pcap_path}")
    print(f"[*] File size: {os.path.getsize(pcap_path)} bytes")
    print()

    flag = None

    # Try pyshark first (best GSM dissection via tshark)
    if HAS_PYSHARK:
        flag = solve_with_pyshark(pcap_path)

    # Try scapy as fallback
    if flag is None and HAS_SCAPY:
        flag = solve_with_scapy(pcap_path)

    # Try tshark directly as last resort
    if flag is None:
        flag = solve_with_tshark(pcap_path)

    # Final output
    print()
    if flag:
        print(f"{'='*50}")
        print(f"FLAG: {flag}")
        print(f"{'='*50}")
    else:
        print("[!] Flag not found automatically.")
        print()
        print("[*] Manual investigation steps:")
        print("    1. Open the PCAP in Wireshark")
        print("    2. Filter: gsmtap")
        print("    3. Look at Statistics > Endpoints for cell tower IDs")
        print("    4. Identify anomalous Cell ID / ARFCN values")
        print("    5. Filter traffic for the rogue tower")
        print("    6. Check SMS content (gsm_sms filter)")
        print("    7. Check packet payloads for encoded flag data")
        print("    8. Try: tshark -r capture.pcap -x | grep -i pico")


if __name__ == '__main__':
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- A rogue cell tower / IMSI catcher exploits GSM's lack of network-to-phone
authentication; the forensic giveaway is anomalous tower identifiers
(MCC/MNC/LAC/CID) or a downgrade to A5/0 (no encryption).
- GSMTAP (UDP port 4729) is the encapsulation used to carry GSM radio frames
into a PCAP; Wireshark/tshark dissect it natively.
- Once encryption is disabled, exfiltrated data (often in SMS PDUs) becomes
plaintext — search SMS bodies, then fall back to hex/base64-decoding and
cross-packet reassembly.
- Tooling: Wireshark display filters (gsmtap, gsm_sms, gsm_a.dtap), tshark for
scripted field extraction, and pyshark/scapy for programmatic parsing.