Executable PNGs: run images as programs



This Image and Simultaneous Program



A few weeks ago I read about the PICO-8 , a fictional game console with great limitations. Of particular interest to me was an innovative way of distributing her games - encoding their PNG image. It includes everything - the game code, resources, everything. The image can be anything: screenshots from the game, cool art or just text. To load the game, you need to transfer the image to the input of the PICO-8 program, and you can start playing.



It got me thinking: Would it be cool if you could do the same with programs on Linux? No! I understand you will say that this is a dumb idea, but I did it anyway, and below is a description of one of the dumbest projects I worked on this year.



Coding



I'm not quite sure what the PICO-8 does exactly, but I guess it probably uses steganographic techniques that hide data in the raw bytes of the image. There are many resources on the Internet explaining how steganography works, but its very essence is quite simple: the image in which you want to hide the data consists of bytes and pixels. Pixels are composed of three red, green and blue (RGB) values, represented as three bytes. To hide the data ("payload"), we essentially "mix" the payload bytes with the image bytes.



If you simply replace the bytes of the image with the bytes of the payload, then areas with distorted colors will appear in the image, because they will not match the colors of the original image. The trick is to be as inconspicuous as possible, to hide information out of the blue . This can be done by distributing the payload bytes across the cover image bytes, hiding them in the least significant bits . In other words, make small changes to the byte values ​​so that the color changes are not strong enough for the human eye to perceive.



Let's say our payload is a letter H



represented in binary as 01001000



(72), and the image contains a set of black pixels.





The bits of the input bytes are distributed over the 8 output bytes by hiding them in the



least significant bit. At the output, we get a few pixels that will be slightly less black than before, but can you tell the difference?





Pixel colors have been slightly tweaked.



Perhaps an extremely experienced color connoisseur will be able to tell the difference, but in real life, such tiny shifts are only visible to the machine. To get our top secret letter H



, you just need to read 8 bytes of the resulting image and assemble them again into 1 byte. Obviously, hiding a single letter is a stupid idea, but the scale of the transmission can be freely increased. Let's say you transmit a super-sectoral proposal, a copy of War and Peace , a link to Soundcloud, a Go compiler - the only limitation will be the number of bytes available in the image, because there must be at least 8 times more of them than in the input information.



Hiding programs



So, back to our idea of ​​Linux executables in the image. If you think of executable files as simply bytes, then it is clear that they can be hidden in images, just like the PICO-8 does.



Before implementing this, I decided to write my own steganography library and tool that supports encoding and decoding data in PNG. Of course, there are many ready-made steganographic libraries and tools out there, but I learn best when I do my own thing.



$ 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






Since everything is written in Rust , it was not at all difficult to compile this in WASM, so you can experiment on your own.



So now we can embed data by adding executables to images. But how do we run them?



Run the image



The easiest way would be to just run the above tool, execute the decode



data into a new file, change the rights with chmod +x



, and then run it. It will work, but it will be too boring. I wanted to do something in the PICO-8 style - we pass a PNG image to some entity, and it does the rest.



However, as it turns out, you can't just load an arbitrary set of bytes into memory and tell Linux to jump to it ... at least not directly. However, you can use some simple tricks to get it done.



memfd_create



After reading this post, it became obvious that you can create a file in memory and mark it as executable.



Wouldn't it be nice to just take a block of memory, write the binary data there and run it without patching the kernel, rewriting execve (2) in userland, or loading the library into another process?


This method uses the memfd_create (2) system call to create a file in the namespace of /proc/self/fd



your process and load the data you need into it using write



. I spent quite a lot of time figuring out the libc bindings with Rust to get it all working, and it was difficult for me to understand the data types being passed, the documentation on these Rust bindings didn't help much.



However, I managed to get something working.



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,
    );
}
      
      





A call /proc/self/fd/<fd>



as a child 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();
      
      





With these building blocks in hand, I wrote a pngrun program to run images. Basically, it does the following:



  1. Takes an image from a steganographic tool that contains our binary file and arguments
  2. Decodes it (i.e. extracts and reassembles bytes)
  3. Creates a file in memory with memfd_create



  4. Places the bytes of a binary file into a file in memory
  5. Calls the file /proc/self/fd/<fd>



    as a child process, passing all arguments from the parent.


That is, you can run it like this:



$ pngrun htop.png

<htop output>

$ pngrun go.png run main.go

Hello world!






Upon completion, the pngrun



file in memory is destroyed.



binfmt_misc



However pngrun



, typing is annoying every time , so the last simple trick in this pointless project was to use binfmt_misc - a system that allows you to "execute" files based on their file type. I think this feature was primarily designed for interpreters / virtual machines like Java. Instead of typing, java -jar my-jar.jar



just enter ./my-jar.jar



and this will call the process java



to run the JAR. However, the file my-jar.jar



must first be marked executable.



That is, add an entry for binfmt_misc pngrun



to be able to run any png



with the flag set x



, you can like this:



$ 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 is the meaning of the project



Well, it doesn't really make much sense. I was tempted by the idea of ​​creating PNG images that could run programs, and I developed it a bit, but the project was still interesting. There is something exciting about being able to distribute software as images - think of the funky cardboard boxes of PC software with graphics on the front. Why not bring them back? (Although not really worth it.)



The project is very dumb and has many flaws that make it completely meaningless and impractical. The main flaw is that there must be a stupid program for it to work on the machine pngrun



. However, I noticed some oddities in programs like clang



... I coded it into this funny LLVM logo, and while it works fine, it crashes when trying to compile.





$ ./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 result of the file being anonymous, and the problem can be solved if I had an interest in studying it.



Why else is this project stupid



Many binaries are quite large, and given that they need to be written to images, the size of the graphics must be large and the resulting files are comically huge.



In addition, most software does not consist of just one executable file, so the dream of distributing PNGs will fail for more complex programs like games.



Conclusion



This is probably the dumbest project I've worked on this year, but it was definitely fun, I learned about steganography memfd_create



, binfmt_misc



and played around with Rust a little more.



All Articles