Utilisateurs et groupes sous Linux : UID, GID, passwd et permissions

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

Moderator: Rick

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

Utilisateurs et groupes sous Linux : UID, GID, passwd et permissions

Post by Hydraxx »

Utilisateurs, groupes et authentification sous Linux

Cours orienté développement système Linux — identité utilisateur, UID/GID, fichiers passwd/shadow/group, APIs libc, enumeration et authentification.
  • Objectif du chapitre : comprendre comment Linux représente les utilisateurs, les groupes, les permissions de base et comment un programme C peut interroger ces informations.
  • Niveau : user-mode Linux / programmation système.
  • Pré-requis utiles : fichiers, processus, permissions Unix, appels système de base.
Idée principale du chapitre

Sous Linux, chaque utilisateur possède une identité numérique appelée UID, et chaque groupe possède une identité numérique appelée GID.

Ce sont ces identifiants numériques, et non les noms lisibles comme root, jean, www-data, qui sont utilisés par le noyau pour contrôler les permissions.

Un nom d'utilisateur est surtout une représentation lisible pour l'humain. Le noyau, lui, travaille surtout avec des nombres.

Code: Select all

root  -> UID 0
jean  -> UID 1000
www-data -> UID 33
Même chose pour les groupes :

Code: Select all

root  -> GID 0
users -> GID 100
sudo  -> GID variable selon distribution
Comparaison mentale avec Windows

Pour quelqu'un qui vient du monde Win32/NT, il faut faire ce parallèle :
  • Linux : UID / GID
  • Windows : SID utilisateur / SID de groupes
  • Linux : appartenance à des groupes
  • Windows : token d'accès contenant des groupes
  • Linux : /etc/passwd, /etc/group, /etc/shadow
  • Windows : SAM, Active Directory, LSA, tokens
  • Linux : permissions rwx
  • Windows : ACL / ACE / Security Descriptor
Sous Windows, un processus possède un Access Token.
Sous Linux, un processus possède notamment des IDs utilisateur/groupe : UID réel, UID effectif, GID réel, GID effectif, groupes supplémentaires, etc.

Le chapitre 8 introduit surtout la partie "base de données utilisateurs/groupes". Le chapitre suivant va souvent plus loin sur les IDs réels/effectifs et les changements d'identité.



1. Pourquoi Linux a besoin d'utilisateurs et de groupes ?

Un système multi-utilisateur doit pouvoir répondre à des questions simples :
  • Qui exécute ce programme ?
  • À qui appartient ce fichier ?
  • Ce processus a-t-il le droit de lire ce fichier ?
  • Ce processus a-t-il le droit d'écrire dans ce dossier ?
  • Cet utilisateur fait-il partie du groupe autorisé ?
  • Est-ce que cet utilisateur est administrateur ?
Linux répond à ça principalement avec :
  • un UID pour l'utilisateur ;
  • un GID pour le groupe principal ;
  • une liste de groupes supplémentaires ;
  • des fichiers de configuration système ;
  • des permissions sur les fichiers.
Quand un fichier existe sur le disque, il possède un propriétaire et un groupe :

Code: Select all

-rw-r--r-- 1 jean jean 1234 May 14 notes.txt
Ici :
  • le propriétaire est jean ;
  • le groupe est jean ;
  • les permissions sont rw-r--r--.
Mais en interne, le système ne stocke pas forcément "jean" comme texte. Il stocke surtout l'UID et le GID.

Code: Select all

stat notes.txt
peut afficher les IDs numériques :

Code: Select all

Uid: ( 1000/ jean)
Gid: ( 1000/ jean)
Point important

Le nom utilisateur est une couche de confort.
L'identité réelle pour le noyau, c'est le nombre.



2. Le fichier /etc/passwd

Le fichier /etc/passwd contient la base historique des comptes utilisateurs.

Il est généralement lisible par les utilisateurs normaux :

Code: Select all

cat /etc/passwd
Exemple de ligne :

Code: Select all

jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
Le format est composé de 7 champs séparés par des deux-points :

Code: Select all

login:password:uid:gid:comment:home:shell
Chaque champ a un rôle précis.

Champ 1 : login name

C'est le nom de connexion de l'utilisateur.

Code: Select all

jean
root
www-data
nobody
C'est ce que l'utilisateur tape généralement pour se connecter.

Champ 2 : password

Historiquement, ce champ contenait le mot de passe chiffré/hashé.

Aujourd'hui, sur les systèmes modernes, il contient souvent :

Code: Select all

x
Cela signifie que le mot de passe réel n'est pas stocké ici, mais dans :

Code: Select all

/etc/shadow
Exemple :

Code: Select all

jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
Le x indique que le hash du mot de passe est dans le fichier shadow.

Champ 3 : UID

C'est l'identifiant numérique utilisateur.

Code: Select all

root:x:0:0:root:/root:/bin/bash
jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
Ici :
  • root possède UID 0 ;
  • jean possède UID 1000.
L'UID 0 est spécial.

Code: Select all

UID 0 = superutilisateur
Ce n'est pas le nom "root" qui donne les privilèges ultimes.
C'est le fait d'avoir UID 0.

Donc techniquement, un compte nommé autrement mais avec UID 0 serait aussi privilégié.

Champ 4 : GID

C'est le groupe principal de l'utilisateur.

Exemple :

Code: Select all

jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
Ici :
  • UID = 1000
  • GID principal = 1000
Le détail du groupe est ensuite consultable dans :

