GIF Fun 6: Hide Your Data
In his latest video, Benn Jordan talks about how you can go hiding “shit in inconspicuous things.” You should give his video a watch; it is very interesting, talking about how to modulate audio and video signals to inject your own data without it being noticeable to viewers, as well as the tradeoffs between how much you can store, how clean the result is, and if your secrets will survive compression. Watching that video made me remember something I had thought about before in terms of using GIFs for a Capture The Flag event: how and where one can put extra data into a GIF.
Where to Hide Your Stuff
I will start with some easy and obvious ways that you can put something into a GIF such that someone looking at the rendered images might not see it.
Ways that technically are findable by just looking at the data without any additional processing or such might be viable because, who would look at the data inside a gif?
Then, I will go into some more sophisticated places where data might be hidden, such that even if someone opens up the GIF in, let’s say, my GIF editor, it would not be so obvious that there is something secret hidden in it.
Plain Text & Comments
Because GIFs have the option to add comment blocks, you can just write your data into one as a string. The same goes for Plain Text blocks; just that those in theory could be displayed; if you ever find a software that does display them. These two ways are the easiest to do it, as they use what is already in the GIF, and the only extra step is to convert your data into a string. But they also leave your data out in the open, and a comment with some random base64-encoded string in them would instantly raise suspicions if anyone bothered to check.
Application Extension
The GIF standard allows for arbitrary Application Extension blocks.
Each of these is made out of some info for identification & authentication and then just whatever data you want packed into some sub-data blocks.
There is no size limit, and you can chuck your raw bytes in there, not even converting them in any way beforehand like you would need for the Comment method.
I wrote about the Application Extension block in no, wait, I did not, but maybe will at some point.
In general, Application Extension blocks are rare, and the one you are most likely to have in your GIFs is the NETSCAPE2.0 one that tells software how often to loop a GIF (the standard is looping it infinite times).
Because there is not really any hard convention of who has the authority to create new Application extensions, we can just make up our own, maybe name it something like “SECRET10” (the identifier should be 8 ASCII characters), and give it a random authentication code.
The data is then added into the application data after being packed into sub-blocks.
The upside is that we don’t even need a string representation of our data, and depending on who processes the GIF, application extensions won’t be stripped. Of course, a random application extension will still ring alarm bells if anyone sees it.
The image
Just like Benn Jordan does for videos, we too could introduce subtle differences in colors to transport our data as a continuous signal. As long as the platform we upload to does not recompress its inputs, this would work. However, due to the limited number of colors per image, it would be harder to modulate a signal into the images without it getting too obvious. And of course, depending on what we try to hide, we might need a lot of images, and the GIF could grow rather big.
On the upside, a conversion of the GIF into another format might not destroy the signal, as images themselves persist, and compression shouldn’t be too harsh on the already low number of colors. In general, this method would be the safest way to do it if you have to deal with uploading the GIF anywhere where it might get recompressed or reformatted.
Table Based Image Data
The way that images are compressed via LZW includes an “end of data” code with which the compressed image ends and that signals that no more codes to decompress follow. But the Table Based Image Data block consists of general sub-data blocks, meaning we can add our own data after the “end of data” code, and in theory that data should be ignored by any decompressor that holds true to the specification.
So we simply compress the image as normal, but when packaging the result into sub data blocks, we attach our secret data to the end. Due to the bit-packing within LZW, it would also not be too obvious that not all the data is for an image. While the “end of data” code will not be compressed due to it being a special symbol that only occurs once, it will be packed among the other data, so finding it within random compressed data plus some additional attached data would be hard without running a full decompression. Our hidden data would only be visible to anyone that runs a special decompressor, which outputs all the data after the actual compressed image.
The main downside here would be that if that particular image ever gets changed, the data most likely would be lost. But in terms of hiding stuff, it is one of the more clever ways for sure.
The Index Sequence
One final thing we can do is hide our secret message inside the index sequence itself. As GIFs store not the pixel colors directly but a sequence of indices from the color table, we can even hide a more obvious visual message by using two indices that have the same color in the table, and then later changing one of them would reveal our message. Besides that, we can also come up with convoluted ways to encode messages into the index sequence, but this likely will make it obvious, as keeping a consistent image at the same time would be way harder than our other methods.

What might it say?
Does it work (on Bluesky)?
Nope.
At least not the easy-to-test methods.
Bluesky seemingly takes any GIF that is uploaded and converts it into a video.
When the post is later displayed, all you get is a stream via m3u8.
A look into the m3u8 playlist mentions ‘CODECS=“avc1.64001e”’; so there isn’t any GIF around for us to grab and download anymore at this point, even though the display will have a little “GIF” text in the bottom right corner.
Fun fact: even for GIFs added via the KLIPY integration, they are linked to WebM or MP4 files, so you never actually get a GIF on your end, although KLIPY does indeed have the files as GIFs.
The reason that GIFs are not kept around, is probably bandwidth.
GIFs can grow relatively big compared to video files, and most software does not have a way to pause a GIF, whereas videos can be paused at any time.
So for the user experience, it is easier to use a video format, hide the normal video controls, and allow pausing via a click on the video than to use an actual GIF.
I don’t fault the developers for doing things this way, but that means that if you want to transport your secret messages, uploading GIFs to social media might not be the best way to do it.
And that is all for this post. Thank you for reading and see you next time.