#!/usr/bin/env python3
"""FLARE-On 13 #6 Threat Invaders - static solver (decrypts the captured traffic).

Usage: python3 -I solve.py
Needs: tshark (extract_http.py). Run from this folder after extracting the 7z.

Key material comes only from challenge.pcapng:
  * telemetry session_id (plaintext JSON)
  * telemetry #2 feedback: 16 bytes = MachineGuid[i] ^ SessionGuid[15-i]
Keystream (from the modified runtime in ThreatInvaders.exe):
  seed = murmur3_fmix64(a ^ (b + (a<<6) + (a>>2) + 0x9e3779b97f4a7c15)),
         a,b = MachineGuid bytes (LE u64 halves)
  per byte: s = s*0x5851f42d4c957f2d + 0x14057b7ef767814f ; k = s >> 56
  one stream shared, in order, by: command 1, output 1, command 2, output 2.
Commands are the comment_b64 of high-score entries whose
PlayerName[0:4] ^ Score == "cmd:"; outputs are telemetry feedback_b64
(length = feedback_length & 0xfff).
"""
import base64
import json
import os
import struct
import subprocess
import sys
import uuid

HERE = os.path.dirname(os.path.abspath(__file__))
LOOT = os.path.join(HERE, "loot")
PCAP = os.path.join(LOOT, "extracted", "ThreatInvaders", "challenge.pcapng")
M = (1 << 64) - 1


def seed_from(mg):
    a, b = int.from_bytes(mg[:8], "little"), int.from_bytes(mg[8:], "little")
    x = a ^ ((b + ((a << 6) & M) + (a >> 2) + 0x9E3779B97F4A7C15) & M)
    x ^= x >> 33
    x = (x * 0xFF51AFD7ED558CCD) & M
    x ^= x >> 29
    x = (x * 0xC4CEB9FE1A85EC53) & M
    return x ^ (x >> 32)


class Stream:
    def __init__(self, s):
        self.s = s

    def xor(self, data):
        out = bytearray()
        for c in data:
            self.s = (self.s * 0x5851F42D4C957F2D + 0x14057B7EF767814F) & M
            out.append(c ^ (self.s >> 56))
        return bytes(out)


def main():
    jl = os.path.join(LOOT, "http_pairs.jsonl")
    subprocess.run([sys.executable, "-I", os.path.join(HERE, "extract_http.py"), PCAP, jl],
                   check=True, stdout=subprocess.DEVNULL)
    pairs = [json.loads(l) for l in open(jl)]
    tele = [json.loads(p["req_body"]) for p in pairs if p["uri"] == "/api/v1/telemetry"]
    sess = uuid.UUID(tele[0]["session_id"]).bytes_le

    leak = next(t for t in tele if t["feedback_length"] & 0x2000)
    lb = base64.b64decode(leak["feedback_b64"])[:16]
    mg = bytes(lb[i] ^ sess[15 - i] for i in range(16))
    print(f"[*] MachineGuid (recovered): {uuid.UUID(bytes_le=mg)}")

    # C2 commands: unique high-score entries whose PlayerName[0:4] ^ Score == "cmd:"
    cmds, seen = [], set()
    for p in pairs:
        if "limit=" not in p["uri"]:
            continue
        for e in json.loads(p["resp_body"])["entries"]:
            n = struct.unpack("<I", e["player_name"].upper().encode()[:4].ljust(4, b"\0"))[0]
            if struct.pack("<I", (n ^ e["score"]) & 0xFFFFFFFF) == b"cmd:" and e["id"] not in seen:
                seen.add(e["id"])
                cmds.append(base64.b64decode(e["comment_b64"]))
    outs = [base64.b64decode(t["feedback_b64"])[:t["feedback_length"] & 0xFFF]
            for t in tele if t["feedback_length"] & 0x4000]

    st = Stream(seed_from(mg))
    flag = None
    for c, o in zip(cmds, outs):
        cmd, out = st.xor(c), st.xor(o)
        print(f"[*] command: {cmd!r}")
        print(out.decode(errors="replace"))
        if b"@flare-on.com" in out:
            flag = out.decode().strip()
    print(f"[+] FLAG: {flag}")
    with open(os.path.join(HERE, "flag.txt"), "w") as f:
        f.write(flag + "\n")


if __name__ == "__main__":
    main()
