UnSHc : déchiffrer des scripts shell compilés et chiffrés par SHc

MISC n° 089 | janvier 2017 | Yann Cam
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Comment déchiffrer un script protégé par SHc ? Comment décrypter un fichier *.sh.x ? SHc fait-il bon usage de la cryptographie? UnSHc répond à ces questions : décortiquons son fonctionnement.

Il n’est pas rare de trouver des scripts *.sh sur des serveurs Unix/Linux de production contenant des mots de passe en clair. Très prisés des auditeurs-pentesteurs, mais aussi des assaillants, les scripts de « backup » (SQL / LDAP), les tâches CRON ou encore les scripts de monitoring détiennent ces précieux sésames sans protection particulière (mis à part les droits d’accès au fichier).

Cette problématique d’exposer des mots de passe en clair dans des scripts revient régulièrement, et l’une des mesures de protection consiste à chiffrer le code source d’un script shell via l’utilitaire SHc. Le code source devient confidentiel, et les mots de passe ne sont plus en clair en production (il est aussi envisageable de ne pas stocker ces mots de passe dans les scripts, mais de les récupérer ou les fournir dynamiquement).

Mais sont-ils réellement en sécurité ? Quel est le niveau réel de confidentialité et de résistance à l'analyse offert par SHc ? L'examen de ce dernier et de l'outil UnSHc va nous permettre de répondre à ces questions.

1. Présentation des outils

1.1 Qu’est-ce que SHc ?

SHc [1] pour SHell Compiler est un outil open source développé par Francisco Javier Rosales Garcia dont la dernière version en date est la 3.8.9b et qui permet de chiffrer n’importe quel script shell interprété sous un terminal Linux. Ainsi il est possible de protéger les sources des scripts *.sh sur des environnements de production, notamment ceux renfermant des mots de passe en clair.

Installation :

$ wget -q http://www.datsi.fi.upm.es/~frosal/sources/shc-3.8.9b.tgz

$ tar zxvf shc-3.8.9b.tgz

$ cd shc-3.8.9b

$ make

Son utilisation permet de limiter l’utilisation d’un script shell dans le temps, ainsi que de chiffrer son code source original sans en altérer le fonctionnement [2]. À partir du script shell, SHc génère un code source C, contenant le script sous forme chiffrée, de quoi le déchiffrer et l’exécuter (voir figure 1).

Fig. 1 : Schéma de fonctionnement général de SHc.

1.2 UnSHc

UnSHc [3] est, comme son nom l’indique, un outil permettant de retrouver le code source *.sh initial à partir de sa version chiffrée avec SHc. Plusieurs cas d’utilisation :

- un script en production est chiffré et vous n’avez plus le code source originel ;

- vous réalisez un audit de sécurité/pentest et afin d’accroître votre emprise sur le SI du client ciblé, il vous est nécessaire de déchiffrer de tels fichiers.

UnSHc est un projet initialement démarré en 2008 par Luiz Otavio Duarte (a.k.a. LOD) et remis au goût du jour par ASafety [4] afin de corriger quelques bugs présents et de porter l’utilisation de celui-ci sur les nouvelles architectures.

Ce présent article vise à détailler finement le fonctionnement de UnSHc (voir figure 2) et vous permettra d’opérer à un déchiffrement manuel des scripts chiffrés par SHc (*.sh.x).

Fig. 2 : Schéma de fonctionnement général de UnSHc.

UnSHc s’inspire du template du code source C généré par SHc (voir figure 1), pour régénérer un code source C (decryptor sur la figure 2) avec les données extraites depuis le binaire.

1.3 Prérequis

UnSHc va analyser statiquement l’exécutable pour retrouver les parties chiffrées et les paramètres permettant le déchiffrement.

Pour réaliser un déchiffrement dans les meilleures conditions via l’exécution de l’outil UnSHc ou en suivant la procédure manuelle, certaines commandes Unix/Linux sont nécessaires. Notamment objdump, grep, cut, shred, uniq, sort, gcc, wc, awk, sed, tr et head.

