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

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.