Hiding Your Bits in the Bytes: A basic example of modern steganography using C#

I recently found a video on youtube where some CSI personnel zoomed into an innocuous image to discover that there were embedded several hidden contraband images in the larger image. The actual link to the video escapes my googling abilities at the moment of writing. The first thing I thought when seeing the video was that “this is definitely not how I would have embedded data into an image.” I hypothesized that it in essence could be done by wiggling the less significant bits of information in the image. A tiny difference in hues for each pixel in the image would probably not be perceptible by humans. I set out to confirm my hypothesis as a small personal project for the evening, knowing full well that this was nothing new and naturally done before.

I have later learned the name of the practice of hiding information in larger works: Steganography. Wikipedia defines Steganography as “the art or practice of concealing a message, image, or file within another message, image, or file.” It’s quite an intriguing concept.

Concept and theory

A computer image, such as PNG-files, when uncompressed is simply an m-by-n matrix, i.e., table, of pixels each of a specific color (and transparency). The pixels can be further decomposed into three primary component colors, Red, Green, and Blue, and the transparency, Alpha. In C# these can be extracted as an integer value per component per pixels. I focused on only the color components, and ignored the Alpha value even though it could have been used too. Besides, it would be quite suspicious having loads subtle transparencies in an image.

The following code reads the image file “image.png” as a Bitmap object, extracts the Color object of the pixel at the arbitrary column 7 and row 11, and writes the RGB values to the console window.

Bitmap bmp = new Bitmap("image.png");
int x = 7; int y = 11;
Color px = bmp.GetPixel(x, y);
Console.Write("Red={0}, Green={1}, Blue={2}",
px.R, px.G, px.B);

The code requires a reference to the System.Drawing assembly and namespace, and will output something like

Red=124, Green=156, Blue=43

Notice how the pixel color components px.R, px.G, px.B are simply small integer values, in fact they are simple byte structs, with a minimum value of 0 and a maximum value of 255. Changing the value of these by one bit changes the color, however, only imperceptibly.

The first of the two following images is named light turquoise by MS Paint, and has the RGB values 153, 217, and 234 respectively. The second of the two images I have perturbed by one bit such that the RGB values are now 152, 218, and 233.
light_turquoise_pert

These are for all esthetical purposes identical, and we can utilize this to encode our bits. First though, we need to be able to make out if a color component has been perturbed or not. A simple way would be to make all of the components even (or odd) and encode the one-bits as odd values and the zero-bits as even values.

The following table illustrates how the ascii string “Hi!” (or “100100011010010100001” in binary) can be encoded in the first six pixels of an image:

Char Row Col. Comp. Original Flat Encoded Parity Bit
‘H’ 0 0 Red 41 40 41 Odd 1
0 0 Green 42 42 42 Even 0
0 0 Blue 56 56 56 Even 0
0 1 Red 133 132 133 Odd 1
0 1 Green 104 104 104 Even 0
0 1 Blue 127 126 126 Even 0
0 2 Red 64 64 64 Even 0
‘i’ 0 2 Green 80 80 81 Odd 1
0 2 Blue 138 138 139 Odd 1
0 3 Red 242 242 242 Even 0
0 3 Green 178 178 179 Odd 1
0 3 Blue 182 182 182 Even 0
0 4 Red 190 190 190 Even 0
0 4 Green 169 168 169 Odd 1
‘!’ 0 4 Blue 192 192 192 Even 0
0 5 Red 250 250 251 Odd 1
0 5 Green 213 212 212 Even 0
0 5 Blue 143 142 142 Even 0
0 6 Red 185 184 184 Even 0
0 6 Green 193 192 192 Even 0
0 6 Blue 7 6 7 Odd 1

In this table we can see that each pixel can store three hidden bits of information. Thus an 800 by 600 pixel image could store 1.44 Mb (megabits) or 180 kB (kilobytes) of hidden data.

Code and application

