aaSSfxxx se remet au sport

Written by aaSSfxxx -

Après presque deux ans a n'avoir rien glandé (hormis ma participation à deux ou trois CTF cette année) et rien écrit sur mon blog, il est temps de passer aux choses sérieuses, et faire un petit article histoire de se remettre au sport.

Comme certains l'ont remarqué sur les toilettes des Internets Twitter ainsi que Discord, je m'amuse en ce moment à installer des vieux UNIXes qui datent pour certains d'avant ma naissance, principalement pour le lulz.

Je suis donc tombé sur un ensemble d'images de disquettes pour installer un SCO UNIX 3.2v4.2, sauf que manque de bol, le serial donné sur WinWorld ne fonctionnait pas, et de même pour un lâcher de serial keygennés sur un obscur post de newsgroup datant de l'époque des dinosaures. Comme tout bon reverser qui se respecte, péter du serial est une raison de vivre, du coup let's go au pays des merveilles.

Alice au pays de XENIX

Il est désormais temps de sortir notre bon vieux IDA cracké Ghidra pour passer aux choses sérieuses. Nous allons regarder le binaire "brandy", qu'il va falloir extraire de l'image disquette "M1.IMG", qui se décompresse très bien avec tar. Une fois muni de notre fichier, il va falloir le décompresser (les fichiers sont compressés pour en faire tenir plus sur une disquette), ce que gzip parvient également à faire très bien.

Ouvrons donc le binaire dans Ghidra (ouais, j'essaie de changer mon avis sur cet outil malgré le fait qu'il soit écrit en Java). On tombe sur l'entrypoint, et on voit un uVar1 = FUN_0000026b(); qui ressemble fortement à la fonction main du programme. Changeons son prototype directement, puis regardons ce que nous renvoie le décompilateur. On trouve pas mal de fonctions en FUN_a0000XXX, qui lorsqu'on double-clique dessus, renvoient vers une zone non initialisée dans le listing.

En effet, ce sont des adresses de /shlib/libc_s.so, l'implémentation de la libc de SCO, qui a une gestion très primitive de l'édition de liens dynamique: la libc sera chargée à une adresse fixe, et aura toutes ses fonctions exportées à la même adresse (bien que la fonction "exportée" ne soit qu'un saut vers la fonction réelle).

Cependant, récupérer cette libc à notre étape de l'aventure va être un peu compliqué (elle n'est présente que sur les disquettes N1.IMG et N2.IMG qui contiennent un filesystem qu'un Linux récent n'arrive plus à monter). On va donc y aller à la bite et au couteau, et essayer de deviner le nom des fonctions à partir de leurs arguments et le rôle qu'elles jouent dans le code.

Pour FUN_a00000d7(argc,argv,"lpnsqicb:G"), il s'agit probablement de la fonction "getopt", pareil pour la fonction ci-dessous qui ressemble bien à "fprintf".

      FUN_a0000096(&DAT_004026a8,
                   "Usage: %s [-p] [-l] [-n] [-s] [-q] [-b prd bundlelist] serialno activationkey fi le\n"
                   ,*(undefined4 *)argv);

Après avoir renommé un peu quelques fonctions tout ça, on tombe sur ce bloc de code qui a l'air fort intéressant:

    cur_args_count = argc - optind;
    current_args = argv + optind;
    strncpy(serialno_ak,*current_args,10);
    do_the_hustle(current_args[1]);
    strncat(serialno_ak,current_args[1],9);
    argv = current_args + 2;
    argc = cur_args_count + -2;
    cur_args_count = compare_checksum(serialno_ak);
    if (cur_args_count == 0) {
      if (_flag_q == 0) {
        fprintf(&stderr,"Invalid Activation Key\n");
      }
      exit(2);
    }

L'outil récupère donc le serial number et "l'activation key" sur la ligne de commande, puis fait des carabistouilles avec pour nous dire si c'est bon ou si GTFO. Regardons plus en détail la fonction que j'ai nommé do_the_hustle:

  chksum = 0;
  for (ak_end = (byte *)ak; *ak_end != 0; ak_end = ak_end + 1) {
  }
  for (; ak <= ak_end; ak_end = ak_end + -1) {
    if ((ascii_flags[*ak_end] & 2) == 0) {
      tmp = (int)(char)*ak_end;
    }
    else {
      tmp = (int)(char)*ak_end;
      lower_offset = (char)chksum;
      if (tmp - chksum < L'a') {
        lower_offset = (*ak_end - lower_offset) + '\xb9'; /* - 'G' */
      }
      else if (((char)*ak_end - chksum) + L'\xffffff9f' < 26) {
        lower_offset = (*ak_end - lower_offset) + '\x9f'; /* - 'a' */
      }
      else {
        lower_offset = (*ak_end - lower_offset) + '\x85'; /* - '{' */
      }
      *ak_end = lower_offset + 'a';
    }
    chksum = (tmp + chksum) % 26;
  }

