┌───────────────────────┐
                                                         ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄       │
                                                         │ █   █ █ █ █   █       │
                                                         │ █   █ █ █ █▀▀▀▀       │
                                                         │ █   █   █ █     ▄     │
                                                         │                 ▄▄▄▄▄ │
                                                         │                 █   █ │
                                                         │                 █   █ │
                                                         │                 █▄▄▄█ │
                                                         │                 ▄   ▄ │
                                                         │                 █   █ │
                                                         │                 █   █ │
                                                         │                 █▄▄▄█ │
                                                         │                 ▄▄▄▄▄ │
Introduction au chargement SHELF                         │                   █   │
Le lien entre code statique et position-independent      │                   █   │
~ @ulexec and @Anonymous_                                └───────────────────█ ──┘

[ Traduction par kylma ]

1. Introduction

Ces dernières années plusieurs améliorations ont été appportées à l'arsenal
offensif sous Linux, notamment en matière de sophistication et de complexité.
Le malware Linux devient de plus en plus populaire, comme en atteste le
nombre croissant de rapports publics documentant les menaces sous Linux. Ces
dernières incluent des implants Linux, soutenus par des gouvernements, comme
VPNFilter attribué à APT28, Drovorub ou encore la large gamme de malwares Linux
du groupe Winnti.

Cependant, ce gain de popularité ne semble pas encore avoir eu d'effet
si l'on considère la sophistication dans sa globalité du paysage actuel des
menaces sous Linux. C'est un écosystème assez jeune, où les cybercriminels
n'ont pas encore été en mesure d'identifier des leviers fiables de monétisation,
en dehors du minage de cryptomonnaie, des DDos et, plus récemment, des attaques
par ransomware.

Dans l'environnement des menaces sous Linux d'aujourd'hui, même la plus petite
amélioration ou l'ajout d'un peu de complexité aboutit souvent à un contournement
de l'antivirus. C'est pour cette raison que les auteurs de malwares sous Linux
n'ont pas d'intérêt à s'investir inutilement pour sophistiquer leurs implants.
Plusieurs raisons expliquent pourquoi ce phénomène se produit et c'est un sujet
au caractère ambigu. L'écosystème Linux, au contraire d'autres plus populaires
comme Windows et MacOS, est plus dynamique et plus varié, à la fois en raison des
nombreuses variétés de fichiers ELF pour différentes architectures, mais aussi
du fait que les binaires ELF peuvent être valides sous différentes formes et
que la visibilité des menaces sous Linux reste plutôt faible.

Pour ces raisons, les éditeurs d'antivirus se retrouvent face à un
ensemble de challenges complètement différents pour détecter ces menaces.
Souvent, l'échec disproportionné de la détection de menaces pourtant simples ou
non sophistiquées donne implicitement l'impression que les malwares Linux ne sont
naturellement pas complexes. Cette affirmation ne saurait être plus loin de la
vérité, et ceux qui sont familiers avec le format de fichiers ELF savent qu'il
s'agit d'un terrain propice à l'innovation, ce dont d'autres formats de fichiers
ne pourraient se vanter, faute de flexibilité.

Dans cet article, nous allons discuter d'une technique qui permet d'exploiter une
fonctionnalité inhabituelle pour un format de fichier. Cette technique permet de
convertir de manière générique un fichier exécutable complet en un shellcode et
démontre encore une fois que les binaires ELF peuvent être manipulés à des fins
offensives, ce qui reste difficile, voire impossible, pour d'autres formats.


2. Une introduction au chargement réflexif d'ELF

Pour comprendre cette technique, nous devons d'abord évoquer les techniques
préexistantes relatives au format ELF, sur lesquelles cette technique s'appuie,
avec une comparaison des bénéfices et l'évocation de certains compromis.

La plupart des packers ELF, ou toute autre application implémentant n'importe
quelle forme de chargement de binaire ELF, sont principalement basées sur ce que
l'on appelle "Userland Exec" (exécution en espace utilisateur).

