Steganography With Images In PHP

15th January 2021 - 18 minutes read time

Steganography is the practice of placing a secret message inside another message and only by looking at the original message in a different way can the hidden message be seen. This might be as mundane as writing a page of text and hiding a message in the text using the first letter of every sentence. Only by collecting the first letter of each sentence together can the hidden message be seen and read.

Modern steganography is defined as hiding a message (or file) inside a file. For example, you might look at an image and see an image, but if a message has been hidden you would also be able to extract it if you know where to look.

There are a few tutorials out there that look at hiding files inside other files by injecting the files before the header of other files. For example, you can inject a text document into an image file by injecting the text before the header of the image. If you open the file using a text editor you'll see the text, if you open the file using a picture viewer you'll see the image. Whilst this heading approach is technically steganography as you only know to look for the text file if you know it's here, this isn't what I will be talking about here.

Image steganography is the practice of altering the colours of an image slightly as a mechanism of hiding data in the image. This is normally done by changing what is called the least significant bit of the colour of a pixel to change the colour very slightly. A pixel in an image is stored as a 24 bit binary number consisting of the red, green and blue channels. This might sometimes be 32 bit if an alpha (or transparency value is added).

Let's look at an example. When we extract the colour of a random pixel we get the number 9,613,560. This number contains information for the red, green and blue channels. If we extract this to be represented as a 24 bit binary number it is clear to see the different channels. 

RED      GREEN    BLUE
10010010 10110000 11111000
146      176      248

The least significant bit in this number is the bit on the far right hand side (in this case 0) of each channel of colour. If we were to change any channels least significant bit to a 1 then the colour wouldn't change very much. By contrast, if we changed the most significant bit, which is on the far left hand side, then the colour would change a lot. To demonstrate, let's change the blue channel's least significant number and look at the result.

Original Colour
RED      GREEN    BLUE
146      176      248
10010010 10110000 11111000

New Colour
RED      GREEN    BLUE
146      176      249
10010010 10110000 11111001

The original colour is a light blue colour with the hex value of #92B1F8, the new colour is still a light blue colour with the hex value of #92B1F9. So whilst we have changed the colour we haven't actually changed it much. In fact it would take a very good eye to be able to tell the difference. It is also possible to change the colour of the red and green channels in this way to hide data. Some steganography also looks at changing the last two significant bits of the colour channels, and whilst this changes the value more than changing one bit, it still makes the change difficult to detect. By storing two bits for each pixel we can also add more data to the image overall.

Up until this point we have just been encoding a single bit, so what happens if we want to encode more than this into the image. If we took a binary number of 101 we would need to encode that into three different pixels in the image as changing all three colour channels might change the image enough to be noticeable. As we run though each pixel in the image we would change the least significant bit in a single channel if the bit did not match out value. In this way we can encode data quite quickly.

10010010 10110000 11111000    10010010 10110000 11111001
10000010 10000010 11100010 -> 10000010 10000010 11100010
10010010 10110110 11111000    10010010 10110110 11111001

In order to have binary data to encode we first need to create our data. Let's say that we wanted to encode the letter 'a' in an image. The first step would be to convert that letter to its ASCII equivalent binary number. Looking up the ASCII tables on Wikipedia shows us that the binary number for 'a' is as follows.

a = 01100001

To convert a string to its ASCII equivalent using PHP we need to run each character through the ord() function that will give us an integer for that character that represents its ASCII code. Using this integer we then need to convert this to its binary equivalent so that it can be encoded into the image. To convert a decimal number to binary in PHP we use the decbin() function, which will return a string representing the binary number. The problem with this function is that it will return out binary number to the most significant digit. In other words, out 8 bit number that we need to represent 'a' will be turned into a 7 bit number as the initial 0 will be stripped off. As the return of this function is a string we pass it though the str_pad() function to force all of the 8 bit number to be present.

Here is the code that will take some text and convert it to its binary equivalent.

$message = 'text';
$binaryMessage = '';
for ($i = 0; $i < mb_strlen($message); ++$i) {
  $character = ord($message[$i]);
  $binaryMessage .= str_pad(decbin($character), 8, '0', STR_PAD_LEFT);
}

