Somehow I got the idea that it would be nice to have a way to send secret messages disguised as ordinary images. I called the result Jailbird.
Once you found yourself locked in a cell and you needed to send information to the will in order to turn one little business, so that the security didn’t notice anything? Well, you found the right solution!
Okay, okay, I'm joking, this is just an experiment.
Today I would like to show you how you can save Shakespeare's Hamlet in an image almost unnoticed. (Hehe, I think many people had the problem of smuggling Hamlet somewhere ...)
Source code is available on Github: https://github.com/ClanCatsStation/Jailbird
To get started, we need to know how much space we need. (Size matters ;)).
I took Hamlet and put it in hamlet.txt
. Then he created a php script that called size.php
.
To remove php
from the script call in the command line, install the interpreter to run:
#!/usr/bin/env php <?php
To get the data, read the content from STDIN
:
echo strlen(stream_get_contents(STDIN));
Now run the command:
$ cat hamlet.txt | ./size.php
We 175132
length of the string / number of bytes equal to 175132
. Let's try to archive:
echo strlen(gzcompress(stream_get_contents(STDIN), 9));
And we get: 70681
bytes.
Jailbird can store 1 bit per color per pixel .
Total 565'448
bits, which means that we need 188'483
pixels. Or an image size of at least 435x435 pixels.
Add these calculations to the script size.php
, which makes it easy to know what size image we need.
#!/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);
Why did I add 16 bytes to the content length? We need to determine the end of the data by the presence of a sequence of characters, I decided that this would be the line @endOfJailbird;
which contains 16 characters.
Actually, what we gathered here for. (And then I sold something)
The easiest way to embed data in an image, I thought, is to convert it into a binary string.
#!/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 returns a byte to its ASCII representation.
Then, using the decbin
function, decbin
convert the resulting number into a binary one and wrap it in sprintf
to save the leading zeros.
At the output we get a fairly large string of zeros and ones.
string(565448) "01111000110110101010110011111101110010011001001011100011010110001101001000101100000011001010111001111111001111000000010101111101110100111101110011010000111111010000000111101000000010111001011111001000001100011111110011011110110010001000100010111100000110010101000110010101010100101111110101001001001011010100000000000010001001001001000100001110000000100010110000001100110011100110010000111101011111011001101110101010100110100001110110000000011101001100111111111010101111101101101111011101001000100101010100011001"...
Here we need an image in which we will record. Pass the path to the image as the first argument to the inject.php
script.
// array_shift($argv); // $imagePath = array_shift($argv);
Check if we have access to the image:
if ((!file_exists($imagePath)) || (!is_readable($imagePath))) { die("The given image does not exist or is not readable.\n"); }
The write command will be:
$ cat hamlet.txt | ./inject.php cats.png
The image of cats.png
from (original, approx. Lane) KDPV:
Now run the loop:
// GD $image = imagecreatefrompng($imagePath); $imageWidth = imagesx($image); $imageHeight = imagesy($image); // , // $dataIndex = 0; // Y for ($iy = 0; $iy < $imageHeight; $iy++) { // for ($ix = 0; $ix < $imageWidth; $ix++) { $rgb = imagecolorat($image, $ix, $iy); // rgb $rgb = [($rgb >> 16) & 0xFF, ($rgb >> 8) & 0xFF, $rgb & 0xFF]; // for($ic = 0; $ic < 3; $ic++) { // , if (!isset($data[$dataIndex])) { break 2; } $color = $rgb[$ic]; $bit = $data[$dataIndex]; // // } imagesetpixel($image, $ix, $iy, imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2])); } }
This code simply passes through the pixel colors sequentially and saves the color change back to the pixel.
But the most important thing:
$negative = ($color % 2 == 0);
This short line of code tells us about the current color of the current pixel is it even or uncent .
And $bit = $data[$dataIndex];
tells us whether the current color value should be even or odd.
Thus, we create another layer of data on top of the image simply by rounding the color values ​​to an even or odd number.
Now, all we need to do is update the color values:
// 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 actually everything! It remains only to save the image:
imagepng($image, dirname($imagePath) . '/jailbirded_' . basename($imagePath), 0);
The reverse process - data extraction is quite simple, after we have understood the implementation.
Create a new file extract.php
.
The data extraction script will also receive the path to the image as 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 do not need to modify anything, just check if the value is even or not.
// 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'; } } } }
The $data
variable contains the raw data that we need to convert to bytes:
$content = ''; foreach(str_split($data, 8) as $char) { $content .= chr(bindec($char)); }
The resulting content is archived, and ordered by the endOfJailbird value. We can drop everything that comes after it:
// 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;')));
As a result, we can run the script:
$ ./extract.php jailbirded_cats.png
And get the beautiful Shakespearean play back.
If you put 2 images side by side, there’s probably no difference, although you may notice a slight blur:
But there are a couple of moments:
The fact is that we save the image without compression, otherwise it will lead to data loss. While the source file weighs 307 KB , after saving the data we get 759 KB . But I, unfortunately, do not see any solutions to this problem.
Also, when sending an image to, for example, facebook, reddit or twitter, the image will be compressed on the servers of these services and the data will most likely be lost.
In the end it was a pretty fun experiment, I had a good time with him for a few hours. I hope you also find this idea interesting and that you have not spent this time in vain. (Otherwise: hehe, I stole your time!)
Never gonna give you up
Source: https://habr.com/ru/post/306902/
All Articles