"Userland Exec" est une méthode introduite par @thegrugq, dans laquelle
un binaire ELF peut être chargé sans utiliser aucun appel système de la famille
execve(), d'où son nom.

Pour simplifier, les étapes classiques pour implémenter "Userland Exec" avec
support des binaires ELF ET_EXEC et ET_DYN sont illustrées dans le schéma
suivant, montrant une implémentation du packer UPX pour les binaires ELF :



Comme nous pouvons l'observer, cette technique rassemble les prérequis suivants
(par @thegrugq) :

  1. Nettoyer l'espace d'adressage.
  2. Si le binaire est chargé dynamiquement, charger le linker dynamique.
  3. Charger le binaire
  4. Initialiser la stack.
  5. Déterminer le point d'entrée (le linker dynamique ou l'exécutable
     principal).
  6. Transférer l'exécution vers le point d'entrée.

Sur un plan plus technique, nous arrivons aux prérequis suivants :

  1. Mettre en place la stack de l'exécutable embarqué avec son Auxiliary
     Vector correspondant.
  2. Parser le PHDR et identifier s'il y a un segment PT_INTERP, indiquant si
     le fichier est un exécutable linké dynamiquement.
  3. Charger l'interpréteur si PT_INTERP est présent.
  4. Charger l'exécutable embarqué cible.
  5. Pivoter vers le e_entry de l'exécutable cible ou de l'interpréteur, en
     fonction de si l'exécutable cible est un binaire linké dynamiquement.

Pour une explication plus détaillée, nous recommendons la lecture de l'article
de @thegrugq sur le sujet [9].

Une des caractéristiques d'un "Userland Exec" conventionnel est l'absence
d'execve() comme mentionné précédemment, contrairement à d'autres techniques
comme memfd_create/execveat qui sont également largement utilisées pour
charger et exécuter un ficher ELF. Comme le loader mappe et charge
l'exécutable cible, l'exécutable embarqué a le loisir de pouvoir avoir
une structure non conventionnelle, un avantage utile pour la furtivité et
l'anti-forensics.

D'un autre côté, de nombreux artefacts cruciaux sont impliqués dans le
processus de chargement, qui peuvent être facilement reconnus par des reverse
engineers. Ils rendent ainsi le processus fragile en raison du fait que cette
technique repose énormément sur ces éléments. C'est pourquoi écrire des
loaders basés sur "Userland Exec" est fastidieux. De plus, au fur et à mesure
que de nouvelle fonctionnalités sont ajoutées au format de fichier ELF, cette
technique s'est de fait complexifiée.

La nouvelle technique que nous allons traiter dans ce papier s'appuie sur
l'implémentation d'un loader "Userland Exec" générique avec un ensemble limité
de contraintes, supportant un PIE hybride et les binaires ELF linkés
statiquement. À notre connaissance, cette technique n'a pas encore été détaillée
dans la littérature.

Nous pensons que notre technique représente une amélioration drastique des
versions précedentes des loaders reposant sur "Userland Exec". En raison de
l'absence de contraintes d'implémentation techniques et par la nature même de
cette nouvelle variante hybride ELF statique/PIE, l'éventail des possibilités
qu'elle peut offrir est bien plus large et permet plus de furtivité que les
variantes précédentes de "Userland Exec".


3. Fonctionnement interne de la génération d'exécutable statique PIE

3.1 Contexte

En juillet 2017, H. J. Lu fournit un patch pour un bug listé dans le bugzilla
de GCC et le nomme "Support de la création de binaires statiques PIE". Ce patch
présent dans sa branche glibc hjl/pie/static décrit l'implémentation d'un
exécutable PIE statique. Lu y documente que des exécutables statiques PIE ELF
pourraient être ainsi générés en passant les flags -static et -pie au linker,
et en utilisant les versions PIE de crt*.o comme entrée. Il est important de
noter que, à l'époque de ce patch, la génération de binaires PIE entièrement
linkés statiquement n'était pas possible.[1]

