|
#!/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) |
Hi @mathysbriand
tsm
is installed withpip install tinyscript
. In other words, you should have it in your terminal once Tinyscript is installed, unless yourPATH
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
;