Code: Select all

/etc/group
Champ 5 : commentaire / GECOS

Ce champ contient des informations textuelles sur l'utilisateur.

Exemple :

Code: Select all

Jean Michel
Michael Kerrisk
Service Account
Il peut contenir un nom complet, un bureau, un numéro de téléphone, etc.
Aujourd'hui, il est souvent peu utilisé ou simplement utilisé pour le nom complet.

Champ 6 : home directory

C'est le répertoire personnel initial de l'utilisateur.

Exemple :

Code: Select all

/home/jean
/root
/var/www
/nonexistent
Quand l'utilisateur se connecte, son environnement peut utiliser ce chemin pour définir :

Code: Select all

HOME=/home/jean
Champ 7 : login shell

C'est le programme lancé comme shell lors de la connexion.

Exemples :

Code: Select all

/bin/bash
/bin/sh
/usr/bin/zsh
/usr/sbin/nologin
/bin/false
Un utilisateur normal peut avoir :

Code: Select all

/bin/bash
Un compte de service peut avoir :

Code: Select all

/usr/sbin/nologin
Cela signifie que le compte existe, mais qu'il n'est pas censé ouvrir une session interactive.

Exemple complet analysé

Code: Select all

mtk:x:1000:100:Michael Kerrisk:/home/mtk:/bin/bash
Analyse :
  • mtk : nom de connexion.
  • x : mot de passe stocké dans /etc/shadow.
  • 1000 : UID.
  • 100 : GID principal.
  • Michael Kerrisk : commentaire / nom complet.
  • /home/mtk : répertoire personnel.
  • /bin/bash : shell de connexion.
À retenir sur /etc/passwd
  • /etc/passwd ne veut pas dire qu'il contient encore les mots de passe.
  • Il contient surtout les informations publiques des comptes.
  • Il est généralement lisible par tous.
  • Les mots de passe sensibles sont dans /etc/shadow.
  • Les champs sont séparés par :.

────────────────────────────────────────


3. Le fichier /etc/shadow

Le fichier /etc/shadow contient les informations sensibles liées aux mots de passe.

Contrairement à /etc/passwd, il n'est normalement pas lisible par un utilisateur standard.

Code: Select all

cat /etc/shadow
donnera souvent :

Code: Select all

Permission denied
Il faut des privilèges élevés pour le lire.

Exemple de ligne shadow :

Code: Select all

jean:$y$j9T$abcdef...:19800:0:99999:7:::
Le format général ressemble à :

Code: Select all

login:encrypted-password:last-change:min:max:warn:inactive:expire:reserved
Champ 1 : login

Nom de l'utilisateur correspondant à /etc/passwd.

Code: Select all

jean
root
daemon
Champ 2 : encrypted password

C'est le hash du mot de passe.

Il peut ressembler à ceci :

Code: Select all

$6$salt$hash
$y$j9T$salt$hash
$2y$salt$hash
Selon l'algorithme utilisé.

Ce n'est pas le mot de passe en clair.

Champ 3 : last change

Nombre de jours depuis le 1er janvier 1970 où le mot de passe a été changé.

Champ 4 : min

Nombre minimal de jours avant qu'un changement de mot de passe soit autorisé.

Champ 5 : max

Nombre maximal de jours pendant lesquels le mot de passe reste valide.

Champ 6 : warn

Nombre de jours avant expiration où l'utilisateur est averti.

Champ 7 : inactive

Nombre de jours après expiration où le compte devient inactif.

Champ 8 : expire

Date d'expiration absolue du compte, en jours depuis le 1er janvier 1970.

Champ 9 : reserved

Champ réservé.

Pourquoi /etc/shadow existe ?

Historiquement, les hashes étaient dans /etc/passwd.
Problème : /etc/passwd doit être lisible par tout le monde pour que les programmes puissent convertir les UID en noms.

Donc n'importe quel utilisateur pouvait lire les hashes et tenter une attaque hors-ligne.

La solution moderne :
  • /etc/passwd reste lisible ;
  • /etc/shadow contient les hashes ;
  • /etc/shadow est réservé à root ou à un groupe privilégié.
Résumé mental

Code: Select all

/etc/passwd = informations publiques des comptes
/etc/shadow = informations sensibles de mot de passe




4. Le fichier /etc/group

Le fichier /etc/group contient les groupes du système.

Exemple :

Code: Select all

sudo:x:27:jean
developers:x:1001:jean,paul,marie
www-data:x:33:
Format :

Code: Select all

group-name:password:gid:user-list
Champ 1 : group name

Nom du groupe.

Exemples :

Code: Select all

sudo
users
developers
docker
www-data
Champ 2 : password

Champ historique pour un mot de passe de groupe.
Aujourd'hui, il est généralement inutilisé et contient souvent :

Code: Select all

x
ou rien d'important.

Champ 3 : GID

Identifiant numérique du groupe.

Exemple :

Code: Select all

sudo:x:27:jean
Ici, le groupe sudo possède le GID 27.

Champ 4 : user list

Liste des utilisateurs membres supplémentaires du groupe.

Exemple :

Code: Select all

developers:x:1001:jean,paul,marie
Cela signifie que jean, paul et marie sont membres du groupe developers.

Attention au groupe principal

Le groupe principal d'un utilisateur est indiqué dans /etc/passwd par le champ GID.

Mais les groupes supplémentaires sont indiqués dans /etc/group.

Exemple :

Code: Select all