En août, Lu soumet un second patch[2] pour le driver GCC, qui rajoute le flag
-static pour supporter les fichiers PIE statiques qu'il a réussi à générer
dans son patch précédent. Le patch est accepté dans trunk[3] et cette
fonctionnalité est publiée dans GCC v8.

De plus, en décembre 2017, un commit fait dans glibc[4] rajoute l'option
-enable-static-pie. Grâce à ce patch, il est possible d'inclure les parties
nécessaires de ld.so pour produire des exécutables statiques PIE autonomes.

La modification majeure dans glibc qui permet d'utiliser des binaires statiaues
PIE est l'addition de la fonction _dl_relocate_static_pie, qui est appelée par
__libc_start_main. Cette fonction est utilisée pour identifier l'adresse de
chargement au run-time, lire le segment dynamique et effectuer les relocations
dynamiques avant l'initialisation et enfin transférer le control flow à
l'application.

Pour savoir quel flags and quelles étapes de compilation/linking sont nécessaires
pour génerer des exécutables statiques PIE, nous avons passé le flag -static-pie -v
à GCC. Cependant, nous avons réalisé qu'en faisant cela, le linker générait une
pléthore de flags et d'appels à des wrappers internes. Par example, la phase
de linking est gérée par l'outil /usr/lib/gcc/x86_64-linux-gnu/9/collect2 et
GCC lui-même est wrappé par /usr/lib/gcc/x86_64-linux-gnu/9/cc1. Nous avons
néanmoins réussi à enlever les flags non pertinents, ce qui nous donne les
étapes suivantes :



Ces étapes sont en fait les même que celles indiquées par Lu, à savoir donner
en entrée au linker des fichiers compilés avec -fpie, -static, -pie, -z text
et --no-dynamic-linker.
En particulier, les artefacts les plus pertinents pour la création d'un binaire
statique PIE sont rcrt1.o, libc.a et notre fichier d'entrée, test.o. L'objet
rcrt1.o notamment contient _start qui possède le code requis pour charger
l'application correctement avant d'exécuter son propre point d'entrée en
appelant le code de démarrage de la libc contenu dans __libc_start_main :



Comme mentionné précedemment, __libc_start_main va appeler la nouvelle fonction
_dl_relocate_static_pie (définie dans le fichier efl/dl-reloc-static-pie.c
des sources de la glibc). Les principales étapes réalisées par cette fonction
sont commentées directement dans le code source :



Avec l'aide de ces fonctionnalités, GCC est capable de générer des exécutables
statiques qui peuvent être chargés à n'importe quelle addresse arbitraire.

On peut remarquer que _dl_relocate_static_pie va gérer les relocations dynamiques
nécessaires. Une différence notable entre rcrt1.o et le plus conventionnel crt1.o
est que tout le code embarqué est position-independent. Si l'on examine à quoi
ressemble le binaire ainsi généré, on peut voir les propriétés suivantes :



Au premier coup d'oeil, on dirait un exécutable linké dynamiquement assez courant,
basé sur le type d'exécutable ET_DYN, récupéré du header ELF. Cependant, si l'on
examine les segments d'un peu plus près, on observe que le segment PT_INTERP,
qui habituellement indique le chemin vers l'intepréteur dans les exécutables
linkés dynamiquement, est absent et qu'un segment PT_TLS est présent, qui est
lui habituellement seulement présent dans les exécutables linkés statiquement.



Si l'on vérifie comment le linker dynamique identifie l'exécutable cible, on peut
confirmer que le type de fichier est identifié correctement :



