←
$ Forensics: Jetpack Drift
Flag: BITSCTF{LF1_st4nds_4_1ost_fr0m_1ns1d3}Summary of Method
I reconstructed the HTTP transfer from packet captures, decoded the chunked stream, rebuilt encrypted chunk order using hash chaining, decrypted with the recovered credential logic, carved the embedded PNG from decrypted output, and read the flag from that image.
Main Investigation Points
- Parsed
chall.pcap(Linux cooked v2 / SLL2) and manually reassembled TCP streams. - Identified the key flow:
GET /send-chunks.php- Large binary HTTP response with
Transfer-Encoding: chunked
- Extracted supporting files from adjacent HTTP requests:
/encryption.py(AES-CTR encryption/chaining logic)/database.sql(credential dataset)
- Decoded chunked payload and split records in this format:
NXTCHNKHASH:<64-hex>DATA:<ciphertext>
- Reconstructed correct record order by hash-chain traversal:
- start chunk = ciphertext hash not referenced by any
NXTCHNKHASH - follow
NXTCHNKHASHpointers until terminal zero-hash
- start chunk = ciphertext hash not referenced by any
- Correlated web clue + DB row and selected password:
tyler13bradley→1VL7p6Rcli8mxgkh
- Decrypted each ordered chunk using:
key0 = SHA256(password)pt_i = AES-CTR(key_i, iv=key_i[:16]).decrypt(ct_i)key_{i+1} = SHA256(pt_i)
- Carved embedded PNG signature from plaintext blob and extracted the image.
- Read flag text from the recovered image.
Main Program
#!/usr/bin/env python3
import struct
import hashlib
from pathlib import Path
from collections import defaultdict
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
PCAP = Path("chall.pcap")
OUT = Path("recovered_report")
OUT.mkdir(exist_ok=True)
def parse_pcap_sll2_ipv4_tcp(path):
with path.open("rb") as f:
gh = f.read(24)
le = gh[:4] == b"\\xd4\\xc3\\xb2\\xa1"
ph = "<IIII" if le else ">IIII"
packets = []
while True:
h = f.read(16)
if not h:
break
ts, us, inc, _ = struct.unpack(ph, h)
d = f.read(inc)
packets.append((ts + us / 1e6, d))
return packets
def collect_tcp_payloads(pkts):
flows = defaultdict(list)
for ts, d in pkts:
if len(d) < 20:
continue
ip = d[20:] # SLL2 header is 20 bytes
if len(ip) < 20 or (ip[0] >> 4) != 4:
continue
total_len = struct.unpack("!H", ip[2:4])[0]
if len(ip) < total_len:
continue
ip = ip[:total_len]
if ip[9] != 6:
continue
ihl = (ip[0] & 0x0F) * 4
tcp = ip[ihl:]
if len(tcp) < 20:
continue
sp, dp, seq, ack, off_flags, *_ = struct.unpack("!HHIIHHHH", tcp[:20])
doff = ((off_flags >> 12) & 0x0F) * 4
if len(tcp) < doff:
continue
payload = tcp[doff:]
src = ".".join(map(str, ip[12:16]))
dst = ".".join(map(str, ip[16:20]))
flows[(src, sp, dst, dp)].append((seq, ts, payload))
return flows
def reassemble(segs):
seen = set()
items = []
for seq, ts, p in sorted(segs, key=lambda x: (x[0], x[1])):
if not p:
continue
k = (seq, p)
if k in seen:
continue
seen.add(k)
items.append((seq, p))
if not items:
return b""
base = min(s for s, _ in items)
out = bytearray()
for seq, p in items:
off = seq - base
end = off + len(p)
if end > len(out):
out.extend(b"\\x00" * (end - len(out)))
for i, b in enumerate(p):
if out[off + i] == 0:
out[off + i] = b
return bytes(out)
def decode_chunked(body):
i = 0
out = bytearray()
while True:
j = body.find(b"\\r\\n", i)
n = int(body[i:j].split(b";", 1)[0], 16)
i = j + 2
if n == 0:
break
out.extend(body[i:i+n])
i += n
assert body[i:i+2] == b"\\r\\n"
i += 2
return bytes(out)
def aes_ctr_dec(data, key):
dec = Cipher(algorithms.AES(key), modes.CTR(key[:16])).decryptor()
return dec.update(data) + dec.finalize()
pkts = parse_pcap_sll2_ipv4_tcp(PCAP)
flows = collect_tcp_payloads(pkts)
# Reassemble key HTTP flow
req = reassemble(flows[("172.18.0.1", 46460, "172.18.0.10", 80)])
resp = reassemble(flows[("172.18.0.10", 80, "172.18.0.1", 46460)])
hdr, body = resp.split(b"\\r\\n\\r\\n", 1)
decoded = decode_chunked(body)
(OUT / "send_chunks.decoded").write_bytes(decoded)
# Parse NXTCHNKHASH records
pos = []
i = 0
while True:
j = decoded.find(b"NXTCHNKHASH:", i)
if j < 0:
break
pos.append(j)
i = j + 1
records = []
for k, p in enumerate(pos):
q = pos[k+1] if k+1 < len(pos) else len(decoded)
rec = decoded[p:q]
nxt = rec[12:76].decode()
assert rec[76:81] == b"DATA:"
enc = rec[81:]
records.append({
"nxt": nxt,
"enc": enc,
"hash": hashlib.sha256(enc).hexdigest()
})
# Order by hash chain
hmap = {r["hash"]: i for i, r in enumerate(records)}
ref = {r["nxt"] for r in records if r["nxt"] != "0"*64}
start = [i for i, r in enumerate(records) if r["hash"] not in ref][0]
order = []
i = start
while True:
order.append(i)
nxt = records[i]["nxt"]
if nxt == "0"*64:
break
i = hmap[nxt]
# Password recovered during DB analysis
password = "1VL7p6Rcli8mxgkh"
key = hashlib.sha256(password.encode()).digest()
plain = bytearray()
for idx in order:
pt = aes_ctr_dec(records[idx]["enc"], key)
plain.extend(pt)
key = hashlib.sha256(pt).digest()
plain = bytes(plain)
(OUT / "plain.bin").write_bytes(plain)
# Carve PNG
sig = b"\\x89PNG\\r\\n\\x1a\\n"
off = plain.find(sig)
assert off != -1, "PNG signature not found"
png = plain[off:]
(OUT / "carved.png").write_bytes(png)
print("Done. Wrote:", OUT / "carved.png")Recovered Flag
BITSCTF{LF1_st4nds_4_1ost_fr0m_1ns1d3}