Skip to content

Instantly share code, notes, and snippets.

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


This Tinyscript-based tool allows to apply steganography based on LSB (Least Significant Bit) in order to retrieve hidden data from an image.

$ 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] [...]
# -*- coding: UTF-8 -*-
from PIL import Image
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.2"
__copyright__ = ("A. D'Hondt", 2020)
__license__ = "gpl-3.0"
__examples__ = [
"-v extract -b 0 test.png",
"extract test.png --cols 128 --rows 1 --column-step 2",
"-w secret.txt bruteforce test.png",
__docformat__ = "md"
__doc__ = """
*StegoLSB* allows to apply steganography based on LSB (Least Significant Bit) in order to retrieve hidden data from an image.
BANNER_FONT = "standard"
BANNER_STYLE = {'fgcolor': "lolcat"}
HOTKEYS = {'d': lambda: show_data(), 'h': lambda: show_data(first=1), 't': lambda: show_data(last=1)}
def show_data(first=0, last=0, width=16):
global p
if not hasattr(p, "data"):
d =
n_lines, PRINT = len(d) // width, re.sub(r"\s", "", string.printable)
for i in range(0, len(d), width):
if first > 0 and i // width >= first:
elif last > 0 and i // width <= n_lines - last:
h = ts.str2hex(d[i:i+width])
h = " ".join(h[j:j+4] for j in range(0, len(h), 4))
b = "".join(c if c in PRINT else "." for c in d[i:i+width])
print("%0.8x: %s %s" % (i, h.ljust(39), b))
class LSB(object):
def __init__(self, image, secret=None):
self.__image = image
self.__secret = secret
self.__write = True
self.__obj =
logger.debug("Image size: {}x{}".format(*self.__obj.size))
def bruteforce(self, bits=False, channels=False, nchars=16, maxstep=10):
self.__write = False
for ch in (ts.bruteforce(3, "RGB", repeat=False) if channels else ["RGB"]):
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] = ""
for y in range(rowoffset, rows, max(1, rowstep)):
data = ""
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]
d = ts.bin2str(data) += d
if self.__write:
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, content):
fn = self.__secret
if fn is None:
fn = os.path.basename(self.__image)
fn, _ = os.path.splitext(fn)
fn = "{}-secret.txt".format(fn)
with open(fn, 'ab') as f:
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, starting from LSB")
extract.add_argument("-c", "--channels", default="RGB", help="channels to be considered")
extract.add_argument("--columns", dest="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" +
Copy link

neoxeo commented Jul 23, 2022


It's me again :-)

I have tried to use StegoLsb to find "file" hidden into image but without success

I use two tools to hide file :

I don't understand why stegolsb doesn't find something.

Here are 2 samples of images contains an other image created with 2 tools.

Stego-LSB Sample :

ImageContainer Sample :

If you find time to have a look, it will be fantastic.

Thank you again for this excellent tool.

Copy link

kevinmillegmailcom commented Jul 27, 2022

from markdown.util import etree is deprecated.


Previously, Python-Markdown was using either the xml.etree.cElementTree module or the xml.etree.ElementTree module, based on their availability. In modern Python versions, the former is a deprecated alias for the latter. Thus, the compatibility layer is deprecated and extensions are advised to use xml.etree.ElementTree directly. Importing markdown.util.etree will raise a DeprecationWarning beginning in version 3.2 and may be removed in a future release.
Therefore, extension developers are encouraged to replace from markdown.util import etree with import xml.etree.ElementTree as etree in their code.

Copy link

kevinmillegmailcom commented Jul 27, 2022

in mdv/, line 157

Copy link

dhondta commented Jul 28, 2022

Hi @neoxeo !
Thanks for the heads up.
I updated the Gist with a new feature from Tinyscript ; some hotkeys for printing hexdumps of data while computing. With "h" and "t", you can respectively check the heading (i.e. to see if you get a file signature like "\x89PNG") and trailing lines of data.
Can you provide the command lines and/or options you used for both of your examples ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment