import socket, re, sys
from typing import List, Optional

HOST = "45.55.60.238"
PORT = 1337

KNOWN_PREFIX = b"NICC{https://ww"

PROMPT = b"Enter p (hexadecimal): "
BLOCK_BYTES = 16
SOCKET_TIMEOUT = 10.0

def recv_until_prompt(s: socket.socket) -> bytes:
    buf = b""
    s.settimeout(SOCKET_TIMEOUT)
    while True:
        try:
            part = s.recv(4096)
            if not part:
                break
            buf += part
            if PROMPT in buf:
                break
        except socket.timeout:
            break
    return buf

def send_and_get_ciphertext(hexpayload: str) -> str:
    with socket.create_connection((HOST, PORT), timeout=SOCKET_TIMEOUT) as s:
        recv_until_prompt(s)
        s.sendall((hexpayload + "\n").encode())
        resp = recv_until_prompt(s)
        text = resp.decode(errors="ignore")
        if "Enter p (hexadecimal):" in text:
            text = text.split("Enter p (hexadecimal):")[0]
        hex_cands = re.findall(r"[0-9A-Fa-f]+", text)
        if not hex_cands:
            raise RuntimeError("No hex-like substring in server response. Raw (truncated):\n" + text[:2000])
        return max(hex_cands, key=len).lower()

def split_blocks(hexstr: str) -> List[str]:
    blen = BLOCK_BYTES * 2
    return [hexstr[i:i+blen] for i in range(0, len(hexstr), blen)]

def recover_flag(seed: Optional[bytes] = None, max_bytes: int = 512) -> bytes:
    recovered = bytearray()
    if seed:
        recovered.extend(seed)
        print(f"[+] Seeded with known prefix ({len(seed)} bytes): {seed!r}")

    # iterate until max_bytes or until we find a closing brace
    i = len(recovered)
    while i < max_bytes:
        # compute filler so that the unknown byte becomes the last byte of a chosen block:
        r = (BLOCK_BYTES - 1) - (i % BLOCK_BYTES)     # filler length
        p_tail = b"A" * r

        # known window: the bytes immediately preceding the unknown byte within the flag block
        # this is recovered[i - (BLOCK_BYTES - 1) : i] (may be shorter for early bytes)
        start_index = max(0, i - (BLOCK_BYTES - 1))
        known_window = bytes(recovered[start_index:i])

        # Build dictionary blocks: for each candidate c, create a 16-byte block that ends with c.
        # Construct raw = p_tail + known_window + c, but take the LAST 16 bytes of that to ensure correct alignment
        dict_blocks = []
        for c in range(256):
            raw = p_tail + known_window + bytes([c])
            block = raw[-BLOCK_BYTES:]  # ensure exactly one 16-byte block aligned to the right
            dict_blocks.append(block)
        # final payload: dictionary blocks concatenated, then p_tail (so the flag follows)
        payload = b"".join(dict_blocks) + p_tail
        hexpayload = payload.hex()

        try:
            C = send_and_get_ciphertext(hexpayload)
        except Exception as e:
            print("[!] Network or parsing error:", e)
            break

        blocks = split_blocks(C)
        dict_len = len(dict_blocks)
        # target block index: dictionary occupies dict_len blocks; the block containing the unknown byte
        # will be at offset dict_len + (i // BLOCK_BYTES)
        target_index = dict_len + (i // BLOCK_BYTES)
        if target_index >= len(blocks):
            print("[!] Unexpected response: insufficient blocks. Received", len(blocks), "needed", target_index + 1)
            break
        target_block = blocks[target_index]

        # search match in the dictionary region (first dict_len blocks)
        found = False
        for idx in range(dict_len):
            if blocks[idx] == target_block:
                recovered.append(idx)
                printable = chr(idx) if 32 <= idx < 127 else '.'
                print(f"[+] Recovered byte {i}: 0x{idx:02x} ({printable})")
                found = True
                break

        if not found:
            print(f"[!] No match for byte index {i}. Diagnostic: returned {len(blocks)} blocks; sample target (first 32 hex): {target_block[:32]}")
            break

        i += 1
        # stop if we recovered the closing brace
        if recovered.endswith(b"}"):
            print("[+] Closing brace encountered; finishing.")
            break

    return bytes(recovered)

if __name__ == "__main__":
    print(f"Starting recovery against {HOST}:{PORT}\n")
    flag_bytes = recover_flag(KNOWN_PREFIX, max_bytes=512)
    print("\nRecovered (raw):", flag_bytes)
    try:
        print("Recovered (utf-8):", flag_bytes.decode())
    except Exception:
        print("Recovered contains non-UTF8 bytes.")