La variable ascii_flags ayant le flag '2' positionné uniquement sur les lettres minuscules, on en déduit que ascii_flags[(byte)*ptr] & 2) == 0 peut être réécrit en !islower(*ptr). Ainsi, notre fonction ne va traiter que les caractères minuscules. Dans le cas où on a un caractère en minuscules, on voit que le le code ASCII du caractère 'G' (71) correspond au code ASCII de la lettre 'a' (97) - 26, soit le nombre de lettres de l'alphabet :þ. De même, le code ASCII du caractère '{' correspond au code ASCII de la lettre 'a' + 26.

En faisant un peu de maths niveau collège, on peut facilement réécrire ce bloc de code en la version ci-dessous, et s'épargner une imbrication de "if":

char *ptr = ak + strlen(ak);
char acc = 0;
while(ak <= ptr) {
  if(islower(*ptr)) {
    char tmp = ((*ptr - acc - 'a' + 26) % 26) + 'a';
    acc = (acc + *ptr) % 26;
    *ptr = tmp;
  }
  else {
    acc = (acc + *ptr) % 26;
  }
  ptr--;
}

Cette "activation key" déchiffrée va être ensuite concaténée au "serial number" dans la fonction main, puis le buffer résultant sera transmis en paramètre de la fonction "compare_checksum", que nous allons examiner.

Cette fonction recopie la chaîne dans un buffer local, avant d'invoquer une fonction checksum, qui fait ceci:

char * checksum(char *str)

{
  ushort chksm;
  int roundz;
  int i;
  char *loopvar;

  chksm = 0;
  loopvar = str + 8;
  for (; roundz = (int)(*loopvar % 16), *str != 0; str = str + 1) {
    chksm = chksm + (short)*str;
    chksm = (ushort)((chksm & 0x8000) != 0) | chksm * 2;
  }
  while (roundz != 0) {
    chksm = (ushort)((chksm & 0x8000) != 0) | chksm << 1;
    roundz = roundz + -1;
  }
  for (i = 1; -1 < i; i = i + -1) {
    chksum_buf[i] = (char)((uint)chksm % 26) + 'a';
    chksm = chksm / 26;
  }
  chksum_buf[2] = 0;
  return chksum_buf;
}

Puis, la fonction va comparer les deux derniers caractères de notre entrée avec le buffer retourné par checksum:

  ptr = checksum(local_buf);
  local_buf_len = 0;
  while( true ) {
    if (1 < local_buf_len) {
      return 1;
    }
    iVar1 = strlen(serialno_ak);
    if (serialno_ak[local_buf_len + iVar1 + -2] != ptr[local_buf_len]) break;
    local_buf_len = local_buf_len + 1;
  }

Ces algorithmes proviennent directement du mécanisme de vérification de licence de XENIX, l'ancêtre de SCO UNIX (qui n'est globalement qu'un XENIX amélioré et dont SCO a obtenu les droits pour l'appeler "UNIX"). Cependant, la méthode pour keygen notre Saint Graal, diffère de celle de XENIX (pour les curieux, regarder par ici peut être cool).

Sans plus attendre, passons aux choses sérieuses.

Cryptobat, qui peut te battre !

Maintenant qu'on a réussi à décoder notre "activation key", il faut comprendre comment elle est utilisée dans le processus de vérification/licensing de SCO UNIX (parce que là, n'importe quel couple serial number/activation key avec un checksum valide devrait permettre l'installation, sauf que ce n'est évidemment pas le cas :þ)

Dans notre fonction main, peu après la cuisine vue précédemment, on remarque ce bout de code qui a l'air fort intéressant:

    if (_flag_b != 0) {
      strncpy(pk,serialno_ak + 9,3);
      pk[3] = '\0';
      strcat(pk,"Tb");
      dec_serial = get_decrypted_bundle_serial(prd,pk,bundlelist);
      if (dec_serial == (char *)0x0) {
        exit(7);
      }
      tmp = check_function(serialno_ak,dec_serial);
      if (tmp == 0) {
        exit(7);
      }
      strncpy(serialno_ak,tmp,0x11);
    }