/etc/passwd:
jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash

/etc/group:
sudo:x:27:jean
docker:x:999:jean
developers:x:1001:jean
Ici :
  • groupe principal de jean : GID 1000 ;
  • groupes supplémentaires : sudo, docker, developers.
Commande utile

Pour voir les groupes de l'utilisateur courant :

Code: Select all

groups
ou :

Code: Select all

id
Exemple :

Code: Select all

uid=1000(jean) gid=1000(jean) groups=1000(jean),27(sudo),999(docker)


5. Les types C importants : uid_t et gid_t

En C, les UID et GID sont représentés par des types dédiés :

Code: Select all

uid_t
gid_t
Ils sont généralement disponibles via :

Code: Select all

#include <sys/types.h>
#include <unistd.h>
Exemple :

Code: Select all

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    uid_t uid = getuid();
    gid_t gid = getgid();

    printf("UID = %ld\n", (long) uid);
    printf("GID = %ld\n", (long) gid);

    return 0;
}
Compilation :

Code: Select all

gcc main.c -o test
./test
Sortie possible :

Code: Select all

UID = 1000
GID = 1000
Pourquoi caster en long ?

Parce que le type exact de uid_t/gid_t peut dépendre du système.
Pour l'affichage portable, on caste souvent en long.




6. La structure struct passwd

Les informations d'un utilisateur sont représentées par :

Code: Select all

#include <pwd.h>

struct passwd {
    char   *pw_name;    /* Login name */
    char   *pw_passwd;  /* Encrypted password, or placeholder */
    uid_t   pw_uid;     /* User ID */
    gid_t   pw_gid;     /* Group ID */
    char   *pw_gecos;   /* User information */
    char   *pw_dir;     /* Home directory */
    char   *pw_shell;   /* Login shell */
};
Champs importants
  • pw_name : nom utilisateur.
  • pw_passwd : champ mot de passe historique ou placeholder.
  • pw_uid : UID.
  • pw_gid : GID principal.
  • pw_gecos : commentaire / nom complet.
  • pw_dir : répertoire personnel.
  • pw_shell : shell de connexion.
Exemple : chercher un utilisateur par nom

API :

Code: Select all

struct passwd *getpwnam(const char *name);
Elle cherche un utilisateur à partir de son nom.

Exemple :

Code: Select all

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct passwd *pwd;

    pwd = getpwnam("root");

    if (pwd == NULL) {
        printf("Utilisateur introuvable\n");
        return EXIT_FAILURE;
    }

    printf("Nom      : %s\n", pwd->pw_name);
    printf("UID      : %ld\n", (long) pwd->pw_uid);
    printf("GID      : %ld\n", (long) pwd->pw_gid);
    printf("Home     : %s\n", pwd->pw_dir);
    printf("Shell    : %s\n", pwd->pw_shell);

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc main.c -o get_user
./get_user
Sortie possible :

Code: Select all

Nom      : root
UID      : 0
GID      : 0
Home     : /root
Shell    : /bin/bash
Exemple : chercher un utilisateur par UID

API :

Code: Select all

struct passwd *getpwuid(uid_t uid);
Exemple :

Code: Select all

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
    uid_t uid;
    struct passwd *pwd;

    uid = getuid();
    pwd = getpwuid(uid);

    if (pwd == NULL) {
        printf("Aucun utilisateur trouvé pour cet UID\n");
        return EXIT_FAILURE;
    }

    printf("UID courant : %ld\n", (long) uid);
    printf("Nom         : %s\n", pwd->pw_name);
    printf("Home        : %s\n", pwd->pw_dir);

    return EXIT_SUCCESS;
}
Différence entre getpwnam et getpwuid
  • getpwnam("jean") : cherche par nom.
  • getpwuid(1000) : cherche par UID.
C'est exactement le genre de fonction de conversion entre identité lisible et identité numérique.



7. La structure struct group

Les informations d'un groupe sont représentées par :

Code: Select all

#include <grp.h>

struct group {
    char   *gr_name;    /* Group name */
    char   *gr_passwd;  /* Group password */
    gid_t   gr_gid;     /* Group ID */
    char  **gr_mem;     /* NULL-terminated array of member names */
};
Champs importants
  • gr_name : nom du groupe.
  • gr_passwd : champ historique de mot de passe groupe.
  • gr_gid : identifiant numérique du groupe.
  • gr_mem : tableau de chaînes contenant les membres.
Exemple : chercher un groupe par nom

API :

Code: Select all

struct group *getgrnam(const char *name);
Exemple :

Code: Select all

#include <grp.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct group *grp;
    char **member;

    grp = getgrnam("sudo");

    if (grp == NULL) {
        printf("Groupe introuvable\n");
        return EXIT_FAILURE;
    }

    printf("Nom du groupe : %s\n", grp->gr_name);
    printf("GID           : %ld\n", (long) grp->gr_gid);

    printf("Membres :\n");
    for (member = grp->gr_mem; *member != NULL; member++) {
        printf(" - %s\n", *member);
    }

    return EXIT_SUCCESS;
}
Exemple : chercher un groupe par GID

API :

Code: Select all

struct group *getgrgid(gid_t gid);
Exemple :

Code: Select all

#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
    gid_t gid;
    struct group *grp;

    gid = getgid();
    grp = getgrgid(gid);

    if (grp == NULL) {
        printf("Aucun groupe trouvé\n");
        return EXIT_FAILURE;
    }

    printf("GID courant : %ld\n", (long) gid);
    printf("Nom groupe  : %s\n", grp->gr_name);

    return EXIT_SUCCESS;
}
Attention à gr_mem