objdump va désassembler le binaire et le reste du traitement s’opèrera sur deux sorties textes (assembleur et dump hexa) issues de cet outil (OBJFILE et STRINGFILE sur la figure 2).

Le binaire chiffré *.sh.x à déchiffrer pour en récupérer le code source originel en clair doit nécessairement :

- avoir été compilé sur une architecture (x86 / x64) identique à la machine servant au déchiffrement (ARM actuellement non supporté) ;

- ou disposer des exports de la commande objdump réalisés sur le système d’accueil du script *.sh.x, pour un traitement ultérieur sur une autre machine via UnSHc.

2. Analyse d’une exécution standard de SHc

2.1 Qu’avons-nous en sortie de shc ?

Script d’exemple :

#!/bin/bash

# This script is very critical !

echo "I'm a super critical and private script !"

PASSWORD="L3ffe4Ev3R"';

myService --user=milo --password=$PASSWORD > /dev/null 2>&1

Chiffrement de myScript.sh :

$ ./shc -r -f myScript.sh

[root@server:~/scripts/shc]$ ll myScript.sh*

-rwxr-xr-x 1 root root 191 2016-09-18 17:03 myScript.sh*

-rwx--x--x 1 root root 10508 2016-09-18 17:06 myScript.sh.x*

-rw-r--r-- 1 root root 10421 2016-09-18 17:06 myScript.sh.x.c

Nous disposons donc :

- du script initial myScript.sh dont le code source est accessible en clair ;

- de la version intermédiaire produite par SHc à savoir myScript.sh.x.c, qui n’est autre que le code source en C contenant notre script myScript.sh chiffré ;

- de la version chiffrée/compilée produite par SHc à savoir myScript.sh.x.

C’est cette dernière version *.sh.x qui est vouée à être placée en production. Seul ce fichier (qui assure les mêmes fonctionnalités que la version en clair *.sh) est nécessaire, et garantit la confidentialité du code source originel (jusqu’à maintenant…).

2.2 Analyse du *.sh.x.c

2.2.1 Traitements réalisés

En détaillant pas à pas le fonctionnement interne de SHc, voici dans les grandes lignes les traitements réalisés :

1. SHc récupère tout le code source du script *.sh à chiffrer dans une variable (commentaires inclus), sous forme de string ;

2. SHc génère son matériel cryptographique destiné aux étapes de chiffrement, notamment une clé (pswd) ;

3. SHc produit un code source en C, comprenant diverses fonctions (notamment arc4() qui opère un chiffrement avec l’algorithme symétrique RC4) ;

4. SHc incorpore dans ce code source en C les données nécessaires à la génération du flot aléatoire et au déchiffrement ainsi que le code source (*.sh) chiffré avec ces données/clés ;

a. La définition et déclaration de ces éléments statiques du code source se fait de manière aléatoire. Un bloc complet en notation hexadécimale est déclaré dans la source ;

b. Chaque donnée cryptographique est concaténée dans ce bloc. Des #define permettent de déduire les emplacements (offset) et les tailles (size) de chaque bloc concaténé ;

5. Le code source C généré est compilé via gcc sur l’architecture d’accueil ;

6. Le binaire *.sh.x est produit, disposant des fonctionnalités similaires au *.sh originel.

2.2.2 Bloc de données aléatoire

Chaque script *.sh.x.c dispose d’un bloc data statique en amont sous format hexadécimal, puis les fonctions servant à exploiter ce bloc (extraction des données, déchiffrement de la source à la volée, etc.) sont définies.

Si l’on régénère un autre fichier *.sh.x.c à partir du même script *.sh à chiffrer, la définition en amont des données présentera des déclarations aléatoires et les données cryptographiques seront bien évidemment renouvelées.

Exemple de comparaison de deux en-têtes de fichiers *.sh.x.c générés à partir du même script *.sh (voir figure 3).

