TJCTF 2026 — loud-packets Çözüm Yazısı

Kendini Bluetooth dosya transferi gibi gösteren bir pcap aslında 459 UDP paketine bölünmüş bir WAV dosyası taşıyor. Ses bir protokol değil — bir Hellschreiber bitmap'i; spektrogram da çözücünün ta kendisi.

4 dk okuma
ibrahimsql
662 kelime

TJCTF 2026 — loud-packets

Çözücüyü aracın değil, gözlerinin olduğu bir adli bilişim sorusu.

  • Etkinlik: TJCTF 2026
  • Kategori: Adli Bilişim (Forensics)
  • Dosya: chall.pcap
  • Bayrak formatı: tjctf{...}

1. Yakalama Bluetooth değil

Açıklama, hassas bir dosyanın "bluetooth üzerinden" transfer edildiğini söylüyor. Bu bir yanıltmaca. Yakalamayı açtığınızda HCI yok, L2CAP yok, SDP yok — sadece IPv4 üzerinden 509 UDP çerçevesi var.

eth     frames:509
  ip    frames:509
    udp frames:509

Akışa göre ayırdığınızda iki çok farklı şey görünür:

  • 459 "ana" paket 192.168.1.100:50000 → 192.168.1.200:62000, her biri tam 608 bayt.
  • 50 "yem" paketi rastgele 10.0.0.x adreslerinden, rastgele görünen port ve yüklerle.

Yem paketleri gürültüden ibaret. Ana akış ise gerçek bir veri akışı.

2. Dosyayı yeniden birleştirme

Her ana paketin yükü aynı 8 baytlık başlıkla başlıyor:

42 54 41 56   BTAV            sihirli imza (magic)
00 00 00 ae   sıra numarası (big-endian u32)
... 600 bayt parça verisi

BTAV, yazarın "Bluetooth Audio" anlamına gelen bir göz kırpması. Sonraki dört bayt, 0..458 aralığında kesintisiz bir sıra numarası (459 değer, boşluk yok). Başlıktan sonraki baytlar tek bir dosyanın parçası: son paket 428 bayt olmak üzere her biri 600 bayt. Toplam 458 * 600 + 428 = 275228 bayt.

8 baytlık başlığı at, sıra numarasına göre sırala, birleştir:

Python
from scapy.all import rdpcap, UDP
 
def carve(pcap):
    chunks = {}
    for p in rdpcap(pcap):
        if UDP not in p:
            continue
        if (p[UDP].sport, p[UDP].dport) != (50000, 62000):
            continue
        data = bytes(p[UDP].payload)
        if data[:4] != b"BTAV":
            continue
        seq = int.from_bytes(data[4:8], "big")
        chunks[seq] = data[8:]
    return b"".join(chunks[i] for i in sorted(chunks))
 
blob = carve("chall.pcap")
open("ordered.wav", "wb").write(blob)

Sonuç geçerli bir WAV: RIFF başlığı, fmt bloğu, data bloğu — 44.1 kHz, mono, 16 bit PCM, 3.12 saniye. İlk parça, BTAV başlığından hemen sonra RIFF....WAVEfmt ile başlıyor.

3. Ses bir protokol gibi görünüyor

WAV'ı çaldığınızda hızlı, kesik kesik çok tonlu patlamalar duyarsınız. Spektrogramda ise neredeyse fazla düzenli: yedi yatay taşıyıcı, hepsi ~440 Hz'in temiz katlarına oturuyor:

 880   1320   1760   2200   2640   3080   3520 Hz

Yaklaşık 26 ayrık patlama var, araları yaklaşık 120 ms. İnsanı MFSK, DTMF, ggwave, SSTV gibi çözücülere yöneltiyor — ama hiçbiri bir şey çözemiyor. Nedeni basit: ortada sembol tablosu yok. Ses, metni bir protokolle kodlamıyor; metni çiziyor.

4. Hellschreiber — resim protokolü

Hellschreiber, bir karakteri küçük bir bitmap olarak, sütun sütun boyayarak iletir. Standart yazı tipi 7 piksel yüksekliğindedir; buradaki yedi taşıyıcı tam olarak o yedi satırdır. Her patlama bir karakter hücresidir:

  • Karakter hücresi = 120 ms genişliğinde, her biri 20 ms'lik 6 sütundan oluşur.
  • Bir sütunda yanan piksel = karşılık gelen taşıyıcı tonun aktif olması.
  • Yalnızca en düşük taşıyıcının (880 Hz) yandığı sütun = alt satırda tek nokta = alt çizgi karakteri.

3.12 saniye boyunca 26 karakter hücresi. Spektrogram çözücüdür — render et ve noktaları oku.

5. Resmi çizmek

İşin sırrı yatay çözünürlükte. Matplotlib'e sütun adımı birkaç örnek olan (neredeyse tam örtüşmeli) bir spektrogram verip figürü genişçe uzatın:

Python
import wave, numpy as np
import matplotlib.pyplot as plt
 
with wave.open("ordered.wav") as w:
    sr = w.getframerate()
    data = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32)
 
Pxx, freqs, t = plt.specgram(data, NFFT=2048, Fs=sr, noverlap=2048 - 4, mode="magnitude")[:3]
plt.close()
 
fm = (freqs >= 700) & (freqs <= 4000)
img = 20 * np.log10(Pxx[fm, :] + 1e-10)
img -= img.max()
 
fig, ax = plt.subplots(figsize=(48, 8))
ax.imshow(-img, aspect="auto", origin="lower", cmap="gray",
          extent=[t[0], t[-1], freqs[fm][0], freqs[fm][-1]], vmin=0, vmax=35)
plt.tight_layout()
plt.savefig("flag.png", dpi=180)

noverlap=2048-4, pencereyi 4 örnek kaydırır; bu da boyanmış glifleri ayırt etmeye yetecek sütun sayısını verir. Renk haritasını ters çevirmek (-img) noktaları açık zeminde koyu çizer; siyah üstünde parlayan kehribardan çok daha okunaklıdır. Yedi taşıyıcı satırı, blok yazı tipinin yedi satırına dönüşür ve bayrak orada, 7 piksel yüksekliğinde harflerle durur.

6. Bayrak

tjctf{v3ry_l0ud_pc4p_f1le}

26 karakter, 26 patlama hücresi ve alt çizgiler, yalnızca en düşük taşıyıcının yandığı üç patlamayla bire bir örtüşüyor.

Çıkarılacak ders: bir spektrogramda belirgin bantlanma ve sütun yapısı varsa, çözmeye çalışmadan önce okumayı deneyin. Hellschreiber, spektrogram sanatı ve MFSK aynı özelliği paylaşır — ortada çözücü yoktur, çünkü resmin kendisi zaten mesajdır.

---
Bu yazıyı paylaş:
TwitterLinkedInFacebook

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar