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)))
@Cyber-Broccoli
Copy link

Cyber-Broccoli commented Aug 10, 2022

Personnelement pour l'erreur markdown j'ai pu résoudre en rétrogradant la version pour installer markdown-3.3.7 via pip install markdown==3.3.7
En revanche quand je fais un test sur mon fichier j'ai l'erreur suivante:

#python3 paddinography.py -s "." -f "Comment" < ch15.jpg                                                   
Traceback (most recent call last):
  File "/home/kali/Desktop/Rootme/ch15/paddinography.py", line 83, in <module>
    data = exif(data, args.exif)
  File "/home/kali/Desktop/Rootme/ch15/paddinography.py", line 29, in exif
    t = TempPath().tempfile().name
NameError: name 'TempPath' is not defined

(Il y a bien un "Comment" EXIF dans mon fichier)
Merci pour ton aide.

EDIT : j'ai cette erreur si je call le script paddinograph.py mais si j'appelle le bin tout fonctionne depuis ton upgrade du code :)

@Fufu-btw
Copy link

Fufu-btw commented Aug 12, 2022

@tom-weber Merci d'avoir rapporté ce problème. Cette erreur provient de MarkdownViewer (mdv). Dans la dernière version de Tinyscript, au lieu de charger mdv sous Python2 et mdv3 (une version alternative qui fixe la compatibilité) sous Python3, j'ai rétabli un seul import de mdv (et plus mdv3 car cette compatibilité semble avoir été fixée récemment dans le paquet de MarkdownViewer. Par conséquent, si pip install --upgrade mdv et pip install --upgrade tinyscript ne résolvent pas le problème, c'est que le problème de compatibilité n'est toujours pas fixé. Peux-tu essayer ceci et tester de ton côté, stp ?

Après pip install --upgrade mdv et pip install --upgrade tinyscript toutes les dépendances étaient déjà ok + modules à jour.
J'ai relancé le script, même erreur : ImportError: cannot import name 'etree' from 'markdown.util' (/usr/lib/python3/dist-packages/markdown/util.py)

Personnelement pour l'erreur markdown j'ai pu résoudre en rétrogradant la version pour installer markdown-3.3.7 via pip install markdown==3.3.7 En revanche quand je fais un test sur mon fichier j'ai l'erreur suivante:

#python3 paddinography.py -s "." -f "Comment" < ch15.jpg                                                   
Traceback (most recent call last):
  File "/home/kali/Desktop/Rootme/ch15/paddinography.py", line 83, in <module>
    data = exif(data, args.exif)
  File "/home/kali/Desktop/Rootme/ch15/paddinography.py", line 29, in exif
    t = TempPath().tempfile().name
NameError: name 'TempPath' is not defined

(Il y a bien un "Comment" EXIF dans mon fichier) Merci pour ton aide.

EDIT : j'ai cette erreur si je call le script paddinograph.py mais si j'appelle le bin tout fonctionne depuis ton upgrade du code :)

Après pip install markdown==3.3.7, tout est ok ! Merci à vous deux pour votre aide :)

Il ne serait pas plus simple d'avoir un projet python avec les dépendance utilisées ? @dhondta Cela pourrait éviter toutes les erreurs concernant les version !

Merci d'avance !

@dhondta
Copy link
Author

dhondta commented Aug 13, 2022

Salut @tom-weber !
Merci pour ces explications.
Après pip3 install --upgrade markdown, j'ai :

$ pip show markdown
Name: Markdown
Version: 3.4.1
Summary: Python implementation of Markdown.
...

Et j'obtiens bien l'erreur que tu as rapportée :

$ paddinganograph --help
Traceback (most recent call last):
[...]
ImportError: cannot import name 'etree' from 'markdown.util' (/home/morfal/.local/lib/python3.8/site-packages/markdown/util.py)

Le problème vient bien de mdv qui n'est pas compatible avec markdown>=3.4 apparemment.
Je peux épingler markdown==3.3.7 temporairement pour résoudre le problème.
Merci en tout cas.

@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