Pour pouvoir charger ce fichier, tout ce qu'il faudrait faire serait de mapper
tous les segments PT_LOAD en mémoire, mettre en place la stack du processus avec
les entrées Auxiliary Vector correspondantes, puis pivoter vers le point
d'entrée de l'exécutable mappé. Nous n'avons pas besoin de nous préoccuper de
mapper le RTLD puisque nous n'avons aucune dépendances externes ou de
restrictions sur les adresses lors du linkage.

Comme on peut le voir, nous obtenons quatre segments chargeables, classiques
des binaires SCOP ELF. Néanmoins, pour permettre un déploiement plus simple, il sera
crucial de pouvoir fusionner ces segments en un seul, comme il est habituellement
fait dans les injections ELF sur disque dans un exécutable étranger. Il est
possible de faire ça en utilisant le flag -N du linker pour fusionner data et
text en un seul segment.

3.2. Incompatibilité des flags -N et -static-pie de GCC

Si nous passons les flags -static-pie et -N ensemble à GCC, on voit que
l'exécutable suivant est généré :



La première chose que l'on peut remarquer sur ce type d'ELF généré en utilisant
uniquement -static-pie est qu'il possède un type ET_DYN, et que l'ajout du flag
-N le transforme en un type ET_EXEC.

De plus, si nous observons de près les addresses virtuelles du segment, on
peut voir que le binaire généré n'est pas un exécutable position-independent
Cela est dû au fait que les addresses virtuelles semblent être des addresses
absolues et non relatives. Pour comprendre pourquoi notre programme n'est pas
linké comme attendu, nous avons inspecté le linker script qui a été utilisé.

Comme nous utilisons le linker ld de binutils, nous avons regardé comment ld
sélectionne le linker script. Ceci est réalisé dans le code de ld/ldmain.c à
la ligne 345 :



ldfile_open_default_command_file est en fait un appel indirect à une fonction
indépendante de l'architecture et générée au moment de la compilation. Cette
fonction contient un ensemble de linker scripts internes qui seront
sélectionnés en fonction des flags passés à ld. Comme nous utilisons Ici
l'architecture x86_64, le fichier de code source généré sera ld/elf_x86_64.c,
et la fonction qui sera appelée pour sélectionner ce script sera
gldelf_x86_64_get_script, qui est simplement un ensemble d'expressions
if-else-if pour sélectionner un linker script interne.
L'option -N quant à elle met la variable config.text_read_only à false, ce qui
force la fonction de sélection à utiliser un script interne qui ne produit pas
du code position-independent, comme illustré ci-dessous :



Cette méthode pour sélectionner le script par défaut rend les flags -static-pie
et -N incompatibles, du fait que la sélection du script basée sur l'option -N
est parsée avant -static-pie.

3.3. Contournement via un linker script personnalisé

L'incompatibilité entre les flags -N, -static et -pie mène à une impasse, et
nous a forcé à penser à plusieurs moyens pour surmonter cet obstacle. Nous
avons donc essayé de fournir un script personnalisé pour contrôler le linker.
Comme nous avons besoin de fusionner le comportement de deux linker scripts
séparés, notre approche a été de choisir l'un des deux scripts et de l'adapter
pour qu'il génère le résultat attendu avec des fonctionnalités du script restant.

Nous avons choisi le script par défaut de -static-pie comme base, par rapport à 
celui utilisé avec -N parce qu'il était plus facile à modifier dans notre cas.
Modifier le script par défaut de -N pour supporter la génération de PIE est
beaucoup plus difficile.

Pour réaliser cet objectif, il nous faut modifier les définitions des segments,
qui sont controlés par le champs PHRDRS [5] dans le linker script. Si la
commande n'est pas utilisée, le linker fournit au programme des headers générés
par défaut. Cependant, si nous ignorons cela dans le linker script, le linker
ne va pas créer de headers additionnel au programme, et suivra strictement
les directives définies dans le linker script cible.

