Decidí que era hora de que hubiera un desempaquero de Pyarmor apropiado lanzado. Todos los que actualmente son públicos están desactualizados, no funcionan en absoluto o solo dan una producción parcial. Planeo hacer que esta sea compatible con la última versión de Pyarmor.
Estrella el repositorio si lo encontró útil. Realmente lo apreciaría.
Hay 3 métodos diferentes para desempacar Pyarmor, en la carpeta de métodos en este repositorio encontrará todos los archivos necesarios para cada método. A continuación encontrará una redacción detallada sobre cómo comencé hasta el producto final. Espero que más personas realmente entiendan cómo funciona de esta manera en lugar de solo usar la herramienta.
Esta es una lista de todos los problemas conocidos/características faltantes. No tengo suficiente tiempo para arreglarlos yo mismo, así que confío en gran medida en los contribuyentes.
Asuntos:
Características faltantes:
IMPORTANTE: Use la misma versión de Python en todas partes, mire con qué se compila el programa con el que está desempaquetando. Si no lo hace, enfrentará problemas.
method_1.pyrun.pydumps puede encontrar el archivo .pyc totalmente desempaquetado. Nota: No use el desempaquador estático para nada a continuación, la versión 3.9.7, el registro de auditoría marshal.loads solo se agregó en y después de 3.9.7. Cualquier contribuyente es bienvenido a agregar apoyo
python3 bypass.py filename.pyc (reemplace filename.pyc con el nombre de archivo real, obviamente)dumps puede encontrar el archivo .pyc totalmente desempaquetado.Las contribuciones son realmente importantes. No tengo tiempo suficiente para solucionar todos los problemas enumerados anteriormente. Por favor, contribuya si puede.
Las donaciones también son realmente bienvenidas:
BTC - 37RQ1XEB5Q8SCMMKK3MVMD4RBE5FV7EMMH
Eth - 0x28152666867856fa48b3924c185d7e1fb36f3b9a
LTC - mfhdlrdzaqygzxuvxqfm4rwvgbmrzmdzao
Este es el recurso tan esperado sobre el proceso completo que pasé para desobfuscar o más bien desempacar Pyarmor, revisaré toda la investigación que hice y al final dará 3 métodos para desempacar Pyarmor, todos son únicos y aplicables en diferentes situaciones. Quiero mencionar que no sabía mucho sobre las partes internas de Python, así que me llevó mucho más tiempo que para otras personas con más experiencia en Python Internals.
Pyarmor tiene una documentación muy extensa sobre cómo hacen todo, le recomendaría que lo lea por completo. Pyarmor esencialmente bucle a través de cada objeto de código y lo cifra. Sin embargo, hay un encabezado y pie de página fijo. Esto depende de si el "modo de envoltura" está habilitado, es por defecto.
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
De los documentos de Pyarmor
En el encabezado hay una llamada a la función __armor_enter__ , que descifrará el objeto de código en la memoria. Después de que el objeto de código haya terminado, se llamará a la función __armor_exit__ que volverá a entrenar el objeto de código, por lo que no se dejan los objetos de código descifrados en la memoria.
Cuando compilamos un script Pyarmor, podemos ver que está el archivo de punto de entrada y una carpeta Pytransform. Esta carpeta contiene una DLL y un archivo __init__.py .
dist
│ test.py
└───pytransform
| _pytransform.dll
| __init__.py
El archivo __init__.py no tiene que hacer mucho para descifrar los objetos de código. Se usa principalmente para que podamos importar el módulo. Hace algunas verificaciones como qué sistema operativo está usando, si desea leerlo, es de código abierto, por lo que puede abrirlo como un script de Python normal.
Lo más importante que hace es cargar _pytransform.dll y exponer sus funciones a los globales del intérprete de Python. En todos los scripts podemos ver que de Pytransform, importa pyarmor_runtime.
from pytransform import pyarmor_runtime
pyarmor_runtime ()
__pyarmor__ ( __name__ , __file__ , b' x50 x59 x41 x5...' ) Esta función creará todas las funciones necesarias para ejecutar scripts de Pyarmor, como la función __armor_enter__ y __armor_exit__ .
El primer recurso que encontré fue este hilo en el foro tuts4You, aquí los usuarios extremecoders escribieron algunas publicaciones sobre cómo desempaquetó el archivo protegido de Pyarmor. Editó el código fuente de CPyThon para volcar al mariscal de cada objeto de código que se ejecuta.
Si bien este método es excelente para exponer todas las constantes, es menos ideal si desea obtener el Bytecode, esto se debe a:
__armor_enter__ , que está al inicio del objeto de código. Dado que la función __armor_enter__ lo descifra en la memoria, no será abandonado por CPython. Hay algunas personas que han experimentado al descargar los objetos de código descifrados de la memoria al inyectar el código de Python.
En este video, alguien demuestra cómo desmonta todas las funciones descifradas en la memoria.
Sin embargo, aún no ha descubierto cómo tirar el módulo principal, solo funciona. Afortunadamente publicó su código que usó para inyectar el código de Python. En el repositorio de GitHub podemos ver que crea una DLL en la que llama una función exportada de la DLL de Python para ejecutar el código Python simple. Actualmente solo ha agregado soporte para encontrar las DLL de Python para las versiones 3.7 a 3.9, pero puede agregar fácilmente más versiones modificando la fuente y recompensándola. Lo hizo para que ejecute el código encontrado en Code.py, de esta manera es fácil editar el código de Python sin tener que reconstruir el proyecto cada vez.
En el repositorio incluye un archivo de Python que arrojará todos los nombres de la función a un archivo con su dirección correspondiente en la memoria, si no se encuentra memoria, significa que aún no se ha llamado, por lo que aún no se ha descifrado.
# 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 () Desde el repositorio de Call-042PE
En el código que puede ver, agregó un comentario diciendo que el problema que tiene es que no puede acceder al objeto del código del módulo principal.
Después de mucho en Google, me quedé perplejo, incapaz de encontrar nada sobre cómo obtener el objeto de código de ejecución actual. Algún tiempo después, en un proyecto no relacionado, vi una llamada de función a sys._getframe() . Investigué un poco sobre lo que hace, obtiene el marco de ejecución actual.
Puede dar un entero como un argumento que subirá la pila de llamadas y obtendrá el marco en un índice específico.
sys . _getframe ( 1 ) # get the caller's frameAhora, la razón por la que esto es importante es porque un marco en Python es básicamente solo un objeto de código pero con más información sobre su estado en la memoria. Para obtener el objeto de código de un marco, podemos usar el atributo .f_code, también estará familiarizado con esto si ha creado una versión personalizada de CPyThon que descarga los objetos de código que se ejecutan a medida que también obtenemos el objeto de código de un marco allí.
...
1443 tstate -> frame = frame ;
1444 co = frame -> f_code ;
... Desde mi versión personalizada de Cpython
Así que ahora hemos descubierto cómo obtener el objeto de código de ejecución actual, simplemente podemos subir la pila de llamadas hasta que encontremos el módulo principal, que se descifrará.
Ahora hemos descubierto la idea principal de cómo desempacar Pyarmor. Ahora mostraré 3 métodos para desempacar que personalmente he encontrado útil en diferentes situaciones.
El primero requiere que inyecte el código Python, por lo que tendrá que ejecutar el script Pyarmor. Cuando descargamos el objeto de código principal como expliqué anteriormente, el problema principal será que algunas funciones aún se encriptarán, por lo tanto, el primer método invoca la función de tiempo de ejecución de Pyarmor para que todas las funciones necesarias para descifrar los objetos de código se carguen, como __armor_enter__ y __armor_exit__ .
Esto parece algo bastante simple, pero Pyarmor pensó en esto, implementaron un modo de restricción. Puede especificar esto al compilar un script Pyarmor, de forma predeterminada, el modo de restricción es 1.
No he probado todos los modos de restricción, pero funciona para el predeterminado.
Cuando intentemos ejecutar este código en nuestro replica, obtendrá el siguiente error:
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
Check bootstrap restrict mode failed Esto nos impide que podamos usar __armor_enter__ y __armor_exit__ .
Entonces, el siguiente paso que tomé fue contactar extremecoders en Tuts4You. Me ayudó al mencionar que podría parchear de forma nativa _pytransform.dll . También quiero agradecerle por darme la solución de hacer esto únicamente en Python.
Si abrimos _pytransform.dll en un depurador nativo, elegí X64DBG, buscaremos todas las cadenas en el módulo actual.
Si filtramos esto ahora buscando "bootstrap", obtendremos lo siguiente.
Cuando observamos el desmontaje en el primer resultado de búsqueda, ves que hay una referencia de _errno que indica que podría haber algún error, algunas líneas debajo de que podemos ver el error que recibimos en Python.
Cuando simplemente no hemos todo desde el punto del salto que salta sobre el código que desencadena el error al retorno, no hay forma de que se pueda plantear el error.
Ahora, si guardamos esto y reemplazamos el _pytransform.dll , verá que cuando intentemos el mismo código nuevamente, el error no sucederá y tenemos acceso a las funciones __armor_enter__ y __armor_exit__ .
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
> >> __armor_enter__
< built - in function __armor_enter__ >
> >> __armor_exit__
< built - in function __armor_exit__ > Ahora, esto es bastante agotador si tenemos que hacer esto para cada script de Pyarmor que queremos desempacar, por lo que extremecoders hicieron un script que come las direcciones específicas en la memoria en 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 colocamos este código A en un archivo llamado restrict_bypass.py , podemos usarlo como lo siguiente, usando el original _pytransform.dll
> >> 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__ > El segundo método comienza lo mismo que el primer método, inyectamos el script que obtiene el objeto de código de ejecución actual.
Solo ahora la diferencia es que no solo lo descargaremos, lo "arreglaremos". Con eso me refiero a eliminar Pyarmor por completo para que obtengamos el objeto de código original.
Dado que Pyarmor tiene múltiples opciones al ofuscar, decidí agregar soporte para todos los comunes.
Cuando detecta, un script tiene __armor_enter__ dentro de él, lo modificará para que el objeto de código se devuelva justo después de que se haya llamado a __armor_enter__ .
Hay un código de operación POP_TOP después de la llamada de función, esto se usa para que el valor de retorno de la función se elimine de la pila, simplemente lo reemplazamos con el código de operación RETURN_VALUE para que podamos obtener el valor de retorno de la función __armor_enter__ y para que tengamos el objeto de código descifrado en la memoria sin ejecutar el by -órgano original. Vea el ejemplo a continuación
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 ) Debido a que Pyarmor edita el objeto de código en la memoria, los cambios permanecerán incluso después de salir del objeto de código.
Ahora podemos invocar (exec) el objeto de código. Ahora tenemos acceso al objeto de código descifrado. Todo lo que queda ahora es eliminar las modificaciones de Pyarmor al objeto de código, que es el encabezado y el pie de página.
Después de eso se ha limpiado, tenemos que eliminar el __armor_enter__ y __armor_exit__ de los co_names .
Repetimos esto recursivamente para todos los objetos de código.
La salida será el objeto de código original. Será como si Pyarmor nunca se aplicara.
Debido a esto, podemos usar todas nuestras herramientas favoritas, por ejemplo, Decompyle3 para obtener el código fuente original.
El tercer método soluciona el último problema con el método #2.
En el Método #2 todavía tenemos que ejecutar el programa e inyectarlo.
Esto puede ser un problema porque:
El tercer método intenta desempacar estáticamente a Pyarmor, con lo que quiero decir sin ejecutar nada del programa ofuscado.
Hay algunas maneras en que podría hacer lo que puede desempacarlo, pero el método que explicaré se ve más fácil de implementar sin tener que usar otras herramientas y/o idiomas.
Utilizaremos registros de auditoría, los registros de auditoría se implementaron en Python por razones de seguridad. Ahora, irónicamente, explotaremos los registros de auditoría para eliminar la seguridad.
Los registros de auditoría esencialmente registran funciones internas de CPython. Incluyendo exec y marshal.loads , los cuales podemos usar para obtener el objeto de código ofensivo principal sin tener que inyectar/ejecutar el código. Se puede encontrar una lista completa de registros de auditoría aquí.
Cpython agregó algo ordenado llamado Audit Hooks, cada vez que se activa un registro de auditoría, hará una devolución de llamada al gancho que instalamos. El gancho simplemente será una función que tome 2 argumentos, event , arg .
Ejemplo de un gancho de auditoría:
import sys
def hook ( event , arg ):
print ( event , arg )
sys . addaudithook ( hook ) La única forma de guardar los objetos de código en el disco es reunirlo. Esto significa que Pyarmor tiene que cifrar los objetos de código organizados, por lo que, naturalmente, tienen que descifrarlo cuando quieran acceder a él en Python.
Ellos, como la mayoría de las otras personas, usan el Marshaller incorporado. El paquete se llama marshal y es un paquete incorporado, escrito en C. Es uno de los paquetes que tiene registros de auditoría, por lo que cuando Pyarmor lo llama podemos ver los argumentos.
El objeto de código aún habrá cifrado el código de bytecodo, pero ya logramos superar la primera "capa", básicamente podemos reutilizar nuestro método #2 de esta etapa, ya que también tiene que lidiar con los objetos de código cifrados. La única diferencia ahora es que cada objeto de código estará encriptado en lugar de los que normalmente ya se habrían ejecutado, como el objeto de código principal.
Porque en el Método #2 inyectamos el código, ya tenemos acceso a todas las funciones de Pyarmor como __armor_enter__ y __armor_exit__ . Como tratamos de desempacarlo estáticamente, no tenemos ese lujo.
Como mencioné anteriormente, Pyarmor ha restringido los modos, ya mostré cómo omitir el modo de restricción de bootstrap ya que eso solo se activa cuando ejecutamos la función pyarmor_runtime() .
Ahora necesitamos ejecutar todo el archivo ofuscado, que incluye la llamada __pyarmor__ . Esa función desencadena otro modo de restricción, por lo que tenemos que evitarlo. Primero estaba pensando que usamos un método similar al parcharlo de forma nativa.
Un amigo ayudó con eso, estos son los pasos que puede hacer para repetirlo. Tenga en cuenta que encontré un método mejor y más fácil. Comprobaciones de pyarmor si la cadena Pyarmor está presente en una dirección de memoria específica en __main__ . Necesitamos parchear este cheque. Vea la imagen a continuación
Ahora, el mejor método que encontré es que el modo de restricción de Pyarmor no verifica si Python ejecuta directamente el archivo principal o si fue invocado, por lo que simplemente podemos hacer esto:
exec ( open ( filename )) Por supuesto, después de instalar el gancho de auditoría.
El problema que tuve fue que el gancho de auditoría activado en marshal.loads , pero obviamente después de haber activado, necesitaba cargar el objeto de código, pero eso lo activaría nuevamente, así que agregué un cheque para ver si existe el directorio dumps . Esto es peligroso porque si todavía queda una carpeta de dumps antes de antes de ejecutar el script protegido sin detenerlo. Tenemos que encontrar una mejor manera de hacerlo.
EDITAR : Recientemente descubrí que olvidé la parte donde necesitamos editar los saltos absolutos. Esta parte cubrirá eso.
Cuando necesita hacer esto tanto en el Método #2 como en el Método #3. Cuando quitemos el pie de página, no habrá coliones con los índices. Sin embargo, cuando retiramos el encabezado, hará que los índices cambien por el tamaño del encabezado, por lo que necesitamos recurrir a todos los saltos absolutos y reste el tamaño del encabezado. Esa parte es bastante fácil.
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 Del método #3
Presentamos el Bytecode y verificamos si el código de operación es el código de operación JUMP_ABSOLUTE . Si es así, calcularemos el argumento (teniendo en cuenta el EXTENDED_ARG ). Luego tomamos el try_start , que es el tamaño del encabezado (en realidad es el índice del último código de operación del encabezado, por eso agregamos 2) y se restamos del argumento del código de operación JUMP_ABSOLUTE .
La parte más difícil de implementar esto fue cuidar los códigos de operación EXTENDED_ARG que potencialmente tenemos que agregar cuando el argumento pasa por encima del tamaño máximo de 1 byte (255). Nos manejamos en 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 Del método #3
Para escribir este código, primero tuve que entender cómo funcionó exactamente el EXTENDED_ARG .
Este artículo ayudó mucho a comprender este código de operación.
Una instrucción en Python es de 2 bytes en las versiones más recientes (3.6+). Se usa un byte para el código de operación y un byte es para el argumento. Cuando necesitamos exceder un byte, usamos el EXTENDED_ARG . Básicamente funciona así:
arg = 300 # Let's say this is the size of our argumentSabemos que el máximo permitido es 255, por lo que necesitamos usar Extended_arg, pensaría que sería así:
extended_arg = 255
arg = 45Eso es lo que asumí por primera vez, pero después de mirar el código que generó Python, noté que era así:
extended_arg = 1
arg = 44 Estaba muy confundido por qué era así ya que no vi correlación entre lo que esperaba y la realidad. El artículo vinculado anteriormente lo explicaba todo.
Python maneja el Extended_arg como lo siguiente:
extended_arg = extended_arg * 256Después de ver esto, todo estaba claro ya que significaría que
extended_arg = 1 * 256
arg = 44
print ( extended_arg + arg ) Emitiría 300 .
Apliqué esa lógica a la función para que devuelva una lista de los códigos de operación Extended_ARG necesarios y el nuevo valor de argumento (que estaría bajo o igual a 255).
Luego solo inserto el Extended_ARG en el índice correcto.