DIY Steganography: A Simple Python Script to Hide Messages in Images
Published on
You’ve played the game. You’ve uncovered secrets hidden in metadata, source code, and even the fabric of sound itself. You understand the theory behind Least Significant Bit (LSB) steganography—the art of making tiny, invisible changes to an image’s pixels to encode data.
Now, it’s time to move from being the detective to being the spymaster.
In this tutorial, we’re going to build our own simple steganography tool from scratch. Using Python, one of the most popular and accessible programming languages, we will write a script that can both hide a secret message inside a PNG image and extract a hidden message from one.
This is your first step into the world of practical steganography. Let’s begin.
The Goal
We will create a command-line script with two main functions:
1.hide
: Takes a carrier image, a secret message, and an output path, and creates a new image with the message hidden inside.
2.reveal
: Takes a stego-image and extracts the hidden message.
Prerequisites
To follow along, you’ll need:
*Python 3: Most modern systems (macOS, Linux) have it pre-installed. For Windows, you can download it from python.org.
*The Pillow Library: This is a powerful and easy-to-use Python Imaging Library. You can install it by opening your terminal or command prompt and running:
bash pip install Pillow
*A Carrier Image: Find a simple, medium-sized PNG image. Using PNG is important because it is a “lossless” format, meaning it won’t be altered by compression, which could destroy our hidden message. Save it as carrier.png
in the same folder where you’ll save your script.
The Script: stego.py
Create a new file named stego.py
and let’s start building it, section by section.
Part 1: Hiding the Message
First, we need a function to encode our message. It will convert our text into binary and then hide those bits in the image’s pixels.
from PIL import Image
def message_to_binary(message):
"""Converts a string message into its binary representation."""
# We add a special "delimiter" to know where the message ends.
# This is crucial for the reveal process.
message += "#####"
binary_message = ''.join(format(ord(char), '08b') for char in message)
return binary_message
def hide_message(image_path, secret_message, output_path):
"""Hides a secret message within an image file."""
try:
image = Image.open(image_path, 'r')
except FileNotFoundError:
print(f"Error: The file {image_path} was not found.")
return
width, height = image.size
img_data = list(image.getdata())
binary_secret_message = message_to_binary(secret_message)
message_length = len(binary_secret_message)
# Check if the image is large enough to hold the message
if message_length > len(img_data) * 3:
print("Error: The message is too long to be hidden in this image.")
return
data_index = 0
new_img_data = []
for pixel in img_data:
new_pixel = []
# Each pixel has 3 color values (R, G, B) we can modify
for i in range(3):
if data_index < message_length:
# Get the original color value and modify its last bit
# with the corresponding bit from our secret message.
original_value = pixel[i]
bit_to_hide = int(binary_secret_message[data_index])
# We use bitwise operations for efficiency
new_value = (original_value & ~1) | bit_to_hide
new_pixel.append(new_value)
data_index += 1
else:
# If the message is finished, just append the original value
new_pixel.append(pixel[i])
# The Alpha channel (transparency) is left untouched
if len(pixel) == 4:
new_pixel.append(pixel)
new_img_data.append(tuple(new_pixel))
# Create and save the new image
new_image = Image.new(image.mode, image.size)
new_image.putdata(new_img_data)
new_image.save(output_path, "PNG")
print(f"Message hidden successfully! New image saved as {output_path}")
Part 2: Revealing the Message
Now, we need the reverse function. This function will read the LSBs from an image and reconstruct the hidden message.
def reveal_message(image_path):
"""Reveals a hidden message from an image file."""
try:
image = Image.open(image_path, 'r')
except FileNotFoundError:
print(f"Error: The file {image_path} was not found.")
return
img_data = image.getdata()
binary_data = ""
for pixel in img_data:
for i in range(3): # R, G, B
# Extract the last bit from each color value
binary_data += str(pixel[i] & 1)
# Convert the binary stream back into characters
all_bytes = [binary_data[i: i+8] for i in range(0, len(binary_data), 8)]
decoded_message = ""
for byte in all_bytes:
if len(byte) == 8:
decoded_message += chr(int(byte, 2))
# Check if we've found our special end-of-message delimiter
if decoded_message[-5:] == "#####":
# Remove the delimiter and stop
print("Secret message found:")
print(decoded_message[:-5])
return decoded_message[:-5]
print("No hidden message found.")
return None
How it Works
- It reads every pixel’s RGB values.
- It extracts only the last bit (the LSB) from each value and appends it to a long binary string.
- It then groups this binary string into 8-bit chunks (bytes).
- It converts each byte back into a character.
- It continues until it finds our special delimiter (
#####
), at which point it knows the message is complete.
Part 3: Putting It All Together (The Main Program)
Finally, let’s create a simple command-line interface so we can easily use our functions. Add this code to the bottom of your stego.py
file.
if __name__ == "__main__":
import sys
if len(sys.argv) < 3:
print("Usage: python stego.py <mode> [args...]")
print("Modes:")
print(" hide <carrier_img> <output_img> \"<secret_message>\"")
print(" reveal <stego_img>")
sys.exit(1)
mode = sys.argv
if mode == 'hide':
if len(sys.argv) != 5:
print("Usage: python stego.py hide <carrier_img> <output_img> \"<secret_message>\"")
sys.exit(1)
carrier_path = sys.argv
output_path = sys.argv
message = sys.argv
hide_message(carrier_path, message, output_path)
elif mode == 'reveal':
if len(sys.argv) != 3:
print("Usage: python stego.py reveal <stego_img>")
sys.exit(1)
image_path = sys.argv
reveal_message(image_path)
else:
print(f"Error: Unknown mode '{mode}'")
How to Use Your New Tool
Save the complete stego.py
file. Make sure you have your carrier.png
in the same directory. Now, open your terminal in that directory and try it out!
To hide a message:
python stego.py hide carrier.png stego_image.png "This is a top secret message."
This will create a new file named stego_image.png
. Open it—it will look identical to carrier.png
!
To reveal the message:
python stego.py reveal stego_image.png
The script will analyze the image and print your hidden message to the console.
You have now successfully built a working steganography tool. You can use it to create your own puzzles, hide data for fun, and better understand the practical application of the techniques you’ve been learning. Welcome to the other side of the game.