En prenant en compte les détails discutés ci-dessus, nous avons ajouté une
commande PHDRS au linker script par défaut, qui commence avec la création de
tous les segments originels créés par défaut quand -static-pie est utilisé :



Après ça, nous avons besoin de savoir comment chaque section est mappée vers chaque
segment, et pour cela nous pouvons utiliser readelf comme illustré ci-dessous :



Avec la connaissance des mappings, nous avons juste besoin de changer la
définition du segment de sortie dans le linker script, qui ajoute le nom du
segment approprié à la fin de chaque définition de fonction, comme démontré
par l'exemple suivant :



Ici, les sections .tdata et .tbss sont assignées à des segments mappés dans le
même ordre que celui observé dans la sortie de la commande readelf -l.
Finalement, nous obtenons un script qui fonctionne et qui modifie exactement
toutes les sections qui étaient mappées dans le segment data pour qu'elles soient
mappées dans le segment text.



Si nous compilons notre fichier de test avec ce linker script, on voit
l'exécutable généré suivant :



Nous avons maintenant un static-pie avec seulement un seul segment qui peut être
chargé. On peut répéter la même démarche pour enlever les segments non
pertinents et garder suelement les segments critiques à l'exécution du binaire.
Par example, voici une instance d'un exécutable static-pie avec des headers 
minimaux suffisants pour pouvoir s'exécuter :



L'example suivant illustre le résultat final de la structure ELF désirée à
avoir seulement un segment PT_LOAD généré par un linker script avec la
command PHDRS configurée comme dans la capture d'écran ci-dessous :




4. Chargement SHELF

La variante ELF que nous avons générée nous donne des possibilités intéressantes,
que les autres types d'ELF ne possèdent pas. À des fins de simplicité, nous avons
qualifié ce type de binaire ELF de SHELF et nous utiliserons ce nom pour nous y
référer dans la suite de cette article. Ci-dessous, un schéma mis à jour illustrant
les étapes nécessaires pour un chargement SHELF.



Comme nous pouvons voir dans le schéma ci-dessus, le processus pour charger
des fichiers SHELF est bien moins complexe que le mécanisme de chargement
d'un ELF conventionnel.

L'exemple de code ci-dessous illustre l'ensemble restreint de contraintes
nécessaires pour générer un binaire SHELF minimaliste selon la méthode
"Userland Exec" :



En utilisant cette approche, un fichier SHELF ressemblerait à cela en mémoire
et sur disque :



Comme nous pouvons l'observer, le header ELF et les Program Headers sont absents
de l'image du processus. Nous détaillons cette fonctionnalité de notre variante ELF
dans la section suivante.

4.1 Capacités Anti-Forensics

Cette nouvelle approche du "Userland Exec" présenter deux étapes intéressantes
pour intégrer des capacités d'anti-forensics. Comme la fonction dl_relocate_static_pie
va obtenir depuis l'Auxiliary Vector tous les champs requis pour la relocation, cela
nous laisse une marge de manoeuvre sur ce à quoi la structure du fichier SHELF va
ressembler en mémoire ou sur disque.

La suppression du header ELF va directement avoir un effet sur les possibilités de
reconstruction, parce que la plupart des scanners Linux analysent la mémoire
des processus et cherchent des images ELF en tentant d'identifier les headers ELF. Le
header ELF sera alors parsé et contiendra l'emplacement de la Program Header Table
et par conséquent, du reste des artefacts mappés du fichier.

L'élimination du header ELF est triviale puisque cet artefact n'est pas vraiment
nécessaire au loader : toutes les informations requises du fichier cible seront
récupérées depuis l'Auxiliary Vector, comme mentionné précédemment.

Un autre artefact qui peut être caché est la Program Header Table elle-même. C'est un
cas un peu différent par rapport au header ELF. Le Auxiliary Vector a besoin de
localiser la Program Header Table afin que le RTLD puisse charger avec succès
le fichier, en appliquant les relocations nécessaires lors de l'exécution. Néanmoins,
il existe plusieurs approches pour obfusquer la PHT. L'approche la plus simple
consiste à supprimer l'information de l'emplacement originel de la Program Header Table
et de la transférer à un autre endroit dans le fichier qui n'est connu que par
l'Auxiliary Vector.



