[Forensic] SunshineCTF 2025 Intergalactic Copyright Infringement write-up

TL;DR

torrent 파일 복구

 

NASA received a notification from their ISP that it appeared that some copyrighted files were transferred to and from the ISS (Guess astronauts need movies too). We weren't able to recover the all of the files, but we were able to capture some traffic from the final download before the user signed off. If you can help recover the file that was downloaded perhaps you can shed some light on what they were doing?


Analysis

Evidence.pcap

 

192.168.1.23 <-> 34.10.241.248의 의심스러운 통신

 

BitTorrent 패킷 다수 식별됨

 

follow stream하여 one-direction 잡아서 raw 파일로 저장했다.

 

 

BitTorrent 복호화 코드를 짜준다..

 

import sys
import os
import struct
from collections import defaultdict

USAGE = f"""Usage:
  python {os.path.basename(__file__)} <stream.bin> [output.bin]

Where:
  <stream.bin>  - TCP stream data saved as RAW (Wireshark: Follow TCP Stream → Show data as Raw → Save As)
  [output.bin]  - (optional) output path, default: recovered_payload.bin
"""
BT_MSG_PIECE = 7

def parse_stream_and_reassemble(stream_path: str, out_path: str) -> None:
    with open(stream_path, 'rb') as f:
        data = f.read()
    proto = b"BitTorrent protocol"
    start = data.find(proto)
    if start != -1:
        # <pstrlen=1><pstr=19 bytes><reserved=8><info_hash=20><peer_id=20>
        if start >= 1 and data[start-1] == 19:
            hs_beg = start - 1
            hs_len = 1 + 19 + 8 + 20 + 20
            cursor = hs_beg + hs_len
        else:
            cursor = start + len(proto)
    else:
        cursor = 0

    total = len(data)
    blocks = []  # list of (index, begin, bytes_block)

    other_msgs = 0
    keep_alive = 0
    malformed = 0

    while cursor + 4 <= total:
        (msg_len,) = struct.unpack(">I", data[cursor:cursor+4])
        cursor += 4

        if msg_len == 0:
            # Keep-alive
            keep_alive += 1
            continue

        if cursor + msg_len > total:
            # Truncated frame at end of capture
            malformed += 1
            break

        msg_id = data[cursor]
        payload = data[cursor+1: cursor+msg_len]
        cursor += msg_len

        if msg_id == BT_MSG_PIECE:
            if len(payload) < 8:
                malformed += 1
                continue
            index = struct.unpack(">I", payload[0:4])[0]
            begin = struct.unpack(">I", payload[4:8])[0]
            block = payload[8:]
            blocks.append((index, begin, block))
        else:
            other_msgs += 1
            # ignore non-piece messages

    if not blocks:
        print("No PIECE messages found. Make sure you saved the DOWNSTREAM (peer → you) direction as RAW.")
        return

    per_index_max_end = defaultdict(int)
    for idx, begin, block in blocks:
        end = begin + len(block)
        if end > per_index_max_end[idx]:
            per_index_max_end[idx] = end

    from collections import Counter
    c = Counter(per_index_max_end.values())
    piece_size, _ = c.most_common(1)[0]

    max_index = max(idx for idx, _, _ in blocks)
    est_size = (max_index + 1) * piece_size

    with open(out_path, 'wb') as out:
        out.truncate(est_size)
        for idx, begin, block in blocks:
            offset = idx * piece_size + begin
            out.seek(offset)
            out.write(block)

    written = sum(len(b) for _, _, b in blocks)
    coverage_pct = 100.0 * written / est_size if est_size else 0.0

    print(f"[OK] PIECE blocks: {len(blocks)}")
    print(f"[OK] Estimated piece size: {piece_size} bytes")
    print(f"[OK] Max index: {max_index}")
    print(f"[OK] Estimated output size: {est_size:,} bytes")
    print(f"[OK] Bytes written from capture: {written:,} bytes (~{coverage_pct:.1f}% coverage)")
    print(f"[OK] Output file: {out_path}")
    print(f"(Other messages seen: {other_msgs}, keep-alives: {keep_alive}, malformed frames skipped: {malformed})")
    print("\nTIP: If coverage is low or file doesn't open, capture didn't contain all blocks. You can still try file carving tools on the output (binwalk/foremost/scalpel).")

def main():
    if len(sys.argv) < 2:
        print(USAGE)
        sys.exit(1)
    stream_path = sys.argv[1]
    out_path = sys.argv[2] if len(sys.argv) >= 3 else "recovered_payload.bin"
    parse_stream_and_reassemble(stream_path, out_path)

if __name__ == "__main__":
    main()

 

그러면 파일이 하나 복구되는데 pdf 식별자를 추가해주면 플래그가 출력된다.

 

sun{4rggg_sp4c3_p1r4cy}