From 13b65f01132c41be9ab8d9f92c2c5ca605c366d8 Mon Sep 17 00:00:00 2001 From: Louis Burda Date: Sat, 29 May 2021 14:24:31 +0200 Subject: changed repo structure and commited releease files such that default docker-compose worklow commands work in testvm --- .dockerignore | 1 + .gitignore | 4 + README.md | 8 + do.sh | 90 ++++++++ service/.dockerignore | 1 - service/.gitignore | 5 +- service/Dockerfile | 22 ++ service/cleaner.sh | 24 ++ service/container/.gitignore | 2 - service/container/Dockerfile | 22 -- service/container/cleaner.sh | 24 -- service/container/data/lastclean | 0 service/container/docker-compose.yml | 8 - service/container/entrypoint.sh | 13 -- service/do.sh | 90 -------- service/docker-compose.yml | 8 + service/entrypoint.sh | 13 ++ service/src/.gitignore | 4 - service/src/Makefile | 1 - service/src/main.c | 27 ++- service/src/patches/flagstore1.diff | 17 -- service/src/patches/flagstore2.diff | 11 - service/src/stlfile.c | 9 +- service/src/stlfile.h | 2 +- service/src/util.c | 8 +- service/src/util.h | 2 +- service/tests/data/evil1.stl | 9 - service/tests/data/flag1.stl | 16 -- service/tests/data/sample-ascii.stl | 16 -- service/tests/data/sample-binary.stl | Bin 134 -> 0 bytes service/tests/test.sh | 200 ----------------- src/.gitignore | 4 + src/Makefile | 21 ++ src/main.c | 376 +++++++++++++++++++++++++++++++ src/msgs/cat_flag | 5 + src/msgs/welcome | 2 + src/patches/flagstore1.diff | 17 ++ src/patches/flagstore2.diff | 11 + src/stlfile.c | 415 +++++++++++++++++++++++++++++++++++ src/stlfile.h | 54 +++++ src/util.c | 146 ++++++++++++ src/util.h | 38 ++++ tests/data/evil1.stl | 9 + tests/data/flag1.stl | 16 ++ tests/data/sample-ascii.stl | 16 ++ tests/data/sample-binary.stl | Bin 0 -> 134 bytes tests/test.sh | 200 +++++++++++++++++ 47 files changed, 1521 insertions(+), 466 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 do.sh delete mode 100644 service/.dockerignore create mode 100644 service/Dockerfile create mode 100644 service/cleaner.sh delete mode 100644 service/container/.gitignore delete mode 100644 service/container/Dockerfile delete mode 100644 service/container/cleaner.sh delete mode 100644 service/container/data/lastclean delete mode 100644 service/container/docker-compose.yml delete mode 100755 service/container/entrypoint.sh delete mode 100644 service/do.sh create mode 100644 service/docker-compose.yml create mode 100755 service/entrypoint.sh delete mode 100644 service/src/.gitignore delete mode 100644 service/src/patches/flagstore1.diff delete mode 100644 service/src/patches/flagstore2.diff delete mode 100644 service/tests/data/evil1.stl delete mode 100644 service/tests/data/flag1.stl delete mode 100644 service/tests/data/sample-ascii.stl delete mode 100644 service/tests/data/sample-binary.stl delete mode 100644 service/tests/test.sh create mode 100644 src/.gitignore create mode 100644 src/Makefile create mode 100644 src/main.c create mode 100644 src/msgs/cat_flag create mode 100644 src/msgs/welcome create mode 100644 src/patches/flagstore1.diff create mode 100644 src/patches/flagstore2.diff create mode 100644 src/stlfile.c create mode 100644 src/stlfile.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 tests/data/evil1.stl create mode 100644 tests/data/flag1.stl create mode 100644 tests/data/sample-ascii.stl create mode 100644 tests/data/sample-binary.stl create mode 100644 tests/test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4baccb8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +patches diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ca3e64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data/* +!data/.keep +.cleansrc +src/.safebuild diff --git a/README.md b/README.md index b201949..64aa7c4 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,11 @@ Enowars5 STLDoctor An STL file inspection service 🔍. + +General +------- + +Prebuilt service files are avilable in `/service`, source in `/src` and +automation via `do.sh`. + +More details to service functionality in `/documentation`! diff --git a/do.sh b/do.sh new file mode 100644 index 0000000..e01873c --- /dev/null +++ b/do.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +SCRIPTPATH="$(dirname $(readlink -f "$0"))" +cd "$SCRIPTPATH" + +makefile=" +all: .cleansrc + +.cleansrc: src/* + bash do.sh cleansrc src service/src + touch .cleansrc +" + +shopt -s expand_aliases +alias pushd="pushd &>/dev/null" +alias popd="popd &>/dev/null" + +if [ "$1" == "compose" ]; then + # ensure built service files are up to date + make --file <(echo "$makefile") + + # forward commands to compose + pushd service + docker-compose ${@:2} + popd +elif [ "$1" == "cleansrc" ]; then + if [ $# -lt 3 ]; then + echo "USAGE: do.sh cleansrc " + exit 0 + fi + + # copy files + src="$2" + dst="$3" + [ -e "$dst" ] && rm -rf "$dst" + mkdir -p "$dst" + cp -r "$src"/{*.c,*.h,Makefile,msgs} "$dst" + + # strip comments + find "$dst" | while read path; do + if [ -f "$path" ]; then + if [ ! -z $(echo "$path" | grep '.[hc]$') ]; then + sed -i -e 's/^\s*\/\*.*\*\/\s*$//g' "$path" # remove /* */ style comments + sed -i -e 's/\s*\/\*.*\*\/\s*/ /g' "$path" # remove /* */ style comments + sed -i -e 's/\/\/.*//g' "$path" # remove // style comments + sed -i -e ':a;N;$!ba;s/\n\{3,\}/\n\n/g' "$path" # collapse multiple newlines + sed -i -e 's/fprintf(\s*stderr\s*,\s*/printf(/g' "$path" # replace fprintf stderr + elif [ "$(basename "$path")" == "Makefile" ]; then + sed -i -e 's/\s*#.*//g' "$path" # remove # style comments + sed -i -e ':a;N;$!ba;s/\n\{3,\}/\n\n/g' "$path" # collapse multiple newlines + fi + fi + done +elif [ "$1" == "test" ]; then + SRCDIR="$PWD/src" DATADIR="$PWD/service/data" bash "tests/test.sh" ${@:2} +elif [ "$1" == "make" ]; then + # build a normal version + pushd src + if [ -e ".safebuild" ]; then + make clean + rm ".safebuild" + fi + make + popd +elif [ "$1" == "make-safe" ]; then + # build a 'safe' version with flagstore patches + + pushd src + make clean + touch ".safebuild" + + for f in $(ls | grep '\.[ch]$'); do + cp "$f" "safe_$f" + done + + git apply patches/flagstore1.diff + git apply patches/flagstore2.diff + + PREFIX="safe_" make + + rm safe_* + popd +else + echo "USAGE: do.sh (compose) [args..]" + echo "EXAMPLES:" + echo " do.sh compose up --build # starts the docker container" + echo " do.sh cleansrc # post-process source files for release" + echo " do.sh make-safe # create patched version of binary" + echo " do.sh test # run a test on the binary" +fi diff --git a/service/.dockerignore b/service/.dockerignore deleted file mode 100644 index 4baccb8..0000000 --- a/service/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -patches diff --git a/service/.gitignore b/service/.gitignore index 5ca3e64..8fce603 100644 --- a/service/.gitignore +++ b/service/.gitignore @@ -1,4 +1 @@ -data/* -!data/.keep -.cleansrc -src/.safebuild +data/ diff --git a/service/Dockerfile b/service/Dockerfile new file mode 100644 index 0000000..c932e66 --- /dev/null +++ b/service/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:18.04 + +RUN apt update && apt install -y --no-install-recommends socat build-essential + +RUN addgroup --system service +RUN adduser --system --ingroup service --uid 1000 service + +COPY entrypoint.sh / +RUN chmod 755 /entrypoint.sh + +COPY cleaner.sh / +RUN chmod 755 /cleaner.sh + +COPY src/ /service/ + +WORKDIR /service/ +RUN make clean && make + +EXPOSE 9000 +ENV RESULTDIR=/data/uploads + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/service/cleaner.sh b/service/cleaner.sh new file mode 100644 index 0000000..bd67705 --- /dev/null +++ b/service/cleaner.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +timeref="/data/lastclean" + +if [ -z "$RESULTDIR" ]; then + echo "RESULTDIR is undefined! skipping cleanup.." + exit 1 +fi + +if [ -f "$timeref" ]; then + files="$(find "$RESULTDIR" -mindepth 1 \! -newer "$timeref")" + echo "$files" | while read path; do + rm -rf "$path" + done + if [ -z "$files" ]; then + filecount=0 + else + filecount=$(echo "$files" | wc -l) + fi + echo "[ $(date +%T) ] Removed $filecount old files!" +fi + +touch "$timeref" + diff --git a/service/container/.gitignore b/service/container/.gitignore deleted file mode 100644 index 8156e3e..0000000 --- a/service/container/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -src/ -data/ diff --git a/service/container/Dockerfile b/service/container/Dockerfile deleted file mode 100644 index c932e66..0000000 --- a/service/container/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM ubuntu:18.04 - -RUN apt update && apt install -y --no-install-recommends socat build-essential - -RUN addgroup --system service -RUN adduser --system --ingroup service --uid 1000 service - -COPY entrypoint.sh / -RUN chmod 755 /entrypoint.sh - -COPY cleaner.sh / -RUN chmod 755 /cleaner.sh - -COPY src/ /service/ - -WORKDIR /service/ -RUN make clean && make - -EXPOSE 9000 -ENV RESULTDIR=/data/uploads - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/service/container/cleaner.sh b/service/container/cleaner.sh deleted file mode 100644 index bd67705..0000000 --- a/service/container/cleaner.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -timeref="/data/lastclean" - -if [ -z "$RESULTDIR" ]; then - echo "RESULTDIR is undefined! skipping cleanup.." - exit 1 -fi - -if [ -f "$timeref" ]; then - files="$(find "$RESULTDIR" -mindepth 1 \! -newer "$timeref")" - echo "$files" | while read path; do - rm -rf "$path" - done - if [ -z "$files" ]; then - filecount=0 - else - filecount=$(echo "$files" | wc -l) - fi - echo "[ $(date +%T) ] Removed $filecount old files!" -fi - -touch "$timeref" - diff --git a/service/container/data/lastclean b/service/container/data/lastclean deleted file mode 100644 index e69de29..0000000 diff --git a/service/container/docker-compose.yml b/service/container/docker-compose.yml deleted file mode 100644 index 1da888f..0000000 --- a/service/container/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: '3' -services: - printdoc: - build: . - volumes: - - ./data/:/data:rw - ports: - - 9090:9000 diff --git a/service/container/entrypoint.sh b/service/container/entrypoint.sh deleted file mode 100755 index b7b6509..0000000 --- a/service/container/entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -mkdir -p "$RESULTDIR" -chown -R service:service "$RESULTDIR" - -while [ 1 ]; do - /cleaner.sh - sleep 200 -done & - -servicecmd='socat -T30 -s TCP-LISTEN:9000,reuseaddr,fork EXEC:"/service/build/stldoctor",raw,pty,echo=0,stderr' - -su -s /bin/sh -c "$servicecmd" service diff --git a/service/do.sh b/service/do.sh deleted file mode 100644 index d0ac5ed..0000000 --- a/service/do.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/sh - -SCRIPTPATH="$(dirname $(readlink -f "$0"))" -cd "$SCRIPTPATH" - -makefile=" -all: .cleansrc - -.cleansrc: src/* - bash do.sh cleansrc src container/src - touch .cleansrc -" - -shopt -s expand_aliases -alias pushd="pushd &>/dev/null" -alias popd="popd &>/dev/null" - -if [ "$1" == "compose" ]; then - # ensure container files are up to date - make --file <(echo "$makefile") - - # forward commands to compose - pushd container - docker-compose ${@:2} - popd -elif [ "$1" == "cleansrc" ]; then - if [ $# -lt 3 ]; then - echo "USAGE: do.sh cleansrc " - exit 0 - fi - - # copy files - src="$2" - dst="$3" - [ -e "$dst" ] && rm -rf "$dst" - mkdir -p "$dst" - cp -r "$src"/{*.c,*.h,Makefile,msgs} "$dst" - - # strip comments - find "$dst" | while read path; do - if [ -f "$path" ]; then - if [ ! -z $(echo "$path" | grep '.[hc]$') ]; then - sed -i -e 's/^\s*\/\*.*\*\/\s*$//g' "$path" # remove /* */ style comments - sed -i -e 's/\s*\/\*.*\*\/\s*/ /g' "$path" # remove /* */ style comments - sed -i -e 's/\/\/.*//g' "$path" # remove // style comments - sed -i -e ':a;N;$!ba;s/\n\{3,\}/\n\n/g' "$path" # collapse multiple newlines - sed -i -e 's/fprintf(\s*stderr\s*,\s*/printf(/g' "$path" # replace fprintf stderr - elif [ "$(basename "$path")" == "Makefile" ]; then - sed -i -e 's/\s*#.*//g' "$path" # remove # style comments - sed -i -e ':a;N;$!ba;s/\n\{3,\}/\n\n/g' "$path" # collapse multiple newlines - fi - fi - done -elif [ "$1" == "test" ]; then - SRCDIR="$PWD/src" DATADIR="$PWD/container/data" bash "tests/test.sh" ${@:2} -elif [ "$1" == "make" ]; then - # build a normal version - pushd src - if [ -e ".safebuild" ]; then - make clean - rm ".safebuild" - fi - make - popd -elif [ "$1" == "make-safe" ]; then - # build a 'safe' version with flagstore patches - - pushd src - make clean - touch ".safebuild" - - for f in $(ls | grep '\.[ch]$'); do - cp "$f" "safe_$f" - done - - git apply patches/flagstore1.diff - git apply patches/flagstore2.diff - - PREFIX="safe_" make - - rm safe_* - popd -else - echo "USAGE: do.sh (compose) [args..]" - echo "EXAMPLES:" - echo " do.sh compose up --build # starts the docker container" - echo " do.sh cleansrc # post-process source files for release" - echo " do.sh make-safe # create patched version of binary" - echo " do.sh test # run a test on the binary" -fi diff --git a/service/docker-compose.yml b/service/docker-compose.yml new file mode 100644 index 0000000..1da888f --- /dev/null +++ b/service/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + printdoc: + build: . + volumes: + - ./data/:/data:rw + ports: + - 9090:9000 diff --git a/service/entrypoint.sh b/service/entrypoint.sh new file mode 100755 index 0000000..b7b6509 --- /dev/null +++ b/service/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +mkdir -p "$RESULTDIR" +chown -R service:service "$RESULTDIR" + +while [ 1 ]; do + /cleaner.sh + sleep 200 +done & + +servicecmd='socat -T30 -s TCP-LISTEN:9000,reuseaddr,fork EXEC:"/service/build/stldoctor",raw,pty,echo=0,stderr' + +su -s /bin/sh -c "$servicecmd" service diff --git a/service/src/.gitignore b/service/src/.gitignore deleted file mode 100644 index 5f14e4d..0000000 --- a/service/src/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -stldoctor -*.o -vgcore.* -safe_* diff --git a/service/src/Makefile b/service/src/Makefile index d7732b3..2fee8c4 100644 --- a/service/src/Makefile +++ b/service/src/Makefile @@ -1,6 +1,5 @@ CFLAGS = -g -I . -# fortify source code CFLAGS += -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 LDFLAGS = -Wl,-z,now -Wl,-z,relro diff --git a/service/src/main.c b/service/src/main.c index de2bd48..d76ceb8 100644 --- a/service/src/main.c +++ b/service/src/main.c @@ -144,21 +144,21 @@ upload_cmd(const char *arg) bufp = ask("How large is your file? "); len = strtoul(bufp, &end, 10); if (len <= 0 || len >= MAXFILESIZE || *end) { - fprintf(stderr, "Invalid file length!\n"); + printf("Invalid file length!\n"); return; } printf("Ok! Im listening..\n"); contents = checkp(malloc(len + 1)); if (fread(contents, 1, len, stdin) != len) { - fprintf(stderr, "Hm, I'm missing some bytes.. try again!\n"); + printf("Hm, I'm missing some bytes.. try again!\n"); goto cleanup; } contents[len] = '\0'; if ((cached.valid = parse_file(&cached, contents, len))) { if (save_submission(&cached, contents, len) != OK) - fprintf(stderr, "Failed to save your submission!\n"); + printf("Failed to save your submission!\n"); else printf("Your file was saved with ID %s!\n", cached.hash); } @@ -180,7 +180,7 @@ search_cmd(const char *arg) if (arg && !strcmp(arg, "last")) { if (!cached.valid) { - fprintf(stderr, "No cached info report available\n"); + printf("No cached info report available\n"); return; } hash = cached.hash; @@ -201,12 +201,12 @@ search_cmd(const char *arg) } if (i == 0) { - fprintf(stderr, "Sorry, couldnt find a matching scan result!\n"); + printf("Sorry, couldnt find a matching scan result!\n"); goto cleanup; } else { which = strtoul(ask("Which of these results? "), &end, 10); if (which >= i || which < 0 || *end) { - fprintf(stderr, "Invalid index!\n"); + printf("Invalid index!\n"); goto cleanup; } } @@ -224,9 +224,8 @@ search_cmd(const char *arg) } } - /* file got cleaned up during race condition by background task */ if (!scandir) { - fprintf(stderr, "Selected result spontaneously combusted!\n"); + printf("Selected result spontaneously combusted!\n"); goto cleanup; } @@ -271,7 +270,7 @@ list_cmd(const char *arg) DIR *d; if (!loggedin) { - fprintf(stderr, "Not logged in!\n"); + printf("Not logged in!\n"); return; } @@ -283,7 +282,7 @@ list_cmd(const char *arg) path = aprintf("%s/%s/info", resultdir, de->d_name); if ((f = fopen(path, "r"))) { if (load_info(&info, f) != OK) - fprintf(stderr, "Failed to read saved file info!\n"); + printf("Failed to read saved file info!\n"); else print_info(&info); fclose(f); @@ -301,7 +300,7 @@ auth_cmd(const char *arg) int ret; if (loggedin) { - fprintf(stderr, "Already logged in!\n"); + printf("Already logged in!\n"); return; } @@ -313,7 +312,7 @@ auth_cmd(const char *arg) } else if (ret && errno == EEXIST) { printf("Success!\nWelcome back!\n"); } else { - fprintf(stderr, "Auth failed!\n"); + printf("Auth failed!\n"); return; } @@ -339,7 +338,7 @@ main() int exit, i, cmdlen; if (!(resultdir = checkp(strdup(getenv("RESULTDIR"))))) { - fprintf(stderr, "RESULTDIR not defined\n"); + printf("RESULTDIR not defined\n"); return 1; } @@ -371,6 +370,6 @@ main() } if (i == ARRSIZE(commands) && strlen(cmd) != 0) - fprintf(stderr, "No such command!\n"); + printf("No such command!\n"); } } diff --git a/service/src/patches/flagstore1.diff b/service/src/patches/flagstore1.diff deleted file mode 100644 index f0f8d4a..0000000 --- a/service/src/patches/flagstore1.diff +++ /dev/null @@ -1,17 +0,0 @@ ---- a/service/src/safe_util.c -+++ b/service/src/safe_util.c -@@ -78,13 +78,12 @@ void - freadstr(FILE *f, char **dst) - { - size_t start, len, tmp; -- char c; - - /* VULN #1: BAD CAST */ - /* see documentation/README.md for more details */ - - start = ftell(f); -- for (len = 0; (c = fgetc(f)) != EOF && c; len++); -+ for (len = 0; fgetc(f) > 0; len++); - fseek(f, start, SEEK_SET); - - *dst = checkp(calloc(1, len + 1)); diff --git a/service/src/patches/flagstore2.diff b/service/src/patches/flagstore2.diff deleted file mode 100644 index b34a0c0..0000000 --- a/service/src/patches/flagstore2.diff +++ /dev/null @@ -1,11 +0,0 @@ ---- a/service/src/safe_util.c -+++ b/service/src/safe_util.c -@@ -58,7 +58,7 @@ mhash(const char *str, int len) - srand(v); - - for (bp = buf, i = 0; i < MHASHLEN / 2; i++) -- bp += sprintf(bp, "%02x", str[i % len] ^ (rand() % 256)); -+ bp += sprintf(bp, "%02x", (unsigned char) str[i % len] ^ (rand() % 256)); - - return buf; - } diff --git a/service/src/stlfile.c b/service/src/stlfile.c index 88fc430..7b37df4 100644 --- a/service/src/stlfile.c +++ b/service/src/stlfile.c @@ -98,7 +98,7 @@ consume_keyword(char **start) for (i = 0; i < ARRSIZE(kwmap); i++) { len = strlen(kwmap[i].str); if (!strncmp(kwmap[i].str, bp, len) && (!bp[len] || isws(bp[len]))) { - // printf("GOT: %s\n", kwmap[i].str); + *start = bp + len + (bp[len] ? 1 : 0); return kwmap[i].code; } @@ -108,7 +108,7 @@ consume_keyword(char **start) } #define PARSE_FAIL(...) \ - do { fprintf(stderr, "FORMAT ERR: " __VA_ARGS__); goto fail; } while (0) + do { printf("FORMAT ERR: " __VA_ARGS__); goto fail; } while (0) int parse_file_ascii(struct parseinfo *info, char *buf, size_t len) @@ -272,13 +272,12 @@ parse_file(struct parseinfo *info, char *buf, size_t len) if (info->valid) free_info(info); if (len < 7) { - fprintf(stderr, "File too small!\n"); + printf("File too small!\n"); return FAIL; } info->filesize = len; - /* check bin vs ascii with first keyword */ for (bp = buf; isws(*bp); bp++); status = !strncmp("solid", bp, 5) && isws(bp[5]) ? parse_file_ascii(info, buf, len) @@ -290,7 +289,7 @@ parse_file(struct parseinfo *info, char *buf, size_t len) if (!info->modelname) { resp = ask("Please enter your model name: "); if (strlen(resp) < 4) { - fprintf(stderr, "Model name is too short!\n"); + printf("Model name is too short!\n"); return FAIL; } info->modelname = checkp(strdup(resp)); diff --git a/service/src/stlfile.h b/service/src/stlfile.h index d321282..11b7f66 100644 --- a/service/src/stlfile.h +++ b/service/src/stlfile.h @@ -51,4 +51,4 @@ int load_info(struct parseinfo *info, FILE *f); void print_info(struct parseinfo *info); void free_info(struct parseinfo *info); -#endif /* STLFILE_H */ +#endif diff --git a/service/src/util.c b/service/src/util.c index ce22c4e..140d08b 100644 --- a/service/src/util.c +++ b/service/src/util.c @@ -13,7 +13,7 @@ die(const char *fmtstr, ...) va_list ap; va_start(ap, fmtstr); - vfprintf(stderr, fmtstr, ap); + vprintf(fmtstr, ap); va_end(ap); exit(EXIT_FAILURE); @@ -49,9 +49,6 @@ mhash(const char *str, int len) int i, k, v; char c, *bp; - /* VULN #2: BUFFER OVERFLOW */ - /* see documentation/README.md for more details */ - if (len == -1) len = strlen(str); for (v = 0, i = 0; i < len; i++) v += str[i]; @@ -80,9 +77,6 @@ freadstr(FILE *f, char **dst) size_t start, len, tmp; char c; - /* VULN #1: BAD CAST */ - /* see documentation/README.md for more details */ - start = ftell(f); for (len = 0; (c = fgetc(f)) != EOF && c; len++); fseek(f, start, SEEK_SET); diff --git a/service/src/util.h b/service/src/util.h index c0e9064..7b6eed0 100644 --- a/service/src/util.h +++ b/service/src/util.h @@ -35,4 +35,4 @@ float fle32toh(float v); extern int echo; -#endif /* UTIL_H */ +#endif diff --git a/service/tests/data/evil1.stl b/service/tests/data/evil1.stl deleted file mode 100644 index 706e9e2..0000000 --- a/service/tests/data/evil1.stl +++ /dev/null @@ -1,9 +0,0 @@ -solid test -facet normal 0 0 1.0 - outer loop - vertex 1 0 0 - vertex 1 1 0 - vertex 0 1 0 - endloop - endfacet -endsolid diff --git a/service/tests/data/flag1.stl b/service/tests/data/flag1.stl deleted file mode 100644 index f2a3854..0000000 --- a/service/tests/data/flag1.stl +++ /dev/null @@ -1,16 +0,0 @@ -solid ENO{TESTFLAG} - 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 diff --git a/service/tests/data/sample-ascii.stl b/service/tests/data/sample-ascii.stl deleted file mode 100644 index e3c89ef..0000000 --- a/service/tests/data/sample-ascii.stl +++ /dev/null @@ -1,16 +0,0 @@ -solid test - 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 test diff --git a/service/tests/data/sample-binary.stl b/service/tests/data/sample-binary.stl deleted file mode 100644 index 13c02e4..0000000 Binary files a/service/tests/data/sample-binary.stl and /dev/null differ diff --git a/service/tests/test.sh b/service/tests/test.sh deleted file mode 100644 index 4835024..0000000 --- a/service/tests/test.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/sh - -set -e - -if [ -z "$SRCDIR" -o -z "$DATADIR" ]; then - echo "Missing either SRCDIR or DATADIR env vars" - exit 1 -fi - -export RESULTDIR="$DATADIR/uploads" -export ECHO_INPUT=1 - -SCRIPTPATH="$(dirname $(readlink -f "$0"))" -TESTDATA="$SCRIPTPATH/data" - -shopt -s expand_aliases -alias pushd="pushd &>/dev/null" -alias popd="popd &>/dev/null" - -pushd "$SRCDIR" - -announce() { - count=$(echo "$1" | wc -c) - python3 -c " -import math -s = '$1' -c = 80 -print() -print('#'*c) -print('#' + ' '*math.floor((c - len(s))/2-1) + s + ' '*math.ceil((c - len(s))/2-1) + '#') -print('#'*c) -print() - " -} - -checkleaks() { - valgrind --leak-check=full --show-leak-kinds=all ./build/stldoctor 2>&1 | tee /tmp/testlog - if [ -z "$(grep "no leaks are possible" /tmp/testlog)" ]; then - echo "Valgrind exited with errors!" - exit 1 - fi -} - -connect() { - if [ "$RUNTYPE" == "remote" ]; then - nc localhost 9090 - elif [ "$RUNTYPE" == "debug" ]; then - checkleaks - else - ./build/stldoctor - fi -} - -cleanuploads() { - [ ! -z "$RESULTDIR" ] && rm -rf "$RESULTDIR" - mkdir -p "$RESULTDIR" -} - -if [ "$1" == "stl-leaks" ]; then - cleanuploads - - announce "Testing ASCII STL Parsing" - ( - echo "echo" - echo "upload" - 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" - cat "$TESTDATA/sample-binary.stl" | wc -c - cat "$TESTDATA/sample-binary.stl" - echo "BIN-testname" - ) | checkleaks - -elif [ "$1" == "stl-upload" ]; then - cleanuploads - - popd - file="$(realpath $2)" - if [ ! -e "$file" ]; then - echo "Supply a file to upload" - exit 1 - fi - pushd "$SRCDIR" - - name="${3:-samplefile}" - ( - echo "echo" - echo "upload" - cat "$file" | wc -c - cat "$file" - echo "$name" - ) | checkleaks - -elif [ "$1" == "vuln1" ]; then - cleanuploads - - announce "Testing Flagstore 1" - - echo -e "\n--- Uploading target STL ---\n" 1>&2 - ( - echo "echo" - echo "upload" - cat "$TESTDATA/flag1.stl" | wc -c - cat "$TESTDATA/flag1.stl" - echo "N0TaFL4G" - echo "exit" - ) | connect - - echo -e "\n--- Uploading evil STL ---\n" 1>&2 - ( - echo "echo" - echo "upload" - cat "$TESTDATA/evil1.stl" | wc -c - cat "$TESTDATA/evil1.stl" - echo "EV1L" - echo "exit" - ) | connect - - echo -e "\n--- Testing Exploit ---\n" 1>&2 - ( - echo "echo" - - # try index 0 - echo "search" - echo "EV1L" - echo "0" - echo "n" - - echo "search last" - echo "0" - echo "n" - - # try index 1 - echo "search" - echo -e "EV1L" - echo "0" - echo "n" - - echo "search last" - echo "1" - echo "n" - echo "exit" - ) | connect - -elif [ "$1" == "vuln2" ]; then - cleanuploads - - announce "Testing Flagstore 2" - - echo -e "\n--- Uploading target STL ---\n" 1>&2 - ( - echo "echo" - echo "auth test" - echo "upload" - cat "$TESTDATA/flag1.stl" | wc -c - cat "$TESTDATA/flag1.stl" - echo "N0TaFL4G" - echo "exit" - ) | connect - - echo -e "\n--- Testing Exploit ---\n" 1>&2 - ( - echo "echo" - echo -e "search \xff\xff\xff\xff\xff0000000000000000" - echo "auth" - echo "list" - echo "exit" - ) | connect - -elif [ "$1" == "auth-upload" ]; then - cleanuploads - - ( - echo "echo" - - echo "auth test" - echo "upload" - cat "$TESTDATA/sample-ascii.stl" | wc -c - cat "$TESTDATA/sample-ascii.stl" - echo "testname" - ) | connect - - ( - echo "echo" - - echo "auth test" - echo "list" - echo "search testname" - ) | connect -else - connect -fi - -popd diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..5f14e4d --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,4 @@ +stldoctor +*.o +vgcore.* +safe_* diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..d7732b3 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,21 @@ +CFLAGS = -g -I . + +# fortify source code +CFLAGS += -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 +LDFLAGS = -Wl,-z,now -Wl,-z,relro + +.PHONY: all clean + +all: build/stldoctor + +clean: + rm -rf build + +build: + mkdir build + +build/%.o: %.c %.h | build + $(CC) -c -o $@ $< $(CFLAGS) $(LDLIBS) + +build/stldoctor: build/$(PREFIX)stlfile.o build/$(PREFIX)util.o $(PREFIX)main.c | build + $(CC) -o $@ $^ $(CFLAGS) $(LDLIBS) diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..de2bd48 --- /dev/null +++ b/src/main.c @@ -0,0 +1,376 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stlfile.h" +#include "util.h" + +#define MAXFILESIZE 7000 + +struct command { + const char *name; + void (*func)(const char *); + const char *desc; +}; + +int save_submission(struct parseinfo *info, char *data, int len); + +void cat_cmd(const char *arg); +void help_cmd(const char *arg); +void exit_cmd(const char *arg); +void echo_cmd(const char *arg); +void upload_cmd(const char *arg); +void search_cmd(const char *arg); +void list_cmd(const char *arg); +void auth_cmd(const char *arg); + +void cleanexit(); + +struct command commands[] = { + { "cat", cat_cmd, "Cat cmd go prrrrr." }, + { "help", help_cmd, "You already know what this does." }, + { "exit", exit_cmd, "Closes the session." }, + { "echo", echo_cmd, "Repeat after me!" }, + { "upload", upload_cmd, "Upload an STL file to analyze." }, + { "search", search_cmd, "Search for an STL file by model name." }, + { "list", list_cmd, "List your uploaded files." }, + { "auth", auth_cmd, "Login to upload files to a private dir." } +}; + +struct parseinfo cached; +char *resultdir; +int echo = 0, loggedin = 0; + +int +save_submission(struct parseinfo *info, char *stldata, int stlsize) +{ + DIR *d; + FILE *f = NULL; + char *dirpath = NULL, *infopath = NULL, *modelpath = NULL; + + if (loggedin) + dirpath = aprintf("%s/.%s-%i", resultdir, info->hash, time(NULL)); + else + dirpath = aprintf("%s/%s-%i", resultdir, info->hash, time(NULL)); + if (mkdir(dirpath, S_IRWXU | S_IRWXG | S_IRWXO)) goto fail; + + modelpath = aprintf("%s/%s", dirpath, "model"); + if (!(f = fopen(modelpath, "w+"))) goto fail; + if (fwrite(stldata, 1, stlsize, f) != stlsize) goto fail; + fclose(f); + f = NULL; + + infopath = aprintf("%s/%s", dirpath, "info"); + if (!(f = fopen(infopath, "w+"))) goto fail; + if (save_info(info, f) != OK) goto fail; + fclose(f); + f = NULL; + + free(dirpath); + free(modelpath); + free(infopath); + + return OK; + +fail: + if (f) fclose(f); + + if (infopath) remove(infopath); + if (modelpath) remove(modelpath); + if (dirpath) remove(dirpath); + + free(dirpath); + free(modelpath); + free(infopath); + + return FAIL; +} + +void +cat_cmd(const char *arg) +{ + if (arg && !strncmp(arg, "flag", 4)) + dump("msgs/cat_flag"); + else + printf("meow\n"); +} + +void +help_cmd(const char *arg) +{ + int i; + + if (arg) { + for (i = 0; i < ARRSIZE(commands); i++) { + if (!strcmp(commands[i].name, arg)) { + printf("%s\n", commands[i].desc); + return; + } + } + } + + printf("Available commands:\n"); + for (i = 0; i < ARRSIZE(commands); i++) + printf("%s%s", i ? " " : "", commands[i].name); + printf("\n"); +} + +void +exit_cmd(const char *arg) +{ + exit(0); +} + +void +echo_cmd(const char *arg) +{ + echo ^= 1; + printf("Echo is %s\n", echo ? "enabled" : "disabled"); +} + +void +upload_cmd(const char *arg) +{ + const char *bufp; + char *end, *contents; + size_t len; + + bufp = ask("How large is your file? "); + len = strtoul(bufp, &end, 10); + if (len <= 0 || len >= MAXFILESIZE || *end) { + fprintf(stderr, "Invalid file length!\n"); + return; + } + + printf("Ok! Im listening..\n"); + contents = checkp(malloc(len + 1)); + if (fread(contents, 1, len, stdin) != len) { + fprintf(stderr, "Hm, I'm missing some bytes.. try again!\n"); + goto cleanup; + } + contents[len] = '\0'; + + if ((cached.valid = parse_file(&cached, contents, len))) { + if (save_submission(&cached, contents, len) != OK) + fprintf(stderr, "Failed to save your submission!\n"); + else + printf("Your file was saved with ID %s!\n", cached.hash); + } + +cleanup: + free(contents); +} + +void +search_cmd(const char *arg) +{ + char *end, *scandir = NULL, *infopath = NULL, *modelpath = NULL; + int i, which, dirstart, ishidden; + const char *hash, *name; + struct dirent *de; + DIR *d = NULL; + FILE *f = NULL; + size_t size; + + if (arg && !strcmp(arg, "last")) { + if (!cached.valid) { + fprintf(stderr, "No cached info report available\n"); + return; + } + hash = cached.hash; + } else { + hash = mhash(arg ? arg : ask("Model name: "), -1); + } + + if (!(d = opendir(resultdir))) return; + + dirstart = telldir(d); + for (i = 0; (de = readdir(d));) { + name = de->d_name; + if (loggedin && *name == '.' && !strpfcmp(hash, name + 1) + || !loggedin && *name != '.' && !strpfcmp(hash, name)) { + printf("%i : %s\n", i, de->d_name); + i++; + } + } + + if (i == 0) { + fprintf(stderr, "Sorry, couldnt find a matching scan result!\n"); + goto cleanup; + } else { + which = strtoul(ask("Which of these results? "), &end, 10); + if (which >= i || which < 0 || *end) { + fprintf(stderr, "Invalid index!\n"); + goto cleanup; + } + } + + seekdir(d, dirstart); + for (i = 0; (de = readdir(d));) { + name = de->d_name; + if (loggedin && *name == '.' && !strpfcmp(hash, name + 1) + || !loggedin && *name != '.' && !strpfcmp(hash, name)) { + if (i == which) { + scandir = aprintf("%s/%s", resultdir, de->d_name); + break; + } + i++; + } + } + + /* file got cleaned up during race condition by background task */ + if (!scandir) { + fprintf(stderr, "Selected result spontaneously combusted!\n"); + goto cleanup; + } + + infopath = aprintf("%s/%s", scandir, "info"); + if (!(f = fopen(infopath, "r"))) goto cleanup; + free_info(&cached); + if (load_info(&cached, f) != OK) goto cleanup; + fclose(f); + f = NULL; + + print_info(&cached); + + if (strchr(ask("Download the model? "), 'y')) { + modelpath = aprintf("%s/%s", scandir, "model"); + if (!(f = fopen(modelpath, "r"))) goto cleanup; + fseek(f, 0, SEEK_END); + size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size > MAXFILESIZE) goto cleanup; + printf("Here you go.. (%liB)\n", size); + while ((i = getc(f)) != EOF) + putc(i, stdout); + fclose(f); + f = NULL; + } + +cleanup: + if (f) fclose(f); + closedir(d); + free(scandir); + free(infopath); + free(modelpath); +} + +void +list_cmd(const char *arg) +{ + struct dirent *de; + struct parseinfo info; + char *path; + FILE *f; + DIR *d; + + if (!loggedin) { + fprintf(stderr, "Not logged in!\n"); + return; + } + + if (!(d = opendir(resultdir))) return; + + while ((de = readdir(d))) { + if (*de->d_name == '.' && !strchr(".", de->d_name[1])) { + printf(">> %s\n", de->d_name); + path = aprintf("%s/%s/info", resultdir, de->d_name); + if ((f = fopen(path, "r"))) { + if (load_info(&info, f) != OK) + fprintf(stderr, "Failed to read saved file info!\n"); + else + print_info(&info); + fclose(f); + } + free(path); + } + } +} + +void +auth_cmd(const char *arg) +{ + const char *hash; + char *ndir; + int ret; + + if (loggedin) { + fprintf(stderr, "Already logged in!\n"); + return; + } + + hash = mhash(arg ? arg : ask("Enter a password: "), -1); + ndir = aprintf("%s/.%s", resultdir, hash); + ret = mkdir(ndir, S_IRWXU | S_IRWXG | S_IRWXO); + if (!ret) { + printf("Success!\n"); + } else if (ret && errno == EEXIST) { + printf("Success!\nWelcome back!\n"); + } else { + fprintf(stderr, "Auth failed!\n"); + return; + } + + free(resultdir); + resultdir = ndir; + loggedin = 1; + cached.valid = 0; +} + +void +cleanexit() +{ + printf("see you later!\n"); + free_info(&cached); + free(resultdir); +} + +int +main() +{ + const char *cmd; + char *cp, *arg; + int exit, i, cmdlen; + + if (!(resultdir = checkp(strdup(getenv("RESULTDIR"))))) { + fprintf(stderr, "RESULTDIR not defined\n"); + return 1; + } + + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); + + atexit(cleanexit); + + dump("msgs/welcome"); + + exit = 0; + while (!exit) { + errno = 0; + cmd = ask("$ "); + if (!*cmd && errno == EBADMSG) break; + if (!*cmd) continue; + + cp = strchr(cmd, ' '); + arg = cp ? cp + 1 : NULL; + cmdlen = cp ? cp - cmd : strlen(cmd); + + for (i = 0; i < ARRSIZE(commands); i++) { + if (!strncmp(commands[i].name, cmd, cmdlen) + && cmdlen == strlen(commands[i].name)) { + commands[i].func(arg); + break; + } + } + + if (i == ARRSIZE(commands) && strlen(cmd) != 0) + fprintf(stderr, "No such command!\n"); + } +} diff --git a/src/msgs/cat_flag b/src/msgs/cat_flag new file mode 100644 index 0000000..d29ceea --- /dev/null +++ b/src/msgs/cat_flag @@ -0,0 +1,5 @@ + + /\_/\ [ENO] _ + = u u =_______| \\ + _ w __( \__)) + c_____>__(_____)__, diff --git a/src/msgs/welcome b/src/msgs/welcome new file mode 100644 index 0000000..2f77bfb --- /dev/null +++ b/src/msgs/welcome @@ -0,0 +1,2 @@ +Welcome to STLDoctor! +Submit a stl file and we'll analyze it! diff --git a/src/patches/flagstore1.diff b/src/patches/flagstore1.diff new file mode 100644 index 0000000..f0f8d4a --- /dev/null +++ b/src/patches/flagstore1.diff @@ -0,0 +1,17 @@ +--- a/service/src/safe_util.c ++++ b/service/src/safe_util.c +@@ -78,13 +78,12 @@ void + freadstr(FILE *f, char **dst) + { + size_t start, len, tmp; +- char c; + + /* VULN #1: BAD CAST */ + /* see documentation/README.md for more details */ + + start = ftell(f); +- for (len = 0; (c = fgetc(f)) != EOF && c; len++); ++ for (len = 0; fgetc(f) > 0; len++); + fseek(f, start, SEEK_SET); + + *dst = checkp(calloc(1, len + 1)); diff --git a/src/patches/flagstore2.diff b/src/patches/flagstore2.diff new file mode 100644 index 0000000..b34a0c0 --- /dev/null +++ b/src/patches/flagstore2.diff @@ -0,0 +1,11 @@ +--- a/service/src/safe_util.c ++++ b/service/src/safe_util.c +@@ -58,7 +58,7 @@ mhash(const char *str, int len) + srand(v); + + for (bp = buf, i = 0; i < MHASHLEN / 2; i++) +- bp += sprintf(bp, "%02x", str[i % len] ^ (rand() % 256)); ++ bp += sprintf(bp, "%02x", (unsigned char) str[i % len] ^ (rand() % 256)); + + return buf; + } diff --git a/src/stlfile.c b/src/stlfile.c new file mode 100644 index 0000000..88fc430 --- /dev/null +++ b/src/stlfile.c @@ -0,0 +1,415 @@ +#include "stlfile.h" + +static const char wsset[] = " \r\n\t"; +static const struct { + int code; + const char *str; +} kwmap[] = { + { KW_SOLID_BEGIN, "solid" }, + { KW_SOLID_END, "endsolid" }, + { KW_LOOP_BEGIN, "outer loop" }, + { KW_LOOP_END, "endloop" }, + { KW_FACET_BEGIN, "facet normal" }, + { KW_FACET_END, "endfacet" }, + { KW_VERTEX, "vertex" }, +}; + +void +stack_init(struct stack *stack) +{ + stack->cap = 10; + stack->data = checkp(malloc(sizeof(int) * stack->cap)); + stack->count = 0; +} + +void +stack_push(struct stack *stack, int v) +{ + if (stack->count == stack->cap) { + stack->cap *= 2; + stack->data = checkp(realloc(stack->data, sizeof(int) * stack->cap)); + } + + stack->data[stack->count] = v; + stack->count++; +} + +int +stack_pop(struct stack *stack) +{ + if (stack->count == 0) + return -1; + + stack->count--; + return stack->data[stack->count]; +} + +void +stack_free(struct stack *stack) +{ + free(stack->data); +} + +int +stack_ind(struct stack *stack, int search) +{ + int i; + for (i = 0; i < stack->count; i++) + if (stack->data[i] == search) + return i; + return -1; +} + +int +isws(char c) +{ + return c && strchr(wsset, c); +} + +char* +skipws(char *p) +{ + for (; isws(*p); p++); + return p; +} + +char* +consume_arg(char **start, char **end) +{ + char *c, *tmp; + + *start = skipws(*start); + if (!*start) return NULL; + for (c = *start; *c && !isws(*c); c++); + tmp = *start; + *start = c + 1; + *end = c; + return tmp; +} + +int +consume_keyword(char **start) +{ + char *bp; + int i, len; + + bp = skipws(*start); + + for (i = 0; i < ARRSIZE(kwmap); i++) { + len = strlen(kwmap[i].str); + if (!strncmp(kwmap[i].str, bp, len) && (!bp[len] || isws(bp[len]))) { + // printf("GOT: %s\n", kwmap[i].str); + *start = bp + len + (bp[len] ? 1 : 0); + return kwmap[i].code; + } + } + + return KW_UNKNOWN; +} + +#define PARSE_FAIL(...) \ + do { fprintf(stderr, "FORMAT ERR: " __VA_ARGS__); goto fail; } while (0) + +int +parse_file_ascii(struct parseinfo *info, char *buf, size_t len) +{ + char *bp, *arg, *prev, *tmp, *end; + struct stack states; + float farg; + int i, kw; + + stack_init(&states); + + info->type = TYPE_ASCII; + info->loopcount = 0; + + memset(info->header, 0, 80); + + for (i = 0; i < 3; i++) { + info->bbmin[i] = INFINITY; + info->bbmax[i] = -INFINITY; + } + + bp = prev = buf; + while ((kw = consume_keyword(&bp))) { + switch (kw) { + case KW_SOLID_BEGIN: + stack_push(&states, STATE_SOLID); + if (stack_ind(&states, KW_SOLID_BEGIN) != -1) + PARSE_FAIL("Multiple nested solids!\n"); + tmp = bp; + if (!consume_keyword(&bp) && (arg = consume_arg(&bp, &end))) { + info->solidname = strndup(arg, end - arg); + } else { + bp = tmp; + } + break; + case KW_SOLID_END: + if ((kw = stack_pop(&states)) != STATE_SOLID) + PARSE_FAIL("Improper nesting, parent: %s\n", kwmap[kw].str); + tmp = bp; + if (info->solidname && !consume_keyword(&bp) + && (arg = consume_arg(&bp, &end))) { + if (strncmp(info->solidname, arg, end - arg)) + PARSE_FAIL("Solid end/begin names do not match!\n"); + } else { + bp = tmp; + } + break; + case KW_LOOP_BEGIN: + stack_push(&states, STATE_LOOP); + if (stack_ind(&states, KW_LOOP_BEGIN) != -1) + PARSE_FAIL("Multiple nested loops!\n"); + break; + case KW_LOOP_END: + if ((kw = stack_pop(&states)) != STATE_LOOP) + PARSE_FAIL("Improper nesting, parent: %s\n", kwmap[kw].str); + info->loopcount++; + break; + case KW_FACET_BEGIN: + stack_push(&states, STATE_FACET); + if (stack_ind(&states, KW_LOOP_BEGIN) != -1) + PARSE_FAIL("Multiple nested facets!\n"); + for (i = 0; i < 3; i++) { + if (!(arg = consume_arg(&bp, &end))) + PARSE_FAIL("Facet with less than 3 args!\n"); + farg = strtof(arg, &tmp); + if (!isws(*tmp)) + PARSE_FAIL("Facet with invalid arg '%s'!\n", arg); + } + break; + case KW_FACET_END: + if ((kw = stack_pop(&states)) != STATE_FACET) + PARSE_FAIL("Improper nesting, parent: %s\n", kwmap[kw].str); + break; + case KW_VERTEX: + for (i = 0; i < 3; i++) { + if (!(arg = consume_arg(&bp, &end))) + PARSE_FAIL("Vertex with less than 3 args!\n"); + farg = strtof(arg, &tmp); + if (!isws(*tmp)) + PARSE_FAIL("Vertex with invalid arg '%s'!\n", arg); + info->bbmin[i] = MIN(info->bbmin[i], farg); + info->bbmax[i] = MAX(info->bbmax[i], farg); + } + break; + case KW_UNKNOWN: + prev = skipws(prev); + PARSE_FAIL("Expected keyword, got:\n%.*s...\n", 30, prev); + } + prev = bp; + } + + if (states.count) + PARSE_FAIL("Expected keyword, got:\n%.*s...\n", 30, bp); + + stack_free(&states); + return OK; + +fail: + stack_free(&states); + return FAIL; +} + +int +parse_file_bin(struct parseinfo *info, char *buf, size_t len) +{ + char *bp, *end = buf + len; + int i, k, m; + float v; + + info->type = TYPE_BIN; + + if (len < 84) + PARSE_FAIL("Truncated data! (header missing)\n"); + + memcpy(info->header, buf, 80); + + if (strlen(buf + 1)) + info->solidname = checkp(strdup(buf + 1)); + + bp = buf + 80; + info->loopcount = le32toh(*(uint32_t*)bp); + + if (!info->loopcount) { + memset(info->bbmax, 0, sizeof(float) * 3); + memset(info->bbmin, 0, sizeof(float) * 3); + return OK; + } + + for (i = 0; i < 3; i++) { + info->bbmin[i] = INFINITY; + info->bbmax[i] = -INFINITY; + } + + for (i = 0; i < info->loopcount; i++) { + if (bp + 50 > end) + PARSE_FAIL("Truncated data! (loops missing)\n"); + bp += 12; + for (k = 0; k < 3; k++, bp += 12) { + for (m = 0; m < 3; m++) { + v = fle32toh(*(float*)(bp + 4 * m)); + info->bbmin[m] = MIN(info->bbmin[m], v); + info->bbmax[m] = MAX(info->bbmax[m], v); + } + } + bp += 2; + } + + return OK; + +fail: + return FAIL; +} + +int +parse_file(struct parseinfo *info, char *buf, size_t len) +{ + int status; + const char *resp; + char *bp; + + if (info->valid) free_info(info); + + if (len < 7) { + fprintf(stderr, "File too small!\n"); + return FAIL; + } + + info->filesize = len; + + /* check bin vs ascii with first keyword */ + for (bp = buf; isws(*bp); bp++); + status = !strncmp("solid", bp, 5) && isws(bp[5]) + ? parse_file_ascii(info, buf, len) + : parse_file_bin(info, buf, len); + if (status == FAIL) return FAIL; + + if (!info->solidname) info->solidname = checkp(strdup("")); + + if (!info->modelname) { + resp = ask("Please enter your model name: "); + if (strlen(resp) < 4) { + fprintf(stderr, "Model name is too short!\n"); + return FAIL; + } + info->modelname = checkp(strdup(resp)); + } + + info->hash = checkp(strdup(mhash(info->modelname, -1))); + + return OK; +} + +int +save_info(struct parseinfo *info, FILE *f) +{ + size_t nwrote = 0; + int i; + + nwrote += fwrite(&info->type, sizeof(int), 1, f); + nwrote += fwrite(&info->loopcount, sizeof(int), 1, f); + nwrote += fwrite(&info->filesize, sizeof(unsigned), 1, f); + + for (i = 0; i < 3; i++) { + nwrote += fwrite(&info->bbmin[i], sizeof(float), 1, f); + nwrote += fwrite(&info->bbmax[i], sizeof(float), 1, f); + } + + nwrote += fwrite(info->header, 80, 1, f); + + if (nwrote != 10) return FAIL; + + fputstr(f, info->solidname); + fputstr(f, info->hash); + fputstr(f, info->modelname); + + return OK; +} + +int +load_info(struct parseinfo *info, FILE *f) +{ + size_t nread = 0; + int i; + + nread += fread(&info->type, sizeof(int), 1, f); + nread += fread(&info->loopcount, sizeof(int), 1, f); + nread += fread(&info->filesize, sizeof(unsigned), 1, f); + + for (i = 0; i < 3; i++) { + nread += fread(&info->bbmin[i], sizeof(float), 1, f); + nread += fread(&info->bbmax[i], sizeof(float), 1, f); + } + + nread += fread(info->header, 80, 1, f); + + if (nread != 10) return FAIL; + + freadstr(f, &info->solidname); + freadstr(f, &info->hash); + freadstr(f, &info->modelname); + + info->valid = 1; + + return OK; +} + +void +print_info(struct parseinfo *info) +{ + int i, k; + +#define FILTERCHAR(c) ((c) >= 32 ? (c) : ' ') + + printf(" === Model info === \n"); + + printf(" File Size: %u\n", info->filesize); + + if (info->type == TYPE_BIN) { + printf(" Header:\n"); + for (i = 0; i < 80; i += k) { + printf(" "); + for (k = 0; k < MIN(80 - i, 20); k++) + printf(" %02x", (uint8_t) info->header[i+k]); + printf(" | "); + for (k = 0; k < MIN(80 - i, 20); k++) + printf("%c", FILTERCHAR(info->header[i+k])); + printf("\n"); + } + } + + printf(" Model ID: %s\n", info->hash); + printf(" Model Name: %s\n", info->modelname); + printf(" Solid Name: %s\n", info->solidname); + printf(" Triangle Count: %i\n", info->loopcount); + printf(" Bounding Box Size: %.2f x %.2f x %.2f\n", + info->bbmax[0] - info->bbmin[0], + info->bbmax[1] - info->bbmin[1], + info->bbmax[2] - info->bbmin[2]); + printf(" Bounding Box Origin: %.2f x %.2f x %.2f\n", + info->bbmin[0], info->bbmin[1], info->bbmin[2]); + + printf(" ================== \n"); +} + +void +free_info(struct parseinfo *info) +{ + NULLFREE(info->hash); + NULLFREE(info->modelname); + NULLFREE(info->solidname); + info->valid = 0; +} + +float fle32toh(float v) +{ + union { + uint32_t u; + float f; + } conv; + + conv.f = v; + conv.u = le32toh(conv.u); + return conv.f; +} diff --git a/src/stlfile.h b/src/stlfile.h new file mode 100644 index 0000000..d321282 --- /dev/null +++ b/src/stlfile.h @@ -0,0 +1,54 @@ +#ifndef STLFILE_H +#define STLFILE_H + +#include +#include +#include +#include +#include + +#include "util.h" + +enum { + KW_INVALID = -1, + KW_UNKNOWN, + KW_SOLID_BEGIN, + KW_SOLID_END, + KW_FACET_BEGIN, + KW_FACET_END, + KW_LOOP_BEGIN, + KW_LOOP_END, + KW_VERTEX +}; + +enum { + STATE_SOLID, + STATE_FACET, + STATE_LOOP +}; + +enum { + TYPE_ASCII, + TYPE_BIN +}; + +struct stack { + int *data; + size_t count, cap; +}; + +struct parseinfo { + char header[80], *hash, *modelname, *solidname; + uint32_t loopcount; + unsigned filesize; + float bbmin[3], bbmax[3]; + int type, valid; +}; + +int parse_file(struct parseinfo *info, char *buf, size_t len); +int save_info(struct parseinfo *info, FILE *f); +int load_info(struct parseinfo *info, FILE *f); +void print_info(struct parseinfo *info); +void free_info(struct parseinfo *info); + +#endif /* STLFILE_H */ diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..ce22c4e --- /dev/null +++ b/src/util.c @@ -0,0 +1,146 @@ +#include "util.h" + +void* +checkp(void *p) +{ + if (!p) die("pointer assertion failed, OOM?\n"); + return p; +} + +void* +die(const char *fmtstr, ...) +{ + va_list ap; + + va_start(ap, fmtstr); + vfprintf(stderr, fmtstr, ap); + va_end(ap); + + exit(EXIT_FAILURE); +} + +char* +aprintf(const char *fmtstr, ...) +{ + va_list ap, cpy; + size_t nb; + char *str; + + va_copy(cpy, ap); + + va_start(cpy, fmtstr); + nb = vsnprintf(NULL, 0, fmtstr, cpy); + va_end(cpy); + + if (nb <= 0) die("Invalid fmtstr!\n"); + str = checkp(malloc(nb+1)); + + va_start(ap, fmtstr); + nb = vsnprintf(str, nb+1, fmtstr, ap); + va_end(ap); + + return str; +} + +const char* +mhash(const char *str, int len) +{ + static char buf[MHASHLEN + 1]; + int i, k, v; + char c, *bp; + + /* VULN #2: BUFFER OVERFLOW */ + /* see documentation/README.md for more details */ + + if (len == -1) len = strlen(str); + + for (v = 0, i = 0; i < len; i++) v += str[i]; + + srand(v); + for (bp = buf, i = 0; i < MHASHLEN / 2; i++) + bp += sprintf(bp, "%02x", str[i % len] ^ (rand() % 256)); + + return buf; +} + +int +checkalph(const char *str, const char *alph) +{ + int i; + + for (i = 0; i < strlen(str); i++) + if (str[i] && !strchr(alph, str[i])) return 0; + + return 1; +} + +void +freadstr(FILE *f, char **dst) +{ + size_t start, len, tmp; + char c; + + /* VULN #1: BAD CAST */ + /* see documentation/README.md for more details */ + + start = ftell(f); + for (len = 0; (c = fgetc(f)) != EOF && c; len++); + fseek(f, start, SEEK_SET); + + *dst = checkp(calloc(1, len + 1)); + tmp = fread(*dst, len, 1, f); + fgetc(f); +} + +void +fputstr(FILE *f, char *s) +{ + fprintf(f, "%s", s); + fputc(0, f); +} + +const char* +ask(const char *fmtstr, ...) +{ + static char linebuf[256]; + va_list ap; + int fail; + + va_start(ap, fmtstr); + vprintf(fmtstr, ap); + va_end(ap); + + fail = !fgets(linebuf, sizeof(linebuf), stdin); + + if (!fail && *linebuf) { + if (linebuf[strlen(linebuf)-1] == '\n') + linebuf[strlen(linebuf)-1] = '\0'; + if (echo) printf("%s\n", linebuf); + } + + if (fail) errno = EBADMSG; + + return fail ? "" : linebuf; +} + +void +dump(const char *filename) +{ + char buf[256]; + FILE *f; + int nb; + + if (!(f = fopen(filename, "r"))) return; + + while ((nb = fread(buf, 1, sizeof(buf) - 1, f))) + printf("%.*s\n", nb, buf); + + fclose(f); +} + +int +strpfcmp(const char *prefix, const char *str) +{ + return strncmp(prefix, str, strlen(prefix)); +} + diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..c0e9064 --- /dev/null +++ b/src/util.h @@ -0,0 +1,38 @@ +#ifndef UTIL_H +#define UTIL_H + +#include +#include +#include +#include +#include + +#define ARRSIZE(x) (sizeof(x)/sizeof((x)[0])) +#define MIN(x,y) ((x) > (y) ? (y) : (x)) +#define MAX(x,y) ((x) < (y) ? (y) : (x)) + +#define NULLFREE(p) do { free(p); p = NULL; } while (0) + +#define MHASHLEN 40 + +enum { FAIL = 0, OK = 1 }; + +void* checkp(void *p); +void* die(const char *fmtstr, ...); +char* aprintf(const char *fmtstr, ...); + +const char* mhash(const char *filename, int len); +int checkalph(const char *str, const char *alph); + +void freadstr(FILE *f, char **dst); +void fputstr(FILE *f, char *s); + +const char* ask(const char *fmtstr, ...); +void dump(const char *filepath); +int strpfcmp(const char *prefix, const char *str); + +float fle32toh(float v); + +extern int echo; + +#endif /* UTIL_H */ diff --git a/tests/data/evil1.stl b/tests/data/evil1.stl new file mode 100644 index 0000000..706e9e2 --- /dev/null +++ b/tests/data/evil1.stl @@ -0,0 +1,9 @@ +solid test +facet normal 0 0 1.0 + outer loop + vertex 1 0 0 + vertex 1 1 0 + vertex 0 1 0 + endloop + endfacet +endsolid diff --git a/tests/data/flag1.stl b/tests/data/flag1.stl new file mode 100644 index 0000000..f2a3854 --- /dev/null +++ b/tests/data/flag1.stl @@ -0,0 +1,16 @@ +solid ENO{TESTFLAG} + 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 diff --git a/tests/data/sample-ascii.stl b/tests/data/sample-ascii.stl new file mode 100644 index 0000000..e3c89ef --- /dev/null +++ b/tests/data/sample-ascii.stl @@ -0,0 +1,16 @@ +solid test + 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 test diff --git a/tests/data/sample-binary.stl b/tests/data/sample-binary.stl new file mode 100644 index 0000000..13c02e4 Binary files /dev/null and b/tests/data/sample-binary.stl differ diff --git a/tests/test.sh b/tests/test.sh new file mode 100644 index 0000000..4835024 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,200 @@ +#!/bin/sh + +set -e + +if [ -z "$SRCDIR" -o -z "$DATADIR" ]; then + echo "Missing either SRCDIR or DATADIR env vars" + exit 1 +fi + +export RESULTDIR="$DATADIR/uploads" +export ECHO_INPUT=1 + +SCRIPTPATH="$(dirname $(readlink -f "$0"))" +TESTDATA="$SCRIPTPATH/data" + +shopt -s expand_aliases +alias pushd="pushd &>/dev/null" +alias popd="popd &>/dev/null" + +pushd "$SRCDIR" + +announce() { + count=$(echo "$1" | wc -c) + python3 -c " +import math +s = '$1' +c = 80 +print() +print('#'*c) +print('#' + ' '*math.floor((c - len(s))/2-1) + s + ' '*math.ceil((c - len(s))/2-1) + '#') +print('#'*c) +print() + " +} + +checkleaks() { + valgrind --leak-check=full --show-leak-kinds=all ./build/stldoctor 2>&1 | tee /tmp/testlog + if [ -z "$(grep "no leaks are possible" /tmp/testlog)" ]; then + echo "Valgrind exited with errors!" + exit 1 + fi +} + +connect() { + if [ "$RUNTYPE" == "remote" ]; then + nc localhost 9090 + elif [ "$RUNTYPE" == "debug" ]; then + checkleaks + else + ./build/stldoctor + fi +} + +cleanuploads() { + [ ! -z "$RESULTDIR" ] && rm -rf "$RESULTDIR" + mkdir -p "$RESULTDIR" +} + +if [ "$1" == "stl-leaks" ]; then + cleanuploads + + announce "Testing ASCII STL Parsing" + ( + echo "echo" + echo "upload" + 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" + cat "$TESTDATA/sample-binary.stl" | wc -c + cat "$TESTDATA/sample-binary.stl" + echo "BIN-testname" + ) | checkleaks + +elif [ "$1" == "stl-upload" ]; then + cleanuploads + + popd + file="$(realpath $2)" + if [ ! -e "$file" ]; then + echo "Supply a file to upload" + exit 1 + fi + pushd "$SRCDIR" + + name="${3:-samplefile}" + ( + echo "echo" + echo "upload" + cat "$file" | wc -c + cat "$file" + echo "$name" + ) | checkleaks + +elif [ "$1" == "vuln1" ]; then + cleanuploads + + announce "Testing Flagstore 1" + + echo -e "\n--- Uploading target STL ---\n" 1>&2 + ( + echo "echo" + echo "upload" + cat "$TESTDATA/flag1.stl" | wc -c + cat "$TESTDATA/flag1.stl" + echo "N0TaFL4G" + echo "exit" + ) | connect + + echo -e "\n--- Uploading evil STL ---\n" 1>&2 + ( + echo "echo" + echo "upload" + cat "$TESTDATA/evil1.stl" | wc -c + cat "$TESTDATA/evil1.stl" + echo "EV1L" + echo "exit" + ) | connect + + echo -e "\n--- Testing Exploit ---\n" 1>&2 + ( + echo "echo" + + # try index 0 + echo "search" + echo "EV1L" + echo "0" + echo "n" + + echo "search last" + echo "0" + echo "n" + + # try index 1 + echo "search" + echo -e "EV1L" + echo "0" + echo "n" + + echo "search last" + echo "1" + echo "n" + echo "exit" + ) | connect + +elif [ "$1" == "vuln2" ]; then + cleanuploads + + announce "Testing Flagstore 2" + + echo -e "\n--- Uploading target STL ---\n" 1>&2 + ( + echo "echo" + echo "auth test" + echo "upload" + cat "$TESTDATA/flag1.stl" | wc -c + cat "$TESTDATA/flag1.stl" + echo "N0TaFL4G" + echo "exit" + ) | connect + + echo -e "\n--- Testing Exploit ---\n" 1>&2 + ( + echo "echo" + echo -e "search \xff\xff\xff\xff\xff0000000000000000" + echo "auth" + echo "list" + echo "exit" + ) | connect + +elif [ "$1" == "auth-upload" ]; then + cleanuploads + + ( + echo "echo" + + echo "auth test" + echo "upload" + cat "$TESTDATA/sample-ascii.stl" | wc -c + cat "$TESTDATA/sample-ascii.stl" + echo "testname" + ) | connect + + ( + echo "echo" + + echo "auth test" + echo "list" + echo "search testname" + ) | connect +else + connect +fi + +popd -- cgit v1.2.3-71-gd317