Sécuriser les programmes privilégiés et utiliser les capabilities sous Linux

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Sécuriser les programmes privilégiés et utiliser les capabilities sous Linux

Post by Hydraxx »

Sécuriser les programmes privilégiés et utiliser les capabilities sous Linux

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
Les notions de :

Code: Select all

UID réel
UID effectif
UID sauvegardé
setuid()
seteuid()
setresuid()
ont déjà été étudiées dans le cours précédent :

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.
Exemple d’abandon temporaire :

Code: Select all

if (seteuid(getuid()) == -1)
{
    perror("seteuid");
    return 1;
}
Le programme peut parfois récupérer son UID effectif privilégié grâce à son UID sauvegardé.

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;
}
Ces mécanismes sont détaillés dans le cours précédent.


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.
Il faut notamment :
  • 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()
lancent un interpréteur de commandes.

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);
Un utilisateur pourrait fournir des caractères spéciaux interprétés par le shell.

Il faut préférer une fonction de la famille :

Code: Select all

execve()
execv()
execl()
Exemple :

Code: Select all

char *args[] =
{
    "/usr/bin/cat",
    argv[1],
    NULL
};

execve("/usr/bin/cat", args, environnement);
Il faut utiliser :
  • 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
Une bonne méthode consiste à fournir un environnement minimal à execve().

Exemple :

Code: Select all

char *envp[] =
{
    "PATH=/usr/bin:/bin",
    "LANG=C",
    NULL
};
Puis :

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);
Entre stat() et open(), un attaquant peut remplacer le fichier.

C’est une attaque :

Code: Select all

TOCTOU
Time Of Check To Time Of Use
Il faut plutôt ouvrir le fichier puis vérifier l’objet réellement ouvert avec fstat() :

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;
}
Pour créer un fichier sans écraser un fichier existant :

Code: Select all

open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
Pour créer un fichier temporaire sécurisé :

Code: Select all

char modele[] = "/tmp/programmeXXXXXX";

int fd = mkstemp(modele);
mkstemp() crée directement un fichier unique et l’ouvre, ce qui limite les courses entre processus.


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
Exemple :

Code: Select all

int fd = open(path, O_RDONLY | O_CLOEXEC);
Le descripteur sera alors automatiquement fermé lors d’un exec() réussi.


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()
Le programme ne doit jamais continuer avec un état de sécurité incertain.


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.
Les capabilities découpent les privilèges de root en droits plus précis.

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
Cette capability permet notamment d’ouvrir un port privilégié sans rendre le programme setuid root.


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
Attention :

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
La commande setcap permet d’en attribuer :

Code: Select all

sudo setcap 'cap_net_bind_service=ep' ./programme
Ici :

Code: Select all

e = effective
p = permitted
Pour les supprimer :

Code: Select all

sudo setcap -r ./programme
Cette méthode permet souvent d’éviter un exécutable setuid root.


12. API libcap

L’API principale se trouve dans :

Code: Select all

#include <sys/capability.h>
Les fonctions essentielles sont :

Code: Select all

cap_get_proc()
cap_set_proc()
cap_init()
cap_set_flag()
cap_get_flag()
cap_free()
Pour compiler un programme utilisant libcap :

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;
}
La structure doit ensuite être libérée :

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;
}
La modification n’est pas encore appliquée au processus.

Il faut ensuite appeler :

Code: Select all

if (cap_set_proc(caps) == -1)
{
    perror("cap_set_proc");
    cap_free(caps);
    return 1;
}
Pour désactiver la capability :

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);
Cette opération est utile lorsque le programme a terminé toutes ses opérations privilégiées.


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);
permet de conserver certaines capabilities après un changement d’UID.

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.
Cette opération reste sensible et doit être utilisée avec prudence.


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);
Après son retrait, le processus ne pourra normalement plus récupérer cette capability par l’exécution d’un programme.

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()
Capabilities

Code: Select all

cap_get_proc()
cap_get_flag()
cap_set_flag()
cap_set_proc()
cap_init()
cap_free()
prctl()
Commandes

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.
Phrase à retenir :
Un programme sécurisé doit posséder le moins de privilèges possible, pendant le moins de temps possible.

Who is online

Users browsing this forum: No registered users and 1 guest