Crushing is an Easy reversing challenge. Players will reverse engineer a 'compression' algorithm, then write a script to recover the original message.
Analyse du code
Dans le main, on voit que l’on initialise un tableau de 256 éléments, tous à 0.
Ensuite on rentre dans un while, tant que getchar() ne retourne pas -1 alors on appelle la fonction add_to_char_map avec notre tableau, le caracètre récupéré et le numéro du tour de boucle auquel nous somme.
On comprend donc par le nom de la fonction que notre tableau est une map à laquelle on va ajouter l’index de notre char
int main(void) {
long i;
undefined8 *key;
undefined8 char_map [256];
int c;
long index;
key = char_map;
for (i = 0xff; i != 0; i = i + -1) {
*key = 0;
key = key + 1;
}
index = 0;
while( true ) {
c = getchar();
if (c == -1) break;
add_char_to_map((long)char_map,(byte)c,index);
index = index + 1;
}
serialize_and_output((long)char_map);
return 0;
}
On peut le confirmer en regardant la fonction add_to_char_map qui fait bien ce que l’on imaginait
void add_char_to_map(long char_map,byte c,undefined8 index) {
undefined8 *node;
long char_map_entry;
// Récupère la case du tableau en fonction du char passé en paramètre, en gros char_map[c]
char_map_entry = *(long *)(char_map + (ulong)c * 8);
// Créé un tableau de 2 items
node = (undefined8 *)malloc(0x10);
// Le premier item est l'index de notre char
*node = index;
// La seconde est initialisé à 0, ce sera la référence à notre prochaine node
node[1] = 0;
// Si notre char_map[c] est vide, on ajoute la réf de notre node dedans
if (char_map_entry == 0) {
*(undefined8 **)((ulong)c * 8 + char_map) = node;
}
// Sinon on parcours node après node (c'est une liste chainée) pour arriver jusqu'à la dernière
else {
for (; *(long *)(char_map_entry + 8) != 0; char_map_entry = *(long *)(char_map_entry + 8)) {
}
// On ajoute la réf de notre nouvelle node à la dernière node
*(undefined8 **)(char_map_entry + 8) = node;
}
return;
}
Exemple : Charmap de BAKA
La charmap ressemblera à :
{
"0": [],
"1": [],
...SNIP...
"65": [1, 3], // A en ascii correpond à 65
"66": [0], // B en ascii correpond à 66
...SNIP...
"75": [2], // K en ascii correpond à 75
...SNIP...
"254": [],
"255": []
}
Enfin, la fonction serialize_and_output qui va récupérer pour chaque key de notre map, sa valeur associé (c’est à dire la liste chainée d’index) et écrire pour chacun :
Le nombre d’index à lire ensuite
Tous les index
void serialize_and_output(long char_map) {
long nodes_count;
void **first_node;
void *index;
int i;
for (i = 0; i < 0xff; i = i + 1) {
// Récupère la première node de la liste chainée
first_node = (void **)(char_map + (long)i * 8);
// Compte le nombre de node chainée
nodes_count = list_len((long *)first_node);
// Ecrit ce nombre
fwrite(&nodes_count,8,1,stdout);
// Pour chaque node, écrit la valeur qu'elle contient (c'est à dire l'index du char)
for (index = *first_node; index != (void *)0x0; index = *(void **)((long)index + 8)) {
fwrite(index,8,1,stdout);
}
}
return;
}
Exemple : Sérialiser BAKA
En reprenant la charmap de l’exemple précedent, on écrira 0 pour chacune des listes vides, donc notre fichier commencera par 64 0
Puis arrivé à A (65), on écrira : 2 1 3 où 2 correspond au nombre d’index à lire ensuite et 1, 3 sont les index
Pour B on aura 1 0 et pour K on aura 1 2
Il faut tout de même souligner que toutes les valeurs sont écrite en sur 8 bytes. Donc il faut le prendre en compte pour ne pas se tromper pendant la déserialisation
Script de résolution
from struct import unpack
message = bytearray(1000)
with open('message.txt.cz', 'rb') as f:
for char_code in range(256):
try:
to_read = unpack('Q', f.read(8))[0]
except Exception:
break
for _ in range(to_read):
index = unpack('Q', f.read(8))[0]
message[index] = char_code
print(message.decode())
Organizer 1: Hey, did you finalize the password for the next... you know?
Organizer 2: Yeah, I did. It's "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}"
Organizer 1: "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}," got it. Sounds ominous enough to keep things interesting. Where do we spread the word?
Organizer 2: Let's stick to the usual channels: encrypted messages to the leaders and discreetly slip it into the training manuals for the participants.
Organizer 1: Perfect. And let's make sure it's not leaked this time. Last thing we need is an early bird getting the worm.
Organizer 2: Agreed. We can't afford any slip-ups, especially with the stakes so high. The anticipation leading up to it should be palpable.
Organizer 1: Absolutely. The thrill of the unknown is what keeps them coming back for more. "HTB{4_v3ry_b4d_compr3ss1on_sch3m3}" it is then.