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.
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.
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.
Avec ce que nous avons trouvé précédemment, on sait que:
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 ! :þ