Skip to content

Instantly share code, notes, and snippets.

@dhondta
Last active October 22, 2023 13:06
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Embed
What would you like to do?
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",
]
__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)}
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)
@neoxeo
Copy link

neoxeo commented Jul 23, 2022

Hi,

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 :
Stego-lsb_Sample

ImageContainer Sample :
ImageContainer_Sample

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

Thank you again for this excellent tool.

@kevinmillegmailcom
Copy link

from markdown.util import etree is deprecated.

cf. https://python-markdown.github.io/change_log/release-3.2/#markdownutiletree-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.

@kevinmillegmailcom
Copy link

in mdv/markdownviewer.py, line 157

@dhondta
Copy link
Author

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 ?

@CriimBow
Copy link

In the usage example and in your README example, it is written that the option to specify the columns is --cols, when in fact it is --columns.

@dhondta
Copy link
Author

dhondta commented Mar 26, 2023

@CriimBow Thank you for mentioning this ! 👍

@BREBION-Mathis
Copy link

BREBION-Mathis commented Sep 1, 2023

Hi, i have this error with this command:

python stego_lsb.py extract ch9.png --column-step 2 --rows 1 --columns 128

Error:

Traceback (most recent call last):
  File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/data/utils.py", line 35, in __init_bitstring
    patchy.replace(bs.Bits._getlength, OLD_CODE % " -> int", NEW_CODE % " -> int")
  File "/home/kali/.local/lib/python3.11/site-packages/patchy/api.py", line 56, in replace
    _assert_ast_equal(current_source, expected_source, func.__name__)
  File "/home/kali/.local/lib/python3.11/site-packages/patchy/api.py", line 350, in _assert_ast_equal
    raise ValueError(msg)
ValueError: The code of '_getlength' has changed from expected.
The current code is:
def _getlength(self) -> int:
    """Return the length of the bitstring in bits."""
    return len(self._bitstore)

The expected code is:

def _getlength(self) -> int:
    """Return the length of the bitstring in bits."""
    return self._datastore.bitlength


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kali/Documents/Tools/Steganography/LSB-Steganography/stego_lsb.py", line 133, in <module>
    p.extract(args.bits, args.channels, args.cols, args.rows, args.coloffset, args.rowoffset, args.colstep,
  File "/home/kali/Documents/Tools/Steganography/LSB-Steganography/stego_lsb.py", line 76, in extract
    B = ts.int2bin(pixel[c])[::-1]
        ^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/data/transform/common.py", line 143, in int2bin
    bs = Bits()
         ^^^^^^
  File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/data/utils.py", line 60, in __init_bitarray
    class BitArray(bitstring.BitArray):
                   ^^^^^^^^^^^^^^^^^^
  File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/common.py", line 53, in _load
    postload(m)
  File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/data/utils.py", line 37, in __init_bitstring
    patchy.replace(bs.Bits._getlength, OLD_CODE % "", NEW_CODE % "")
  File "/home/kali/.local/lib/python3.11/site-packages/patchy/api.py", line 56, in replace
    _assert_ast_equal(current_source, expected_source, func.__name__)
  File "/home/kali/.local/lib/python3.11/site-packages/patchy/api.py", line 350, in _assert_ast_equal
    raise ValueError(msg)
ValueError: The code of '_getlength' has changed from expected.
The current code is:
def _getlength(self) -> int:
    """Return the length of the bitstring in bits."""
    return len(self._bitstore)

The expected code is:

def _getlength(self):
    """Return the length of the bitstring in bits."""
    return self._datastore.bitlength

Python version:

Python 3.11.4

@kevinmillegmailcom
Copy link

@BREBION-Mathis
Try
python3 stegolsb.py -v extract ch9.png --column-step 2 --rows 1 --columns 128
or
python3 stegolsb.py bruteforce ch9.png

@BREBION-Mathis
Copy link

@kevinmillegmailcom

Tks for you'r post but i have the same error...

@MonkeyCst
Copy link

Hello !
Déjà merci pour ton tool ! Lorsque je l'utilise sur une image png j'ai une énorme chaine de caractère illisible en sortie apres [Hidden data], ca me semble etre du binaire je pense :

python stegolsb.py extract hidden.png
_ _ _
| | ___ __ _ ___ | || |_
/ | / _ / ` |/ _ | / | ' \
_
\ || __/ (
| | () | _ \ |) |
|
/___
|_, |___/||/./
|___/

09:04:10 [DEBUG] Image size: 225x225
09:04:10 [DEBUG] Bits [0], channels RGB, column step 1, row step 1
09:04:17 [INFO] Hidden data:
ó®ûãþº÷;çÏ;ï¯>÷+§ÜÕÆùqø·Þkã¯Z· aòßÏ¿öÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿï¿ÛÞnG}@»FÏY;V¬wçÿEÿÿöÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿïÿÛÿãÛß3Ô2I$ñ¿ûÿöÛÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿßÿü[k$
U*Ih▒ëÿ_׿·Ûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿý¶ßüÚß_à]Ñë(§¹¶öÛ·Ûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ ETC.....

Une idée de ce que ca représente exactement ?

@BREBION-Mathis
Copy link

@MonkeyCst
Salut à toi !
Juste une question sur quel système et quelle version de python utilises-tu ce tool ?

@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. :)

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