MC Chouffe : Reconstruire les fonctions redirigées par Asprotect
Par Baboon le vendredi, mars 13 2009, 16:19 - Lien permanent
Comme vous l'avez surement remarqué, je suis pris d'une frénésie de rédaction d'articles ces temps ci (2 en février, 1 en mars !!!) espérons que cela dure et que vous apprendrez 2-3 trucs en me lisant
Aujourd'hui donc nous allons voir comment fixer les fonctions redirigées par asprotect. Je n'ai pas vu de tuto sur ce sujet et bizarrement je n'en entends jamais parlé alors que c'est une des features les plus rigolotes d'Asprotect et surement la plus compliquée à fixer.
Asprotect permet en effet de rediriger des fonctions du programme au moment de la protection, il va désassembler le code de la fonction, le junker (ajout d'instructions inutiles, code spaghetti) et le polymorphiser (1 instructions -> 3-4 instructions) il va aussi émuler les sauts (conditionnels ou non) ainsi que certains couples comparaisons / jcc (remplacement des instructions par des call AsproFonction qui se chargent de remplacer la (les) instruction(s)).
Pour fixer tout ça (c'est à dire retrouver le code original ou un code ayant le même comportement et de taille inférieure ou égale à l'original) il faut donc :
- "tracer" le code des fonctions redirigées, retrouver l'intégralité des instructions en explorant tous les chemins possibles du code et en décodant les instructions émulée,
- Dé-junker le code c'est à dire repérer les instructions inutiles en nous basant sur l'utilisation des registres
- Dé-polymorphiser le code en utilisant des règles qu'il faudra définir
- Ré-assembler le code à son adresse d'origine en utilisant le moin de place possible et donc en utilisant des saut courts le plus souvent possible.
J'ai pris beaucoup de plaisir à coder ce "function fixer", j'espere que vous en aurez autant en me lisant
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 :
- 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)
- 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 :
Commentaires
Effectivement c'est technique, bravo le singe
Et après qu'on nous dise pas que Asprotect c'est un truc de mickey !
Eheh, c'est vrai que c'est assez énervant de voir des gens dire d'asprotect que c'est "tro fassil" parce qu'il n'ont eu affaire qu'a asprotect en version basique, sans utilisation du sdk, avec juste une petite redirection d'imports
Asprotect a vraiment des options simpas ...
Je me rappelle avoir vu un anti dump assez évolué sur une dll delphi :
Asprotect appelait des fonctions d'initialisation de la dll avant de rendre la main à l'oep du programme
Delphi faisant des tests pour voir si ses fonctions d'initialisation ont bien été appelées ne les rappellait pas et donc apres dumping, les adresses des zones mémoires allouées n'etait forcément plus bonnes et Delphi ne s'initialisait plus, ce qui bien sur faisait crasher le programme que bien plus tard
Il m'a fallut du temps pour localiser le probleme :D
MC Chouffe ! purée, elle m'en aura donnée des kilomètres à faire en plus celle là au retour... jvous garantie que le tramway de Bordeaux il était pas droit ce soir là! Bon bah voila, il est fini cet article! très bonne qualité le singe, bravo à toi
cool, je viens d'apprendre que faire un tut sur asprotect
c'est pas uniquement faire de la redirection d'imports
merci baboon
Je viens de me perdre sur ton blog, et effectivement j'aime beaucoup tes articles. T'a fais un joli boulot sur cette partie d'asprotec
J'attend le prochain post avec impatience, et merci de m'avoir (re)fait scotcher sur un article de reverse
hey Mr Gbillou :D
Je suis honnoré de ta présence ici
merci beaucoup pour ton commentaire :D ca me touche vraiment, j'espere que mon prochain article te satisfera
Sinon j'espere aussi te revoir un peu plus présent sur la """scene""" du RE, ca fait longtemps qu'on ne t'as pas vu !