Skip to content

Instantly share code, notes, and snippets.



Last active Aug 6, 2020
What would you like to do?
Tinyscript steganography tool implementing the Least Significant Bit algorithm


This can be installed using:

$ pip install tinyscript
$ wget && chmod +x && sudo mv /usr/bin/stegolsb

This tool is especially useful in the use cases hereafter.

Extract hidden data from an image using LSB stegano

$ stegolsb -v extract test.png --column-step 2 --rows 1 --cols 128
12:34:56 [DEBUG] Image size: 225x225
12:34:56 [DEBUG] Bits [0], channels RGB, column step 2, row step 1
12:34:56 [INFO] Hidden data:

Bruteforce LSB stegano parameters to recover hidden data from an image

This will display readable strings recovered using bruteforced paramters.

$ stegolsb bruteforce test.png
12:34:56 [INFO] [...]
#! /usr/bin/python3
# -*- coding: UTF-8 -*-
from PIL import Image
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.0"
__copyright__ = "A. D'Hondt"
__license__ = "agpl-3.0"
__doc__ = """
This tool allows to apply steganography based on LSB (Least Significant Bit)
in order to retrieve hidden data from an image.
__examples__ = [
"-v extract -b 0 test.png",
"extract test.png --cols 128 --rows 1 --column-step 2",
"-w secret.txt bruteforce test.png",
class LSB(object):
def __init__(self, image, secret=None):
self.__image = image
self.__secret = secret
self.__obj =
logger.debug("Image size: {}x{}".format(*self.__obj.size))
def bruteforce(self, bits=False, channels=False, nchars=16, maxstep=10):
i = self.__obj
for ch in (ts.bruteforce(3, "RGB", repeat=False) if channels else \
for bi in (ts.bruteforce(8, range(8), repeat=False) if bits else \
[(0, )]):
for y_s in range(1, maxstep + 1):
for x_s in range(1, maxstep + 1):
self.extract(bi, ch, colstep=x_s, rowstep=y_s)
for s in ts.strings(, nchars):
if self.__secret:
def extract(self, bits=(0, ), channels="RGB", cols=None, rows=None,
coloffset=0, rowoffset=0, colstep=1, rowstep=1):
logger.debug("Bits {}, channels {}, column step {}, row step {}"
.format(list(bits), channels, colstep, rowstep))
i = self.__obj
cols = cols or i.size[0]
rows = rows or i.size[1]
data = ""
for y in range(rowoffset, rows, max(1, rowstep)):
for x in range(coloffset, cols, max(1, colstep)):
pixel = {k: v for k, v in zip("RGB", i.getpixel((x, y)))}
for c in channels.upper():
B = ts.int2bin(pixel[c])[::-1]
for b in bits:
data += B[b] = ts.bin2str(data)
return self
def hide(self, data):
bin_data = ts.str2bin(data)
#TODO: implement hiding data
bin_len = ts.int2bin(len(bin_data))
return self
def write(self, filename=None, content=None):
filename = self.__secret or filename
if filename is None:
filename = os.path.basename(self.__image)
filename, _ = os.path.splitext(filename)
filename = "{}-secret.txt".format(filename)
with open(filename, 'ab') as f:
f.write(b(content or
return self
if __name__ == "__main__":
parser.add_argument("-w", "--write", help="write data to a file")
subparsers = parser.add_subparsers(help="commands", dest="command")
extract = subparsers.add_parser('extract',
help="manually extract hidden data")
bruteforce = subparsers.add_parser('bruteforce', help="bruteforce "
"parameters for extracting hidden data")
extract.add_argument("image", type=ts.file_exists, help="image path")
extract.add_argument("-b", "--bits", type=ts.pos_ints, default="0",
help="bits to be considered, starging from the LSB")
extract.add_argument("-c", "--channels", default="RGB",
help="channels to be considered")
extract.add_argument("--cols", type=ts.pos_int,
help="number of image columns to be considered")
extract.add_argument("--column-offset", dest="coloffset", type=ts.pos_int,
default=0, help="column offset for searching for data")
extract.add_argument("--column-step", dest="colstep", type=ts.pos_int,
default=1, help="step number for iterating columns")
extract.add_argument("--rows", type=ts.pos_int,
help="number of image rows to be considered")
extract.add_argument("--row-offset", dest="rowoffset", type=ts.pos_int,
default=0, help="row offset for searching for data")
extract.add_argument("--row-step", dest="rowstep", type=ts.pos_int,
default=1, help="step number for iterating rows")
bruteforce.add_argument("image", type=ts.file_exists, help="image path")
bruteforce.add_argument("-b", "--bits", action="store_true",
help="bruteforce the bits positions",
note="if false, only the LSB is considered")
bruteforce.add_argument("-c", "--channels", action="store_true",
help="bruteforce the color channels",
note="if false, RGB are considered")
bruteforce.add_argument("-n", "--nchars", type=ts.pos_int, default=16,
help="minimal length for readable strings")
bruteforce.add_argument("-s", "--max-step", type=ts.pos_int, default=10,
help="maximum bit step to be considered",
note="e.g. 3 will lookup every 3 bits in the "
"LSB-collected data")
p = LSB(args.image, args.write)
if args.command == "bruteforce":
p.bruteforce(args.bits, args.channels, args.nchars, args.max_step)
elif args.command == "extract":
p.extract(args.bits, args.channels, args.cols, args.rows,
args.coloffset, args.rowoffset, args.colstep, args.rowstep)"Hidden data:\n" +
if args.write:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment