summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/writeups/IceCTF_2018/Hot_or_Not.txt617
-rw-r--r--docs/writeups/IceCTF_2018/hotornot.jpgbin0 -> 106466 bytes
-rw-r--r--docs/writeups/IceCTF_2018/hotornot_detail.jpgbin0 -> 96744 bytes
-rw-r--r--docs/writeups/IceCTF_2018/hotornot_final_overlay.pngbin0 -> 21355 bytes
-rw-r--r--docs/writeups/IceCTF_2018/hotornot_initial_overlay.jpgbin0 -> 79397 bytes
-rw-r--r--docs/writeups/IceCTF_2018/hotornot_refined_overlay.pngbin0 -> 19113 bytes
6 files changed, 617 insertions, 0 deletions
diff --git a/docs/writeups/IceCTF_2018/Hot_or_Not.txt b/docs/writeups/IceCTF_2018/Hot_or_Not.txt
new file mode 100644
index 0000000..f50a295
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/Hot_or_Not.txt
@@ -0,0 +1,617 @@
+"According to my friend Zuck, the first step on the path to great power is to
+rate the relative hotness of stuff... think Hot or Not."
+
+
+Setup
+-----
+We are provided a JPEG image.
+
+hotornot.jpg shows the original in its entirety, with the caviot that this one
+is scaled WAY down and compressed more heavily. The actual original is very
+large (19488x19488, 69 MB).
+
+hotornot_detail.jpg shows a small detailed portion of the original image. We
+can see that the original is made up of a grid of images, either pictures of
+hotdogs or pictures of dogs.
+
+
+Ideas
+-----
+This was a steganography task. As with tasks like this we first try the usual
+cheap searching techniques, strings, binwalk, and hexdump to look for anything
+that sticks out. Once reasonably sure there was nothing there, we moved on to
+the image data.
+
+The original image, as stated before, is a grid of embedded images. Each one
+measured to be 224x224 pixels. The entire image is 19488x19488, which means it
+is 87 embedded images across and 87 tall.
+
+Given the state of things and the clues dropped in the task title and
+description, we were fairly confident in guessing this was a game of
+hotdog-notdog.
+
+This comes from an episode of the HBO show "Silicon Valley", where one of the
+characters designs a mobile app which can identify food. In reality, it can
+only identify hotdogs and will otherwise report that the picture is *not* of a
+hotdog. Today I learned that this is actually a real thing, with a trained AI
+and public API on the internet. As I'm going back and writing this up, I can't
+find the website we used anymore. But here's the API from my script
+"https://api.deepai.org/hot_dog_or_not". Hit that with a POST to have an image
+analyzed (as we will see below).
+
+With a plan formulating, we are theorizing on what the hotdog-notdog results
+will mean. We want to try interpreting the results as bits, packing every 8
+into a byte, and reading the resulting stream. Otherwise, we will try
+overlaying the results back on the original image to see if a visual pattern
+emerges.
+
+
+Solution
+--------
+
+# Image Analysis
+Starting with our original image above, we want to offload the analysis of each
+embedded image to the API we found. As a convention, I will be refering to
+these embedded images as cells.
+
+The first step was extracting each cell into its own image file. For this I
+wrote a C program utilizing libjpeg (See Appendix A). This left us with a
+directory of files "imgXxY.jpg", where X and Y are the corresponding row and
+column of the cell in grid-space [0, 87).
+
+The website we found the AI engine on had some minimal documentation on its API
+and left us with a curl command to interact with it. It just takes a POST
+passing it the URL to an image to analyze.
+
+curl -X POST -d '{"img_url": "http://..."}' https://api.deepai.org/hot_dog_or_not
+
+Note that it takes images by URL when using the API. So we had to host the
+images somewhere. I just resorted to our trusty internal server and opened the
+firewall gates!
+
+To automate the process of calling the API with each image, the following
+scripts were written. The first Python script just procedurally generates all
+the filenames of all the cell images. The second Bash script takes each name,
+plugs it into a full URL and makes the curl call, writing the results to a
+image-specific text file.
+
+#!/usr/bin/env python
+for row in range(87):
+ for col in range(87):
+ print(f"img{row}x{col}.jpg")
+
+#!/bin/bash -e
+
+# Read names off stdin, tell deepai to check it out from host #
+while read name; do
+ URL="{\"img_url\":\"http://<REDACTED>/cells/$name\"}"
+ curl -X POST -d $URL 'https://api.deepai.org/hot_dog_or_not' >results/$name.txt
+done
+
+We now have a directory of imgXxY.jpg files and another of imgXxY.jpg.txt files.
+Each text file contains the analysis result of its corresponding cell image file.
+The contents of the text files looks like this:
+
+{"is_hot_dog": "hot dog"}
+
+in the case of a hotdog, and
+
+{"is_hot_dog": "not hot dog"}
+
+in the case of a notdog. So, we can check one by searching it for the word
+"not".
+
+
+# Interpreting Data
+We first tried inspecting the bitstream which resulted from image analysis.
+Calling each hotdog a 1 and each notdog a 0 and packing each 8 into a byte, no
+recognizable data can be found in the resulting bitstream. The same happens
+when calling hotdogs 0 and vice-virsa. We know that there are some false
+results back from the AI, but were assuming that the data would be more-or-less
+kinda readable as a starting point.
+
+Moving on to an image overlay. We produce a copy of the original image, with
+each hotdog covered with white pixels and each notdog covered with black pixels
+(see Appendix B). No patterns initially jumped out, but after a while of
+studying I started to think I was seeing a QR code in the noise. We both
+confirmed some characteristics of the image to gauge the likelyhood of this and
+decided to presue it.
+
+
+## QR Code
+'hotornot_initial_overlay.jpg' shows our initial image overlay. The cell
+resolution is still 87x87, which is not a valid code according to the standard.
+We noticed that the original image happened to always contain hotdog and notdog
+images in clusters of 3x3 cells, which was really useful for two reasons. First,
+this gave us a way to weed-out some false results from the AI, just group cells
+together and produce a cluster based on the majority result. Second, once we
+scaled-down our image by replacing cells with clusters, we have an image which
+is 29x29 clusters, and this *is* a valid code version as described in the
+standard. It's version 3.
+
+We regenerated our QR code using a cluster-oriented approach (See Appendix C).
+We also inverted the colors since we noticed a few things that didn't quite make
+sense in the original. This produced 'hotornot_refined_overlay.jpg'. This is
+still not a valid QR code as it is missing the large markers in the corners and
+a few pixels are still malformed according to the version 3 standard. In
+particular, this included some alternating patterns along the top and left-hand
+side, as well as the small square marker towards the bottom-right. We suspect
+these subtle remaining flaws were unintended by the problem designers, since we
+can see what images we expect back in the original. It was probably just
+unlucky bad results from the AI.
+
+We fixed up the small errors and manually drew in the large corner markers.
+This produced 'hotornot_final_overlay.png'. Upon scanning this image with a QR
+code reader, we finally recovered the flag.
+
+IceCTF{h0td1gg1tyd0g}
+
+
+== Appendices ==
+=== Appendix A ===
+C code to split original image into cells
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <jpeglib.h>
+
+void write_cell(unsigned int matr, unsigned int matc,
+ unsigned char *buff, unsigned long fwid)
+{
+ struct jpeg_compress_struct cinfo;
+ struct jpeg_error_mgr jerr;
+
+ /* this is a pointer to one row of image data */
+ JSAMPROW row_pointer[1];
+
+ char fname[32];
+ sprintf(fname, "img%ix%i.jpg", matr, matc);
+
+ FILE *outfile = fopen(fname, "wb"); // res
+
+ if (!outfile)
+ {
+ fprintf(stderr, "Failed to open output file '%s'\n", fname);
+ return;
+ }
+
+ cinfo.err = jpeg_std_error( &jerr );
+ jpeg_create_compress(&cinfo);
+ jpeg_stdio_dest(&cinfo, outfile);
+
+ /* Setting the parameters of the output file here */
+ cinfo.image_width = 224;
+ cinfo.image_height = 224;
+ cinfo.input_components = 3;
+ cinfo.in_color_space = JCS_RGB;
+ /* default compression parameters, we shouldn't be worried about these */
+ jpeg_set_defaults( &cinfo );
+ /* Now do the compression .. */
+ jpeg_start_compress( &cinfo, TRUE );
+
+
+ unsigned long row = matr * 224;
+ unsigned long col = matc * 224;
+
+ while (cinfo.next_scanline < cinfo.image_height)
+ {
+ unsigned long idx = (cinfo.next_scanline + row) * fwid * cinfo.input_components;
+ idx += col * cinfo.input_components;
+
+ row_pointer[0] = &buff[idx];
+ jpeg_write_scanlines(&cinfo, row_pointer, 1);
+ }
+
+
+ /* similar to read file, clean up after we're done compressing */
+ jpeg_finish_compress( &cinfo );
+ jpeg_destroy_compress( &cinfo );
+ fclose( outfile );
+}
+
+int main(int argc, char **argv)
+{
+ if (argc < 2)
+ {
+ fprintf(stderr, "Usage: %s <file>\n", argv[0]);
+ return 1;
+ }
+
+ char *file = argv[1];
+
+ struct jpeg_decompress_struct jds;
+ struct jpeg_error_mgr err;
+
+ FILE *f;
+ f = fopen(file, "rb"); // res
+ if (!f)
+ {
+ fprintf(stderr, "Failed to open file\n");
+ return 1;
+ }
+
+ jds.err = jpeg_std_error(&err);
+ jpeg_create_decompress(&jds); // res
+ jpeg_stdio_src(&jds, f);
+ jpeg_read_header(&jds, TRUE);
+ jpeg_start_decompress(&jds);
+
+ unsigned long width = jds.output_width;
+ unsigned long height = jds.output_height;
+ unsigned char *buff = malloc(width * height * 3); // res
+ unsigned char *tmp[1];
+
+ if (!buff)
+ {
+ fprintf(stderr, "Could not allocate image buffer\n");
+ jpeg_finish_decompress(&jds);
+ return 1;
+ }
+
+ while (jds.output_scanline < height)
+ {
+ tmp[0] = buff + (3 * width * jds.output_scanline);
+ jpeg_read_scanlines(&jds, tmp, 1);
+ }
+
+ jpeg_finish_decompress(&jds);
+ jpeg_destroy_decompress(&jds);
+ fclose(f);
+
+
+ /* we have the img in memory now, write every 224x224
+ * block of pixels out to a matrix of files 'img0x0.jpg' */
+ unsigned int matr = 0;
+ unsigned int matc = 0;
+
+ printf("width: %li\n", width);
+ printf("height: %li\n", height);
+
+ /* lines */
+ for (matr = 0; (matr * 224) < height; matr++)
+ {
+ /* cells */
+ for (matc = 0; (matc * 224) < width; matc++)
+ write_cell(matr, matc, buff, width);
+ }
+
+ printf("rows: %i\n", matr);
+ printf("cols: %i\n", matc);
+
+ free(buff);
+ return 0;
+}
+
+
+=== Appendix B ===
+C code to produce initial QR code image
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <jpeglib.h>
+
+void interpret_cell(unsigned int matr, unsigned int matc,
+ unsigned char *buff, unsigned long fwid)
+{
+ char results_file[32], line[64];
+ sprintf(results_file, "deepai/results/img%ix%i.jpg.txt", matr, matc);
+
+ FILE *rf = fopen(results_file, "r"); // res
+
+ if (!rf)
+ {
+ fprintf(stderr, "Failed to open results file '%s'\n", results_file);
+ return;
+ }
+
+ fgets(line, 64, rf);
+ char *not = strstr(line, "not");
+
+ unsigned long height = 224;
+ unsigned long width = 224;
+ unsigned char *linebuff = malloc(3 * width); // res
+
+ if (!linebuff)
+ {
+ fprintf(stderr, "Failed to allocate result tmp buffer\n");
+ fclose(rf);
+ return;
+ }
+
+ if (not)
+ memset(linebuff, 0, 3*width);
+ else
+ memset(linebuff, 255, 3*width);
+
+ unsigned long i;
+
+ unsigned long row = matr * 224;
+ unsigned long col = matc * 224;
+
+ for (i = 0; i < height; i++)
+ {
+ unsigned long idx = (i + row) * fwid * 3;
+ idx += col * 3;
+
+ unsigned char *ptr = &buff[idx];
+ memcpy(ptr, linebuff, 3*width);
+ }
+
+ free(linebuff);
+ fclose(rf);
+}
+
+void write_img(unsigned char *buff, unsigned long width, unsigned long height)
+{
+ struct jpeg_compress_struct cinfo;
+ struct jpeg_error_mgr jerr;
+
+ /* this is a pointer to one row of image data */
+ JSAMPROW row_pointer[1];
+
+ char fname[32];
+ sprintf(fname, "myoutput.jpg");
+
+ FILE *outfile = fopen(fname, "wb"); // res
+
+ if (!outfile)
+ {
+ fprintf(stderr, "Failed to open output file '%s'\n", fname);
+ return;
+ }
+
+ cinfo.err = jpeg_std_error( &jerr );
+ jpeg_create_compress(&cinfo);
+ jpeg_stdio_dest(&cinfo, outfile);
+
+ /* Setting the parameters of the output file here */
+ cinfo.image_width = width;
+ cinfo.image_height = height;
+ cinfo.input_components = 3;
+ cinfo.in_color_space = JCS_RGB;
+ /* default compression parameters, we shouldn't be worried about these */
+ jpeg_set_defaults( &cinfo );
+ /* Now do the compression .. */
+ jpeg_start_compress( &cinfo, TRUE );
+
+
+ while (cinfo.next_scanline < cinfo.image_height)
+ {
+ row_pointer[0] = &buff[cinfo.next_scanline * width * cinfo.input_components];
+ jpeg_write_scanlines(&cinfo, row_pointer, 1);
+ }
+
+
+ /* similar to read file, clean up after we're done compressing */
+ jpeg_finish_compress( &cinfo );
+ jpeg_destroy_compress( &cinfo );
+ fclose( outfile );
+}
+
+int main(int argc, char **argv)
+{
+ if (argc < 2)
+ {
+ fprintf(stderr, "Usage: %s <file>\n", argv[0]);
+ return 1;
+ }
+
+ char *file = argv[1];
+
+ struct jpeg_decompress_struct jds;
+ struct jpeg_error_mgr err;
+
+ FILE *f;
+ f = fopen(file, "rb"); // res
+ if (!f)
+ {
+ fprintf(stderr, "Failed to open file\n");
+ return 1;
+ }
+
+ jds.err = jpeg_std_error(&err);
+ jpeg_create_decompress(&jds); // res
+ jpeg_stdio_src(&jds, f);
+ jpeg_read_header(&jds, TRUE);
+ jpeg_start_decompress(&jds);
+
+ unsigned long width = jds.output_width;
+ unsigned long height = jds.output_height;
+ unsigned char *buff = malloc(width * height * 3); // res
+ unsigned char *tmp[1];
+
+ if (!buff)
+ {
+ fprintf(stderr, "Could not allocate image buffer\n");
+ jpeg_finish_decompress(&jds);
+ return 1;
+ }
+
+ while (jds.output_scanline < height)
+ {
+ tmp[0] = buff + (3 * width * jds.output_scanline);
+ jpeg_read_scanlines(&jds, tmp, 1);
+ }
+
+ jpeg_finish_decompress(&jds);
+ jpeg_destroy_decompress(&jds);
+ fclose(f);
+
+
+ unsigned int matr = 0;
+ unsigned int matc = 0;
+
+ printf("width: %li\n", width);
+ printf("height: %li\n", height);
+
+ /* lines */
+ for (matr = 0; (matr * 224) < height; matr++)
+ {
+ /* cells */
+ for (matc = 0; (matc * 224) < width; matc++)
+ interpret_cell(matr, matc, buff, width);
+ }
+
+ printf("rows: %i\n", matr);
+ printf("cols: %i\n", matc);
+
+ write_img(buff, width, height);
+
+ free(buff);
+ return 0;
+}
+
+
+=== Appendix C ===
+C code to produce clustered QR code image. We switched to PNG format to
+eliminate JPEG compression artifacts. This program also writes an image that is
+1 pixel per cluster, to aid manual editing.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define PNG_DEBUG 3
+#include <png.h>
+
+#define OUT_DIM 29
+#define IMG_LEN ((OUT_DIM*OUT_DIM)/8)
+#define ORIG_DIM 87
+
+#define FILE_NAME "myoutput.png"
+
+unsigned int get_px_idx(unsigned int wid, unsigned int row,
+ unsigned int col)
+{
+ return (row*wid) + col;
+}
+
+/* transform from cluster-space into cell-space */
+unsigned int get_cell_output_px_idx(unsigned cell_row,
+ unsigned cell_col)
+{
+ return get_px_idx(OUT_DIM, cell_row/3, cell_col/3);
+}
+
+void write_image(unsigned char *buff)
+{
+ png_structp png_ptr;
+ png_infop info_ptr;
+
+ /* create file */
+ FILE *fp = fopen(FILE_NAME, "wb");
+ if (!fp)
+ printf("[write_png_file] File %s could not be opened for writing", FILE_NAME);
+
+
+ /* initialize stuff */
+ png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+
+ if (!png_ptr)
+ printf("[write_png_file] png_create_write_struct failed");
+
+ info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr)
+ printf("[write_png_file] png_create_info_struct failed");
+
+ if (setjmp(png_jmpbuf(png_ptr)))
+ printf("[write_png_file] Error during init_io");
+
+ png_init_io(png_ptr, fp);
+
+
+ /* write header */
+ if (setjmp(png_jmpbuf(png_ptr)))
+ printf("[write_png_file] Error during writing header");
+
+ png_set_IHDR(png_ptr, info_ptr, OUT_DIM, OUT_DIM,
+ 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE,
+ PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+
+ png_write_info(png_ptr, info_ptr);
+
+
+ /* write bytes */
+ if (setjmp(png_jmpbuf(png_ptr)))
+ printf("[write_png_file] Error during writing bytes");
+
+ unsigned int row;
+ for (row=0; row < OUT_DIM; row++)
+ png_write_row(png_ptr, &buff[get_px_idx(OUT_DIM, row, 0)]);
+
+ //png_write_image(png_ptr, &buff);
+
+
+ /* end write */
+ if (setjmp(png_jmpbuf(png_ptr)))
+ printf("[write_png_file] Error during end of write");
+
+ png_write_end(png_ptr, NULL);
+
+ fclose(fp);
+}
+
+int interpret_cell(unsigned int cell_row, unsigned int cell_col)
+{
+ char fname[32], line[64];
+
+ snprintf(fname, 32, "deepai/results/img%ix%i.jpg.txt",
+ cell_row, cell_col);
+
+ FILE *f = fopen(fname, "r");
+ if (!f)
+ {
+ fprintf(stderr, "Failed to open result file '%s'\n", fname);
+ return 0;
+ }
+
+ fgets(line, 64, f);
+ char *not = strstr(line, "not");
+ fclose(f);
+
+ return not == NULL;
+}
+
+int main()
+{
+ /* output image information */
+ unsigned char *outbuff = malloc(OUT_DIM*OUT_DIM);
+ memset(outbuff, 0, OUT_DIM*OUT_DIM);
+
+
+ /* iterate original cells (not clusters) */
+ unsigned int cell_row, cell_col;
+
+ for (cell_row = 0; cell_row < ORIG_DIM; cell_row++)
+ {
+ for (cell_col = 0; cell_col < ORIG_DIM; cell_col++)
+ {
+ int res = interpret_cell(cell_row, cell_col);
+ outbuff[get_cell_output_px_idx(cell_row, cell_col)] += res;
+ }
+ }
+
+
+ /* counts to colors */
+ unsigned int r, c;
+
+ for (r = 0 ; r < OUT_DIM; r++)
+ {
+ for (c = 0; c < OUT_DIM; c++)
+ {
+ unsigned int idx = get_px_idx(OUT_DIM, r, c);
+ if (outbuff[idx] >= 5)
+ outbuff[idx] = 0;
+ else
+ outbuff[idx] = 255;
+ }
+ }
+
+
+ /* write image */
+ write_image(outbuff);
+
+ free(outbuff);
+ return 0;
+}
diff --git a/docs/writeups/IceCTF_2018/hotornot.jpg b/docs/writeups/IceCTF_2018/hotornot.jpg
new file mode 100644
index 0000000..c649289
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/hotornot.jpg
Binary files differ
diff --git a/docs/writeups/IceCTF_2018/hotornot_detail.jpg b/docs/writeups/IceCTF_2018/hotornot_detail.jpg
new file mode 100644
index 0000000..106b40f
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/hotornot_detail.jpg
Binary files differ
diff --git a/docs/writeups/IceCTF_2018/hotornot_final_overlay.png b/docs/writeups/IceCTF_2018/hotornot_final_overlay.png
new file mode 100644
index 0000000..fa93ac7
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/hotornot_final_overlay.png
Binary files differ
diff --git a/docs/writeups/IceCTF_2018/hotornot_initial_overlay.jpg b/docs/writeups/IceCTF_2018/hotornot_initial_overlay.jpg
new file mode 100644
index 0000000..c29c332
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/hotornot_initial_overlay.jpg
Binary files differ
diff --git a/docs/writeups/IceCTF_2018/hotornot_refined_overlay.png b/docs/writeups/IceCTF_2018/hotornot_refined_overlay.png
new file mode 100644
index 0000000..5cb5eda
--- /dev/null
+++ b/docs/writeups/IceCTF_2018/hotornot_refined_overlay.png
Binary files differ