Solution de erdnaxe pour À l'ancienne

forensics réseau

20 novembre 2023

Analyse initiale avec Wireshark

On ouvre la capture réseau dans Wireshark et on remarque :

  • quelques trames ICMP (sûrement du ping) ;
  • beaucoup de trames DNS.

Les requêtes DNS semblent résoudre des noms de domaines très étranges. Par exemple la première requête DNS demande l’adresse IPv4 (type A, classe IN) associée au nom de domaine :

H4sICFctM2IAA3Bhc3N3ZACVVl9v4zYMf**n0OMG1H-.BsJ2mjt9sCbMNde0FT3Oug2GosxJY8SU7SffqRlPOv-.1nY3SHVFiqQo8icy1hjPj3wCw*IyDd*N0ulGuPquEr-.I1GiQyGAOR9s6mDkQuq1SbxmyVvkPukecwaD8u5N4d-.cGFzc3dk

On copie-colle ce nom de domaine dans CyberChef, puis on applique le filtre From Base64, CyberChef nous indique trouver un Gzip ! La description de l’épreuve indique qu’il faut décompresser, cela fait donc sens. L’utilisateur exfiltre donc du contenu compressé en Gzip en base64 à travers un serveur DNS.

On applique le filtre dns dans Wireshark, puis on exporte en JSON la dissection (File > Export Packet Dissections > As JSON...). Ainsi on va pouvoir travailler avec Python sur le contenu des trames DNS.

Première tentative de reconstruction

On écrit le script Python suivant pour afficher les requêtes DNS :

import json

# Load dissections
with open("dns.json", "r") as f:
    data = json.load(f)

# Print each DNS query
for pkt in data:
    dns_layer = pkt["_source"]["layers"]["dns"]
    if "Answers" in dns_layer:
        continue  # ignore answers
    dns_query = list(dns_layer["Queries"].values())[0]["dns.qry.name"]
    print(dns_query)

Ensuite on le lance et on essaie d’extraire l’archive :

$ python script.py > output.txt
$ base64 -d < output.txt | hexdump -C
base64: invalid input
00000000  1f 8b 08 08 57 2d 33 62  00 03 70 61 73 73 77 64  |....W-3b..passwd|
00000010  00 95 56 5f 6f e3 36 0c                           |..V_o.6.|
00000018

Mince ! La commande base64 s’est arrêtée à cause d’une mauvaise entrée *.

L’alphabet classique de la base64 est A-Za-z0-9+/=. Ici on reconnaît bien les A-Za-z0-9/=, mais il manque le +. On tente de remplacer les * par des +. On retente :

$ python script.py > output.txt
$ base64 -d < output.txt | hexdump -c
base64: invalid input
0000000 037 213  \b  \b   W   -   3   b  \0 003   p   a   s   s   w   d
0000010  \0 225   V   _   o 343   6  \f 177 357 247 320 343 006 324
000001f

Youpi ! On est allé légèrement plus loin, mais ce n’est toujours pas de la base64 valide. On retente en enlevant les -. :

$ python script.py > output.txt
$ base64 -d < output.txt > output.bin
$ file output.bin
output.bin: gzip compressed data, was "passwd", last modified: Thu Mar 17 12:45:11 2022, from Unix, original size modulo 2^32 845507689

Pas d’erreur, et on a bien un fichier compressé gzip. On tente de décompresser :

$ zcat output.bin
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:/u/bib
x in/sbin/D/u/4:65534:in/D//in:/bin:/in/D
[...] more garbage
gzip: output.bin: invalid compressed data--crc error

gzip: output.bin: invalid compressed data--length error

Arg ! On y était presque, mais le fichier final semble corrompu.

Seconde tentative de reconstruction

On retourne lire output.txt. On remarque la ligne nTrTr7DFlrCHTkdO+wfTUcW67goAAA==cGFzc3dk.

Cela ne fait pas beaucoup de sens en base64, car = est le caractère de padding. Donc à la fin de chaque ligne il y a 8 caractères « en trop ».

On tente de décoder ces 8 derniers caractères :

$ echo -n cGFzc3dk | base64 -d
passwd
$ echo -n ZmlsZTE= | base64 -d
file1
$ echo -n ZmlsZTM= | base64 -d
file3
$ echo -n ZmlsZTI= | base64 -d
file2

Donc les 8 derniers caractères correspondent au nom du fichier exfiltré. On modifie alors notre script précédent :

import json
from base64 import b64decode

# Load dissections
with open("dns.json", "r") as f:
    data = json.load(f)

# Print DNS querys
files = {}
for pkt in data:
    dns_layer = pkt["_source"]["layers"]["dns"]
    if "Answers" in dns_layer:
        continue  # ignore answers
    dns_query = list(dns_layer["Queries"].values())[0]["dns.qry.name"]
    b64_all = dns_query.replace("*", "+").replace("-.", "")

    # Last 8 char are filename in base64
    data, filename = b64decode(b64_all[:-8]), b64decode(b64_all[-8:]).decode()

    # Append data to filename
    files[filename] = files.get(filename, b"") + data

# Write files to disk
for filename, data in files.items():
    with open(filename, "wb") as f:
        f.write(data)

On l’exécute :

$ python script.py
$ file file* passwd
file1:  gzip compressed data, was "file1", last modified: Thu Mar 17 16:31:54 2022, from Unix, original size modulo 2^32 131579
file2:  gzip compressed data, was "file2", last modified: Thu Mar 17 16:32:21 2022, from Unix, original size modulo 2^32 11404
file3:  gzip compressed data, was "file3", last modified: Thu Mar 17 16:33:19 2022, from Unix, original size modulo 2^32 12654
file4:  gzip compressed data, was "file4", last modified: Thu Mar 17 16:31:00 2022, from Unix, original size modulo 2^32 4231
passwd: gzip compressed data, was "passwd", last modified: Thu Mar 17 12:45:11 2022, from Unix, original size modulo 2^32 2798
$ zcat passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
[...]
fv2:x:1000:1000:fv2,,,:/home/fv2:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
$ zcat file1 | file -
/dev/stdin: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 1667x1667, components 3
$ zcat file2 | file -
/dev/stdin: JPEG image data, baseline, precision 8, 181x66, components 4
$ zcat file3 | file -
/dev/stdin: PNG image data, 80 x 80, 8-bit/color RGBA, non-interlaced
$ zcat file4 | file -
/dev/stdin: Microsoft Word 2007+
$ zcat file4 > file4.doc

Parfait ! On a réussi à reconstruire les fichiers sans corrumption.

file1, file2 et file3 sont des images faisant référence au FCSC et à l’ANSSI. On ouvre file4.doc avec libreoffice et on obtient le flag : FCSC{18e955473d2e12feea922df7e1f578d27ffe977e7fa5b6f066f7f145e2543a92}.