Apprendre à utiliser la fonction system

Mohamed AHMED

Nous abordons dans cet article l'utilité de la fonction system en C++.

Nous illustrerons son importance par des exemples sous la plate-forme Windows tels que la manipulation de comptes Windows ainsi que la réparation du gestionnaire de tâches et du registre.

En outre, nous discuterons les cas où la fonction system() peut entraîner parfois des vulnérabilités exploitables dans la norme POSIX avant de parler des autres solutions préférées de cette fonction.

Commentez

- Titre : Apprendre à utiliser la fonction system

- Auteur : Mohamed AHMED

- Parution : 12 novembre 2013

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 snack3r. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- Premier pas

- Pourquoi écrit-on l'expression system("PAUSE") ?

- Qu'est-ce que la fonction system ?

- À quoi sert cette fonction ?

- Quels sont ses paramètres ?

- Exemple 1 : affichage d'heure et date

- Exemple 2 : manipuler les états de l'ordinateur

- Exemple 3 : changer le mot de passe du compte Windows

- Exemple 4 : activer le gestionnaire de tâches et le registre

- Dangers de la fonction system dans la norme POSIX

- system() VS la famille exec

- Conclusion

- Liens

- Remerciements

Introduction

Comme l'utilisation de la fonction system() soulève de nombreuses questions et qu'elle peut être utile, je me suis décidé à procéder à son explication détaillée afin de permettre aux non-initiés de la comprendre facilement.

Premier pas

Jetons un coup d'oeil sur le code suivant :

premier pas

1. #include <cstdlib>

2. #include <iostream>

4. int main(int argc, char** argv) {

5. std::cout << "Apprendre à utiliser la fonction system !"<<std::endl;

6. int value_returned = std::system("PAUSE");

7. std::cout << "La valeur retournée est : "<<value_returned<< std::endl;

8. return EXIT_SUCCESS;

9. }

Cette fois-ci, on a vérifié le code de retour de la commande exécutée, mais dans le reste de l'article nous ne nous occuperons plus de vérifier ce genre de code, quelle qu'en soit l'utilité.

Ce programme, une fois compilé et exécuté, affichera dans la console :

Apprendre à utiliser la fonction system !

Revenons sur la ligne 6 :

int value_returned = std::system("PAUSE");

Pourquoi écrit-on l'expression system("PAUSE") ?

La console ouverte pour exécuter le programme fermera à la fin du code du main(). Comme le processeur est capable d'exécuter des milliers d'instructions par seconde, le temps entre l'affichage du message et la fermeture du code est très court, il ne permet pas au développeur de lire le message affiché. Ainsi, la ligne std::system("PAUSE") permet de mettre en pause le programme et de laisser le temps de lire le message.

Note : la commande PAUSE n'est compatible qu'avec Windows, pour Linux on utilise la commande read comme ceci :

la commande read

read -rsp $'Appuyer sur une touche pour continuer ...\n'

Qu'est-ce que la fonction system ?

La fonction system() permet au programmeur de pouvoir utiliser les commandes DOS/Shell dans son code C++.

À quoi sert cette fonction ?

Supposons que vous utilisiez un système comme Linux ou Windows et que vous vouliez exécuter des instructions DOS/Shell dans votre code, dans ce cas, la fonction system() va s'en occuper ! Mais comment ? Regardons l'exemple suivant :

si nous voulons effacer l'écran, nous pouvons tout à fait utiliser la fonction clrscr() qui se trouve dans le fichier d'en-tête conio.h(1) mais cette fois-ci, nous préférons utiliser la fonction system() en lui passant la commande à exécuter, cls dans le cas de Windows ou clear dans le cas de Linux :

effacer l'écran

system("cls");
system("clear");

Quels sont ses paramètres ?

La fonction system() reçoit un seul paramètre qui représente bien une commande système à exécuter. Son prototype est :

prototype de la fonction system()

#include<cstdlib>
int system(const char* command);

Par exemple dans le code précédent nous avons utilisé la commande PAUSE qui permet d'afficher la ligne suivante : Appuyer sur une touche pour continuer…

En ce moment le programme demeure en attente jusqu'à ce que l'utilisateur tape une touche clavier pour continuer l'exécution. Le principe de la commande PAUSE se résume dans les deux lignes suivantes : (sauf que dans cet exemple, l'utilisateur doit appuyer sur la touche entrée pour continuer l'exécution)

l'équivalant de la commande PAUSE

