Ben oui, parce que j'ai l'impression que plutot que de me voir commenter le code source obtenu apres des heures, des jours, des mois de labeur, certains prefereraient me voir donner des explications sur le labeur en question...
He bien c'est le sujet du post du jour ... mais je sens que vous allez etre decus.
pour commencer, une liste des outils dont je me sers pour demarrer la decompilation (pour un jeu datant du debut des annees 90 maxi):
- dosbox (l'indispensable)
- debug.exe (une version qui tourne sous dosbox bien sur)
- un editeur hexa
Cette liste d'outils augmente au fur et a mesure que le projet avance, mais pour commencer, je veux juste recuperer des informations sur la genereation des executables du jeu.
Au debut, je lance debug sur le programme qui m'interesse et je regarde un peu le debut du code.
A ce niveau-la, je peut deja reconnaitre si le programme a ete compresse, avec exepack par exemple, et si c'est le cas, utiliser un programme (genre UNP) pour le decompresser et travailler sur le resultat obtenu: un exe non compresse !
Un truc pratique, c'est d'avoir un peu en tete le format de l'entete d'une fichier ".exe".
Avec les deux champs "nombre d'octets du dernier secteur" et "nombre de secteurs" on peut calculer la taille de l'exe principal.
Si jamais le fichier est plus gros que cette taille, ca veut dire qu'il contient des overlays, des infos de debug, ou encore que sais-je.
Dans le cas de GAME.EXE, pas de compression, pas d'overlays, pas d'infos de debug, ...
Toujours dans debug, et en desassemblant le code du point d'entree, je lis:
-u 0838:0000 BABD35 MOV DX,35BD 0838:0003 2E CS: 0838:0004 89160502 MOV [0205],DX 0838:0008 B430 MOV AH,30 0838:000A CD21 INT 21 0838:000C 8B2E0200 MOV BP,[0002] 0838:0010 8B1E2C00 MOV BX,[002C] 0838:0014 8EDA MOV DS,DX 0838:0016 A39200 MOV [0092],AX 0838:0019 8C069000 MOV [0090],ES 0838:001D 891E8C00 MOV [008C],BX
d 35bd:0000
35BD:0000 00 00 00 00 54 75 72 62-6F 2D 43 20 2D 20 43 6F ....Turbo-C - Co 35BD:0010 70 79 72 69 67 68 74 20-28 63 29 20 31 39 38 38 pyright (c) 1988 35BD:0020 20 42 6F 72 6C 61 6E 64-20 49 6E 74 6C 2E 00 4E Borland Intl..N 35BD:0030 75 6C 6C 20 70 6F 69 6E-74 65 72 20 61 73 73 69 ull pointer assi 35BD:0040 67 6E 6D 65 6E 74 0D 0A-44 69 76 69 64 65 20 65 gnment..Divide e 35BD:0050 72 72 6F 72 0D 0A 41 62-6E 6F 72 6D 61 6C 20 70 rror..Abnormal p 35BD:0060 72 6F 67 72 61 6D 20 74-65 72 6D 69 6E 61 74 69 rogram terminati 35BD:0070 6F 6E 0D 0A 00 00 00 00-00 00 00 00 00 00 00 00 on..............
En gros ce qu'il faut savoir c'est qu'un programme ecrit dans un langage un peu evolue, genre C ou Pascal,
ne commence pas directement par executer le code ecrit par le programmeur, mais par ce qu'on pourrait appeler un "chargeur" ou "loader" qui dans ses premieres instructions, fait reference a un moment ou un autre a un segment de donnees, lequel contient souvent une indication de copyright de la librairie utilisee.
Bref, comme vous pouvez voir, le gagnant du jour est "Turbo c" dans sa version 1988.
La prochaine etape sera de mettre la main dessus. Alors la, ca peut aller du totalement facile au vachement dur pour le retrouver.
Par exemple, si vous chercher "turbo c" sur internet, grande sont les chances que vous tombiez sur la version "2.1".
Les ayants-droits actuels des produits borland ayant eu la gentille idee de mettre en ligne GRATUITEMENT la version la plus recente du compilateur C.
Manque de chance, la version qui nous interesse est anterieure. Comment le savoir ?
En gros par, d'une part, le comportement du compilateur, et d'autre part, le contenu de la librairie.
La charue se retrouvant legerement avant les boeufs, revenons un peu en arriere, a notre debug.
Maintenant que je sais que c'est un compilateur C de borland qui a fait (presque tout) le boulot, je vais essayer de trouver le "modele de memoire" qui a ete utilise.
(pour une description precise de ces modeles, je vous invite a aller faire une recherche sur internet, ca sera beaucoup mieux explique que si je m'y colle)
Toujours avec debug, j'explore un peu plus le chargeur:
-u e9 0838:00E9 BF2E32 MOV DI,322E 0838:00EC B9FAED MOV CX,EDFA 0838:00EF 2BCF SUB CX,DI 0838:00F1 F3 REPZ 0838:00F2 AA STOSB 0838:00F3 0E PUSH CS 0838:00F4 FF162832 CALL [3228] 0838:00F8 9A35063808 CALL 0838:0635 0838:00FD 9A27073808 CALL 0838:0727 0838:0102 B400 MOV AH,00 0838:0104 CD1A INT 1A 0838:0106 89169800 MOV [0098],DX
-u 0838:010A 890E9A00 MOV [009A],CX 0838:010E 0E PUSH CS 0838:010F FF162C32 CALL [322C] 0838:0113 FF368800 PUSH [0088] 0838:0117 FF368600 PUSH [0086] 0838:011B FF368400 PUSH [0084] 0838:011F 9A5E0C0309 CALL 0903:0C5E 0838:0124 50 PUSH AX 0838:0125 9A0C00E933 CALL 33E9:000C
Je remarque deja que les appels de fonctions se font avec une adresse 32 bits, les donnees etant par contre dans un seul segment, j'en deduisons que c'est le modele "medium".
Et la phase de desassemblage peut commencer.
Alors pour commencer, le plus facile, les donnes; il suffit de lancer debug en redirigeant la sortie standard dans un fichier, et de dumper le segment de donnees (35BD, on l'a trouve au debut) et 0 jusqu'a 322e, qui est le debut de la zone non initialisee.
Pour le desassemblage, ca serait facile dans le cas d'un seul segment de code, mais vu que le modele est medium, ca veut dire qu'il faut reperer tous les segments, et les desassembler un par un.
C'est plutot ennuyeux comme process, mais c'est comme ca que je fais :-(
Un petit coup d'oeil au passage au resultat de la commande 'r' sous debug:
-r AX=FFFF BX=0003 CX=3CA0 DX=0000 SP=0080 BP=0000 SI=0000 DI=0000 DS=0828 ES=0828 SS=449D CS=0838 IP=0000 NV UP EI PL NZ NA PO NC 0838:0000 BABD35 MOV DX,35BD
Le PSP fait 256 octets et est immediatemment suivi de l'executable charge en memoire (moins son en-tete et apres relocation).
Pour trouver le segment en question, on fait "segment de psp" + (256 / 4); en hexadecimal, le calcul donne 0x0828 + 0x100 / 4 = 0x0828 + 0x10 = 0x0838
(Il se trouve que c'est le segment ou se trouve le loader, mais ce n'est pas toujours le cas)
Sachant que le segment de donnees commence en 35BD et que l'exe a ete charge en 0838, il reste donc (0x35BD - 0x0838) * 0x10 octets a analyser;
le resultat: 0x2d850, ce qui depasse la taille d'un segment.
Donc pour commencer, je desassemble le segment 0838 jusqu'au segment 0903 (celui qui contient le main, rappelez-vous), cela fait une taille maximum de 0xcbf octets;
je suppose que le tout correspond a un seul segment.
(spoiler: c'est bien le cas! Mais a ce stade-la, pas moyen d'en etre sur).
Alors un autre indice; ce segment 0838 contient le chargeur, et dans le cas de Turbo C, ce segment s'appelle a l'origine _TEXT;
Il contient les fonctions assembleur de la librairie C, mais en farfouillant le code, je trouve aussi (par exemple a partir de l'offset 0207) des fonctions qui ne semblent pas faire partie de la librairie C;
ce sont des fonctions ecrites vraisemblablement en assembleur, dont le segment de code a ete nomme _TEXT et qui se retrouvent donc dans ce fameux segment 0838.
[ce sont les fonctions OSI_file dont j'ai parle dans un post precedent]
Le segment 0838 s'arrete a l'octet 0838:0cb0 (compris), le segment 0903 commence donc en 0903:0001.
Et c'est la que ca devient rebarbatif, parce que je dois trouver tous les segments entre 0903 et 35BD maintenant.
Perso, j'y vais a l'arrache; je desassemble, je verifie, j'efface ce qu'il y a en trop, je fais des recherches textuelles pour voir si il est fait reference a tel ou tel segment, ...
Au final, j'obtiens un fichier texte assez consequent (dans les 3 megaoctets) qui contient l'integralite de l'exe original sous forme desassemblee pour le code, dumpee pour les donnees.
Dans la mesure du possible, je repere toutes les fonctions de la libc, retrouve leur nom original, et remplace leurs appels, par exemple:
-u1944:17b9 1944:17B9 33C0 XOR AX,AX 1944:17BB 50 PUSH AX 1944:17BC 8D5EFC LEA BX,[BP-04] 1944:17BF 8CD2 MOV DX,SS 1944:17C1 8BC3 MOV AX,BX 1944:17C3 B90400 MOV CX,0004 1944:17C6 9A8A0C3808 CALL 0838:0C8A 1944:17CB 8B1EB3B6 MOV BX,[B6B3] 1944:17CF D1E3 SHL BX,1 1944:17D1 8B874835 MOV AX,[BX+3548] 1944:17D5 25FF03 AND AX,03FF 1944:17D8 50 PUSH AX
1944:17B9 33C0 XOR AX,AX 1944:17BB 50 PUSH AX 1944:17BC 8D5EFC LEA BX,[BP-04] 1944:17BF 8CD2 MOV DX,SS 1944:17C1 8BC3 MOV AX,BX 1944:17C3 B90400 MOV CX,0004 1944:17C6 9A8A0C3808 CALL SPUSH@ 1944:17CB 8B1EB3B6 MOV BX,[B6B3] 1944:17CF D1E3 SHL BX,1 1944:17D1 8B874835 MOV AX,[BX+3548] 1944:17D5 25FF03 AND AX,03FF 1944:17D8 50 PUSH AX
Ce qui est cool c'est que ca vous donne deux informations importantes sur l'objet en question:
- le fait que c'est une structure
- sa taille (dans notre cas, 4 octets)
Il ne me reste plus qu'a traduire 3 mega-octets de code assembleur en langage C ....

Mais ca sera pour la prochaine fois ...

Ceci dit, c'est toujours intéressant de savoir comment tu t'y prends ... D'ailleurs, comment fais-tu pour "sauvegarder" ce que sort DEBUG quand tu désassembles telle ou telle partie du code ? Tant qu'il n'y a qu'un segment, c'est "simple", mais c'est malheureusement souvent pas le cas et souvent tu dois "trafiquer" pour récupérer l'intégralité du code


Commentaire