Solution de lrstx pour CryptoLocker v2

forensics memory windows

14 juin 2024

Cette fois-ci, on va commencer par utiliser MemProcFs.

On regarde les process qui tournaient, via sys/proc.txt:

-- update_v0.8.ex             3596   1420     U* IEUser           2020-04-13 19:18:13 UTC                      ***

En effet, ça semble être de nouveau une mise à jour. On envoie le binaire name/update_v0.8.ex-3596/files/modules/update_v0.8.exe dans ghidra et après l’analyse automatique, on regarde un peu les fonctions définies. La première qui semble intéressante affiche le nom de l’outil :

undefined4 FUN_00401d3b(void)

{
  int iVar1;
  time_t tVar2;
  int local_14;
  
  FUN_00401fd0();
  puts("\tENCRYPTOR v0.8\n");
  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  for (local_14 = 0; local_14 < 0x32; local_14 = local_14 + 1) {
    iVar1 = rand();
    (&DAT_004063e0)[local_14] = (char)iVar1 + (char)(iVar1 / 0x19) * -0x19 + 'A';
  }
  DAT_00406412 = 0;
  FUN_00401b80("./");
  _DAT_004063e0 = 0x30303030;
  _DAT_004063e4 = 0x30303030;
  _DAT_004063e8 = 0x30303030;
  _DAT_004063ec = 0x30303030;
  _DAT_004063f0 = 0x30303030;
  _DAT_004063f4 = 0x30303030;
  _DAT_004063f8 = 0x30303030;
  _DAT_004063fc = 0x30303030;
  _DAT_00406400 = 0x30303030;
  _DAT_00406404 = 0x30303030;
  _DAT_00406408 = 0x30303030;
  _DAT_0040640c = 0x30303030;
  _DAT_00406410 = 0x30;
  puts(&DAT_00405070);
  getchar();
  return 0;
}

On voit qu’elle bricole des valeurs dans le buffer _DAT_004063e0 et lance une fonction avec en paramètre le répertoire courant. Ouvrons celle-ci :

void __cdecl FUN_00401b80(char *param_1)

{
  char cVar1;
  int iVar2;
  uint uVar3;
  char *pcVar4;
  undefined2 uStack_459;
  char local_358 [260];
  char acStack_254 [260];
  int local_150;
  int local_14c;
  char local_124 [260];
  int local_20;
  
  printf("[info] entering the folder : %s\n",param_1);
  FUN_00401530(local_124,param_1);
  do {
    if (local_20 == 0) {
      FUN_00401681(local_124);
      return;
    }
    FUN_0040179f(local_124,local_358);
    if (local_150 == 0) {
LAB_00401c8b:
      if (local_14c != 0) {
        iVar2 = strcmp(acStack_254,"flag.txt");
        if (iVar2 == 0) {
          strcpy((char *)((int)&uStack_459 + 1),param_1);
          strcat((char *)((int)&uStack_459 + 1),acStack_254);
          printf("[info] file encryptable found : %s\n",(int)&uStack_459 + 1);
          FUN_004019ab((char *)((int)&uStack_459 + 1));
        }
      }
    }
    else {
      iVar2 = strcmp(acStack_254,".");
      if (iVar2 == 0) goto LAB_00401c8b;
      iVar2 = strcmp(acStack_254,"..");
      if (iVar2 == 0) goto LAB_00401c8b;
      strcpy((char *)((int)&uStack_459 + 1),param_1);
      strcat((char *)((int)&uStack_459 + 1),acStack_254);
      uVar3 = 0xffffffff;
      pcVar4 = (char *)((int)&uStack_459 + 1);
      do {
        if (uVar3 == 0) break;
        uVar3 = uVar3 - 1;
        cVar1 = *pcVar4;
        pcVar4 = pcVar4 + 1;
      } while (cVar1 != '\0');
      *(undefined2 *)((int)&uStack_459 + ~uVar3) = 0x2f;
      FUN_00401b80((char *)((int)&uStack_459 + 1));
    }
    FUN_00401721((int)local_124);
  } while( true );
}

Toujours en lisant en diagonal, cette dernière semble lancer un traitement en présence d’un fichier flag.txt. Descendons encore d’un niveau :

void __cdecl FUN_004019ab(char *param_1)