Fig. 3 : Exemple du bloc data de la source C suite à deux exécutions distinctes de SHc pour un même script.

Pour un même fichier *.sh à chiffrer, les codes sources *.sh.x.c générés par deux exécutions distinctes de SHc produisent des blocs en amont différents. Le placement des données identifié par les #define (cachés de la figure 3) est aléatoire.

On remarque également que la clé de chiffrement (en rouge sur la figure 3) ainsi que d’autres données cryptographiques varient, engendrant une source chiffrée (en vert sur la figure 3) différente d’une exécution sur l’autre.

2.2.3 Fonctions principales

La fonction C au cœur des traitements cryptographiques est la fonction arc4(). Celle-ci équivaut à l’algorithme RC4 de chiffrement par flot.

Cette fonction est appelée exactement 14 fois par la fonction principale xsh(). Ce nombre d’appels est particulièrement important pour la suite (voir figure 4).

Fig. 4 : Les 14 appels de arc4() et l’appel de key().

L’appel à la fonction key() avec le paramètre pswd est réalisé juste avant le chiffrement par arc4() (encadré en vert sur la figure 4) : il s’agit donc de la fonction de génération du flot pseudo-aléatoire. Ensuite les 14 ensembles de données (nommés msg1, date, shll, inlo, xecc, lsto, tst1, chk1, msg2, rlax, opts, text, tst2, en enfin chk2) sont fournis, dans cet ordre, accompagnés de leur taille respective, à la fonction de (de)chiffrement arc4().

2.3 Analyse du *.sh.x

Lorsqu’un script chiffré par SHc est exécuté, les opérations suivantes sont réalisées :

1. Les données cryptographiques statiques contenues au sein du binaire *.sh.x sont chargées en mémoire. Celles-ci se trouvent dans le binaire, de manière statique, à des offsets aléatoires choisis lors de la génération de la source C (voir figure 1 et 3) ;

2. Les diverses fonctions du binaire *.sh.x sont appelées, notamment les 14 appels à la fonction arc4() exploitant tour à tour et de manière ordonnée les données cryptographiques (voir figure 4) ;

3. Une fois la source originelle déchiffrée en mémoire, celle-ci peut être exécutée toujours en mémoire. Utilisation d’un execvp() via un sh -c en passant les arguments du script au sous-processus (voir figure 5).

Fig. 5 : execvp() du script chiffré.

Le binaire chiffré contient donc toutes les données cryptographiques (déclarées à des offsets aléatoires) nécessaires au déchiffrement à la volée et en mémoire de la source originelle.

Ainsi, dans la logique des choses, il suffit de disposer uniquement du fichier chiffré *.sh.x pour récupérer son code source *.sh originel en clair puisqu’il renferme tout le nécessaire cryptographique : c’est là toute sa force et sa plus grande faiblesse.

Une fois un binaire *.sh.x en notre possession, il sera nécessaire d’en récupérer le code objet (code machine) pour en extraire les données utiles.

3. Désassemblage du binaire

3.1 Utilisation d’objdump

Pour débuter l’ingénierie inverse de notre binaire chiffré (analyse statique), il est nécessaire dans un premier temps de disposer d’un export du code objet (code machine désassemblé), ainsi qu’un export en hexadécimal complet de son contenu :

$ objdump -D myScript.sh.x > OBJFILE

$ objdump -s myScript.sh.x > STRINGFILE

3.2 Offset de la fonction arc4()

À partir du fichier OBJFILE contenant le code désassemblé du binaire chiffré, il est possible de détecter tous les appels de fonctions avec grep à partir de l’instruction call ou callq :

$ cat OBJFILE | grep call

400b58: e8 03 01 00 00 callq 400c60 <__gmon_start__@plt>

0000000000400c20 <calloc@plt>:

400d94: e8 67 fe ff ff callq 400c00 <__libc_start_main@plt>

