TransWikia.com

Is it possible to hide data in a bitmap using LockBits?

Stack Overflow Asked by Jeremy James on November 16, 2021

I’m writing a small steganography application in C# and was able to hide text in images. However the method I used was the GetPixel/SetPixel method which was a lot slower for larger images, which I noticed after trying to hide a mp3 file in the image. After some google searches, I found out about LockBits. While the speed did improve drastically, I discovered that I was unable to extract the encrypted data (the cipher text) which was hidden in the image.

I’m not sure if the issue is with how I insert the data or when extracting it. When attempting to extract the Base64 cipher text, it would be corrupted (random symbols and characters) and throw an exception about it not being a Base64String. I ended up changing the code by following what was on the documentation for LockBits, I’ll paste it below.

Merging the cipher text

public static unsafe void MergeEncryptedData(string data, Bitmap bmp, string output) {
    State s = State.HIDING;

    int height = bmp.Height;
    int width = bmp.Width;

    var bitmapData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bmp.PixelFormat);
    byte * scan0 = (byte * ) bitmapData.Scan0;

    int bytesPerPixel = 4;
    int dataIndex = 0;
    byte dataValue = 0;
    long colorUnitIndex = 0;
    int zeros = 0;
    byte R, G, B;

    Parallel.For(0, height, (i, loopState) = > {

        byte * currentLine = scan0 + (i * bitmapData.Stride);

        for (int j = 0; j < (bitmapData.Width * bytesPerPixel); j += bytesPerPixel) {
            R = currentLine[i + 2];
            G = currentLine[i + 1];
            B = currentLine[i];

            for (int n = 0; n < 3; n++) {

                if (colorUnitIndex % 8 == 0) {
                    if (zeros == 8) {
                        if ((colorUnitIndex - 1) % 3 < 2) {
                            currentLine[i + 2] = R;
                            currentLine[i + 1] = G;
                            currentLine[i] = B;
                            //bmp.SetPixel(j, i, Color.FromArgb(R, G, B));
                        }
                        loopState.Stop();
                    }

                    if (dataIndex >= data.Length) {
                        s = State.FILL_WITH_ZEROS;
                    } else {
                        dataValue = (byte) data[dataIndex++];
                    }
                }

                switch (colorUnitIndex % 3) {
                case 0:
                    {
                        if (s == State.HIDING) {
                            B += (byte)(dataValue % 2);
                            dataValue /= 2;
                        }
                    }
                    break;
                case 1:
                    {
                        if (s == State.HIDING) {
                            G += (byte)(dataValue % 2);
                            dataValue /= 2;
                        }
                    }
                    break;
                case 2:
                    {
                        if (s == State.HIDING) {
                            R += (byte)(dataValue % 2);
                            dataValue /= 2;
                        }
                        currentLine[i + 2] = R;
                        currentLine[i + 1] = G;
                        currentLine[i] = B;
                        //bmp.SetPixel(j, i, Color.FromArgb(R, G, B));
                    }
                    break;
                }

                colorUnitIndex++;

                if (s == State.FILL_WITH_ZEROS) {
                    zeros++;
                }
            }
        }
    });

    bmp.UnlockBits(bitmapData);
    bmp.Save(output, ImageFormat.Png);
}

Extracting the cipher text

public static unsafe string ExtractData(Bitmap bmp) {
    int height = bmp.Height;
    int width = bmp.Width;

    var bitmapData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, bmp.PixelFormat);
    byte * scan0 = (byte * ) bitmapData.Scan0.ToPointer();

    int bytesPerPixel = 4;
    int colorUnitIndex = 0;
    int charValue = 0;
    string extractedText = String.Empty;

    Parallel.For(0, height, (i, loopState) = > {

        byte * currentLine = scan0 + (i * bitmapData.Stride);

        for (int j = 0; j < (bitmapData.Width * bytesPerPixel); j += bytesPerPixel) {

            for (int n = 0; n < 3; n++) { //this particular loop feels incorrect

                switch (colorUnitIndex % 3) {
                case 0:
                    {
                        charValue = charValue * 2 + currentLine[i] % 2;
                    }
                    break;
                case 1:
                    {
                        charValue = charValue * 2 + currentLine[i + 1] % 2;
                    }
                    break;
                case 2:
                    {
                        charValue = charValue * 2 + currentLine[i + 2] % 2;
                    }
                    break;
                }

                colorUnitIndex++;

                if (colorUnitIndex % 8 == 0) {
                    charValue = reverseBits(charValue);

                    if (charValue == 0) {
                        loopState.Stop();
                    }

                    char c = (char) charValue;
                    extractedText += c.ToString();
                }
            }
        }
    });

    bmp.UnlockBits(bitmapData);
    return extractedText;
}

An example of what the extracted cipher text looks like when the error is thrown:
I$I$I$I$I$I$I$I$I$I$I$I$I$I䥉II!J$$.
It should be a Base-64 String

Just for reference, I’m using a LUT PNG image to hide the data, so I’m able see a slight difference in color when compared to the original. So I know the RGB values are indeed being changed.

One Answer

You need to consider:

  • Stride - image data with may be different of image width. More info here.
  • Image number of color/data channels
    • 8 bpp (1 channel)
    • 24 bpp (3 channels RGB)
    • 32 bpp (4 channels ARGB, where A means alpha, transparency)

enter image description here

You mention RGB PNG but you are using 4 channels in your code (ARGB), double check that.

Here is a sample method that WILL obtain the same data that using the slow Bitmap GetPixel, but really fast. Based on this sample you can fix your code, as:

  • How to calculate bits per pixel
  • How to use stride properly
  • How to read multiple channels

Code:

/// <summary>
/// Get pixel directly from unmanaged pixel data based on the Scan0 pointer.
/// </summary>
/// <param name="bmpData">BitmapData of the Bitmap to get the pixel</param>
/// <param name="p">Pixel position</param>
/// <returns>Pixel value</returns>
public static byte[] GetPixel(BitmapData bmpData, Point p)
{
    if ((p.X > bmpData.Width - 1) || (p.Y > bmpData.Height - 1))
       throw new ArgumentException("GetPixel Point p is outside image bounds!");
    
    int bitsPerPixel = ((int)bmpData.PixelFormat >> 8) & 0xFF;
    int channels = bitsPerPixel / 8;

    byte[] data = new byte[channels];
    int id = p.Y * bmpData.Stride + p.X * channels;
    unsafe
    {
        byte* pData = (byte*)bmpData.Scan0;
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = pData[id + i];
        }
    }
    return data;
}

Answered by Pedro77 on November 16, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP