Plausible Mission - Shadow Protocol

Inigo A-H., Ugo M., Mattéo B., Yves-Antoine G., Eliot S., Guilhem C.

← Plausible Mission - Shadow Protocol · Le papier ne ment jamais / L'intrusion / Une visite sur le serveur de fichiers / Le retournement

Ce challenge demande de récupérer des données chiffrées par un ransomware.

Lors de l’analyse du serveur de fichiers, il a été constaté que toutes les copies du film ont été chiffrées par un ransomware, et que les sauvegardes ont été supprimées. L’objectif est donc de récupérer la clé de déchiffrement pour pouvoir restaurer les documents.

Si vous extrayez des binaires, il est très important de ne pas exécuter les binaires extraits sur vos machines principales surtout si vous êtes sur un système Windows.

Cote 95 pts

Indices

Le binaire est constitué des deux fonctions principales, une pour créer la clé de chiffrement et une autre pour chiffrer les fichiers.

Faire son rapport

Résolution de l'exercice 4

Analyse de la copie de la mémoire

volatility3 : Installation

L'outil volatility3 peut être installé avec le script suivant :

git clone https://github.com/volatilityfoundation/volatility3.git
cd volatility3/
python3 -m venv venv && . venv/bin/activate
pip install -e ".[dev]"

Base de l'investigation

Pour commencer, rechercher un processus potentiellement responsable du
chiffrement.
pslist permet d'énumérer les processus actifs :

./volatility3/vol.py -f dump_srv2.dmp windows.pslist

En parcourant la sortie, on remarquera un processus au nom inhabituel
VwXIz5Kj3V5NMS (PID 4444).
Pour vérifier son contexte (parent/enfants, threads), afficher l'arborescence
des processus :

./volatility3/vol.py -f dump_srv2.dmp windows.pstree --pid 4444

Extrait de la sortie pstree :

Volatility 3 Framework 2.27.0
Progress:  100.00       PDB scanning finished                                
PID PPID    ImageFileName   Offset(V)   Threads Handles SessionId   Wow64   CreateTime  ExitTime    Audit   Cmd Path

760 684 winlogon.exe    0xcb8f884a9080  6   -   1   False   2025-10-10 07:19:40.000000 UTC  N/A \Device\HarddiskVolume3\Windows\System32\winlogon.exe   winlogon.exe    C:\WINDOWS\system32\winlogon.exe
* 5244  760 userinit.exe    0xcb8f899750c0  0   -   1   False   2025-10-10 07:19:53.000000 UTC  2025-10-10 07:20:27.000000 UTC  \Device\HarddiskVolume3\Windows\System32\userinit.exe   -   -
** 5300 5244    explorer.exe    0xcb8f897ca080  60  -   1   False   2025-10-10 07:19:53.000000 UTC  N/A \Device\HarddiskVolume3\Windows\explorer.exe    C:\WINDOWS\Explorer.EXE C:\WINDOWS\Explorer.EXE
*** 1232    5300    powershell.exe  0xcb8f8974b080  12  -   1   False   2025-10-10 07:20:54.000000 UTC  N/A \Device\HarddiskVolume3\Windows\System32\WindowsPowerShell\v1.0\powershell.exe  "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"     C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
**** 4444   1232    VwXIz5Kj3V5NMS  0xcb8f8cfce080  4   -   1   False   2025-10-10 07:22:21.000000 UTC  N/A \Device\HarddiskVolume3\Users\SRV2\VwXIz5Kj3V5NMScrmw.exe   "C:\Users\SRV2\V...exe" C:\Users\SRV2\V...exe

On constate que VwXIz5Kj3V5NMS est un enfant de powershell.exe et que le
chemin complet de l'exécutable est C:\Users\SRV2\V...exe, cela paraît
suspect.

À partir de là, l'étape suivante est d'extraire le binaire du processus pour
l'analyser.

Trouver l'offset du fichier en mémoire et récupérer le binaire

Lister les objets fichiers en mémoire et rechercher l'offset correspondant :

./volatility3/vol.py -f dump_srv2.dmp windows.filescan 2>/dev/null \
  | grep -m1 VwXIz5Kj3V5NMS \
  | awk '{print $1}'

Résultat observé : 0xcb8f8a42b690

Récupérer le fichier à cet offset :

./volatility3/vol.py -f dump_srv2.dmp windows.dumpfiles --virtaddr 0xcb8f8a42b690

