Introduction
Un programme privilégié possède davantage de droits que l’utilisateur qui le lance.
C’est notamment le cas des programmes :
Code: Select all
setuid
setgid
Code: Select all
UID réel
UID effectif
UID sauvegardé
setuid()
seteuid()
setresuid()
Privilèges des processus Linux : UID, EUID, SUID et sécurité
Nous ne les reprendrons donc ici que très brièvement.
Le but de ce cours est de comprendre :
- comment réduire les risques liés aux programmes privilégiés ;
- comment éviter de donner tous les droits de root ;
- comment utiliser les capabilities Linux.
1. Principe du moindre privilège
Un programme doit posséder uniquement les droits nécessaires et uniquement pendant la durée nécessaire.
Un programme privilégié doit donc :
- effectuer le moins de code possible avec des privilèges élevés ;
- abandonner temporairement ses privilèges pendant les opérations ordinaires ;
- les supprimer définitivement lorsqu’ils ne sont plus utiles ;
- toujours vérifier les valeurs de retour.
Code: Select all
if (seteuid(getuid()) == -1)
{
perror("seteuid");
return 1;
}
Pour abandonner définitivement les privilèges, on peut utiliser une fonction comme :
Code: Select all
if (setresuid(getuid(), getuid(), getuid()) == -1)
{
perror("setresuid");
return 1;
}
2. Ne jamais faire confiance aux entrées
Dans un programme privilégié, toutes les données externes doivent être considérées comme potentiellement dangereuses :
- arguments de la ligne de commande ;
- variables d’environnement ;
- noms de fichiers ;
- fichiers de configuration ;
- données reçues depuis le réseau.
- vérifier les tailles ;
- refuser les valeurs invalides ;
- éviter les dépassements de buffers ;
- ne jamais utiliser directement une entrée utilisateur dans une commande shell.
3. Éviter system() et popen()
Les fonctions :
Code: Select all
system()
popen()
Elles sont dangereuses dans un programme privilégié car une entrée mal contrôlée peut provoquer une injection de commandes.
Exemple dangereux :
Code: Select all
char commande[256];
snprintf(commande, sizeof(commande), "cat %s", argv[1]);
system(commande);
Il faut préférer une fonction de la famille :
Code: Select all
execve()
execv()
execl()
Code: Select all
char *args[] =
{
"/usr/bin/cat",
argv[1],
NULL
};
execve("/usr/bin/cat", args, environnement);
- un chemin absolu ;
- un tableau d’arguments séparés ;
- un environnement contrôlé.
4. Contrôler l’environnement
Un programme privilégié ne doit pas faire confiance aux variables d’environnement héritées.
Les variables à surveiller comprennent notamment :
Code: Select all
PATH
IFS
LD_PRELOAD
LD_LIBRARY_PATH
Exemple :
Code: Select all
char *envp[] =
{
"PATH=/usr/bin:/bin",
"LANG=C",
NULL
};
Code: Select all
execve("/usr/bin/programme", args, envp);
5. Sécuriser les opérations sur les fichiers
Une erreur classique consiste à vérifier un fichier puis à l’ouvrir plus tard.
Exemple dangereux :
Code: Select all
stat(path, &st);
open(path, O_RDONLY);
C’est une attaque :
Code: Select all
TOCTOU
Time Of Check To Time Of Use
Code: Select all
int fd = open(path, O_RDONLY | O_NOFOLLOW);
if (fd == -1)
{
perror("open");
return 1;
}
if (fstat(fd, &st) == -1)
{
perror("fstat");
close(fd);
return 1;
}
Code: Select all
open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
Code: Select all
char modele[] = "/tmp/programmeXXXXXX";
int fd = mkstemp(modele);
6. Fermer les descripteurs sensibles avant exec()
Un programme exécuté avec exec() peut hériter des descripteurs ouverts.
Il faut donc fermer les descripteurs qui ne doivent pas être transmis.
On peut aussi utiliser :
Code: Select all
O_CLOEXEC
Code: Select all
int fd = open(path, O_RDONLY | O_CLOEXEC);
7. Vérifier toutes les erreurs
Chaque appel système peut échouer.
Un programme privilégié doit toujours vérifier les retours de fonctions comme :
Code: Select all
open()
read()
write()
close()
setuid()
seteuid()
setresuid()
execve()
fstat()
mkstemp()
8. Les capabilities Linux
Historiquement, Linux séparait principalement les processus en deux catégories :
- root avec presque tous les privilèges ;
- les utilisateurs ordinaires.
Au lieu de donner tous les droits de root, on peut accorder uniquement le privilège nécessaire.
Exemple :
Code: Select all
CAP_NET_BIND_SERVICE
9. Capabilities importantes
Quelques capabilities courantes :
Code: Select all
CAP_NET_BIND_SERVICE
CAP_NET_ADMIN
CAP_NET_RAW
CAP_SYS_PTRACE
CAP_SYS_TIME
CAP_SETUID
CAP_SETGID
CAP_CHOWN
CAP_DAC_OVERRIDE
CAP_SYS_ADMIN
CAP_SYS_ADMIN est extrêmement large. Elle donne accès à de nombreuses opérations sensibles et doit être évitée lorsque qu’une capability plus précise existe.
10. Ensembles de capabilities d’un processus
Un processus possède plusieurs ensembles.
Permitted
Les capabilities que le processus est autorisé à activer.
Effective
Les capabilities actuellement actives et utilisées par le noyau pour les contrôles.
Inheritable
Les capabilities susceptibles de participer à la transmission de privilèges lors d’un exec().
Règle essentielle :
Code: Select all
Une capability doit être dans permitted
pour pouvoir être activée dans effective.
11. Capabilities associées à un exécutable
La commande getcap affiche les capabilities d’un fichier :
Code: Select all
getcap ./programme
Code: Select all
sudo setcap 'cap_net_bind_service=ep' ./programme
Code: Select all
e = effective
p = permitted
Code: Select all
sudo setcap -r ./programme
12. API libcap
L’API principale se trouve dans :
Code: Select all
#include <sys/capability.h>
Code: Select all
cap_get_proc()
cap_set_proc()
cap_init()
cap_set_flag()
cap_get_flag()
cap_free()
Code: Select all
gcc programme.c -o programme -lcap
13. Lire les capabilities du processus
cap_get_proc() récupère les capabilities actuelles du processus.
Code: Select all
cap_t caps = cap_get_proc();
if (caps == NULL)
{
perror("cap_get_proc");
return 1;
}
Code: Select all
cap_free(caps);
14. Activer ou désactiver une capability
cap_set_flag() modifie un ensemble dans une structure cap_t.
Exemple pour activer CAP_NET_BIND_SERVICE dans l’ensemble effectif :
Code: Select all
cap_value_t valeur = CAP_NET_BIND_SERVICE;
if (cap_set_flag(
caps,
CAP_EFFECTIVE,
1,
&valeur,
CAP_SET) == -1)
{
perror("cap_set_flag");
cap_free(caps);
return 1;
}
Il faut ensuite appeler :
Code: Select all
if (cap_set_proc(caps) == -1)
{
perror("cap_set_proc");
cap_free(caps);
return 1;
}
Code: Select all
cap_set_flag(
caps,
CAP_EFFECTIVE,
1,
&valeur,
CAP_CLEAR
);
15. Supprimer toutes les capabilities
cap_init() crée un ensemble vide.
On peut l’appliquer au processus pour supprimer ses capabilities :
Code: Select all
cap_t vide = cap_init();
if (vide == NULL)
{
perror("cap_init");
return 1;
}
if (cap_set_proc(vide) == -1)
{
perror("cap_set_proc");
cap_free(vide);
return 1;
}
cap_free(vide);
16. prctl() et conservation des capabilities
Lorsqu’un processus quitte l’UID 0, Linux supprime normalement certaines capabilities.
La commande :
Code: Select all
prctl(PR_SET_KEEPCAPS, 1);
Le principe peut être :
Code: Select all
1. démarrer avec les privilèges nécessaires ;
2. demander la conservation des capabilities ;
3. abandonner l’UID root ;
4. conserver uniquement les capabilities utiles ;
5. supprimer les autres.
17. Le capability bounding set
Le bounding set limite les capabilities qu’un processus et ses descendants pourront acquérir lors d’un exec().
On peut en retirer une capability avec :
Code: Select all
prctl(PR_CAPBSET_DROP, CAP_SYS_ADMIN);
Cela permet de réduire définitivement les privilèges futurs d’une branche de processus.
18. Exemple de stratégie sécurisée
Un programme privilégié devrait suivre cette logique :
Code: Select all
1. démarrer avec le minimum de privilèges ;
2. vérifier toutes les entrées ;
3. activer une capability uniquement avant l’opération sensible ;
4. effectuer l’opération ;
5. retirer immédiatement la capability effective ;
6. supprimer définitivement les privilèges devenus inutiles ;
7. continuer comme utilisateur ordinaire.
19. API et commandes à retenir
Sécurité des programmes privilégiés
Code: Select all
seteuid()
setuid()
setresuid()
execve()
open()
fstat()
mkstemp()
Code: Select all
cap_get_proc()
cap_get_flag()
cap_set_flag()
cap_set_proc()
cap_init()
cap_free()
prctl()
Code: Select all
getcap
setcap
20. Ce qu’il faut retenir
- Un programme privilégié est une cible sensible.
- Il doit exécuter le moins de code possible avec des privilèges élevés.
- Il ne doit jamais faire confiance aux arguments, fichiers ou variables d’environnement.
- system() et popen() doivent être évités dans un programme privilégié.
- Il faut utiliser execve() avec un chemin absolu et un environnement contrôlé.
- Il faut éviter les courses TOCTOU avec open() puis fstat().
- Les capabilities permettent de donner un privilège précis sans donner tous les droits de root.
- permitted représente les droits disponibles.
- effective représente les droits actuellement actifs.
- libcap permet de consulter et modifier les capabilities d’un processus.
- Les privilèges doivent être supprimés dès qu’ils ne sont plus nécessaires.
Un programme sécurisé doit posséder le moins de privilèges possible, pendant le moins de temps possible.