Le champ gr_mem est un tableau de pointeurs vers des chaînes.

Il se termine par NULL.

Il faut donc le parcourir comme ceci :

Code: Select all

char **p;

for (p = grp->gr_mem; *p != NULL; p++) {
    printf("%s\n", *p);
}


8. Les fonctions getpwent(), setpwent(), endpwent()

Ces fonctions permettent de scanner toutes les entrées de la base utilisateurs.

Elles fonctionnent comme une lecture séquentielle.

Prototypes

Code: Select all

#include <pwd.h>

struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
Rôle
  • setpwent() : remet le curseur au début de la base passwd.
  • getpwent() : lit l'entrée suivante.
  • endpwent() : ferme/libère la lecture.
Exemple : lister tous les utilisateurs

Code: Select all

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct passwd *pwd;

    setpwent();

    while ((pwd = getpwent()) != NULL) {
        printf("%-20s UID=%ld GID=%ld HOME=%s SHELL=%s\n",
               pwd->pw_name,
               (long) pwd->pw_uid,
               (long) pwd->pw_gid,
               pwd->pw_dir,
               pwd->pw_shell);
    }

    endpwent();

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc list_users.c -o list_users
./list_users
Ce que fait ce programme
  • Il ouvre la base des utilisateurs.
  • Il lit chaque entrée.
  • Il affiche nom, UID, GID, home et shell.
  • Il termine proprement avec endpwent().
Important

Même si historiquement cela lit /etc/passwd, sur un système moderne, la base utilisateurs peut venir d'autres sources :
  • /etc/passwd ;
  • LDAP ;
  • NIS ;
  • SSSD ;
  • Active Directory intégré ;
  • autres backends NSS.
Donc il vaut mieux utiliser les APIs libc que parser manuellement /etc/passwd si on veut un code propre.



9. Les fonctions getgrent(), setgrent(), endgrent()

Ces fonctions sont l'équivalent pour les groupes.

Prototypes

Code: Select all

#include <grp.h>

struct group *getgrent(void);
void setgrent(void);
void endgrent(void);
Rôle
  • setgrent() : remet le curseur au début de la base des groupes.
  • getgrent() : lit le groupe suivant.
  • endgrent() : termine la lecture.
Exemple : lister tous les groupes

Code: Select all

#include <grp.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct group *grp;
    char **member;

    setgrent();

    while ((grp = getgrent()) != NULL) {
        printf("Groupe: %-20s GID=%ld\n",
               grp->gr_name,
               (long) grp->gr_gid);

        printf("Membres:");
        for (member = grp->gr_mem; *member != NULL; member++) {
            printf(" %s", *member);
        }

        printf("\n\n");
    }

    endgrent();

    return EXIT_SUCCESS;
}
À retenir
  • getpwent() énumère les utilisateurs.
  • getgrent() énumère les groupes.
  • set*ent() remet au début.
  • end*ent() ferme la lecture.



10. Les fonctions getspnam() et le fichier shadow

Pour lire les informations du fichier shadow, on utilise :

Code: Select all

#include <shadow.h>

struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
void setspent(void);
void endspent(void);
La structure principale est :

Code: Select all

struct spwd {
    char *sp_namp;     /* Login name */
    char *sp_pwdp;     /* Encrypted password */
    long  sp_lstchg;   /* Last password change */
    long  sp_min;      /* Minimum days between changes */
    long  sp_max;      /* Maximum days between changes */
    long  sp_warn;     /* Days before expiry to warn user */
    long  sp_inact;    /* Days after expiry until account inactive */
    long  sp_expire;   /* Account expiry date */
    unsigned long sp_flag;
};
Important

Lire /etc/shadow demande généralement des privilèges.

Un programme normal ne pourra pas forcément faire :

Code: Select all

getspnam("jean")
si l'utilisateur courant n'a pas les droits nécessaires.

Exemple minimal

Code: Select all

#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct spwd *sp;

    sp = getspnam("root");

    if (sp == NULL) {
        perror("getspnam");
        return EXIT_FAILURE;
    }

    printf("Login : %s\n", sp->sp_namp);
    printf("Hash  : %s\n", sp->sp_pwdp);

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc shadow_test.c -o shadow_test
./shadow_test
En utilisateur normal :

Code: Select all

getspnam: Permission denied
Avec privilèges :

Code: Select all

sudo ./shadow_test
Attention sécurité

Afficher un hash de mot de passe est sensible.
Dans un vrai programme, il faut éviter de logger ou d'afficher ce type d'information.



11. Le piège des structures statiques

Beaucoup de fonctions anciennes comme :

Code: Select all

getpwnam()
getpwuid()
getgrnam()
getgrgid()
getspnam()
retournent un pointeur vers une structure gérée en interne par la libc.

Cela signifie que le résultat peut être écrasé par l'appel suivant.

Exemple dangereux :

Code: Select all

struct passwd *p1;
struct passwd *p2;

p1 = getpwnam("root");
p2 = getpwnam("jean");

printf("%s\n", p1->pw_name);
printf("%s\n", p2->pw_name);
On pourrait croire que p1 contient toujours root.
Mais selon l'implémentation, p1 peut pointer vers une zone statique écrasée par le deuxième appel.

Version plus sûre : copier les données utiles

