Christophe Blaess est un ingénieur indépendant dans le domaine de l'aéronautique Passionné par Linux, il effectue l'essentiel de son travail sur ce système, et assure la coordination des traductions des pages de manuel publiées par le Linux Documentation Project.
Christophe Grenier est étudiant en 5ème année à l'ESIEA, où il est également administrateur système. La sécurité informatique est l'une de ses passions.
Frédéric Raynal utilise Linux, certifié sans brevet (logiciels ou autres). A part ça, il faut absolument voir Dancer in the Dark : outre Björk qui est exceptionnelle, ce film ne laisse pas indifférent (je ne veux en dire plus sans dévoiler la fin, tragique et magnifique à la fois).
Cet article est le premier d'une série traitant des principales failles de sécurité susceptibles d'apparaître dans une application. Au cours des articles, nous présenterons les moyens de les éviter en modifiant quelque peu les habitudes de développement.
Il s'écoule rarement plus d'une quinzaine de jours consécutifs sans qu'une application majeure fournie sur l'essentiel des distributions Linux ne révèle une faille de sécurité permettant, par exemple, à un utilisateur local de devenir root. Malgré la qualité indéniable de la plupart de ces logiciels, assurer la fiabilité d'un programme au niveau sécurité se révèle une tâche très compliquée : il ne faut pas qu'il permette à un utilisateur mal intentionné de tirer illégalement profit des ressources du système. La disponibilité des sources des applications est une excellente chose, que tous les programmeurs apprécient largement, mais le moindre défaut dans la cuirasse d'un logiciel se retrouve ainsi exposé aux yeux de tout le monde. De surcroît, la détection de tels défauts résulte souvent d'une démarche volontaire et les personnes s'y astreignant ne sont pas toutes bien intentionnées.
Du côté de l'administrateur du système, un travail quotidien doit être
consacré à la lecture des listes de diffusion traitant des problèmes de
sécurité, et à la mise à jour immédiate des paquetages incriminés. Du côté
du programmeur en revanche, il est plus judicieux d'essayer d'anticiper les
problèmes. Il est préférable d'éviter les failles éventuelles dès la
conception ou la programmation du logiciel.Nous allons ici
essayer de caractériser quelques comportements "classiquement" dangereux, et
présenter des solutions pour réduire les risques tout en réalisant les mêmes
fonctionnalités. Nous ne traiterons pas des problèmes de sécurité liés au
réseau, car ils relèvent généralement d'erreurs de configuration
(scripts CGI-bin dangereux, ...) ou de bogues situés dans le
système lui-même et qui permettent des attaques de type DOS (Denial Of
Service) afin de rendre la machine sourde à ses clients légitimes. Ces
problèmes concernent l'administrateur du système ou les
développeurs du noyau, mais aussi le programmeur applicatif s'il
traite des données d'origine externe. Par exemple,
pine
, acroread
, netscape
,
access
,... dont certaines versions sous certaines
conditions permettaient des accès distants ou des fuites d'information.
La programmation sécurisée concerne finalement tout le
monde.
Cette série d'articles présente des méthodes qui permettent de nuire à un système type Unix. Nous aurions pu uniquement les évoquer, ou en parler à mots couverts, mais nous préférons le faire ouvertement pour que chacun ait bien conscience des risques. Ainsi, lorsque vous corrigerez un programme ou que vous développerez le votre, vous serez à même d'éviter ou bien de corriger ces erreurs. Pour chaque faille présentée, nous adopterons une démarche identique. Nous commencerons par en détailler le fonctionnement. Ensuite, nous montrerons les précautions à prendre pour les éviter. A chaque fois, nous illustrerons nos propos avec des trous de sécurité présents dans des logiciels encore largement répandus.
Ce premier article présente les bases nécessaires à la
compréhension des failles de sécurité, c'est à dire la notion de privilèges,
et de bits Set-UID ou Set-GID. Nous nous consacrons
ensuite à l'étude des failles les plus faciles à comprendre, fondées
sur l'emploi de la fonction system()
.
Nous utiliserons souvent de petits programmes en C pour illustrer nos
propos. Toutefois, les démarches indiquées dans ces articles restent
valables pour d'autres langages : perl, java, les shell
scripts... Certaines failles dépendent d'un langage, mais ce n'est
pas le cas de toutes, comme nous le verrons dès cet article avec
system()
.
Les utilisateurs ne sont pas égaux sur un système Unix, pas plus d'ailleurs
que ne le sont les applications. L'accès aux différents noeuds du système de
fichiers - et de ce fait aux principaux périphériques de la machine - est
soumis à un contrôle d'identité strict.
De même, certains utilisateurs sont autorisés à entreprendre des
opérations sensibles pour garantir le bon fonctionnement du système.
L'identification des
utilisateurs se fait par l'intermédiaire d'un numéro nommé UID (User
Identifier). Par commodité, chaque numéro est associé à un nom
d'utilisateur plus parlant, la correspondance s'établissant dans le fichier
/etc/passwd
.
L'utilisateur root, dont l'UID vaut 0 par définition, dispose
de tous les droits sur le système. Il peut non seulement créer, modifier, ou
supprimer n'importe quel noeud du système de fichiers, mais il peut aussi
intervenir sur la configuration physique de la machine, en montant des
partitions dans l'arborescence des fichiers, en activant des interfaces
réseau et en modifiant leur configuration (adresse IP), ou simplement en
invoquant des appels-système privilégiés comme mlock()
qui agit
sur la mémoire physique, ou sched_setscheduler()
qui modifie le
mécanisme d'ordonnancement. Nous étudierons dans un article ultérieur les
capacités Posix.1e qui permettent de limiter un peu la toute puissance d'une
application exécutée sous l'identité root, mais pour le moment nous
considérerons que le super-utilisateur est tout-puissant sur la machine.
Les attaques que nous essaierons de prévenir dans nos articles sont internes, c'est à dire qu'un utilisateur dûment autorisé à se connecter sur la machine tente de s'approprier des privilèges ne lui appartenant pas. À l'opposé les attaques réseau sont généralement externes, en provenance de personnes cherchant à obtenir une connexion sur la station alors qu'elles n'y ont normalement pas d'accès légitime. S'approprier les privilèges d'un autre utilisateur signifie que l'opération à réaliser le sera sous couvert de son identité, de son UID, et non de la sienne propre. Naturellement, tout pirate tend à usurper l'identité root, mais d'autres comptes utilisateur s'avèrent également intéressants, soit parce qu'ils donnent accès à des informations système (news, mail, lp...) soit parce qu'ils permettent la lecture de données privées (courriers, fichiers personnels, etc) ou la dissimulation d'activités illégales comme les attaques dirigées vers d'autres sites.
Pour utiliser les privilèges réservés à un autre utilisateur, sans pour autant avoir la possibilité de se connecter directement sous son identité, il faut, au minimum, être en mesure de dialoguer avec une application s'exécutant sous l'UID de la victime. Lorsqu'une application - un processus - se déroule sous Linux, c'est avec une identité bien définie. Tout d'abord un programme est doté d'un attribut nommé RUID (Real UID) correspondant à l'identité de l'utilisateur l'ayant lancé. Cette donnée remplie par le noyau est normalement immuable. Un second attribut vient compléter cette information : le champ EUID (Effective UID) qui correspond à l'identité que le noyau prend effectivement en compte pour vérifier les autorisations d'accès pour les opérations nécessitant une identification (ouverture de fichier, appel-système réservé).
Pour qu'une application s'exécute avec un UID effectif (ses
autorisations) différent de son UID réel (l'utilisateur qui l'a lancé), il
faut fixer sur son fichier exécutable un bit de permission particulier nommé
Set-UID. Ce bit se trouve dans l'attribut des permissions du fichier (comme
les bits d'exécution, de lecture et d'écriture pour l'utilisateur, les
membres du groupe ou le reste du monde), et correspond à la valeur octale
4000. Le bit Set-UID est représenté par un s
lors de
l'affichage des permissions avec la commande ls
:
La commande ">> ls -l /bin/su -rwsr-xr-x 1 root root 14124 Aug 18 1999 /bin/su >>
find / -type f -perm +4000
" fournit une liste des
applications du système ayant leur bit Set-UID à 1. Lorsque le noyau lance
une application dont le bit Set-UID du fichier exécutable vaut 1, il
utilise l'identité du propriétaire du fichier comme EUID du processus. Le
RUID, en revanche, ne varie pas, et correspond à la personne ayant lancé le
programme. Dans le cas de /bin/su
par exemple, chaque
utilisateur dispose de cette commande, mais elle se déroule sous l'identité
de son propriétaire (root), et possède donc tous les privilèges
possibles sur le système. Inutile de préciser qu'il convient d'être
particulièrement prudent lors de l'écriture d'un programme avec cet attribut.
Il existe symétriquement pour chaque processus un identificateur du groupe
d'utilisateur effectif EGID, et un identificateur réel RGID. De même le bit
Set-GID (2000 en octal) dans les permissions associées à un fichier
exécutable demande au noyau de prendre comme EGID celui du groupe
propriétaire du fichier, plutôt que le groupe de l'utilisateur ayant lancé
le programme. Une combinaison
curieuse, avec le bit Set-GID à 1 mais sans l'autorisation d'exécution pour
le groupe, apparaît parfois. Il s'agit en réalité d'une convention n'ayant rien à voir avec
les privilèges associés aux applications, mais indiquant seulement que le
fichier est susceptible de faire l'objet de verrouillage strict avec la
fonction fcntl(fd, F_SETLK, lock)
. Une
application tire rarement parti des possibilités offerte
par le bit Set-GID, mais cela se produit parfois, par exemple certains
jeux emploient ce mécanisme pour sauvegarder les meilleurs scores
dans un répertoire système.
Il existe plusieurs types d'attaques à l'encontre de la sécurité d'un système. Les mécanismes que nous allons observer aujourd'hui reposent sur l'invocation d'une commande externe par une application, grâce à laquelle on s'arrange pour démarrer un autre programme, souvent un shell, avec l'identité du propriétaire de l'application principale. Un deuxième type d'attaque, que nous étudierons dans les prochains articles, s'appuie sur la technique du débordement de buffer (buffer overflow) permettant à l'attaquant de faire exécuter à l'application des instructions de code personnelles. Enfin, le troisième type principal d'attaque est fondé sur une condition de concurrence (race condition), laps de temps entre deux instructions pendant lequel une modification d'un élément du système - souvent un fichier - intervient alors que l'application le considère immuable.
Les deux premiers types d'attaques cherchent souvent à exécuter un shell
avec les privilèges du propriétaire de l'application, alors que le troisième
est plutôt orienté vers l'accès, généralement en écriture, à des
fichiers système protégés. L'accès en lecture
est parfois considéré comme une atteinte à la sécurité du système (fichiers personnels,
courriers électroniques, fichier des mots de passe /etc/shadow
,
et même pseudo-fichiers de configuration du noyau dans /proc
).
Les cibles des attaques de sécurité sont pour l'essentiel les programmes
dont le fichier exécutable possède un bit Set-UID (ou Set-GID). Toutefois
cela concerne également toute application qui s'exécute sous une identité
différente de celle de l'utilisateur avec lequel elle dialogue. Une partie
de ces programmes est représentée par les démons système. Un démon est une
application généralement démarrée dès l'initialisation du système, qui
s'exécute à l'arrière-plan sans terminal de contrôle, et qui réalise des
opérations privilégiées pour le compte d'utilisateur quelconque. Par exemple
le démon lpd
permet à tout utilisateur de transmettre des
documents à l'imprimante, sendmail
reçoit et aiguille des
courriers électroniques, ou encore apmd
interroge le Bios pour
connaître le niveau de charge d'une batterie sur un ordinateur portable. Il
existe également des démons chargés de communiquer avec des utilisateurs
externes, par l'intermédiaire du réseau (services Ftp, Html, Telnet...). Ils
sont gérés globalement par un serveur nommé inetd
qui assure
l'établissement de la connexion.
En définitive nous pouvons en conclure qu'un programme est susceptible de faire l'objet d'attaques s'il dialogue - même brièvement - avec un utilisateur différent de celui qui l'a fait démarrer. Si la conception d'une application fait apparaître une telle caractéristique, il est important de faire preuve de prudence lors du développement, et de garder à l'esprit les risques présentés par les fonctions que nous étudierons ici.
Lorsqu'une application s'exécute avec un UID effectif différent de son UID réel, c'est dans le but de disposer par moment de privilèges auxquels son utilisateur n'a pas droit directement (accès à des fichiers, appels-système réservés...). Toutefois ce besoin ne s'exprime en général que très ponctuellement, par exemple lors de l'ouverture d'un fichier, et le reste du temps l'application pourrait très bien se contenter du niveau de privilèges attribué à son utilisateur. Il est possible de modifier temporairement l'UID effectif d'une application grâce à l'appel-système :
int seteuid (uid_t uid);Un processus peut toujours modifier la valeur de son UID effectif en lui celle de son UID réel. Dans ce cas l'ancien UID est mémorisé dans un champ de sauvegarde nommé SUID (Saved UID) à ne pas confondre avec le SID (Session ID) qui sert à la gestion du terminal de contrôle. Il est également toujours possible de reprendre la valeur de l'UID sauvé pour l'employer en UID effectif. Naturellement un programme ayant un UID effectif nul (root) peut modifier à volonté ses UID effectif et réel (c'est ainsi que fonctionne
/bin/su
).
Pour limiter les risques d'attaques, il est conseillé de modifier au plus vite l'UID effectif pour employer l'identité réelle de l'utilisateur (R-UID). Lorsqu'une portion de code nécessite des privilèges correspondants à ceux du propriétaire du fichier, on replace temporairement l'UID sauvé dans l'UID effectif. Voici un exemple de code :
uid_t e_uid_initial; uid_t r_uid; int main (int argc, char * argv []) { /* Sauvegarde des différents UIDs */ e_uid_initial = geteuid (); r_uid = getuid (); /* limitation des droits à ceux du lanceur */ seteuid (r_uid); ... fonction_privilegiee (); ... } void fonction_privilegiee (void) { /* Restitution des privilèges initiaux */ seteuid (e_uid_initial); ... /* Portion nécessitant les privilèges */ ... /* Retour aux droits du lanceur */ seteuid (r_uid); }
Cette manière de procéder est beaucoup plus sûre que l'inverse, trop souvent rencontrée, et qui consiste à fonctionner avec l'UID effectif initial en permanence, puis à réduire temporairement les privilèges juste avant d'effectuer une opération "à risque". Il faut noter malgré tout que cette réduction des privilèges n'est pas efficace contre les attaques par débordement de buffer. En effet, comme nous le verrons dans le prochain article, ces attaques visent à faire exécuter des instructions personnelles à l'application, et peuvent contenir les appels-système nécessaires pour remonter le niveau de privilège. Cette approche protège toutefois contre les exécutions de commandes externes que nous allons examiner, mais également contre l'essentiel des conditions de concurrence.
Il est fréquent qu'une application ait besoin de faire appel à un service
système externe. L'exemple le plus classique est l'invocation de la commande
mail
pour transmettre un courrier électronique (rapport
d'exécution, alarme, statistiques, etc.) sans avoir besoin de mettre en
oeuvre un dialogue complexe avec le système de messagerie. La
manière la plus facile de
procéder est d'employer la fonction de bibliothèque :
int system (const char * commande)
Cette fonction présente un danger important : elle invoque le shell
lui-même pour exécuter la commande transmise en argument. Or, le
comportement du shell est sensible à des éléments que l'utilisateur peut
configurer à sa guise. L'exemple le plus frappant provient de la variable
d'environnement PATH
. Supposons qu'une application
appelle, à un moment ou à un autre, la fonction mail
. Par
exemple le programme suivant envoie son propre code-source à l'utilisateur
qui l'a lancé :
Nous supposons que ce programme est installé Set-UID root :/* system1.c */ #include <stdio.h> #include <stdlib.h> int main (void) { if (system ("mail $USER < system1.c") != 0) perror ("system"); return (0); }
Pour exécuter ce programme, le système lance un shell (avec le nom>> cc system1.c -o system1 >> su Password: [root] chown root.root system1 [root] chmod +s system1 [root] exit >> ls -l system1 -rwsrwsr-x 1 root root 11831 Oct 16 17:25 system1 >>
/bin/sh
) et lui transmet, avec l'option
-c
, l'instruction
à invoquer. Le shell parcourt alors l'ensemble des répertoires
indiqués dans la variable d'environnement PATH
à la recherche
d'un exécutable nommé mail
.
L'utilisateur n'a qu'à modifier
le contenu de cette variable avant de lancer l'application
principale. Par exemple, le remplacement suivant :
dirige la recheche de la commande>> export PATH=. >> ./system1
mail
uniquement dans le
répertoire courant. Il suffit alors d'y créer un fichier exécutable
quelconque (par exemple un script qui lance un nouveau shell) et de le
nommer mail
pour que ce programme soit automatiquement exécuté
avec l'UID effectif du propriétaire de l'application principale ! Ici,
notre script lance /bin/sh
. Toutefois comme il est exécuté
avec son entrée standard redirigée (comme la commande mail
initiale), nous devons la rétablir sur le terminal. Ainsi nous créons le
script :
L'exécution est concluante :#! /bin/sh # Script "mail" fictif lançant un shell en # rétablissant son entrée standard. /bin/sh < /dev/tty
>> export PATH="." >> ./system1 bash# /usr/bin/whoami root bash#
Bien sûr, une première solution consiste à donner le chemin d'accès complet
du fichier exécutable visé, par exemple /bin/mail
. Une
nouvelle difficulté se dresse alors : l'application dépend de
l'installation du
système. Si /bin/mail
est généralement disponible sur tous les
systèmes, des problèmes vont commencer à se poser par exemple pour trouver
GhostScript (se trouve-t il en /usr/bin
,
/usr/share/bin
, /usr/local/bin
?). D'autre part,
un deuxième type d'attaque est alors possible sur certains vieux
shells :
l'utilisation de la variable d'environnement IFS
. Le
shell l'emploie pour distinguer les différents mots de la ligne de
commande. Cette variable contient les caractères de séparation à considérer.
Par défaut, il s'agit de l'espace, la tabulation et le
retour-chariot. Si un utilisateur y ajoute le caractère slash /
,
la commande "/bin/mail
" est interprétée par le shell comme
"bin mail
". Un fichier exécutable nommé
bin
dans le répertoire en cours et une modification du PATH
,
comme nous l'avons vu précédemment permettent à ce programme d'être lancé avec l'UID
effectif de l'application.
En réalité, sous Linux, la variable d'environnement IFS
ne pose
plus de problèmes, car bash la remplit automatiquement avec les caractères
par défaut dès son démarrage (comme pdksh également). Mais pour conserver
une certaine portabilité à l'application, il faut prévoir que d'autres
systèmes peuvent être moins sûrs vis-à-vis de cette variable.
D'autres variables d'environnement posent parfois des problèmes inattendus.
Par exemple l'application mail
autorise l'utilisateur à lancer
une commande lors de la rédaction d'un message en employant une séquence
d'échappement "~!
". Si l'utilisateur entre en début de
ligne la chaîne "~!commande
", la commande indiquée est
invoquée. D'autre part l'application /usr/bin/suidperl
qui sert
à faire fonctionner des scripts Perl en version Set-UID appelle, lorsqu'elle
détecte un problème, /bin/mail
pour envoyer un message à
root. L'application étant Set-UID root, l'appel de
/bin/mail
se fait bien entendu sous cette identité. Dans le message
transmis à root, le nom du fichier posant un problème est
présent. Un utilisateur peut donc créer un fichier dont le nom contient
un retour-chariot suivi d'une séquence ~!commande
suivi à
nouveau d'un retour-chariot. Si un script Perl qui appelle
suidperl
échoue sur un problème bas-niveau lié à ce fichier,
un message est émis sous l'identité root, contenant la séquence
d'échappement de l'application mail
.
Ce problème ne devrait théoriquement pas exister car le programme
mail
n'accepte pas les séquences d'échappement lorsqu'il est
invoqué de manière automatique (sans être piloté depuis un terminal).
Malheureusement, une fonctionnalité non documentée de cette application
(probablement un reste d'une option de débogage), veut que si la variable
d'environnement interactive
est définie, les séquences soient
autorisées. Résultat ? Une faille de sécurité facilement exploitable
(et largement exploitée) dans une application justement censée améliorer la
sûreté du système. La faute en est partagée. D'abord /bin/mail
contient une option non documentée et fondamentalement dangereuse
puisqu'elle autorise l'exécution de code sous l'unique contrôle des
données transmises, et qui, pour un utilitaire de courrier
électronique, sont a priori suspectes. En second lieu, même
si les développeurs de /usr/bin/suidperl
ignoraient
l'existence de la variable interactive
, ils n'auraient
jamais dû
laisser l'environnement d'exécution intact au moment d'appeler une commande
externe lors de l'écriture de ce programme Set-UID root.
En fait, Linux ignore totalement les bits Set-UID et Set-GID lors de
l'exécution de scripts (voir pour cela
/usr/src/linux/fs/binfmt_script.c
et
/usr/src/linux/fs/exec.c
). Des artifices permettent
toutefois de contourner cette règle, comme Perl le fait si bien avec
ses propres scripts en passant par
/usr/bin/suidperl
pour honorer indirectement ces bits.
Il n'est pas toujours facile de trouver un remplacement pour la fonction
system()
. La première possibilité est d'employer directement
les appels-système execl()
ou execle()
. Toutefois,
la sémantique diffère totalement puisque le programme externe n'est
plus invoqué comme une sous-routine, mais la commande appelée
remplace le processus en cours. Il est nécessaire de rajouter une
duplication du processus, et de séparer nous-mêmes les arguments de
la ligne de commande. Ainsi, le programme :
devient :if (system ("/bin/lpr -Plisting stats.txt") != 0) { perror ("Impression"); return (-1); }
Le code s'allourdit quand même sensiblement ! Dans certaines situations, l'écriture est franchement complexe, par exemple lorsqu'il s'agit de rediriger l'entrée standard de l'application exécutée comme dans :pid_t pid; int status; if ((pid = fork()) < 0) { perror("fork"); return (-1); } if (pid == 0) { /* processus fils */ execl ("/bin/lpr", "lpr", "-Plisting", "stats.txt", NULL); perror ("execl"); exit (-1); } /* processus père */ waitpid (pid, & status, 0); if ((! WIFEXITED (status)) || (WEXITSTATUS (status) != 0)) { perror ("Impression"); return (-1); }
En effet, la redirection imposée parsystem ("mail root < stat.txt");
<
est établie par le
shell. Il est possible de réaliser la même opération, mais au prix d'une
manipulation complexe autour d'une séquence fork()
,
open()
, dup2()
, execl()
, etc. Dans
ce cas, une solution acceptable est d'employer quand même la fonction
system()
, mais en configurant entièrement l'environnement.
Sous Linux, les variables d'environnement sont stockées sous forme
d'un tableau de pointers sur des caractères : char **
environ
. Ce tableau se termine par NULL. Les chaînes de
caractères sont de la forme "NOM=valeur
".
On commence par effacer tout le contenu de l'environnement en utilisant l'extension Gnu :
ou en forçant le pointeurint clearenv (void);
à prendre la valeur NULL. Puis les variables d'environnement importantes sont initialisées, en utilisant des valeurs bien contrôlées, à l'aide des fonctionsextern char ** environ;
avant d'appeler la fonctionint setenv (const char * nom, const char * valeur, int ecrase) int putenv(const char *string)
system()
. Par
exemple :
Au besoin, on récupère le contenu de certaines variables utiles avant d'effacer l'environnement (clearenv (); setenv ("PATH", "/bin:/usr/bin:/usr/local/bin", 1); setenv ("IFS", " \t\n", 1); system ("mail root < /tmp/msg.txt");
HOME
, LANG
,
TERM
, TZ
,etc.). Une vérification
stricte du contenu, de
la forme, la taille de ces variables s'impose nécessairement. Insistons
encore une fois sur l'effacement impératif de tout l'environnement avant
de reconstruire les variables indispensables. La faille de sécurité
suidperl
n'aurait jamais existé si l'environnement avait été
bien effacé.
Par analogie, la protection d'une machine sur un réseau passe dans un premier temps par un refus systématique de toutes les connexions qui lui sont destinées. Ensuite, seuls les services nécessaires à son bon fonctionnement ou utiles pour le réseau sont activés. De la même manière, lors de la programmation d'une application Set-UID, l'environnement doit être vidé puis rempli uniquement avec les variables indispensables.
Dans la continuité, la vérification du format d'un paramètre se fait en comparant la valeur candidate aux formats autorisés. Si la comparaison réussit, le paramètre est validé. Dans le cas contraire, il est immédiatement rejeté. Si le test est fait en utilisant une liste d'expressions invalides du format, le risque de laisser passer une valeur malformée s'accroît, ce qui peut avoir des conséquences désastreuses sur le système.
Il faut remarquer que ce qui présente des dangers avec system()
en présente tout autant avec certaines fonctions dérivées comme
popen()
, ou même avec les appels-systèmes execlp()
ou execvp()
qui prennent en charge la variable
PATH
.
Pour améliorer l'ergonomie d'un programme, il est pratique de laisser
l'utilisateur libre de configurer une partie importante du comportement
du logiciel par le biais de macros par exemple. Pour manipuler des
variables et des motifs génériques comme le shell le fait habituellement,
il existe une fonction très puissante nommée wordexp()
.
Elle nécessite toutefois une grande prudence, car la transmission d'une chaîne
du type $(commande)
permet l'exécution de la
commande externe mentionnée. Il suffit de lui passer la chaîne
"$(/bin/sh)
" pour disposer instantanément d'un shell Set-UID.
Afin d'éviter cela wordexp()
dispose d'un attribut nommé
WRDE_NOCMD
qui désactive l'interprétation des séquences
$( )
.
Il faut également prendre garde, lors de l'invocation de commandes externes
telles que nous les avons vues plus haut, à ne pas appeler un utilitaire
qui à son tour offre un mécanisme d'échappement vers un shell (comme
les séquences :!commande
de vi par exemple). Il est
difficile d'en faire la liste, certaines applications sont évidentes
(éditeurs de texte, gestionnaire de fichiers...) d'autres s'avèrent plus
difficile à déceler (comme nous l'avons vu avec /bin/mail
) ou
disposer de modes de débogage dangereux.
Cet article illustre plusieurs préceptes :
Le prochain article traitera de la mémoire, de son organisation, des appels de fonctions ... tout cela pour préparer le terrain aux débordements de buffer (buffer overflows). Nous verrons également comment construire un shellcode.
Christophe BLAESS - ccb@club-internet.fr Christophe GRENIER - grenier@cgsecurity.org Frédéreric RAYNAL - pappy@users.sourceforge.net Last modified: Thu Jan 4 15:57:19 CET 2001