==Phrack Inc.==
Volume 0x0b, Issue 0x3c, Phile #0x09 of 0x10
|=-------------------=[ Big Loop Integer Protection]=------------------=|
|=----------------------------------------------------------------------------=|
|=-----------=[ Oded Horovitz ohorovitz@entercept.com ]=----------=|
--[ Table des matieres
1 - Introduction
2 - Partie I - Problemes de nombres entiers
2.1 - Introduction
2.2 Exemples de codes basiques
3 - Partie II - Modele d'exploitation
3.1 - Une entree, deux interpretations
3.2 - Quelle est la nature de l'entree ?
3.3 - Detection suggeree
4 - Partie III - Implementation
4.1 - Introduction
4.2 - Pourquoi gcc ?
4.3 - Quelques mots a propos de gcc
4.3.1 - Flux de compilation
4.3.2 - Le AST
4.3.3 - Demarrons
4.4 - Buts du patch
4.5 - Passage en revue du patch
4.5.1 - Tactique
4.5.2 - Modifions le AST
4.6 - Limites
5 - References
6 - Remerciements
7 - Annexe A - Des exemples dans la vraie vie
7.1 - Encodage de Apache Chunked
7.2 - Authentification OpenSSH
8 - Annexe B - Utilisons blip
--[ 1 - Introduction
Les failes de debordement de nombres entiers et de signe de nombres entiers
sont aujourd'hui connues de tous/toutes. Ceci a mene a une exploitation
avancee des failles du meme genre. L'article tentera de suggerer un moyen
de detecter ces failles en ajoutant un support au compilateur qui detecte
et signale les exploitations de failles integer. Un patch pour gcc est
specialement presente pour demontrer la faisabilite de cette technique.
L'article est divise en trois parties. La premiere contient une breve
introduction a plusieurs des failles communes basees sur des integers. Nous
listons plusieurs des failles publiques recentes. La deuxieme partie de
l'article tente d'expliquer la cause premiere du probleme des failles
integer. En utilisant de vrais exemples, l'article explique en
premier lieu pourquoi l'exploitation est possible, et comment il serait
possible de detecter l'exploitation de failles intger, meme quand la faille
n'est pas connue d'avance. La troisieme partie passe en revue
l'implementation du plan de detection suggere. Comme la forme de cette
implementation de ce plan de detection suggere est un patch de gcc, des
informations d'introduction a propos du fonctionnement interne de gcc sont
donnees. Nous resummons l'article en demontrant la protection en action
contre OpenSSH et les paquetages htppd de Apache
--[ 2 - Partie I - Problemes de nombres entiers
----[ 2.1 - Introduction
Au cours de l'annee derniere l'attention semble s'etre tournee vers une
nouvelle mauvaise pratique de programmation. Cette pratique a a voir avec
la possibilite de deborder des nombres entiers ce qui cause des
debordements de tampons. Il se trouve que les tres populaires et
(soit-disant) tres securises paquetages de logiciels (OpenSSH, Apache, *BSD
kernel) partagent cette faille. La cause premiere de cette mauvaise
pratique est l'insuffisante verification d'entree pour les entrees de type
integer. L'entree de nombres entiers semble tellement naive, seulement
longue de quelques bits. Qu'est ce qui peut aller mal ici ? Et bien, il
semble que beaucoup de choses peuvent aller mal. Ce qui suit est un tableau
des failles liees a des nombres entiers prises des listes de securite de
OpenBSD et FreeBSD. Toutes les failles ont ete rapportees pendant l'annee
2002.
--------------------------------------------------------------------------
| Paquetage vulnerable | Courte description de la faille |
--------------------------------------------------------------------------
| OpenBSD select syscall | Le test de la limite positive contre les |
| (See reference [4]) | valeurs entieres permet un debordement de pile|
--------------------------------------------------------------------------
| RPC xdr_array | Un integer overflow cause une allocation de |
| | petit tampon qui est overflow-e par l'entree |
--------------------------------------------------------------------------
| OpenSSH Authentication | Un integer overflow cause une allocation de |
| | petit tampon qui est overflow-e par l'entree |
--------------------------------------------------------------------------
| Apache chunked-encoding| La condition positive faite sur les entiers |
| | signes permet un heap overflow |
--------------------------------------------------------------------------
| | La condition positive faite sur les entiers |
| FreeBSD get_pallette | signes permet une fuite d'informations |
| | de l'espace noyau vers l'espace utilisateur |
--------------------------------------------------------------------------
| FreeBSD accept1,getsoc-| La condition positive faite sur les entiers |
| kname1,getpeername1 | signes permet une fuite d'informations |
| | de l'espace noyau vers l'espace utilisateur |
--------------------------------------------------------------------------
Table 1 - Exemple de failles integer dans l'annee 2002.
Le probleme commun qui existe dans toutes les failles ci-dessus est qu'une
entree de type integer (signe ou non-signe) etait utilisee pour declencher
l'overflow (en ecriture) ou une fuite d'information (en lecture) vers/en
provenance des tampons du programme. Toutes les failles ci-dessus auraient
ete evitees si des limites propres avaient ete mises en vigueur.
Les failles integer peuvent etre illustree plus en avant en regardant les
deux simples exemples de code suivants :
Example 1 (int overflow):
-------------------------
01 int main(int argc,char* argv[]){
02 unsigned int len,i;
03 char *buf;
04 if(argc != 3) return -1;
05 len=atoi(argv[1]);
06 buf = (char*)malloc(len+1);
07 if(!buf){
08 printf("Allocation echouee\n");
09 return -1;
10 }
11 for(i=0; i < len; i++){
12 buf[i] = _toupper(argv[2][i]);
13 }
14 buf[i]=0;
15 printf("%s\n",buf);
16 }
Le code ci-dessus semble tout a fait regulier. Le programe convertit
une chaine de caracteres en sa representation en majuscules. D'abord
il alloue assez d'espace pour la chaine et le caractere de
terminaison NULL. Ensuite il convertit chaque caractere en sa valeur
en majuscule. Mais quand on y regarde d'un peu plus pres, nous pouvons
identifier deux problemes majeurs dans le code. D'une part le programe
fait confiance a l'utilisateur pour avoir autant de caracteres qu'il
le specifie (ce qui n'est evidemment pas le ca) (ligne 5). D'autre
part le programe ne tient pas compte du fait qu'en calculant l'espace
a allouer, un integer overflow pourrait se produire (ligne 6). En
essayant de generaliser le probleme, le premier bug peut permettre a
l'attaquant de lire l'information qu'il ne fournit pas (en faisant
confiance a l'entree de l'utilisateur et en lisant les caracteres
*len* en provenance de argv[2]). Le second bug permet a l'attaquant de
faire un heap overflow avec ses propres donnees, et par consequent de
compromettre totalement le programe.
Example 2 (outrepassage des tests de signe ):
------------------------------
01 #define BUF_SIZE 10
02 int max = BUF_SIZE;
03 int main(int argc,char* argv[]){
04 int len;
05 char buf[BUF_SIZE];
06 if(argc != 3) return -1;
07 len=atoi(argv[1]);
08 if(len < max){
09 memcpy(buf,argv[2],len);
10 printf("Donnees copiees\n");
11 }
12 else
13 printf("Trop de donnees\n");
14 }
Le second exemple montre un programe qui a l'intention de resoudre le
probleme presente dans le premier exemple, en tentant de mettre en
vigueur une longueur d'entree utilisateur a une valeur maximale connue
et predefinie. Le probleme dans ce code est que len est definit comme
un entier signe. Dans ce cas uns tres grosse valeur (non-signee bien
entendu) est interpretee comme une valeur negative (ligne 8), qui
outrepassera le est de limite. Encore, a la ligne 9 la meme valeur est
interpretee comme un nombre positif non-signe causant uun debordement
de tampon et permettant une possible compromission totale.
--[ 3 - Partie II - Modele d'exploitation
----[ 3.1 - Une entree, deux interpretations
Alors quel est le vrai probleme ? Comment de tels packetages orientes
securite ont ces failles ? La reponse est que les entrees d'integer
ont parfois une interpretation ambigue a differantes parties du code
(les entiers peut changer de signe a differantes valeurs, type cast
implicite, integer overflows). Cette interpretation ambigue est
difficile a remarquer en implementant du code de validation d'entree.
Pour expliquer cette ambiguite laissez-nous regarder le premier
exemple. Au moment de l'allocation (ligne 6), le code croit que comme
l'entree est un nombre, alors ajouter un autre nombre produira un gros
nombre (len+1). Mais comme le langage C habituel ignore les les
integer overflows que la nombre particulier 0xffffffff ne s'applique
pas a cet essai et produira un reultat innattendu. Malheureusement, la
meme erreur nest *PAS* repetee plus tars dans le code. Par consequent
la meme entree 0xffffffff interpretee cette fois comme une valeur
non-signee (un enorme nombre positif).
Dans le second exemple l'ambiguite de l'entree est encore plus
evidente. Le code inclut un casting de type silencieux genere par le
compilateur quand il appelle memcpy. Par consequent le code teste la
valeur de l'entree comme si c'etait un nombre signe (ligne 8) pendant
qu'il l'utilise pour copier des donnees comme si c'etait non-signe
(ligne 9).
Cette ambiguite est invisible pour l'oeil du coder, et peut ne pas
etre detecte, lassant le code vulnerable a cette attaque "furtive".
----[ 3.2 - Quelle est la nature de l'entree?
En regardant en arriere les exemples ci-dessus revelent unn sens
commun pour l'entree de l'attaquant. (Desole si les quelques lignes a
venir expliquent l'evidence :>)
L'entree du dessus est un nombre pour une raison. C'est un compteur !
Cela compte des items ! Pas d'importance ce que sont ces "items"
(octets, caracteres, objets, fichiers, etc.). Ce sont de tranquilles
quantites comptables d'items. Et que pouvez-vous faire avec un
compteur ? Eh bien, vous etes probablement en train de faire quelques
comptes. En remarque je dirais que *tout* nombre n'est pas aussi un
compteur. Il y a beaucoup d'autres raisons d'avoir des nombres
autour. Mais le seul qui ait quelque chose a voir avec les failles
integer sont la plupart du temps des "compteurs".
Par exemple, si le compte est pour deviner les reponses vous voudriez
peut-etre lire "compte" la somme des reponses (OpenSSH). Ou si le
compte est une longueur de tampon vous voudriez peut-etre copiez
"compte" la somme d'octets de d'un endroit de la memoire vers l'autre
(Apache httpd).
La ligne du bas est que quelque part derriere ce nombre il y a la
"boucle" propre dans le code qui fera des processus, et elle les fera
"compte" fois. Cette "boucle" pourrait avoir de multiples formes comme
la boucle for dans le premier exemple, ou comme une boucle implicite
dans memcpy. Toutes ces sortes de boucles se terminerons apres le
"compte".
----[ 3.3 - Detection suggeree
OK, qu'avons-nous a faire a propos de ces failles ?
- L'entree etait utilisee de maniere ambigue dans le code.
- Quelque par dans le code il y a une boucle qui utilise l'entree de
nombre entier comme un compteur d'iteration.
Pour l'interpretation du nombre ambigu, l'attaquant doit envoyer un
enorme nombre. En regardant le premier exemple nous voyons que pour
faire que le nombre soit ambigu l'attaquant avait besoin d'envoyer un
tel nombre que si on fait (len+1) le nombre va deborder. Pour que cela
arrive l'attaquant devra envoyer la valeur 0xffffffff. En regardant le
second exemple, pour l'interpretation du nombre ambigu, l'attaquant a
besoin d'envoyer un tel nombre qu'il va tomber dans le rang negative
d'un entier 0x80000000-0xffffffff.
Le meme nombre enorme envoye par l'attaquant pour declencher la faille
est plus tard utilise dans une boucle comme le compteur d'iterations
(comme decrit dans la section "Quelle est la nature de l'entree ?").
A present analysons le processus de l'exploit :
1. L'attaquant veut deborder un tampon.
2. L'attaquant pourrait utiliser une faille integer.
3. L'attaquant envoie un enorme entier pour declencher la faille.
4. Le compte de la boucle s'execute (probablement) en utilisant
l'entree de l'attaquant comme borne de la boucle.
5. Un tampon est deborde (aux premieres iterations de la boucle !)
Par consequent detecter (et prevenir) l'exploitation d'une faille
integer est possible en validant les bornes de la boucle avant son
execution. La validation de la boucle testera si la limite de boucle
n'est pas au dessus d'un seuil predefini, et si la limite est pus
haute que le seuil un handler special sera declenche pour s'occuper de
la possible exploitation.
Comme la valeur requise pour declencher la plupart les failles
integer est enorme, nous pouvons supposer (esperer) que la plupart des
boucles legitimes ne declencherons pas cette protection.
Pour avoir une idee de quelles valeurs nous nous attendons a voir dans
les failles integer, examinons les exemples suivants :
- Allocation d'un tampon pour les donnees de l'utilisateur et les
donnees du programme
Ca ressemble a : buf = malloc(len + sizeof(header));
Dans ce cas la valeur requise pour declencher un integer verflow est
tres pres de 0xffffffff car les tailles des structures de la plupart
des programmes sont de l'ordre de plusieurs octets a des centaines
d'octets au plus.
- Allocation de tableau
Ca ressemble a : buf = malloc(len * sizeof(object));
Dans ce cas la valeur requise pour declencher l'overflow pourrait etre
bien plus petite que dans le premier exemple mais c'est toujours une
valeur relativement grande. Par exemple si sizeof(object) == 4 alors
la valeur devrait etre plus grande que 0x40000000 (un giga). De meme
si sizeof(object)== 64 alors la valeur devrait etre plus grande que
0x4000000 (64 megas) pour causer un overflow.
- Tomber dans l'ordre negatif
Dans ce cas la valeur requise pour faire un nombre negatif est
n'importe quel nombre plus grand que 0x7fffffff.
En regardant les valeurs requises pour declencher la faille integer,
nous pouvons choisir un seuil comme 0x40000000 (un giga) qui va
prendre en main la plupart des cas. Ou nous pouvons choisir un seuil
plus petit pour une meilleure protection, qui pourrait declencher des
faux-positifs.
--[ 4 - Partie III - Implementation
----[ 4.1 - Introduction
Une fois que nous avons un moyen de detecter les attaques integer, ce
sera joli d'implementer un systeme base sur cette idee. Un candidat
possible l'implementation de se systeme est d'etendre un compilateur
existant. Comme le compilateur est au courant de toutes les boucles
dans l'application, il sera possible pour le compilateur d'ajouter les
tests de securite appropries avant tout "comptage de boucle". Faire
comme ceci securisera l'application sans aucune connaissance de la
faille specifique.
Par consequent j'ai choisi d'implementer ce systeme comme un patch
pour gcc et de le nommer "Big Loop Integer Protection" alias
blip. Utiliser le flag --fblip pourrait etre capable de proteger cette
application du prochain integer exploit a etre public.
----[ 4.2 - Pourquoi gcc ?
Coisir gcc n'etait pas une decision difficile. D'abord ce compilateur
est l'un des compilateurs les plus communs dans le monde GNU/Linux et
*nix. Par consequent, patcher gcc permettra de proteger toutes les
applications compilees avec gcc. Deuxiemement, gcc est open-source et
donc il serait faisable d'y implementer ce patch en premier
lieu. Troisiemement, de precedants patchs de securite furent
implementes comme des patches pour gcc (Stackguard, ProPolice). Donc
pourquoi pas suivre leur sagesse ?
---- [ 4.2 - Quelques mots a propos de gcc
Eh bien..., tout joyeux je pose que je suis sur le point de faire un
patch pour gcc pour eviter les attaques integer. Mais, sauf cela, que
sais-je reellement a propos de gcc ? Je dois admettre que la reponse a
cette question etait - "pas grand chose".
Pour depasser ce petit probleme, j'ai cherche de la documentation a
propos du fonctionnement interne de gcc. J'ai egalement espere trouver
quelque chose de similaire a ce que je voulais faire, qui existait
deja. Assez rapidement, il devint clair qu'avant de sauter sur
d'autres exemples, je devait comprendre la bete gcc.
.. Deux semaines plus tard, j'avais lu suffisamment de la
documentation interne de gcc et j'avais passe suffisamment de temps en
sessiosn de debugging du compilteur pour etre capable de commencer a
modifier le code. Malgre tout avant de commencer a aller dans les
details je voudrais fournir quelques generalites sur comment
fonctionne gcc, que j'espere le lecteur / la lectrice trouvera utile.
------[ 4.3.1 - Flux de compilation.
Le compilateur gcc est reellement une machine amusante. Les intentions
de gcc incluent la capacite a supporter de multiples langages de
programmation, qui peuvent plus tard etre compiles sur de multiples
plates-formes et sets d'instructions. Pour atteindre un tel but, le
compilateur utilise plusieurs niveaux d'abstraction./
En premier lieu, un fichier de langage est traite (parse) par un
"Front End" de langage. A chaque fois que vous invoquez le compilateur
gcc, il decidera lequel des "Front End" disponibles est bon pour
parser les fichier entree, et executera ce "Front End". Le "Front
End" va parser le fichier entree en entier et le convertira (en
utilisant des fonctions globales assistantes) en un "Arbre
d'abstraction de syntaxe" (AST [ : "Abstract Syntax Tree"]). En
faisant comme cela le compilateur fait que le langage de programmation
d'origine devient transparant pour le le "Back End" de gcc. Le AST,
comme son nom l'indique, est une structure de donnees, qui reside en
memoire et qui peut representer toutes les fonctionalites de tous les
langages de programmation que gcc supporte.
A chaque fois que le "Front End" finit de parser une fonction
complete, et la convertit en sa representation AST, une fonction de
gcc nommee rest_of_compilation est appellee. Cette fonction prend le
AST sorti du parser et "l'etend" en un "Langage de Transfer de
Registre" (["Register Transfer Language"] : RTL). Le RTL, qui est la
version "etendue" du AST, est ensuite traite encore et encore au
travers de differantes phases de compilation.
[NDT : et oui, RTL, comme quoi gcc, c'est pour les grosses tetes :]
Pour avoir une idee de ce qui est fait sur l'arbre RTL, voici une
liste des differantes phases :
- Obtimisation des jump
- CSE (Common sub-expression elimination : Elimination des
sous-expression communes)
- Analyse du flux de donnees
- Combinaison des instructions
- Plan d'execution des intructions
- Re-ordonnancement des blocs de base
- Raccourcissement des branches
- Finale (generation du code)
J'ai choisi seulement quelques phases parmi la longue liste des phases
pour demontrer le travail effectue sur le RTL. La liste complete est
nettement plus longue et peut etre trouvee dans les docs sur le
fonctionnement interne de gcc (voir la sections "Demarrons" pour les
liens vers les docs). Le truc sympa a propos du RTL est que toutes ces
phases sont effectuees independamment de la machine cible.
La derniere phase qui est effectuee sur l'arbre RTL sera la phase
"finale". A ce point la representation est prete a etre substituee a
d'actuelles instructions assembleur qui ont a voir avec l'architecture
specifique. Cette phase est possible grace au fait que gcc maintient
une definition d'abstraction des "modes machine". Un set de fichier
qui peut decrire chaque hardware des machines supportees et un set
d'intruction dans un sens qui rend possible de traduire le RTL en code
approprie a la machine.
------[ 4.3.3 - Le AST
Je me focaliserai sur le AST, auquel je vais me referer comme
"l'ARBRE". Cet ARBRE est la sortie du parsing du front end d'un
fichier langage. L'ARBRE contient toute l'information existant dans le
fichier source qui est requise pour la generation du code
(i.e. declarations, fonctions, types,...). En plus le TREE inclut
egalement plusieurs des attributs et des transformations implicites
que le compilateur pourrait choisir d'effectuer (i.e. conversion de
type, variables auto,...).
Comprendre l'ARBRE est critique pour creer ce patch. Par chance,
l'ARBRE est bien structure est meme si sa
programmation-comme-orientee-objet-en-utilisant-le-C [NDT : y'en a
j'vous jure ...] est accablante au debut, apres un peu de sessions de
debugging, tout commence a se mettre en place.
Le noyau de structure de donnee de l'ARBRE est le tree_node (defini
dans tree.h). Cette structure est actuellement une grosse union qui
peut representer n'importe quel morceau d'information. La maniere dont
cela fonctionne est que tout noeud de l'arbre a son code, qui est
accessible en faisant "TREE_CODE (tree node)". En utilisant ce code le
compilateur peut savoir lesquels des champs de l'union sont appropries
pour ce noeud (i.e. Un nombre constant aura le
TREE_CODE() == INTEGER_CST, par consequent le node->int_cst est sur le
point d'etre le membre de l'union qui aura l'information valide.). En
remarque, je dirai qu'il n'y a aucun besoin d'acceder a n'importe quel
champ de structure de noeud de l'arbre directement. Pour chaque champ
dans cette structure il y a une macro dediee qui uniformise l'acces a
ce champ. Dans la plupart des cas cette macro contiendra des
verifications additionelles du noeud, et peut-etre meme de la logique
a executer a chaque fois qu'un acces a ce champ est effectue
(i.e. DECL_RTL qui est responsable d'extraire la representation RTL
d'un noeud d'un arbre, appellera make_decl() si aucune expression RTL
n'existe poiur ce noeud).
Donc nous savons a propos de l'ARBRE et des noeuds, et nous savons que
chaque noeud peut representer des choses differantes, qu'est-t-il
important de connaitre a propos des noeuds d'un arbre ? Eh bien, une
chose est la maniere dont les noeuds d'un arbre sont lies a tous les
autres. Je vais tenter de donner quelques exemples de scenarios qui
representent la plupart des cas ou un noeud est relie a un autre.
Reference I : Chaines :
Une chaine est une relation qui peut etre mieux decrite comme une
liste. Lorsque le compilateur a besoin de maintenir une liste de
noeuds *qui n'ont aucune information a propos des liens*, il utilisera
simplement le champ chaine du noeud (accessible par la macro
TREE_CHAIN() ). Un exemple pour un tel cas est la liste des
declarations de noeuds dans le corps d'une fonction. Pouir chaque
declaration dans une liste CMOPOUND_STMT il y a une declaration de
chaine qui represente la declaration suivante dans le code.
Reference II : Listes :
A chaque fois qu'une chaine simple n'est pas suffisante, le
compilateur utilisera un noeud special de TREE_LIST. TREE_LIST permet
au compilateur de sauvegarder des informations attachees a chaque
objet dans la liste. Pour ce faire chaque objet dans la liste est
represente par trois noeuds. Le premier noeud aura le code TREE_LIST.
Ce noeud aura le pointeur TREE_CHAIN pointant sur le prochain noeud
de la liste. Il aura le pointeur TREE_VALUE pointant sur l'objet du
noeud actuel, et il aura egalement le pointeur TREE_PURPOSE qui
pourrait pointer sur un autre noeud qui tient des informations
supplementaires a propos de la signification de cet objet dans la
liste. Par exemple le noeud du code CALL_EXPR aura un TREE_LIST comme
seconde operande. Cette liste representera les parametres envoyes a la
fonction appelee.
Reference III : Reference directe :
Plusieurs des champs du noeud d'un arbre sont eux meme des noeuds d'un
arbre. Cela pourrait etre sujet a confusion au premier coup d'oeuil,
mais ce sera clair assez prochainement. Quelques exemples communs
sont :
- TREE_TYPE ce champ represent le type d'un noeud. Par exemple chaque
noeud avec une expression de code doit avoir un type.
- DECL_NAME a chaque fois que des noeud de declaration ont un nom, il
n'existera pas en tant que chaine de caractere pointee directement par
le noeud de declaration. Au lieu d'utiliser DECL_NAME on peut obtenir
l'acces a un autre noeud de code IDENTIFIER_NODE. Le second aura
l'information demandee.
- TREE_OPERAND() L'une des commandes les plus utilisees. A chaque
fois qu'il y a un noeud qui a un nombre defini de noeud "fils", le
tableau TREE_OPERAND sera utilise (i.e. le noeud de code IF_STMT
aura TREE_OPERAND(t,0) comme un noeud COND_EXPR, TREE_OPERAND(t,1)
comme un noeud de declaration THEN_CLAUSE, et TREE_OPERAND(t,2)
comme noeud de declaration ELSE_CLAUSE.)
Reference IV - Vecteurs :
La derniere et la moins commune est le noeud vecteur. Ce conteneur,
qui est accessible en utilisant les macros TREE_VEC_XXX, est utilise
pour maintenir des vecteurs de tailles qui peuvent varier.
Il y a beaucoup plus a savoir a propos des noeuds de l'arbre AST pour
lesquels les documents internes de gcc auront peut-etre de meilleures
et plus completes explications. Je vais donc stopper ici ma revue de
AST en suggerant de lire les docs.
En addition au conte des codes d'abstraction dans le AST. Il y a
plusieurs structures globales, qui sont tres utilisees par le
compilateur. Je tenterai d'en nommer quelques-unes de ces structures
globales que je trouve tres utiles a verifier au cours des sessions de
debugging.
- current_stmt_tree : fournit le dernier stmt ajoute a l'arbre, le
type de la derniere expression, et le nom de fichier de l'expression.
- current/global_binding_level : fournit des informations de lien,
comme les noms definis dans un niveau de lien particulier, et les
pointeurs de blocs.
- lineno : variable contenant le numero de la ligne qui est parsee a
ce moment
- input_filename : nom du fichier qui est parse a ce moment
------[ 4.3.3 - Demarrons
Si vous voulez experimenter le AST par vous-meme, ou creuser dans les
details du patch, il est recommende de lire cette sections. Vous
n'aurez aucun probleme a passer a la section suivante si vous ne
voulez pas faire ceci.
La premiere de toutes les choses, prenez le code source du
compilateur. La version que j'ai utilise comme base pour ce patch est
gcc 3.2. Pour de l'information a propos du telechargement et de la
construction du compilateur regardez s'il vous plait
http://gcc.gnu.org/install/ .
(Rappellez-vous s'il vous plait de specifier la version du compialteur
que vous voulez telecharger. La version par defaut pourrait etre la
derniere sortie, qui n'a pas ete teste avec ce patch).
La chose suivante que vous voudriez peut-etre faire est de vous
asseoir et de lire attentivement les documentations internes de
gcc. (Pour la cause de ce patch, vous devriez vous devriez etre
familier avec la section 9 de ce document.) Le document est situe a
http://gcc.gnu.org/onlinedocs/gccint/
Supposant que vous lisez le document et que vous voulez allez au
niveau suivant, je recommande d'avoir un set de programmes simple pour
etre utilise comme fichier de langage du compilateur, le debugger de
votre choix, et de commencer a debugger le compilateur. Quelques bons
points d'arret que vous pourriez trouver utiles :
- add_stmt : appele a chaque fois que le parse decide d'ajouter une
nouvelle declaration dans le AST. Ce point pourrait etre tres pratique
quand le comment un noeud specifique est cree n'est pas aussi
clair. En breakant sur add_stmt et en verifiant la pile d'appel, il
est facile de trouver de plus interessants endroits ou creuser.
- rest_of_compiliation : appele a chaque fois qu'une fonction est
completement convertie en une representation AST. Si vous etes
interesse a regarder comment le AST devient RTL c'est un bon endroit
pour debuter.
- expand_stmt : appele a chaque fois qu'une declaration est sur le
point d'etre etendue en code RTL. Mette un point d'arret ici vous
permettra d'investiguer facilement dans la structure d'un noeud AST
sans le besoin d'aller au travers de niveaux niches interminables.
Comme le compilateur gcc arretera d'appeler le compilateur cc1
pour les fichiers *.c, vous voudriez peut-etre debugger cc1 dans un
premier temps, et sauver vous-meme le trouble de faire suivre a votre
debugger le processus fils de gcc.
Assez rapidement vous aurez besoin de quelques references pour toutes
les petites macros utilisee par la construction de l'arbre AST. Pour
cela je recommende de se familiariser avec les fichiers suivants :
gcc3.2/gcc/gcc/tree.h
gcc3.2/gcc/gcc/tree.def
----[ 4.4 - Buts du patch
Comme pour chaque projet dans votre vie, vous devez definir les buts
du projets. D'abord vous saurez mieux si vous avez atteint vos
buts. Deuxiemement, qui n'est pas moins important, comme les
ressources sont limitees, il est plus simple de vous proteger d'un
projet sans fin.
Les buts de ce patch etaient avant tout d'etre une preuve de concept
pour l'expose suggere de prevention des exploits integer. Ce n'est par
consequent *pas un but de resoudre tous les problemes actuels et
futurs dans le monde de la securite, ou meme de resoudre tous les
exploits qui sont lies a une entree d'integer.
Le second but de cette implementation est de garder le patch
simple. Comme le patch n'est seulement qu'une preuve de concept, nous
avons prefere de conserver les choses simples et d'eviter les
solutions fantaisistes si elles requierent du code plus complexe.
Last but not least, le troisieme but est de rendre ce patch
utilisable. Cela signifie simple a utiliser, intuitif et capable de
proteger les paquetages du monde reel plus gros que 30 lignes de code
:) .
----[ 4.5 - Passage en revue du patch
Le patch introduira un nouveau flag au compilateur gcc nomme
"blip". En compilant un fichier en utilisant le flag -fblip, le
compilateur genere du code qui verifira les conditions "blip" pour
toutes les bocles for/while et pour tous les appels a des fonctions
pareilles a des boucles.
Une fonction pareille a une boucle est n'importe quelle fonction qui
est un synonyme pour une boucle (i.e. mecpy, bcopy, memset, etc.).
La verification generee evaluera si une boucle est sur le point
de s'executer un "enorme" nombre de fois (determine par
LIBP_MAX). A chaque fois qu'une boucle est sur le point de s'executer,
le code genere verifie que la limite de la boucle est plus petite que
le seuil. Si une tentative d'execution d'une boucle superieure a la
valeur du seuil est identifiee, le handler __blip_violation() sera
appele au lieu de la boucle, menant a une fin controlee du processus.
La version actuelle du patch ne supportera que le langage C. Cette
decision a ete prise pour garder cette premiere version du patch
petite et simple. Egalement, tous les paquetages vulnerables que ce patch
projetait de proteger sont ecrits en C. J'ai donc pense que n'avoir
que le C est un bon debut.
------[ 4.5.1 - Tactique
Ayant en tete les buts ci-dessus, j'ai du prendre des decisions durant
le developpement de ce patch. L'un des problemes que j'avais etait de
choisir le bon endroit ou tailler le code. Il y a beaucoup d'options
disponibles, et je tenterai de donner des pour et des contres pour
chaque option, en esperant que cela aidera d'autres a prendre des
decisions reflechies lorsqu'ils / elles rencontrerons les memes
dilemmes.
La premiere chose que je devais decider etait la representation du
programme que je voulais modifier. Le processus de compilation
ressemble plus ou moins a ceci :
Processus Representation du programme
------------- -----------------------------
Programmation => 1. Code source
Parsing => 2. AST
Etendement => 3. RTL
"final" => 4. Fichier objet
Alors quelle est la bonne place ou implementer les verifications ?
Le tableau suivant liste quelques pour et contre pour la modification
du code aux differantes etapes durat le processus de compilation.
+----------+----------------------------+---------------------------+
|Etape |Pour | Contre |
+----------+----------------------------+---------------------------+
| AST |- Independant de la cile |- Pas d'acces aux registres|
| |- Independent du langage | ni aux instructions |
| |- Independant des | materiels |
| | optimisations | |
| |- Acces haut niveau au | |
| | langage "source" | |
| |- Ajout du code intuituif | |
+----------+----------------------------+---------------------------+
| RTL |- Independant de la cible |- Acces de bas niveau a |
| |- Independant du langage | la source |
| |- Plein acces au materiel |- Pourrait interferer avec |
| | de la cible | les optimisations |
+----------+----------------------------+---------------------------+
| Fichier |- Independant du langage |- Dependent du materiel |
| objet | |- Manque d'information |
| | | de syntaxe |
| | |- La modification du flux |
| | | pourrait casser la logique|
| | | du compilateur |
+----------+----------------------------+---------------------------+
Apres quelques reflexions j'ai decide de modifier la representation
AST. Cela semble etre l'endroit le plus naturel pour faire un tel
changement. Premierement le patch n'a pas vraiment besoin d'acceder a
des informations de bas niveau comme les registres materiels, ou meme
les allocations de registres virtuels. Deuxiemement le patch peut
facilement modifier le ASt pour injecter dedans de la logique sur
mesure, alors que faire la meme chose au niveau du RTL requiererait
des changements majeurs, qui blesserons les niveaux d'abstraction
definis dans gcc.
Resoudre mon second dilemme ne fut pas aussi simple que le premier. A
present que le patching du AST etait le plan que j'avais en tete,
j'avais besoin de trouver le meilleur point dans le temps ou
j'examinerai l'arbre AST existant, et emettre mes tests
dessus. J'avais trois options possibles.
1) Ajouter un appel a ma fonction depuis le code du parser de quelques
langages (qui passent pour etre le C). En faisant cela, j'ai la chance
d'evaluer et de modifier l'arbre "a la volee" et par consequent sauver
un passage supplementaire sur l'arbre plus tard. Un desavantage clair
est que le patch devient dependant du langage.
2) Attendre jusqu'a ce que la fonction entiere soit parsee par le
front-end. Apres aller a travers l'arbre cree, avant de le convertir
en RTL et trouiver les endroit qui requierent un test et les patcher.
Un avantage de cette methode est que le patch n'est plus dependant du
langage. D'un autre cote, implementer un "parcours d'arbre" qui
scannera un arbre donne est une tache trescomplexe et source d'erreur,
qui ira contre les buts que nous avons definits au-dessus comme un
patch simple et utile.
3) Patcher l'arbre AST *pendant* qu'il est convertit en RTL. Malgre
que cette option semble etre la plus avantageuse (independante du
langage, pas besoin d'un parcours d'arbre) elle a toujours un
desavantage majeur qui est l'incertitude d'etre capable de modifier de
maniere *sure* l'arbre AST a ce moment. Comme la "convertion machine"
du RTL est deja faite pour certaines parties de l'arbre AST, il peut
etre dangereux de patcher l'arbre AST a ce moment.
Finalement, j'ai decide que le but de faire ce patch simple impliquait
de choisir la premiere option d'appeler mes fonctions d'evolution
depuis le parser C.
J'ai place le grapin dans mon patch en trois endroits. Deux appels
dans le code de c-parse.y (fichier principal du parser) permettant
d'examiner les boucles FOR et WHILE et de les modifier a la volee. Le
troisieme appel est localise en dehors du parser car attraper tous les
endroits etait tres delicat a faire depuis l'interieur du
parser. Basiquement, comme dans plein d'autres situations differantes,
un CALL_EXPR est cree les reliant tous semble ne pas etre
naturel. L'alternative que j'ai trouvee qui semble bien fonctionner
pour moi, etait d'ajouter un appel a ma fonction dans
build_function_call() a l'interieur du fichier typeck.c (constructeur
d'expressions de verification de type du compilateur C).
L'entree principale dans le patch est la fonction
blip_check_loop_limit() qui fera tout le travail de verifier si une
boucle semble etre approprie, et d'appeler la bonne fonction qui fera
le patch actuel de l'arbre AST.
Pour qu'une boucle soit consideree elle doit ressembler a une boucle
de comptage. Le patch blip essaiera par consequent d'examiner chaque
boucle et de decider si la boucle semble etre une boucle de comptage
(des criteres supplementaires pour l'examen des boucles
suivront). Pour chaque boucle de comptage un essai est fait pour
detecter la variable "compteur" et la variable "limite".
Exemple de boucles simples et leurs variables :
- for(i=0; i < j; i+=3}{;} ==> boucle incrementale, i = compteur
j = limite
- while(len--){;} ==> boucle decrementale, len = compteur, 0 = limite
L'implementation actuelle considere une boucle comme etant une boucle
de comptage si et seulement si :
- 2 variables sont detectees dans la condition de la boucle
(parfois l'une d'entre elles peut etre une constante)
- l'une de ces variables est modifiee dans la condition de la boucle
ou dans l'expression de la boucle
- *seulement une* variable est modifiee
- La modification est du type incrementation/decrementation
(++,--,+=,-=)
Le code qui examine la boucle est execute dans blip_find_loop_vars()
et il pourrait etre ameliore a l'avenir pour identifier plus de
boucles comme boucle de comptage.
Apres avoir detecte la direction de la boucle, son compteur et sa
limite, l'arbre AST est modifie pour inclure un test qui verifie
qu'une grosse boucle est rapportee comme une violation de blip.
Dans le but de maintenir le patch simple et sans risque, a chaque fois
qu'une boucle semble trop complexe pour etre prise pour une boucle de
comptage, la boucle sera ignoree (en utilisant les flags de warning de
blip il est possible de lister les boucles ignorees, et la raison pour
laquelle elles furent ignorees).
------[ 4.5.2 - Modifions le AST
Lorsque vous commencez a patcher des applications complexes comme gcc,
vous voulez vous assurer que vous ne causez aucun "effet papillon"
pendant que vous modifiez a la volee des structures residant en
memoire. Pour vous garder de beaucoup d'ennuis je suggererai d'eviter
les modification d'aucune structure directement. Mais utilisez a la
place les fonctions existantes que le paser de langage aurait utilise
si le code que vous voulez "injecter" avait ete trouve dans le code
source oiginal. Suivre ce niveau d'encapsulation sous empechera de
faire des erreurs comme oublier d'initialiser un membre de structure,
ou ne pas mettre a jour une autre variable globale ou flag.
J'ai trouve tres utile de simuler l'injection de code en modifiant
reellement le code source, et de tracer le compilateur quand il
contruit l'arbre AST, et de mimer plus tard la creation de code en
utilisant les memes fonctions utilisees par le parser pour construire
mon nouveau code de test. De cette maniere j'etais capable d'eliminer
le besoin d'acces "impropre" a l'arbre AST, lequel m'effrayais pendant
que je commencais la modification.
Connaissant le bon set de fonctions a utiliser pour injecter n'importe
quel code voulu, la question devint qu'aimerais vraiment injecter ? La
reponse differe un peu entre les differents types de boucles. Dans le
cas d'une boucle for le patch blip ajoutera l'expression de test comme
derniere expresion dans la declaration de FOR_INIT. Dans le cas d'une
boucle while le patch blip ajoutera l'expression de test en tant que
nouvelle declaration avant la boucle while. Dans le cas d'un appel a
une fonction ressemblant a une boucle, comme memcpy, le patch blip
remplacera l'expression d'appel entiere avec une nouvelle expression
de condition, ayant le __blip_violation sur le cote "vrai" et
l'espression d'appel originale sur le cote "faux".
Illustrons le dernier paragraphe avec quelques exemples...
Avant blip
----------
1) for(i=0;i< len;i++){}
2) While(len--){}
3) p = memcpy(d,s,l)
Apres blip
----------
1) for(i=0,?__blip_violation:0;i?__blip_violation:0;
while(len--){}
3) p = ?__blip_violation : memcpy(d,s,l)
Le en lui-meme est tres simple. Si la boucle est
incrementale (allant en augmentant) alors le test ressemblera a
(limite > compteur && limite - compteur > max). Si la boucle va en
baissant le test sera (compteur > limit && compteur - limite > max). Il
y a besoin de tester la differance entre le compteur et la limite et
pas seulement la limite car nous ne voulons pas declencher un faux
positif dans une boucle comme :
len = 0xffff0000;
for(i=len-20;i < len; i++){};
L'exemple ci-dessus pourrait ressembler au debut a un exploit
integer. Mais il pourrait egalement etre une boucle legitime qui
s'occupe simplement d'iterer sur de tres grandes valeurs.
La fonction responsable du est blip_build_check_exp(), et
son code est explicite, donc je ne vais pas dupliquer les commentaires
ici.
L'une des difficultes que j'ai eues en injectant le code de blip fut
l'injection de la fonction __blip_violation dans le fichier
cible. Pendant la creation de j'ai simlpement cree des
expressions qui referencie les memes noeuds d'arbres que j'ai trouves
dans la condition de boucle ou comme parametre d'appel d'une fonction
qui ressemble a une boucle. MAis la fonction __blip_violation n'existe
pas dans l'espace nom du fichier compile, et par consequent tenter de
la referencer etait un peu plus astucieux, ou du moins c'est ce que je
pensais. D'habitude lorsqu'un CALL_EXPR est cree, un FUNCTION_DECL est
identifie (comme l'une des fonctions disponibles visibles depuis
l'appelant) et un ADDR_EXPR est plus tard cree pour exprimer l'adresse
de la fonction declaree. Comme __blip_violation n'etait pas declaree,
les tentatives d'executer lookup_name() pour cela produiront une
declaration vide.
Par chance gcc etait suffisamment aimable / negligent, et je fus
capable de construire une FUNCTION_DECL et de la referencer en
laissant tout le reste du travail pour que le RTL le calcule.
Toutes les modifications ci-dessus ont ete faites dans
l'esprit d'une preuve de concept pour la detection d'exploits integer
blip. Il n'y a aucune garantie que le patch augmentera reellement la
protection de quelque systeme que ce soit, ni qu'il gardera le
compilateur stable et utilisable (en utilisant -fblip), ni que quelque
recommandation de coding / patching que ce soit aura un quelconque
sens pour ne noyau dur de mainteneur du projet gcc :>.
----[ 4.6 - Limites
Cette section resumme les limites a ma connaissance au moment d'ecrire
cet article. Je commencerai par les limites de haut niveau en allant
vers les limites techniques de bas niveau.
- La premiere limite est la couverture de ce patch. Le patch est
concu pour stopper les failles integer qui produisent de grosses
boucles. D'autres failles qui sont dues a une mauvaise conception ou
un manque de validation de nombres entiers ne seront pas protegees.
Par exemple le code suivant est vulnerable mais ne peut pas etre
protege par le patch :
void foo(unsigned int len,char* buf){
char dst[10];
if(len < 10){
strcpy(dst,buf);
}
}
- Parfois un integer overflow generique fait "comme dans les livres"
ne sera pas detecte. Un exemple pour un tel cas sera la faille
xdr_array. Le probleme est du au fait que la fonction malloc etait
appelee avec l'expression debordee de *deux* entree integer
differantes, alors que la protection blip ne peut intercepter qu'une
simple boucle de comptage. En regardant la boucle xdr_array, nous
pouvons voir qu'il serai facile pour l'attaquant / l'attaquante de
fournir de telles entrees de nombres entiers qui deborderons
l'expression malloc, mais qui gardera encore le compte de boucle
petit.
- Quelques boucles de comptage ne seront pas considerees. Un exemple
est une condition de boucle complexe et ce n'est pas trivial
d'identifier la boucle de comptage. De telles boucles doivent etre
ignorees, ou autrement des faux positifs pourraient apparaitre qui
pourraient mener a une execution non-definie.
- [Limite technique] La version actuelle est concue pour fonctionner
uniquement avec le langage C.
- [Limite technique] La version actuelle n'examinera pas le code
assembleur embarque qui pourrait inclure des instructions de
boucles. Par consequent elle permettra a l'exploitation d'integer
owerflow de n'etre pas detectee.
--[ 5 - References
[1] StackGuard
Detection et prevention automatique des attaques de smash de pile
http://www.immunix.org/StackGuard/
[2] ProPolic
Extension GCC pour proteger les applications des attaques de smash
de pile
http://www.trl.ibm.com/projects/security/ssp/
[3] GCC
GNU Compiler Collection
http://gcc.gnu.org
[4] noir
Smashing The Kernel Stack For Fun And Profit
Phrack Issue #60, Phile 0x06 by noir
[5] Halvar Flake
Third Generation Exploits on NT/Win2k Platforms
http://www.blackhat.com/presentations/bh-europe-01/halvar-flake/bh-
europe-01-halvarflake.ppt
[6] MaXX
Vudo malloc tricks
Phrack Issue 0x39, Phile #0x08
[7] Once upon a free()..
Phrack Issue 0x39, Phile #0x09
[8] Aleph One
Smashing The Stack For Fun And Profit
Phrack Issue 0x31, Phile #0x0E
--[ 6 - Thanks
Je veux remercier mon equipe pour m'avoir aide dans le processus de
creation de l'article. Merci Monty, sinan, yona, shok pour vos
commentaires utiles et vos idees pour ameliorer l'article. Si vous
pensez que l'Anglais est massacre dans cet article alors imaginez ce
que mon team a du supporter :>. Sans vous les mecs je ne l'aurai
jamais fait.
Merci a anonymous : > pour avoir relu l'article, et m'avoir fournit du
feedback technique interessant et de l'assurance.
--[ 7 - Annexe A - Des exemples dans la vraie vie
Ayant le patch pret, j'ai voulu le tester sur une faille connue. Les
criteres utililses pour tester le patch etaient :
- le paquetage doit etre compile avec succes avec le patch
- le patch doit etre capable de proteger le paqutetage contre
l'exploitation des bugs connus.
J'ai choisi de tester le patch sur les paquetages Apache httpd et
OpenSSH. Car les deux paquetages sont : de haut profil ont des failles
dont le patch est cense proteger, et sont suffisamment gros pour
certifier le patch.
Le test de protectiona demontre son succes :), et la version
vulnerable compilee avec -fblip a ete demontree comme etant non
exploitable.
La section suivante explique comment compiler les paquetages avec le
patch blip. Nous montrerons le code assembleur genere avant et apres
le patch pour le code qui permettait l'exploit de deborder les tampos
du programe.
----[ 7.1 - Apache Chunked encoding
--[ Informations sur la faille
Juste pour nous assurer que tout est OK avec la le probleme de la
faille du chunked-encoding de Apache, je vais lister une partie du
code vulnerable suivi de quelques explications.
Code: Apache src/main/http_protocol.c : ap_get_client_block()
01 len_to_read = get_chunk_size(buffer);
02 r->remaining = len_to_read;
03 len_to_read = (r->remaining > bufsiz) ? bufsiz : r->remaining;
04 len_read = ap_bread(r->connection->client, buffer , len_to_read);
La faille dans ce cas autorise a un(e) attaquant(e) d'envoyer une
longueur de chunk negative. Faire ceci passera au travers du test de
la ligne 03, et terminera en appelant ap_bread() avec un enorme nombre
positif.
--[ Testons le patch
Pour compiler le Apache httpd avec le -fblip accorde, on pourrait edit
le fichier src/apaci et ajouter la ligne suivante a la fin du fichier
"echo '-fblip'".
Toute tentative d'envoyer une longueur de chunk negative apres avoir
compile Apache httpd avec le patch blip terminera avec le httpd
executant la __blip_ violation.
Selon la theorie de blip, l'attaque devrait declencher une sorte de
boucle. Nous voyons a la ligne 04 du code liste qu'un appel est fait a
la fonction ap_bread(). Docn si la theorie est correcte nous sommes
supposes trouver une boucle a l'interieur de cette fonction.
/*
* Lis jusqu'a nbyte octets dans buf
* Si moins d'octets que byte sont actuellement disponible,
alors les retourner
* Retourne 0 pour EOF, -1 pour erreur.
* NOTE EBCDIC: Le tampon readaway contient _toujours_ des donnees
*non converties*
* Une conversion n'est faite que lorsque l'appelant reprend des
donnees du tampon (appelle bread), si le flag de conversion est mis a
ce moment.
*/
API_EXPORT(int) ap_bread(BUFF *fb, void *buf, int nbyte)
{
int i, nrd;
if (fb->flags & B_RDERR)
return -1;
if (nbyte == 0)
return 0;
if (!(fb->flags & B_RD)) {
/*Lecture non dans le tampon. Verifier d'abord si il y avait
* quelque chose dans le tampon avant que nous soyons
* non-tampon-ises. */
if (fb->incnt) {
i = (fb->incnt > nbyte) ? nbyte : fb->incnt;
#ifdef CHARSET_EBCDIC
if (fb->flags & B_ASCII2EBCDIC)
ascii2ebcdic(buf, fb->inptr, i);
else
#endif /*CHARSET_EBCDIC*/
memcpy(buf, fb->inptr, i);
fb->incnt -= i;
fb->inptr += i;
return i;
}
i = read_with_errors(fb, buf, nbyte);
#ifdef CHARSET_EBCDIC
if (i > 0 && ap_bgetflag(fb, B_ASCII2EBCDIC))
ascii2ebcdic(buf, buf, i);
#endif /*CHARSET_EBCDIC*/
return i;
}
nrd = fb->incnt;
/* pouvons-nous remplir le tampon */
if (nrd >= nbyte) {
#ifdef CHARSET_EBCDIC
if (fb->flags & B_ASCII2EBCDIC)
ascii2ebcdic(buf, fb->inptr, nbyte);
else
#endif /*CHARSET_EBCDIC*/
memcpy(buf, fb->inptr, nbyte);
fb->incnt = nrd - nbyte;
fb->inptr += nbyte;
return nbyte;
}
if (nrd > 0) {
#ifdef CHARSET_EBCDIC
if (fb->flags & B_ASCII2EBCDIC)
ascii2ebcdic(buf, fb->inptr, nrd);
else
#endif /*CHARSET_EBCDIC*/
memcpy(buf, fb->inptr, nrd);
nbyte -= nrd;
buf = nrd + (char *) buf;
fb->incnt = 0;
}
if (fb->flags & B_EOF)
return nrd;
/* fait une simple lecture */
if (nbyte >= fb->bufsiz) {
/* lit directement dans le tampon de l'appelant */
i = read_with_errors(fb, buf, nbyte);
#ifdef CHARSET_EBCDIC
if (i > 0 && ap_bgetflag(fb, B_ASCII2EBCDIC))
ascii2ebcdic(buf, buf, i);
#endif /*CHARSET_EBCDIC*/
if (i == -1) {
return nrd ? nrd : -1;
}
}
else {
/* lit dans le tampon tenu, puis memcpy */
fb->inptr = fb->inbase;
i = read_with_errors(fb, fb->inptr, fb->bufsiz);
if (i == -1) {
return nrd ? nrd : -1;
}
fb->incnt = i;
if (i > nbyte)
i = nbyte;
#ifdef CHARSET_EBCDIC
if (fb->flags & B_ASCII2EBCDIC)
ascii2ebcdic(buf, fb->inptr, i);
else
#endif /*CHARSET_EBCDIC*/
memcpy(buf, fb->inptr, i);
fb->incnt -= i;
fb->inptr += i;
}
return nrd + i;
}
Nous voyons dans le code plusieurs flux d'execution possibles. Chacun
d'eux inclut une boucle qui bouge toute les donnees dans le paraletre
buf. Si le code supporte CHARSET_EBCDIC alors la fonction
ascii2ebdcdic execute la boucle mortelle. Dans les cas normaux, la
fonction memcpy implemente la boucle meurtriere.
Ce qui suit est le code assembleur genere pour la fonction ci-dessus.
.type ap_bread,@function
ap_bread:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
movl %ebx, -12(%ebp)
movl %esi, -8(%ebp)
movl %edi, -4(%ebp)
movl 8(%ebp), %edi
movl 16(%ebp), %ebx
testb $16, (%edi)
je .L68
movl $-1, %eax
jmp .L67
.L68:
movl $0, %eax
testl %ebx, %ebx
je .L67
testb $1, (%edi)
jne .L70
cmpl $0, 8(%edi)
je .L71
movl 8(%edi), %esi
cmpl %ebx, %esi
jle .L72
movl %ebx, %esi
.L72:
cmpl $268435456, %esi ----------------------------
jbe .L73
movl %esi, (%esp) Test de blip (utilisant esi)
call __blip_violation
jmp .L74 ----------------------------
.L73:
movl 4(%edi), %eax
movl 12(%ebp), %edx
movl %edx, (%esp)
movl %eax, 4(%esp)
movl %esi, 8(%esp)
call memcpy
.L74:
subl %esi, 8(%edi)
addl %esi, 4(%edi)
movl %esi, %eax
jmp .L67
.L71:
movl %edi, (%esp)
movl 12(%ebp), %eax
movl %eax, 4(%esp)
movl %ebx, 8(%esp)
call read_with_errors
jmp .L67
.L70:
movl 8(%edi), %edx
movl %edx, -16(%ebp)
cmpl %ebx, %edx
jl .L75
cmpl $268435456, %ebx ----------------------------
jbe .L76
movl %ebx, (%esp) Test de blip (utilisant ebx)
call __blip_violation
jmp .L77 ----------------------------
.L76:
movl 4(%edi), %eax
movl 12(%ebp), %edx
movl %edx, (%esp)
movl %eax, 4(%esp)
movl %ebx, 8(%esp)
call memcpy
.L77:
movl -16(%ebp), %eax
subl %ebx, %eax
movl %eax, 8(%edi)
addl %ebx, 4(%edi)
movl %ebx, %eax
jmp .L67
.L75:
cmpl $0, -16(%ebp)
jle .L78
cmpl $268435456, -16(%ebp) ------------------------
jbe .L79
movl -16(%ebp), %eax Test de blip
movl %eax, (%esp) (utilisant [ebp-16])
call __blip_violation
jmp .L80 ------------------------
.L79:
movl 4(%edi), %eax
movl 12(%ebp), %edx
movl %edx, (%esp)
movl %eax, 4(%esp)
movl -16(%ebp), %eax
movl %eax, 8(%esp)
call memcpy
.L80:
subl -16(%ebp), %ebx
movl -16(%ebp), %edx
addl %edx, 12(%ebp)
movl $0, 8(%edi)
.L78:
testb $4, (%edi)
je .L81
movl -16(%ebp), %eax
jmp .L67
.L81:
cmpl 28(%edi), %ebx
jl .L82
movl %edi, (%esp)
movl 12(%ebp), %eax
movl %eax, 4(%esp)
movl %ebx, 8(%esp)
call read_with_errors
movl %eax, %esi
cmpl $-1, %eax
jne .L85
jmp .L91
.L82:
movl 20(%edi), %eax
movl %eax, 4(%edi)
movl %edi, (%esp)
movl %eax, 4(%esp)
movl 28(%edi), %eax
movl %eax, 8(%esp)
call read_with_errors
movl %eax, %esi
cmpl $-1, %eax
jne .L86
.L91:
cmpl $0, -16(%ebp)
setne %al
movzbl %al, %eax
decl %eax
orl -16(%ebp), %eax
jmp .L67
.L86:
movl %eax, 8(%edi)
cmpl %ebx, %eax
jle .L88
movl %ebx, %esi
.L88:
cmpl $268435456, %esi ----------------------------
jbe .L89
movl %esi, (%esp) Test de blip (utilisant esi)
call __blip_violation
jmp .L90 ----------------------------
.L89:
movl 4(%edi), %eax
movl 12(%ebp), %edx
movl %edx, (%esp)
movl %eax, 4(%esp)
movl %esi, 8(%esp)
call memcpy
.L90:
subl %esi, 8(%edi)
addl %esi, 4(%edi)
.L85:
movl -16(%ebp), %eax
addl %esi, %eax
.L67:
movl -12(%ebp), %ebx
movl -8(%ebp), %esi
movl -4(%ebp), %edi
movl %ebp, %esp
popl %ebp
ret
On peut remarquer qu'avant tout appel a la fonction memcpy (qui est
l'une des fonctions ressemblant a une boucle) [NDT : dans tout
l'article c'est l'expression "loop like" function que j'ai traduite
comme cela, desole], un peu de code fut ajoute qui appelle
__blip_violation au cas ou le troisieme parametre de memcpy est plus
grand que blip_max.
Une autre chose qui vaut d'etre mentionnee est le moyen par lequel le
test injecte accede au troisieme parametre. Dans le premier bloc de
code injecte le parametre est stocke dans le registre esi, dans le
second bloc le parametre est dans le registre ebx et dans le troisieme
bloc le parametre est stocke dans la pile a ebp-16. La raison a cela
est tres simple. Comme la modification du code a ete faite dans
l'arbre AST, et comme de patch etaient en train d'utiliser exactement
le meme noeud de l'arbre qui etait utilise dans l'expression d'appel a
memcpy, le RTL a genere le meme code pour l'expression d'appel et
l'expression de test.
A present retournons a la fonction ap_bread. Et supposons que
CHARSET_EBCDIC etait en effet definie. Dans ce cas la fonction
ascii2ebcdic aurait ete celle qui aurait eu la boucle
"vulnerable". Par consequent nous esperons que le patch blip aurait
aussi bien teste la boucle dans cette fonction.
Ce qui suit est le code de ascii2ebcdic extrait de src/ap/ap_ebcdic.c
API_EXPORT(void *)
ascii2ebcdic(void *dest, const void *srce, size_t count)
{
unsigned char *udest = dest;
const unsigned char *usrce = srce;
while (count-- != 0) {
*udest++ = os_toebcdic[*usrce++];
}
return dest;
}
Resultat de la compilation de la focntion ci-dessus avec -fblip
.type ascii2ebcdic,@function
ascii2ebcdic:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
subl $12, %esp
movl 16(%ebp), %ebx
movl 8(%ebp), %edi
movl 12(%ebp), %esi
cmpl $0, %ebx -------------------
jbe .L12
cmpl $268435456, %ebx
jbe .L12 Test de Blip
movl %ebx, (%esp)
call __blip_violation
.L12: -------------------
decl %ebx
cmpl $-1, %ebx
je .L18
.L16:
movzbl (%esi), %eax
movzbl os_toebcdic(%eax), %eax
movb %al, (%edi)
incl %esi
incl %edi
decl %ebx
cmpl $-1, %ebx
jne .L16
.L18:
movl 8(%ebp), %eax
addl $12, %esp
popl %ebx
popl %esi
popl %edi
popl %ebp
ret
.Lfe2:
Pendant qu'il s'occupait de la fonction ascii2ebcdic, le patch blip a
identifie la boucle while comme etant une boucle de comptage. La
condition de la boucle fournti toute les informations requises pour
creer un . D'abord nous identifions les variables de la
boucle. Dans ce cas "count" est une variable est la constante "0" est
la seconde. En cherchant des modifications de la variable, nous voyons
que "count" est decrementee dans l'expression "count--". Comme "count"
est la seule variable modifiee nous pouvosn dire que "count" est la
variable-compteur et que la constante 0 est la varable limite. Nous
pouvons egalement dire que la boucle est une boucle decrementale car
l'operation de modification est "--". Le test sera par consequent
(count > limit && count - limit > MAX_BLIP). En regardant le code
assembleur ci-dessus, nous voyons que le compteur de boucle est stocke
dans le registre ebx (Il est facile de reperer ceci en regardant le
code sous l'etiquette 12 (L12). Ce code represente la condition
while. Ca decremente d'abord ebx et le compare plus tard avec la
constant de la boucle.). Par consequent le utilsera le
registre ebx pour le test.
----[ 7.2 - Authentification OpenSSH
--[ Informations sur la faille
La faille OpenSSH est un exemple de bug integer overflow, qui mene a
un mauvais calcul de la taille de l'allocation. Ce qui suit est un
bout du code vulnerable :
OpenSSH auth2-chall.c : input_userauth_info_response()
01 nresp = packet_get_int();
02 response = xmalloc(nresp * sizeof(char*));
03 for(i = 0; i < nresp; i++)
04 response[i] = packet_get_string(NULL);
A la ligne 01 le code lit un entier dans une variable non-signee. Plus
tard la code alloue un tableau a nresp entrees. Le probleme est que
nresp * sizeof(char*) est une expression qui pourreait deborder. Par
consequent envoyer nresp plus grand que 0x40000000 permet l'allocation
d'un petit tampon qui peut etre plus tard deborde par l'assigment de
la ligne 04.
--[ Testons le patch
Pour compiler le paquetage OpenSSH avec -fblip, on peut ajouter
-fbliup a la definition CFLAGS de Makefile.in
(i.e. CFLAGS=@CFLAGS@ -fblip)
Toute tentative d'envoyer un grand nombre de reponses apres avoir
compile OpenSSH avec le patch blip terminera avec OpenSSH executant la
__blīp_violation.
Ce qui suit est un morceau de la fonction vulnerable.
static void
input_userauth_info_response(int type, u_int32_t seq, void *ctxt)
{
Authctxt *authctxt = ctxt;
KbdintAuthctxt *kbdintctxt;
int i, authenticated = 0, res, len;
u_int nresp;
char **response = NULL, *method;
nresp = packet_get_int();
if (nresp != kbdintctxt->nreq)
fatal("input_userauth_info_response: wrong number of
replies");
if (nresp > 0) {
-----------------------------------------
** Code vulnerable**
-----------------------------------------
response = xmalloc(nresp * sizeof(char*));
for (i = 0; i < nresp; i++)
response[i] = packet_get_string(NULL);
}
}
La fonction ci-dessus se traduit par le code assembleur qui suit si
compilee avec la protection -fblip. (Pour rendre lisible la
modification blip, lle code a ete compile en utilisant -O au lieu
d'utiliser -O2, qui va reordonner les blocs de base).
.type input_userauth_info_response,@function
input_userauth_info_response:
movl -16(%ebp), %eax
movl $0, 4(%eax)
call packet_get_int
movl %eax, %esi
movl -20(%ebp), %edx
cmpl 12(%edx), %eax
je .L111
movl $.LC15, (%esp)
call fatal
.L112:
testl %esi, %esi
je .L113
leal 0(,%esi,4), %eax
movl %eax, (%esp)
call xmalloc
movl %eax, -32(%ebp)
movl $0, %ebx
cmpl $0, %esi
jbe .L115
cmpl $268435456, %esi ------------------------
jbe .L115
movl %esi, (%esp) Test de Blip
call __blip_violation
.L115: ------------------------
cmpl %esi, %ebx
jae .L113
.L120:
movl $0, (%esp)
call packet_get_string
movl -32(%ebp), %ecx
movl %eax, (%ecx,%ebx,4)
incl %ebx
cmpl %esi, %ebx
jb .L120
Le patch blip a identifie la boucle for comme une boucle de comptage
est a injecte un code pour rediriger le flux vers l'intercepteur
_blip_violation dans le cas ou la limite (i.e. nresp) est plus grand
que BLIP_MAX. Par consequent si la valeur de nresp est assez haute
pour declencher un overflow dans l'appel de xmalloc, elle sera
egalement assez haute pour se faire prendre par le .
--[ 8 - Annexe B - Utilisons blip
Pour mettre en service le patch blip on devrait d'abord ajouter le
flag -fblip quand on execute le compilateur gcc.
Le patch blip tentera d'emettre le a chaque fois que cela
semble possible de le faire. Le patch ignorera sans rien dire toutes
les boucles qui ne peuvent pas etre protegees. Pour voir les boucles
ignorees on peut utiliser l'un des flags de warning suivants, qui vont
egalement fournir un message decrivant la raison pour laquelle la
boucle a ete ignoree.
Flags de warning :
- blip_for_not_emit - rapporte les boucles for ignorees.
- blip_while_not_emit - rapporte les boucles while ignorees.
- blip_call_not_emit - rapporte les appels ignores a des fonctions
ressemblant a des boucles.
Une raison pour ignorer une boucle sera l'une des suivantes :
- Les variables de boucles sont longues de moins de 4 octets
- L'intitalisation du for n'est pas une expression
- L'appel a la fonction est fait en utilisant un pointeur vers une
fonction
- Les parametres d'appel ont des effets de bords. Reutiliser
l'expression peut causer des resultats inattendus
- La condition de boucle est trop complexe pour trouver les variables
de la boucle
- Aucune des variables de la boucle n'est modifiee (pas suffisamment
d'informations pour effectuer le test)
- Les deux variables de la boucle sont modifiees
- La condition est trop complexe
Le patch blip est egalement capable de rapporter les statistiques de
de tests. En utilisant -fblip_stat on peut faire imprimer des
informations statistiques a propos du nombre de boucles effectuees et
le nombre de boucles qui ont ete testees avec succes.
La ligne de commande qui suit compilera le code du premier exemple. La
sortie de la compilation suivra.
$ gcc -o sample -fblip -fblip_stat -O sample.c
-=] Blip statistics (checks emits)
Total: 1/100% 1/100%
for: 1/100% 1/100%
while: 0/0% 0/0%
calls: 0/0% 0/0%
-=] End Blip Statistics
begin 640 blip.patch
M9&EF9B`M3G5R(&=C8RTS+C(O9V-C+TUA:V5F:6QE+FEN(&=C8RTS+C(M8FQI
M<"]G8V,O36%K969I;&4N:6X-"BTM+2!G8V,M,RXR+V=C8R]-86ME9FEL92YI
M;@E4:'4@36%Y(#(S(#$P.C4W.C(Q(#(P,#(-"BLK*R!G8V,M,RXR+6)L:7`O
M9V-C+TUA:V5F:6QE+FEN"4UO;B!$96,@(#(@,3DZ-#(Z,SD@,C`P,@T*0$`@
M+36]U="YO('-T&ET(%]A8G-V2!O9@T-"BL@
M*B`@("!-15)#2$%.5$%"24Q)5%D@;W(@1DE43D534R!&3U(@02!005)424-5
M3$%2(%!54E!/4T4N("!3964@=&AE#0T**R`J("`@($=.52!'96YE7-T96TN:"(-#0HK(VEN8VQU9&4@(FUA8VAM;V1E
M+F@B#0T**R-I;F-L=61E(")R=&PN:"(-#0HK(VEN8VQU9&4@(G1R964N:"(-
M#0HK(VEN8VQU9&4@(G1O<&QE=BYH(@T-"BLC:6YC;'5D92`B8FQI<"YH(@T-
M"BLC:6YC;'5D92`B9FQA9W,N:"(-#0HK(VEN8VQU9&4@(F,M8V]M;6]N+F@B
M#0T**PT-"BLO*B!T:&ES('-T7,@#0T**R`J('-T871L97-S
M+"!T:&%N(&ETR)B8V]P>2(L,GTL#0T**PE[(F)Z
M97)O(BPQ?2P-#0HK"7LB2D@"7D@/R`H>"`J(#$P,"DO>2`Z(#`-
M#0HK#0T**R\J('!R:6YT(&)L:7`@2!D;R!S
M;R!I9B!T:&4@7!E("AI;G1E9V5R7W1Y<&5?;F]D92Q.54Q,7U12144I*3L-#0HK
M#0T**PE$14-,7T%25$E&24-)04P@*&)L:7!?=FEO;&%T:6]N7V1E8VPI(#T@
M,3L-#0HK"41%0TQ?15A415).04P@*&)L:7!?=FEO;&%T:6]N7V1E8VPI(#T@
M,3L-#0HK"41%0TQ?24Y,24Y%("AB;&EP7W9I;VQA=&EO;E]D96-L*2`](#`[
M#0T**PE44D5%7U!50DQ)0R`H8FQI<%]V:6]L871I;VY?9&5C;"D@/2`Q.PT-
M"BL)#0T**PEB;&EP7V9U;F-?9&5C;"`](&)L:7!?=FEO;&%T:6]N7V1E8VP[
M#0T**PT-"BL)<&%R86US("`]('1R965?8V]N'`@/2!B=6EL9%]F=6YC=&EO;E]C
M86QL*&)L:7!?9G5N8U]D96-L+'!A'`[#0T**WT-#0HK#0T**R\J(`E#'`@9F]R('1H
M92!B;&EP(&-O;F1I=&EO;B!T:&4@97AP('=I;&P@8F4@;V8@0T].1%]%6%!2
M(`T-"BL@*B`)='EP92P@86YD('=I;&P@:&%V92!T:&4@9F]L;&]W:6YG(&9O
M2!T:&4@9&ER96-T:6]N(&]F('1H92!L;V]P(`T-"BL@*@T-"BL@
M*B`)($%S(&$@;F]T92P@:2!C;W5L9"!H879E(&%D9"!S;VUE(&5X=')A(&QO
M9VEC('1O(&5L:6UI;F%T92!T:&4@8V]M<&QE>"`-#0HK("H@"2!C:&5C:R!I
M9B!T:&4@;&EM:70O8V]U;G0@87)E(&-O;G-T86YT