Nous pouvons pré-calculer l'emplacement de toutes les entrées de l'Auxiliary
Vector et definir chaque entrée comme une macro dans un fichier include,
ce qui nous permet d'adapter notre loader à notre fichier SHELF cible lors de
la compilation. Ci-dessous un example de comment ces macros peuvent être générées :



Comme nous pouvons l'observer, nous avons parsé le fichier SHELF cible pour
y trouver les champs e_entry et e_phnum, et créé les macros correspondantes
pour pouvoir stocker ces valeurs. Il nous a également fallu choisir une base
de chargement de l'image aléatoire. Finalement, nous avons localisé la PHT et nous 
l'avons convertie en tableau, puis enlevée de son emplacement originel. Appliquer 
ces modifications nous permet de complétement éliminer le header ELF et de
changer l'emplacement par défaut de la PHT du fichier SHELF à la fois sur le
disque mais aussi en mémoire (!).

Sans pouvoir localiser et récupérer la Program Header Table, les possibilités
de reconstruction sont sévèrement limitées et des heuristiques plus avancées
seront nécessaires pour une reconstruction réussie de l'image du process.

Une approche supplémentaire pour compliquer la reconstruction de la Program
Header Table consiste à instrumenter la manière dont glibc implémente la
résolution des champs de l'Auxiliary Vector.

4.2 Dissimuler les caractéristiques SHELF en patchant PT_TLS

Même après avoir modifié l'emplacement par défaut de la Program Header Table
en choisissant un nouvel endroit arbitraire lors de la construction de l'Auxiliary 
Vector, la Program Header Table reste néanmoins toujours présente en mémoire et 
pourrait être découverte en cherchant bien. Afin de la dissimuler encore plus, 
nous pouvons altérer la manière dont le code de démarrage va lire les champs de 
l'Auxiliary Vector.

Le code responsable de ça trouve dans elf/dl_support.c, dans la fonction
_dl_aux_init. Pour résumer, le code itère sur toutes les entrées de auxv_t et
pour chaque entrée initialise les variables internes de la glibc :



La seule raison pour laquelle l'Auxiliary Vector est requis est l'initialisation
variables internes _dl_*. Sachant ça, nous pouvons
contourner entièrement la création de l'Auxiliary Vector et faire le même travail
que _dl_aux_init aurait fait avant de passer la main au fichier SHELF cible.

Les seules entrées qui sont critiques sont AT_PHDR, AT_PHNUM et AT_RANDOM.
Ainsi, nous avons seulement besoin de patcher les variables _dl_* respectives
qui dépendent de ces champs. Pour récupérer ces valeurs, nous pouvons par
exemple utiliser cette ligne de commande pour générer un fichier d'include
avec des macros précalculées contenant les décalages pour chaque variable dl_* :



Maintenant que nous connaissance le décalage vers ces variables, nous avons seulement
besoin de les patcher de la même manière que le code de démarrage l'aurait fait en 
utilisant l'Auxiliary Vector. Pour illustrer cette technique, le code suivant va 
initialiser les adresses des Program Headers à new_address avec le bon nombre de 
Program Headers :



À partir de là, nous avons un program fonctionnel, sans fournir d'Auxiliary Vector.
Comme le binaire cible est linké statiquement et que le code qui va charger
le fichier SHELF est notre loader, nous pouvons ignorer tous les autres segments
de l'Auxiliary Vector (AT_PHDR et AT_PHNUM) ou dl_phdr et dl_phnum respectivement.
Il subsiste une exception, le segment PT_TLS, qui est l'interface par laquelle
le Thread Local Storage est implémenté dans le format de fichier ELF.

