Decidi que era hora de que havia um descendente de pyarmor adequado lançado. Todos os que atualmente são públicos estão desatualizados, não estão trabalhando ou apenas dando saída parcial. Eu pretendo fazer com que este suporte a versão mais recente do Pyarmor.
Por favor, estrela o repositório se você achou útil. Eu realmente aprecio isso.
Existem 3 métodos diferentes para descompactar o pyarmor, na pasta Métodos neste repositório, você encontrará todos os arquivos necessários para cada método. Abaixo, você encontrará uma redação detalhada sobre como eu comecei até o produto final. Espero que mais pessoas entendam como funciona dessa maneira, em vez de apenas usar a ferramenta.
Esta é uma lista de todos os problemas conhecidos/recursos ausentes. Não tenho tempo suficiente para consertá -los sozinho, por isso confio muito nos colaboradores.
Problemas:
Recursos ausentes:
IMPORTANTE: Use a mesma versão Python em todos os lugares, veja o que o programa que você está descompactando é compilado. Se não o fizer, você enfrentará problemas.
method_1.pyrun.pydumps , você pode encontrar o arquivo .pyc totalmente descompactado. NOTA: Não use o desempacote estático para qualquer coisa abaixo da versão 3.9.7, o log de auditoria marshal.loads foi adicionado apenas e após 3.9.7. Quaisquer colaboradores são bem -vindos para adicionar suporte
python3 bypass.py filename.pyc (substitua filename.pyc pelo nome do arquivo real, obviamente)dumps , você pode encontrar o arquivo .pyc totalmente descompactado.Contribuições são realmente importantes. Não tenho tempo suficiente para corrigir todos os problemas listados acima. Contribua se puder.
As doações também são realmente bem -vindas:
BTC - 37RQ1XEB5Q8SCMMKK3MVMD4RBE5FV7EMMH
ETH - 0x28152666867856FA48B3924C185D7E1FB36F3B9A
LTC - mfhdlrdzaqygzxuvxqfm4rwvgbmrzmdzao
Este é o tão esperado redação sobre o processo completo pelo qual passei para Deobfuscate, ou melhor, descompacte o pyarmor, passarei por toda a pesquisa que fiz e, no final, darei 3 métodos para descompactar o pyarmor, todos são únicos e aplicáveis em diferentes situações. Quero mencionar que não sabia muito sobre os internos do Python, por isso demorou muito mais para mim do que para outras pessoas com mais experiência em Python Internals.
O Pyarmor tem uma documentação muito extensa sobre como eles fazem tudo, eu recomendaria que você leia isso completamente. O pyarmor passa essencialmente através de todos os objetos de código e o criptografa. Há um cabeçalho e rodapé fixo. Isso depende se o "modo de embrulho" estiver ativado, é por padrão.
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
Dos documentos de pyarmor
No cabeçalho, há uma chamada para a função __armor_enter__ , que descriptografará o objeto Código na memória. Depois que o objeto de código terminar, a função __armor_exit__ será chamada, que re-criptografará o objeto de código novamente para que nenhum código de código descriptografado seja deixado para trás na memória.
Quando compilamos um script pyarmor, podemos ver que existe o arquivo de ponto de entrada e uma pasta pytransform. Esta pasta contém uma DLL e um arquivo __init__.py .
dist
│ test.py
└───pytransform
| _pytransform.dll
| __init__.py
O arquivo __init__.py não precisa fazer muito com descriptografar os objetos do código. É usado principalmente para que possamos importar o módulo. Ele faz algumas verificações como o sistema operacional que você está usando, se quiser lê -lo, é de código aberto para que você possa abri -lo como um script python normal.
A coisa mais importante que ele faz é carregar o _pytransform.dll e expor suas funções aos globais do intérprete Python. Em todos os scripts, podemos ver que, do Pytransform, ele importa pyarmor_runtime.
from pytransform import pyarmor_runtime
pyarmor_runtime ()
__pyarmor__ ( __name__ , __file__ , b' x50 x59 x41 x5...' ) Esta função criará todas as funções necessárias para executar scripts pyarmor, como a função __armor_enter__ e __armor_exit__ .
O primeiro recurso que encontrei foi este tópico no fórum tuts4you, aqui os extremecoders do usuário escreveram algumas postagens sobre como ele desempacotou o arquivo protegido por pyarmor. Ele editou o código -fonte do CPYTHON para despejar o marechal de todo objeto de código que é executado.
Embora esse método seja ótimo para expor todas as constantes, é menos ideal se você deseja obter o bytecode, isso ocorre porque:
__armor_enter__ é chamada, que está no início do objeto Código. Como a função __armor_enter__ descriptografa -a na memória, ela não será despejada pelo Cpython. Algumas pessoas experimentaram despejar os objetos de código descriptografados da memória, injetando código Python.
Neste vídeo, alguém demonstra como ele desmonta todas as funções descriptografadas na memória.
No entanto, ele ainda não descobriu como despejar o módulo principal, apenas funciona. Felizmente, ele publicou seu código que costumava injetar o código Python. No repositório do GitHub, podemos ver que ele cria uma DLL na qual ele chama uma função exportada da DLL Python para executar o código Python simples. Atualmente, ele só adicionou suporte para encontrar as DLLs do Python para as versões 3.7 a 3.9, mas você pode adicionar facilmente mais versões modificando a fonte e recompilando -a. Ele fez isso para executar o código encontrado no code.py, dessa maneira é fácil editar o código Python sem precisar reconstruir o projeto sempre.
No repositório, ele inclui um arquivo python que despejará todos os nomes da função em um arquivo com o endereço correspondente na memória, se não houver memória encontrada, significa que ainda não foi chamado, por isso também não foi descriptografado.
# 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 () Do repositório do Call-042PE
No código que você pode ver, ele adicionou um comentário dizendo que o problema que ele tem é que ele não pode acessar o objeto de código principal do módulo.
Depois de muito pesquisas no Google, fiquei perplexo, incapaz de encontrar nada sobre como obter o objeto do código de execução atual. Algum tempo depois, em um projeto não relacionado, vi uma chamada de função para sys._getframe() . Eu fiz algumas pesquisas sobre o que faz, ele recebe o quadro de corrida atual.
Você pode dar um número inteiro como um argumento que subirá a pilha de chamadas e obterá o quadro em um índice específico.
sys . _getframe ( 1 ) # get the caller's frameAgora, a razão pela qual isso é importante é porque um quadro no Python é basicamente apenas um objeto de código, mas com mais informações sobre seu estado na memória. Para obter o objeto Code de um quadro, podemos usar o atributo .f_code, você também estará familiarizado com isso se tiver criado uma versão CPYTHON personalizada que despeja os objetos de código que são executados à medida que obtemos o objeto Code de um quadro lá.
...
1443 tstate -> frame = frame ;
1444 co = frame -> f_code ;
... Da minha versão personalizada do cpython
Então agora descobrimos como obter o objeto de código em execução atual, podemos simplesmente subir a pilha de chamadas até encontrarmos o módulo principal, que será descriptografado.
Agora, descobrimos a idéia principal de como descompactar pyarmor. Agora mostrarei 3 métodos de descompactação que eu pessoalmente achei úteis em diferentes situações.
O primeiro exige que você injete o código Python, então você terá que executar o script pyarmor. Quando despejarmos o objeto de código principal, como expliquei acima do problema principal, será que algumas funções ainda serão criptografadas, portanto, o primeiro método invoca a função Pyarmor Run -time, para que todas as funções necessárias para descriptografar os objetos de código sejam carregadas, como __armor_enter__ e __armor_exit__ .
Parece uma coisa bastante simples de fazer, mas Pyarmor pensou nisso, eles implementaram um modo restrito. Você pode especificar isso ao compilar um script pyarmor, por padrão, o modo restrito é 1.
Não testei todos os modos restritos, mas funciona para o padrão.
Quando tentamos executar este código em nosso REPL, você receberá o seguinte erro:
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
Check bootstrap restrict mode failed Isso nos impede de usar os __armor_enter__ e __armor_exit__ .
Então, o próximo passo que tomei foi entrar em contato com extremecoders em tuts4you. Ele me ajudou mencionando que eu poderia corrigir de forma nativa o _pytransform.dll . Também quero agradecê -lo por me dar a solução de fazer isso exclusivamente em Python.
Se abrirmos o _pytransform.dll em um depurador nativo, escolhi x64dbg, procuraremos todas as strings no módulo atual.
Se filtrarmos isso agora pesquisando "Bootstrap", obteremos o seguinte.
Quando assistimos à desmontagem no resultado da primeira pesquisa, você vê que há uma referência de _errno indicando que pode haver algum erro levantado, algumas linhas abaixo de que podemos ver o erro que obtemos no Python.
Quando acabamos de notar tudo, desde o ponto do salto que salta sobre o código que aciona o erro ao retorno, não há como o erro ser levantado.
Agora, se salvarmos isso e substituir o _pytransform.dll , você verá que, quando tentarmos o mesmo código novamente, o erro não acontecerá e temos acesso às funções __armor_enter__ e __armor_exit__ .
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
> >> __armor_enter__
< built - in function __armor_enter__ >
> >> __armor_exit__
< built - in function __armor_exit__ > Agora, isso é bastante cansativo se tivermos que fazer isso para todos os scripts de pyarmor que queremos descompactar, para que extremecoders fizeram um script que nops os endereços específicos na memória no 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" ) Se colocarmos este código A em um arquivo chamado restrict_bypass.py , podemos usá -lo como o seguinte, usando o _pytransform.dll original
> >> 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__ > O segundo método inicia o mesmo que o primeiro método, injetamos o script que obtém o objeto de código em execução atual.
Só agora a diferença é que não vamos apenas despejar, "consertaremos". Com isso, quero dizer remover completamente o pyarmor para obter o objeto Código original.
Como o Pyarmor tem várias opções ao ofuscar, decidi adicionar suporte a todos os comuns.
Quando detecta um script possui __armor_enter__ dentro dele, ele o modificará para que o objeto Code retorne logo após a chamada __armor_enter__ .
Existe um código de operação POP_TOP após a chamada de função, isso é usado para que o valor de retorno da função seja removido da pilha, apenas a substituímos pela Opcode RETURN_VALUE para que possamos obter o valor de retorno da função __armor_enter__ e para que tenhamos o objeto de código descriptado na memória sem realmente a execução do original. Veja o exemplo abaixo
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 ) Como o pyarmor edita o objeto Código na memória, as alterações permanecerão mesmo depois de sairmos do objeto Código.
Agora podemos invocar (exec) o objeto de código. Agora temos acesso ao objeto de código descriptografado. Tudo o que resta agora é remover as modificações do pyarmor para o objeto de código, sendo o cabeçalho e o rodapé do envoltório.
Depois disso, precisamos remover os __armor_enter__ e __armor_exit__ dos co_names .
Repetimos isso recursivamente para todos os objetos de código.
A saída será o objeto de código original. Será como se Pyarmor nunca fosse aplicado.
Por esse motivo, podemos usar todas as nossas ferramentas favoritas, por exemplo, descompyle3 para obter o código -fonte original.
O terceiro método corrige o último problema com o método nº 2.
No método nº 2, ainda precisamos executar o programa e injetar -o.
Isso pode ser um problema porque:
O terceiro método tenta descompactar estaticamente o pyarmor, com o qual quero dizer sem executar nada do programa ofuscado.
Existem algumas maneiras pelas quais você pode descompactá -lo estaticamente, mas o método que explicarei parece mais fácil de implementar sem precisar usar outras ferramentas e/ou idiomas.
Usaremos os logs de auditoria, os logs de auditoria foram implementados no Python por razões de segurança. Agora, ironicamente, exploraremos os logs de auditoria para remover a segurança.
Os logs de auditoria registram essencialmente as funções internas do cpython. Incluindo exec e marshal.loads , os quais podemos usar para obter o principal objeto de código ofuscado sem ter que injetar/executar o código. Uma lista completa dos registros de auditoria pode ser encontrada aqui
O CPYTHON adicionou algo de gancho de auditoria chamado, toda vez que um log de auditoria é acionado, ele fará um retorno de chamada ao gancho que instalamos. O gancho será simplesmente uma função fazendo 2 argumentos, event , arg .
Exemplo de um gancho de auditoria:
import sys
def hook ( event , arg ):
print ( event , arg )
sys . addaudithook ( hook ) A única maneira de salvar os objetos de código no disco é marechando -o. Isso significa que o pyarmor precisa criptografar os objetos de código organizados, então, naturalmente, eles precisam descriptografá -lo quando desejam acessá -lo no Python.
Eles, como a maioria das outras pessoas, usam o Marshaller embutido. O pacote é chamado de marshal e é um pacote embutido, escrito em C. É um dos pacotes que possui logs de auditoria; portanto, quando o pyarmor o chama, podemos ver os argumentos.
O objeto Code ainda terá o bytecode criptografado, mas já conseguimos superar a primeira "camada", podemos basicamente reutilizar nosso método nº 2 a partir deste estágio, pois também precisa lidar com objetos de código criptografados. A única diferença agora é que todo objeto de código será criptografado em vez dos que normalmente já teriam sido executados, como o objeto principal do código.
Porque no método #2, injetamos o código, já temos acesso a todas as funções pyarmor como __armor_enter__ e __armor_exit__ . Como tentamos desempacotar estaticamente, não temos esse luxo.
Como mencionei acima, o Pyarmor tem modos restringidos, já mostrei como ignorar o modo de restrição de bootstrap, pois isso só é acionado quando executamos a função pyarmor_runtime() .
Agora precisamos executar todo o arquivo ofuscado, que inclui a chamada __pyarmor__ . Essa função desencadeia outro modo restrito, por isso temos que ignorar isso. Primeiro, eu estava pensando que usamos um método semelhante, corrigindo -o nativamente.
Um amigo ajudou nisso, essas são as etapas que você pode fazer para repeti -lo. Lembre -se de que encontrei um método melhor e mais fácil. O pyarmor verifica se a string pyarmor está presente em um endereço de memória específico em __main__ . Precisamos corrigir esta verificação. Veja a imagem abaixo
Agora, o melhor método que descobri é que o modo restrito de Pyarmor não verifica se o arquivo principal é executado diretamente pelo Python ou se foi invocado, para que possamos simplesmente fazer isso:
exec ( open ( filename )) É claro que depois de instalarmos o gancho de auditoria.
O problema que tive foi que o gancho de auditoria acionado no marshal.loads , mas obviamente depois que ele desencadeou, eu precisava carregar o objeto de código, mas isso o desencadearia novamente, então adicionei uma verificação para ver se o diretório dumps existe. Isso é perigoso, porque se ainda houver uma pasta de dumps , antes de resultar na execução do script protegido sem interromper. Temos que encontrar uma maneira melhor de fazer isso.
EDIT : Descobri recentemente que esqueci a parte em que precisamos editar os saltos absolutos. Esta parte vai cobrir isso.
Quando precisar fazer isso nos dois métodos e métodos nº 3. Quando removermos o rodapé, não haverá coliões com os índices. Quando removemos o cabeçalho, no entanto, ele fará com que os índices mudem pelo tamanho do cabeçalho, para que precisemos fazer um loop sobre todos os saltos absolutos e subtrair o tamanho do cabeçalho. Essa parte é 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 Do método nº 3
Voltamos através do bytecode e verificamos se o código OPCOU é o código OPCODSOLUTE JUMP_ABSOLUTE . Se for, calcularemos o argumento (mantendo em mente o EXTENDED_ARG ). Em seguida, pegamos o try_start , que é o tamanho do cabeçalho (na verdade é o índice do último código de operação do cabeçalho, é por isso que adicionamos 2) e subtraímos -o do argumento do código de opção JUMP_ABSOLUTE .
A parte mais difícil da implementação disso foi cuidar dos códigos de operações EXTENDED_ARG que potencialmente temos que adicionar quando o argumento passa pelo tamanho máximo de 1 byte (255). Lidamos com isso em 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 Do método nº 3
Para escrever este código, primeiro tive que entender como o EXTENDED_ARG funcionou exatamente.
Este artigo ajudou muito a entender esse código OPC.
Uma instrução no Python é de 2 bytes nas versões mais recentes (3,6+). Um byte é usado para o código de opções e um byte é para o argumento. Quando precisamos exceder um byte, usamos o EXTENDED_ARG . Basicamente funciona assim:
arg = 300 # Let's say this is the size of our argumentSabemos que o máximo permitido é 255, por isso precisamos usar estendidas_arg, você pensaria que seria assim:
extended_arg = 255
arg = 45Foi o que eu assumi pela primeira vez, mas depois de olhar para o código que o Python gerou, notei que era assim:
extended_arg = 1
arg = 44 Fiquei muito confuso por que era assim, pois não via correlação entre o que eu esperava e a realidade. O artigo vinculado acima explicou tudo.
Python lida com o estendido_arg como o seguinte:
extended_arg = extended_arg * 256Depois de ver isso, tudo ficou claro, pois significaria que
extended_arg = 1 * 256
arg = 44
print ( extended_arg + arg ) Produziria 300 .
Apliquei essa lógica à função para que ele retorne uma lista dos códigos OPCODES EXTENDEND_ARG necessários e o novo valor de argumento (que estaria abaixo ou igual a 255).
Em seguida, apenas insiro o Extended_arg no índice correto.