{
  char cVar1;
  uint uVar2;
  char *pcVar3;
  char local_12c [256];
  size_t local_2c;
  size_t local_28;
  void *local_24;
  size_t local_20;
  FILE *local_1c;
  FILE *local_18;
  int local_14;
  uint local_10;
  
  strcpy(local_12c,param_1);
  uVar2 = 0xffffffff;
  pcVar3 = local_12c;
  do {
    if (uVar2 == 0) break;
    uVar2 = uVar2 - 1;
    cVar1 = *pcVar3;
    pcVar3 = pcVar3 + 1;
  } while (cVar1 != '\0');
  *(undefined4 *)(local_12c + (~uVar2 - 1)) = 0x636e652e;                               
  local_12c[~uVar2 + 3] = '\0';
  local_18 = fopen(param_1,"rb");
  local_1c = fopen(local_12c,"wb+");
  fseek(local_18,0,2);
  local_20 = ftell(local_18);
  local_24 = malloc(local_20);
  fseek(local_18,0,0);
  local_28 = fread(local_24,1,local_20,local_18);                                       
  _strrev(&DAT_004063e0);
  local_2c = strlen(&DAT_004063e0);
  for (local_10 = 0; (int)local_10 < (int)  ; local_10 = local_10 + 1) {
    *(byte *)((int)local_24 + local_10) =
         *(byte *)((int)local_24 + local_10) ^ (&DAT_004063e0)[local_10 % local_2c];    /* XOR avec DAT_004063e0 */
  }
  fseek(local_18,0,0);
  for (local_14 = 0; local_14 < (int)local_20; local_14 = local_14 + 1) {
    putc((int)*(char *)((int)local_24 + local_14),local_1c);
  }
  free(local_24);
  fclose(local_18);
  fclose(local_1c);
  remove(param_1);
  return;
}

À vue de nez, ça ressemble au chiffrement d’un fichier. On voit des ouvertures de fichiers, dont l’un porte le nom de celui passé en paramètre, complété par .enc (0x636e652e). On voit aussi des opérations XOR avec le buffer DAT_004063e0, généré dans la première fonction, et donc on a inversé l’ordre (_strrev).

La clef serait dans DAT_004063e0, revenons donc à sa génération :

  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  for (local_14 = 0; local_14 < 0x32; local_14 = local_14 + 1) {
    iVar1 = rand();
    (&DAT_004063e0)[local_14] = (char)iVar1 + (char)(iVar1 / 0x19) * -0x19 + 'A';
  }

On voit aussi qu’à la fin de la fonction, la clef est écrasée par des zéros, donc aucune chance de la retrouver dans la capture mémoire.

D’après la doc :

La fonction time retourne le nombre de secondes écoulées depuis minuit (00:00:00), le 1er janvier 1970, temps universel coordonné (UTC), d’après l’horloge système.

Or, on sait que le process a été lancé à la date et l’heure 2020-04-13 19:18:13 UTC. On a donc la graine (1586805493) qui permet de regénérer la série.

Par contre, le code décompilé ne semble pas avoir de sens. Le code assembleur est :

                             LAB_00401d73                                    XREF[1]:     00401dbd(j)  
        00401d73 e8 48 16        CALL       MSVCRT.DLL::rand                                 int rand(void)
                 00 00
        00401d78 89 c1           MOV        ECX,EAX
        00401d7a ba 1f 85        MOV        EDX,0x51eb851f
                 eb 51
        00401d7f 89 c8           MOV        EAX,ECX
        00401d81 f7 ea           IMUL       EDX
        00401d83 c1 fa 03        SAR        EDX,0x3
        00401d86 89 c8           MOV        EAX,ECX
        00401d88 c1 f8 1f        SAR        EAX,0x1f
        00401d8b 29 c2           SUB        EDX,EAX
        00401d8d 89 d0           MOV        EAX,EDX
        00401d8f c1 e0 02        SHL        EAX,0x2
        00401d92 01 d0           ADD        EAX,EDX
        00401d94 8d 14 85        LEA        EDX,[EAX*0x4 + 0x0]
                 00 00 00 00
        00401d9b 01 d0           ADD        EAX,EDX
        00401d9d 29 c1           SUB        ECX,EAX
        00401d9f 89 ca           MOV        EDX,ECX
        00401da1 89 d0           MOV        EAX,EDX
        00401da3 83 c0 41        ADD        EAX,0x41
        00401da6 89 c2           MOV        EDX,EAX
        00401da8 8b 44 24 1c     MOV        EAX,dword ptr [ESP + local_14]
        00401dac 05 e0 63        ADD        EAX,DAT_004063e0                                 = ??
                 40 00
        00401db1 88 10           MOV        byte ptr [EAX],DL=>DAT_004063e0                  = ??

