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 d9f95490d15da221844b066784149f68db92cc5d
parent 9763bd5c6d2ab498e989b738c35884c9d7bf8d5e
Author: Louis Burda <quent.burda@gmail.com>
Date:   Sat, 26 Jun 2021 13:28:18 +0200

add checking of returned stl info in test_good_upload havocs

Diffstat:
Mchecker/src/checker.py | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mchecker/src/requirements.txt | 1+
Mservice/src/main.c | 2+-
Msrc/main.c | 2+-
Mtests/test.sh | 8++++----
5 files changed, 93 insertions(+), 16 deletions(-)

diff --git a/checker/src/checker.py b/checker/src/checker.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 from enochecker import BaseChecker, BrokenServiceException, EnoException, run -from enochecker.utils import SimpleSocket, assert_equals, assert_in -import math, os, random, string, struct, subprocess, logging, selectors, time, socket +from enochecker.utils import SimpleSocket +import logging, math, os, random, re, socket, string, struct, subprocess, selectors, time import numpy as np +from io import BytesIO +from stl import mesh logging.getLogger("faker").setLevel(logging.WARNING) logging.getLogger("pwnlib").setLevel(logging.WARNING) @@ -56,6 +58,9 @@ def havocid(): idlen = rand.randint(10, 40) return bytes([rand.randint(32, 127) for i in range(idlen)]) +def approx_equal(f1, f2, precision = 2): + return round(f1, precision) == round(f2, precision) + def reverse_hash(hashstr): if type(hashstr) is bytes: hashstr = hashstr.decode() @@ -76,6 +81,20 @@ def parse_int(intstr): except: return None +def parse_float(floatstr): + try: + return float(floatstr) + except: + return None + +def assert_match(data, pattern, exception): + rem = re.search(pattern, data) + if rem is None: + raise exception(f"Expected pattern {pattern} to match {data}") + if len(rem.groups()) > 0: + return rem.group(1) + return rem.group(0) + class STLDoctorChecker(BaseChecker): service_name = "stldoctor" port = 9090 @@ -190,12 +209,39 @@ class STLDoctorChecker(BaseChecker): elif filetype == "bin": return self.genfile_bin(solidname, malformed = malformed) elif filetype == "garbage-tiny": - return ("".join([rand.choice(generic_alphabet) for i in range(rand.randint(3, 8))])).encode() + return bytes([ord(rand.choice(generic_alphabet)) for i in range(rand.randint(3, 8))]) elif filetype == "garbage": - return ("".join([rand.choice(generic_alphabet) for i in range(rand.randint(100, 300))])).encode() + return bytes([ord(rand.choice(generic_alphabet)) for i in range(rand.randint(100, 300))]) else: raise EnoException("Invalid file type supplied") + def parse_stlinfo(self, stlfile): + fakefile = BytesIO() + fakefile.write(stlfile) + fakefile.seek(0) + try: + name, data = mesh.Mesh.load(fakefile) + meshinfo = mesh.Mesh(data, True, name=name, speedups=True) + except Exception as e: + raise BrokenServiceException(f"STL file parsing failed: {e}") + bmin = [math.inf for i in range(3)] + bmax = [-math.inf for i in range(3)] + if len(meshinfo.points) == 0: + raise EnoException("Parsed STL mesh has 0 points!") + for p in meshinfo.points: + for k in range(3): + for i in range(3): + bmin[k] = min(bmin[k], float(p[3*i+k])) + bmax[k] = max(bmax[k], float(p[3*i+k])) + info = { + "points": meshinfo.points, + "bb_origin": bmin, + "bb_size": [bmax[i] - bmin[i] for i in range(3)], + "size": len(stlfile), + "triangle_count": len(meshinfo.points) + } + return info + def openconn(self): conn = self.connect() resp = conn.recvuntil(b'\n' + self.prompt) @@ -296,7 +342,8 @@ class STLDoctorChecker(BaseChecker): stlfile = b"" if download: # Parse file contents conn.recvuntil(b"Here you go.. (") - size = parse_int(conn.recvuntil(b"B)\n")[:-3]) + resp = conn.recvuntil(b"B)\n")[:-3] + size = parse_int(resp) if size is None: raise BrokenServiceException(f"Received invalid download size, response:\n{resp}") @@ -342,6 +389,29 @@ class STLDoctorChecker(BaseChecker): raise BrokenServiceException(f"Unexpectedly {modelname} info contains one of {includes}: {combined}") return resp + def check_stlinfo(self, resp, ref_info): + size = parse_int(assert_match(resp, b"File Size: (.*)\n", BrokenServiceException)) + if not size or size != ref_info["size"]: + raise BrokenServiceException(f"STL info returned no / invalid file size: {size} != {ref_info['size']}") + + triangle_count = parse_int(assert_match(resp, b"Triangle Count: (.*)\n", BrokenServiceException)) + if not triangle_count or triangle_count != ref_info["triangle_count"]: + raise BrokenServiceException(f"STL info returned no / invalid triangle count: {triangle_count} != {ref_info['triangle_count']}") + + bb_size_str = assert_match(resp, b"Bounding Box Size: (.*)\n", BrokenServiceException) + bb_size = [parse_float(v) for v in bb_size_str.split(b" x ")] + if None in bb_size: + raise BrokenServiceException(f"STL info returned invalid bounding box size: {bb_size_str}") + if False in [approx_equal(bb_size[i], ref_info["bb_size"][i]) for i in range(3)]: + raise BrokenServiceException(f"Returned bounding box size is too far off: (REF) {ref_info['bb_size']} {bb_size}") + + bb_origin_str = assert_match(resp, b"Bounding Box Origin: (.*)\n", BrokenServiceException) + bb_origin = [parse_float(v) for v in bb_origin_str.split(b" x ")] + if None in bb_origin: + raise BrokenServiceException(f"STL info returned invalid bounding box origin: {bb_origin_str}") + if False in [approx_equal(bb_origin[i], ref_info["bb_origin"][i]) for i in range(3)]: + raise BrokenServiceException(f"Returned bounding box origin is too far off: (REF) {ref_info['bb_origin']} {bb_origin}") + # TEST METHODS # def test_good_upload(self, filetype, register): @@ -351,13 +421,17 @@ class STLDoctorChecker(BaseChecker): authstr = havocid() stlfile = self.genfile(solidname, filetype) + # Calculate properties to test response against + ref_info = self.parse_stlinfo(stlfile) + # Create new session and user and upload file conn = self.openconn() if register: self.do_auth(conn, authstr) modelid = self.do_upload(conn, modelname, stlfile) expected = [modelname, solidname, stlfile, modelid] - resp = self.check_in_search(conn, modelname, expected, download = True) + info, stlfile = self.check_in_search(conn, modelname, expected, download = True) + self.check_stlinfo(info, ref_info) if register: resp = self.check_listed(conn, [modelname, modelid]) self.closeconn(conn) @@ -367,10 +441,12 @@ class STLDoctorChecker(BaseChecker): if register: self.check_not_in_search(conn, modelname, expected, download = True, fail = True) self.do_auth(conn, authstr) - self.check_in_search(conn, modelname, expected, download = True) + info, stlfile = self.check_in_search(conn, modelname, expected, download = True) + self.check_stlinfo(info, ref_info) self.check_listed(conn, [modelid, modelname]) else: - self.check_in_search(conn, modelname, expected, download = True) + info, stlfile = self.check_in_search(conn, modelname, expected, download = True) + self.check_stlinfo(info, ref_info) self.closeconn(conn) @@ -568,8 +644,8 @@ class STLDoctorChecker(BaseChecker): conn.write(b"search \xff\xff\xff\xff\xff0000000000000000\n") conn.recvuntil(self.prompt) conn.write("auth\n") - assert_in(b"Already logged in!", conn.recvuntil(self.prompt), - "Exploit did not set 'loggedin' variable via overflow") + if b"Already logged in!" not in conn.recvuntil(self.prompt): + raise BrokenServiceException("Exploit did not set 'loggedin' variable via overflow") # Get private user hashes via 'list' resp = self.do_list(conn) diff --git a/checker/src/requirements.txt b/checker/src/requirements.txt @@ -4,3 +4,4 @@ eventlet==0.30.2 gunicorn==20.1.0 numpy==1.20.1 Faker==8.1.4 +numpy-stl==2.16.0 diff --git a/service/src/main.c b/service/src/main.c @@ -218,7 +218,7 @@ echo_cmd(const char *arg) void upload_cmd(const char *arg) { - char *end, *contents, *modelname; + char *end, *contents = NULL, *modelname = NULL; const char *resp; size_t len; diff --git a/src/main.c b/src/main.c @@ -218,7 +218,7 @@ echo_cmd(const char *arg) void upload_cmd(const char *arg) { - char *end, *contents, *modelname; + char *end, *contents = NULL, *modelname = NULL; const char *resp; size_t len; diff --git a/tests/test.sh b/tests/test.sh @@ -63,18 +63,18 @@ if [ "$1" == "stl-leaks" ]; then ( echo "echo" echo "upload" + echo "ASCII-testname" cat "$TESTDATA/sample-ascii.stl" | wc -c cat "$TESTDATA/sample-ascii.stl" - echo "ASCII-testname" ) | checkleaks announce "Testing BIN STL Parsing" ( echo "echo" echo "upload" + echo "BIN-testname" cat "$TESTDATA/sample-binary.stl" | wc -c cat "$TESTDATA/sample-binary.stl" - echo "BIN-testname" ) | checkleaks elif [ "$1" == "stl-upload" ]; then @@ -90,9 +90,9 @@ elif [ "$1" == "stl-upload" ]; then ( echo "echo" echo "upload" + echo "$name" cat "$file" | wc -c cat "$file" - echo "$name" ) | connect elif [ "$1" == "auth-upload" ]; then @@ -101,9 +101,9 @@ elif [ "$1" == "auth-upload" ]; then echo "auth test" echo "upload" + echo "testname" cat "$TESTDATA/sample-ascii.stl" | wc -c cat "$TESTDATA/sample-ascii.stl" - echo "testname" ) | connect (