UnSHc : déchiffrer des scripts SH compilés et chiffrés par SHc – Partie 1/2

Comment déchiffrer un script protégé par SHc ? Comment décrypter un fichier *.sh.x ? Est-ce que les aspects cryptographiques de l’outil SHc sont suffisamment robustes? 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 la plus souvent conseillée consiste à chiffrer le code source d’un script SH via l’utilitaire SHc. Le code source devient confidentiel, et les mots de passe ne sont plus en clair en production.

Mais sont-ils réellement en sécurité ? Les scripts chiffrés *.sh.x par Shc sont-ils parfaitement sûrs ? UnSHc répond à 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 SH 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 :

Son utilisation permet de limiter l’utilisation d’un script SH dans le temps, ainsi que de chiffrer son code source original sans en altérer le fonctionnement [2].

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. 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 et vous permettra d’opérer à un déchiffrement manuel des scripts chiffrés par SHc (*.sh.x).

1.3 Prérequis

Pour réaliser un déchiffrement optimal 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.

De plus, 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 :

Chiffrement de myScript.sh :

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 diverses données pseudo-aléatoires destinées aux étapes cryptographiques, notamment une clé de chiffrement (pswd) ;

3. SHc produit un code source en C, comprenant diverses fonctions (notamment arc4() qui opère le chiffrement symétrique par flot) ;

4. SHc incorpore dans ce code source en C les données nécessaires aux phases cryptographiques ainsi que le code source (*.sh) chiffré avec ces données/clés ;

  • 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 ;
  • 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 dynamique 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 1).

Fig. 1 : 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 1) est aléatoire.

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

2.2.3 Fonctions principales

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

Fig. 2 : Fonction arc4().


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 3).

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


Chaque appel est réalisé dans un ordre précis en passant en paramètre de la fonction arc4() l’offset des données à traiter du bloc en amont, et la taille de celles-ci.

L’ordre de traitement est le suivant : msg1, date, shll, inlo, xecc, lsto, tst1, chk1, msg2, rlax, opts, text, tst2, en enfin chk2.

On note également que l’appel à la fonction key() avec la variable pswd est réalisé juste avant le tout premier appel à la fonction arc4() (encadré en vert sur la figure 3).

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 ;
  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 ;
  3. Une fois la source originelle déchiffrée en mémoire, celle-ci peut être exécutée. Utilisation d’un execvp() via un sh -c en passant les arguments du script au sous-processus (voir figure 4).

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

 

Le binaire chiffré contient donc tout le matériel cryptographique (déclaré de manière aléatoire) nécessaire 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é, 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 :

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 :

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()) :

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.

22Pour mémoire, tout appel de fonction en code machine est précédé de l’empilement des arguments nécessaires à cet appel. Ainsi, en analysant le code objet via grep sur la base de l’offset d’arc4, et en retournant N ligne avant cet appel call, nous devrions voir l’empilement des arguments.

Pourquoi N ligne avant le call? Car en fonction des architectures, les instructions machines permettant d’empiler les arguments diffèrent.

On observe que chaque call (callq sur mon architecture 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 :

Extraction des sizes correspondantes :

Yann CAM
Security Researcher @ASafety / Security Consultant @SYNETIS

La seconde partie de cet article sera publiée prochainement sur le blog, restez connectés 😉

Retrouvez cet article (et bien d’autres) dans MISC n°89, disponible sur la boutique et sur la plateforme de lecture en ligne Connect !