Skip to content

Instantly share code, notes, and snippets.

@dhondta
Last active April 5, 2025 18:41
Show Gist options
  • Save dhondta/d2151c82dcd9a610a7380df1c6a0272c to your computer and use it in GitHub Desktop.
Save dhondta/d2151c82dcd9a610a7380df1c6a0272c to your computer and use it in GitHub Desktop.
Tinyscript steganography tool implementing the Least Significant Bit algorithm

StegoLSB

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
$ tsm install 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 --columns 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.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",
]
__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)}
SCRIPTNAME_FORMAT = "none"
def show_data(first=0, last=0, width=16):
global p
if not hasattr(p, "data"):
return
d = p.data
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:
break
elif last > 0 and i // width <= n_lines - last:
continue
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 = Image.open(image)
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(self.data, nchars):
logger.info(s)
if self.__secret:
self.write(s)
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]
self.data = ""
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)
self.data += d
if self.__write:
self.write(d)
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:
f.write(b(content))
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")
initialize(noargs_action="demo")
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)
logger.info("Hidden data:\n" + p.data)
@drace94
Copy link

drace94 commented Feb 28, 2025

@jonbot Hi, I had to face the same issue. To solve it, I just used a virtual environnement with the libs installed and used the command-line :

python3 /path/to/stegolsb bruteforce image.png

instead of

stegolsb bruteforce image.png

Hope this helps !

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