Saturday, August 24, 2013

Data cloaking inside of BMP pictures

Tonight, I decided to create a small program that:
  • encodes any file into a picture
  • decodes this file from the same picture
A schema being better than any long sentence...


How does it work?

So easy... you know a bitmap file is nothing more than a succession of pixels, and a pixel is made of 3 bytes Red Green Blue, right? (let's ignore the file header, the reverse order of rows, etc etc... that's not important here)
The big picture is to hide one byte of the message per pixel. Red is coded on 8 bits, right? Let's use the 2 least significant bits (LSB) to encode 2 bits of this message byte. Green is coded on 8 bits too, so let's use the 3 LSB to encode 3 bits of the message byte. Same thing for Blue, let's use the 3 LSB to encode 3 bits of the message byte. You got it: 2 bits + 3 bits + 3 bits = 1 byte. This is how we will hide each byte from the message in one pixel.
The good news is that we always modified the LSB of the pixel components... when you will look at the modified picture (containing the message), you will not be able to detect the color changes from the original one.

Now, have a closer look at the two interesting parts in this program: encoding of data, decoding of data.

Encoding

The following method inserts a message (a memory Stream, it can be anything) into the original picture. Of course, we have to ensure the picture file is big enough to contain the message.
Note the the first 4 pixels will be used to encode the size of the message. We will need it to decode the message.
The source code is a bit messy, because it contains a homemade BMP writer. But the data cloaking itself is quite straightforward, provided you are familiar with bitmasks.

private void ProcessBmp(string InputFilename, System.IO.MemoryStream Message, string OutputFilename)
{
    using (System.IO.FileStream fs = new System.IO.FileStream(InputFilename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read, 1024 * 1024))
    {
        using (System.Drawing.Bitmap b = new System.Drawing.Bitmap(fs))
        {
            if (b.Width * b.Height < Message.Length + 4)
                throw new Exception(string.Format("BMP file is too small, it must be at least {0} bytes to encode this message", (Message.Length + 4) * 3));
            using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(new System.IO.FileStream(OutputFilename, System.IO.FileMode.Create, System.IO.FileAccess.Write)))
            {
                int ComplementToAdd = (4 - ((b.Width * 3) % 4)) % 4;
                byte[] BlackPixel = new byte[3] { 0, 0, 0 };
                bw.Write('B');
                bw.Write('M');

                int x, y;

                bw.Write((uint)(54 + b.Width * b.Height * 3 + b.Height * ComplementToAdd));
                bw.Write((uint)0);
                bw.Write((uint)54);
                bw.Write((uint)40);
                bw.Write((uint)b.Width);
                bw.Write((uint)b.Height);
                bw.Write((uint)24 * 0x10000 + 1);   // planes & bitcount
                bw.Write((uint)0);   // compression
                bw.Write((uint)b.Width * b.Height * 3 + ComplementToAdd * b.Height);    // size of image
                bw.Write((uint)0);   // XPPM
                bw.Write((uint)0);   // YPPM
                bw.Write((uint)0);   // color used

                int Idx = 0;
                for (y = b.Height - 1; y >= 0; y--)
                {
                    for (x = 0; x < b.Width; x++)
                    {
                        Color c = b.GetPixel(x, y);
                        byte[] RGB = new byte[3] { c.B , c.G, c.R };

                        byte ByteToEncode;
                        switch (Idx)
                        { // The 4 first bytes wil be used to encode the size of the message
                            case 0: ByteToEncode = (byte)(Message.Length & 0x000000FF); break;
                            case 1: ByteToEncode = (byte)((Message.Length & 0x0000FF00) >> 8); break;
                            case 2: ByteToEncode = (byte)((Message.Length & 0x00FF0000) >> 16); break;
                            case 3: ByteToEncode = (byte)((Message.Length & 0xFF000000) >> 32); break;
                            default: ByteToEncode = Idx - 4 < Message.Length
                                 ? (byte)Message.ReadByte()
                                 : (byte)0; break;
                        }

                        byte EncodeB = (byte)((ByteToEncode & (32 + 64 + 128)) >> 5);
                                       // Find the biggest 3 bits in the byte to encode
                        byte EncodeG = (byte)((ByteToEncode & (4 + 8 + 16)) >> 2);
                                       // Find the middle 3 bits in the byte to encode
                        byte EncodeR = (byte)(ByteToEncode & (1 + 2));
                                       // Find the smallest 2 bits in the byte to encode
                        RGB[0] = (byte)((RGB[0] & 0xF8) + EncodeB);
                                 // In Blue, 3 bits are used to encode (0xF8 = 1111 1000)
                        RGB[1] = (byte)((RGB[1] & 0xF8) + EncodeG);
                                 // In Green, 3 bits are used to encode (0xF8 = 1111 1000)
                        RGB[2] = (byte)((RGB[2] & 0xFC) + EncodeR);
                                 // In Red, 2 bits are used to encode (0xFC = 1111 1100)

                        bw.Write(RGB, 0, 3);
                        Idx++;
                    }
                    // Rows in bmp must be a multiple of 4, so let's fill with black pixels if necessary
                    if (ComplementToAdd != 0)
                        bw.Write(BlackPixel, 0, ComplementToAdd);
                }
            }
        }
    }
}

Decoding

This one is much easier than the encoding source code. So straightforward that is does not need any comment...

private System.IO.MemoryStream UnprocessBmp(string InputFilename)
{
    System.IO.MemoryStream Result = new System.IO.MemoryStream();
    using (System.IO.FileStream fs = new System.IO.FileStream(InputFilename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read, 1024 * 1024))
    {
        using (System.Drawing.Bitmap b = new System.Drawing.Bitmap(fs))
        {
            int x, y, MessageSize = 0, Idx = 0;
            for (y = b.Height - 1; y >= 0; y--)
            {
                for (x = 0; x < b.Width; x++)
                {
                    Color c = b.GetPixel(x, y);
                    byte EncodedB = (byte)(c.B & 0x07);
                                    // Find the last 3 bits of the blue component
                    byte EncodedG = (byte)(c.G & 0x07);
                                    // Find the last 3 bits of the green component
                    byte EncodedR = (byte)(c.R & 0x03);
                                    // Find the last 2 bits of the red component
                    byte EncodedByte = (byte)((EncodedB << 5) + (EncodedG << 2) + EncodedR);
                                    // combine the 3 + 3 + 2 bits to make a byte

                    switch (Idx)
                    { // The 4 first bytes wil be used to encode the size of the message
                        case 0: MessageSize = EncodedByte; break;
                        case 1: MessageSize = MessageSize + (EncodedByte << 8); break;
                        case 2: MessageSize = MessageSize + (EncodedByte << 16); break;
                        case 3: MessageSize = MessageSize + (EncodedByte << 32); break;
                        default: if (Idx - 4 < MessageSize) Result.WriteByte(EncodedByte); break;
                    }
                    Idx++;
                }
            }
        }
    }
    Result.Seek(0, System.IO.SeekOrigin.Begin);
    return Result;
}

You can download the solution here. Please, pay special attention to my high-level skills in GUI design... \m/

No comments:

Post a Comment