Si tu as besoin de garder une information longtemps, copie-la.

Exemple simple :

Code: Select all

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    struct passwd *pwd;
    char name_copy[256];

    pwd = getpwnam("root");

    if (pwd == NULL) {
        return EXIT_FAILURE;
    }

    snprintf(name_copy, sizeof(name_copy), "%s", pwd->pw_name);

    printf("Copie du nom : %s\n", name_copy);

    return EXIT_SUCCESS;
}
Versions réentrantes

Il existe aussi des versions avec suffixe _r :

Code: Select all

getpwnam_r()
getpwuid_r()
getgrnam_r()
getgrgid_r()
getspnam_r()
Ces versions demandent au programmeur de fournir le buffer de stockage.

Elles sont plus adaptées aux programmes multithreadés.



12. Exemple avec getpwnam_r()

Prototype simplifié :

Code: Select all

#include <pwd.h>

int getpwnam_r(const char *name,
               struct passwd *pwd,
               char *buf,
               size_t buflen,
               struct passwd **result);
Paramètres
  • name : nom utilisateur à chercher.
  • pwd : structure fournie par le programmeur.
  • buf : buffer pour stocker les chaînes.
  • buflen : taille du buffer.
  • result : reçoit NULL si introuvable ou un pointeur vers pwd si trouvé.
Exemple robuste

Code: Select all

#define _POSIX_C_SOURCE 200809L

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    struct passwd pwd;
    struct passwd *result;
    char buffer[16384];
    int ret;

    ret = getpwnam_r("root", &pwd, buffer, sizeof(buffer), &result);

    if (ret != 0) {
        errno = ret;
        perror("getpwnam_r");
        return EXIT_FAILURE;
    }

    if (result == NULL) {
        printf("Utilisateur introuvable\n");
        return EXIT_FAILURE;
    }

    printf("Nom   : %s\n", pwd.pw_name);
    printf("UID   : %ld\n", (long) pwd.pw_uid);
    printf("Home  : %s\n", pwd.pw_dir);
    printf("Shell : %s\n", pwd.pw_shell);

    return EXIT_SUCCESS;
}
Pourquoi c'est plus propre ?
  • Le programme fournit lui-même la mémoire.
  • On évite l'écrasement par une structure statique interne.
  • C'est plus adapté aux programmes multithreadés.
  • La gestion d'erreur est plus explicite.


13. Authentification avec crypt()

Le chapitre présente aussi la fonction :

Code: Select all

crypt()
Elle permet de hasher un mot de passe en utilisant un sel et un algorithme compatible avec les hashes du système.

Prototype :

Code: Select all

#define _XOPEN_SOURCE

#include <unistd.h>

char *crypt(const char *key, const char *salt);
Sur certains systèmes, il faut inclure :

Code: Select all

#include <crypt.h>
et compiler avec :

Code: Select all

-lcrypt
Exemple :

Code: Select all

gcc check.c -o check -lcrypt
Principe

Le fichier shadow contient un hash, par exemple :

Code: Select all

$6$saltvalue$hashvalue
On récupère ce hash complet, puis on appelle :

Code: Select all

crypt(password_saisi, hash_du_shadow)
La fonction crypt() utilise la partie "algorithme + salt" contenue dans le hash existant pour produire un nouveau hash comparable.

Ensuite on compare :

Code: Select all

strcmp(hash_calcule, hash_stocke)
Si c'est identique, le mot de passe est correct.

Exemple conceptuel

Code: Select all

stored = "$6$abc$XXXXXXXXXXXXXXXX";
entered = "secret";

calculated = crypt(entered, stored);

if (strcmp(calculated, stored) == 0) {
    printf("Mot de passe correct\n");
} else {
    printf("Mot de passe incorrect\n");
}
Attention

Ce genre de code touche à l'authentification.
Dans un vrai système moderne, on utilise plutôt PAM ou les mécanismes système prévus, plutôt que de bricoler soi-même une authentification complète.



14. getpass() : lire un mot de passe sans echo

La fonction :

Code: Select all

getpass()
lit un mot de passe depuis le terminal sans afficher les caractères tapés.

Prototype :

Code: Select all

#include <unistd.h>

char *getpass(const char *prompt);
Exemple :

Code: Select all

#include <unistd.h>
#include <stdio.h>

int main(void)
{
    char *password;

    password = getpass("Password: ");

    printf("Mot de passe lu, longueur approximative: %zu\n", strlen(password));

    return 0;
}
Il faut inclure :

Code: Select all

#include <string.h>
si on utilise strlen().

Attention

getpass() est considéré comme ancien et limité.
Dans du code moderne, on préfère manipuler le terminal avec termios ou utiliser une bibliothèque adaptée.

Pourquoi ?
  • buffer statique ;
  • limites historiques ;
  • pas idéal en multithread ;
  • pas toujours recommandé selon les plateformes.
Mais pédagogiquement, la fonction est utile pour comprendre comment un programme peut demander un secret sans l'afficher.


────────────────────────────────────────


15. Exemple pédagogique : vérifier un mot de passe local

Avertissement

Le code suivant est pédagogique.
Il illustre les notions du chapitre, mais ce n'est pas une recommandation pour créer son propre système d'authentification en production.

En production, on utilise généralement PAM.

Code exemple

Code: Select all

