Skip to content

Instantly share code, notes, and snippets.

@dhondta
Last active February 10, 2024 10:30
Show Gist options
  • Save dhondta/90a07d9d106775b0cd29bb51ffe15954 to your computer and use it in GitHub Desktop.
Save dhondta/90a07d9d106775b0cd29bb51ffe15954 to your computer and use it in GitHub Desktop.
Tinyscript steganography tool based on base32/64 padding

Paddinganograph

This Tinyscript-based tool allows to unhide data hidden in base32/base64 strings. It can take a PNG or JPG in input to retrieve an EXIF value as the input data.

This can be installed using:

$ pip install tinyscript
$ tsm install paddinganograph
$ paddinganograph -e base64 -f Comment -s . < test.jpg
[...]
$ paddinganograph -e base64 -f Comment -s . < test.jpg | paddinganograph -e base32
[...]

This tool is especially useful in the use cases hereafter.

Retrieve hidden data from an image using Base32/64 padding

Select the "Comment" field, split its value with the separator "." and extract hidden data:

$ paddinganograph -s "." -f "Comment" < image.jpg
[...]
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
import math
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.6"
__copyright__ = ("A. D'Hondt", 2019)
__license__ = "gpl-3.0"
__reference__ = "https://inshallhack.org/paddinganography/"
__examples__ = ["-s . -f \"Comment\" < image.jpg > base32.enc", "-e base32 < base32.enc"]
__doc__ = """
*Paddinganograph* allows to unhide data hidden in base32/base64 strings. It can take a PNG or JPG in input to retrieve an EXIF value as the input data.
"""
SCRIPTNAME_FORMAT = "none"
DEF_BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
DEF_BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
def exif(raw_data, key):
fp = ts.TempPath().tempfile()
with fp.open('wb') as f:
f.write(raw_data)
logger.debug("Getting EXIF data...")
exif = subprocess.check_output(["exiftool", str(fp)])
fp.remove()
exif = codext.decode(exif, "utf-8")
exif = {l.split(':', 1)[0].strip(): l.split(':', 1)[1].strip() for l in exif.split('\n') if l.strip() != ""}
return exif if not key else exif[key]
def unhide(encoded, encoding="base64", charset=None, sep=".", pad="=", n_pad=8):
try:
charset = (charset or globals()["DEF_{}".format(encoding.upper())]).strip()
except NameError:
raise ValueError("Bad encoding")
logger.debug("Unhidding data...")
bits = b("")
for token in b(encoded).split(b(sep)):
bits += unhide_bits(token.strip(), charset, pad, n_pad) or b("")
return b("").join(b(ts.bin2str(ensure_str(bits[i:i+8]))) for i in range(0, len(bits), 8))
def unhide_bits(encoded, charset, pad="=", n_pad=8):
def __gcd(a,b):
while b > 0:
a, b = b, a % b
return a
padding = encoded.count(b(pad))
n_repr = int(math.ceil(math.log(len(charset), 2)))
w_len = n_repr * n_pad / __gcd(n_repr, n_pad)
n_char = int(math.ceil(float(w_len) / n_repr))
if encoded == "" or len(encoded) % n_char != 0 or padding == 0:
return
unused = {n: int(w_len - n * n_repr) % n_pad for n in range(n_char)}
b_val = bin(b(charset).index(encoded.rstrip(b(pad))[-1]))[2:].zfill(n_repr)
return b(b_val[-unused[padding]:])
if __name__ == '__main__':
parser.add_argument("-c", "--charset", help="characters set")
parser.add_argument("-e", "--encoding", choices=["base32", "base64"], default="base64", help="character encoding")
parser.add_argument("-f", "--exif-field", dest="exif", default="Comment", help="EXIF metadata field to be selected")
parser.add_argument("-p", "--padding-char", default="=", help="padding character")
parser.add_argument("-s", "--separator", default="\n", help="base-encoded token separator")
initialize()
data = b("").join(l for l in ts.stdin_pipe())
if data.startswith(b("\x89PNG")) or data.startswith(b("\xff\xd8\xff\xe0")):
try:
data = exif(data, args.exif)
except KeyError:
logger.error("No EXIF field '%s'" % args.exif)
sys.exit(1)
print(ensure_str(unhide(data, args.encoding, args.charset, args.separator)))
@harmtemolder
Copy link

Can you explain what exactly happens in the unhide_bits function?

@Blasci
Copy link

Blasci commented May 31, 2023

Bonjour a tous,

