Skip to content

Instantly share code, notes, and snippets.

@dhondta
Last active July 8, 2024 19:18
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)
@dhondta
Copy link
Author

dhondta commented Oct 19, 2023

@BREBION-Mathis
Bonjour !
Mes excuses pour le temps que je mets à répondre (emploi du temps très chargé).

L'erreur vient d'un changement de code de la librairie bitstring ;

Je vais regarder à ceci dès que possible et mettre à jour Tinyscript.

@dhondta
Copy link
Author

dhondta commented Oct 19, 2023

@BREBION-Mathis
J'ai rencontré récemment un problème avec une version au-delà de 4.0.2 de bitstring. Je n'ai actuellement pas de fix, j'ai donc piné la version dans ma dernière release (1.28.6) de Tinyscript.
Si tu fais pip install --upgrade tinyscript (il faut peut-être ajouter d'autres options avec Python3.11 et une version récente de Pip) pour mettre à jour vers Tinyscript 1.28.6, l'outil devrait fonctionner.

@dhondta
Copy link
Author

dhondta commented Oct 19, 2023

@MonkeyCst
C'est certainement du bullshit. Ce sont certainement tes paramètres qui ne sont pas encore les bons.
Il faut jouer avec les options --column-step, --rows et --columns.

@dhondta
Copy link
Author

dhondta commented Oct 19, 2023

@BREBION-Mathis @MonkeyCst
J'en profite pour faire ma pub' ; si vous aimez, n'oubliez pas de starrer, y compris Tinyscript ;-)

@BREBION-Mathis
Copy link

@dhondta

Merci beaucoup de ta réponse et pas de soucis je comprend, on a tous nos propres préoccupations.
Je vais donc suivre tes conseils et aussi essayer de regarder de mon côté et changer de version tinyscript. :)

@CoSmOo1
Copy link

CoSmOo1 commented Feb 10, 2024

Hello, I have the next problem with your tool @dhondta :
Command: tsm install stegolsb
Result:

 [WARNING] No cache available ; please use 'tinyscript update' to get script links from sources
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\**********\AppData\Local\Programs\Python\Python312\Scripts\tsm.exe\__main__.py", line 7, in <module>
  File "C:\Users\**********\AppData\Local\Programs\Python\Python312\Lib\site-packages\tinyscript\__main__.py", line 192, in main
    _update_cache(cache)
                  ^^^^^
UnboundLocalError: cannot access local variable 'cache' where it is not associated with a value

How can i fix it?

@dhondta
Copy link
Author

dhondta commented Feb 11, 2024

@CoSmOo1 Thank you for mentioning.
I pointed out this issue when solving another one of these last days on this similar Gist.

With this latest modification, you should now get the warning with "tsm update" instead of "tinyscript update" and no exception related to the cache.

Please do the following and retest :

$ pip install --upgrade tinyscript
$ tsm update
$ tsm install --force stegolsb
$ stegolsb --help

@mathysbriand
Copy link

Hi, i wanted to install stegolsb to test it on the root-me challenge but i can't found the tsm command, i tried to install it but wont do

@dhondta
Copy link
Author

dhondta commented Jul 8, 2024

Hi @mathysbriand
tsm is installed with pip install tinyscript. In other words, you should have it in your terminal once Tinyscript is installed, unless your PATH environment variable does not contain the folder where scripts get installed.
Check out your PATH variable and you can add ~/.local/bin permanently in your .bashrc ;

$ echo PATH
# you should get something like this
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games

$ vim ~/.bashrc
[...]
<<add this line at the end of the file>>
export PATH="~/.local/bin:$PATH"

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