#define _GNU_SOURCE
#define _XOPEN_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <shadow.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    const char *username;
    char *password;
    struct spwd *shadow_entry;
    char *encrypted;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <username>\n", argv[0]);
        return EXIT_FAILURE;
    }

    username = argv[1];

    shadow_entry = getspnam(username);
    if (shadow_entry == NULL) {
        perror("getspnam");
        return EXIT_FAILURE;
    }

    password = getpass("Password: ");
    if (password == NULL) {
        fprintf(stderr, "Erreur getpass\n");
        return EXIT_FAILURE;
    }

    encrypted = crypt(password, shadow_entry->sp_pwdp);
    if (encrypted == NULL) {
        perror("crypt");
        return EXIT_FAILURE;
    }

    if (strcmp(encrypted, shadow_entry->sp_pwdp) == 0) {
        printf("Authentification réussie pour %s\n", username);
    } else {
        printf("Mot de passe incorrect\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc check_password.c -o check_password -lcrypt
Exécution :

Code: Select all

sudo ./check_password jean
Ce que fait le programme
  • Il prend un nom d'utilisateur en argument.
  • Il lit l'entrée correspondante dans /etc/shadow avec getspnam().
  • Il demande un mot de passe avec getpass().
  • Il hash le mot de passe saisi avec crypt().
  • Il compare le hash calculé avec le hash stocké.
Point sécurité important

Dans un code plus sérieux, il faudrait :
  • effacer le mot de passe de la mémoire après usage ;
  • éviter les logs sensibles ;
  • éviter les comparaisons vulnérables aux timings ;
  • utiliser PAM ;
  • faire attention aux privilèges du programme ;
  • ne pas laisser traîner de dumps mémoire.
Le chapitre mentionne justement qu'un programme manipulant un mot de passe devrait l'effacer rapidement de la mémoire.



16. Pourquoi ne pas parser /etc/passwd à la main ?

On pourrait être tenté de faire :

Code: Select all

FILE *f = fopen("/etc/passwd", "r");
puis parser les lignes soi-même.

Mais ce n'est pas l'approche la plus propre.

Raison 1 : toutes les bases utilisateurs ne viennent pas forcément du fichier

Selon la configuration du système, les utilisateurs peuvent venir de :
  • /etc/passwd ;
  • LDAP ;
  • NIS ;
  • SSSD ;
  • Active Directory ;
  • autres sources NSS.
Les APIs comme getpwnam() utilisent la couche NSS du système.

NSS signifie généralement :

Code: Select all

Name Service Switch
Elle permet au système de décider où chercher les informations.

Raison 2 : compatibilité

Les formats peuvent évoluer ou avoir des particularités.
Les APIs libc cachent ces détails.

Raison 3 : sécurité

Parser soi-même des fichiers système peut créer des bugs :
  • buffer overflow ;
  • mauvaise gestion des champs vides ;
  • problèmes d'encodage ;
  • erreurs sur les lignes malformées ;
  • comportement différent selon distribution.
Bonne pratique

Pour récupérer des utilisateurs :

Code: Select all

getpwnam()
getpwuid()
getpwent()
Pour récupérer des groupes :

Code: Select all

getgrnam()
getgrgid()
getgrent()
Pour shadow :

Code: Select all

getspnam()
getspent()

17. Commandes Linux utiles liées au chapitre

Afficher son identité

Code: Select all

id
Exemple :

Code: Select all

uid=1000(jean) gid=1000(jean) groups=1000(jean),27(sudo)
Afficher seulement l'UID

Code: Select all

id -u
Afficher seulement le GID

Code: Select all

id -g
Afficher les groupes

Code: Select all

groups
Voir /etc/passwd

Code: Select all

cat /etc/passwd
Voir /etc/group

Code: Select all

cat /etc/group
Voir /etc/shadow

Code: Select all

sudo cat /etc/shadow
Chercher un utilisateur

Code: Select all

getent passwd jean
Chercher un groupe

Code: Select all

getent group sudo
Pourquoi getent est intéressant ?

La commande getent passe par les bases système configurées, un peu comme les APIs libc.
Elle ne se contente pas forcément de lire le fichier directement.

Exemples :

Code: Select all

getent passwd
getent group
getent shadow


18. Les includes importants du chapitre

Pour les utilisateurs

Code: Select all

#include <pwd.h>
Contient :

Code: Select all

struct passwd
getpwnam()
getpwuid()
getpwent()
setpwent()
endpwent()
Pour les groupes

Code: Select all

#include <grp.h>
Contient :

Code: Select all

struct group
getgrnam()
getgrgid()
getgrent()
setgrent()
endgrent()
Pour les UID/GID et fonctions processus

Code: Select all

#include <unistd.h>
#include <sys/types.h>
Contient ou expose notamment :

Code: Select all

uid_t
gid_t
getuid()
geteuid()
getgid()
getegid()
Pour shadow

Code: Select all

#include <shadow.h>
Contient :

Code: Select all

struct spwd
getspnam()
getspent()
setspent()
endspent()
Pour crypt

Selon système :

Code: Select all

#include <unistd.h>
ou :

Code: Select all

#include <crypt.h>
Compilation possible :

Code: Select all

gcc file.c -o file -lcrypt
Pour les erreurs

Code: Select all

#include <errno.h>
#include <string.h>
#include <stdio.h>
Utile pour :

Code: Select all

errno
perror()
strerror()


19. Tableau récapitulatif des APIs

Utilisateurs

Code: Select all

getpwnam(name)
Cherche un utilisateur par nom.

Code: Select all

getpwuid(uid)
Cherche un utilisateur par UID.

Code: Select all

getpwent()
Lit l'utilisateur suivant dans la base.

Code: Select all

setpwent()
Revient au début de la base utilisateur.

Code: Select all

endpwent()
Termine la lecture de la base utilisateur.

Groupes

Code: Select all

getgrnam(name)
Cherche un groupe par nom.

Code: Select all

getgrgid(gid)
Cherche un groupe par GID.

Code: Select all

getgrent()
Lit le groupe suivant dans la base.

Code: Select all

setgrent()
Revient au début de la base groupe.

Code: Select all

endgrent()
Termine la lecture de la base groupe.

Shadow

Code: Select all

getspnam(name)
Cherche une entrée shadow par nom.

Code: Select all

getspent()
Lit l'entrée shadow suivante.

Code: Select all

setspent()
Revient au début de la base shadow.

Code: Select all

endspent()
Termine la lecture shadow.

Authentification historique

Code: Select all

crypt()
Calcule un hash compatible avec le format password/shadow.

Code: Select all

getpass()
Lit un mot de passe sans l'afficher à l'écran.



20. Erreurs classiques à éviter

Erreur 1 : croire que /etc/passwd contient toujours les mots de passe

Faux sur les systèmes modernes.

Aujourd'hui :

Code: Select all

/etc/passwd
contient souvent :

Code: Select all

x
Et les hashes sont dans :

Code: Select all

/etc/shadow
Erreur 2 : croire que root est puissant grâce à son nom

Le pouvoir de root vient surtout de :

Code: Select all

UID 0
Pas seulement de la chaîne "root".

Erreur 3 : oublier que les fonctions retournent parfois des buffers statiques

Les fonctions comme getpwnam() peuvent retourner un pointeur vers une zone statique interne.

Si on veut garder les données :
  • on copie les champs utiles ;
  • ou on utilise les versions _r.
Erreur 4 : parser /etc/passwd à la main pour du code sérieux

Mieux vaut utiliser :

Code: Select all

getpwnam()
getpwuid()
getpwent()
car le système peut utiliser NSS, LDAP, etc.

Erreur 5 : afficher ou logger des hashes

Un hash de mot de passe reste sensible.

Même si ce n'est pas le mot de passe en clair, il peut être attaqué hors-ligne.

Erreur 6 : oublier les privilèges nécessaires pour /etc/shadow

Un utilisateur normal ne doit pas pouvoir lire shadow.

Donc un programme normal ne pourra généralement pas utiliser getspnam() librement.

Erreur 7 : confondre groupe principal et groupes supplémentaires

Le groupe principal vient du champ GID dans /etc/passwd.
Les groupes supplémentaires sont listés dans /etc/group.



21. Mini-projet : afficher l'identité complète de l'utilisateur courant

Objectif :
  • récupérer UID/GID courant ;
  • trouver le nom utilisateur ;
  • trouver le nom du groupe principal ;
  • afficher le home et le shell.
Code :

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>

int main(void)
{
    uid_t uid;
    gid_t gid;
    struct passwd *pwd;
    struct group *grp;

    uid = getuid();
    gid = getgid();

    pwd = getpwuid(uid);
    if (pwd == NULL) {
        perror("getpwuid");
        return EXIT_FAILURE;
    }

    grp = getgrgid(gid);
    if (grp == NULL) {
        perror("getgrgid");
        return EXIT_FAILURE;
    }

    printf("UID numérique   : %ld\n", (long) uid);
    printf("Nom utilisateur : %s\n", pwd->pw_name);
    printf("GID numérique   : %ld\n", (long) gid);
    printf("Groupe principal: %s\n", grp->gr_name);
    printf("Home directory  : %s\n", pwd->pw_dir);
    printf("Login shell     : %s\n", pwd->pw_shell);

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc identity.c -o identity
./identity
Sortie possible :

Code: Select all

UID numérique   : 1000
Nom utilisateur : jean
GID numérique   : 1000
Groupe principal: jean
Home directory  : /home/jean
Login shell     : /bin/bash
Ce mini-projet résume très bien le chapitre

Il utilise :
  • getuid()
  • getgid()
  • getpwuid()
  • getgrgid()
  • struct passwd
  • struct group


22. Mini-projet : lister les utilisateurs humains probables

Sur beaucoup de distributions, les utilisateurs humains ont souvent un UID à partir de 1000.

Attention : ce n'est pas une vérité universelle, mais c'est fréquent.

Code :

Code: Select all

#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    struct passwd *pwd;

    setpwent();

    while ((pwd = getpwent()) != NULL) {
        if (pwd->pw_uid >= 1000 && pwd->pw_uid < 60000) {
            printf("%-20s UID=%ld HOME=%s SHELL=%s\n",
                   pwd->pw_name,
                   (long) pwd->pw_uid,
                   pwd->pw_dir,
                   pwd->pw_shell);
        }
    }

    endpwent();

    return EXIT_SUCCESS;
}
Compilation :

Code: Select all

gcc list_humans.c -o list_humans
./list_humans
Intérêt pédagogique

Ce programme montre comment scanner la base utilisateurs et filtrer selon l'UID.



23. Mini-projet : vérifier si un utilisateur appartient à un groupe

Objectif :
  • prendre un nom de groupe ;
  • lire la liste de ses membres ;
  • chercher un utilisateur dedans.
Code :

Code: Select all

#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    struct group *grp;
    char **member;
    const char *group_name;
    const char *user_name;

    if (argc != 3) {
        fprintf(stderr, "Usage: %s <group> <user>\n", argv[0]);
        return EXIT_FAILURE;
    }

    group_name = argv[1];
    user_name = argv[2];

    grp = getgrnam(group_name);
    if (grp == NULL) {
        printf("Groupe introuvable\n");
        return EXIT_FAILURE;
    }

    for (member = grp->gr_mem; *member != NULL; member++) {
        if (strcmp(*member, user_name) == 0) {
            printf("%s est membre du groupe %s\n", user_name, group_name);
            return EXIT_SUCCESS;
        }
    }

    printf("%s n'est pas listé comme membre supplémentaire de %s\n",
           user_name, group_name);

    return EXIT_FAILURE;
}
Compilation :

Code: Select all

gcc is_member.c -o is_member
./is_member sudo jean
Attention

Ce code regarde seulement la liste gr_mem du groupe.
Il ne tient pas forcément compte du groupe principal indiqué dans /etc/passwd.

Pour une vérification complète, il faudrait aussi comparer le GID principal de l'utilisateur avec le GID du groupe.


24. Version plus complète : groupe principal + groupes supplémentaires

Code :

Code: Select all

#include <pwd.h>
#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    const char *group_name;
    const char *user_name;
    struct group *grp;
    struct passwd *pwd;
    char **member;

    if (argc != 3) {
        fprintf(stderr, "Usage: %s <group> <user>\n", argv[0]);
        return EXIT_FAILURE;
    }

    group_name = argv[1];
    user_name = argv[2];

    grp = getgrnam(group_name);
    if (grp == NULL) {
        fprintf(stderr, "Groupe introuvable\n");
        return EXIT_FAILURE;
    }

    pwd = getpwnam(user_name);
    if (pwd == NULL) {
        fprintf(stderr, "Utilisateur introuvable\n");
        return EXIT_FAILURE;
    }

    if (pwd->pw_gid == grp->gr_gid) {
        printf("%s appartient à %s via son groupe principal\n",
               user_name, group_name);
        return EXIT_SUCCESS;
    }

    for (member = grp->gr_mem; *member != NULL; member++) {
        if (strcmp(*member, user_name) == 0) {
            printf("%s appartient à %s via les groupes supplémentaires\n",
                   user_name, group_name);
            return EXIT_SUCCESS;
        }
    }

    printf("%s n'appartient pas au groupe %s\n", user_name, group_name);

    return EXIT_FAILURE;
}
Compilation :

