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}
'CTF' 카테고리의 다른 글
| [Crypto] Securinets CTF 2025 Fl1pper Zer0, 1 (0) | 2025.10.06 |
|---|---|
| [Crypto] snakeCTF 2025 Qual free-start write-up (1) | 2025.08.31 |
| [PWN] scriptCTF 2025 Vault 3 write-up (0) | 2025.08.21 |
| [Rev] scriptCTF 2025 - plastic-shield 1, 2 write-up (3) | 2025.08.21 |
| [Crypto] scriptCTF 2025 - Secure-Server-2 write-up (1) | 2025.08.21 |