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:
- Takes an image from a steganographic tool that contains our binary file and arguments
- Decodes it (i.e. extracts and reassembles bytes)
- Creates a file in memory with
memfd_create
- Places the bytes of a binary file into a file in memory
- 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.