6 min read

Storing Shakespeare's "Hamlet" invisibly inside an image

So, a long time ago, I came up with an idea to send secret messages masked as normal images. I called it Jailbird.

Have you ever found yourself stuck in jail and needed to send out some information to run your business, without the guards noticing? Well, then this library is the solution you have been looking for!

Obviously, I'm joking; this thing is an experiment.

This practice is called Steganography.

Today, I want to show you how you could store Shakespeare's Hamlet inside an image with almost no visible impact on it. (HaHaaa, well, I am entirely sure this solves the problem for so many people who had to smuggle Hamlet into whatever...)

Use the Source, Luke

The entire source is available on Github: https://github.com/ClanCatsStation/Jailbird

The size

To get started we need to know how much space we are going to need. (Size matters after all wink wink)
So I took the entire play from here, and put it into a text file hamlet.txt.

Then create a new php script, let's just call it size.php.

To remove the need of prefixing php on the command line set the shebang to the following:

#!/usr/bin/env php
<?php

We want to be able to pipe in our data so just read the contents of STDIN:

echo strlen(stream_get_contents(STDIN));

Now running the command:

$ cat hamlet.txt | ./size.php

Will give us a string length / number of bytes of 175132. There are many duplicates / patterns in the data so we probably can zip them down alot:

echo strlen(gzcompress(stream_get_contents(STDIN), 9));

Which will give us: 70681 bytes.

Jailbird can store 1 bit per color per pixel.

So we have 565'448 bits means we need 188'483 pixels. Or an image at least the size of 435x435 pixels.

Let's just add the calculation to our size.php so we can easly find out how large our image has to be.

#!/usr/bin/env php
<?php

$neededBits = (strlen(gzcompress(stream_get_contents(STDIN), 9)) + 16) * 8;

$neededPixels = ceil($neededBits / 3);

$neededSize = ceil(sqrt($neededPixels)); 

echo sprintf("bits: %s pixels: %s min-size: %sx%s \n", $neededBits, $neededPixels, $neededSize, $neededSize);

I've added 16 bytes to the base size of the content why? We need some type of and of data sequence and I decided to use @endOfJailbird; which has exactly 16 chars.

Injecting the data

And the action part you are probably here for. (I can smell, my own BULLSHIT)

Building the bit string

To be able to inject the data onto the image pixels I thought it would be the easiest way to first build the string of bits by converting byte per byte to a string representing the binary digits.

#!/usr/bin/env php
<?php

$content = gzcompress(stream_get_contents(STDIN), 9) . '@endOfJailbird; ';
$data = '';

for($i=0; $i<strlen($content); $i++)
{
	$data .= sprintf( "%08d", decbin(ord($content[$i])));
}

The ord function converts our byte into its ASCII value.

Then we convert it using decbin to binary and wrap it with sprintf to fix the prepending zeros.

This will give us a pretty massive string of zeros and ones:

string(565448) "01111000110110101010110011111101110010011001001011100011010110001101001000101100000011001010111001111111001111000000010101111101110100111101110011010000111111010000000111101000000010111001011111001000001100011111110011011110110010001000100010111100000110010101000110010101010100101111110101001001001011010100000000000010001001001001000100001110000000100010110000001100110011100110010000111101011111011001101110101010100110100001110110000000011101001100111111111010101111101101101111011101001000100101010100011001"...

Writing the bits

First we need the image we want to write on. We will just use the first argument of our inject.php as image path.


// we dont need the first argument
array_shift($argv);

// get image by argument
$imagePath = array_shift($argv);

And we need to check if we can access the given image:

if ((!file_exists($imagePath)) || (!is_readable($imagePath)))
{
	die("The given image does not exist or is not readable.\n");
}

So our inject command will be used like this:

$ cat hamlet.txt | ./inject.php cats.png

And the cats.png file looks the following:

cats.png

Now we can start the iteration:


// load the image with GD
$image = imagecreatefrompng($imagePath);
$imageWidth = imagesx($image);
$imageHeight = imagesy($image);

// we need to keep track of what data we have to write next so
// lets set a data index variable
$dataIndex = 0;