echo $binaryMessage;

This produces the following output.

01110100011001010111001101110100

With that number in hand we then need to inject it into our image. For the purposes of this I will be looking at the following image that I took myself whilst walking the dog a few months ago. This is a jpeg image that is 250 x 188 pixels so it's very small for the purposes of steganography, but as this is just a demo I didn't want a massive image on this screen.

Mushroom.

The code to inject a message into this image is below. Here I am using a simple version of steganography by only using the least significant bit of the blue channel in each pixel. I start from the top left hand side of the image and process each row of pixels at a time until the message has been encoded. The following method will take a message, convert it to a binary string and then inject it into the blue colour of the pixels running from the top left of the image. The string that we inject into the image contains the ASCII character 'end of text', which is represented in binary as 00000011. We will use to detect the end of the message when extracting the message.

function steganize($file, $message) {
  // Encode the message into a binary string.
  $binaryMessage = '';
  for ($i = 0; $i < mb_strlen($message); ++$i) {
    $character = ord($message[$i]);
    $binaryMessage .= str_pad(decbin($character), 8, '0', STR_PAD_LEFT);
  }

  // Inject the 'end of text' character into the string.
  $binaryMessage .= '00000011';

  // Load the image into memory.
  $img = imagecreatefromjpeg($file);

  // Get image dimensions.
  $width = imagesx($img);
  $height = imagesy($img);

  $messagePosition = 0;

  for ($y = 0; $y < $height; $y++) {
    for ($x = 0; $x < $width; $x++) {

      if (!isset($binaryMessage[$messagePosition])) {
        // No need to keep processing beyond the end of the message.
        break 2;
      }

      // Extract the colour.
      $rgb = imagecolorat($img, $x, $y);
      $colors = imagecolorsforindex($img, $rgb);

      $red = $colors['red'];
      $green = $colors['green'];
      $blue = $colors['blue'];
      $alpha = $colors['alpha'];

      // Convert the blue to binary.
      $binaryBlue = str_pad(decbin($blue), 8, '0', STR_PAD_LEFT);

      // Replace the final bit of the blue colour with our message.
      $binaryBlue[strlen($binaryBlue) - 1] = $binaryMessage[$messagePosition];
      $newBlue = bindec($binaryBlue);

      // Inject that new colour back into the image.
      $newColor = imagecolorallocatealpha($img, $red, $green, $newBlue, $alpha);
      imagesetpixel($img, $x, $y, $newColor);

      // Advance message position.
      $messagePosition++;
    }
  }

  // Save the image to a file.
  $newImage = 'secret.png';
  imagepng($img, $newImage, 9);

  // Destroy the image handler.
  imagedestroy($img);
}

You might wonder why I am not performing bit wise operations on this, since I have the binary numbers at hand. Well because PHP stores all numbers as essentially the same data type we are likely to lose precision if we did convert this into a PHP binary number. For this reason we first convert the number to a string, then perform the change before converting it back to an integer value again. This also makes it pretty clear what is going on and allows us to extend this code to, for example, change the last two significant bits or change more colour channels.

To run this code we supply the file name of the image and the message.

$file = 'mushroom.jpg';
$message = 'Message hidden in plain sight.';
steganize($file, $message);

This will take the message we have given and inject it into the image, one pixel at a time. As an example, the first character of the message is M, which converts to the binary number 01001101. This is injected into the image in the following way.

Original Blue ChannelInjected BitNew Blue Channel
01110101001110100
01110001101110001
01100011001100010
01001001001001000
01010001101010001
01100100101100101
01101010001101010
01101101101101101

The final output of this function is the following image. This looks almost unchanged from the image above, but I assure you that it is a different image that contains the message. In fact it looks so similar that it would take a direct comparison of the two images to be able to spot the difference.

A secret file.

This image has the same dimensions as the original image. The only difference is that the image is now a png and has had its colours slightly altered. Using the above technique there is an upper limit on the amount of text we can store. Let's do some maths to figure out how many total characters we can store in this image.

Our original image is 250 x 188 = 47,000 pixels. 47,000 pixels / 8 bits per character (assuming we use ASCII) means we can store 5,875 characters. Our message can therefore be a maximum of 5,874 characters as we need to leave a space for the stop character.