std::cout << "Appuyer sur une touche «Entrée» pour continuer ..." << std::endl;
std::cin.ignore(std::numeric_limits<int>::max(), '\n');

Pour conclure :

La fonction system() joue le rôle de lanceur de commande entre l'utilisateur et la console, elle est responsable de l'exécution des commandes système.

L'utilisation de base de la fonction system() est tout à fait simple, mais quand même il faut être prudent en récupérant les erreurs.

Parmi les remarques importantes lors de l'utilisation des fonctions système (surtout la fonction system) on peut citer :

- il faut tester si le processeur de commande est disponible(2) ;

- d'une manière générale, l'utilisation des fonctions système donne un code plus ou moins « non portable » parce que les commandes systèmes diffèrent d'un système à l'autre(3) ;

- à l'origine, il n'y a pas de possibilité de lancer plusieurs commandes à la fois ;

- on ne récupère que le code de retour de la commande lancée. En plus, les codes de retour dépendent de l'implémentation ;

- system() ne permet pas de vérifier que la commande lancée a été bien exécutée ou pas ;

- pendant l'exécution de la commande, SIGCHLD sera bloquée, SIGINT et SIGQUIT seront ignorés(4) ;

- on ne sépare pas très bien le « Contrôleur » de la « Vue » si on veut faire du MVC(5) ;

- ne pas utiliser system() dans un programme de privilèges set-user-ID ou set-group-ID.(6)

Donnons des exemples maintenant pour bien comprendre la fonction system et se rendre compte de son importance.

Note : rappelons-nous que ces exemples sont spécifiques à la plate-forme Windows.

Exemple 1 : affichage d'heure et date

Dans cet exemple, nous voulons écrire un programme qui permet d'afficher et de changer l'heure et la date courantes quand il le faut.

Cette fois-ci nous avons  besoin de la commande TIME et DATE, la première permet d'afficher l'heure et la deuxième d'afficher la date :

Affichage d'heure et date

#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
system("TIME");
system("DATE");
system("CLS");
return EXIT_SUCCESS;
}

Et si nous voulons changer l'heure, nous ajoutons l'option ou le commutateur /t :

changer l'heure

system("TIME /t");

À part la fonction system(), il existe d'autres méthodes de récupérer la date et l'heure courantes, parmi lesquelles on cite(1) :
- la fonction time qui se trouve dans le fichier d'en-tête ctime ;
- la fonction GetSystemTime qui se trouve dans le fichier d'en-tête windows.h ;
- les classes CTime et CTimeSpan si vous développez avec Visual C++ et les MFC ;
- les classes TDate et TDateTime si vous développez avec C++ Builder et la VCL ;
- la bibliothèque boost::date_time si vous souhaitez garder un code C++ portable.

Exemple 2 : manipuler les états de l'ordinateur

Cette fois-ci nous allons écrire un programme dont voici le menu :

- Arrêter l'ordinateur

- Redémarrer

- Fermer la session

- Quitter

Pour écrire le code correspondant, nous aurons besoin des commandes suivantes :

SHUTDOWN et EXIT

SHUTDOWN /S /T 00
SHUTDOWN /R /T 00
SHUTDOWN /L
EXIT

Explication de la commande SHUTDOWN

La commande SHUTDOWN permet de manipuler les états de l'ordinateur (arrêt, redémarrage, mise en veille, veille prolongée…). Cette commande possède beaucoup de commutateurs dont voici quelques-uns  :

- /i : manipuler les états d'ordinateur en mode graphique ;

- /l : fermer la session courante de l'utilisateur ;

- /s : arrêter l'ordinateur ;

- /r : redémarrer l'ordinateur ;

- /h : mise en veille prolongée ;

- /t xxx : définir la période de délai avant l'arrêt de l'ordinateur au bout de xxx secondes ;

- /a : annuler l'arrêt du système ;

/f : oblige la fermeture des applications en cours d'exécution sans prévenir les utilisateurs.

Pour en savoir plus, vous pouvez taper la commande : SHUTDOWN /HELP.

