Etape 1 : Tracer le code redirigé

Par tracer j'entends : parcourir l'intégralité du code de la fonction et retrouver les instructions émulées.

Voila comment je procède : La fonction Go appelée en tout premier lieu va scanner la mémoire du processus à la recherche de plusieurs signatures qui permettrons de poser des HBPs (Hardware Break Points) sur les fonctions d'asprotect émulant les instructions :

  • GetFlags : edx contient l'index du type de jcc émulé il suffit de récupérer le code de l'instruction dans la table JmpType en utilisant cet index, pour la destination et l'instruction suivante, il suffit de faire des opérations sur des variables stockées dans la pile
  • JmpEmu / JmpIntermu : eax contient la destination du saut
  • CmpEmu : ecx contient l'ID du cmp et eax l'adresse d'une table de fonctions qui seront appelées pour déterminer le type du cmp émulé

Une fois les HBPs posés, je mets en place un VectoredExceptionHandler pour récupérer les exceptions générées par ces HBPs avant qu'elles ne soient passées aux SEH.

Le code de l'exécutable est ensuite scanné à la recherche de fonctions redirigées (call sur un jmp long pointant en dehors de l'exécutable) à chaque fonction une structure est créée contenant la liste des instructions de cette fonction et son adresse de début. Ensuite pour chaque instruction :

  • Si c'est un JMP, on ne logge pas l'instruction et on place le curseur sur à l'adresse où saute le jmp (code spaghettis),
  • Si c'est un CALL qui correspond à une instruction émulée, la fonction GetJcc va enregistrer les flags et les registres puis sauter sur le call en question, si tout se passe bien, le processus breakera sur l'un des HBP et l'exception sera récupérée par la fonction ExcHandler qui se chargera de retrouver le type du jmp émulé, son adresse de destination, l'adresse de l'instruction qui le suit ainsi que l'instruction cmp émulé si il y en a une,
  • Si l'instruction a déjà été loggée alors on ajoute à la liste chainée un jmp vers cette instruction et on arrête de parcourir le code,
  • Si l'instruction corespond à un retn, on arrête de parcourir le code,
  • Sinon on ajoute à la liste chainée l'instruction désassemblée.

A chaque instruction est associée un label, un label est constitué d'un code (l'équivalent du nom), une adresse qui sera calculée plus tard et un alias qui permet d'avoir plusieurs "codes" qui pointent vers la même adresse enfin chaque label est associé à la fonction à laquelle appartient l'instruction. Ces labels serviront au moment du ré-assemblage.

Une fois ce premier désassemblage fait, on parcours la liste chainée des instructions pour tracer les instructions pointées par les saut conditionnels

J'ai conscience du fait que ce n'est peut être pas très clair au premier abord clair, c'est difficile d'expliquer ce que fait mon call fixer de façon clair :D si vous voulez vraiment comprendre ce que je fais en détail, le plus simple est, je pense, de regarder mon code en parallèle de mes explications, il n'est pas très compliqué et contient aussi plus de renseignements sur l'émulation des instructions et la façon dont je gère les labels ...

Etape n°2 : Déjunker le Code