400e1d: e8 7e ff ff ff callq 400da0 <fork@plt+0x40>

[...]

arc4() est appelée exactement 14 fois. Ainsi, en épurant les résultats précédents avec grep, il est assez aisé de repérer l’offset qui est appelé 14 fois (donc l’adresse de la fonction arc4()) :

$ grep -Eo "call.*[0-9a-f]{6,}" OBJFILE | grep -Eo "[0-9a-f]{6,}" | sort | uniq -c | sort | grep -Eo "(14).*[0-9a-f]{6,}" | grep -Eo "[0-9a-f]{6,}"

400f9b

Cet offset 400f9b est celui correspondant aux appels de la fonction arc4() puisque présent 14 fois dans le code désassemblé.

3.3 Localisation des arguments de arc4()

Une fois l’offset de arc4() trouvé, l’idée va être d’analyser les 14 appels de cette fonction dans le code désassemblé et d’en extraire les arguments (offset + size) pour chaque appel.

Ainsi, en analysant le code objet via grep sur la base de l’offset d’arc4, et en retournant N lignes avant cet appel call, nous devrions voir l’empilement des arguments.

Pourquoi N lignes avant le call ? Car en fonction des architectures et de la distribution, les instructions machines permettant d’empiler les arguments diffèrent [5].

$ grep -B 2 "call.*400f9b" OBJFILE

4014c0: be 2a 00 00 00 mov $0x2a,%esi

4014c5: bf 49 21 60 00 mov $0x602149,%edi

4014ca: e8 cc fa ff ff callq 400f9b <fork@plt+0x23b>

4014cf: be 01 00 00 00 mov $0x1,%esi

4014d4: bf 8c 22 60 00 mov $0x60228c,%edi

4014d9: e8 bd fa ff ff callq 400f9b <fork@plt+0x23b>

[...]

On observe que chaque call (callq sur ma distribution courante) est précédé de deux instructions mov.

La première pousse dans le registre esi un nombre hexadécimal correspondant à une taille (size) de la chaîne à lire (dans data).

La seconde pousse dans le registre edi l’adresse (offset) pour démarrer la lecture de size dans la chaîne data.

Il convient donc d’extraire pour les 14 appels de arc4(), chaque size et chaque offset des arguments qui sont passés, le tout dans l’ordre d’appel.

Extraction des offsets :

$ grep -B 2 "call.*400f9b" OBJFILE | grep -v "400f9b" | grep -Eo "(0x[0-9a-f]{6,})"

0x602149

0x60228c

0x6022a4

0x6022b0

0x602290

0x6022b3

0x6022d3

0x602179

0x6022b8

0x6022cd

0x6022ed

0x6021c4

0x602194

0x6022f1

Extraction des sizes correspondantes :

$ grep -B 2 "call.*400f9b" OBJFILE | grep -v "400f9b" | grep -Eo "(0x[0-9a-f]+,)" | grep -Eo "(0x[0-9a-f]+)" | grep -Ev "0x[0-9a-f]{6,}"

0x2a

0x1

0xa

0x3

0xf

0x1

0x16

0x16

0x13

0x1

0x1

0xc1

0x13

0x13

3.4 Extraction des arguments de arc4()

Maintenant que nous disposons de toutes les tailles et adresses de localisation des 14 arguments de la fonction arc4, il est possible d’en extraire les valeurs à partir du dump STRINGFILE.

$ OFFSET="0x602149"

$ NBYTES="0x2a"

$ OFFSET=$(echo $OFFSET | cut -d 'x' -f 2)

$ # A 2 bytes variable (NBYTES > 0) can be found like this: (in STRINGFILE)

$ # ---------------X

$ # X---------------

$ NLINES=$(( ($NBYTES / 16) +2 ))

$ let LASTBYTE="0x${OFFSET:$((${#OFFSET}-1))}"