Lets now turn the concepts we have been talking about into code.

Disclaimer: The following code is by no means production ready, or safe to use for real applications. Its simply a proof of concept. There probably exists plenty of methods which could easily detect the data we are hiding.

There are two main steps to be concerned about. The encoding of data we want to hide into some innocuous image file, and the recovery of this data through decoding. This outline is encapsulated in the main method:

static void Main()
{
    byte[] hiddenBytes = Util.BitmapToByteArray(Image.FromFile("hidden.png"));
    Encode(hiddenBytes, "innocuous.png", "encoded.png");
    byte[] loadedHiddenBytes = Decode("encoded.png");
    Util.ByteArrayToBitmap(loadedHiddenBytes).Save("decoded.png", ImageFormat.Png);
}

The first line loads the image “hidden.png” into memory as a simple byte array.

hidden.png - An image of Charles Darwin we would like hide.

hidden.png – An image of Charles Darwin we would like hide.

The “hidden.png” image is the data we would like to encode into the data of some other innocuous file. The second line does this by loading the image “innocuous.png” to memory, encodes and hides the data of “hidden.png” into the “innocuous.png” data and stores it as “encoded.png”. Thus “innocuous.png” and “encoded.png” should look identical, just that the latter has the “hidden.png” data embedded into it.

innocuous.png - An innocuous image of a forest. There is no embedded data in this image.

innocuous.png – An innocuous image of a forest. There is no embedded data in this image.

encoded.png – An innocuous-looking image of a forest. The hidden.png data is embedded in this image.

encoded.png – An innocuous-looking image of a forest. The hidden.png data is embedded in this image.

Now we want to extract the hidden “hidden.php” data from the “encoded.png” image. The third line loads and decodes the hidden bytes into memory, and in the fourth line we write this data back to file as “decoded.png”. Thus “hidden.png” and “decoded.png” will be identical.

decoded.png - An image of Charles Darwin we just extracted from the encoded.php image file.

decoded.png – An image of Charles Darwin we just extracted from the encoded.php image file.

To recap, the first and second line is what one would use to hide your data, while lines three and four is what you would use to get it back.

High level data flow of the encoding and decoding process.

High level data flow of the encoding and decoding process.

Lets go into more detail.

Encoding

Too keep things somewhat organized helper methods have been encapsulated into a custom Util class, for lack of a better name. In the main method the Util.BitmapToByteArray is used to convert our contraband Darwin image into bytes. Specifically it takes the image object generated when we loaded the “hidden.png” file and converts it into a byte array using a new MemoryStream object.

class Util
{
    public static byte[] BitmapToByteArray(Image img)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            img.Save(ms, ImageFormat.Png);
            return ms.ToArray();
        }
    }
...
}

When this is done the byte array is passed to the Encode method with the file name of the innocuous image, and the desired output file name.

public static void Encode(byte[] hiddenBytes, string inputImageFileName, string outputImageFileName)
{
    // Loading the data we want to hide to a byte array
    byte[] hiddenLengthBytes = BitConverter.GetBytes(hiddenBytes.Length);
    byte[] hiddenCombinedBytes = Util.Combine(hiddenLengthBytes, hiddenBytes);
	
     // Loading an innocuous image we want to store the hidden data in to a byte array
    Image innocuousBmp = Image.FromFile(inputImageFileName);
    byte[] rgbComponents = Util.RgbComponentsToBytes(innocuousBmp);
	
     // Encoding the hidden data into the innocuous image, and storing it to file.
    byte[] encodedRgbComponents = EncodeBytes(hiddenCombinedBytes, rgbComponents);
    Bitmap encodedBmp = Util.ByteArrayToBitmap(encodedRgbComponents, innocuousBmp.Width, innocuousBmp.Height);
    encodedBmp.Save(outputImageFileName, ImageFormat.Png);
}

