Я решил, что пришло время, чтобы выпустил правильный распаков из Пьярмора. Все те, которые в настоящее время являются общедоступными, либо устарели, вообще не работают, либо только дают частичный выход. Я планирую сделать это, чтобы поддержать последнюю версию Pyarmor.
Пожалуйста, снимайте репозиторий, если вы нашли его полезным. Я бы очень признателен.
Существует 3 различных метода распаковки Pyarmor, в папке методов в этом репозитории вы найдете все файлы, необходимые для каждого метода. Ниже вы найдете подробную статью о том, как я начал до конца продукта. Я надеюсь, что больше людей на самом деле понимают, как это работает таким образом, а не просто использовать инструмент.
Это список всех известных проблем/отсутствующих функций. У меня не хватает времени, чтобы починить их сам, поэтому я сильно полагаюсь на участников.
Проблемы:
Отсутствуют функции:
ВАЖНО: Используйте одну и ту же версию Python повсюду, посмотрите, с какой программой вы распаковываете. Если вы этого не сделаете, вы столкнетесь с проблемами.
method_1.pyrun.pydumps вы можете найти полностью распакованный файл .pyc . ПРИМЕЧАНИЕ marshal.loads Не используйте статический непосредственный раскевер. Любые участники могут добавить поддержку
python3 bypass.py filename.pyc (замените имя filename.pyc .dumps вы можете найти полностью распакованный файл .pyc .Вклад действительно важен. У меня нет достаточно времени, чтобы исправить все проблемы, перечисленные выше. Пожалуйста, внесите свой вклад, если можете.
Пожертвования также действительно приветствуются:
BTC - 37RQ1XEB5Q8SCMMKK3MVMD4RBE5FV7EMMH
ETH - 0x28152666867856FA48B3924C185D7E1FB36F3B9A
LTC - mfhdlrdzaqygzxuvxqfm4rwvgbmrzmdzao
Это долгожданная статья о полном процессе, через который я прошел, чтобы Deobfuscate или, скорее, распаковывать Pyarmor, я проведу все исследования, которые я провел, и в конце даст 3 метода распаковки Pyarmor, все они уникальны и применимы в разных ситуациях. Я хочу упомянуть, что я мало что знал о внутренних интернатах Python, так что для меня потребовалось намного больше времени, чем для других людей с большим опытом работы в Python Internals.
У Pyarmor есть очень обширная документация о том, как они все делают, я бы порекомендовал вам прочитать это полностью. Паярмор по существу проходит через каждый объект кода и шифрует его. Есть фиксированный заголовок и нижний колонтитул, хотя. Это зависит от того, включен ли «режим обертывания», он по умолчанию.
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
От документов Pyarmor
В заголовке есть призыв к функции __armor_enter__ , которая расширит расшифровку кодового объекта в памяти. После того, как объект кода завершил функцию __armor_exit__ , которая снова будет повторно зашелона по объекту кода, поэтому никакие расшифрованные кодовые объекты не останутся в памяти.
Когда мы скомпилируем сценарий Pyarmor, мы видим, что есть файл точки входа и папку Pytransform. Эта папка содержит DLL и файл __init__.py .
dist
│ test.py
└───pytransform
| _pytransform.dll
| __init__.py
Файл __init__.py не должен многое делать с расшифровкой объектов кода. Он в основном используется, чтобы мы могли импортировать модуль. Он выполняет некоторые проверки, например, какую ОС вы используете, если вы хотите прочитать ее, это открытый исходный код, так что вы можете просто открыть ее, как обычный сценарий Python.
Самое важное, что он делает, - это загрузка _pytransform.dll и выявление своих функций на глобальные значения интерпретатора Python. Во всех сценариях мы видим, что из Pytransform это импортирует Pyarmor_Runtime.
from pytransform import pyarmor_runtime
pyarmor_runtime ()
__pyarmor__ ( __name__ , __file__ , b' x50 x59 x41 x5...' ) Эта функция создаст все функции, необходимые для запуска сценариев Pyarmor, таких как функция __armor_enter__ и __armor_exit__ .
Первым ресурсом, который я нашел, была эта ветка на форуме Tuts4you, здесь пользователь extremecoders написал несколько сообщений о том, как он распаковал защищенный Pyarmor File. Он отредактировал исходный код CPYTHON, чтобы сбросить маршал каждого объекта кода, который выполняется.
Хотя этот метод отлично подходит для разоблачения всех констант, он менее идеально, если вы хотите получить байт -код, это потому, что:
__armor_enter__ , которая находится в начале объекта кода. Поскольку функция __armor_enter__ расшифровывает ее в памяти, она не будет сброшена CPYTHON. Есть некоторые люди, которые экспериментировали с выбросом расшифрованных объектов кода из памяти путем инъекции кода Python.
В этом видео кто -то демонстрирует, как он разбирает все расшифрованные функции в памяти.
Тем не менее, он еще не узнал, как сбросить основной модуль, только функции. К счастью, он опубликовал свой код, который использовал для ввода кода Python. На репозитории GitHub мы видим, что он создает DLL, в котором он вызывает экспортируемую функцию из DLL Python для выполнения простого кода Python. В настоящее время он только добавил поддержку для поиска DLL Python для версий от 3.7 до 3.9, но вы можете легко добавить больше версий, изменяя источник и перекомпинув его. Он сделал так, чтобы он выполнял код, найденный в Code.py, таким образом, легко редактировать код Python без необходимости восстанавливать проект каждый раз.
В репозитории он включает файл Python, который будет выбросить все имена функции в файл с соответствующим адресом в памяти, если не найдено памяти, это означает, что она еще не была вызвана, поэтому он также еще не был расшифрован.
# 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 () От репозитория Call-042PE
В коде вы можете видеть, что он добавил комментарий, в котором говорится, что у него проблема заключается в том, что он не может получить доступ к основному объекту кода модуля.
После большого количества гуглирования я был в тупике, не смог найти что -либо о том, как получить текущий объект работающего кода. Через некоторое время в не связанном проекте я увидел функциональный вызов sys._getframe() . Я провел некоторое исследование того, что он делает, он получает текущую кадр.
Вы можете дать целое число в качестве аргумента, который будет проходить стек вызовов и получить кадр по определенному индексу.
sys . _getframe ( 1 ) # get the caller's frameТеперь причина, по которой это важно, заключается в том, что кадр в Python - это в основном просто кодовый объект, но с большей информацией о его состоянии в памяти. Чтобы получить объект кода из кадра, мы можем использовать атрибут .f_code, вы также будете знакомы с этим, если вы создали пользовательскую версию CPYTHON, которая выпускает объекты кода, которые выполняются, поскольку мы также получаем объект кода из кадра.
...
1443 tstate -> frame = frame ;
1444 co = frame -> f_code ;
... Из моей пользовательской версии cpython
Итак, теперь мы выяснили, как получить текущий объект CODE, мы можем просто пройти стек вызовов, пока не найдем основной модуль, который будет расшифрован.
Теперь мы в значительной степени выяснили основную идею о том, как распаковывать Pyarmor. Теперь я покажу 3 метода распаковки, которые я лично нашел полезным в разных ситуациях.
Первый требует, чтобы вы вводили код Python, поэтому вам придется запустить сценарий Pyarmor. Когда мы сбрасываем основной объект кода, как я объяснил выше основной задачи, будет то, что некоторые функции все еще будут зашифрованы, поэтому первый метод вызывает функцию времени выполнения Pyarmor, так что все функции, необходимые для расшифровки кодовых объектов, были загружены, например, __armor_enter__ и __armor_exit__ .
Это кажется довольно простым, но Пьярмор думал об этом, они внедрили режим ограничения. Вы можете указать это при составлении сценария Pyarmor, по умолчанию режим ограничения составляет 1.
Я не проверял каждый режим ограничения, но он работает для по умолчанию.
Когда мы попытаемся запустить этот код в нашей повседлении, вы получите следующую ошибку:
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
Check bootstrap restrict mode failed Это мешает нам использовать __armor_enter__ и __armor_exit__ .
Таким образом, следующим шагом, который я сделал, было связаться с extremecoders на Tuts4ou. Он помог мне, упомянув, что я могу быть изначально исправить _pytransform.dll . Я также хочу поблагодарить его за то, что он дал мне решение о том, чтобы сделать это исключительно в Python.
Если мы откроем _pytransform.dll в нативном отладчике, я выбрал x64dbg, мы будем искать все строки в текущем модуле.
Если мы отфильтровали это сейчас, поиск «начальной загрузки», мы получим следующее.
Когда мы смотрим разборку в первом результате поиска, вы видите, что существует ссылка на _errno , указывающая, что может быть вызвана некоторая ошибка, несколько строк ниже, которые мы можем увидеть ошибку, которую мы получаем в Python.
Когда мы просто не все с точки зрения прыжка, который перепрыгивает через код, который запускает ошибку до возврата, нет никакого способа, чтобы ошибка была поднята.
Теперь, если мы сохраним это и заменим _pytransform.dll , вы увидите, что когда мы снова попробуем тот же код, ошибка не произойдет, и у нас будет доступ к функциям __armor_enter__ и __armor_exit__ .
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
> >> __armor_enter__
< built - in function __armor_enter__ >
> >> __armor_exit__
< built - in function __armor_exit__ > Теперь это довольно утомительно, если мы должны сделать это для каждого сценария Pyarmor, который мы хотим распаковать, поэтому extremecoders создали сценарий, который не соответствует конкретным адресам в памяти в 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" ) Если мы поместим этот код A в файл с именем restrict_bypass.py , мы можем использовать его как следующее, используя исходный _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__ > Второй метод начинается с того же, что и первый метод, мы вводим скрипт, который получает текущий объект запущенного кода.
Только теперь разница в том, что мы не будем просто его сбросить, мы «починим». Под этим я имею в виду удаление Pyarmor из него полностью, чтобы мы получили исходный объект кода.
Поскольку у Пьярмора есть несколько вариантов, когда запутывает, я решил добавить поддержку для всех общих.
Когда он обнаруживает сценарий имеет __armor_enter__ внутри него, он будет изменен, чтобы объект кода возвращался сразу после вызова __armor_enter__ .
Существует Opcode POP_TOP после вызова функции, он используется таким образом, чтобы возвращаемое значение функции удалялось из стека, мы просто заменяем его на OpCode RETURN_VALUE , чтобы мы могли получить возвращаемое значение функции __armor_enter__ и, чтобы у нас был расшифрованный объект кода в памяти без запуска исходного ByteCode. См. Пример ниже
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 ) Поскольку Pyarmor редактирует объект кода в памяти, изменения останутся даже после того, как мы выйдем из объекта кода.
Теперь мы можем вызвать (exec) объект кода. Теперь у нас есть доступ к расшифрованному кодовому объекту. Все, что осталось сейчас, - это удалить модификации Pyarmor в кодовый объект, который является заголовком обертки и нижнего колонтитула.
После того, как это было очищено, мы должны удалить __armor_enter__ и __armor_exit__ из co_names .
Мы повторяем это рекурсивно для всех объектов кода.
Вывод будет исходный объект кода. Это будет похоже на то, что Пьярмор никогда не применялся.
Из -за этого мы можем использовать все наши любимые инструменты, например, DeCompyle3, чтобы получить исходный исходный код.
Третий метод исправляет последний выпуск с методом № 2.
В методе № 2 мы все еще должны запустить программу и ввести ее.
Это может быть проблемой, потому что:
Третий метод пытается статически распаковывать Pyarmor, с которым я имею в виду, не запуская ничего из запутанной программы.
Есть несколько способов, которыми вы могли бы использовать статически распаковывать его, но метод, который я объясню, выглядит самым простым в реализации без необходимости использовать другие инструменты и/или языки.
Мы будем использовать журналы аудита, журналы аудита были реализованы в Python по соображениям безопасности. Теперь по иронии судьбы мы будем эксплуатировать журналы аудита для удаления безопасности.
Ревизионные журналы по существу регистрируют внутренние функции CPYTHON. Включая exec и marshal.loads , оба из которых мы можем использовать для получения основного запутанного объекта кода без необходимости вводить/запустить код. Полный список журналов аудита можно найти здесь
CPYTHON добавил что -то аккуратное, называемое аудиторскими крючками, каждый раз, когда запускается журнал аудита, он будет выполнять обратный вызов на крючок, который мы установили. Крюк будет просто функцией, принимающей 2 аргумента, event , arg .
Пример аудиторского крючка:
import sys
def hook ( event , arg ):
print ( event , arg )
sys . addaudithook ( hook ) Единственный способ сохранить кодовые объекты на диск, - это маршал. Это означает, что Pyarmor должен зашифровать маршалированные объекты кода, поэтому, естественно, им приходится расшифровать его, когда они хотят получить к нему доступ в Python.
Они, как и большинство других людей, используют встроенный маршаллер. Пакет называется marshal , и это встроенный пакет, написанный в C. Это один из пакетов, в которых есть журналы аудита, поэтому, когда Pyarmor называет его, мы можем увидеть аргументы.
Кодовый объект по-прежнему будет зашифрован, но нам уже удалось преодолеть первый «слой», мы можем в основном повторно использовать наш метод № 2 с этого этапа, поскольку он также должен иметь дело с зашифрованными объектами кода. Единственное отличие сейчас заключается в том, что каждый объект кода будет зашифрован, а не те, которые обычно уже были бы запускаются, например, основной объект кода.
Потому что в методе № 2 мы вводим код, у нас уже есть доступ ко всем функциям Pyarmor, таким как __armor_enter__ и __armor_exit__ . Поскольку мы стараемся распаковать его по статически, у нас нет такой роскоши.
Как я уже упоминал выше, у Pyarmor есть режимы ограничения, я уже показал, как обойти режим ограничения начальной загрузки, поскольку это запускается только тогда, когда мы запускаем функцию pyarmor_runtime() .
Теперь нам нужно запустить весь сфузуенный файл, который включает в себя звонок __pyarmor__ . Эта функция запускает еще один режим ограничения, поэтому мы должны обойти это. Сначала я думал, что мы используем аналогичный метод, устанавливая его назначен.
Друг помог с этим, это шаги, которые вы можете сделать, чтобы повторить это. Имейте в виду, что я нашел лучший и более простой метод. Pyarmor проверяет, присутствует ли строка Pyarmor на определенном адресе памяти в __main__ . Нам нужно исправить этот чек. Смотрите изображение ниже
Теперь лучший метод, который я обнаружил, состоит в том, что режим ограничения Pyarmor не проверяет, находится ли основной файл непосредственно с помощью Python или он был вызван, поэтому мы можем просто сделать это:
exec ( open ( filename )) Конечно, после того, как мы установили аудиторский крючок.
Проблема, которая у меня была, заключалась в том, что аудиторский крючок, запускаемый на marshal.loads , но, очевидно, после того, как он запустил, мне нужно было загрузить кодовый объект самостоятельно, но это просто запустило бы его снова, поэтому я добавил чек, чтобы увидеть, существует ли каталог dumps . Это опасно, потому что, если все еще остается папка dumps , прежде чем она просто приведет к выполнению защищенного сценария, не останавливая его. Мы должны найти лучший способ сделать это.
РЕДАКТИРОВАТЬ : Я недавно обнаружил, что забыл о той части, где нам нужно отредактировать абсолютные прыжки. Эта часть будет охватывать это.
Когда необходимо сделать это как в методе № 2, так и в методе № 3. Когда мы снимаем нижний колонтитул, с индексами не будет коллионов. Когда мы снимаем заголовок, однако это приведет к тому, что индексы сдвинутся на размер заголовка, поэтому нам нужно зацикливаться на всех абсолютных прыжках и вычесть размер заголовка. Эта часть довольно проста.
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 Из метода № 3
Мы пробегаем через байт -код и проверяем, является ли оптовой код opcode JUMP_ABSOLUTE opcode. Если это так, мы рассчитаем аргумент (помня EXTENDED_ARG ). Затем мы берем try_start , который имеет размер заголовка (на самом деле это индекс последнего опкода из заголовка, поэтому мы добавляем 2) и вычитаем его из аргумента JUMP_ABSOLUTE opcode.
Самая сложная часть реализации этого заключалась в том, чтобы позаботиться о опкодах EXTENDED_ARG , которые мы потенциально должны добавить, когда аргумент превышает максимальный размер 1 байт (255). Мы обрабатываем с этим в 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 Из метода № 3
Чтобы написать этот код, я сначала должен был понять, как точно работал EXTENDED_ARG .
Эта статья очень помогла понять этот Opcode.
Инструкция в Python составляет 2 байта в более поздних версиях (3,6+). Один байт используется для OpCode, а один байт предназначен для аргумента. Когда нам нужно превышать один байт, мы используем EXTENDED_ARG . Это в основном работает так:
arg = 300 # Let's say this is the size of our argumentМы знаем, что максимально разрешение - 255, поэтому нам нужно использовать extended_arg, вы думаете, что это будет похоже на это:
extended_arg = 255
arg = 45Это то, что я впервые предположил, но, посмотрев на код, который сгенерировал Python, я заметил, что это было так:
extended_arg = 1
arg = 44 Я был очень смущен, почему это было так, так как я не увидел никакой корреляции между тем, что я ожидал и реальностью. Связанная выше статья объяснила все.
Python обрабатывает extended_arg, как следующее:
extended_arg = extended_arg * 256После того, как все это было ясно, поскольку это будет означать, что
extended_arg = 1 * 256
arg = 44
print ( extended_arg + arg ) Выведет 300 .
Я применил эту логику к функции, чтобы она возвращала список необходимых Opcodes extended_arg и нового значения аргумента (которое было бы ниже или равно 255).
Затем я просто вставляю extended_arg в правильный индекс.