djhworld

thoughts


Executable PNGs


It's an image and a program

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 bits from the input bytes are spread across 8 output bytes by hiding them in the least significant bit

The output is two-and-a-bit pixels that are slightly less black than before, but can you tell the difference?


The pixels have been adjusted in colour slightly.

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.

$ stegtool encode \
--cover-image htop-logo.png \
--input-data /usr/bin/htop \
--output-image htop.png
$
$ echo "Super secret hidden message" | stegtool encode \ 
--cover-image image.png \
--output-image image-with-hidden-message.png
$ stegtool decode --image image-with-hidden-message.png
Super secret hidden message

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

unsafe {
    let write_mode = 119; // w
    // create executable in-memory file
    let fd = syscall(libc::SYS_memfd_create, &write_mode, 1);
    if fd == -1 {
        return Err(String::from("memfd_create failed"));
    }

    let file = libc::fdopen(fd, &write_mode); 

    // write contents of our binary
    libc::fwrite(
        data.as_ptr() as *mut libc::c_void, 
        8 as usize,
        data.len() as usize,
        file,
    );
}

Invoking /proc/self/fd/<fd> as a child process from the parent that created it is enough to run your binary.

let output = Command::new(format!("/proc/self/fd/{}", fd))
    .args(args)
    .stdin(std::process::Stdio::inherit())
    .stdout(std::process::Stdio::inherit())
    .stderr(std::process::Stdio::inherit())
    .spawn();

Given these building blocks, I wrote pngrun to run the images. It essentially…

  1. Accepts an image that has had our binary embedded in it from the steganography tool, and any arguments
  2. Decodes it (i.e. extracts and re-assembles the bytes)
  3. Creates an in-memory file using memfd_create
  4. Puts the bytes of the binary into the in-memory file
  5. Invokes the file /proc/self/fd/<fd> as a child process, passing any arguments from the parent

So you can run it like this

$ pngrun htop.png
<htop output>
$ pngrun go.png run main.go
Hello world!

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

$ cat /etc/binfmt.d/pngrun.conf
:ExecutablePNG:E::png::/home/me/bin/pngrun:
$ sudo systemctl restart binfmt.d
$ chmod +x htop.png
$ ./htop.png
<output>

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.

$ ./clang.png --version
clang version 11.0.0 (Fedora 11.0.0-2.fc33)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /proc/self/fd
$ ./clang.png main.c
error: unable to execute command: Executable "" doesn't exist!

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.