First thing we need is a strategy to know how much data is encoded. There are multiple ways of doing this. I have chosen to simply encode it as a fixed length header. This can be done simply by encoding the length of the hidden data using a BitConverter, then append these bytes to the front of the data we want to hide. This is done in the Util.Combine method.

class Util
{
...
    public static byte[] Combine(byte[] left, byte[] right)
    {
        byte[] combined = new byte[left.Length + right.Length];
        Buffer.BlockCopy(left, 0, combined, 0, left.Length);
        Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
        return combined;
    }
...
}

Next we load the innocuous image file and converts the color components into an array of consecutive bytes stored as an array. This is done in the Util.RgbComponentsToBytes method.

class Util
{
...
    public static byte[] RgbComponentsToBytes(Image innocuousImg)
    {
        Bitmap innocuousBmp = new Bitmap(innocuousImg);
        int counter = 0;
        byte[] components = new byte[3 * innocuousBmp.Width * innocuousBmp.Height];
        for (int y = 0; y < innocuousBmp.Height; y++)
        {
            for (int x = 0; x < innocuousBmp.Width; x++)
            {
                Color c = innocuousBmp.GetPixel(x, y);
                components[counter++] = c.R;
                components[counter++] = c.G;
                components[counter++] = c.B;
            }
        }
        return components;
    }
...
}

This creates a medium for hiding our bits as the less significant bits of the color component bytes. Now that we have data we want to hide and the medium we want to hide the data in as suitable data types we can do the actual encoding.

private static byte[] EncodeBytes(byte[] hiddenBytes, byte[] innocuousBytes)
{
    BitArray hiddenBits = new BitArray(hiddenBytes);
    byte[] encodedBitmapRgbComponents = new byte[innocuousBytes.Length];
    for (int i = 0; i < innocuousBytes.Length; i++)
    {
        if (i < hiddenBits.Length)
        {
            byte evenByte = (byte)(innocuousBytes[i] - innocuousBytes[i] % 2);
            encodedBitmapRgbComponents[i] = (byte)(evenByte + (hiddenBits[i] ? 1 : 0));
        }
        else
        {
            encodedBitmapRgbComponents[i] = innocuousBytes[i];
        }
    }
    return encodedBitmapRgbComponents;
}

First thing is to convert the data we want to hide, and it’s size header into a BitArray. Then we loop through all the component color bytes of the innocuous data. If we are looping through parts of the component colors we want to encoding data we truncate the least significant bit. If it is zero then we keep it zero, if it is one we make it zero, in effect this makes all the color components an even number. To encode our hidden bits we literally add them to the even color component bytes. This makes hidden one bits odd color component numbers, and zeroe bits even color component numbers. A slight perturbation which isn’t recognizable to the naked eye. When we have encoded all the hidden bits, we simply keep the original color component bytes as they are. I’ve intentionally made this part of the code as simple as possible so that it is easier to explain. However, there are innumerous ways of doing this in more complex ways to avoid detection. This is outside the scope of this simple tutorial, although I’ll do a simple analysis at the end to show how things look when encoded.

When the actual encoding is done we must convert the color components byte array back to an image.

class Util
{
...
    public static Bitmap ByteArrayToBitmap(byte[] rgbComponents, int width, int hight)
    {
        Queue<byte> rgbComponentQueue = new Queue<byte>(rgbComponents);
        Bitmap bitmap = new Bitmap(width, hight);
        for (int y = 0; y < hight; y++)
        {
            for (int x = 0; x < width; x++)
            {
                bitmap.SetPixel(x, y, Color.FromArgb(rgbComponentQueue.Dequeue(), rgbComponentQueue.Dequeue(), rgbComponentQueue.Dequeue()));
            }
        }
        return bitmap;
    }
}

When we again have an image object (Bitmap extends the Image class) we can save it using it’s save method. This outputs the “encoded.png” image file, which looks identical to the “innocuous.png” image file.

Decoding

