Executable PNGs
A few weeks ago I was reading about PICO-8, a fantasy games console with limited constraints. What really piqued my interest about it was the novel way games are distributed, you encode them into a PNG image. This includes the game code, assets, everything. The image can be whatever you want, screenshots from the game, cool artwork or just text. To load them you pass the image as input to the PICO-8 program and start playing.
This got me thinking, wouldn’t it be cool if you could do that for programs on Linux? No! I hear you cry, that’s a dumb idea, but whatever, herein lies an overview of possibly the dumbest things I’ve worked on this year.
Encoding
I’m not entirely sure what PICO-8 is actually doing, but at a guess it’s probably use Steganography techniques to ‘hide’ the data within the raw bytes of the image. There are a lot of resources out there that explain how Steganography works, but the crux of it is quite simple, your image your want to hide data into is made up of bytes, an image is made up of pixels. Pixels are made up of 3 Red Green and Blue (RGB) values, represented as 3 bytes. To hide your data (the “payload”) you essentially “mix” the bytes from your payload with the bytes from the image.
If you just replaced each byte in your cover image with the bytes from your payload, you would end up with sections of the image looking distorted as the colours probably wouldn’t match with what your original image was. The trick is to be as subtle as possible, or hide in plain sight. This can be achieved by spreading your payload bytes over the bytes of the cover image by using the least significant bits to hide them in. In other words, make subtle adjustments to the byte values so the colour changes are not drastic enough to be perceptive by the human eye.
For example if your payload was the letter H
, represented as 01001000
in binary (72), and your image contained a series of black pixels
The output is two-and-a-bit pixels that are slightly less black than before, but can you tell the difference?
Well, an exceptionally trained colour connoisseur might be able to, but in reality these subtle shifts can really only be noticed by a machine. Retrieving your super secret H
is just a matter of reading 8 bytes from the resulting image and re-assembling them back into 1 byte. Obviously hiding a single letter is lame, but this can scale to anything you want, a super secret sentence, a copy of War and Peace, a link to your soundcloud, the go compiler, the only limit is the amount of bytes available in your cover image as you’ll require at least 8x whatever your input is.
Hiding programs
So, back to the whole linux-executables-in-an-image thing, that old chestnut. Well, seeing as executables are just bytes, they can be hidden in images. Just like in the PICO-8 thing.
Before I could achieve this I decided to write my own Steganography library and tool to support encoding and decoding data into PNGs. Yes, there are lots of steganography libraries and tools out there but I learn better by building.
As it’s all written in Rust it wasn’t that difficult to compile to WASM, so feel free to play with it here:
Anyway, now that can embed data, including executables into an image, how do we run them?
Get it running
The simple option would be to just run the tool above, decode
the data into a new file, chmod +x
it and then run it. It works but that’s not fun enough. What I wanted was something similar to the PICO-8 experience, you pass something a PNG image and it takes care of the rest.
However, as it turns out, you can’t just load some arbitrary set of bytes into memory and tell Linux to jump to it. Well, not in a direct way anyway, but you can use some cheap tricks to fudge it.
memfd_create
After reading this blogpost it became apparent to me you can create an in-memory file and mark it as executable
Wouldn’t it be cool to just grab a chunk of memory, put our binary in there, and run it without monkey-patching the kernel, rewriting execve(2) in userland, or loading a library into another process?
This method uses the syscall memfd_create(2) to create a file under the /proc/self/fd
namespace of your process and load any data you want in it using write
. I spent quite a while messing around with the libc bindings for Rust to get this to work, and had a lot of trouble understanding the data types you pass around, the documentation for these Rust bindings doesn’t help much.
I got something working eventually though
Invoking /proc/self/fd/<fd>
as a child process from the parent that created it is enough to run your binary.
Given these building blocks, I wrote pngrun to run the images. It essentially…
- Accepts an image that has had our binary embedded in it from the steganography tool, and any arguments
- Decodes it (i.e. extracts and re-assembles the bytes)
- Creates an in-memory file using
memfd_create
- Puts the bytes of the binary into the in-memory file
- Invokes the file
/proc/self/fd/<fd>
as a child process, passing any arguments from the parent
So you can run it like this
Once pngrun
exits the in-memory file is destroyed.
binfmt_misc
It’s annoying having to type pngrun
every time though, so my last cheap trick to this pointless gimmick was to use binfmt_misc, a system that allows you to “execute” files based on its file types. I think it was mainly designed for interpreters/virtual machines, like Java. So instead of typing java -jar my-jar.jar
you can just type ./my-jar.jar
and it will invoke the java
process to run your JAR. The caveat is your file my-jar.jar
needs to be marked as executable first.
So adding an entry to binfmt_misc for pngrun
to attempt to run any png
files that have the x
flag set was as simple as
What’s the point
Well, there isn’t one really. I was seduced by the idea of making PNG images run programs and got a bit carried away with it, but it was fun none the less. There’s something amusing to me about distributing programs as an image, remember the ridiculous cardboard boxes PC software used to come in with artwork on the front, why not bring that back! (lets not)
It’s really dumb though and comes with a lot of caveats that make it completely pointless and impractical, the main one being needing the stupid pngrun
program on your machine. But I also noticed some weird stuff around programs like clang
. I encoded it into this fun LLVM logo and while it runs OK, it fails when you try to compile something.
This is probably a product of the anonymous file thing, which can probably be overcome if I could be bothered to investigate.
Additional reasons why this is dumb
A lot of binaries are quite large, and given the constraints of needing to fit them into an image, sometimes these need to be big, meaning you end up with comically large files.
Also most software isn’t just one executable so the dream of just distributing a PNG kinda falls flat for more complex software like games.
Conclusion
This is probably the dumbest project I’ve worked on all year but it’s been fun, I’ve learned about Steganography, memfd_create
, binfmt_misc
and played a little more with Rust.