==Phrack Inc.==
Volume 0x0b, Issue 0x3e, Phile #0x05 of 0x10
|=-----------------------------------------------------------------------=|
|=-----=[ Bypassing 3rd Party Windows Buffer Overflow Protection ]=------=|
|=-----------------------------------------------------------------------=|
|=--------------=[ anonymous <p62_wbo_a@author.phrack.org ]=-------------=|
|=--------------=[ Jamie Butler <james.butler@hbgary.com> ]=-------------=|
|=--------------=[ anonymous <p62_wbo_b@author.phrack.org ]=-------------=|
|=-----------------------------------------------------------------------=|
|=------------=[ Traduit par TboWan pour arsouyes.org ]=-----------------=|
--[ Sommaire
1 - Introduction
2 - Retour sur trace dans la pile
3 - S'évader des détournements noyaux
3.1 - Retour sur trace dans la pile noyau
3.2 - Falsifier un cadre de pile
4 - S'évader des détournement en mode utilisateur
4.1 - Problème d'implémentation - Détournement de l'API incomplete
4.1.2 - Ne pas détourner assez profondément
4.1.3 - Ne pas détourner assez complètement
4.2.1 - Saut vers la table de Patch
4.2.2 - Hook Hopping
4.3 - Repatching de l'API Win32
4.4 - Attaquer les composants du mode utilisateur
4.4.1 - Patching de l'IAT
4.4.2 - Patching de la section de données
4.5 - Appeller les syscalls directement
4.6 - Falsifier les cadres de pile
5 - Conclusions
--[ 1 - Introduction
Récement, de nombreux systèmes de sécurités commercieux ont commencé à
offrir des protections contre les buffer overflow [NDT : débordement de
tampons]. Ce papier analyse les prétentions de ces protections et décrit
quelques techniques pour les contourner.
Les systèmes commerciaux existants implémente de nombreuses techniques
pour se protéger des buffer overflows. Actuellement, le retour sur trace
dans la pile [NDT : stack backtracking] est la plus populaire. Elle est
aussi la plus facile à implémenter et la plus facile à contourner.
Quelques produits commerciaux comme Entercept (maintenant NAI Entercept)
et Okena (maintenant Cisco Security Agent) implémentent cette technique.
--[ 2 - Retour sur trace dans la pile
La pluspart des systèmes de sécurité commerciaux existants ne protègent en
fait pas contre les buffer overflow mais essaye plutôt de détecter
l'exécution de shellcode.
La technologie la plus commune pour détecter les shellcode est la
vérification de la permission de la page du code qui implique de vérifier
si le code s'exécute sur une page inscriptible de la mémoire. C'est
nécessaire puisque des architectures comme x86 ne supportent pas de bits
de mémoire non-exécutable.
Certains systèmes effectuent des vérifications additionnelles pour savoir
si la page du code en mémoire appartient à une page correspondante à une
section de fichier et pas à une section de mémoire anonyme.
[-----------------------------------------------------------]
page = get_page_from_addr( code_addr );
if (page->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 ]=---------------------------------------------------------------=|