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.
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
Code: Select all
root -> GID 0
users -> GID 100
sudo -> GID variable selon distribution
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 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 ?
- 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.
Code: Select all
-rw-r--r-- 1 jean jean 1234 May 14 notes.txt
- le propriétaire est jean ;
- le groupe est jean ;
- les permissions sont rw-r--r--.
Code: Select all
stat notes.txt
Code: Select all
Uid: ( 1000/ jean)
Gid: ( 1000/ jean)
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
Code: Select all
jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
Code: Select all
login:password:uid:gid:comment:home:shell
Champ 1 : login name
C'est le nom de connexion de l'utilisateur.
Code: Select all
jean
root
www-data
nobody
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
Code: Select all
/etc/shadow
Code: Select all
jean:x:1000:1000:Jean Michel:/home/jean:/bin/bash
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
- root possède UID 0 ;
- jean possède UID 1000.
Code: Select all
UID 0 = superutilisateur
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
- UID = 1000
- GID principal = 1000
Code: Select all
/etc/group
Ce champ contient des informations textuelles sur l'utilisateur.
Exemple :
Code: Select all
Jean Michel
Michael Kerrisk
Service Account
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
Code: Select all
HOME=/home/jean
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
Code: Select all
/bin/bash
Code: Select all
/usr/sbin/nologin
Exemple complet analysé
Code: Select all
mtk:x:1000:100:Michael Kerrisk:/home/mtk:/bin/bash
- 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.
- /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
Code: Select all
Permission denied
Exemple de ligne shadow :
Code: Select all
jean:$y$j9T$abcdef...:19800:0:99999:7:::
Code: Select all
login:encrypted-password:last-change:min:max:warn:inactive:expire:reserved
Nom de l'utilisateur correspondant à /etc/passwd.
Code: Select all
jean
root
daemon
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
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é.
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:
Code: Select all
group-name:password:gid:user-list
Nom du groupe.
Exemples :
Code: Select all
sudo
users
developers
docker
www-data
Champ historique pour un mot de passe de groupe.
Aujourd'hui, il est généralement inutilisé et contient souvent :
Code: Select all
x
Champ 3 : GID
Identifiant numérique du groupe.
Exemple :
Code: Select all
sudo:x:27:jean
Champ 4 : user list
Liste des utilisateurs membres supplémentaires du groupe.
Exemple :
Code: Select all
developers:x:1001:jean,paul,marie
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
- groupe principal de jean : GID 1000 ;
- groupes supplémentaires : sudo, docker, developers.
Pour voir les groupes de l'utilisateur courant :
Code: Select all
groups
Code: Select all
id
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
Code: Select all
#include <sys/types.h>
#include <unistd.h>
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;
}
Code: Select all
gcc main.c -o test
./test
Code: Select all
UID = 1000
GID = 1000
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 */
};
- 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.
API :
Code: Select all
struct passwd *getpwnam(const char *name);
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;
}
Code: Select all
gcc main.c -o get_user
./get_user
Code: Select all
Nom : root
UID : 0
GID : 0
Home : /root
Shell : /bin/bash
API :
Code: Select all
struct passwd *getpwuid(uid_t uid);
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;
}
- getpwnam("jean") : cherche par nom.
- getpwuid(1000) : cherche par UID.
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 */
};
- 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.
API :
Code: Select all
struct group *getgrnam(const char *name);
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;
}
API :
Code: Select all
struct group *getgrgid(gid_t gid);
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;
}
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);
- setpwent() : remet le curseur au début de la base passwd.
- getpwent() : lit l'entrée suivante.
- endpwent() : ferme/libère la lecture.
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;
}
Code: Select all
gcc list_users.c -o list_users
./list_users
- Il ouvre la base des utilisateurs.
- Il lit chaque entrée.
- Il affiche nom, UID, GID, home et shell.
- Il termine proprement avec endpwent().
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.
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);
- setgrent() : remet le curseur au début de la base des groupes.
- getgrent() : lit le groupe suivant.
- endgrent() : termine la lecture.
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;
}
- 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);
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;
};
Lire /etc/shadow demande généralement des privilèges.
Un programme normal ne pourra pas forcément faire :
Code: Select all
getspnam("jean")
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;
}
Code: Select all
gcc shadow_test.c -o shadow_test
./shadow_test
Code: Select all
getspnam: Permission denied
Code: Select all
sudo ./shadow_test
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()
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);
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;
}
Il existe aussi des versions avec suffixe _r :
Code: Select all
getpwnam_r()
getpwuid_r()
getgrnam_r()
getgrgid_r()
getspnam_r()
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);
- 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é.
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;
}
- 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()
Prototype :
Code: Select all
#define _XOPEN_SOURCE
#include <unistd.h>
char *crypt(const char *key, const char *salt);
Code: Select all
#include <crypt.h>
Code: Select all
-lcrypt
Code: Select all
gcc check.c -o check -lcrypt
Le fichier shadow contient un hash, par exemple :
Code: Select all
$6$saltvalue$hashvalue
Code: Select all
crypt(password_saisi, hash_du_shadow)
Ensuite on compare :
Code: Select all
strcmp(hash_calcule, hash_stocke)
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");
}
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()
Prototype :
Code: Select all
#include <unistd.h>
char *getpass(const char *prompt);
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;
}
Code: Select all
#include <string.h>
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.
────────────────────────────────────────
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;
}
Code: Select all
gcc check_password.c -o check_password -lcrypt
Code: Select all
sudo ./check_password jean
- 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é.
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.
16. Pourquoi ne pas parser /etc/passwd à la main ?
On pourrait être tenté de faire :
Code: Select all
FILE *f = fopen("/etc/passwd", "r");
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.
NSS signifie généralement :
Code: Select all
Name Service Switch
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.
Pour récupérer des utilisateurs :
Code: Select all
getpwnam()
getpwuid()
getpwent()
Code: Select all
getgrnam()
getgrgid()
getgrent()
Code: Select all
getspnam()
getspent()
17. Commandes Linux utiles liées au chapitre
Afficher son identité
Code: Select all
id
Code: Select all
uid=1000(jean) gid=1000(jean) groups=1000(jean),27(sudo)
Code: Select all
id -u
Code: Select all
id -g
Code: Select all
groups
Code: Select all
cat /etc/passwd
Code: Select all
cat /etc/group
Code: Select all
sudo cat /etc/shadow
Code: Select all
getent passwd jean
Code: Select all
getent group sudo
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>
Code: Select all
struct passwd
getpwnam()
getpwuid()
getpwent()
setpwent()
endpwent()
Code: Select all
#include <grp.h>
Code: Select all
struct group
getgrnam()
getgrgid()
getgrent()
setgrent()
endgrent()
Code: Select all
#include <unistd.h>
#include <sys/types.h>
Code: Select all
uid_t
gid_t
getuid()
geteuid()
getgid()
getegid()
Code: Select all
#include <shadow.h>
Code: Select all
struct spwd
getspnam()
getspent()
setspent()
endspent()
Selon système :
Code: Select all
#include <unistd.h>
Code: Select all
#include <crypt.h>
Code: Select all
gcc file.c -o file -lcrypt
Code: Select all
#include <errno.h>
#include <string.h>
#include <stdio.h>
Code: Select all
errno
perror()
strerror()
19. Tableau récapitulatif des APIs
Utilisateurs
Code: Select all
getpwnam(name)
Code: Select all
getpwuid(uid)
Code: Select all
getpwent()
Code: Select all
setpwent()
Code: Select all
endpwent()
Groupes
Code: Select all
getgrnam(name)
Code: Select all
getgrgid(gid)
Code: Select all
getgrent()
Code: Select all
setgrent()
Code: Select all
endgrent()
Shadow
Code: Select all
getspnam(name)
Code: Select all
getspent()
Code: Select all
setspent()
Code: Select all
endspent()
Authentification historique
Code: Select all
crypt()
Code: Select all
getpass()
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
Code: Select all
x
Code: Select all
/etc/shadow
Le pouvoir de root vient surtout de :
Code: Select all
UID 0
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.
Mieux vaut utiliser :
Code: Select all
getpwnam()
getpwuid()
getpwent()
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: 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;
}
Code: Select all
gcc identity.c -o identity
./identity
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
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;
}
Code: Select all
gcc list_humans.c -o list_humans
./list_humans
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: 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;
}
Code: Select all
gcc is_member.c -o is_member
./is_member sudo jean
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;
}
Code: Select all
gcc check_group.c -o check_group
./check_group sudo jean
- 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
Code: Select all
login:password:uid:gid:comment:home:shell
Code: Select all
group:password:gid:members
Code: Select all
struct passwd
struct group
struct spwd
Code: Select all
getpwnam()
getpwuid()
getpwent()
setpwent()
endpwent()
Code: Select all
getgrnam()
getgrgid()
getgrent()
setgrent()
endgrent()
Code: Select all
getspnam()
getspent()
setspent()
endspent()
Code: Select all
crypt()
getpass()
Code: Select all
uid_t
gid_t
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
Code: Select all
/etc/passwd
/etc/group
/etc/shadow
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 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
Parallèle WindowsSous 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.
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
