Sous Linux, la signification chinoise de pic signifie « code indépendant de la position », ce qui signifie que le code peut être exécuté normalement, quelle que soit l'adresse à laquelle il est chargé. PIC est utilisé pour générer des bibliothèques partagées indépendantes de la position. Cela signifie que les segments de code des bibliothèques partagées sont en lecture seule et stockés dans le segment de code. Plusieurs processus peuvent partager ce segment de code en même temps sans. copier.
L'environnement d'exploitation de ce tutoriel : système linux7.3, ordinateur Dell G3.
Sous Linux, le nom complet de pic est « Position Independent Code », qui signifie « code indépendant de position » en chinois.
1. Présentation du code lié à l'espace d'adressage virtuel et à l'emplacement du programme
Lorsqu'un processus Linux est chargé du disque vers la mémoire et exécuté, le noyau allouera un espace d'adressage virtuel au processus et l'adresse virtuelle L'espace est divisé en blocs. Zone (Segment), les zones les plus importantes sont les suivantes :
Figure 1 - Description de l'espace d'adressage virtuel de l'application
L'espace d'adressage du noyau est le même pour toutes les applications et les applications qui s'y trouvent. une partie de l’espace d’adressage ne peut pas y accéder directement. L'espace d'adressage du noyau n'est pas le sujet de cet article. Nous nous concentrons sur certains SEGMENTS importants de l'application.
Tableau 1 - Description des segments importants de l'application
Si le système n'active pas la randomisation des adresses (ASLR - Address Space Layout Randomization, randomisation des adresses, qui sera introduite plus tard), Linux utilisera chaque segment dans le tableau ci-dessus L'espace d'adressage est placé à une adresse fixe.
Écrivons un programme réel pour voir comment les adresses de chaque segment sont disposées sur une machine Linux X86_64. Le programme est le suivant, couvrant les segments qui nous intéressent.
Figure 2 - Programme de démonstration de l'espace d'adressage virtuel
Compilé
gcc -o addr_test addr_test.c -static
(La liaison statique est utilisée ici pour démontrer les caractéristiques du code lié à la position)
Nous exécutons ce programme 3 fois et nous constaterons que toutes les adresses sont une valeur fixe. En effet, lorsque la fonction ASLR n'est pas activée, le système n'attribue pas de manière aléatoire l'espace d'adressage virtuel du programme et toutes les adresses du programme sont générées selon des règles fixes.
Figure 3 - Distribution d'adresses de segments fixes
Après le démontage avec la commande objdump, vous pouvez voir que pour l'accès aux variables globales et aux appels de fonction, les adresses des instructions d'assemblage sont fixes. Nous appelons ce code dépend de l'emplacement. .
Figure 4 - Exemple d'instruction d'assemblage de code liée à la position
Ce type de code, parce que l'adresse est codée en dur, ne peut être chargé qu'à l'adresse spécifiée et exécuté une fois que l'adresse de chargement change, en raison de. les variables et fonctions accessibles dans le code L'adresse est fixe et le programme ne peut pas s'exécuter normalement après le changement d'adresse de chargement.
Bien que la méthode d'adresse fixe soit simple, elle ne peut pas implémenter certaines fonctionnalités avancées telles que la prise en charge de bibliothèques dynamiques. Le code de la bibliothèque dynamique sera mappé à l'espace d'adressage virtuel du processus via l'appel système mmap() Dans différents processus, l'adresse virtuelle mappée par la même bibliothèque dynamique est incertaine. Si du code lié à la position est utilisé dans l'implémentation d'une bibliothèque dynamique, l'objectif de s'exécuter à n'importe quelle adresse ne peut pas être atteint. Dans ce cas, nous devons introduire le concept de code PIC indépendant de la position.
De plus, nous pouvons voir que sur les systèmes où la fonction de randomisation des adresses n'est pas activée, puisque les adresses de chaque segment du programme sont fixes, il sera plus facile pour les pirates d'attaquer (les étudiants intéressés peuvent rechercher Ret2shellcode ou Ret2libc attaques), à l'heure actuelle, le concept de PIE doit être introduit avec l'ASLR pour la protection.
2. Implémentation du code indépendant de la position PIC et de la bibliothèque dynamique
Le code indépendant de la position PIC signifie que le code peut être exécuté normalement quelle que soit l'adresse dans laquelle il est chargé. L'ajout de -fPIC à l'option gcc générera du code pertinent.
PIC est utilisé pour générer des bibliothèques partagées indépendantes de la position. Ce que l'on appelle indépendant de la position signifie que les segments de code des bibliothèques partagées sont en lecture seule et stockés dans le segment de code. Plusieurs processus peuvent partager ce segment de code en même temps. temps sans copier. Les variables (variables globales et variables statiques) de la bibliothèque sont accessibles via la table GOT, et les fonctions de la bibliothèque sont accessibles via l'emplacement de fonction PLT->GOT->. Lors de la compilation d'une bibliothèque partagée sous Linux, vous devez ajouter le paramètre -fPIC, sinon il y aura un message d'erreur lors de la liaison (certaines informations disent que cette erreur ne se produit que sur les machines AMD64, mais elle s'est également produite sur ma machine Inter).
Point clé n°1 - Décalage du segment de code et du segment de données
Le décalage entre le segment de code et le segment de données est donné par l'éditeur de liens lors de la liaison, ce qui est très important pour le PIC. Lorsque l'éditeur de liens combine tous les p de chaque fichier objet, l'éditeur de liens connaît parfaitement la taille de chaque p et la position relative entre eux.
Figure 5 - Exemple de décalage de segment de code et de segment de données
Comme le montre la figure ci-dessus, TEXT et DATA sont étroitement proches l'un de l'autre dans l'exemple. En fait, l'éditeur de liens peut connaître le décalage de ces deux segments. Sur la base de ce décalage, le décalage relatif de toute instruction dans le segment TEXT par rapport à l'adresse de départ du segment DATA peut être calculé. Comme le montre la figure ci-dessus, quelle que soit l'adresse virtuelle sur laquelle le segment TEXT est placé, en supposant qu'une instruction mov se trouve au décalage 0xe0 à l'intérieur de TEXT, nous pouvons alors savoir que la position de décalage relatif du segment DATA est : la taille du Segment TEXT - l'instruction mov est au TEXT Décalage interne = 0xXXXXE000 - 0xXXXX00E0 = 0xDF20
Point clé #2- Calcul du décalage relatif des instructions sur X86
Si vous utilisez la position relative pour le traitement, vous pouvez voir que le le code peut être indépendant de la position. Mais sur la plateforme X86, l'instruction mov nécessite une adresse absolue pour la référence des données, alors que devons-nous faire ?
D'après la description dans "Point clé 1", si nous connaissons l'adresse de l'instruction en cours, nous pouvons calculer l'adresse du segment de données. Il n'y a pas d'instruction pour obtenir la valeur IP actuelle du registre de pointeur d'instruction sur la plateforme X86 (RIP est accessible directement sur X64), mais elle peut être obtenue grâce à une petite astuce. Regardons un morceau de pseudo-code :
Figure 6 - Assemblage d'adresses d'instructions d'acquisition de plate-forme X86
Lorsque ce code est réellement exécuté, les choses suivantes se produiront :
Lorsque le CPU exécute l'appel STUB, il enregistrera l'adresse de l'instruction suivante dans la pile, puis passera à l'étiquette STUB pour l'exécution.
L'instruction sur STUB est pop ebx, donc l'adresse de l'instruction "pop ebx" est extraite de la pile dans le registre ebx, obtenant ainsi la valeur du registre IP.
1. Table de décalage globale GOT
Après avoir compris les points précédents, examinons comment les références de données indépendantes de la position sont implémentées sur X86. Cette fonctionnalité se fait via la table de décalage globale (GOT). à réaliser.
GOT est une table enregistrée dans data p, qui enregistre de nombreux champs d'adresse (entrées). Supposons qu'une instruction veuille faire référence à une variable. Elle n'utilise pas directement l'adresse absolue, mais fait référence à une entrée dans le GOT. L'adresse de la table GOT dans les données p est claire et l'entrée du GOT contient l'adresse absolue de la variable.
Figure 7 - La relation entre l'adresse du code et l'entrée de la table GOT
Comme le montre la figure ci-dessus, selon "Key Point 1" et "Key Point 2", nous pouvons d'abord obtenir la valeur du IP actuelle, puis calculez la valeur absolue de l'adresse de la table GOT, puisque le décalage de l'entrée d'adresse de la variable dans la table GOT est également connu, un accès aux données indépendant de la position peut être obtenu.
Prenons comme exemple le pseudocode d'une instruction mov d'adresse absolue (plateforme X86) :
Figure 8 - Exemple d'instruction mov liée à la position
Si vous souhaitez la transformer en code indépendant de la position, vous avez besoin quelques étapes supplémentaires
Figure 9 - Exemple d'instruction mov indépendante de la position combinée avec GOT
Grâce aux étapes ci-dessus, vous pouvez obtenir un accès au code indépendant de l'adresse aux variables. Mais il reste une question : comment la valeur VAR_ADDR stockée dans la table GOT devient-elle l'adresse absolue réelle ?
Supposons qu'il y ait un libtest.so et une variable globale g_var. Après avoir passé readelf -r libtest.so, nous verrons le résultat suivant
Figure 10 - Champ de description de redirection de variable globale du segment rel.dyn
Le chargeur dynamique analysera le segment rel.dyn. Lorsqu'il verra que le type de redirection est R_386_GLOB_DAT, il fera ce qui suit : remplacera la valeur d'adresse réelle du symbole g_var par le décalage 0x1fe4 (c'est-à-dire remplacera la valeur de Sym. Valeur Remplacée par la valeur réelle de l'adresse)
2. Implémentation indépendante de la position des appels de fonction
Théoriquement, l'implémentation PIC de la fonction peut également être indépendante de la position de la même manière que la table GOT de référence de données. Au lieu d'utiliser directement l'adresse de la fonction, l'adresse absolue réelle de la fonction est trouvée en recherchant le GOT. Mais en fait, les caractéristiques PIC de la fonction ne le font pas, et la situation réelle est plus compliquée. Pourquoi ne pas suivre la même méthode que les références de données et examiner d'abord un concept : la liaison différée.
Pour les fonctions de bibliothèque dynamique, l'adresse réelle de la fonction est inconnue avant qu'elle ne soit chargée dans l'espace d'adressage du programme. Le chargeur dynamique gérera ces problèmes et résoudra l'adresse réelle. Ce processus est appelé liaison. L'action de liaison prendra un certain temps car le chargeur doit effectuer des opérations spéciales de recherche et de remplacement de table.
如果动态库有成百上千个函数接口,而实际的进程只用到了其中的几十个接口,如果全部都在加载的时候进行绑定操作,没有意义并且非常耗时。因此提出了延迟绑定的概念,程序只有在使用到对应接口时才实时地绑定接口地址。
因为有了延迟绑定的需求,所以函数的PIC实现和数据访问的PIC有所区别。为了实现延迟绑定,就额外增加了一个间接表PLT(过程链接表)。
PLT搭配GOT实现延迟绑定的过程如下:
第一次调用函数
图11 - 首次调用PIC函数时PLT,GOT关系
首先跳到PLT表对应函数地址PLT[n],然后取出GOT中对应的entry。GOT[n]里保存了实际要跳转的函数的地址,首次执行时此值为PLT[n]的prepare resolver的地址,这里准备了要解析的函数的相关参数,然后到PLT[0]处调用resolver进行解析。
resolver函数会做几件事情:
(1)解析出代码想要调用的func函数的实际地址A
(2)用实际地址A覆盖GOT[n]保存的plt_resolve_addr的值
(3)调用func函数
首次调用后,上图的链接关系会变成下图所示:
图12 - 首次调用PIC函数后PLT,GOT关系
随后的调用函数过程,就不需要再走resolver过程了
三、位置无关可执行程序PIE
PIE,全称Position Independent Executable。2000年早期及以前,PIC用于动态库。对于可执行程序来讲,仍然是使用绝对地址链接,它可以使用动态库,但程序本身的各个segment地址仍然是固定的。随着ASLR的出现,可执行程序运行时各个segment的虚拟地址能够随机分布,这样就让攻击者难以预测程序运行地址,让缓存溢出攻击变得更困难。OS在使能ASLR的时候,会检查可执行程序是否是PIE的可执行程序。gcc选项中添加-fPIE会产生相关代码。
四、Linux ASLR机制和PIE的关系
ASLR的全称为 Address Space Layout Randomization。在Linux 2.6.12 中被引入到 Linux 系统,它将进程的某些虚拟地址进行随机化,增大了入侵者预测目的地址的难度,降低应用程序被攻击成功的风险。
在Linux系统上,ASLR有三个级别
表2 - ASLR级别描述
ASLR的级别通过两种方式配置:
echo level > /proc/sys/kernel/randomize_va_space
或
sysctl -w kernel.randomize_va_space=level
例子:
echo 0 > /proc/sys/kernel/randomize_va_space 关闭地址随机化
或
sysctl -w kernel.randomize_va_space=2 最大级别的地址随机化
我们还是以文章开头的那个程序来说明ASLR在不同级别下时如何表现的,首先在ASLR关闭的情况下,相关地址不变,输出如下:
图13 - ASLR=0时虚拟地址空间分配情况
我们把ASLR级别设置为1,运行两次,看看结果:
图14 - ASLR=1时虚拟地址空间分配情况
可以看到STACK和MMAP的地址发生了变化。堆、数据段、代码段仍然是固定地址。
接下来我们把ASLR级别设置为2,运行两次,看看结果:
图15 - ASLR=2,PIE不启用时虚拟地址空间分配情况
可以看到此时堆的地址也发生了变化,但是我们发现BSS,DATA,TEXT段的地址仍然是固定的,不是说ASLR=2的时候,是完全随机化吗?
这里就引出了PIE和ASLR的关系了。从上面的实验可以看出,如果不对可执行文件做一些特殊处理,ASLR即使在设置为完全随机化的时候,也仅能对STACK,HEAP,MMAP等运行时才分配的地址空间进行随机化,而可执行文件本身的BSS,DATA,TEXT等没有办法随机化。结合文章前面讲到的PIE相关知识,我们也很容易理解这一点,因为编译和链接过程中,如果没有PIE的选项,生成的可执行文件里都是位置相关的代码。如果OS不管这一点,ASLR=2时也将BSS,DATA,TEXT等随意排布,可想而知程序根本不能正常运行起来。
明白了原因,我们在编译时加入PIE选项,然后在ASLR=2时重新运行一下看看结果如何
图16 - ASLR=2,PIE启用时虚拟地址空间分配情况
Vous pouvez voir qu'avec PIE activé et ASLR=2, les adresses virtuelles de chaque segment peuvent être complètement aléatoires.
Recommandations associées : "Tutoriel vidéo Linux"
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!