Si comme moi vous avez cette erreur :

NameError: name 'codecs' is not defined. Did you mean: 'code'?

Ajouter simplement la ligne suivante dans le fichier

import codecs

Salutation ✋

@dhondta
Copy link
Author

dhondta commented Jun 2, 2023

@Blasci Merci pour cette remarque !
Le problème vient du fait que j'ai retiré codecs des pré-imports dans Tinyscript, sur lequel s'appuie le présent outil.
J'y ai donc remplacé codecs par codext (qui est son extension).

@roumy
Copy link

roumy commented Nov 6, 2023

Hi there
I had following problem on my kali 2023.2
File "/home/kali/.local/lib/python3.11/site-packages/tinyscript/helpers/data/transform/common.py", line 27, in __validation raise ValueError(f"Bad input binary string '{v}'") ValueError: Bad input binary string 'b'01000110''
looks like there is a compatibility problem with bytes and string

i "ugly dont juge me" fixed it by commenting validation in tinyscript /tinyscript/helpers/data/transform/common.py

def __validation(**kwargs) -> None:
    """ Private generic validation function for the whole data formats. """
    for k, v in kwargs.items():
        if k == "b":
            #print("k,v:" , v)
            continue
            #if not is_bin(v):
             #   raise ValueError(f"Bad input binary string '{v}'")

and convert str to bytes in unhide function

def unhide(encoded, encoding="base64", charset=None, sep=".", pad="=", n_pad=8):
    try:
        charset = (charset or globals()["DEF_{}".format(encoding.upper())]).strip()
    except NameError:
        raise ValueError("Bad encoding")
    logger.debug("Unhidding data...")
    bits = b("")
    for token in b(encoded).split(b(sep)):
        bits += unhide_bits(token.strip(), charset, pad, n_pad) or b("")
    #print (type(bits))
    tmp=[]
    for i in range(0, len(bits), 8):
        tmp.append( bytes(ts.bin2str(bits[i:i+8]),'ascii') )
    #print (tmp)
    return b("").join(tmp)```

Do not hesitate to improve, cheers

@dhondta
Copy link
Author

dhondta commented Nov 9, 2023

Hi @roumy
Thank you for mentioning this.
I don't have that much time to check right now, but did you first pip install --upgrade tinyscript ? It seems like a bug that may have occurred in a previous release but I think this should have been solved with some recent changes. If not, tell me and I will look into it.

@roumy
Copy link

roumy commented Nov 10, 2023

Thanks @dhondta ,
Pip upgrade did not solve it
i confirm i have the pb on a kali 2023.2 with python 3.11.2 , tinyscript update to 1.29.3
i also add it on a kali 2022 with python 3.9.2 .
Tip was given by a guy that previously solve the chall but were also not able to solve it anymore.

@dhondta
Copy link
Author

dhondta commented Nov 11, 2023

@roumy OK, I figured it out ; the offending line is :

return b("").join(ts.bin2str(bits[i:i+8]) for i in range(0, len(bits), 8))
>>> from tinyscript import *
>>> ts.bin2str("01010101")
'U'
>>> ts.bin2str(b"01010101")
Traceback (most recent call last):
  File "/usr/lib/python3.8/idlelib/run.py", line 559, in runcode
    exec(code, self.locals)
  File "<pyshell#10>", line 1, in <module>
  File "/home/morfal/.local/lib/python3.8/site-packages/tinyscript/helpers/data/transform/common.py", line 65, in _wrapper
    return f(binary, *a, **kw)
  File "/home/morfal/.local/lib/python3.8/site-packages/tinyscript/helpers/data/transform/common.py", line 104, in bin2str
    __validation(b=bs, n_b=nbits_in, n_B=nbits_out)
  File "/home/morfal/.local/lib/python3.8/site-packages/tinyscript/helpers/data/transform/common.py", line 27, in __validation
    raise ValueError(f"Bad input binary string '{v}'")
ValueError: Bad input binary string 'b'01010101''

And now, it can be fixed by ensuring that the input to ts.bin2str is of type str and not bytes :

>>> from tinyscript import *
>>> ts.bin2str(ensure_str(b"01010101"))
'U'

Ugly but I have no time right now to fix this directly into Tinyscript. I added the ensure_str invocation in the offending line on this Gist.

You can tsm update then tsm install paddinganograph --force to get the latest version.

@roumy
Copy link

roumy commented Nov 11, 2023

Corrected 👍
Thank you @dhondta

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