J'ai décidé qu'il était temps qu'il y ait un bon déballage de Pyarmor. Tous ceux qui sont actuellement publics sont dépassés, ne fonctionnent pas du tout ou ne donnent que des résultats partiels. J'ai l'intention de faire de cette prise en charge la dernière version de Pyarmor.
Veuillez jouer le référentiel si vous l'avez trouvé utile. J'apprécierais vraiment.
Il existe 3 méthodes différentes pour déballer Pyarmor, dans le dossier des méthodes dans ce référentiel, vous trouverez tous les fichiers nécessaires pour chaque méthode. Vous trouverez ci-dessous un article détaillé sur la façon dont j'ai commencé jusqu'au produit final. J'espère que plus de gens comprennent réellement comment cela fonctionne de cette façon plutôt que de simplement utiliser l'outil.
Il s'agit d'une liste de tous les problèmes / fonctionnalités manquantes connues. Je n'ai pas assez de temps pour les réparer moi-même, donc je compte fortement sur les contributeurs.
Problèmes:
Caractéristiques manquantes:
IMPORTANT: Utilisez la même version Python partout, regardez avec quoi le programme que vous déballiez est compilé. Si vous ne le faites pas, vous rencontrerez des problèmes.
method_1.pyrun.pydumps , vous pouvez trouver le fichier .pyc entièrement déballé. Remarque: N'utilisez pas le déballage statique pour quoi que ce soit en dessous de la version 3.9.7, le journal d'audit marshal.loads n'a été ajouté qu'à 3,9.7. Tous les contributeurs sont les bienvenus pour ajouter du support
python3 bypass.py filename.pyc (remplacer filename.pyc par le nom de fichier réel, évidemment)dumps , vous pouvez trouver le fichier .pyc entièrement déballé.Les contributions sont vraiment importantes. Je n'ai pas assez de temps pour résoudre tous les problèmes énumérés ci-dessus. Veuillez contribuer si vous le pouvez.
Les dons sont également vraiment les bienvenus:
BTC - 37RQ1XEB5Q8SCMMKK3MVMD4RBE5FV7EMMH
ETH - 0x28152666867856FA48B3924C185D7E1FB36F3B9A
LTC - Mfhdlrdzaqygzxuvxqfm4rwvgbmrzmdzao
Il s'agit de la rédaction tant attendue sur le processus complet que j'ai suivi pour désobfusquer ou plutôt déballer Pyarmor, je vais passer par toutes les recherches que j'ai faites et à la fin, donnez 3 méthodes pour déballer Pyarmor, elles sont toutes uniques et applicables dans différentes situations. Je tiens à mentionner que je ne savais pas grand-chose sur les internes Python, donc cela m'a pris beaucoup plus de temps que pour d'autres personnes ayant plus d'expérience dans les internes python.
Pyarmor a une documentation très approfondie sur la façon dont ils font tout, je vous recommande de lire entièrement cela. Pyarmor boucle essentiellement via chaque objet de code et le chiffre. Il y a cependant un en-tête fixe et un pied de page. Cela dépend si le «Mode enveloppe» est activé, il est par défaut.
wrap header:
LOAD_GLOBALS N (__armor_enter__) N = length of co_consts
CALL_FUNCTION 0
POP_TOP
SETUP_FINALLY X (jump to wrap footer) X = size of original byte code
changed original byte code:
Increase oparg of each absolute jump instruction by the size of wrap header
Obfuscate original byte code
...
wrap footer:
LOAD_GLOBALS N + 1 (__armor_exit__)
CALL_FUNCTION 0
POP_TOP
END_FINALLY
Des documents Pyarmor
Dans l'en-tête, il y a un appel à la fonction __armor_enter__ , qui décryptera l'objet de code en mémoire. Une fois l'objet de code terminé, la fonction __armor_exit__ sera appelée qui sera à nouveau à nouveau à nouveau l'objet de code, donc aucun objet de code décrypté ne sera laissé en mémoire.
Lorsque nous compilons un script pyarmor, nous pouvons voir qu'il y a le fichier de points d'entrée et un dossier Pytransform. Ce dossier contient une DLL et un fichier __init__.py .
dist
│ test.py
└───pytransform
| _pytransform.dll
| __init__.py
Le fichier __init__.py n'a pas à faire grand-chose avec le décryptage des objets de code. Il est principalement utilisé pour que nous puissions importer le module. Il fait des vérifications comme le système d'exploitation que vous utilisez, si vous souhaitez le lire, il est open source afin que vous puissiez l'ouvrir comme un script Python normal.
La chose la plus importante qu'il fait est de charger le _pytransform.dll et d'exposer ses fonctions aux globals de l'interprète Python. Dans tous les scripts, nous pouvons voir que de pytransform, il importe pyarmor_runtime.
from pytransform import pyarmor_runtime
pyarmor_runtime ()
__pyarmor__ ( __name__ , __file__ , b' x50 x59 x41 x5...' ) Cette fonction créera toutes les fonctions nécessaires pour exécuter des scripts Pyarmor, comme la fonction __armor_enter__ et __armor_exit__ .
La première ressource que j'ai trouvée a été ce fil sur le forum TUTS4You, ici les user extremecoders ont écrit quelques articles sur la façon dont il a déballé le fichier protégé Pyarmor. Il a édité le code source CPYthon pour vider le maréchal de chaque objet de code qui est exécuté.
Bien que cette méthode soit idéale pour exposer toutes les constantes, elle est moins idéale si vous voulez obtenir le bytecode, c'est parce que:
__armor_enter__ est appelée, qui est au début de l'objet de code. Étant donné que la fonction __armor_enter__ le décrypte en mémoire, il ne sera pas déversé par cpython. Il y a des gens qui ont expérimenté le vidage des objets de code déchiffré de la mémoire en injectant le code Python.
Dans cette vidéo, quelqu'un montre comment il démonte toutes les fonctions décryptées en mémoire.
Cependant, il n'a pas encore découvert comment vider le module principal, seulement des fonctions. Heureusement, il a publié son code qu'il avait utilisé pour injecter le code Python. Sur le référentiel GitHub, nous pouvons voir qu'il crée une DLL dans laquelle il appelle une fonction exportée à partir de la DLL Python pour exécuter un code Python simple. Actuellement, il n'a ajouté que pour trouver les DLL Python pour les versions 3.7 à 3.9, mais vous pouvez facilement ajouter plus de versions en modifiant la source et en la recompilant. Il a fait en sorte qu'il exécute le code trouvé dans code.py, de cette façon, il est facile de modifier le code Python sans avoir à reconstruire le projet à chaque fois.
Dans le référentiel, il comprend un fichier Python qui videra tous les noms de la fonction dans un fichier avec leur adresse correspondante en mémoire, s'il n'y a pas de mémoire que cela signifie qu'il n'a pas encore été appelé, donc il n'a pas encore été décrypté.
# Copyright holder: https://github.com/call-042PE
# License: GNU GPL v3.0 (https://github.com/call-042PE/PyInjector/blob/main/LICENSE)
import os , sys , inspect , re , dis , json , types
hexaPattern = re . compile ( r'b0x[0-9A-F]+b' )
def GetAllFunctions (): # get all function in a script
functionFile = open ( "dumpedMembers.txt" , "w+" )
members = inspect . getmembers ( sys . modules [ __name__ ]) # the code will take all the members in the __main__ module, the main problem is that it can't dump main code function
for member in members :
match = re . search ( hexaPattern , str ( member [ 1 ]))
if ( match ):
functionFile . write ( "{ " functionName " : " " + str ( member [ 0 ]) + " " , " functionAddr " : " " + match . group ( 0 ) + " " } n " )
else :
functionFile . write ( "{ " functionName " : " " + str ( member [ 0 ]) + " " , " functionAddr " :null} n " )
functionFile . close ()
GetAllFunctions () Du référentiel de Call-042PE
Dans le code, vous pouvez voir, il a ajouté un commentaire disant que le problème qu'il a est qu'il ne peut pas accéder à l'objet de code du module principal.
Après beaucoup de recherches sur Google, j'ai été perplexe, incapable de trouver quoi que ce soit sur la façon d'obtenir l'objet du code en cours d'exécution actuel. Quelque temps plus tard dans un projet non lié, j'ai vu un appel de fonction à sys._getframe() . J'ai fait des recherches sur ce qu'elle fait, elle obtient le cadre en cours d'exécution actuel.
Vous pouvez donner un entier comme argument qui montera la pile d'appels et obtiendra le cadre à un index spécifique.
sys . _getframe ( 1 ) # get the caller's frameMaintenant, la raison pour laquelle cela est important est parce qu'un cadre de Python est essentiellement juste un objet de code mais avec plus d'informations sur son état en mémoire. Pour obtenir l'objet code à partir d'un cadre, nous pouvons utiliser l'attribut .f_code, vous le connaissez également si vous avez créé une version CPYTHON personnalisée qui vide les objets de code qui sont exécutés car nous obtenons également l'objet code à partir d'un cadre là-bas.
...
1443 tstate -> frame = frame ;
1444 co = frame -> f_code ;
... De ma version Cypython personnalisée
Alors maintenant, nous avons compris comment obtenir l'objet de code en cours d'exécution actuel, nous pouvons simplement monter la pile d'appels jusqu'à ce que nous trouvions le module principal, qui sera décrypté.
Maintenant, nous avons à peu près compris l'idée principale de la façon de déballer Pyarmor. Je vais maintenant montrer 3 méthodes de déballage que j'ai personnellement trouvées utiles dans différentes situations.
Le premier vous oblige à injecter du code Python, vous devrez donc exécuter le script Pyarmor. Lorsque nous vidons l'objet de code principal comme je l'ai expliqué ci-dessus, le problème principal sera que certaines fonctions seront toujours cryptées, donc la première méthode invoque la fonction d'exécution de Pyarmor afin que toutes les fonctions nécessaires pour décrypter les objets de code soient chargés, comme __armor_enter__ et __armor_exit__ .
Cela semble être une chose assez simple à faire, mais Pyarmor a pensé à cela, ils ont mis en œuvre un mode de restriction. Vous pouvez spécifier cela lors de la compilation d'un script pyarmor, par défaut, le mode de restriction est 1.
Je n'ai pas testé tous les modes de restriction, mais cela fonctionne pour le par défaut.
Lorsque nous essayons d'exécuter ce code dans notre REP, vous obtiendrez l'erreur suivante:
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
Check bootstrap restrict mode failed Cela nous empêche de pouvoir utiliser __armor_enter__ et __armor_exit__ .
Ainsi, la prochaine étape que j'ai prise a été de contacter extremecoders sur TUTS4You. Il m'a aidé en mentionnant que je pouvais nativement de corriger le _pytransform.dll . Je tiens également à le remercier de m'avoir donné la solution pour le faire uniquement en Python.
Si nous ouvrons le _pytransform.dll dans un débogueur natif, j'ai choisi x64dbg, nous rechercherons toutes les chaînes du module actuel.
Si nous le filtrons maintenant en recherchant "bootstrap", nous obtiendrons ce qui suit.
Lorsque nous regardons le démontage sur le premier résultat de recherche, vous voyez qu'il y a une référence de _errno indiquant qu'il pourrait y avoir une erreur soulevée, quelques lignes ci-dessous que nous pouvons voir l'erreur que nous obtenons dans Python.
Lorsque nous ne faisons que tout du point du saut qui saute par-dessus le code qui déclenche l'erreur au retour, il n'y a aucun moyen que l'erreur puisse être augmentée.
Maintenant, si nous enregistrons ceci et remplaçons le _pytransform.dll , vous verrez que lorsque nous réessayons le même code, l'erreur ne se produira pas et nous avons accès aux fonctions __armor_enter__ et __armor_exit__ .
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
> >> __armor_enter__
< built - in function __armor_enter__ >
> >> __armor_exit__
< built - in function __armor_exit__ > Maintenant, c'est assez fatigant si nous devons le faire pour chaque script pyarmor que nous voulons déballer, donc extremecoders ont fait un script qui dénonce les adresses spécifiques en mémoire dans Python.
# Credit to extremecoders (https://forum.tuts4you.com/profile/79240-extreme-coders/) for writing the script
# Credit to me for adding the comments explaining it
import ctypes
from ctypes . wintypes import *
VirtualProtect = ctypes . windll . kernel32 . VirtualProtect
VirtualProtect . argtypes = [ LPVOID , ctypes . c_size_t , DWORD , PDWORD ]
VirtualProtect . restype = BOOL
# Load the dll in memory, this is useful because once it's loaded in memory it won't need to get loaded again so all the changes we make will be kept, including the bootstrap bypass
h_pytransform = ctypes . cdll . LoadLibrary ( "pytransform \ _pytransform.dll" )
pytransform_base = h_pytransform . _handle # Get the memory address where the dll is loaded
print ( "[+] _pytransform.dll loaded at" , hex ( pytransform_base ))
# We got this offset like I showed above with x64dbg, it's the first address where we start the NOP
patch_offset = 0x70A18F80 - pytransform_base
num_nops = 0x70A18FD5 - 0x70A18F80 # Minus the end address, this is the size that the NOP will be. The result will be 0x55
oldprotect = DWORD ( 0 )
PAGE_EXECUTE_READWRITE = DWORD ( 0x40 )
print ( "[+] Setting memory permissions" )
VirtualProtect ( pytransform_base + patch_offset , num_nops , PAGE_EXECUTE_READWRITE , ctypes . byref ( oldprotect ))
print ( "[+] Patching bootstrap restrict mode" )
ctypes . memset ( pytransform_base + patch_offset , 0x90 , num_nops ) # 0x90 is NOP
print ( "[+] Restoring memory permission" )
VirtualProtect ( pytransform_base + patch_offset , num_nops , oldprotect , ctypes . byref ( oldprotect ))
print ( "[+] All done! Pyarmor bootstrap restrict mode disabled" ) Si nous mettons ce code A dans un fichier appelé restrict_bypass.py , nous pouvons l'utiliser comme ce qui suit, en utilisant le _pytransform.dll d'origine,
> >> import restrict_bypass
[ + ] _pytransform . dll loaded at 0x70a00000
[ + ] Setting memory permissions
[ + ] Patching bootstrap restrict mode
[ + ] Restoring memory permission
[ + ] All done ! Pyarmor bootstrap restrict mode disabled
>> > from pytransform import pyarmor_runtime
>> > pyarmor_runtime ()
>> > __armor_enter__
< built - in function __armor_enter__ >
> >> __armor_exit__
< built - in function __armor_exit__ > La deuxième méthode commence la même chose que la première méthode, nous injectons le script qui obtient l'objet de code en cours d'exécution actuel.
Ce n'est que maintenant que la différence est que nous ne le viderons pas simplement, nous allons le "réparer". J'entends par là en supprimant complètement Pyarmor afin que nous obtenions l'objet de code d'origine.
Étant donné que Pyarmor a plusieurs options lors de l'obscurcissement, j'ai décidé d'ajouter la prise en charge de toutes les courantes.
Lorsqu'il détecte un script a __armor_enter__ à l'intérieur, il le modifiera afin que l'objet de code revienne juste après que le __armor_enter__ ait été appelé.
Il y a un OPCode POP_TOP suivant l'appel de la fonction, ceci est utilisé pour que la valeur de retour de la fonction soit supprimée de la pile, nous la remplacons simplement par l'opcode RETURN_VALUE afin que nous puissions obtenir la valeur de retour de la fonction __armor_enter__ et afin que nous ayons l'objet de code décrypté en mémoire sans réellement exécuter la bytecode d'origine. Voir l'exemple ci-dessous
1 0 JUMP_ABSOLUTE 18
2 NOP
4 NOP
>> 6 POP_BLOCK
3 8 < 53 >
10 NOP
12 NOP
14 NOP
7 16 JUMP_ABSOLUTE 82
>> 18 LOAD_GLOBAL 5 ( __armor_enter__ )
20 CALL_FUNCTION 0
22 POP_TOP # we change this to RETURN_VALUE
9 24 NOP
26 NOP
28 NOP
30 SETUP_FINALLY 50 ( to 82 ) Parce que Pyarmor modifie l'objet code en mémoire, les modifications resteront même après avoir quitté l'objet code.
Nous pouvons maintenant invoquer (EXEC) l'objet de code. Nous avons maintenant accès à l'objet de code décrypté. Il ne reste plus que de supprimer les modifications de Pyarmor à l'objet de code, c'est-à-dire l'en-tête et le pied de page.
Après cela, nous devons supprimer les __armor_enter__ et __armor_exit__ des co_names .
Nous répétons cela récursivement pour tous les objets de code.
La sortie sera l'objet de code d'origine. Ce sera comme si Pyarmor n'avait jamais été appliqué.
Pour cette raison, nous pouvons utiliser tous nos outils préférés, par exemple Decompyle3 pour obtenir le code source d'origine.
La troisième méthode résout le dernier problème avec la méthode n ° 2.
Dans la méthode n ° 2, nous devons encore exécuter le programme et l'injecter.
Cela peut être un problème car:
La troisième méthode tente de déballer statiquement Pyarmor, avec lequel je veux dire sans rien exécuter du programme obscurci.
Il y a quelques façons de le déballer statiquement, mais la méthode que je vais expliquer semble la plus facile à mettre en œuvre sans avoir à utiliser d'autres outils et / ou langues.
Nous utiliserons les journaux d'audit, les journaux d'audit ont été implémentés dans Python pour des raisons de sécurité. Ironiquement, nous exploiterons les journaux d'audit pour supprimer la sécurité.
Les journaux d'audit loges essentiellement les fonctions internes cpython. Y compris exec et marshal.loads , tous deux que nous pouvons utiliser pour obtenir l'objet de code obscurt principal sans avoir à injecter / exécuter le code. Une liste complète des journaux d'audit peut être trouvée ici
CPYthon a ajouté quelque chose de soigné appelé Hooks Audit, chaque fois qu'un journal d'audit est déclenché, il fera un rappel au crochet que nous avons installé. Le crochet sera simplement une fonction prenant 2 arguments, event , arg .
Exemple d'un crochet d'audit:
import sys
def hook ( event , arg ):
print ( event , arg )
sys . addaudithook ( hook ) La seule façon de sauvegarder des objets de code sur le disque est de le rassembler. Cela signifie que Pyarmor doit crypter les objets de code rassemblés, donc naturellement ils doivent le déchiffrer lorsqu'ils veulent y accéder à Python.
Ils, comme la plupart des autres personnes, utilisent le maréchaueur intégré. Le package s'appelle marshal et c'est un package intégré, écrit en C. C'est l'un des packages qui a des journaux d'audit, donc lorsque Pyarmor l'appelle, nous pouvons voir les arguments.
L'objet Code aura toujours crypté Bytecode, mais nous avons déjà réussi à passer la première "couche", nous pouvons essentiellement réutiliser notre méthode n ° 2 à partir de cette étape car elle doit également traiter avec des objets de code chiffrés. La seule différence maintenant est que chaque objet de code sera chiffré au lieu de ceux qui auraient normalement déjà été exécutés, comme l'objet de code principal.
Parce que dans la méthode n ° 2, nous injectons le code, nous avons déjà accès à toutes les fonctions pyarmor comme __armor_enter__ et __armor_exit__ . Puisque nous essayons de le déballer statiquement, nous n'avons pas ce luxe.
Comme je l'ai mentionné ci-dessus, Pyarmor a des modes de restriction, j'ai déjà montré comment contourner le mode de restriction bootstrap car cela ne se déclenche que lorsque nous exécutons la fonction pyarmor_runtime() .
Nous devons maintenant exécuter l'ensemble du fichier obscurci, qui comprend l'appel __pyarmor__ . Cette fonction déclenche un autre mode de restriction, nous devons donc contourner cela. Je pensais d'abord que nous utilisons une méthode similaire en le corrigeant en nativement.
Un ami a aidé à cela, ce sont les étapes que vous pouvez faire pour le répéter. Gardez à l'esprit que j'ai trouvé une méthode meilleure et plus facile. Pyarmor vérifie si la chaîne Pyarmor est présente à une adresse mémoire spécifique dans __main__ . Nous devons corriger ce chèque. Voir l'image ci-dessous
Maintenant, la meilleure méthode que j'ai trouvée est que le mode de restriction de Pyarmor ne vérifie pas si le fichier principal est directement géré par Python ou s'il a été invoqué, nous pouvons donc simplement le faire:
exec ( open ( filename )) Bien sûr, après avoir installé le crochet d'audit.
Le problème que j'avais était que le crochet d'audit s'est déclenché sur marshal.loads , mais évidemment, après avoir déclenché, je devais charger l'objet de code moi-même, mais cela le déclencherait à nouveau, j'ai donc ajouté un chèque pour voir si le répertoire des dumps existe. Ceci est dangereux car s'il reste un dossier de dumps à partir de avant, cela entraînerait simplement l'exécution du script protégé sans l'arrêter. Nous devons trouver une meilleure façon de le faire.
Edit : J'ai récemment découvert que j'avais oublié la partie où nous devons modifier les sauts absolus. Cette partie couvrira cela.
Lorsque vous devez le faire dans la méthode n ° 2 et la méthode n ° 3. Lorsque nous supprimons le pied de page, il n'y aura pas de collions avec les index. Lorsque nous supprimons l'en-tête, cela entraînera le déplacement des index par la taille de l'en-tête, nous devons donc faire bouclez tous les sauts absolus et soustraire la taille de l'en-tête. Cette partie est assez facile.
for i in range ( 0 , len ( raw_code ), 2 ):
opcode = raw_code [ i ]
if opcode == JUMP_ABSOLUTE :
argument = calculate_arg ( raw_code , i )
new_arg = argument - ( try_start + 2 )
extended_args , new_arg = calculate_extended_args ( new_arg )
for extended_arg in extended_args :
raw_code . insert ( i , EXTENDED_ARG )
raw_code . insert ( i + 1 , extended_arg )
i += 2
raw_code [ i + 1 ] = new_arg De la méthode n ° 3
Nous parcourons le code bytecode et vérifions si l'opcode est l'opcode JUMP_ABSOLUTE . Si c'est le cas, nous calculerons l'argument (en gardant à l'esprit le EXTENDED_ARG ). Ensuite, nous prenons le try_start qui est de la taille de l'en-tête (c'est en fait l'index du dernier opcode de l'en-tête, c'est pourquoi nous ajoutons 2) et le soustrayons de l'argument de l'opcode JUMP_ABSOLUTE .
La partie la plus difficile de la mise en œuvre était de prendre soin des opcodes EXTENDED_ARG que nous devons potentiellement ajouter lorsque l'argument dépasse la taille maximale de 1 octet (255). Nous gérons cela dans calculate_extended_args .
def calculate_extended_args ( arg : int ): # This function will calculate the necessary extended_args needed
extended_args = []
new_arg = arg
if arg > 255 :
extended_arg = arg >> 8
while True :
if extended_arg > 255 :
extended_arg -= 255
extended_args . append ( 255 )
else :
extended_args . append ( extended_arg )
break
new_arg = arg % 256
return extended_args , new_arg De la méthode n ° 3
Pour écrire ce code, j'ai d'abord dû comprendre comment le EXTENDED_ARG a fonctionné exactement.
Cet article a beaucoup aidé à comprendre cet opcode.
Une instruction en Python est de 2 octets dans les versions les plus récentes (3.6+). Un octet est utilisé pour l'opcode et un octet est pour l'argument. Lorsque nous devons dépasser un octet, nous utilisons le EXTENDED_ARG . Cela fonctionne comme ceci:
arg = 300 # Let's say this is the size of our argumentNous savons que le maximum autorisé est 255, nous devons donc utiliser étend_arg, on pourrait penser que ce serait comme ceci:
extended_arg = 255
arg = 45C'est ce que j'ai supposé d'abord, mais après avoir regardé le code généré par Python, j'ai remarqué que c'était comme ceci:
extended_arg = 1
arg = 44 J'étais très confus pourquoi c'était comme ça depuis que je ne voyais aucune corrélation entre ce à quoi je m'attendais et la réalité. L'article lié ci-dessus a tout expliqué.
Python gère le prolongé_arg comme suivant:
extended_arg = extended_arg * 256Après avoir vu cela, tout était clair car cela signifierait que
extended_arg = 1 * 256
arg = 44
print ( extended_arg + arg ) Sortirait 300 .
J'ai appliqué cette logique à la fonction afin qu'il renvoie une liste des opcodes étends_arg nécessaires et de la nouvelle valeur d'argument (qui serait sous ou égal à 255).
J'insère ensuite simplement le prolongé_arg aux bons index.