The next part is about restoring the hidden image from the encoded innocuous file. This is done in the Decode method.

public static byte[] Decode(string imageFileName)
{
    // Loading the seemingly innocuous image with hidden data into a byte array
    Bitmap loadedEncodedBmp = new Bitmap(imageFileName);
    byte[] loadedEncodedRgbComponents = Util.RgbComponentsToBytes(loadedEncodedBmp);
    const int bytesInInt = 4;
    byte[] loadedHiddenLengthBytes = DecodeBytes(loadedEncodedRgbComponents, 0, bytesInInt);
    int loadedHiddenLength = BitConverter.ToInt32(loadedHiddenLengthBytes, 0);
    byte[] loadedHiddenBytes = DecodeBytes(loadedEncodedRgbComponents, bytesInInt, loadedHiddenLength);
    return loadedHiddenBytes;
}

Here we again use Util.RgbComponentsToBytes to extract the color component bytes. We know there is a header which says how much data there is. This is an Int32 converted which is stored as four bytes stored in the first 32 color component bytes. To extract the length data, we use the DecodeBytes method. It takes the color color component bytes, the first byte index and length of data. This means we can read any part of the hidden data, doesn’t have to be the start. In this case it is the first byte index and four bytes down.

private static byte[] DecodeBytes(byte[] innocuousLookingData, int byteIndex, int byteCount)
{
    const int bitsInBytes = 8;
    int bitCount = byteCount * bitsInBytes;
    int bitIndex = byteIndex * bitsInBytes;
    bool[] loadedHiddenBools = new bool[bitCount];
    for (int i = 0; i < bitCount; i++)
    {
        loadedHiddenBools[i] = innocuousLookingData[i + bitIndex] % 2 == 1;
    }
    BitArray loadedHiddenBits = new BitArray(loadedHiddenBools);
    byte[] loadedHiddenBytes = new byte[loadedHiddenBits.Length / bitsInBytes];
    loadedHiddenBits.CopyTo(loadedHiddenBytes, 0);
    return loadedHiddenBytes;
}

When we have extracted the length of the hidden data to follow we use the DecodeBytes method again, this time from byte index four to the end of the hidden data. The way we extract the hidden data is by checking if a color component is even or odd using the modulus operation. We put all the detected bits into a bool array and convert it to a BitArray which is converted into a Byte array and returned.

Back in the main method we convert the hidden bytes into an image object and store it to file as “decoded.png”.

Simple Analysis

Again, I’d like to stress that this isn’t the best way to do this in practice. This is simply a hobby project where I did next to zero research. When that is said, lets look at what the encoded data looks like.

First, lets look at the difference between the “innocuous.png” image and the “encoded.png” image.

The visual difference between innocuous.png and encoded.png.

The visual difference between innocuous.png and encoded.png.

I added the black border manually to emphasis that the bottom part is actually blank. If a pixel is white it means that there is no difference between the images at all, while if it is black then all three color components of the pixels are perturbed by one. Gray scales between mean that only one or two of the three color components of the pixels are identical. As we can see the first one third of the image have perturbed pixels. The rest are identical. This means that the hidden Darwin image and its length header are stored in this top part of the image. It kind of looks like noise.

This is apposed to what the parity of the color components look like:

Looking at the parity of the color components of the innocuous.png  image.

Looking at the parity of the color components of the innocuous.png image.

What we see here is that the compression algorithm of the “innocuous.png” image lumps some of the pixels together. Especially where there are really dark or light colors. Notice at the top where there is a white spot where there is some sky in the original image. If we overlay the “innocuous.png” image, its parity image, and the parity image of the “ecoded.png” image we see that a lot of the compression data is lost and that there are quite noticeable perturbations:

Looking at the parity of the color components of the innocuous.png  image versus the ecoded.png image.

Looking at the parity of the color components of the innocuous.png image versus the ecoded.png image.

