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:
Output:
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