This shows you the differences between two versions of the page.
ii:assignments:s2:stegano [2023/03/28 11:01] radu.mantu |
ii:assignments:s2:stegano [2023/05/24 17:14] (current) florin.stancu |
||
---|---|---|---|
Line 1: | Line 1: | ||
~~NOTOC~~ | ~~NOTOC~~ | ||
- | ====== Stegano Tool ====== | + | ====== Web-based steganography tool ====== |
- | <note important> | + | **Deadline**: |
- | **Work In Progress:** the final version will have some extra tasks relating to Flask (lab 2) and Docker (lab 3+). If you want to get started sooner rather than later, note that the base functionality of the script will be the same. | + | * **//04.06.2023//**: <color red>HARD!</color> |
- | </note> | + | |
+ | **Changelog:** | ||
+ | * //24.05.2023//: ''/image/decode'' & ''/image/last/*'' endpoint clarifications + final deadline! | ||
===== Context ===== | ===== Context ===== | ||
- | Steganography is the practice of hiding information within another medium. If in encryption the difficulty lies in finding the secret (i.e.: the key) used to obfuscate the data, here the problem consists of detecting whether the data exists at all. | + | Steganography is the practice of hiding information within another medium (which is, usually, communicated in plain sight). If in encryption the difficulty lies in finding the secret (i.e.: the key) used to obfuscate the data, here the problem consists of detecting whether the data exists at all (the //plausible deniability// concept). |
- | One stegano technique that is easy to understand consists of encoding messages into pixel data. As we all know, images are made out of pixels, and pixels are made out of three color channels: Red, Green, Blue. Usually, each channel is represented via an 8-bit value (0 - 255). The higher the value, the more intense the color. Thus, it follows that altering the more significant bits of any channel will produce visible alterations: in the image below, we masked (i.e.: set to 0) the most significant bit of every channel, of every pixel. But what happens if we play around with some of the less significant bits? Answer: no human will be able to tell the difference. | + | One stegano technique that is easy to understand consists of encoding messages into pixel data. As we all know, images are made out of pixels, and pixels are made out of three color channels: Red, Green, Blue. Usually, each channel is represented via an 8-bit value (0 - 255, or ''0xFF''). The higher the value, the more intense the color. Thus, it follows that altering the more significant bits of any channel will produce visible alterations: in the image below, we masked (i.e.: set to 0) the most significant bit of every channel, of every pixel. |
[[https://ocw.cs.pub.ro/courses/_media/ii/assignments/s2/hackerman-0x7f.png|{{ :ii:assignments:s2:hackerman-0x7f.png?600 |}}]] | [[https://ocw.cs.pub.ro/courses/_media/ii/assignments/s2/hackerman-0x7f.png|{{ :ii:assignments:s2:hackerman-0x7f.png?600 |}}]] | ||
- | As a result, one way to exfiltrate data is by splitting the message into bits and encoding them into the least significant bits of the image. If only the least significant bit is used, you will need 3px in order to encode 1 byte of data (with 1 bit to spare). | + | But what happens if we play around with some of the less significant bits? Answer: no human will be able to tell the difference! |
+ | As a result, one way to exfiltrate data is by splitting the message into bits and encoding them into the least significant bits of the image. If only the least significant bit is used, you will need 3px in order to encode 1 byte of data (with 1 bit to spare, or it could also be considered the next byte of our secret). For example: | ||
- | The goal of this assignment is to write a **Python** tool that helps visualize the bit-level layers of different color channels. | + | <code python> |
+ | # Note: using PIL representation, where each pixel is a (R, G, B) tuple | ||
+ | pixels = [(0xff, 0x00, 0x04), (0xff, 0x19, 0x1d), (0xff, 0x34, 0x37)] | ||
+ | bin_message = [0, 1, 0, 0, 1, 0, 0, 0, 1] # ASCII for 'H' (+ the '1' extra bit for the next byte) | ||
+ | # after the (color & 0xFE | msg_bit) bitwise operation for each pixel / channel: | ||
+ | enc_pixels = [(0xfe, 0x01, 0x04), (0xfe, 0x19, 0x1c), (0xfe, 0x34, 0x37)] | ||
+ | </code> | ||
- | ===== Specification ===== | + | The goal of this assignment is to write a simple Flask-based web application that helps you encode / decode a secret message into an existing image (uploaded by the user) using the steganography method described above. Furthermore, we also want the server containerized using Docker (together with its dependencies) such that, regardless of the machine, it can be easily started with minimal effort (especially for evaluation purposes!). |
- | Your python script should support the following three flags: ''-r'', ''-g'', ''-b''. Each flag should be optional and accept a number in hex format (default value if flag is absent should be 0x00). These numbers represent a mask that is to be applied to each pixel, for its respective color channel. The way you apply the mask is by performing a bitwise AND (&) operation between the color value and the mask. For example, running the script only with ''-g 0x40'' should completely suppress the red and blue channels, all while keeping the second most significant bit of the green channel (0x40 = 0100 0000). So the only pixels that you will see are ''(0, 0, 0)'' and ''(0, 64, 0)'', as in the following image: | + | <note important> |
+ | This technique only works on [[https://en.wikipedia.org/wiki/Lossless_compression|lossless compression formats]] (e.g., ''bmp'', ''png'')! | ||
- | [[https://ocw.cs.pub.ro/courses/_media/ii/assignments/s2/hackerman-g-0x40.png|{{ :ii:assignments:s2:hackerman-g-0x40.png?600 |}}]] | + | In contrast, lossy image formats (e.g., ''jpeg'') may randomly alter the color data of the pixels, so the information concealed there will get corrupted. We will only consider the former case (no lossess)! |
+ | </note> | ||
- | Although this works reasonably well, it would be extremely difficult to visually differentiate pixels when suppressing the more significant bits. After all, 0x00 and 0x01 is ultimately still black, right? To deal with this, you will also add a ''%%--%%boost'' flag. This flag doesn't take any argument and it's presence should force the script to boost the color value to the maximum value (0xff) if the result of the bitwise AND is non-zero. The output should look something like this for ''%%--%%boost -r 0x04'': | + | ===== Specification ===== |
- | [[https://ocw.cs.pub.ro/courses/_media/ii/assignments/s2/hackerman-br-0x04.png|{{ :ii:assignments:s2:hackerman-br-0x04.png?600 |}}]] | + | You must implement a Flask web server serving a basic User Interface with several (*ahem*, two) HTML forms for uploading images, plus specific backend routes for receiving the uploaded files, doing the actual steganography encoding / decoding processing and giving back the results. |
- | Finally, the script should also accept a positional (non-optional) argument representing the target image file. The pixel masking operation should not alter the image on the disk. In stead, your tool should display the RAM-based modified version (see [[https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.show|Image.show()]]). | + | In the following subsections, we define some minimal (required) aspects to be followed (especially to make the grading process easy to automate) + recommandations of the best approaches to consider (as notes / hints). |
- | + | ||
- | ===== Resources ===== | + | |
- | + | ||
- | * [[https://pillow.readthedocs.io/en/stable/|pillow]] is a image processing module that has support for many image formats and grants the developer access to the pixel data. You can install it using ''pip3'' (see our [[:ii:labs:03|previous lab]] for info regarding ''pip'' and virtual environments). | + | |
- | * [[https://docs.python.org/3/library/argparse.html|argparse]] is a command line argument parser. Use it to register flags for your tool. | + | |
- | + | ||
- | ===== Grading ===== | + | |
- | + | ||
- | The base assignment constitutes **2p** out of your final grade (100p homework = **2p** final grade). The 100p are split between the following tasks: | + | |
- | * **[20p] CLI arguments:** Arguments are parsed, have a default value, etc. | + | |
- | * **[30p] Pixel masking:** The specified masks are applied to each channel, regardless of image size. | + | |
- | * **[20p] Channel boost:** If the argument is specified, {R,G,B} channels are set to 0xff for each pixel if their masked value is non-zero. | + | |
- | * **[30p] Secrets found:** The test image contains 5 secrets encoded on certain channels, on single bit layers (i.e.: 0x01, 0x02, 0x04, etc.) Include the resulting images in your submission. | + | |
- | * **[1p] Bonus:** If you know the source for each image :p | + | |
<note> | <note> | ||
- | Write a README containing the description of your implementation, design choices, challenges you encountered, etc. Feel free to add your feedback here as well. All submissions that do not include a README will be ignored. | + | We recommend to start this assignment by first writing small Python functions / modules and/or CLI script for running the steganography encoding / decoding algorithms. |
- | + | ||
- | ---- | + | |
- | **NOTE:** Assistants are free do deduct points for bad / illegible code. | + | This will decouple the tasks (encoding / decoding vs web frontend), allowing you to partly validate the solution before continuing. |
+ | OFC, bonus for using unit testing ;), although this is out of scope. | ||
</note> | </note> | ||
- | ===== Test Images ===== | + | ==== REST-ful Web API ==== |
- | {{:ii:assignments:s2:secret.zip|This archive}} contains a PNG image with 5 secrets hidden at certain bit levels and at different channels. | + | A Web-based API (Application Programming Interface) is a contract between the provider of a service and the user wishing to make use of it. |
- | ===== FAQ ===== | + | This usually consists of the format of the different URLs, specific parameters, HTTP methods they may be called with and request / response body formats. |
+ | [[https://en.wikipedia.org/wiki/Representational_state_transfer|REpresentational State Transfer (REST)]] is a set of common principles / rules which makes such APIs consitent and easy to use. | ||
- | **Q: Can I write the tool in something other than Python?** \\ | + | Your Flask application must, too, adhere to such an API: |
- | A: No. | + | |
+ | * ''/'': serves the front HTML page; | ||
+ | * ''/image/encode'': receives a [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type|''multipart/form-data'']] with the following fields (i.e., form names): ''file'' (the uploaded file, binary data) and ''message'' (the string message to embed into the image using steganography); responds with the generated image (as binary downloaded data); the encoded image should also be saved onto the server's disk for later retrieval requests; | ||
+ | * ''/image/last/encoded'': retrieves (i.e., downloads / displays) the binary contents of the last encoded image (no arguments required); | ||
+ | * ''/image/decode'': takes an encoded image and outputs the original steganography message (using the least significant bits technique); request should have a ''multipart/form-data'' content type and receive a ''file'' field with the stegano-encoded image, and output the decoded message (either as HTML page or as a simple ''plain/text''' output); it should also store the decoded text / binary data (if you wish to support it); | ||
+ | * ''/image/last/decoded'': retrieves the last decoded plain text (or binary data, if you support this); do not output HTML here, just the raw data (used for test automation purposes). | ||
- | **Q: What platform will this assignment be tested on?** \\ | + | Image formats: use any web-compatible lossless compression format (e.g., ''png'' is a safe choice; for advanced users: ''webp'' ;) ). |
- | A: Linux only. | + | |
- | <note> | + | As stated before, this is important for automating the grading process, so please respect it! |
- | **TODO:** Collect questions from Teams / lab and add them here. | + | |
- | </note> | + | |
- | <hidden> | + | You may add any additional routes as required by your HTML+CSS-based UI (described below). |
- | Reference implementation: | + | You may also add additional parameters (but they must be optional!) to the encode / decode endpoints if you with to make the steganography technique customizable (e.g., use more significant bits or encode some redundancy, for bonuses ;) ). |
- | <file python revelio.py> | + | |
- | #!/usr/bin/python3 | + | |
- | from PIL import Image | + | ==== User Interface ==== |
- | import argparse | + | |
- | import itertools | + | |
- | def main(): | + | The web frontend should present a (somewhat) friendly user interface with, at minimum, a simple front page (with a basic description) and the two upload form pages for steganography encoding / decoding. |
- | # parse command line arguments | + | |
- | parser = argparse.ArgumentParser( | + | |
- | prog = 'revelio.py', | + | |
- | description = 'highlights specific color channel bit layers') | + | |
- | parser.add_argument('FILE') | + | |
- | parser.add_argument('-r', metavar='HEX', type=str, default='0x00', nargs=1, | + | |
- | help='red channel mask') | + | |
- | parser.add_argument('-g', metavar='HEX', type=str, default='0x00', nargs=1, | + | |
- | help='green channel mask') | + | |
- | parser.add_argument('-b', metavar='HEX', type=str, default='0x00', nargs=1, | + | |
- | help='blue channel mask') | + | |
- | parser.add_argument('--boost', action='store_true', | + | |
- | help='boost channel if !0') | + | |
- | args = parser.parse_args() | + | |
- | # convert hex strings to int masks | + | All pages must have a common menu bar (hint: use a Jinja2 template!) directly linking to the three pages (index / encode / decode). |
- | r_mask = int(args.r[0], 16) | + | We recommend the use of a CSS framework (e.g., [[https://getbootstrap.com/docs/5.3/getting-started/introduction/|Bootstrap]]) for easily adding vertical / horizontal menus to a HTML page. |
- | g_mask = int(args.g[0], 16) | + | |
- | b_mask = int(args.b[0], 16) | + | |
- | # instantiate PIL Image object from file; access pixels | + | The image upload forms should contain at least one file input, a textbox for the message to encode (for the encoding page) or a box to display the decoded image (for the decoding page). You should also show the last image uploaded to the server side-by-side with the form (e.g., as floating image; use the ''/image/last/*'' REST endpoints for this). |
- | im = Image.open(args.FILE) | + | |
- | px = im.load() | + | |
- | if not px: | + | |
- | exit(-1) | + | |
- | # apply mask to each pixel | + | The design (aspect) of the web pages does not matter as long as it meets the requirements above and one is able to determine which link to press for accessing the required steganography encode / decode functions. |
- | for i, j in itertools.product(range(im.size[0]), range(im.size[1])): | + | |
- | # apply mask to pixel RGB channel | + | |
- | r, g, b = px[i, j] | + | |
- | r &= r_mask | + | |
- | g &= g_mask | + | |
- | b &= b_mask | + | |
- | # boost channel if !0 after mask | + | ==== Containerization ==== |
- | if args.boost: | + | |
- | r = 0xff if r != 0 else 0x00 | + | |
- | g = 0xff if g != 0 else 0x00 | + | |
- | b = 0xff if b != 0 else 0x00 | + | |
- | # update pixel | + | In order for your web application to be easily deployable / shared (with us :P), you must add a Dockerfile installing all of its dependencies (use PIP requirements, ofc!). |
- | px[i, j] = (r, g, b) | + | You may start from any base image, although we recommend ''alpine'' due to its low disk footprint. |
- | # display updated image | + | Thus, a containerized solution must work using the following steps: |
- | im.show() | + | * the ''docker build -t iap-tema2 .'' command should run successfully; |
- | im.close() | + | * ''docker run -p 8080:80 -it iap-tema2'' should start the Flask server and make it accessible on ''http://localhost:8080''. |
- | if __name__ == '__main__': | + | <note> |
- | main() | + | Please follow the archiving conventions and have everything (especially the ''Dockerfile'') inside its root directory! |
- | </file> | + | </note> |
- | Script for embedding secret images: | + | ===== Grading ===== |
- | <file python hide.py> | + | |
- | #!/usr/bin/python3 | + | |
- | from PIL import Image | + | The base assignment constitutes **4p** out of your final grade (100p homework = **4p** final grade). |
- | import argparse | + | The 100p are split between the following tasks: |
- | import itertools | + | |
- | # string to channel idx | + | * **[40p] Stegano encode / decode script:** either working in console (via CLI scripts) or web-based (using Flask + HTML), as long as it works as intended! |
- | channel_d = { 'red' : 0, 'green' : 1, 'blue' : 2 } | + | * **[40p] Web UI (HTML Forms + Flask):** web-based frontend for uploading images and encoding / decoding secret messages using the described technique (Note: it must respect the given specification!); |
+ | * **[20p] Docker container:** write a (working) Dockerfile for easily building & running the server; | ||
+ | * **[up to 10p] Bonus ideas:** | ||
+ | * A nice UX ;) | ||
+ | * Implement both a CLI (using ''argparse'') + Web frontend using a modular approach (code sharing!); | ||
+ | * Extra steganography-related functionality (e.g., by adding additional form fields); e.g., add parameters for visualizing the data of specific color channels of an image using binary masking, use specific color channels / multiple bits for encoding the data etc. | ||
- | def main(): | + | Write a README (.txt / .md) containing a description of your implementation, design choices, any third party libraries used (e.g., PIL), challenges you encountered, etc. Feel free to add your feedback here as well. |
- | # parse command line arguments | + | |
- | parser = argparse.ArgumentParser( | + | |
- | prog = 'hide.py', | + | |
- | description = 'hides image inside pixel bit layer') | + | |
- | parser.add_argument('-c', '--channel', metavar='{red,green,blue}', type=str, | + | |
- | nargs=1, required=True, help='channel to hide image in') | + | |
- | parser.add_argument('-m', '--mask', metavar='HEX', type=str, nargs=1, | + | |
- | required=True, help='hidden bit layer mask') | + | |
- | parser.add_argument('-t', '--thresh', metavar='UINT8', type=int, | + | |
- | default=128, nargs=1, | + | |
- | help='threshold (any ch) for activating secret pixel') | + | |
- | parser.add_argument('--hoff', metavar='PX', type=int, default=[0], nargs=1, | + | |
- | help='horizontal offset for secret image') | + | |
- | parser.add_argument('--voff', metavar='PX', type=int, default=[0], nargs=1, | + | |
- | help='vertical offset for secret image') | + | |
- | parser.add_argument('-d', '--debug', action='store_true', | + | |
- | help='show resulting secret image') | + | |
- | parser.add_argument('-s', '--secret', metavar='FILE', type=str, nargs=1, | + | |
- | required=True, help='secret image') | + | |
- | parser.add_argument('-p', '--public', metavar='FILE', type=str, nargs=1, | + | |
- | required=True, help='public image') | + | |
- | parser.add_argument('-o', '--output', metavar='FILE', type=str, nargs=1, | + | |
- | help='output image name (optional)') | + | |
- | args = parser.parse_args() | + | |
- | # sanity check | + | The project's source code (i.e., no binary / generated files need to be included) must be archived (''.zip'' please) and make sure the scripts (incl. Dockerfile) are placed directly in the root folder (i.e. depth 0) of the archive! Otherwise, the grading process will be slower => lower score :( |
- | if args.channel[0] not in channel_d: | + | |
- | print('Please provide a valid color channel (-c)') | + | |
- | exit(-1) | + | |
- | # arg type conversion & easy access | + | <note important> |
- | channel = channel_d[args.channel[0]] | + | **NOTE:** Assistants are free do deduct points for bad / illegible code! |
- | mask = int(args.mask[0], 16) | + | |
- | thresh = args.thresh | + | |
- | hoff = args.hoff[0] | + | |
- | voff = args.voff[0] | + | |
- | debug = args.debug | + | |
- | # open secret image | + | Also, please double-check if you followed all naming conventions! |
- | s_im = Image.open(args.secret[0]) | + | </note> |
- | s_px = s_im.load() | + | |
- | if not s_px: | + | |
- | exit(-1) | + | |
- | # activate pixel depending on threshold (any channel) | + | ===== Resources ===== |
- | for i, j, in itertools.product(range(s_im.size[0]), range(s_im.size[1])): | + | |
- | # for debug mode, we boost channel value to max | + | |
- | # for normal mode, we set the value used in final image | + | |
- | px = [ 0, 0, 0 ] | + | |
- | active_val = 0xff if debug else mask | + | |
- | px[channel] = active_val if max(s_px[i, j]) >= thresh else 0x00 | + | |
- | s_px[i, j] = tuple(px) | + | |
- | # if debug, show result and quit | + | * [[https://pillow.readthedocs.io/en/stable/|pillow]] is a image processing module that has support for many image formats and grants the developer access to the pixel data. You can install it using ''pip3'' (see our [[:ii:labs:03|IDST labs]] for info regarding ''pip'' and virtual environments). |
- | if debug: | + | * [[https://docs.python.org/3/library/argparse.html|argparse]] is a command line argument parser (useful if you want nice CLI scripts configurable with options). |
- | s_im.show() | + | * [[https://flask.palletsprojects.com/en/2.2.x/|Flask]] web framework for Python. |
- | s_im.close() | + | * [[https://jinja.palletsprojects.com/en/3.1.x/templates/|Jinja2]] template engine (integrated with Flask). |
- | exit(0) | + | * [[https://docs.docker.com/get-started/|Docker]] container engine (Getting started tutorial). |
- | # open public image | + | ===== FAQ ===== |
- | p_im = Image.open(args.public[0]) | + | |
- | p_px = p_im.load() | + | |
- | if not p_px: | + | |
- | exit(-1) | + | |
- | # start by clearing the bit layer for our target channel | + | **Q: Can I write the tool in something other than Python?** \\ |
- | for i, j, in itertools.product(range(p_im.size[0]), range(p_im.size[1])): | + | A: No. You have the [[:ii:assignments:s2:chip8|Chip8 Bonus Assignment]] in C, if you want to be closer to the metal ;) |
- | px = list(p_px[i, j]) | + | |
- | px[channel] = px[channel] & ~mask | + | |
- | p_px[i, j] = tuple(px) | + | |
- | # calculate secret image effective height / width | + | **Q: What platform will this assignment be tested on?** \\ |
- | es_width = min(s_im.size[0], p_im.size[0] - hoff) | + | A: Linux (though, you don't need to use any platform-specific APIs). |
- | es_height = min(s_im.size[1], p_im.size[1] - voff) | + | |
- | # embed secret image in public image | + | <note> |
- | for i, j in itertools.product(range(es_width), range(es_height)): | + | **TODO:** Collect questions from Teams / lab and add them here. |
- | px = list(p_px[i + hoff, j + voff]) | + | </note> |
- | px[channel] = px[channel] | s_px[i, j][channel] | + | |
- | p_px[i + hoff, j + voff] = tuple(px) | + | |
- | + | ||
- | # if output file name provided, save; otherwise, show | + | |
- | if args.output: | + | |
- | p_im.save(args.output[0]) | + | |
- | else: | + | |
- | p_im.show() | + | |
- | # cleanup | ||
- | s_im.close() | ||
- | p_im.close() | ||
- | if __name__ == '__main__': | ||
- | main() | ||
- | </file> | ||
- | </hidden> |