diff --git a/receiver/challenges/carbeat.py b/receiver/challenges/carbeat.py new file mode 100644 index 0000000..54023f6 --- /dev/null +++ b/receiver/challenges/carbeat.py @@ -0,0 +1,157 @@ +from .Challenge import Challenge + +import subprocess +import time +import re +import os + +class Carbeat(Challenge): + flag_location = 'flags/carbeat.txt' + history_location = 'history/carbeat.txt' + + _CONTAINER = "carbeat_container" + _SERVICE_CMD = ["docker", "exec", "-i", _CONTAINER, "/home/ctf/chall/mybini"] + _HEX_RE = re.compile(r'^[0-9a-fA-F]+$') + + def _read_container_flag(self) -> str: + out = subprocess.run(["docker", "exec", self._CONTAINER, "cat", "/flag.txt"], + capture_output=True, text=True) + if out.returncode != 0 or not out.stdout.strip(): + raise FileNotFoundError("Flag not found in container (/flag.txt)") + return out.stdout.strip() + + def _spawn(self): + return subprocess.Popen( + self._SERVICE_CMD, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=0, + ) + + def _read_until(self, proc, token, timeout=5.0, max_bytes=1_000_000): + start = time.time() + buf = [] + r = proc.stdout.read + while True: + if time.time() - start > timeout: + tail = ''.join(buf)[-500:] + raise TimeoutError(f"Timeout waiting for '{token}'. Got so far:\n{tail}") + ch = r(1) + if ch == "" and proc.poll() is not None: + raise RuntimeError(f"Process ended while waiting for '{token}'. Output:\n{''.join(buf)}") + buf.append(ch) + if len(buf) > max_bytes: + raise RuntimeError("Exceeded max read size") + if token in "".join(buf): + return "".join(buf) + + def _send_line(self, proc, s: str): + proc.stdin.write(s + "\n") + proc.stdin.flush() + + def _expect_hex_field(self, text: str, label: str) -> str: + m = re.search(rf"{re.escape(label)}\s*:\s*([0-9a-fA-F]+)", text) + assert m, f"Missing '{label}' in output. Tail:\n{text[-400:]}" + hx = m.group(1) + assert self._HEX_RE.match(hx), f"{label} is not hex" + return hx + + def distribute(self, flag): + try: + os.makedirs(os.path.dirname(self.flag_location), exist_ok=True) + with open(self.flag_location, 'w') as f: + f.write(flag) + + os.makedirs(os.path.dirname(self.history_location), exist_ok=True) + with open(self.history_location, 'a') as f: + f.write(flag + '\n') + + self.logger.info(f'Flag {flag} written to {self.flag_location}') + return True + except Exception as e: + self.logger.error(f'Could not write flag to {self.flag_location}: {e}') + return False + + + def register(self, proc, name, level, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "1") + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, name) + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, str(level)) + + def login(self, proc, name, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "2") + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, name) + + def list_user(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "3") + return self._read_until(proc, "\n1. register") + + def bini(self, proc, id, data, timeout=1.0): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, str(id)) + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, data) + + def logout(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "4") + + def exit(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "3") + + def check(self): + try: + with open(self.flag_location, 'r') as f: + host_flag = f.read().strip() + container_flag = self._read_container_flag() + assert host_flag == container_flag, 'Flag mismatch between host and container' + self.logger.info('[ok] flag parity (carbeat)') + + proc = self._spawn() + + self.register(proc, "karbit", 1337) + + name = self.list_user(proc) + assert "karbit" in name + + self.login(proc, "karbit") + self.bini(proc, 1, "waguri") + self.bini(proc, 2, "0") + waguri = self._read_until(proc, "0. edit", timeout=1.) + assert "waguri" in waguri + + self.bini(proc, 3, "0") + self.logout(proc) + self.exit(proc) + + try: + proc.wait(timeout=2.0) + except subprocess.TimeoutExpired: + proc.kill() + raise AssertionError("Program did not exit after option 3") + self.logger.info("[ok] service exit on 3") + + proc_alarm = self._spawn() + self._read_until(proc_alarm, "zzz", timeout=190.0) # 180s + slack + try: + proc_alarm.wait(timeout=5.0) + except subprocess.TimeoutExpired: + proc_alarm.kill() + raise AssertionError("Alarm fired but process did not exit") + self.logger.info("[ok] alarm fired ('zzz') and process self-terminated") + + self.logger.info('Check passed for carbeat') + return True + + except Exception as e: + self.logger.error(f'Could not check carbeat: {e}') + return False \ No newline at end of file diff --git a/services/carbeat/chall.py b/services/carbeat/chall.py new file mode 100644 index 0000000..3592d1b --- /dev/null +++ b/services/carbeat/chall.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import os +import binascii +import hashlib +import threading +import time +import sys +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + +seed_bits = 23 +seed_max = 1 << seed_bits +seed_len = (seed_bits + 7) // 8 +key = os.urandom(16) + +def hash_seed(seed_int: int) -> bytes: + sb = seed_int.to_bytes(seed_len, "big") + return hashlib.sha256(sb).digest()[:16] + +seed = int.from_bytes(os.urandom(4), "big") % seed_max +seed2 = int.from_bytes(os.urandom(4), "big") % seed_max +K1 = hash_seed(seed) +K2 = hash_seed(seed2) + + +flag = "FLAGOAKSDASKDOASDKAODSKDOK" + +def read_hex(prompt: str): + s = input(prompt).strip() + try: + return binascii.unhexlify(s) + except Exception: + print("hmm") + return None + +def enc_cfb(pt: bytes) -> bytes: + iv = os.urandom(16) + aes = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) + ct = aes.encrypt(pt) + return iv + ct + +def enc_cbc(data: bytes, iv1: bytes, iv2: bytes, padd: bool) -> bytes: + x = pad(data, 16) if padd else data + c1 = AES.new(K1, AES.MODE_CBC, iv=iv1).encrypt(x) + c2 = AES.new(K2, AES.MODE_CBC, iv=iv2).encrypt(c1) + return c2 + +def menu(): + print(""" +1. encrypt +2. profit +3. get third +4. exit + """) + +third = 0 +iv11 = None +iv22 = None + +def alarm(): + time.sleep(180) + print("zzz") + sys.exit(0) + +threading.Thread(target=alarm, daemon=True).start() + +while True: + menu() + op = input("> ").strip() + + if op == "1": + data = read_hex("pt: ") + if data is None: + print() + continue + out = enc_cfb(data) + print("ct: ", out.hex()) + print() + + elif op == "2": + if iv11 is not None and iv22 is not None: + iv1, iv2 = iv11, iv22 + iv11 = iv22 = None + else: + iv1 = os.urandom(16) + iv2 = os.urandom(16) + ct = enc_cbc(flag, iv1, iv2, padd=True) + print("iv1: ", iv1.hex()) + print("iv2: ", iv2.hex()) + print("ct: ", ct.hex()) + print() + + elif op == "3": + if third: + print("sheesh") + continue + block = read_hex("pt: ") + if block is None: + print() + continue + if len(block) != 16: + print("hmmm\n") + continue + iv1 = os.urandom(16) + iv2 = os.urandom(16) + ct = enc_cbc(block, iv1, iv2, padd=False) + iv11, iv22 = iv1, iv2 + print("ct: ", ct.hex()) + third = 1 + print() + + elif op == "4": + break + else: + print("mabokkkk?") \ No newline at end of file diff --git a/services/carbeat/chall/mybini b/services/carbeat/chall/mybini index 9de5401..ba1f823 100644 Binary files a/services/carbeat/chall/mybini and b/services/carbeat/chall/mybini differ diff --git a/services/carbeat/chall/mybini.cpp b/services/carbeat/chall/mybini.cpp index 94ca282..6a1a614 100644 --- a/services/carbeat/chall/mybini.cpp +++ b/services/carbeat/chall/mybini.cpp @@ -158,6 +158,7 @@ int main(){ std::cout << "1. register" << std::endl; std::cout << "2. login" << std::endl; std::cout << "3. list karbit" << std::endl; + std::cout << "4. exit" << std::endl; std::cout << "> "; std::cin >> c; @@ -185,6 +186,9 @@ int main(){ case 3: for (auto& user : users) user.info(); break; + case 4: + std::cout << "bye!" << std::endl; + exit(0); default: std::cout << "invalid option" << std::endl; } diff --git a/services/carbeat/dist/carbeat.zip b/services/carbeat/dist/carbeat.zip index f9ddc34..f6271c9 100644 Binary files a/services/carbeat/dist/carbeat.zip and b/services/carbeat/dist/carbeat.zip differ diff --git a/services/carbeat/sla.py b/services/carbeat/sla.py new file mode 100644 index 0000000..54023f6 --- /dev/null +++ b/services/carbeat/sla.py @@ -0,0 +1,157 @@ +from .Challenge import Challenge + +import subprocess +import time +import re +import os + +class Carbeat(Challenge): + flag_location = 'flags/carbeat.txt' + history_location = 'history/carbeat.txt' + + _CONTAINER = "carbeat_container" + _SERVICE_CMD = ["docker", "exec", "-i", _CONTAINER, "/home/ctf/chall/mybini"] + _HEX_RE = re.compile(r'^[0-9a-fA-F]+$') + + def _read_container_flag(self) -> str: + out = subprocess.run(["docker", "exec", self._CONTAINER, "cat", "/flag.txt"], + capture_output=True, text=True) + if out.returncode != 0 or not out.stdout.strip(): + raise FileNotFoundError("Flag not found in container (/flag.txt)") + return out.stdout.strip() + + def _spawn(self): + return subprocess.Popen( + self._SERVICE_CMD, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=0, + ) + + def _read_until(self, proc, token, timeout=5.0, max_bytes=1_000_000): + start = time.time() + buf = [] + r = proc.stdout.read + while True: + if time.time() - start > timeout: + tail = ''.join(buf)[-500:] + raise TimeoutError(f"Timeout waiting for '{token}'. Got so far:\n{tail}") + ch = r(1) + if ch == "" and proc.poll() is not None: + raise RuntimeError(f"Process ended while waiting for '{token}'. Output:\n{''.join(buf)}") + buf.append(ch) + if len(buf) > max_bytes: + raise RuntimeError("Exceeded max read size") + if token in "".join(buf): + return "".join(buf) + + def _send_line(self, proc, s: str): + proc.stdin.write(s + "\n") + proc.stdin.flush() + + def _expect_hex_field(self, text: str, label: str) -> str: + m = re.search(rf"{re.escape(label)}\s*:\s*([0-9a-fA-F]+)", text) + assert m, f"Missing '{label}' in output. Tail:\n{text[-400:]}" + hx = m.group(1) + assert self._HEX_RE.match(hx), f"{label} is not hex" + return hx + + def distribute(self, flag): + try: + os.makedirs(os.path.dirname(self.flag_location), exist_ok=True) + with open(self.flag_location, 'w') as f: + f.write(flag) + + os.makedirs(os.path.dirname(self.history_location), exist_ok=True) + with open(self.history_location, 'a') as f: + f.write(flag + '\n') + + self.logger.info(f'Flag {flag} written to {self.flag_location}') + return True + except Exception as e: + self.logger.error(f'Could not write flag to {self.flag_location}: {e}') + return False + + + def register(self, proc, name, level, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "1") + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, name) + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, str(level)) + + def login(self, proc, name, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "2") + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, name) + + def list_user(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "3") + return self._read_until(proc, "\n1. register") + + def bini(self, proc, id, data, timeout=1.0): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, str(id)) + self._read_until(proc, ": ", timeout=timeout) + self._send_line(proc, data) + + def logout(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "4") + + def exit(self, proc, timeout=1.): + self._read_until(proc, "> ", timeout=timeout) + self._send_line(proc, "3") + + def check(self): + try: + with open(self.flag_location, 'r') as f: + host_flag = f.read().strip() + container_flag = self._read_container_flag() + assert host_flag == container_flag, 'Flag mismatch between host and container' + self.logger.info('[ok] flag parity (carbeat)') + + proc = self._spawn() + + self.register(proc, "karbit", 1337) + + name = self.list_user(proc) + assert "karbit" in name + + self.login(proc, "karbit") + self.bini(proc, 1, "waguri") + self.bini(proc, 2, "0") + waguri = self._read_until(proc, "0. edit", timeout=1.) + assert "waguri" in waguri + + self.bini(proc, 3, "0") + self.logout(proc) + self.exit(proc) + + try: + proc.wait(timeout=2.0) + except subprocess.TimeoutExpired: + proc.kill() + raise AssertionError("Program did not exit after option 3") + self.logger.info("[ok] service exit on 3") + + proc_alarm = self._spawn() + self._read_until(proc_alarm, "zzz", timeout=190.0) # 180s + slack + try: + proc_alarm.wait(timeout=5.0) + except subprocess.TimeoutExpired: + proc_alarm.kill() + raise AssertionError("Alarm fired but process did not exit") + self.logger.info("[ok] alarm fired ('zzz') and process self-terminated") + + self.logger.info('Check passed for carbeat') + return True + + except Exception as e: + self.logger.error(f'Could not check carbeat: {e}') + return False \ No newline at end of file