// and start iterating y
for ($iy = 0; $iy < $imageHeight; $iy++)
{
    // and x
    for ($ix = 0; $ix < $imageWidth; $ix++)
    {
        $rgb = imagecolorat($image, $ix, $iy);

        // split rgb to an array
        $rgb = [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF];

        // and for every color
        for($ic = 0; $ic < 3; $ic++)
        {
            // check if there is still data available
            if (!isset($data[$dataIndex]))
            {
                break 2;    
            }

            $color = $rgb[$ic];
            $bit = $data[$dataIndex];

            
            // the magic happening just 
            // where this comment is comes in the next
            // snippet
        }

        imagesetpixel($image, $ix, $iy, imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]));
    }
}

This code basically just goes over every pixels color and saves modifications of the color back into that pixel.

What?

The most important thing is the following:

$negative = ($color % 2 == 0);

This little line of code tells us if at the current pixel the current color's value is odd or even.

And $bit = $data[$dataIndex]; tells us if the current color value should be odd or even.

This way we can build another data layer over an image by simply rounding the color values to an odd or even number.

Now the only thing we have to do is apply that:

// should it be positive
if ($bit == '1')
{
    // should be positive but is negative
    if ($negative)
    {
        if ($color < 255) {
            $color++;
        } else {
            $color--;
        }
    }
}
// should be negative
else
{
    // should be negative but is positive
    if (!$negative)
    {
        if ($color < 255) {
            $color++;
        } else {
            $color--;
        }
    }
}

 // set the new color
$rgb[$ic] = $color;

// update the index
$dataIndex++;   

And thats it! Well don't forget to save the image:

imagepng($image, dirname($imagePath) . '/jailbirded_' . basename($imagePath), 0);

Encoding / Extracting the data

The reverse part is pretty simple after the injection.

Create a new file called extract.php.

The extractor will also just get the image path by an argument.

#!/usr/bin/env php
<?php

// we dont need the first argument
array_shift($argv);

// get image by argument
$imagePath = array_shift($argv);

if ((!file_exists($imagePath)) || (!is_readable($imagePath)))
{
	die("The given image does not exist or is not readable.\n");
}

Then we have almost the same iteration with the difference that we don't have to modify anything, we just check if the colors value is even or odd.

// load the image with GD
$image = imagecreatefrompng($imagePath);
$imageWidth = imagesx($image);
$imageHeight = imagesy($image);

// create an empty string where our data will end up 
$data = '';

// and start iterating y
for ($iy = 0; $iy < $imageHeight; $iy++)
{
	// and x
	for ($ix = 0; $ix < $imageWidth; $ix++)
	{
		$rgb = imagecolorat($image, $ix, $iy);

		// split rgb to an array
		$rgb = [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF];

		// and for every color
		for($ic = 0; $ic < 3; $ic++)
		{
			$color = $rgb[$ic];

			// what is the current pixel
			if ($color % 2 == 0)
			{
				$data .= '0';
			}
			else
			{
				$data .= '1';
			}
		}
	}
}

$data contains now the raw values that we have to reformat to bytes:

$content = '';

foreach(str_split($data, 8) as $char)
{
	$content .= chr(bindec($char));
}

Our content is still compressed, thats where our endOfJailbird sequence comes to use. We can just cut everything after the start of @endOfJailbird; from our content string and are left with clean compressed data:

// does the jailbird end of line exist?
if (strpos($content, '@endOfJailbird;') === false)
{
	die('Image does not contain any jailbird data.');
}

// cut the compressed data out,
// decompress it and print it.
echo gzuncompress(substr($content, 0, strpos($content, '@endOfJailbird;')));

After all this we can finally now run:

$ ./extract.php jailbirded_cats.png 

And get that beautiful play of Shakespeare back.

Conclusion

If we put the two images side by side we probably wont see any diffrence:

cats_compare.jpg

You might notice some unsharp edges. Well but the file size makes a diffrence!

The problem is that we have to save the image without compressing it in anyway. Because that would result in a data loss. While the original PNG is 307 KB the jailbirded one is 759 KB. I don't see anyway to get around this and that is the reason why this concept does not work out.

When sending images over for example facebook, reddit or twitter. The images will be opened and compressed by their system and would destroy the data.


In the end this was a cool experiment and entertained me for some hours and maybe some readers will find this idea also interesting and fun and not a complete waste of time. (Otherwise: haha I stole your time!)