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:
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.