steGO is a Go-based tool that I wrote for implementing basic steganography, the practice of hiding data within images. This project demonstrates how to encode and decode messages in PNG images using the least significant bit (LSB) technique.

Features

  • Encode text messages into PNG images
  • Decode hidden messages from PNG images
  • Support for custom LSB bit manipulation
  • Lightweight and easy to use

Installation

To install steGO, either:

  • Download the relevant binary from the GitHub releases page, make it executable and run via a CLI
  • Clone the repository and build the project:
git clone https://github.com/thvl3/steGO.git
cd steGO
go build

Usage

Encoding and Decoding

To encode a message into an image:

steGo encode <original_png> <output_png_filename> -file texttohide.txt

To extract a message from an image:

steGo decode <secret.png>

About the Project

This program was built to be used as a fun example for a hands-on lab I ran for my university’s Cybersecurity Society. The project uses Go to embed binary data from text into the least significant bits of a PNG’s red channel.

LSB

In a PNG file, individual pixels are defined by 4 values: (R)ed, (G)reen, (B)lue, and (A)lpha. Each value can hold an integer value between 0 and 255. LSB steganography functions by replacing and storing data in the last bit of that value, where a single integer difference may not be noticeable to the naked eye. (0x100011000 becomes 0x100011001) This method allows us to hide data in these bits and have it remain relatively unnoticed to the unobservant eye.

LSB Implementation Algorithm

I defined the function hideMessage to take the PNG image and text data converted to binary as parameters. I then defined the outer bounds of the message using the height and width of the image in pixels. From there I was able to iterate through the pixels within these bounds and replace the last bit of every red pixel with a bit of data from my message. I then build a new image using the original green, blue, and alpha channels, along with the altered red channel.

// Take png file and the binary version of our text message
func hideMessage(img image.Image, messageBytes []byte) *image.RGBA {
    // Define boundary by image size
    bounds := img.Bounds()
    // Make sure output image size matches initial
    newImg := image.NewRGBA(bounds)

    msgIndex := 0
    bitIndex := 0
    // Iterate through each pixel and the index of the message
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            originalColor := img.At(x, y)
            r, g, b, a := originalColor.RGBA()
            r, g, b = r>>8, g>>8, b>>8 // Convert to 8-bit values
            
            // Modify LSB of red channel to store message bits
            if msgIndex < len(messageBytes) {
                bit := (messageBytes[msgIndex] >> (7 - bitIndex)) & 1
                r = uint32((uint8(r) & 0xFE) | bit) // Fix: Explicit cast to uint8
                
                bitIndex++
                if bitIndex == 8 {
                    bitIndex = 0
                    msgIndex++
                }
            }
            
            newImg.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a >> 8)})
        }
    }
    return newImg
}

The original image vs the resulting image after encoding "Hello world" into a picture of a cat:

Original:

Original cat image

Output:

Cat image with hidden message

As you can probably tell, there is little if not any discernible visual difference between the images. However, when we run the decode command on the output image:

./stego decode output.png

We get: Hidden message: Hello World