Aller au contenu
Tous les challenges
Forensics Résolu

Soon™

Un client de jeu au lancement qui embarque plus que prévu dans ses DBC.

Scripts
2 fichiers

Énoncé

World of Warcraft: Forever is finally live. Players are rushing back into Azeroth, but the launch client might hold a little more than Blizzard meant to ship.

Fichier fourni : ChrRaces.dbc, la table des races jouables du client WoW.

Reconnaissance

Un fichier DBC (format WDBC) se compose de :

Offset Contenu
0 magic WDBC
4 nombre de records
8 nombre de champs
12 taille d’un record
16 taille du string block
20 records, puis string block (chaînes terminées par \0, référencées par offset)

Header du fichier :

WDBC  records=18  fields=29  record_size=116  string_block=330

Taille attendue : 20 + 18×116 + 330 = 2438 octets. Taille réelle : 6150 octets.

strings affiche les races connues (Human, Orc, Dwarf, NightElf, Scourge, Tauren, Gnome, Troll, Goblin, Skyborne, FelOrc, Naga, Broken, Skeleton, Vrykul, Tuskarr, ForestTroll), une race Ogre qui n’existe pas dans la table d’origine, et beaucoup de chaînes d’un seul caractère ({, }, 4, _, B, A…) intercalées dans le string block.

Point clé

(6150 − 20 − 330) / 116 = 50 records exactement. Le header n’en déclare que 18, donc le client (et tout parseur qui suit le header) ignore les 32 records qui suivent. Le string block reste à la fin du fichier, après les 50 records.

Les 32 records cachés sont des clones de l’Orc nommés « Ogre » (champ 6 = Og, champ 15 = Ogre), avec des IDs de 101 à 132 stockés dans le désordre. Le champ 17 (nom d’affichage) de chacun pointe vers une chaîne d’un seul caractère.

Exploitation

  1. Lire le header, puis calculer le vrai nombre de records : (taille − 20 − string_block) / record_size.
  2. Placer le string block après les 50 records, pas après 18.
  3. Prendre les records d’index ≥ 18 et les trier par ID (champ 0).
  4. Concaténer la chaîne pointée par le champ 17 de chaque record.
101 B, 102 A, 103 2, 104 6, 105 {, 106 0, 107 g, 108 r, 109 3, 110 s, ...  132 }
→ BA26{0gr3s_4r3_f1n4lly_pl4y4bl3}

Script

solve.py (dans ce dossier) :

import struct, sys
d = open(sys.argv[1] if len(sys.argv) > 1 else 'files/ChrRaces.dbc', 'rb').read()
magic, n, nf, rs, ss = struct.unpack('<4s4I', d[:20])
real = (len(d) - 20 - ss) // rs          # 50 au lieu des 18 déclarés
sb = d[20 + real * rs:]                  # string block après TOUS les records
s = lambda o: sb[o:sb.index(b'\0', o)].decode()
recs = [struct.unpack('<%dI' % nf, d[20 + i*rs:20 + (i+1)*rs]) for i in range(real)]
hidden = sorted(recs[n:], key=lambda r: r[0])   # records cachés, triés par ID
print(''.join(s(r[17]) for r in hidden))
python3 -I solve.py files/ChrRaces.dbc
# BA26{0gr3s_4r3_f1n4lly_pl4y4bl3}

parse.py affiche tous les records avec les champs qui ressemblent à des chaînes ; c’est le script que j’ai utilisé pour l’analyse.

Code

# Soon(TM) - ChrRaces.dbc: header declares 18 records, file holds 50.
# 32 hidden "Ogre" records (IDs 101..132) each carry 1 char in the name field (17).
import struct, sys
d = open(sys.argv[1] if len(sys.argv) > 1 else 'files/ChrRaces.dbc', 'rb').read()
magic, n, nf, rs, ss = struct.unpack('<4s4I', d[:20])
real = (len(d) - 20 - ss) // rs
sb = d[20 + real * rs:]
s = lambda o: sb[o:sb.index(b'\0', o)].decode()
recs = [struct.unpack('<%dI' % nf, d[20 + i*rs:20 + (i+1)*rs]) for i in range(real)]
hidden = sorted(recs[n:], key=lambda r: r[0])
print(''.join(s(r[17]) for r in hidden))