Ça me semble différent du code décompilé, mais ça reste du chinois pour moi. En revanche, en recherchant la constante 0x51eb851f sur Internet, je découvre que c’est un trick de compilateur, et en fait une optimisation pour calculer un modulo. D’après l’explication, un SAR EDX,0x5 serait un modulo 100. On a un shift de 3 au lieu de 5, donc je suppose que ce serait un modulo 25. Pour confirmer, je regarde ce que donne la compilation du programme suivant sur Compiler Explorer :

#include <time.h>

int foo(int a)
{
   return a % 25;
}

On obtient alors :

foo:
        push    ebp
        mov     ebp, esp
        mov     ecx, DWORD PTR [ebp+8]
        mov     edx, 1374389535
        mov     eax, ecx
        imul    edx
        sar     edx, 3
        mov     eax, ecx
        sar     eax, 31
        sub     edx, eax
        mov     eax, edx
        sal     eax, 2
        add     eax, edx
        lea     edx, [0+eax*4]
        add     eax, edx
        sub     ecx, eax
        mov     edx, ecx
        mov     eax, edx
        pop     ebp
        ret

Et c’est exactement le code que présente ghidra ! Celui-ci, en plus, ajoute le code ASCII du caractère A à chaque octet de la clef. On a maintenant tous les éléments pour tenter de reconstruire la clef. On se fait un petit programme en C :

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    srand(1586805493);
    for(unsigned int i=0; i<0x32; i++) {
        int r = rand();
        r = r%25;
        printf("%d,", r);
    }
    printf("\n");
}
 gcc -Wall rand.c -o rand && ./rand 
16,9,18,18,2,18,0,22,14,4,13,0,3,18,13,0,5,16,8,22,18,16,12,19,4,11,6,15,14,4,6,5,13,24,0,18,18,0,17,7,5,5,7,10,24,22,10,6,15,20,

Mais en fait, ça ne marchera pas. En effet, l’implémentation de rand() est différente entre Windows et Linux. Heureusement, stackoverflow vient encore à notre secours en fournissant un équivalent de l’implémentation Windows :

#include <stdio.h>

int random_seed = 0;
void srand(int seed) { 
    random_seed = seed; 
} 
int  rand(void) { 
    random_seed = (random_seed * 214013 + 2531011) & 0xFFFFFFFF; 
    return (random_seed >> 16) & 0x7FFF; 
}

int main(int argc, char **argv) {
    srand(1586805493);
    for(unsigned int i=0; i<0x32; i++) {
        int r = rand();
        r = r%25;
        printf("%d,", r);
    }
    printf("\n");
}
 gcc -Wall rand.c -o rand && ./rand 
22,14,14,0,8,14,12,0,0,11,18,12,20,13,23,8,10,11,13,12,1,10,17,9,5,21,0,3,16,2,5,4,15,18,5,24,24,9,23,10,15,14,15,21,17,22,20,7,19,9,

On est prêt à déchiffrer le fichier ! Que l’on n’a pas sous la main, encore. Et là, déception, impossible de le trouver avec MemProcFs. Du coup, on bascule sur Volatility3 :

$ /opt/volatility3/vol.py -f cryptolocker-v2.dmp windows.filescan.FileScan | grep flag
0x3eaec938 100.0\Users\IEUser\Desktop\flag.txt.enc	128

$ /opt/volatility3/vol.py -f cryptolocker-v2.dmp windows.dumpfiles.DumpFiles --physaddr 0x3eaec938
DataSectionObject	0x3eaec938 flag.txt.enc file.0x3eaec938.0x850eb660.DataSectionObject.flag.txt.enc.dat

On a maintenant tout ce qu’il nous faut. Je décide de déchiffrer le flag en utilisant Python, en n’oubliant pas que tous les octets de la clef ont été augmenté du code ASCII de A, qu’elle a été renversée et qu’elle doit être d’une longueur suffisant pour déchiffrer le flag complètement :

>>> encrypted = open('file.0x3eaec938.0x850eb660.DataSectionObject.flag.txt.enc.dat', 'rb').read()
>>> key = reversed([ 22,14,14,0,8,14,12,0,0,11,18,12,20,13,23,8,10,11,13,12,1,10,17,9,5,21,0,3,16,2,5,4,15,18,5,24,24,9,23,10,15,14,15,21,17,22,20,7,19,9 ])
>>> key = [ x+ord('A') for x in key ]
>>> key = key + key
>>> bytes( [ x^y for x,y in zip(encrypted, key) ] )
b'FCSC{93bcf2f427e455685b0580058ba028a0a6f96b42c7336ea13877be5e648aec42}\nQDAVFJRKBMNLKIXNUMSLAAMOIAOOW'

Bingo !