"Reverse Engineering" de programmes

par François-R Boyer

J'espère que ce texte va vous aider.

Premièrement vous savez que le code original était écrit en C.
En C, l'exécution d'un programme c'est l'exécution de la routine principale "main": le programme commence avec cette routine, et il se termine lorsque cette routine retourne.
Les librairies ont parfois besoin d'initialisation, donc souvent le "main" est appelé par une autre routine, qu'on n'a pas besoin de traduire si on a des librairies équivalentes dans le langage qu'on utilise.

En C, les variables locales et les paramêtres sont soit dans des registres soit sur la pile, et les variables globales sont à des adresses fixes en mémoire. L'appel de sous-routine suit une convention d'appel qui est assez uniforme, mais certains compilateurs permettent de changer de convention d'un routine à l'autre.

Voici un code du même genre que ce qui est généré par mon compilateur C mais ce ne sont pas les mêmes registres qui sont utilisés.

	addi  r8,r8,FFFFFFD8h   ; 00001000
	st    r4,00000010h(r8)  ; 00001004
	st    r5,00000014h(r8)  ; 00001008
	st    r6,00000018h(r8)  ; 0000100C
	st    r10,0000001Ch(r8)	; 00001010
	addi  r6,r1,00000000h	; 00001014
	addi  r4,r6,00000000h	; 00001018
	addi  r5,r4,00000000h	; 0000101C
	addi  r1,r6,00000000h	; 00001020
	addi  r2,r5,00000000h	; 00001024
	la    r10,00001118h     ; 00001028
	brl   r10,r10           ; 0000102C
	add   r3,r9,r5          ; 00001030
	shra  r4,r3,01h         ; 00001034
	sub   r9,r4,r5          ; 00001038
	lar   r7,0000101Ch      ; 0000103C
	brmi  r7,r9             ; 00001040
	addi  r9,r5,00000000h   ; 00001044
	ld    r4,00000010h(r8)  ; 00001048
	ld    r5,00000014h(r8)  ; 0000104C
	ld    r6,00000018h(r8)  ; 00001050
	ld    r10,0000001Ch(r8) ; 00001054
	addi  r8,r8,00000028h   ; 00001058
	br    r10               ; 0000105C
	addi  r8,r8,FFFFFFE8h   ; 00001060
	st    r10,00000010h(r8) ; 00001064
	la    r3,00000009h      ; 00001068
	st    r3,00001154h      ; 0000106C
	ld    r1,00001154h      ; 00001070
	la    r10,00001000h     ; 00001074
	brl   r10,r10           ; 00001078
	st    r9,00001150h      ; 0000107C
	ld    r1,00001150h      ; 00001080
	la    r10,000010A0h     ; 00001084
	brl   r10,r10           ; 00001088
	addi  r9,r0,00000000h   ; 0000108C
	ld    r10,00000010h(r8) ; 00001090
	addi  r8,r8,00000018h   ; 00001094
	br    r10               ; 00001098

Supposons que vous avez déjà regardé le programme pour déterminer les choses suivantes (en regardant le comportement des routines de manière générale):
- Le "main" serait fort probablement la routine qui commence à 00001060.
- À l'adresse 00001118 se trouve une routine qui fait la division entière r1/r2 et met le résultat dans r9 (on n'a pas besoin de la traduire, la division existe déjà dans le langage que vous utilisez).
- Àl'adresse 000010A0 se trouve une routine qui affiche le nombre dans r1 en base 10 (existe aussi dans votre langage).

On va commencer par le "main". La routine commence à l'adresse 00001060 et se termine à l'adresse 0000109C.
Essayons de donner des noms aux variable, et faisons une traduction directe vers du C.
Les commentaires montrent comment on pourrait penser, mais ce n'est pas toujours ce qu'il y a de mieux à trouver.

// voir plus bas pour savoir pourquoi les variables sont d'un certain type
int d1150, d1154; // le nom est "d" suivi de l'adresse où se trouve la variable globale

main() // 00001060
{
  int *a;
  int b, c, d, e;
  
  a = a - 6; // FFFFFFE8h c'est -24, qui est 6 entiers (de 32 bits).  "a" se trouve dans "r8"
  // on utilise "a" comme pointeur, et on écrit un mot de 32 bits, donc ça doit être un pointeur sur des entiers
  a[4] = b; // on écrit à l'adresse 10h+a, les entiers sont de 32 bits donc c'est l'entier 4 (en C le premier élément d'un tableau c'est l'élément 0).  "b" se trouve dans "r10"
  // comme "b" n'est pas initialisé depuis le début de la routine, ça doit être un paramêtre
  
  c = 9; // "c" est dans "r3"
  d1154 = c; // on écrit à une adresse absolue, ça doit être une variable globale
  d = d1154; // "d" est dans "r1"
  f1000(); // on fait un appel de sous-routine, r1 semble être le paramêtre, puisque pour les routines connues r1 était le premier paramêtre.  L'adresse de retour est mise dans r10
  d1150 = e; // "e" est dans "r9"
  printf("%d", d1150); // 000010A0 est appelé après avoir mis d1150 dans r1, on traduit par l'affichage correspondant dans notre langage.
  
  b = a[4];
  a = a + 6;
  // branche à "b"
}

Maintenant, à la fin on branche à "b" qui vient de a[4] qui est écrit au début et qui vient de possiblement un paramêtre, selon les commentaires. Donc, soit c'est un paramêtre qui contient un pointeur vers une fonction (peu probable), ou que c'est l'adresse de retour (três probable). Et "a" ressemble étrangement à une pile (donc r8 serait le pointeur de pile). On va se servir de la pile de C dans le code final, plutôt que d'un pointeur.

Maintenant la routine f1000:

int f1000(int b)
{
  int a, c, d, e, f;
// "r8" c'est notre pile, donc on ne traduit pas directement les accès utilisant "r8"
// on sauve: 10h(r8) <- r4;  14h(r8) <- r5;  18h(r8) <- r6;  1Ch(r8) <- r10 (qui est notre adresse de retour);
  a = b;  // "a" est dans "r6", "b" est dans "r1".  "r1" a été modifié la dernière fois par la routine appelante, donc c'est bien un paramêtre.
  c = a;  // "c" est dans "r4"
  do {
    d = c;  // "d" est dans "r5"
    // on fait un appen à 1118h en mettant "a" dans "r1" et "d" dans "r2"
    e = a/d;  // "e" est dans "r9"
    f = e + d;  // "f" est dans "r3"
    c = f>>1;
    e = c - d; // ici on écrase "r9" donc on aurait pu écrire directement  "f = a/d + d;" plus haut, sans passer par "e"
  } while(e < 0);
  // ici on écrit dans "r9" et il est utilisé par le "main" donc c'est la valeur de retour
  return d;
// on restaure: r4, r5, r6, r10
}
Avec tout ce qu'on sait maintenant, on peut réécrire en plus beau:
int d1150, d1154;

int f1000(int a)
{
  int c, d;
  c = a;
  do {
    d = c;
    c = (a/d + d)>>1;
  } while(c - d < 0);
  return d;
}

main()
{
  d1154 = 9;
  d1150 = f1000(d1154);
  printf("%d", d1150);
}
On aurait aussi pu se rendre compte que f1000 est une racine carrée entière.