Cela créera une copie mémoire dans le répertoire courant, par exemple :
file.0xcb8f8a42b690.0xcb8f8c85ed70.ImageSectionObject.VwXIz5Kj3V5NMScrmw.exe.img


Analyse du binaire

Importer le binaire extrait dans un outil de décompilation (Ghidra ou IDA Pro).
Ici nous utilisons Ghidra pour la décompilation et l'analyse des fonctions.

Repérage du main.

En explorant le Function Call Tree on identifie la fonction principale qui
appelle deux fonctions intéressantes.

Fonction principale

La fonction main (FUN_7ff72a802710) appelle notamment FUN_7ff72a801f90 et
FUN_7ff72a8025d0, ce sont les fonctions que nous analysons en priorité.

Fonction principale 2


Analyse détaillée de FUN_7ff72a801f90.

Cette fonction semble récupérer des informations sur le processus (PID et date
d'exécution), puis appeler trois fonctions utilitaires :

  • FUN_7ff72a801aa0 : renvoie la liste des modules (DLL) chargés par le
    processus.
  • FUN_7ff72a801d40 : renvoie la liste des variables d'environnement du
    processus.
  • FUN_7ff72a8010a0 : prends en entrée les listes de modules, d'environnement,
    l'heure d'exécution et écrit ses sorties dans deux buffers (probablement la
    clé et l'IV dérivés).

Génération des clés

FUN_7ff72a8010a0 dérivation de clés

La fonction FUN_7ff72a8010a0 se découpe en quatre parties observables :

  1. Concaténation : transforme les modules, variables d'environnement et la
    date en une chaîne de caractères (jusqu'à la ligne 189 du binaire décompilé).
  2. Buffer formaté : construit un buffer déterministe préfixé, avec des
    préfixes MOD|, ENV| et DATE| pour les sections correspondantes (lignes
    ~190–211).
    Fonction de dérivation des clés
  3. Empreinte : calcule un SHA-256 du buffer et stocke le résultat dans les
    structures passées en argument. Dans le décompilé on repère clairement BCrypt
    SHA-256 :

c NVar3 = BCryptOpenAlgorithmProvider(&local_d8,L"SHA256",(LPCWSTR)0x0,0); NVar3 = BCryptHashData(local_c8,pbInput,(ULONG)lVar5,0);

Ce qui confirme l'utilisation de SHA-256 sur local_c8 (le tampon construit
précédemment).
4. Sortie : écrit la valeur de 32 octets (256 bits) et une autre de 16 (128)
dans les tableaux fournis en argument, vraisemblablement la clé AES-256 (32
o.) et l'IV (16 o.).
Sortie du programme

Conclusion : la fonction dérive deux clés (32 et 16 octets) à partir des
modules, variables d'environnement et de la date d'exécution en appliquant
SHA-256 à un buffer formaté.


Analyse de FUN_7ff72a8025d0.

Cette fonction parcourt l'arborescence (ici depuis C:\), examine chaque sous
dossier et filtre ceux qui correspondent à un pattern donné
(*plausible*;*mission*;*shadow*;*protocol*).
Si un correspond, elle appelle FUN_7ff72a802460 pour descendre dans le
dossier.

Recherche de fichiers

FUN_7ff72a802460 / FUN_7ff72a802240.

FUN_7ff72a802460 liste les fichiers dans le répertoire courant et, pour chaque
fichier, appelle FUN_7ff72a802240.

FUN_7ff72a802240 est la fonction de chiffrement.
Les indices importants observés dans le décompilé :

  • uVar6 = EVP_aes_256_cbc();: l'algorithme utilisé est AES-256 en mode
    CBC
    .
  • L'appel au chiffrement utilise la clé de 256 et l'IV de 128 bits produits par
    FUN_7ff72a8010a0.

Conclusion : FUN_7ff72a802240 chiffre les fichiers trouvés dans les dossiers
correspondant au pattern, en AES-256 CBC, avec la clé/IV dérivées
précédemment.


Conclusion fonctionnelle

Pour déchiffrer les fichiers chiffrés par ce binaire, il faut reproduire
exactement la dérivation de clés :

  1. Extraire la liste des modules chargés par le processus.
  2. Extraire les variables d'environnement du processus.
  3. Récupérer la date/heure d'exécution du programme (au format exact attendu).
  4. Construire le buffer avec les mêmes préfixes et le même ordre (MOD|...,
    TIME|/DATE|..., ENV|...) et appliquer SHA-256.
  5. Prendre les 32 octets résultants comme clé AES-256 et les 16 premiers comme
    IV.
  6. Déchiffrer les fichiers en AES-256 CBC.

Récupération des informations du processus (volatility3)

Modules chargés

./volatility3/vol.py -f dump_srv2.dmp windows.dlllist --pid 4444

Variables d'environnement

./volatility3/vol.py -f dump_srv2.dmp windows.envars.Envars --pid 4444

Date/heure d'exécution

Exemple pour retrouver la ligne pslist associée :

./volatility3/vol.py -f dump_srv2.dmp windows.pslist | grep -i "VwXIz5Kj3V5NMS"

Formatage attendu des informations

Le binaire construit un buffer avec des préfixes ; le format attendu est du type :

MOD|<module1>
MOD|<module2>
...
TIME|YYYY-MM-DDTHH:MM:SS
ENV|VAR1=value1
ENV|VAR2=value2
...

(Il est crucial de reproduire exactement l'ordre, le découpage, l'encodage et
l'absence/présence d'éventuels retours chariot.)


Script d'extraction + dérivation

Le code ci-dessous automatise la récupération et le formatage des informations
depuis la copie mémoire, produit le tampon, calcule l'empreinte SHA-256,
l'affiche complètement et ses 16 premiers octets en hexadécimal.
Adaptez process_name et dump_file selon votre environnement.

#!/bin/bash
set -euo pipefail

# If volatility3 is not installed, install it using git
if [ ! -d "./volatility3" ]; then
  git clone https://github.com/volatilityfoundation/volatility3.git
  cd volatility3/
  python3 -m venv venv && . venv/bin/activate
  pip install -e ".[dev]"
  cd ..
fi

process_name='VwXIz5Kj3V5NMS'
dump_file='./dump/dump_srv2.dmp'

# Produce ISO time without milliseconds
format_time() {
  local t="$1"
  if [[ -z "$t" ]]; then
    date '+%Y-%m-%dT%H:%M:%S'
    return
  fi
  if [[ "$t" =~ ^[0-9]+$ ]]; then
    local secs="$t"
    date -d "@$secs" '+%Y-%m-%dT%H:%M:%S'
    return
  fi
  if [[ "$t" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,6})?$ ]]; then
    echo "${t%%.*}"
    return
  fi
  if parsed=$(date -d "$t" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null); then
    echo "$parsed"
    return
  fi
  echo "Invalid time format: $t" >&2
  return 1
}

