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 64e9b2ad130c0cf28797c3530683fc1cc6b0e9d3
parent 452885a387b3a1613defa378cee79eb97e7b4fc8
Author: Louis Burda <quent.burda@gmail.com>
Date:   Wed, 19 May 2021 00:53:18 +0200

enhanced checker functionality and minor changes / fixes in documentation and src

Diffstat:
Mchecker/src/checker.py | 232+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mchecker/src/requirements.txt | 5+++--
Mdocumentation/README.md | 23+++++++++++++++++++++--
Mservice/src/main.c | 8++++----
Mservice/src/stlfile.c | 2+-
Mservice/src/test.sh | 5++++-
Mservice/src/tests/sample-binary.stl | 0
Dservice/test.stl | 0
8 files changed, 148 insertions(+), 127 deletions(-)

diff --git a/checker/src/checker.py b/checker/src/checker.py @@ -1,82 +1,96 @@ #!/usr/bin/env python3 from enochecker import BaseChecker, BrokenServiceException, EnoException, run from enochecker.utils import SimpleSocket, assert_equals, assert_in -import random -import string - -#### Checker Tenets -# A checker SHOULD not be easily identified by the examination of network traffic -# => satisfied, because checker uses regular user interface and picks strings from a wordlist -# to appear more human (TODO) -# A checker SHOULD use unusual, incorrect or pseudomalicious input to detect network filters -# => satisfied, send various garbage bytes for model name and file contents (TODO) -#### - -samplestl = """ -solid - facet normal 1.0 0 0 - outer loop - vertex 0 1 0 - vertex 0 1 1 - vertex 0 0 1 - endloop - endfacet - facet normal 0 0 1.0 - outer loop - vertex 1 0 0 - vertex 1 1 0 - vertex 0 1 0 - endloop - endfacet -endsolid -""" +import random, string, struct, logging +import numpy as np + +logging.getLogger("faker").setLevel(logging.WARNING) +from faker import Faker + +fake = Faker(["en_US"]) class STLDoctorChecker(BaseChecker): - """ - Change the methods given here, then simply create the class and .run() it. - Magic. - A few convenient methods and helpers are provided in the BaseChecker. - ensure_bytes and ensure_unicode to make sure strings are always equal. - As well as methods: - self.connect() connects to the remote server. - self.get and self.post request from http. - self.chain_db is a dict that stores its contents to a mongodb or filesystem. - conn.readline_expect(): fails if it's not read correctly - To read the whole docu and find more goodies, run python -m pydoc enochecker - (Or read the source, Luke) - """ - - ##### EDIT YOUR CHECKER PARAMETERS flag_variants = 1 noise_variants = 1 - havoc_variants = 0 + havoc_variants = 4 service_name = "stldoctor" port = 9000 - ##### END CHECKER PARAMETERS def login_user(self, conn: SimpleSocket, password: str): self.debug("Sending command to login.") conn.write(f"login\n{password}\n") conn.readline_expect(b"logged in!", read_until=b"$", exception_message="Failed to log in") - def generate_file(self, filetype: str = "ascii", extra: str = ""): + def binwrite(conn: SimpleSocket, buf: bytes): + conn.sock.sendall(buf) + + def fake_filename(self): + allowed = "abcdefghijklmopqrstuvwxyz0123456789-+.!" + return "".join([c for c in fake.name().lower().replace(" ", "-") if c in allowed][:60]).ljust(5, "!") + + def generate_ascii_file(self, solidname: str): + if solidname != "": + content = f"solid {solidname}\n" + else: + content = "solid\n" + facet_count = int(random.random() * 30) + 4 + for fi in range(facet_count): + content += "facet normal " + vs = [[random.random() for i in range(3)] for k in range(3)] + norm = np.cross(np.subtract(vs[1], vs[0]), np.subtract(vs[2],vs[0])) + norm = norm / np.linalg.norm(norm) + content += " ".join([f"{v:.2f}" for v in norm]) + "\n" + content += "outer loop\n" + for i in range(3): + content += "vertex " + " ".join([f"{v:.2f}" for v in vs[i]]) + "\n" + content += "endloop\n" + content += "endfacet\n" + if solidname != b"": + content += f"endsolid {solidname}\n" + else: + content += "endsolid\n" + + return content.encode("latin1") + + def generate_bin_file(self, solidname: str): + if len(solidname.encode()) > 78: + raise EnoException("Solidname to embed in header is larger than header itself") + if solidname != "": + content = b"#" + solidname.encode("ascii").ljust(78, b"\x00") + b"\x00" + else: + content = b"#" + b"\x00" * 79 + facet_count = int(random.random() * 30) + 4 + content += struct.pack("<I", facet_count) + for fi in range(facet_count): + vs = [[random.random() for i in range(3)] for k in range(3)] + norm = np.cross(np.subtract(vs[1], vs[0]), np.subtract(vs[2],vs[0])) + for i in range(3): + content += struct.pack("<f", norm[i]) + for k in range(3): + for i in range(3): + content += struct.pack("<f", vs[k][i]) + content += b"\x00\x00" + return content + + def generate_file(self, filetype: str, solidname: str): if filetype == "ascii": - # TODO handle extra as solidname and gen randomly - return samplestl + return self.generate_ascii_file(solidname = solidname) elif filetype == "bin": - # TODO handle extra as header - return samplestl # TODO: this is not a binary STL! + return self.generate_bin_file(solidname = solidname) else: raise EnoException("Invalid file type supplied"); - def putfile(self, conn: SimpleSocket, solidname = "TODO", modelname = "TODO"): + def putfile(self, conn: SimpleSocket, solidname: str, modelname: str, filetype: str): # Generate file contents - stlfile = samplestl + stlfile = self.generate_file(filetype = filetype, solidname = solidname) # Upload file self.debug("Sending command to submit file") - conn.write(f"submit\n{len(stlfile)}\n{stlfile}{modelname}\n") - conn.read_until(b"with ID ") + conn.write("submit\n") + conn.write(f"{len(stlfile)}\n") + binwrite(conn, stlfile) + conn.write(f"{modelname}\n") + self.debug(b":::RESPONSE:::\n" + conn.read_until(b"with ID ")) # Parse ID fileid = conn.read_until(b"!") @@ -114,6 +128,26 @@ class STLDoctorChecker(BaseChecker): def postdb(self, vdict): self.chain_db = vdict + def havoc_upload(self, filetype: str, registered: bool): + conn = self.openconn() + if registered: + pass # TODO: auth + modelname = self.fake_filename()[:50] + modelname += "".join([chr(127 + int(random.random() * 128)) for i in range(10)]) # noise + contents, mid = self.putfile(conn, filetype = filetype, modelname = modelname, solidname = self.fake_filename()) + resp = self.getfile(conn, modelname = modelname) + assert_in(modelname.encode(), resp, f"Model name '{modelname}' not returned / correctly parsed") + assert_in(solidname.encode(), resp, f"Solid name '{modelname}' not returned / correctly parsed") + assert_in(contents, resp, f"STL File contents not returned / correctly parsed") + self.closeconn(conn) + + conn = self.openconn() + resp = self.getfile(conn, modelname = modelname) + assert_in(modelname.encode(), resp, f"Model name '{modelname}' not returned / correctly parsed") + assert_in(solidname.encode(), resp, f"Solid name '{modelname}' not returned / correctly parsed") + assert_in(contents, resp, f"STL File contents not returned / correctly parsed") + self.closeconn(conn) + def openconn(self): self.debug("Connecting to service") conn = self.connect() @@ -126,96 +160,60 @@ class STLDoctorChecker(BaseChecker): conn.close() def putflag(self): # type: () -> None - """ - This method stores a flag in the service. - In case multiple flags are provided, self.variant_id gives the appropriate index. - The flag itself can be retrieved from self.flag. - On error, raise an Eno Exception. - :raises EnoException on error - :return this function can return a result if it wants - if nothing is returned, the service status is considered okay. - the preferred way to report errors in the service is by raising an appropriate enoexception - """ if self.variant_id == 0: conn = self.openconn() - modelname = self.flag - stlfile, fileid = self.putfile(conn, modelname = modelname) + modelname = self.fake_filename() + stlfile, fileid = self.putfile(conn, solidname = self.flag, modelname = modelname, filetype = "ascii") self.closeconn(conn) - self.chain_db = { "fileid": fileid, "modelname": modelname } + self.postdb({ "fileid": fileid, "modelname": modelname }) else: - raise EnoException("Wrong variant_id provided") + raise EnoException("Invalid variant_id provided") def getflag(self): # type: () -> None - """ - This method retrieves a flag from the service. - Use self.flag to get the flag that needs to be recovered and self.round to get the round the flag was placed in. - On error, raise an EnoException. - :raises EnoException on error - :return this function can return a result if it wants - if nothing is returned, the service status is considered okay. - the preferred way to report errors in the service is by raising an appropriate enoexception - """ if self.variant_id == 0: fileid, modelname = self.querydb("fileid", "modelname") conn = self.openconn() resp = self.getfile(conn, modelname) - assert_in(modelname.encode(), resp, "Resulting flag was found to be incorrect") + assert_in(self.flag.encode(), resp, "Resulting flag was found to be incorrect") self.closeconn(conn) else: - raise EnoException("Wrong variant_id provided") + raise EnoException("Invalid variant_id provided") def putnoise(self): # type: () -> None - """ - This method stores noise in the service. The noise should later be recoverable. - The difference between noise and flag is, that noise does not have to remain secret for other teams. - This method can be called many times per round. Check how often using self.variant_id. - On error, raise an EnoException. - :raises EnoException on error - :return this function can return a result if it wants - if nothing is returned, the service status is considered okay. - the preferred way to report errors in the service is by raising an appropriate enoexception - """ if self.variant_id == 0: conn = self.openconn() - modelname = "NOISE" # TODO - contents, fileid = self.putfile(conn, modelname = modelname) + modelname = self.fake_filename() + solidname = self.fake_filename() + contents, fileid = self.putfile(conn, modelname = modelname, solidname = solidname, filetype = "bin") self.closeconn(conn) - self.postdb({ "fileid": fileid, "modelname": modelname, "contents": contents }) + self.postdb({ "fileid": fileid, "modelname": modelname, "solidname": solidname, "contents": contents }) else: - raise EnoException("Wrong variant_id provided") + raise EnoException("Invalid variant_id provided") def getnoise(self): # type: () -> None - """ - This method retrieves noise in the service. - The noise to be retrieved is inside self.flag - The difference between noise and flag is, that noise does not have to remain secret for other teams. - This method can be called many times per round. Check how often using variant_id. - On error, raise an EnoException. - :raises EnoException on error - :return this function can return a result if it wants - if nothing is returned, the service status is considered okay. - the preferred way to report errors in the service is by raising an appropriate enoexception - """ if self.variant_id == 0: - fileid, modelname, contents = self.querydb("fileid", "modelname", "contents") + fileid, modelname, solidname, contents = self.querydb("fileid", "modelname", "solidname", "contents") conn = self.openconn() resp = self.getfile(conn, modelname) - assert_in(contents.encode(), resp, "Noise file content was found to be incorrect") + # assert_in(contents, resp, "File content returned by service found to be incorrect") + assert_in(solidname.encode(), resp, "Solid name returned by service found to be incorrect") + assert_in(modelname.encode(), resp, "Model name returned by service found to be incorrect") self.closeconn(conn) else: - raise EnoException("Wrong variant_id provided") + raise EnoException("Invalid variant_id provided") def havoc(self): # type: () -> None - """ - This method unleashes havoc on the app -> Do whatever you must to prove the service still works. Or not. - On error, raise an EnoException. - :raises EnoException on Error - :return This function can return a result if it wants - If nothing is returned, the service status is considered okay. - The preferred way to report Errors in the service is by raising an appropriate EnoException - """ - return + if self.variant_id == 0: + self.havoc_upload(filetype = 'ascii', registered = False) + elif self.variant_id == 1: + self.havoc_upload(filetype = 'bin', registered = False) + elif self.variant_id == 2: + self.havoc_upload(filetype = 'ascii', registered = True) + elif self.variant_id == 3: + self.havoc_upload(filetype = 'bin', registered = True) + else: + raise EnoException("Invalid variant_id provided"); # TODO! conn = self.openconn() diff --git a/checker/src/requirements.txt b/checker/src/requirements.txt @@ -20,4 +20,6 @@ requests==2.25.1 six==1.15.0 typish==1.9.2 urllib3==1.26.4 -Werkzeug==1.0.1 -\ No newline at end of file +Werkzeug==1.0.1 +numpy==1.20.1 +Faker==8.1.4 diff --git a/documentation/README.md b/documentation/README.md @@ -68,6 +68,24 @@ The checker checks the following behavior: - Ensure file is not listed in query - Register with previous password - Ensure file is listed in query +- Check upload ordering and accessing indeces != 0: + - Open a session + - Upload a file of random, valid contents with random model and solid name + - Upload a different file of random, valid contents with same model name but different solid name + - Open a new session + - Query for same model name and pick 1st entry + - Compare returned solid name with expected (1st upload) + - Query for same model name and pick 2nd entry + - Compare returned solid name with expected (2nd upload) + + +The checker tenets: + +- A checker SHOULD not be easily identified by the examination of network traffic + satisfied, because checker uses regular user interface and picks strings from a wordlist + to appear more human (TODO) +- A checker SHOULD use unusual, incorrect or pseudomalicious input to detect network filters + satisfied, send various garbage bytes for model name and file contents (TODO) The checker does the following to submit the first flagstore's flag: @@ -78,8 +96,9 @@ The checker does the following to submit the first flagstore's flag: The checker does the following to submit the second flagstore's flag: - Open a session -- Use `submit` to upload a file of the encoded, binary STL flag with - a random model name chosen from a wordlist with numbers for +- Register as a premium user +- Use `submit` to upload a binary STL with the flag as its solidname + and a random model name chosen from a wordlist with numbers for collision resistance The checker should not be easily identifiable, since this could allow diff --git a/service/src/main.c b/service/src/main.c @@ -43,7 +43,7 @@ int save_submission(struct parseinfo *info, char *stldata, int stlsize) { DIR *d; - FILE *f; + FILE *f = NULL; char *dirpath = NULL, *infopath = NULL, *modelpath = NULL; dirpath = aprintf("%s/%s-%i", resultdir, info->hash, time(NULL)); @@ -70,9 +70,9 @@ save_submission(struct parseinfo *info, char *stldata, int stlsize) fail: if (f) fclose(f); - remove(infopath); - remove(modelpath); - remove(dirpath); + if (infopath) remove(infopath); + if (modelpath) remove(modelpath); + if (dirpath) remove(dirpath); free(dirpath); free(modelpath); diff --git a/service/src/stlfile.c b/service/src/stlfile.c @@ -282,7 +282,7 @@ parse_file(struct parseinfo *info, char *buf, size_t len) if (!info->solidname) info->solidname = checkp(strdup("")); if (!info->modelname) { - resp = ask("Please enter your model name:\n"); + resp = ask("Please enter your model name: "); if (strlen(resp) < 4) { fprintf(stderr, "Model name is too short!\n"); return FAIL; diff --git a/service/src/test.sh b/service/src/test.sh @@ -46,17 +46,20 @@ if [ "$1" == "stl" ]; then announce "Testing ASCII STL Parsing" ( + echo "echo" echo "submit" cat tests/sample-ascii.stl | wc -c cat tests/sample-ascii.stl + echo "ASCII-testname" ) | checkleaks announce "Testing BIN STL Parsing" ( + echo "echo" echo "submit" cat tests/sample-binary.stl | wc -c cat tests/sample-binary.stl - echo "testname" + echo "BIN-testname" ) | checkleaks elif [ "$1" == "poc" ]; then diff --git a/service/src/tests/sample-binary.stl b/service/src/tests/sample-binary.stl Binary files differ. diff --git a/service/test.stl b/service/test.stl Binary files differ.