To a computer, an image is nothing but a bunch of pixels. A pixel in turn is just a combination of three numbers in the range 0 to 255. So in the end, an image is just a collection of numbers. Now, if you were to add some data to the image the color of the pixels would change, and hence the image would not be the same as before. However, what if you could add data to the image in such as way that the pixels are modified in such a way that one can hardly discern the difference? That is the premise of LSB Steganography, which despite its fancy-sounding name is quite simple.

Even though the pixel values are different, you cannot tell the difference due to the change being only in the LSB

LSB or Least-Significant Bit is the bit at the rightmost end when you write the number in binary (Refer to the image). If you change the LSB of a pixel, it’d get changed by a value of 1. Given the range of 0 to 255 for the pixel, this change is not significant, especially when it comes to our eyes. Now, if we consider the last two bits, the maximum possible change is 3 which again is not a significant change. Hence we can encode our information into the last two bits of the pixels of the image.

MSB And LSB

Implementation

Based on the above concept, if we have a 256 * 256 RGB image and we are modifying 2 bits of every pixel, we will have total modifiable bits as 2 * 3 * 256 * 256. Our message, however, does not need to be of those exact bits. Hence, we would need an upper limit for the length of our message. And this length would be different for different messages. Hence, we also need to encode the length of the message into the image. Once encoded, the image would be almost identical to the original image but it would contain out the message.

For decoding, we would require the length of the message, and based on that we would decode only the number of bits required. If we do not know the length of the encoded message, we would not be able to decode the message correctly.

So the steps are:

  1. Encode the message into binary
  2. Find the length of the encoded message
  3. Add the length and the message to the LSBs of the pixels
  4. Message has been encoded in the image
  5. To decode the message, first decode the length from the starting bits
  6. Based on the length, decode the pixels to find the encoded message

With the concept clear, let’s move to the implementation. The message we encode would be a string like:

string = “Welcome to HackerShrine”

Main Logic

Let’s first discuss the main logic of the program and then we will deal with the decimal-binary conversions. For this program, we would consider the max length of our message as 9999. Hence, we would require 8 bits per character or 8 * 4 = 32 bits of space to store the length. We would include this length at the start of our message. Hence, whatever be our message, the first 32 bits would represent the length of the encoded message. We would first encode the message in binary and then add this length to it.

message = message_length + message

For the main logic, we would need to access individual pixels of the image. For this, we would need a nested for loop.

We have accessed the pixel and now we need to modify its LSBs. For that, we need to access the last two bits of the pixel. Hence, we would first convert the pixel value (say 243) to binary. 243 in binary is 11110011. Consider that the message encoded bits is 01. Hence the new pixel value would become 11110001. We would then replace the original pixel with this new pixel value. We would use a variable ‘count’ to know which bit of the message we are currently on.

We are done with the encoding. If we were to print both the original image and the image with the encoded message side by side, they’d look like this.

The image on the left is the original image and the image on the right is the image with the encoded information. Can you tell any difference?

To decode the message, we would need to separate out the last two bits from each pixel and append them to form the encoded message. But before that, we would need to decode the first 32 bits to find out the length of the message. So, decoding happens in two steps:

  • Determining the length
  • Separating out that length of the message from the pixels and decoding them

For decoding, we will run the nested loops twice. First, just find the message length by stopping the count variable at 32. That would give us the length of the message to be decoded. Then we can proceed with the second nested loop and find the encoded message by taking count up to the length of the encoded message. Now that we have the encoded message, we can just decode it as

decode_binary_string(encoded_msg)[4:]

Specifying [4:] would exclude the first 4 digits specifying the message length. The remaining message is our original string.

The input and output messages

Conversions

To convert into binary, we would write each letter into their ASCII 8-bit binary forms. So ‘a’ would be written as 01100001. We can write a function for this conversion.

To decode the message, the function would be

Please note that when we convert numbers into binary, it’d convert 32 as BCD. This means 3 would have its 8 bits and 2 would have its bits. Instead of 32, it’d be more like 3 and 2. In situations when we have to modify the binary of the pixel we will have to convert decimal to binary (not BCD). So for that 32 would be 00100000.

For binary to decimal, we simply have

int(x, 2)

So these are all the decimal-binary conversions we need. If you find this a bit confusing, don’t worry, you can just remember that we are converting everything to binary to put in the image LSBs.

Conclusion

The entire code for this is available here on our GitHub page, so if you have any confusion, you can check it out. This brings us to the end of this post. We will continue bringing more interesting posts on this platform. Till then you can check out our previous posts and interview experiences. If you’re interested in product management, this post about Hick’s Law might interest you!