Le code va passer les 3 premiers octets de l'activation key déchiffrée à la fonction get_decrypted_serial, ainsi qu'un nom de prd, et un FILE* ouvert sur un fichier de bundlelist initialisé plus haut, lors du parsing des arguments:

      else if (tmp == L'b') {
        _flag_b = _flag_b + 1;
        prd = optarg;
        bundlelist = (void *)fopen(argv[optind],"r");
        if (bundlelist == (void *)0x0) {
          print_error(argv[optind]);
          exit(7);
        }
        optind = optind + 1;
      }

Regardons plus en détail la tête de ce fichier, qu'on trouve dans l'archive tar de l'image disquette M1.IMG, dans ./tmp/perms/bundle/netos:

macropkg="OS Services" : comp="SCO UNIX System V Runtime System" : \
 prd=unixrts : rel=3.2.4l : mapping=1 : compsize=15768 : vols=3 : \
 mdperms=/etc/perms/rtsmd : mdchar=N : pkgreq=RTS : perms=./tmp/perms/rts : \
 serial="^=Wy2-5_7._/-`^4-,XMNOIA,1" : char=B
#

On y voit notamment ce fameux serial="^=Wy2-5_7._/-`^4-,XMNOIA,1", qui va être extrait par la fonction get_decrypted_bundle_serial, après avoir parsé ce fichier bundle. Une fois extrait, decode_bundle_serial sera appelé pour décoder cette chaîne incompréhensible. Regardons donc ce qu'elle fait:

int decode_bundle_serial(char *serial,char *key)
{
  int dec_len;

  dec_len = custom_uudecode(serial);
  if (dec_len == -1) {
    dec_len = -1;
  }
  else {
    enigma_decrypt(decoded_bundle_serial,key,0);
    decoded_bundle_serial[dec_len] = '\0';
  }
  return dec_len;
}

Notre serial est donc encodé avec l'algorithme UUEncode, à ceci près que les caractères ", \ et : ont été substitués par x, y, z quand le serial a été encodé (et notre fonction applique la substitution avant de décoder la chaîne).

Une fois le serial décodé, il sera déchiffré par une variante de l'algorithme Enigma, étendu sur 256 valeurs (1 octet) dont la clé est la chaîne de trois caractères extraite de l'activation key déchiffrée, concaténée avec Tb. Cet article devenant assez long, je détaillerai peut-être le fonctionnement de l'algorithme Enigma dans un prochain article (sinon utilisez votre moteur de recherche favori :þ).

Une fois proprement décodé, notre serial (enfin celui issu d'un dump de SCO OpenDesktop dont un serial a été publié), ressemble à quelque chose comme ceci: zen,1,0,2,3,1,1,3. La "zen" ressemble étrangement à une "product key" et l'algorithme pour keygen se profile assez facilement.

Keygen like a tapz

Avec ce que nous avons trouvé précédemment, on sait que:

  • Le "serial number" (de 9 caractères) ainsi que "l'activation key" (de 8 caractères) sont concaténés, puis l'activation key est déchiffrée.
  • Les 2 derniers caractères de l'activation key déchiffrée sont un checksum du concaténat, pour vérifier que la clé est valide.
  • Les 3 premiers caractères de l'activation key sont le product code qui sera utilisé pour déchiffrer le "serial" présent dans le fichier /etc/perms/bundle de la disquette d'installation M1.
  • Le "serial" du bundle est encodé avec une version "personnalisée" de UUencode et chiffrée avec une implémentation "maison" de Enigma, dont la clé est les trois premiers caractères de l'activation key déchiffrée.
  • Les 3 premiers caractères du serial déchiffré sont le product code final suivi d'une liste d'entiers séparés par des virgules

Sachant tout ça, un algorithme pour péter cette vérification de serial serait en pseudo-code pythonisé:

sn = sys.argv[1]
bundleserial = sys.argv[2]
for i in range(26):
  for j in range(26):
    for k in range(26):
      pk = chr(i + 97) + chr(j + 97) + chr(k + 97)
      dec = enigma(bundleserial, pk + "Tb")
      if islower(dec[0]) and islower(dec[1]) and islower(dec[2]) and dec[3] == ",":
        newser = sn + pk + "aaa"
        newser += checksum(newser)
        print("%s - %s is a valid serial" % (sn, encrypt_ak(newser[9:])))

En faisant tourner le keygen, on trouve par exemple: aSSfxxx1:qzfxpysu. Le code du keygen (Python) est disponible sur ce lien.

That's all folks ! :þ