From bb7c124d9bd1246077b4e58f8ef2eec753c99765 Mon Sep 17 00:00:00 2001 From: Jacob Bachmann Date: Sat, 20 Jul 2024 17:28:39 +0200 Subject: [PATCH] feat: harden checker --- checker/src/checker.py | 429 +++++++++++++++++++------------- documentation/benchmark/main.py | 72 +++--- 2 files changed, 296 insertions(+), 205 deletions(-) diff --git a/checker/src/checker.py b/checker/src/checker.py index 6be13d2..36b1939 100644 --- a/checker/src/checker.py +++ b/checker/src/checker.py @@ -2,6 +2,7 @@ import random import re import string +import traceback from logging import LoggerAdapter from enochecker3.chaindb import ChainDB @@ -11,10 +12,11 @@ GetflagCheckerTaskMessage, GetnoiseCheckerTaskMessage, MumbleException, + OfflineException, PutflagCheckerTaskMessage, PutnoiseCheckerTaskMessage, ) -from enochecker3.utils import assert_equals, assert_in +from enochecker3.utils import assert_equals from httpx import AsyncClient from websockets.exceptions import ConnectionClosedError, InvalidStatusCode @@ -56,9 +58,9 @@ async def putflag0( db: ChainDB, logger: LoggerAdapter, ) -> str: - (username, cookies, id) = await repl_create(client, db, logger) - flag = base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8") try: + (username, cookies, id) = await repl_create(client, db, logger) + flag = base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8") await repl_websocket( task.address, logger, @@ -66,14 +68,20 @@ async def putflag0( id, [sh(f'echo "{flag}" > flagstore.txt').default()], ) + return username except TimeoutError: raise MumbleException("Websocket timed out") except ConnectionClosedError: raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") - - return username + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Putflag failed") @checker.getflag(0) @@ -83,11 +91,13 @@ async def getflag0( db: ChainDB, logger: LoggerAdapter, ) -> None: - (cookies, id) = await repl_login(client, db, logger) - flag = ( - base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8").replace("+", "\\+") - ) try: + (cookies, id) = await repl_login(client, db, logger) + flag = ( + base64.b64encode(bytes(task.flag, "utf-8")) + .decode("utf-8") + .replace("+", "\\+") + ) await repl_websocket( task.address, logger, @@ -101,6 +111,13 @@ async def getflag0( raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Getflag failed") @checker.exploit(0) @@ -109,24 +126,24 @@ async def exploit0( client: AsyncClient, logger: LoggerAdapter, ): - target_username = task.attack_info + try: + target_username = task.attack_info - if target_username is None: - raise MumbleException("No attack_info") + if target_username is None: + raise MumbleException("No attack_info") - logger.info("Exploit: " + target_username) + logger.info("Exploit: " + target_username) - if len(target_username) != 60: - raise MumbleException("Incorrect attack_info username length") + if len(target_username) != 60: + raise MumbleException("Incorrect attack_info username length") - delta_username = exploit0_apply_delta(target_username) + delta_username = exploit0_apply_delta(target_username) - logger.info("Delta username: " + delta_username) + logger.info("Delta username: " + delta_username) - password = "deafbeefdeadbeefdeadbeef" + password = "deafbeefdeadbeefdeadbeef" - (cookies, id) = await do_repl_auth(client, logger, delta_username, password) - try: + (cookies, id) = await do_repl_auth(client, logger, delta_username, password) response = await repl_websocket( task.address, logger, @@ -134,19 +151,25 @@ async def exploit0( id, [sh(f"echo FLAG && cat ../{target_username}/flagstore.txt").default()], ) + match = re.findall(r"FLAG\s*([A-Za-z0-9\+\=\/]+)\s*OK", response) + if len(match) == 0: + return None + + flag = base64.b64decode(match[0]).decode("utf-8") + return flag except TimeoutError: raise MumbleException("Flag was not found") except ConnectionClosedError: raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") - - match = re.findall(r"FLAG\s*([A-Za-z0-9\+\=\/]+)\s*OK", response) - if len(match) == 0: - return None - - flag = base64.b64decode(match[0]).decode("utf-8") - return flag + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Exploit failed") @checker.putnoise(0) @@ -156,12 +179,12 @@ async def putnoise0( db: ChainDB, logger: LoggerAdapter, ): - (_, cookies, id) = await repl_create(client, db, logger) - sessions = await get_sessions(client, cookies, logger) - assert_equals(len(sessions) > 0, True, "No session created") - - (i, noise) = get_random_noise() try: + (_, cookies, id) = await repl_create(client, db, logger) + sessions = await get_sessions(client, cookies, logger) + assert_equals(len(sessions) > 0, True, "No session created") + + (i, noise) = get_random_noise() await repl_websocket( task.address, logger, @@ -169,14 +192,20 @@ async def putnoise0( id, noise.command_chain, ) + await db.set("noise_id", i) except TimeoutError: raise MumbleException("Putnoise timed out") except ConnectionClosedError: raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") - - await db.set("noise_id", i) + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Putnoise failed") @checker.getnoise(0) @@ -186,17 +215,17 @@ async def getnoise0( db: ChainDB, logger: LoggerAdapter, ): - (cookies, id) = await repl_login(client, db, logger) - sessions = await get_sessions(client, cookies, logger) - assert_equals(len(sessions) > 0, True, "No session created") - try: - i = await db.get("noise_id") - except KeyError: - raise MumbleException("noise_id not present in chaindb") - if not isinstance(i, int): - raise MumbleException("noise_id is not a int: " + str(i)) - noise = get_noise(i) try: + (cookies, id) = await repl_login(client, db, logger) + sessions = await get_sessions(client, cookies, logger) + assert_equals(len(sessions) > 0, True, "No session created") + try: + i = await db.get("noise_id") + except KeyError: + raise MumbleException("noise_id not present in chaindb") + if not isinstance(i, int): + raise MumbleException("noise_id is not a int: " + str(i)) + noise = get_noise(i) await repl_websocket( task.address, logger, @@ -210,34 +239,50 @@ async def getnoise0( raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Getnoise failed") @checker.havoc(0) async def havoc0( client: AsyncClient, + logger: LoggerAdapter, ): - response = await client.get("/", follow_redirects=True) - assert_equals( - response.status_code < 300, - True, - "Failed to get index.html", - ) - - assert_equals( - (await client.get("/icon.png", follow_redirects=True)).status_code < 300, - True, - "Failed to get icon.png", - ) - - pattern = r'[href|src]="(/[^"]*)"' - matches = re.findall(pattern, response.text) - for match in matches: - if isinstance(match, str) and match.startswith("/_next/static"): - assert_equals( - (await client.get(match, follow_redirects=True)).status_code < 300, - True, - "Failed to get " + match, - ) + try: + response = await client.get("/", follow_redirects=True) + assert_equals( + response.status_code < 300, + True, + "Failed to get index.html", + ) + + assert_equals( + (await client.get("/icon.png", follow_redirects=True)).status_code < 300, + True, + "Failed to get icon.png", + ) + + pattern = r'[href|src]="(/[^"]*)"' + matches = re.findall(pattern, response.text) + for match in matches: + if isinstance(match, str) and match.startswith("/_next/static"): + assert_equals( + (await client.get(match, follow_redirects=True)).status_code < 300, + True, + "Failed to get " + match, + ) + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Havoc failed") @checker.putflag(1) @@ -247,18 +292,26 @@ async def putflag1( db: ChainDB, logger: LoggerAdapter, ): - (username, password) = await user_register(client, logger, db) - cookies = await do_user_login(client, logger, username, password) - flag = base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8") - devenvUuid = await create_devenv( - client, logger, cookies, "cat flagstore.txt", "cat flagstore.txt" - ) - await db.set("devenvUuid", devenvUuid) - await do_set_devenv_file_content( - client, logger, cookies, devenvUuid, "flagstore.txt", flag - ) + try: + (username, password) = await user_register(client, logger, db) + cookies = await do_user_login(client, logger, username, password) + flag = base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8") + devenvUuid = await create_devenv( + client, logger, cookies, "cat flagstore.txt", "cat flagstore.txt" + ) + await db.set("devenvUuid", devenvUuid) + await do_set_devenv_file_content( + client, logger, cookies, devenvUuid, "flagstore.txt", flag + ) - return devenvUuid + return devenvUuid + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Putflag failed") @checker.getflag(1) @@ -268,22 +321,32 @@ async def getflag1( db: ChainDB, logger: LoggerAdapter, ): - cookies = await user_login(client, logger, db) try: - devenvUuid = await db.get("devenvUuid") - except KeyError: - raise MumbleException("Missing database entry from putflag") - flag = ( - base64.b64encode(bytes(task.flag, "utf-8")).decode("utf-8").replace("+", "\\+") - ) + cookies = await user_login(client, logger, db) + try: + devenvUuid = await db.get("devenvUuid") + except KeyError: + raise MumbleException("Missing database entry from putflag") + flag = ( + base64.b64encode(bytes(task.flag, "utf-8")) + .decode("utf-8") + .replace("+", "\\+") + ) - payload = await do_get_devenv_file_content( - client, logger, cookies, devenvUuid, "flagstore.txt" - ) + payload = await do_get_devenv_file_content( + client, logger, cookies, devenvUuid, "flagstore.txt" + ) - match = re.match(f".*{flag}.*", payload, re.S) - if match is None: - raise MumbleException("Received unexpected input") + match = re.match(f".*{flag}.*", payload, re.S) + if match is None: + raise MumbleException("Received unexpected input") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Getflag failed") @checker.exploit(1) @@ -292,29 +355,29 @@ async def exploit1( client: AsyncClient, logger: LoggerAdapter, ): - target_devenvUuid = task.attack_info + try: + target_devenvUuid = task.attack_info - if target_devenvUuid is None: - raise MumbleException("No attack_info") + if target_devenvUuid is None: + raise MumbleException("No attack_info") - logger.info("Exploit: " + target_devenvUuid) + logger.info("Exploit: " + target_devenvUuid) - username = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) - password = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) - name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + username = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) + password = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) + name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - await do_user_register(client, logger, username, password) - cookies = await do_user_login(client, logger, username, password) - devenvUuid = await do_create_devenv( - client, - logger, - cookies, - name, - "echo FLAG", - "cat flagstore.txt && echo OK", - ) + await do_user_register(client, logger, username, password) + cookies = await do_user_login(client, logger, username, password) + devenvUuid = await do_create_devenv( + client, + logger, + cookies, + name, + "echo FLAG", + "cat flagstore.txt && echo OK", + ) - try: payload = await do_get_devenv_file_content( client, logger, @@ -333,6 +396,13 @@ async def exploit1( raise MumbleException("Connection was closed") except InvalidStatusCode: raise MumbleException("Invalid status code") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Exploit failed") @checker.putnoise(1) @@ -342,18 +412,26 @@ async def putnoise1( db: ChainDB, logger: LoggerAdapter, ): - (username, password) = await user_register(client, logger, db) - cookies = await do_user_login(client, logger, username, password) - devenvUuid = await create_devenv( - client, logger, cookies, "gcc -o main main.c", "./main" - ) - (i, noise) = get_random_noise1() - await db.set("devenvUuid", devenvUuid) - await do_set_devenv_file_content( - client, logger, cookies, devenvUuid, "main.c", noise - ) + try: + (username, password) = await user_register(client, logger, db) + cookies = await do_user_login(client, logger, username, password) + devenvUuid = await create_devenv( + client, logger, cookies, "gcc -o main main.c", "./main" + ) + (i, noise) = get_random_noise1() + await db.set("devenvUuid", devenvUuid) + await do_set_devenv_file_content( + client, logger, cookies, devenvUuid, "main.c", noise + ) - await db.set("noise_id", i) + await db.set("noise_id", i) + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Putnoise failed") @checker.getnoise(1) @@ -363,27 +441,34 @@ async def getnoise1( db: ChainDB, logger: LoggerAdapter, ): - - cookies = await user_login(client, logger, db) try: - devenvUuid = await db.get("devenvUuid") - except KeyError: - raise MumbleException("Missing database entry from putflag") - try: - i = await db.get("noise_id") - except KeyError: - raise MumbleException("noise_id not present in chaindb") - if not isinstance(i, int): - raise MumbleException("noise_id is not a int: " + str(i)) - noise = get_noise1(i) - payload = await do_get_devenv_file_content( - client, - logger, - cookies, - devenvUuid, - "main.c", - ) - assert_equals(noise.strip(), payload.strip(), "Wrong file content") + cookies = await user_login(client, logger, db) + try: + devenvUuid = await db.get("devenvUuid") + except KeyError: + raise MumbleException("Missing database entry from putflag") + try: + i = await db.get("noise_id") + except KeyError: + raise MumbleException("noise_id not present in chaindb") + if not isinstance(i, int): + raise MumbleException("noise_id is not a int: " + str(i)) + noise = get_noise1(i) + payload = await do_get_devenv_file_content( + client, + logger, + cookies, + devenvUuid, + "main.c", + ) + assert_equals(noise.strip(), payload.strip(), "Wrong file content") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Getnoise failed") @checker.havoc(1) @@ -391,34 +476,44 @@ async def havoc1( client: AsyncClient, logger: LoggerAdapter, ): - username = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) - password = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) - await do_user_register(client, logger, username, password) - cookies = await do_user_login(client, logger, username, password) - - name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - buildCmd = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - runCmd = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - - devenvUuid = await do_create_devenv(client, logger, cookies, name, buildCmd, runCmd) - devenv = await do_get_devenv(client, logger, cookies, devenvUuid) - assert_equals(name, devenv["name"], "Devenv has invalid name") - assert_equals(buildCmd, devenv["buildCmd"], "Devenv has invalid buildCmd") - assert_equals(runCmd, devenv["runCmd"], "Devenv has invalid runCmd") - - name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - await do_patch_devenv(client, logger, cookies, devenvUuid, name=name) - devenv = await do_get_devenv(client, logger, cookies, devenvUuid) - assert_equals(name, devenv["name"], "After patch: Devenv has invalid name") - - filename = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) - await do_create_devenv_file(client, logger, cookies, devenvUuid, filename) - files = await do_get_devenv_files(client, logger, cookies, devenvUuid) - assert_equals(filename in files, True, "Create file did not create file") - - await do_delete_devenv_file(client, logger, cookies, devenvUuid, filename) - files = await do_get_devenv_files(client, logger, cookies, devenvUuid) - assert_equals(filename not in files, True, "Delete file did not delete file") + try: + username = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) + password = "".join(random.choice(string.ascii_lowercase) for _ in range(35)) + await do_user_register(client, logger, username, password) + cookies = await do_user_login(client, logger, username, password) + + name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + buildCmd = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + runCmd = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + + devenvUuid = await do_create_devenv( + client, logger, cookies, name, buildCmd, runCmd + ) + devenv = await do_get_devenv(client, logger, cookies, devenvUuid) + assert_equals(name, devenv["name"], "Devenv has invalid name") + assert_equals(buildCmd, devenv["buildCmd"], "Devenv has invalid buildCmd") + assert_equals(runCmd, devenv["runCmd"], "Devenv has invalid runCmd") + + name = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + await do_patch_devenv(client, logger, cookies, devenvUuid, name=name) + devenv = await do_get_devenv(client, logger, cookies, devenvUuid) + assert_equals(name, devenv["name"], "After patch: Devenv has invalid name") + + filename = "".join(random.choice(string.ascii_lowercase) for _ in range(10)) + await do_create_devenv_file(client, logger, cookies, devenvUuid, filename) + files = await do_get_devenv_files(client, logger, cookies, devenvUuid) + assert_equals(filename in files, True, "Create file did not create file") + + await do_delete_devenv_file(client, logger, cookies, devenvUuid, filename) + files = await do_get_devenv_files(client, logger, cookies, devenvUuid) + assert_equals(filename not in files, True, "Delete file did not delete file") + except MumbleException as e: + raise e + except OfflineException as e: + raise e + except Exception: + logger.error(traceback.format_exc()) + raise MumbleException("Havoc failed") if __name__ == "__main__": diff --git a/documentation/benchmark/main.py b/documentation/benchmark/main.py index 9f63832..badf45d 100644 --- a/documentation/benchmark/main.py +++ b/documentation/benchmark/main.py @@ -50,10 +50,10 @@ def get_ip_address(ifname): SERVICE_ADDR = get_ip_address("wlp3s0") VARIANTS: TVariants = { - 0: ["putflag", "getflag", "exploit", "putnoise", "getnoise"], + 0: ["putflag", "getflag", "exploit", "putnoise", "getnoise", "havoc"], 1: ["putflag", "getflag", "exploit", "putnoise", "getnoise", "havoc"], } -TICKS = 3 +TICKS = 20 MULTIPLIER = 1 EXPLOITS_AMOUNT = 20 EXPLOIT_PAST_ROUNDS = 10 @@ -251,12 +251,18 @@ async def request(self, method: TMethod): return await asyncio.gather(*futures) - async def exec(self, curr_round: int = -1, with_putflag: bool = False): - if with_putflag: + async def exec(self, curr_round: int = -1, with_put: bool = False): + if with_put: await self.request("putflag") + await self.request("putnoise") coros = [] if curr_round >= 0 and curr_round < self.round_id + 3: coros.append(run_in(30, self.request("getflag"))) + if curr_round >= 0 and curr_round < self.round_id + 3: + coros.append(run_in(30, self.request("getnoise"))) + if curr_round < self.round_id + 3: + coros.append(run_before(60, self.request("havoc"))) + coros.append(run_before(40, self.request("havoc"))) for variant in self.variants: for _ in range(self.exploits_amount): coros.append(run_before(60, variant.request("exploit"))) @@ -270,7 +276,22 @@ async def main(): tasks: List[asyncio.Task] = [] async with aiohttp.ClientSession(CHECKER_ADDR) as client: - for i in range(1): + for i in range(10): + round = Round( + client, + curr_round, + VARIANTS, + multiplier=MULTIPLIER, + exploits_amount=EXPLOITS_AMOUNT, + ) + await round.request("putflag") + await round.request("putnoise") + rounds.append(round) + curr_round += 1 + + for i in range(TICKS): + print("TICK", i) + start = time.monotonic() round = Round( client, curr_round, @@ -278,40 +299,15 @@ async def main(): multiplier=MULTIPLIER, exploits_amount=EXPLOITS_AMOUNT, ) - await round.request("havoc") - # await round.request("putnoise") - # await round.request("getnoise") + + tasks.append(asyncio.create_task(round.exec(with_put=True))) + for _round in rounds[-EXPLOIT_PAST_ROUNDS:]: + tasks.append(asyncio.create_task(_round.exec(curr_round))) + rounds.append(round) + + await asyncio.sleep(60) + print(f"TICK {i} END ({str(time.monotonic() - start)[:5]})") curr_round += 1 - # for i in range(10): - # round = Round( - # client, - # curr_round, - # VARIANTS, - # multiplier=MULTIPLIER, - # exploits_amount=EXPLOITS_AMOUNT, - # ) - # await round.request("putflag") - # rounds.append(round) - # curr_round += 1 - # - # for i in range(TICKS): - # print("TICK", i) - # start = time.monotonic() - # round = Round( - # client, - # curr_round, - # VARIANTS, - # multiplier=MULTIPLIER, - # exploits_amount=EXPLOITS_AMOUNT, - # ) - # - # tasks.append(asyncio.create_task(round.exec(with_putflag=True))) - # for _round in rounds[-EXPLOIT_PAST_ROUNDS:]: - # tasks.append(asyncio.create_task(_round.exec(curr_round))) - # - # await asyncio.sleep(60) - # print(f"TICK {i} END ({str(time.monotonic() - start)[:5]})") - # curr_round += 1 await asyncio.gather(*tasks)