# Read and sort entries (case-insensitive)
sort_lines() {
  local file="$1"
  if [[ -z "$file" ]]; then
    return 0
  fi
  iconv -f utf-8 -t utf-8//IGNORE "$file" 2>/dev/null | sed 's/\r$//' | sort -f
}

# Inline derive_keys functionality: build deterministic buffer and print SHA256
derive_keys() {
  local mods_file="$1" env_file="$2" ptime="$3"
  local tmpbuf
  tmpbuf=$(mktemp)
  trap 'rm -f "$tmpbuf"' RETURN

  if [[ -n "$mods_file" && -f "$mods_file" ]]; then
    while IFS= read -r line; do
      printf '%s\n' "$line"
    done < <(sort_lines "$mods_file") | while IFS= read -r m; do
      printf 'MOD|%s\n' "$m" >>"$tmpbuf"
    done
  fi

  local time_iso
  time_iso=$(format_time "$ptime")
  if [[ $? -ne 0 ]]; then return 2; fi
  if [[ -n "$time_iso" ]]; then
    printf 'TIME|%s\n' "$time_iso" >>"$tmpbuf"
  fi

  if [[ -n "$env_file" && -f "$env_file" ]]; then
    while IFS= read -r line; do
      printf '%s\n' "$line"
    done < <(sort_lines "$env_file") | while IFS= read -r e; do
      printf 'ENV|%s\n' "$e" >>"$tmpbuf"
    done
  fi

  local buf_content
  buf_content=$(cat "$tmpbuf")
  # Print the buffer and compute sha256
  printf '%s' "$buf_content"
  local sha_hex
  if command -v sha256sum >/dev/null 2>&1; then
    sha_hex=$(printf '%s' "$buf_content" | sha256sum | awk '{print $1}')
  elif command -v openssl >/dev/null 2>&1; then
    sha_hex=$(printf '%s' "$buf_content" | openssl dgst -sha256 -r | awk '{print $1}')
  else
    echo "No sha256 utility (sha256sum or openssl) found" >&2
    return 3
  fi
  local first16_hex=${sha_hex:0:32}
  printf 'SHA256: %s\n' "$sha_hex"
  printf 'FIRST_16_BYTES(hex): %s\n' "$first16_hex"
  return 0
}