Code: Select all

gcc check_group.c -o check_group
./check_group sudo jean
Ce que ce code montre
  • Un utilisateur peut appartenir à un groupe via son GID principal.
  • Il peut aussi appartenir à un groupe via la liste gr_mem.
  • Il faut vérifier les deux si on veut être plus correct.

25. Ce qu'il faut retenir par coeur

Fichiers

Code: Select all

/etc/passwd
/etc/shadow
/etc/group
Format de /etc/passwd

Code: Select all

login:password:uid:gid:comment:home:shell
Format de /etc/group

Code: Select all

group:password:gid:members
Structures C

Code: Select all

struct passwd
struct group
struct spwd
APIs utilisateur

Code: Select all

getpwnam()
getpwuid()
getpwent()
setpwent()
endpwent()
APIs groupe

Code: Select all

getgrnam()
getgrgid()
getgrent()
setgrent()
endgrent()
APIs shadow

Code: Select all

getspnam()
getspent()
setspent()
endspent()
Authentification historique

Code: Select all

crypt()
getpass()
Types

Code: Select all

uid_t
gid_t
Includes

Code: Select all

#include <pwd.h>
#include <grp.h>
#include <shadow.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>


26. Résumé final du chapitre

Ce chapitre explique comment Linux représente les utilisateurs et les groupes.