Pour nous compliquer la tache asprotect junke le code de la fonction redirigée en rajoutant des instructions inutiles. Ces instructions modifient ou initialisent des registres qui ne seront pas utilisés avant leur prochaine initialisation
un petit exemple (sans les jmps qui sautent partout, le disasm est issu de la liste chainée d'instructions convertie en string avec ma fonction GenInsStr) :

ADD EAX , ECX
SUB ECX , ECX
PUSH EDX
ROL ECX , -7F
ADD ECX , D [ESP+18]
POP ECX

On voit bien ici que ECX n'est pas utilisé entre son initialisation (SUB ECX , ECX) et sa prochaine initialisation par l'instruction POP ECX, on peut donc supprimer le SUB ECX , ECX De la même facon ECX n'est pas utilisé entre sa modification (ADD ECX , D [ESP+8]) et son initialisation, cette instruction est donc elle aussi inutile Pour supprimer toutes ces instructions j'ai utilisé la nouvelle version de ma lib de disasm qui me donne les registres modifiés / initialisés / utilisés pour chaque instruction il suffit ensuite de traduire en C ce que je viens de vous expliquer ce qui donne la fonction dejunk de mon "function fixer"

Etape n°3 : Dépolymorphiser le code

Asprotect polymorphise les instructions à l'aide de règles simples et peux nombreuses mais suffisantes une fois associées au junk et à la "spaghettisation du code" pour rendre sa lecture pénible :

OR ECX , EDI
LEA ECX , D [EDI+EAX]
LEA ECX , D [EAX+2C]
LEA ECX , D [ECX+EBP-2C]
SUB ECX , EBP
SHL EAX , +8
ADD EAX , ECX
=
MOV ECX , EAX
SHL EAX , 8
ADD EAX , ECX

Voila les règles utilisées par Asprotect :

PUSH X
POP Y
=
MOV Y , X  (avec X ou Y qui ne sont pas du type [X] )

LEA R1 , [X + R2] SUB R1 , R2 = LEA R1 , [X]
LEA R1 , [X + C1] LEA R1 , [R1 - C1] = LEA R1 , [X]
LEA R1 , [X + C1] SUB R1 , C1 = LEA R1 , [X]
LEA R1 , [R1] = NOP

Pour dépolymorphiser le code il suffit donc de retranscrire ces règles en C en utilisant BDLib (**pub pub**) cf la fonction Depoly de mon "function fixer"

Etape n°4 : Réassembler le code :

C'est la partie la plus délicate de mon outil, il a fallu que je remanie mon code plusieurs fois pour avoir assez d'information et pour réassembler le code le plus proprement possible, en prenant le moin de place possible.

Probleme n°1 :

Les fonctions redirigées par Asprotect se "sautent les unes sur les autres" c'est à dire que dans le code original, on peut avoir un jmp long vers une autre fonction ce qui est traduit dans le code redirigé par un jmp que l'on ne peut pas différencier des autres, on reconstruit donc les 2 fonctions à l'adresse de la première et forcément on se retrouve avec un code bien plus gros que l'original.

Pour contourner cela, à chaque instruction "tracée", je vérifie qu'il n'y a pas de jmp long qui pointe dessus dans le code de l'exécutable, si c'est le cas, je suspends le tracing de la fonction en cours, je trace la nouvelle fonction puis je reprends l'analyse de la précédente.

Ca marche tres bien sauf lorsque Alexeï trouve rigolo de placer dans le code original des fonction redirigées (qui sont donc en général des bytes aléatoires) des jmps vers des portions de code redirigés que je prends donc pour un début de fonction ce qui engendre un bordel monstre (je vous prie de bien vouloir excuser ma grossièreté) lors de la reconstruction, j'ai donc décidé de ne prendre en compte que les sauts situés à plus de 200 bytes du début de la fonction en cours d'étude, ce qui est plus qu'arbitraire mais je ne vois pas vraiment comment faire autrement, c'est le seul passage de la reconstruction qui n'est pas sur à 100% :/

Probleme n°2 : Jxx short ou long ?

Comment réussir à avoir un code le plus petit possible afin qu'il "tienne" dans la place utilisée initialement par le code original ? Apres avoir longuement réfléchi à la question sur mon balcon en prenant mon café, j'ai décidé de procéder comme suis :

  1. Initialisation des labels, pour chaque fonction :
    • La première instruction prend l'adresse du début de la fonction
    • Chaque instruction i prend l'adresse de l'instruction i-1 plus la taille de l'instruction i-1
    • Si l'instruction i-1 est un saut alors on considère que sa taille est 2 (donc que c'est un jxx short)
  2. Correction des labels, pour chaque saut :
    • Si le jmp pointe sur une adresse trop lointaine pour être un jmp short, on décale toutes les adresses des labels de la fonction à laquelle appartient le jmp et qui se situent "sous" ce jmp en utilisant la différence de taille entre le jxx long et le jxx short
    • On recommence l'opération tant que des changements ont été opérés sur un jmp

Une fois cette opération terminée, on obtient un code le plus petit possible (d'un point de vue taille des instructions, l'organisation des instructions n'est peut etre pas la plus optimisée possible)

Probleme n°3 : Assembler les instructions dépolymorphisées / émulées

Pour ré-assembler les instructions et récupérer leur taille (au moment de la correction de la taille des jxx par exemple), je me contente dans la plupart des cas de copier l'instruction depuis son emplacement dans la fonction redirigée en la désassemblant pour avoir sa taille mais je ne peux bien sur pas le faire dans le cas des instructions émulées / polymorphisées.

Vous trouverez donc dans Assemble.c les fonctions GetInsLen et AssembleIns qui calculent la taille et assemblent les instructions cmp / mov / lea et jxx à partir de la même structure que celle qui est retournée par mon désassembleur.

La structure des fonctions reconstruitent est parfois différente de l'originale, ne loggant jamais les jmps, cela peut engendrer des suites d'instructions qui ressemblent à cela :

Jcc label
jmp X
label:

et qui peuvent être remplacées par le code jnc X (avec jnc l'inverse du saut jcc).

Conclusion

Après toutes ces opérations j'arrive à avoir une fonction qui une fois reconstruite est plus petite que l'originale (j'ai en effet simplifié des sub reg , 1 en dec reg alors que le code original utilise bien un sub reg , 1) qui fonctionne parfaitement.

Cette protection m'a quand même pris plus d'une bonne semaine pour en arriver à bout, au final mon code entier (désassembleur, désassembleur v2, assembleur, tracer etc.) fait quand meme 169Ko le code produit juste pour ce function fixer est de 100Ko la dll finale fait 114Ko

Ce genre de protection est très simpa à analyser et programmer ce "function fixer" a été très instructif et même si j'ai beaucoup ralé ça a quand même été une bonne expérience :D

Ce qui serait interressant, c'est avoir des solutions d'autres personnes afin de voir comment elles procèdent ... Si vous avez ça dans un coin pensez à moi ;)

Con-con(clusion) :

Le nom de cet outil vient d'une bière, la Mac Chouffe que j'ai bu avec __ed lors du dernier FAT-m**t, il faut le prononcer Aimessichouffe avec si possible une chaine en or autour du cou, un cigare dans la main gauche et une bonne pinte de biere dans la droite ;)

J'espère que ce petit texte vous donnera envie de vous pencher sur ce genre de protection si ce n'est pas déjà fait et vous encouragera à publier vos solutions, c'est toujours [in/con]structif de voir comment les autres travaillent ....

Les sources, le function fixer et ce texte est sous la licence Creative Common suivante : Creative Commons License