enowars5-service-stldoctor

STL-Analyzing A/D Service for ENOWARS5 in 2021
git clone https://git.sinitax.com/sinitax/enowars5-service-stldoctor
Log | Files | Refs | README | LICENSE | sfeed.txt

commit 857e53cf30d17a1fe87b683bb26a54899760dcd8
parent f9add70b96f8d5e1544547be8feebc9a71f7ece8
Author: Louis Burda <quent.burda@gmail.com>
Date:   Fri,  9 Jul 2021 09:39:44 +0200

various improvements to logging and performance

added service timeout (180s), improved debug logging,  moved persistence checking havocs into single noise by selecting randomly which type of stl to upload (still check correctness of parsing of any type in havocs)

Diffstat:
Mchecker/src/checker.py | 440+++++++++++++++++++++++++++++++++++++------------------------------------------
Mservice/entrypoint.sh | 2+-
Mservice/src/main.c | 10++++++++++
Msrc/main.c | 10++++++++++
4 files changed, 226 insertions(+), 236 deletions(-)

diff --git a/checker/src/checker.py b/checker/src/checker.py @@ -42,6 +42,7 @@ extra_models = [] for path in os.listdir(f"{script_path}/models"): if path.endswith(".stl"): extra_models.append(f"{script_path}/models/{path}") +assert len(extra_models) > 0 wordlist = [w for w in open(f"{script_path}/wordlist.txt").read().split() if w != ""] prompt = b"\r$ " @@ -63,11 +64,11 @@ app = lambda: checker.app async def timed(promise: Any, logger: LoggerAdapter, ctx: str) -> Any: - logger.debug("Started: {}".format(ctx)) + logger.debug("START: {}".format(ctx)) start = time.time() result = await promise end = time.time() - logger.debug("Done: {} (took {:.3f} seconds)".format(ctx, end - start)) + logger.debug("DONE: {} (took {:.3f} seconds)".format(ctx, end - start)) return result @@ -80,16 +81,21 @@ class Session: self.closed = False async def __aenter__(self) -> "Session": - await timed(self.prepare(), self.logger, ctx="preparing session") + self.logger.debug("Preparing session") + await self.prepare() return self async def __aexit__(self, *args: list[Any], **kwargs: dict[str, Any]) -> None: - await timed(self.close(), self.logger, ctx="closing session") + self.logger.debug("Closing session") + await self.close() async def readuntil(self, target: bytes, ctx: Optional[str] = None) -> bytes: try: ctxstr = f"readuntil {target!r}" if ctx is None else ctx - return await timed(self.reader.readuntil(target), self.logger, ctx=ctxstr) + data = await timed(self.reader.readuntil(target), self.logger, ctx=ctxstr) + msg = f"read: {data[:100]!r}{'..' if len(data) > 100 else ''}" + self.logger.debug(msg) + return data except TimeoutError: self.logger.critical(f"Service timed out while waiting for {target!r}") raise MumbleException("Service took too long to respond") @@ -97,10 +103,13 @@ class Session: async def readline(self, ctx: Optional[str] = None) -> bytes: return await self.readuntil(b"\n", ctx=ctx) - async def read(self, n: int, ctx: Optional[str]) -> bytes: + async def read(self, n: int, ctx: Optional[str] = None) -> bytes: try: ctxstr = f"reading {n} bytes" if ctx is None else ctx - return await timed(self.reader.readexactly(n), self.logger, ctx=ctxstr) + data = await timed(self.reader.readexactly(n), self.logger, ctx=ctxstr) + msg = f"read: {data[:60]!r}{'..' if len(data) > 60 else ''}" + self.logger.debug(msg) + return data except TimeoutError: self.logger.critical(f"Service timed out while reading {n} bytes") raise MumbleException("Service took too long to respond") @@ -109,10 +118,8 @@ class Session: await self.writer.drain() def write(self, data: bytes) -> None: - if len(data) > 100: - self.logger.debug(f"Sending {data[:100]!r}..") - else: - self.logger.debug(f"Sending {data!r}") + msg = f"write: {data[:60]!r}{'..' if len(data) > 60 else ''}" + self.logger.debug(msg) self.writer.write(data) async def prepare(self) -> None: @@ -153,6 +160,16 @@ def includes_any(resp: bytes, targets: list[bytes]) -> bool: return False +def nnt(ino: Optional[tuple[bytes, bytes]]) -> tuple[bytes, bytes]: + assert ino is not None + return ino + + +def nnb(ino: Optional[bytes]) -> bytes: + assert ino is not None + return ino + + def randbool() -> bool: return rand.randint(0, 1) == 1 @@ -236,6 +253,9 @@ def assert_match(data: bytes, pattern: bytes, raiser: Any) -> bytes: def genfile_ascii(solidname: bytes, malformed: bool = None) -> bytes: + # Generate a valid ascii STL file or one that is malformed in a + # way that it can't be interpreted by the service, but might succeed + # with typical stl parsing libraries (as a means to identify them) indent = bytes([rand.choice(b"\t ") for i in range(rand.randint(1, 4))]) facet_count = rand.randint(4, 30) @@ -245,9 +265,9 @@ def genfile_ascii(solidname: bytes, malformed: bool = None) -> bytes: content = b"solid\n" for fi in range(facet_count): - # MALFORM 1: wrong keyword + # MALFORM 1: prefix keyword if malformed == 1: - content += indent * 1 + b"facet nornal " + content += indent * 1 + b"facet norm" else: content += indent * 1 + b"facet normal " @@ -257,9 +277,9 @@ def genfile_ascii(solidname: bytes, malformed: bool = None) -> bytes: content += " ".join([f"{v:.2f}" for v in norm]).encode() + b"\n" - # MALFORM 2: wrong keyword case + # MALFORM 2: too many spaces if malformed == 2: - content += indent * 2 + b"outer lOop\n" + content += indent * 2 + b"outer loop\n" else: content += indent * 2 + b"outer loop\n" @@ -274,17 +294,21 @@ def genfile_ascii(solidname: bytes, malformed: bool = None) -> bytes: content += indent * 2 + b"endloop\n" content += indent + b"endfacet\n" - # MALFORM 3: no endsolid keyword - if malformed != 3: - if solidname != b"": - content += b"endsolid " + solidname + b"\n" - else: - content += b"endsolid\n" + # MALFORM 3: different endsolid name + if malformed == 3: + content += b"endsolid end\n" + if solidname != b"": + content += b"endsolid " + solidname + b"\n" + else: + content += b"endsolid\n" return content def genfile_bin(solidname: bytes, malformed: bool = None) -> bytes: + # Generate a valid binary STL file or one that is malformed in a + # way that it can't be interpreted by the service, but might succeed + # with typical stl parsing libraries (as a means to identify them) facet_count = rand.randint(4, 30) if len(solidname) > 78: @@ -329,16 +353,34 @@ def genfile(solidname: bytes, filetype: str, malformed: Optional[Any] = None) -> return genfile_ascii(solidname, malformed=malformed) elif filetype == "bin": return genfile_bin(solidname, malformed=malformed) - elif filetype == "garbage-tiny": - return bytes([rand.choice(generic_alphabet) for i in range(rand.randint(3, 8))]) elif filetype == "garbage": - return bytes( - [rand.choice(generic_alphabet) for i in range(rand.randint(100, 300))] - ) + return bytes([rand.randint(0, 255) for i in range(rand.randint(100, 600))]) else: raise InternalErrorException("Invalid file type supplied") +def getfile(filetype: str) -> tuple[bytes, bytes]: + if filetype == "ascii": + if rand.randint(0, 20) >= 5: + solidname = fakeid() + stlfile = genfile(solidname, "ascii") + else: + model = rand.choice([v for v in extra_models if v.endswith("-ascii.stl")]) + stlfile = open(model, "rb").read() + solidname = fakeid() + stlfile = stlfile.replace(b"OpenSCAD_Model", solidname) + else: + if rand.randint(0, 20) >= 5: + solidname = fakeid() + stlfile = genfile(solidname, "ascii") + else: + model = rand.choice([v for v in extra_models if v.endswith("-bin.stl")]) + stlfile = open(model, "rb").read() + solidname = fakeid(minlen=15, maxlen=15) + stlfile = solidname + stlfile[15:] # replaces b"OpenSCAD Model\n" + return stlfile, solidname + + def parse_stlinfo(stlfile: bytes) -> Any: fakefile = BytesIO() fakefile.write(stlfile) @@ -388,17 +430,16 @@ async def do_auth( await session.drain() # Check for errors - resp = await session.readline(ctx="reading auth response (1)") - if b"ERR:" in resp: + resp = await session.readuntil(prompt, ctx="reading auth response") + if resp == b"ERR:": if check: session.logger.critical(f"Failed to login with {authstr!r}:\n{resp!r}") raise MumbleException("Authentication not working properly") return None # Also check success message - resp += await session.readuntil(prompt, ctx="reading auth response (2)") try: - userid = resp.split(b"Logged in with ID ")[1].split(b"!")[0] + userid = resp.split(b"!", 1)[0].split(b"Logged in with ID ", 1)[1] except: session.logger.critical(f"Login with pass {authstr!r} failed:\n{resp!r}") raise MumbleException("Authentication not working properly") @@ -414,15 +455,17 @@ async def do_auth( raise MumbleException("Authentication not working properly") return None + session.logger.debug(f"Logged in as user: {userid!r}") + return userid async def do_list(session: Session, check: bool = True) -> Optional[bytes]: session.write(b"list\n") await session.drain() - resp = await session.readuntil(prompt, ctx="reading list response") # Check for errors + resp = await session.readuntil(prompt, ctx="reading list response") if b"ERR:" in resp and b">> " not in resp: if check: session.logger.critical(f"Failed to list private files:\n{resp!r}") @@ -440,40 +483,45 @@ async def do_upload( ) -> Optional[bytes]: session.logger.debug(f"Uploading model with name {modelname!r}") - session.write(b"upload\n") - # enter name and check resp + # start upload and enter name + session.write(b"upload\n") session.write(modelname + b"\n") await session.drain() + + # check for error with name await session.readuntil(b"name: ") resp = await session.read(4, ctx="checking for err response") if resp == b"ERR:": + resp += await session.readuntil(prompt) if check: - resp = resp + await session.readline() session.logger.critical(f"Failed during name check: {resp!r}") raise MumbleException("File upload not working properly") return None - # enter size and check resp + # write size and contents in together await session.readuntil(b"size: ") session.write(f"{len(stlfile)}\n".encode()) + session.write(stlfile) await session.drain() + + # check for error with size resp = await session.read(4, ctx="checking for err response") if resp == b"ERR:": + resp += await session.readuntil(prompt) if check: - resp = resp + await session.readline() session.logger.critical(f"Failed during size check: {resp!r}") raise MumbleException("File upload not working properly") return None + # checker for error with file await session.readuntil(b"listening..\n") - session.write(stlfile) - await session.drain() resp = await session.readline() if b"ERR:" in resp: if check: session.logger.critical(f"Failed during stl parsing: {resp!r}") raise MumbleException("File upload not working properly") + await session.readuntil(prompt) return None # parse returned id @@ -482,12 +530,10 @@ async def do_upload( if modelid == b"": raise Exception except: - session.logger.critical( - f"Invalid file size during upload of {modelname!r}:\n{resp!r}" - ) + session.logger.critical(f"Invalid size during upload:\n{resp!r}") raise MumbleException("File upload not working properly") - session.logger.debug(f"Uploaded model id: {modelid!r}") + session.logger.debug(f"Uploaded model with id {modelid!r}") await session.readuntil(prompt, ctx="waiting for prompt") return modelid @@ -504,22 +550,37 @@ async def do_search( # get possible hashes session.write(b"search " + modelname + b"\n") await session.drain() - resp = await session.readline() - if b"ERR:" in resp: + + # chck for error with search query + resp = await session.read(4) + if resp == b"ERR:": + resp += await session.readuntil(prompt) if check: session.logger.critical( f"Failed to retrieve model {modelname!r}:\n{resp!r}" ) raise MumbleException("File search not working properly") return None - resp = resp + await session.readuntil(b"> ") - results = [l.strip() for l in resp[:-2].split(b"\n")] - # request first result - session.write(results[0] + b"\n") + # collect results + resp += await session.readuntil(b"quit]: ") + try: + resp = resp.split(b">")[0] + except: + session.logger.critical('Search response missing b">" delim') + raise MumbleException("File search not working properly") + results = [l.strip() for l in resp.split(b"\n") if l.strip() != b""] + + # request most recent result (later in index file) + session.write(results[-1] + b"\n") + session.write(b"y\n" if download else b"n\n") await session.drain() - resp = await session.readline() - if b"ERR:" in resp: + + # check for error in hash provided + resp = await session.read(4) + if resp == b"ERR:": + # cleanup download y/n interpreted as command + await session.readuntil(prompt) if check: session.logger.critical(f"Error selecting file: {results[0]!r}") raise MumbleException("File search not working properly") @@ -528,9 +589,6 @@ async def do_search( b"================== \n", ctx="reading stl info" ) - # download if requested - session.write(b"y\n" if download else b"n\n") - await session.drain() stlfile = b"" if download: # Parse file contents await session.readuntil(b"Here you go.. (", ctx="reading stl size (1)") @@ -555,8 +613,7 @@ async def do_search( async def check_listed(session: Session, includes: list[bytes]) -> bytes: - resp = await do_list(session, check=True) - assert resp is not None + resp = nnb(await do_list(session, check=True)) if not includes_all(resp, includes): session.logger.critical(f"Failed to find {includes} in listing:\n{resp!r}") raise MumbleException("File listing not working properly") @@ -590,8 +647,7 @@ async def check_in_search( includes: list[bytes], download: bool = False, ) -> tuple[bytes, bytes]: - resp = await do_search(session, modelname, download, check=True) - assert resp is not None + resp = nnt(await do_search(session, modelname, download, check=True)) if not includes_all(resp[0] + resp[1], includes): session.logger.critical( f"Retrieved info for {modelname!r} is missing {includes}: {resp[0]+resp[1]!r}" @@ -708,64 +764,24 @@ def check_stlinfo( # TEST METHODS # -async def test_good_upload( - di: DependencyInjector, filetype: str, register: bool -) -> None: - solidname, modelname, authstr = fakeids(3) - stlfile = genfile(solidname, filetype) - ref_info = parse_stlinfo(stlfile) +async def test_good_upload(di: DependencyInjector, filetype: str) -> None: + modelname = fakeid() + stlfile, solidname = getfile(filetype) + ref = parse_stlinfo(stlfile) - # Create new session, register and upload file + # Upload file, get it with search, verify returned info session = await di.get(Session) - if register: - await do_auth(session, authstr, check=True, newuser=True) - modelid = await do_upload(session, modelname, stlfile, check=True) - assert modelid is not None - check_hash(modelid) + modelid = nnb(await do_upload(session, modelname, stlfile, check=True)) expected = [modelname, solidname, stlfile, modelid] - info, stlfile = await check_in_search(session, modelname, expected, download=True) + info, stl = await check_in_search(session, modelname, expected, download=True) check_stlinfo( session.logger, info, - ref_info, + ref, ref_modelname=modelname, ref_modelid=modelid, ref_solidname=solidname, ) - if register: - await check_listed(session, [modelname, modelid + b"-"]) - - # Try getting file from a new session - session = await di.get(Session) - if register: - await check_not_in_search( - session, modelname, expected, download=True, fail=True - ) - await do_auth(session, authstr, check=True, newuser=False) - info, stlfile = await check_in_search( - session, modelname, expected, download=True - ) - check_stlinfo( - session.logger, - info, - ref_info, - ref_modelname=modelname, - ref_modelid=modelid, - ref_solidname=solidname, - ) - await check_listed(session, [modelname, modelid + b"-"]) - else: - info, stlfile = await check_in_search( - session, modelname, expected, download=True - ) - check_stlinfo( - session.logger, - info, - ref_info, - ref_modelname=modelname, - ref_modelid=modelid, - ref_solidname=solidname, - ) async def test_bad_upload(di: DependencyInjector, filetype: str, variant: int) -> None: @@ -780,19 +796,17 @@ async def test_bad_upload(di: DependencyInjector, filetype: str, variant: int) - raise MumbleException("Upload validation not working properly") -async def test_search(di: DependencyInjector, registered: bool = False) -> None: - solidname, modelname, authstr = fakeids(3) - - # Ensure searching for a file that doesnt exist causes an error +async def test_hash_collision(di: DependencyInjector) -> None: + # See if using a random string we hit a hash collision for search / auth session = await di.get(Session) - if registered: - await do_auth(session, authstr, check=True, newuser=True) - resp = await do_search(session, modelname, download=False, check=False) - if resp is not None: - session.logger.critical( - f"Search for file that shouldn't exist succeeded:\n{resp[0]+resp[1]!r}" - ) - raise MumbleException("File search not working properly") + sresp = await do_search(session, fakeid(), download=False, check=False) + if sresp is not None: + session.logger.critical("File search succeeded on random file") + raise MumbleException("Hash function not working properly") + aresp = await do_auth(session, fakeid(), check=False, newuser=False) + if aresp is not None: + session.logger.critical("Auth succeeded for user with random str") + raise MumbleException("Hash function not working properly") # CHECKER METHODS # @@ -801,13 +815,12 @@ async def test_search(di: DependencyInjector, registered: bool = False) -> None: @checker.putflag(0) async def putflag_guest(task: PutflagCheckerTaskMessage, di: DependencyInjector) -> str: modelname = fakeid() + stlfile = genfile(task.flag.encode(), "ascii") db = await di.get(ChainDB) # Generate a file with flag in solidname and upload it (unregistered, ascii) session = await di.get(Session) - stlfile = genfile(task.flag.encode(), "ascii") - modelid = await do_upload(session, modelname, stlfile, check=True) - assert modelid is not None + modelid = nnb(await do_upload(session, modelname, stlfile, check=True)) await db.set("info", (modelname, modelid)) return "Model {}.. is kinda sus".format(modelid[:10].decode()) @@ -823,219 +836,171 @@ async def putflag_private( # Generate a file with flag in solidname and upload it (registered, bin) session = await di.get(Session) - userid = await do_auth(session, authstr, check=True, newuser=True) - assert userid is not None - modelid = await do_upload(session, modelname, stlfile, check=True) - assert modelid is not None + userid = nnb(await do_auth(session, authstr, check=True, newuser=True)) + modelid = nnb(await do_upload(session, modelname, stlfile, check=True)) await db.set("info", (modelname, modelid, authstr)) return "User {}.. is kinda sus".format(userid[:10].decode()) @checker.getflag(0) -async def getflag_guest( +async def getflag_unregistered( task: GetflagCheckerTaskMessage, di: DependencyInjector ) -> None: db = await di.get(ChainDB) modelname, modelid = await getdb(db, "info") - # Retrieve flag file info via search and ensure flag's included + # Search for flag file and verify flag is included session = await di.get(Session) - resp = await do_search(session, modelname, download=True, check=True) - assert resp is not None - assert_in(task.flag.encode(), resp[0], "Flag is missing from stl info") - assert_in(task.flag.encode(), resp[1], "Flag is missing from stl file") + sresp = nnt(await do_search(session, modelname, download=True, check=True)) + assert_in(task.flag.encode(), sresp[0] + sresp[1], "Failed to retrieve flag") @checker.getflag(1) -async def getflag_private( +async def getflag_registered( task: GetflagCheckerTaskMessage, di: DependencyInjector ) -> None: db = await di.get(ChainDB) modelname, modelid, authstr = await getdb(db, "info") - # Retrieve private flag file info via search / list and ensure flag's included + # Authenticate, get flag via search and list session = await di.get(Session) await do_auth(session, authstr, check=True, newuser=False) - search_resp = await do_search(session, modelname, download=True, check=True) - assert search_resp is not None - assert_in(task.flag.encode(), search_resp[0], "Flag is missing from stl info") - assert_in(task.flag.encode(), search_resp[1], "Flag is missing from stl file") - list_resp = await do_list(session, check=True) - assert list_resp is not None - assert_in(task.flag.encode(), list_resp, "Flag is missing from list") + sresp = nnt(await do_search(session, modelname, download=True, check=True)) + assert_in(task.flag.encode(), sresp[0] + sresp[1], "Failed to retrieve flag") + lresp = nnb(await do_list(session, check=True)) + assert_in(task.flag.encode(), lresp, "Failed to retrieve flag") -@checker.putnoise(0, 1) -async def putnoise_guest( +@checker.putnoise(0) +async def putnoise_unregistered( task: PutnoiseCheckerTaskMessage, di: DependencyInjector ) -> None: modelname, solidname = fakeids(2) + stlfile, solidname = getfile("ascii" if randbool() else "bin") db = await di.get(ChainDB) - # Generate a random file and upload it (unregistered, bin / ascii) + # Upload file for later checking session = await di.get(Session) - stlfile = genfile(solidname, "ascii" if task.variant_id == 0 else "bin") modelid = await do_upload(session, modelname, stlfile, check=True) await db.set("info", (modelid, modelname, solidname, stlfile)) -@checker.putnoise(2, 3) -async def putnoise_priv( +@checker.putnoise(1) +async def putnoise_registered( task: PutnoiseCheckerTaskMessage, di: DependencyInjector ) -> None: modelname, solidname, authstr = fakeids(3) + stlfile, solidname = getfile("ascii" if randbool() else "bin") db = await di.get(ChainDB) - # Generate a random file and upload it (registered, bin / ascii) + # Upload private file for later checking session = await di.get(Session) - stlfile = genfile(solidname, "ascii" if task.variant_id == 0 else "bin") await do_auth(session, authstr, check=True, newuser=True) modelid = await do_upload(session, modelname, stlfile, check=True) await db.set("info", (modelid, modelname, solidname, stlfile, authstr)) -@checker.getnoise(0, 1) -async def getnoise_guest( +@checker.getnoise(0) +async def getnoise_unregistered( task: GetnoiseCheckerTaskMessage, di: DependencyInjector ) -> None: db = await di.get(ChainDB) modelid, modelname, solidname, stlfile = await getdb(db, "info") + ref = parse_stlinfo(stlfile) - # Retrieve noise file by name via search session = await di.get(Session) - await check_in_search( - session, - modelname, - [modelname, solidname, stlfile, modelid], - download=True, + # check that search works on persisted file + expected = [modelname, solidname, stlfile, modelid] + info, stl = await check_in_search(session, modelname, expected, download=True) + # check that persisted file info is still valid + check_stlinfo( + session.logger, + info, + ref, + ref_modelname=modelname, + ref_modelid=modelid, + ref_solidname=solidname, ) -@checker.getnoise(2, 3) -async def getnoise_priv( +@checker.getnoise(1) +async def getnoise_registered( task: GetnoiseCheckerTaskMessage, di: DependencyInjector ) -> None: db = await di.get(ChainDB) modelid, modelname, solidname, stlfile, authstr = await getdb(db, "info") + ref = parse_stlinfo(stlfile) - # Retrieve noise file by name via search and search (registered) session = await di.get(Session) + # check that auth works and tells us we are logging in again await do_auth(session, authstr, check=True, newuser=False) - await check_in_search( - session, - modelname, - [modelname, solidname, stlfile, modelid], - download=True, + # check that search works on persisted file + expected = [modelname, solidname, stlfile, modelid] + info, stl = await check_in_search(session, modelname, expected, download=True) + # check that persisted file info is still valid + check_stlinfo( + session.logger, + info, + ref, + ref_modelname=modelname, + ref_modelid=modelid, + ref_solidname=solidname, ) - await check_listed(session, [modelname, solidname, modelid]) + # check that list works on persisted file + await check_listed(session, [modelname, modelid + b"-", solidname]) @checker.havoc(0) -async def havoc_good_upload_guest_ascii(di: DependencyInjector) -> None: - await test_good_upload(di, filetype="ascii", register=False) +async def havoc_good_upload_ascii(di: DependencyInjector) -> None: + await test_good_upload(di, "ascii") @checker.havoc(1) -async def havoc_good_upload_guest_bin(di: DependencyInjector) -> None: - await test_good_upload(di, filetype="bin", register=False) - - -@checker.havoc(2) -async def havoc_good_upload_priv_ascii(di: DependencyInjector) -> None: - await test_good_upload(di, filetype="ascii", register=True) - - -@checker.havoc(3) -async def havoc_good_upload_priv_bin(di: DependencyInjector) -> None: - await test_good_upload(di, filetype="bin", register=True) - - -@checker.havoc(4) async def havoc_bad_upload_ascii_v1(di: DependencyInjector) -> None: await test_bad_upload(di, "ascii", 1) -@checker.havoc(5) +@checker.havoc(2) async def havoc_bad_upload_ascii_v2(di: DependencyInjector) -> None: await test_bad_upload(di, "ascii", 2) -@checker.havoc(6) +@checker.havoc(3) async def havoc_bad_upload_ascii_v3(di: DependencyInjector) -> None: await test_bad_upload(di, "ascii", 3) -@checker.havoc(7) +@checker.havoc(4) +async def havoc_good_upload_bin(di: DependencyInjector) -> None: + await test_good_upload(di, "bin") + + +@checker.havoc(5) async def havoc_bad_upload_bin_v1(di: DependencyInjector) -> None: await test_bad_upload(di, "bin", 1) -@checker.havoc(8) +@checker.havoc(6) async def havoc_bad_upload_bin_v2(di: DependencyInjector) -> None: await test_bad_upload(di, "bin", 2) -@checker.havoc(9) +@checker.havoc(7) async def havoc_bad_upload_bin_v3(di: DependencyInjector) -> None: await test_bad_upload(di, "bin", 3) -@checker.havoc(10) +@checker.havoc(8) async def havoc_bad_upload_garbage(di: DependencyInjector) -> None: await test_bad_upload(di, "garbage", 1) -@checker.havoc(11) -async def havoc_bad_upload_garbage_tiny(di: DependencyInjector) -> None: - await test_bad_upload(di, "garbage-tiny", 1) - - -@checker.havoc(12) -async def havoc_test_search_guest(di: DependencyInjector) -> None: - await test_search(di, registered=False) - - -@checker.havoc(13) -async def havoc_test_search_priv(di: DependencyInjector) -> None: - await test_search(di, registered=True) - - -@checker.havoc(14) -async def havoc_test_list_guest(di: DependencyInjector) -> None: - # Ensure that list does not work for unregistered users - session = await di.get(Session) - resp = await do_list(session, check=False) - if resp is not None: - session.logger.critical("Unregistered user can run list without ERR!") - raise MumbleException("User authentication not working properly") - - -@checker.havoc(15) -async def havoc_fluff_upload(di: DependencyInjector) -> None: - if len(extra_models) == 0: - return - model = rand.choice(extra_models) - stlfile = open(model, "rb").read() - modelname = fakeid() - if model.endswith("-bin.stl"): - solidname = fakeid(minlen=15, maxlen=15) - stlfile = solidname + stlfile[15:] # replaces "OpenSCAD Model\n" - else: - solidname = fakeid() - stlfile = stlfile.replace(b"OpenSCAD_Model", solidname) - - # Simple Upload - session = await di.get(Session) - if randbool(): - await do_auth(session, fakeid(), check=True, newuser=True) - modelid = await do_upload(session, modelname, stlfile, check=True) - assert modelid is not None - await check_in_search( - session, modelname, [modelname, solidname, modelid, stlfile], download=True - ) +@checker.havoc(9) +async def havoc_hash_collision(di: DependencyInjector) -> None: + await test_hash_collision(di) @checker.exploit(0) @@ -1099,6 +1064,7 @@ async def exploit_hash_overflow( task: ExploitCheckerTaskMessage, di: DependencyInjector ) -> None: searcher = await di.get(FlagSearcher) + logger = await di.get(LoggerAdapter) assert task.attack_info is not None target_prefix = task.attack_info.split()[1][:-2].encode() @@ -1124,6 +1090,9 @@ async def exploit_hash_overflow( if flag := searcher.search_flag(resp): return flag + logger.debug(f"Searching for user with prefix: {target_prefix!r}") + logger.debug(f"Possible users: {users!r}") + # Login as each private user for userhash in users: if not userhash.startswith(target_prefix): @@ -1134,6 +1103,7 @@ async def exploit_hash_overflow( # Authenticate and check if the user is new session = await di.get(Session) + logger.debug(f"Trying to login as user: {userhash!r}") if not await do_auth(session, authstr, check=False, newuser=False): await session.exit() # We dont raise an exception, because it could be that user dir was cleaned diff --git a/service/entrypoint.sh b/service/entrypoint.sh @@ -12,5 +12,5 @@ while [ 1 ]; do done & CMD="ncat --keep-open --listen -p 9000 --max-conns 4000 \ ---no-shutdown --wait 10s --idle-timeout 180s --exec /service/build/stldoctor" +--no-shutdown --wait 10s --exec /service/build/stldoctor" su -s /bin/sh -c "$CMD" service diff --git a/service/src/main.c b/service/src/main.c @@ -5,6 +5,7 @@ #include <fcntl.h> #include <time.h> #include <errno.h> +#include <signal.h> #include <sys/stat.h> #include <sys/file.h> @@ -454,6 +455,12 @@ cleanexit() free(resultdir); } +void +timeout() +{ + die("time's up!\n"); +} + int main() { @@ -461,6 +468,9 @@ main() int exit, i, cmdlen; char *cp, *arg; + signal(SIGALRM, timeout); + alarm(3); + if (!(envstr = getenv("RESULTDIR"))) die("RESULTDIR not defined\n"); diff --git a/src/main.c b/src/main.c @@ -5,6 +5,7 @@ #include <fcntl.h> #include <time.h> #include <errno.h> +#include <signal.h> #include <sys/stat.h> #include <sys/file.h> @@ -458,6 +459,12 @@ cleanexit() free(resultdir); } +void +timeout() +{ + die("time's up!\n"); +} + int main() { @@ -465,6 +472,9 @@ main() int exit, i, cmdlen; char *cp, *arg; + signal(SIGALRM, timeout); + alarm(180); + if (!(envstr = getenv("RESULTDIR"))) die("RESULTDIR not defined\n");