Steganography is hiding a message so that it is not evident that there is any message there. History has some great examples of steganography. In ancient Greece, Herodotus told the story of a message tattooed on the shaved head of a slave of Histiaeus, hidden by the hair that afterwards grew over it, and exposed by shaving the head. More recently, during World War I, messages were written in Morse code on yarn and then knitted into a piece of clothing worn by a courier.
We can do a similar thing by hiding messages inside a bitmap image! To do this, we take advantage of the fact that our brains are unable to distinguish between two images with very minor differences - such as an individual R, G, or B value of a pixel being changed by just 1 out of the possible 256.
You’re going to use the same Python library, PIL, to work with your images. The first part of the code is the same you’ve used before before:
#import Image module from PIL library
from PIL import Image
#path to image to hide within here
image = Image.open('images/image.png')
rgb_image = image.convert('RGB') #convert to RGB
width, height = image.size #assign the image width and height to variables
You are going to encode your message using the binary representation of the ASCII characters in the message: this next bit of code converts the message into a string of 1s and 0s. Read the included comments to see how it works.
message = input("What message do you want to hide?")
#I'm going to use the message "hello everybody!" as an example
binary_message = ""
#The for loop will repeat for each character of "hello everybody!", including the space and the exclamation mark
for char in message:
ascii_number = ord(char) #This converts the ASCII character into the denary number that represents it
#For the first letter, 'h', this gives '104'
bin_as_string = bin(ascii_number) #This converts that denary number into a string representing a binary number
#So '104' is converted into the string '0b1101000'
#The 'b' is added in by 'bin()' to show the number is binary
bin_as_string = bin_as_string.replace('b','') #This removes the 'b'
#So finally you get '01101000'
while len(bin_as_string) < 8 :
bin_as_string = '0' + bin_as_string #This lengthens the binary number string until it is 8 bits
#Our example is already 8 bits long, so it remains '01101000'
binary_message = binary_message + bin_as_string #This result is then added to the binary message
Now it’s time to hide the binary message in the bitmap image the code opened. As you know, each pixel in the image has a red value from 0 to 255, and that value represented in binary has 8 bits. For example, if the red value is 200, it is represented in binary as 11001000:
We’re going to use this red value’s last bit (in the 1 column) to store a bit of the binary message. To do this, we replace the last bit in the binary value with a bit from our binary message: 1100100 plus 1 or 0. So we get back 11001000 if we need to store a 0, or 11001001 if we need to store a 1.
You can use the following code to do this.
#Create a grid for the output image
output_image = Image.new('RGB', (width,height))
#Create a variable to keep track of how far through the binary message the code has got
i = 0
#Set up loops to modify each pixel within the image
for row in range(height):
for col in range(width):
r, g, b = rgb_image.getpixel((col, row))
#Start by converting the red value of the pixel to binary
bin_r = bin(r)
#If the binary message has not been completely encoded, the code does the following things
if i < len(binary_message):
#Replace the final bit with a 1 or a 0 from the message
if binary_message[i] == '1':
bin_r = bin_r[:-1] + '1'
else:
bin_r = bin_r[:-1] + '0'
#Once the message has been completely encoded, the code replaces every red value's last bit with a zero
#This isn't good steganography, but it's simple to implement
else:
bin_r = bin_r[:-1] + '0'
#The binary number for the red value is converted back to an integer
new_r = int(bin_r,2)
#This integer is set as the pixel's red value
output_image.putpixel((col, row), (new_r,g,b))
#The variable i is increased by 1 to move on to the next binary character of the message
i = i + 1
#Once the code has iterated over all pixels in the image to change their red values, the image is saved
output_image.save("encoded_image.png")
To find the message hidden in an image, you need a script that does the reverse to the one we’ve just written.
See if you can discover the hidden message encoded in the red values of the following image. I’ve included some code to help you.
The following code iterates over all the pixels in encoded_image.png and extracts the binary value of each red pixel.
#Import Image module from PIL library
from PIL import Image
#Path to image containing message here
image = Image.open('encoded_image.png')
#Convert to RGB
rgb_image = image.convert('RGB')
#Assign the image width and height to variables
width, height = image.size
i = 0
output_bits = ""
output_text = ""
#Set up loops to address each pixel in the image
for row in range(height):
for col in range(width):
r, g, b = rgb_image.getpixel((col, row))
bin_r = bin(r)
output_bits = output_bits + bin_r[-1]
Add code to read the final bit of this binary value and add it to the end of the output_bits string.
The code below takes the string output_bits and converts each byte into an ASCII character until it reaches a byte with a value of 0. Then the code prints out the decoded hidden message.
end = False
while end == False:
byte = int(output_bits[:8],2)
print(byte)
if byte == 0:
end = True
else:
output_text = output_text + chr(byte)
output_bits = output_bits[8:]
print(output_text)
There’s also another message hidden in the image’s green values. Can you modify the code to find this message as well?
Share your successes and problems in the comments.
This is a very simple steganography program: because the program changes the pixel colour values so that, once the message is encoded, all the red values’ final bits are zeroes, it’s quite easy to spot that there’s a hidden message in the image.
How could you change how this program encodes the message to make it more secretive?