To reverse the process we need to take the secret file and extract the least significant bit from the blue pixels. We take these bits and build up the original message that we injected into the image as a binary string. Looking at the first character in the message we piece it together like this.

01110100 0
01110001 1
01100010 0
01001000 0   = 01001101 = "M"
01010001 1
01100101 1
01101010 0
01101101 1

Once we find the end of text character then we stop the process and convert the binary message into the original text. Here is a function that will take the png file we created and extract the hidden message from it. We just loop through the pixels in the same order as the encoder and extract the least significant pixel from the blue channel.

function desteganize($file) {
  // Read the file into memory.
  $img = imagecreatefrompng($file);

  // Read the message dimensions.
  $width = imagesx($img);
  $height = imagesy($img);

  // Set the message.
  $binaryMessage = '';

  // Initialise message buffer.
  $binaryMessageCharacterParts = [];

  for ($y = 0; $y < $height; $y++) {
    for ($x = 0; $x < $width; $x++) {

      // Extract the colour.
      $rgb = imagecolorat($img, $x, $y);
      $colors = imagecolorsforindex($img, $rgb);

      $blue = $colors['blue'];

      // Convert the blue to binary.
      $binaryBlue = decbin($blue);

      // Extract the least significant bit into out message buffer..
      $binaryMessageCharacterParts[] = $binaryBlue[strlen($binaryBlue) - 1];

      if (count($binaryMessageCharacterParts) == 8) {
        // If we have 8 parts to the message buffer we can update the message string.
        $binaryCharacter = implode('', $binaryMessageCharacterParts);
        $binaryMessageCharacterParts = [];
        if ($binaryCharacter == '00000011') {
          // If the 'end of text' character is found then stop looking for the message.
          break 2;
        }
        else {
          // Append the character we found into the message.
          $binaryMessage .= $binaryCharacter;
        }
      }
    }
  }

  // Convert the binary message we have found into text.
  $message = '';
  for ($i = 0; $i < strlen($binaryMessage); $i += 8) {
    $character = mb_substr($binaryMessage, $i, 8);
    $message .= chr(bindec($character));
  }

  return $message;
}

To run the above code we just need to feed in our secret file and print out the resulting message.

$secretfile = 'secret.png';
$message = desteganize($secretfile);
echo $message;

This prints out the following, which was our original message.

Message hidden in plain sight.

As I mentioned before this is the most trivial form of steganography. We can easily go a step further here by encoding the message into different colours and across different pixels in the image. It's actually a pretty bad idea to just encode the message statically across the pixels and using the same colour every time as it means your message can be easily extracted. Modern steganography tends to use more complex mathematics to distribute the pixels across the image so that they can be harder to guess. Techniques like the fibonacci sequence or a polynomial distribution of pixels can also be used to pick out which pixels are encoded.

You might note that I have swapped formats of images here, starting with a jpeg and converting it to a png file. This is because jpegs aren't that good at storing steganography due to the compression algorithms that go into the image. If you look at how a jpeg is constructed then you will see that it generates smaller blocks of pixels using a masking algorithm. For this reason the least significant bit is often thrown away to save space. I did experiment with generating a jpeg image at the start but my message kept being lost, and I think this compression mechanism is why.

Some tips to getting the most out of steganography:

  • The bigger the image the easier it is to hide data in it. I have used a small image here, but there is nothing to stop you using a large image and hiding even more data in it.
  • An image with a uniform colour is slightly harder to hide data in as rouge pixels of a slightly different colour are easy to spot.
  • It's also a good idea to encrypt the message you encode into the image. This means that if someone was able to extract your message they wouldn't be able to read it without decrypting it first. An added layer of protection for the message, although it does mean you recipient has to double decode the message.
  • As the message can be extracted out of the image by looking at the original it is best not to release the original image or use an already available image like a stock image.

Feel free to use the above code to experiment with your own images. See if you can extract the message from the image I have uploaded here or encode your own messages. I should note that this is meant to be fun and not intended as a full blown encryption algorithm.

Add new comment

The content of this field is kept private and will not be shown publicly.