This could probably be detected automatically thus notifying an adversary of the existence of hidden data in the image.

Source code

For completeness the full source code is included below. I have also added the mask creation method which I used to analyse the image at the end.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;

namespace SteganographyTest
{
    class Program
    {
        static void Main()
        {
            byte[] hiddenBytes = Util.BitmapToByteArray(Image.FromFile("hidden.png"));
            Encode(hiddenBytes, "innocuous.png", "encoded.png");
            byte[] loadedHiddenBytes = Decode("encoded.png");
            Util.ByteArrayToBitmap(loadedHiddenBytes).Save("decoded.png", ImageFormat.Png);
            CreateMask("innocuous.png", "encoded.png");
        }

        public static void Encode(byte[] hiddenBytes, string inputImageFileName, string outputImageFileName)
        {
            // Loading the data we want to hide to a byte array
            byte[] hiddenLengthBytes = BitConverter.GetBytes(hiddenBytes.Length);
            byte[] hiddenCombinedBytes = Util.Combine(hiddenLengthBytes, hiddenBytes);

            // Loading an innocuous image we want to store the hidden data in to a byte array
            Image innocuousBmp = Image.FromFile(inputImageFileName);
            byte[] rgbComponents = Util.RgbComponentsToBytes(innocuousBmp);

            // Encoding the hidden data into the innocuous image, and storing it to file.
            byte[] encodedRgbComponents = EncodeBytes(hiddenCombinedBytes, rgbComponents);
            Bitmap encodedBmp = Util.ByteArrayToBitmap(encodedRgbComponents, innocuousBmp.Width, innocuousBmp.Height);
            encodedBmp.Save(outputImageFileName, ImageFormat.Png);
        }

        private static byte[] EncodeBytes(byte[] hiddenBytes, byte[] innocuousBytes)
        {
            BitArray hiddenBits = new BitArray(hiddenBytes);
            byte[] encodedBitmapRgbComponents = new byte[innocuousBytes.Length];
            for (int i = 0; i < innocuousBytes.Length; i++)
            {
                if (i < hiddenBits.Length)
                {
                    byte evenByte = (byte)(innocuousBytes[i] - innocuousBytes[i] % 2);
                    encodedBitmapRgbComponents[i] = (byte)(evenByte + (hiddenBits[i] ? 1 : 0));
                }
                else
                {
                    encodedBitmapRgbComponents[i] = innocuousBytes[i];
                }
            }
            return encodedBitmapRgbComponents;
        }

        public static byte[] Decode(string imageFileName)
        {
            // Loading the seemingly innocuous image with hidden data into a byte array
            Bitmap loadedEncodedBmp = new Bitmap(imageFileName);
            byte[] loadedEncodedRgbComponents = Util.RgbComponentsToBytes(loadedEncodedBmp);

            const int bytesInInt = 4;
            byte[] loadedHiddenLengthBytes = DecodeBytes(loadedEncodedRgbComponents, 0, bytesInInt);
            int loadedHiddenLength = BitConverter.ToInt32(loadedHiddenLengthBytes, 0);
            byte[] loadedHiddenBytes = DecodeBytes(loadedEncodedRgbComponents, bytesInInt, loadedHiddenLength);
            return loadedHiddenBytes;
        }

        private static byte[] DecodeBytes(byte[] innocuousLookingData, int byteIndex, int byteCount)
        {
            const int bitsInBytes = 8;
            int bitCount = byteCount * bitsInBytes;
            int bitIndex = byteIndex * bitsInBytes;
            bool[] loadedHiddenBools = new bool[bitCount];
            for (int i = 0; i < bitCount; i++)
            {
                loadedHiddenBools[i] = innocuousLookingData[i + bitIndex] % 2 == 1;
            }
            BitArray loadedHiddenBits = new BitArray(loadedHiddenBools);
            byte[] loadedHiddenBytes = new byte[loadedHiddenBits.Length / bitsInBytes];
            loadedHiddenBits.CopyTo(loadedHiddenBytes, 0);
            return loadedHiddenBytes;
        }