# Get pid of the process
pid=$(./volatility3/vol.py -f "$dump_file" windows.pslist | grep -i "$process_name" | awk '{print $1}')

# DLL list (drop first 4 header lines, case-insensitive sort)
dll_loads=$(./volatility3/vol.py -f "$dump_file" windows.dlllist --pid $pid | awk '{print $6}' | sed '1,4d' | sed '/^[[:space:]]*$/d' | sort -f)

# produce YYYY-MM-DDTHH:MM:SS
date_time=$(./volatility3/vol.py -f "$dump_file" windows.pslist | grep -i "$process_name" | awk '{
    d=$9; t=$10;
    split(t,a,"[.]");
    sec=a[1];
    printf "%sT%s", d, sec;
}')

# Run the volatility3 command to extract environment variables
environment_vars=$(./volatility3/vol.py -f "$dump_file" windows.envars.Envars --pid $pid | awk '/^[[:space:]]*[0-9]+[[:space:]]/ { var=$4; $1=$2=$3=$4=""; sub(/^ +/,""); print var"="$0 }' | sed '/^[[:space:]]*$/d' | sort -f)

# Prepare temporary files for modules and environment variables, then run derive_keys
mods_tmp=$(mktemp)
env_tmp=$(mktemp)
trap 'rm -f "$mods_tmp" "$env_tmp"' EXIT

if [ -n "$dll_loads" ]; then
  printf '%s\n' "$dll_loads" >"$mods_tmp"
else
  : >"$mods_tmp"
fi

if [ -n "$environment_vars" ]; then
  printf '%s\n' "$environment_vars" >"$env_tmp"
else
  : >"$env_tmp"
fi

echo "Derived keys (from extracted process info):"
derive_keys "$mods_tmp" "$env_tmp" "$date_time" || echo "derive_keys failed"

# Dump the binary of the process
offset_file=$(./volatility3/vol.py -f dump/dump_srv2.dmp windows.filescan 2>/dev/null \
  | grep -m1 VwXIz5Kj3V5NMS \
  | awk '{print $1}')

./volatility3/vol.py -f "$dump_file" windows.dumpfiles --virtaddr "$offset_file"

Ce script :

  • Récupère PID, DLL, variables d'environnement et la date d'exécution depuis la
    copie de la mémoire,
  • Construit le buffer trié et déterministe,
  • Calcule et affiche le SHA-256 et les 16 premiers octets hexadécimaux (utiles
    comme IV),
  • Copie le binaire du processus.

Script d'exemple pour le déchiffrement

Voici un code Python (utilisant pycryptodome) pour déchiffrer un fichier AES
CBC.
Remplacer key et iv par les valeurs dérivées trouvées précédemment et
initialiser encrypted_file / decrypted_file avant d'exécuter.

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_file(input_path: str, output_path: str, key: bytes, iv: bytes):
    """Déchiffre un fichier AES-CBC"""
    cipher = AES.new(key, AES.MODE_CBC, iv)

    with open(input_path, "rb") as f_in:
        ciphertext = f_in.read()
        plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

    with open(output_path, "wb") as f_out:
        f_out.write(plaintext)

    print(f"[+] Fichier déchiffré : {output_path}")


if __name__ == "__main__":
    # Remplacer par les valeurs réellement dérivées
    key = bytes.fromhex("8dac...") # 32 octets (256 bits)
    iv  = bytes.fromhex("8dac...") # 16 octets (128 bits)
    encrypted_file = "script.odt"
    decrypted_file = "script1.odt"
    decrypt_file(encrypted_file, decrypted_file, key, iv)

Résumé technique

  • La dérivation de clés se fait par SHA-256 d'un buffer construit à partir des
    modules chargés, des variables d'environnement et de la date d'exécution
    (format/délimiteurs précis = éléments critiques).
  • Le chiffrement appliqué aux fichiers est AES-256 en mode CBC, en utilisant la
    clé 256-bit et l'IV 128-bit produits par la dérivation.
  • Le déchiffrement est possible si l'on reproduit fidèlement la construction du
    buffer et le calcul du SHA-256.