$ STRING=$( grep -A $(($NLINES-1)) -E "^ ${OFFSET:0:$((${#OFFSET}-1))}0 " STRINGFILE | awk '{ print $2$3$4$5}' | tr '\n' 'T' | sed -e "s:T::g")

$ STRING=${STRING:$((2*$LASTBYTE))}

$ # Cut the string to size

$ STRING=${STRING:0:$(($NBYTES * 2))}

$ # Convert to a \x??\x??:

$ FINALSTRING=""

$ for ((i = 0; i < $((${#STRING} /2 )); i++)); do

> FINALSTRING="${FINALSTRING}\x${STRING:$(($i * 2)):2}"

> done

$

$ echo $FINALSTRING

\xec\x7d\xb1\x9a\xbf\x1a\x2e\x10\xb9\x36\xcf\xa9\x3e\xe3\xe1\xa6\x26\xa2\x3e\x1a\x64\xd8\x3f\x82\x91\x24\xd5\xe2\xf2\x1d\x28\x0e\x78\x70\x6c\x46\xf3\x8a\x88\xeb\x28\xfd

Cette extraction porte sur le tout premier appel de la fonction arc4, ainsi la valeur sous forme de chaîne hexadécimale résultante correspond à la variable msg1.

Cette opération est à répéter pour l’ensemble des 14 autres variables dont nous avons les offsets et sizes.

3.5 Récupération du pswd et de sa taille

La variable pswd, dernière donnée à récupérer composant le bloc data en amont du script *.sh.x.c est une donnée (offset + size) qui n’est pas exploitée par la fonction arc4(). C’est pourquoi elle ne fait pas partie des 14 autres valeurs précédemment extraites.

Si l’on se réfère au code source *.sh.x.c, on remarque que cette donnée est passée en argument à la fonction key(), qui est l’instruction précédant le tout premier appel de la fonction arc4() (voir figure 4).

Ainsi, si l’on grep le premier appel de la fonction arc4() sur le code désassemblé, et que l’on prend les N lignes précédentes (N dépendant de l’architecture), l’instruction call qui précédera correspondra à notre offset de fonction key() :

$ grep -B 5 -m 1 "call.*400f9b" OBJFILE

4014b1: be 00 01 00 00 mov $0x100,%esi

4014b6: bf 3b 23 60 00 mov $0x60233b,%edi

4014bb: e8 f8 f9 ff ff callq 400eb8 <fork@plt+0x158>

4014c0: be 2a 00 00 00 mov $0x2a,%esi

4014c5: bf 49 21 60 00 mov $0x602149,%edi

4014ca: e8 cc fa ff ff callq 400f9b <fork@plt+0x23b>

3.6 Synthèse des extractions

Nous avons extrait à ce stade tous les offsets, sizes (depuis OBJFILE) et valeurs (depuis STRINGFILE) des arguments passés aux 14 call de la fonction arc4, le tout ordonné. Ainsi que l’offset, la taille et la valeur de l’argument pswd de la fonction key().

On peut donc déduire :

- msg1 : offset 0x602149 (size 0x2a)

- date : offset 0x60228c (size 0x1)

- shll : offset 0x6022a4 (size 0xa)

- inlo : offset 0x6022b0 (size 0x3)

- xecc : offset 0x602290 (size 0xf)

- lsto : offset 0x6022b3 (size 0x1)

- tst1 : offset 0x6022d3 (size 0x16)

- chk1 : offset 0x602179 (size 0x16)

- msg2 : offset 0x6022b8 (size 0x13)

- rlax : offset 0x6022cd (size 0x1)

- opts : offset 0x6022ed (size 0x1)

- text : offset 0x6021c4 (size 0xc1)

- tst2 : offset 0x602194 (size 0x13)

- chk2 : offset 0x6022f1 (size 0x13)

- pswd : offset 0x60233b (size 0x100)

Pour ces 15 parties composant le bloc data originel de la source en C, nous avons leurs valeurs respectives au format \x??\x??.

Il est par conséquent possible de régénérer une source en C avec ces données, dans l’objectif d’obtenir le code source *.sh en clair originel.

4. Création d’une source C personnalisée

L’idée consiste à s’inspirer d’une source *.sh.x.c produite légitimement par SHc (voir figure 1), pour procéder à la reconstruction de notre *.sh (plutôt qu’à son exécution, voir figure 2).

4.1 Déclaration des valeurs extraites

L’ensemble des 15 valeurs de variables nous étant à présent connu, ce code source C peut démarrer en définissant chacune de ces variables extraites par analyse statique associées à leurs tailles respectives.

Les valeurs sont renseignées au format \x??\x??. Les 14 appels de arc4() du code source C devront être alignés pour utiliser ces nouvelles déclarations.

4.2 Afficher le code source sur stdout

Enfin, interrompre l’exécution normale de la fonction xsh() pour ne pas exécuter via execvp() le code déchiffré, mais plutôt l’afficher sur la sortie standard (voir figure 6).

Fig. 6 : Affichage de la source sur stdout plutôt que l’exécuter.

5. Compilation et récupération de la source déchiffrée

La nouvelle source définie, il suffit de la compiler :

$ gcc -o myScript.sh.x.custom.c myScript.sh.x.custom

Exécuter ce nouveau binaire et rediriger la sortie standard dans un fichier *.sh :

$ ./myScript.sh.x.custom > myScript.sh

Visualisation du code source déchiffré en clair :

$ cat myScript.sh

#!/bin/bash

# This script is very critical !

echo "I'm a super critical and private script !"

PASSWORD="L3ffe4Ev3R"';

myService --user=milo --password=$PASSWORD > /dev/null 2>&1

La source originelle, accompagnée des commentaires est à présent disponible en clair.

Conclusion

SHc permet de chiffrer des fichiers SH destinés à des environnements de production. Seulement, par analyse statique du binaire embarquant la version protégée il est possible de retrouver les parties chiffrées, la clé de chiffrement, puis par déchiffrement inverse à SHc, retrouver le script en clair. Ce présent article détaille la démarche manuelle que réalise l’outil UnSHc qui automatise cette récupération.

Embarquer tout le matériel cryptographique (clé, chiffré, aléas) dans un même binaire est une protection limitée, ne résistant pas à l’analyse statique, comme illustrée pas à pas dans cet article. Les scripts *.sh.x contenant tout ce matériel peuvent être reconstitués, ce qui permet d'ôter la protection implémentée par SHc.

D’autres solutions de protection de scripts shell existent, notamment obfsh [6] ou encore shellcrypt [7]. Mais ne remettez pas toute la sécurité de votre système uniquement à ces outils.

UnSHc [4] est disponible sous GitHub [3]. Celui-ci est compatible avec les principales distributions Linux x86 et x64. D’autres supports d’architectures sont prévus (notamment ARM). Toute contribution est la bienvenue ! :)

Remerciements

Je retire mon chapeau à Francisco Javier Rosales Garcia pour son outil SHc, que je continue de conseiller malgré l’existence de UnSHc. Salutations également à toute l’équipe de SYNETIS pour son intérêt, ses conseils et sa motivation sur de tels sujets !

Références

[1] Francisco Javier Rosales García, SHc, http://www.datsi.fi.upm.es/~frosal/

[2] Karsten Günther, « SHC Shell Compiler », Linux-Magazine, http://www.linux-magazine.com/Online/Features/SHC-Shell-Compiler

[3] UnSHc on GitHub, https://github.com/yanncam/UnSHc

[4] UnSHc project on ASafety, https://www.asafety.fr/unshc-the-shc-decrypter/

[5] X86 calling conventions, Wikipedia, https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI

[6] obfsh, « shell script obfuscator », http://www.comp.eonworks.com/scripts/obfuscate_shell_script-20011012.html

[7] ShellCrypt, « shellcrypt shell obfuscation », https://sourceforge.net/projects/shellcrypt/