        public static void CreateMask(string inputImageFileName1, string inputImageFileName2)
        {
            Image image1 = Image.FromFile(inputImageFileName1);
            Image image2 = Image.FromFile(inputImageFileName2);
            Bitmap bmp1 = new Bitmap(image1);
            Bitmap bmp2 = new Bitmap(image2);
            Bitmap maskDiff = new Bitmap(bmp1);
            Bitmap maskParity1 = new Bitmap(bmp1);
            Bitmap maskParity2 = new Bitmap(bmp2);

            for (int i = 0; i < maskDiff.Height; i++)
            {
                for (int j = 0; j < maskDiff.Width; j++)
                {
                    Color px1 = bmp1.GetPixel(j, i);
                    Color px2 = bmp2.GetPixel(j, i);

                    int maskDiffIntensity = 255 - Math.Abs(px2.R - px1.R) * 85 - Math.Abs(px2.G - px1.G) * 85 - Math.Abs(px2.B - px1.B) * 85;
                    maskDiff.SetPixel(j, i, Color.FromArgb(maskDiffIntensity, maskDiffIntensity, maskDiffIntensity));

                    int maskParityIntensity1 = (px1.R % 2) * 85 + (px1.G % 2) * 85 + (px1.B % 2) * 85;
                    maskParity1.SetPixel(j, i, Color.FromArgb(maskParityIntensity1, maskParityIntensity1, maskParityIntensity1));

                    int maskParityIntensity2 = (px2.R % 2) * 85 + (px2.G % 2) * 85 + (px2.B % 2) * 85;
                    maskParity2.SetPixel(j, i, Color.FromArgb(maskParityIntensity2, maskParityIntensity2, maskParityIntensity2));
                }
            }

            maskDiff.Save("maskDiff.png");
            maskParity1.Save("maskParity_" + inputImageFileName1);
            maskParity2.Save("maskParity_" + inputImageFileName2);

        }
    }

    class Util
    {
        public static byte[] BitmapToByteArray(Image img)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                img.Save(ms, ImageFormat.Png);
                return ms.ToArray();
            }
        }

        public static Image ByteArrayToBitmap(byte[] bytes)
        {
            using (MemoryStream ms = new MemoryStream(bytes))
            {
                return Image.FromStream(ms);
            }
        }

        public static byte[] Combine(byte[] left, byte[] right)
        {
            byte[] combined = new byte[left.Length + right.Length];
            Buffer.BlockCopy(left, 0, combined, 0, left.Length);
            Buffer.BlockCopy(right, 0, combined, left.Length, right.Length);
            return combined;
        }

        public static byte[] RgbComponentsToBytes(Image innocuousImg)
        {
            Bitmap innocuousBmp = new Bitmap(innocuousImg);
            int counter = 0;
            byte[] components = new byte[3 * innocuousBmp.Width * innocuousBmp.Height];
            for (int y = 0; y < innocuousBmp.Height; y++)
            {
                for (int x = 0; x < innocuousBmp.Width; x++)
                {
                    Color c = innocuousBmp.GetPixel(x, y);
                    components[counter++] = c.R;
                    components[counter++] = c.G;
                    components[counter++] = c.B;
                }
            }
            return components;
        }

        public static Bitmap ByteArrayToBitmap(byte[] rgbComponents, int width, int hight)
        {
            Queue<byte> rgbComponentQueue = new Queue<byte>(rgbComponents);
            Bitmap bitmap = new Bitmap(width, hight);
            for (int y = 0; y < hight; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    bitmap.SetPixel(x, y, Color.FromArgb(rgbComponentQueue.Dequeue(), rgbComponentQueue.Dequeue(), rgbComponentQueue.Dequeue()));
                }
            }
            return bitmap;
        }
    }
}