Le noyau travaille avec des identifiants numériques :

Code: Select all

UID
GID
Les informations lisibles sont stockées dans des bases système comme :

Code: Select all

/etc/passwd
/etc/group
/etc/shadow
Le fichier /etc/passwd contient les informations générales des comptes.
Le fichier /etc/group contient les groupes et leurs membres.
Le fichier /etc/shadow contient les informations sensibles liées aux mots de passe.

En C, on interroge ces informations avec :

Code: Select all

getpwnam()
getpwuid()
getgrnam()
getgrgid()
getpwent()
getgrent()
getspnam()
La structure struct passwd représente un utilisateur.
La structure struct group représente un groupe.
La structure struct spwd représente une entrée shadow.

Le point de sécurité important est que les hashes de mots de passe ne doivent pas être lisibles par tout le monde.
C'est pour cette raison que les systèmes modernes utilisent /etc/shadow au lieu de stocker les hashes directement dans /etc/passwd.

Phrase à retenir
Sous Linux, l'identité système repose principalement sur des UID, des GID et des groupes ; les noms utilisateurs ne sont qu'une représentation lisible de ces identifiants numériques.
Parallèle Windows

Code: Select all

Linux UID/GID      <=> Windows SID
Linux groups       <=> Windows token groups
/etc/passwd        <=> base comptes locale / SAM conceptuellement
/etc/shadow        <=> stockage sensible des secrets
permissions rwx    <=> ACL/ACE Windows
Ce chapitre est donc une base indispensable pour comprendre la sécurité Linux, les permissions, les services, les daemons, sudo, l'authentification et les prochains chapitres sur les credentials de processus.

Who is online

Users browsing this forum: No registered users and 1 guest