Le code suivant, se trouvant dans la fonction __libc_setup_tls du fichier
csu/libc-tls.c, montre le type d'information qui est récupérée du segment
PT_TLS :



Dans le morceau de code ci-dessus, nous pouvons voir que l'initialisation du
TLS repose sur la présence du segment PT_TLS. Nous avons plusieurs approches
pour obfusquer cet artefact, comme par exemple patcher la fonction
__libc_setup_tls pour simplement retourner et initialiser le TLS avec notre
propre code. Pour démontrer ça, nous allons l'implémenter comme un patch rapide
dans la glibc.

Pour éviter d'avoir besoin du PT_TLS Program header, nous avons ajouté une
variable globale qui contient toutes les valeurs du PT_TLS et définit les valeurs
dans __libc_setup_tls, pour qu'elles soient lues depuis notre variable globale
et non pas depuis la Program Header Table du fichier SHELF cible. Avec ce
petit changement, nous sommes finalement en mesure d'éliminer tous les Program
Headers :



En utilisant le script suivant pour générer _phdr.h :



Nous pouvons appliquer nos patchs de la manière suivante, après avoir inclus
_phdr.h :



En appliquant la métodologie susmentionnée, nous avons atteint un très haut
niveau de furtivité, en chargeant et en exécutant notre fichier SHELF qui ne
contient ni header ELF, ni Program Header Table, ni Auxiliary Vector - similaire
à la manière dont un shellcode est chargé. Le schéma suivant illustre le
processsus de chargement plutôt simple des fichiers SHELF :




5. Conclusion

Dans cet article, nous avons traité des mécanismes internes du chargement réfléxif
des fichiers ELF, expliqué les implémentations précédentes de "Userland Exec" ainsi
que leurs avantages et inconvénients. Nous avons ensuite expliqué les derniers 
patchs dans le code source de GCC qui implémentent le support des binaires
static-pie, discuté du résultat désiré et des approches que nous avons suivies
pour atteindre notre objectif de générer des fichiers ELF static-pie avec
un segment unique PT_LOAD. Enfin, nous avons parlé des caractéristiques
anti-forensics que le chargement SHELF offre et que nous pensons être une
amélioration considérable comparé aux précédentes versions de chargement
réflexif d'ELF.

Nous pensons que cela pourrait être la prochaine génération de chargement
réfléxif d'ELF et il pourrait être intéressant pour le lecteur de comprendre
l'étendue des capacités offensives offertes par le format de fichier ELF. Si
vous souhaitez avoir accès au code source, contactez @sblip ou @ulexec.

6. Références

[1] (support static pie)
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81498
[2] (first patch gcc)
    https://gcc.gnu.org/ml/gcc-patches/2017-08/msg00638.html
[3] (gcc patch)
    https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=252034
[4] (glibc --enable-static-pie)
    https://sourceware.org/git/?p=glibc.git;a=commit; \
      h=9d7a3741c9e59eba87fb3ca6b9f979befce07826
[5] (ldscript doc)
    https://sourceware.org/binutils/docs/ld/PHDRS.html#PHDRS
[6] https://sourceware.org/binutils/docs/ld/
      Output-Section-Phdr.html#Output-Section-Phdr
[7] https://www.akkadia.org/drepper/tls.pdf
[8] (why ld doesn't allow -static -pie -N)
    https://sourceware.org/git \
      /gitweb.cgi?p=binutils-gdb.git;a=blob;f=ld/ldmain.c; \
      h=c4af10f4e9121949b1b66df6428e95e66ce3eed4;hb=HEAD#l345
[9] (grugq ul_exec paper)
    https://grugq.github.io/docs/ul_exec.txt
[10] (ELF UPX internals)
     https://ulexec.github.io/ulexec.github.io/article \
       /2017/11/17/UnPacking_a_Linux_Tsunami_Sample.html