Sans oublier que la commande EXIT permet de quitter la console. (Oh ! je vois bien que c'est évident quand même !)

Maintenant nous pouvons tout à fait écrire le code de notre programme qui gère les états de l'ordinateur :

manipuler les états de l'ordinateur

#include <cstdlib>
#include <iostream>
using namespace std;

int main(int argc, char** argv) {
char choix;

cout << endl << "\t\t***************************************" << endl
<< endl << "\t\t ******* La commande SHUTDOWN ******* " << endl
<< endl << "\t\t***************************************" << endl;

cout << endl << "1 - Arreter l'ordinateur." << endl
<< endl << "2 - Redermarrer l'ordinateur." << endl
<< endl << "3 - Fermer la session." << endl
<< endl << "4 - Quitter le programme." << endl;

cout << endl << "Donnez votre choix : " << endl;
cin >> choix;

switch (choix) {
case '1':system("SHUTDOWN /S /T 00");
break;
case '2':system("SHUTDOWN /R /T 00");
break;
case '3':system("SHUTDOWN /L");
break;
case '4':system("EXIT");
break;
default:cout << "Erreur, choix incorrect !" << endl;
}

return EXIT_SUCCESS;
}

Si nous voulons colorer la police d'écriture et l'arrière-plan, nous devons utiliser la fonction system() en lui passant la commande COLOR qui prend les attributs des couleurs comme argument. Ces derniers sont spécifiés par DEUX chiffres hexadécimaux, le premier correspond à l'arrière-plan, le second à la police d'écriture. Chaque chiffre peut prendre n'importe laquelle de ces valeurs :

La ligne ajoutée est la suivante :

colorer l'interface

system("COLOR 2E");

Plaçons la commande de coloration avant l'affichage :

La partie modifiée

char choix;
system("COLOR 2E");
cout << endl << "\t\t***************************************" << endl
<< endl << "\t\t ******* La commande SHUTDOWN ******* " << endl
<< endl << "\t\t***************************************" << endl;

Voici l'interface de notre code :

L'utilisation de la fonction system() pour la coloration de l'interface est un peu bizarre, car elle permet à une seule couleur de dominer (c'est-à-dire, nous ne pouvons pas y trouver plusieurs lignes de couleurs différentes). Pour le faire il est recommandé d'utiliser la fonction SetConsoleTextAttribute qui se trouve dans le fichier d'en-tête windows.h. Cette fonction prend deux arguments, le premier est le HANDLE, le second correspond au chiffre hexadécimal qui représente la couleur. Voici un exemple d'appel :

La fonction SetConsoleTextAttribute

SetConsoleTextAttribute(GetStHandle(STD_OUTPUT_HANDLE), 14);

Exemple 3 : changer le mot de passe du compte Windows

Dans cet exemple nous comptons aller un peu plus loin en écrivant un petit code qui permet la modification du mot de passe de l'utilisateur.

La commande utilisée NET USER est une sous-commande de la commande NET(7) qui permet de passer des commandes sur le réseau. Sachant qu'elle possède d'autres sous-commandes comme :

- NET SEND : envoie un message à un ordinateur distant ;

- NET START : démarre un service ;

- NETSTAT : affiche des statistiques et les connexions actives ;

- NET TIME : affiche la date et l'heure actuelles de l'ordinateur désigné comme étant le serveur de temps du domaine ;

-NET FILE : ferme un fichier partagé et supprime les verrous de fichier.

Revenons à notre mouton, la commande NET USER permet de manipuler les comptes d'utilisateurs, elle accepte certains commutateurs dont on peut citer :

- /ADD : ajoute un compte ;

- /DELETE : supprime un compte ;

- /ACTIVE:{YES | NO} : active ou désactive un compte ;

- /EXPIRES:{date | NEVER} : fait expirer le compte à une date définie ;

- /PASSWORDCHG:{YES | NO} : spécifie si les utilisateurs peuvent changer leur mot de passe ;

- /PASSWORDREQ:{YES | NO} : spécifie si un compte d'utilisateur doit avoir un mot de passe ;

- /LOGONPASSWORDCHG:{YES|NO} : spécifie si l'utilisateur doit changer son mot de passe à la prochaine ouverture de session.

Pour plus d'informations, tapez la commande : NET USER /HELP

Comme on s'intéresse ici à la modification du mot de passe d'un utilisateur, la syntaxe se présenterait ainsi : NET USER [nom_utilisateur] [mot_passe]

Une fois la commande connue, nous pouvons tout simplement changer le mot de passe de l'utilisateur courant :

modifier le mot de passe

#include <cstdlib>
#include <iostream>
using namespace std;
int main(int argc, char** argv) {
system("USER NET %USERNAME% 123456");
return EXIT_SUCCESS;
}

L'obtention de l'erreur système 5 lors de l'exécution du code signifie que vous n'avez pas les droits nécessaires pour exécuter la commande NET USER afin de changer le mot de passe. Dans les versions récentes de Windows, pour manipuler ces genres de commandes DOS, vous êtes obligé de lancer le fichier exécutable en tant qu'administrateur pour des raisons de sécurité.

Exemple 4 : activer le gestionnaire de tâches et le registre

Là, nous allons manipuler le registre de Windows afin d'activer/désactiver le gestionnaire de tâches ainsi que l'outil de registre. Évidemment, la plupart des virus désactivent automatiquement le gestionnaire des tâches afin de bloquer l'accès à ce dernier pour que l'utilisateur ne puisse plus tuer les processus virus en cours d'exécution. La commande DOS qui permet de réparer le gestionnaire des tâches est la suivante :

ajouter une clé au registre

REG ADD hkcu\Software\Microsoft\Windows\CurrentVersion\policies\system /v DisableTaklMgr /t reg_dword /d 1

- l'instruction REG ADD permet d'ajouter une clé au registre ;

- Software\Microsoft\Windows\CurrentVersion\policies\system : représente le chemin de la clé que nous voulons ajouter ;

- /v Disable TaskMgr : nom de la clé ;

- /t reg_dword : type de clé.

- /d 0 : la valeur de clé (0 pour l'activation et 1 pour la désactivation).

Et voilà le code correspondant :

activer/désactiver le gestionnaire de tâches

#include <cstdlib>
using namespace std;
int main(int argc, char** argv) {
system("REG ADD hkcu\\Software\\Microsoft\\Windows\\CurrentVersion"
"\\policies\\system /v DisableTaklMgr /t reg_dword /d 1");
system("PAUSE");
return EXIT_SUCCESS;
}

Nous pouvons faire pareil avec l'outil de registre :

activer/désactiver le registre

#include <cstdlib>
using namespace std;
int main(int argc, char** argv) {
system("REG ADD hkcu\\Software\\Microsoft\\Windows\\CurrentVersion"
"\\policies\\system /v DisableRegistryTools /t reg_dword /d 0");
system("PAUSE");
return EXIT_SUCCESS;
}

Dangers de la fonction system dans la norme POSIX

system() est une fonction bien connue des Linux. Elle permet de lancer une commande passée en paramètre de la fonction. Mais contrairement aux fonctions exec(8), comme execve(), system() utilise certaines variables d'environnement pour chercher le fichier à lancer. Ainsi, system() se repère surtout grâce à $PATH. Cette variable contient la liste des répertoires dans lesquels les fichiers pourront être utilisés sans préciser le nom du répertoire dans lequel ils se trouvent.(2)

Par exemple, PATH contient /bin, donc si vous voulez exécuter /bin/commande, vous auriez juste à taper dans le shell : $ commande. En effet pour exécuter une commande ou un programme en C++ il est préférable de vérifier le PATH car la fonction system() utilise les variables d'environnement pour localiser la commande, sa vulnérabilité est donc due au fait que ces variables sont modifiables par l'utilisateur.(9)

Par exemple si nous avons un code qui contient l'instruction :

system("cat /etc/passwd");

Nous pourrions changer le PATH pour que les appels-système soient faits dans le répertoire voulu, par exemple :

export PATH=/home/snacker:$PATH

Dans ce cas, nous disons au Shell d'aller vérifier la présence du binaire cat dans le répertoire /home/snacker.

Maintenant, dans le répertoire /home/snacker, nous allons créer un programme qui s'appellera cat contenant l'instruction d'affichage :

cout << "La fonction system est vulnérable !" << endl;

On compile :

g++ cat.cpp -o cat

et on exécute :

./cat

La sortie du programme va être « La fonction system est vulnérable » au lieu du contenu du fichier /etc/passwd. On remarque donc que la fonction system() exécute bien la commande cat dans notre PATH.

system() VS la famille exec

La famille des fonctions exec permet de recouvrir le processus courant par l'exécution d'un programme, c'est-à-dire qu'elle remplace l'image du processus appelant par une nouvelle image du processus sans changer le numéro du processus (PID). L'argument initial de toutes ces fonctions est le chemin d'accès du fichier à exécuter.(3)

La famille exec contient plusieurs fonctions de natures différentes, à savoir :

familles exec

#include <unistd.h>

extern char ** environ;

int execl (const char * chemin, const char * arg, ...);
int execlp (const char * fichier, const char * arg, ...);
int execle (const char * chemin, const char * arg , ..., char * const envp[]);
int execv (const char * chemin, char * const argv []);
int execvp (const char * fichier, char * const argv []);
int execve (const char * chemin, char * const argv [], char * const envp[]);

Les fonctions execl prennent comme premier argument le nom du programme à exécuter puis la liste de ses arguments terminée par NULL. Le premier argument d'un programme étant le nom de ce programme, les deux premiers arguments de execl doivent être les mêmes.

Par exemple pour recouvrir un processus par /usr/bin/ls -l :

La fonction execl

execl("/usr/bin/ls", "/usr/bin/ls", "-l", NULL);

Les fonctions execv() et execvp() utilisent un tableau de pointeurs sur des chaînes de caractères terminées par des caractères nuls, qui constituent les arguments disponibles pour le programme à exécuter. Par convention, le premier argument doit pointer sur le nom du fichier associé au programme à exécuter. Le tableau de pointeurs doit se terminer par un pointeur NULL.

Si l'une des fonctions exec() revient, c'est qu'une erreur a eu lieu. La valeur de retour est -1, et errno contient le code d'erreur.

La famille exec est plutôt réservée à Linux/Unix. Sous Windows on ne sait pas bien ce que ça peut donner. J'imagine mal qu'on puisse adapter cette famille sous Windows, car la gestion des processus y est beaucoup trop différente.

Si vous ne voulez pas utiliser la fonction system sous Windows, vous pouvez tout simplement appeler createProcess. (En fait, sous Windows, system appelle createProcess.)

La démarche d'exécution de la fonction system passe par les étapes suivantes :

tandis que celle de fork+exec se déroule ainsi :

Conclusion

Il n'est pas toujours facile de trouver un remplacement pour la fonction system(). L'une des possibilités est d'employer directement les appels-système execl() ou execle(). Toutefois, la sémantique diffère totalement puisque le programme externe n'est plus invoqué comme une sous-routine, mais la commande appelée remplace le processus en cours.(4)

En somme, il est préférable d'utiliser au maximum les bibliothèques alternatives en priorité, si cela ne marche pas vous pouvez utiliser la famille exec, et system dans des cas assez spécifiques.

Liens

(1) Comment gérer les dates et les heures en C++ ?

(2) La faille de system()

(3) Man page 3 : exec

(4) Éviter les failles de sécurité dès le développement d'une application

Remerciements

Tout d'abord, je tiens à remercier le responsable de la rubrique C++, germinolegrand, pour sa gestion et son soutien et LittleWhite pour la relecture technique attentive de cet article sans oublier Neckara pour sa disponibilité spontanée malgré ses occupations multiples. Un grand merci également à ClaudeLELOUP qui m'a aidé pour l'orthographe.

(1)En fait, conio existait depuis les années 90, en plus, elle n'est pas portable, car elle est propre à Borland. Pour cela, on préfère utiliser system() avec clear/cls.

(2)- En utilisant system(NULL) : si la commande passée est un pointeur nul, la fonction system vérifie la disponibilité du processeur de commande, sans invoquer n'importe quelle autre commande.

(3)- Par exemple, on peut en tant que root, remplacer toutes les commandes par d'autres et/ou définir des alias.

(4)- SIGCHLD, SIGINT et SIGQUIT sont des signaux (qui se trouvent dans les plates-formes répondant aux normes POSIX) définis dans le fichier d'en-tête signal.h

(5)- Par exemple : comme il n'y a pas de communications, on va directement passer de l'exécution (contrôleur) de la commande à l'affichage (vue) du résultat de la console sans pouvoir modifier l'affichage ou de le faire d'une manière limitée.

(6)- Un programme Set-UID root est susceptible de faire l'objet d'attaques s'il dialogue - même brièvement - avec un utilisateur différent de celui qui l'a programmé. Si la conception d'une application fait apparaître une telle caractéristique, il est important de faire preuve de prudence lors du développement, et de garder à l'esprit les risques possibles.

(7)Net est un utilitaire en ligne de commande qui fait appel à une suite d'instructions qui permettent d'administrer les réseaux et les serveurs.

(8)En fait, les fonctions execlp et execvp agiront comme le shell dans la recherche du fichier exécutable si le nom fourni ne contient pas de slash (/). Le chemin de recherche est spécifié dans la variable d'environnement PATH. Si cette variable n'est pas définie, le chemin par défaut sera « '/bin:/usr/bin: »'.

(9)D'autre part, ces modifications peuvent être aussi vues comme une « personnalisation » du système par l'utilisateur qui prend alors la responsabilité de ses modifications.