我認為是時候發布適當的Pyarmor拆開式釋放了。當前公共的所有產品要么過時,根本不起作用,要么僅提供部分輸出。我計劃使這一支持最新版本的Pyarmor。
如果您發現它有用,請標記存儲庫。我真的很感激。
在此存儲庫中的方法文件夾中,有3種不同的方法來解開Pyarmor,您將找到每個方法所需的所有文件。在下面,您會找到有關我如何一直到最終產品的詳細文章。我希望更多的人實際上了解它的工作方式,而不僅僅是使用該工具。
這是所有已知問題/缺失功能的列表。我沒有足夠的時間自己修復它們,所以我很大程度上依靠貢獻者。
問題:
缺少功能:
重要的是:在各處使用相同的Python版本,查看您要拆開的程序所包含的程序。如果您不這樣做,您將面臨問題。
method_1.py文件run.py運行部分解開的程序dumps目錄”中,您可以找到完全打開包裝的.pyc文件。注意:請勿將靜態解開器用於3.9.7版本以下的任何內容, marshal.loads審核日誌僅在3.9.7之後添加。歡迎任何貢獻者增加支持
python3 bypass.py filename.pyc (顯然,用實際文件名替換filename.pyc )dumps目錄”中,您可以找到完全打開包裝的.pyc文件。貢獻確實很重要。我沒有足夠的時間來解決上述所有問題。如果可以的話,請做出貢獻。
捐款也非常歡迎:
BTC -37RQ1XEB5Q8SCMMKKKKK3MVMD4RBE5FV7EMMH
ETH -0x28152666867856FA48B3924C185D7E1FB36F3B9A
LTC -MFHDLRDZAQYGZXXUVXQFM4RWVGBMRZMDZAO
這是關於我去除顛覆或拆開Pyarmor的完整過程的期待已久的文章,我將仔細研究我所做的所有研究,最後提供3種拆開Pyarmor的方法,它們都是獨特的,並且適用於不同的情況。我想提一下,我對Python內部的了解不多,所以對我來說花了很多時間比其他在python內部經驗的人更長。
Pyarmor擁有有關他們如何完成所有操作的非常廣泛的文檔,我建議您完全閱讀。 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解釋器的Globals。在所有腳本中,我們都可以看到,從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受保護文件的文章。他編輯了CPYTHON源代碼,以丟棄執行每個代碼對象的元帥。
儘管此方法非常適合公開所有常數,但如果您想獲得字節碼,則不太理想,這是因為:
__armor_enter__函數時才發生,而該函數在代碼對象的開頭。由於__armor_enter__函數將其解密在內存中,因此不會被CPYTHON傾倒。有些人通過注入Python代碼來嘗試從內存中傾倒解密的代碼對象。
在此視頻中,有人演示了他如何在內存中解密所有解密的功能。
但是,他尚未找到如何拋棄主模塊,只有功能。值得慶幸的是,他發表了他用來注入Python代碼的代碼。在GitHub存儲庫上,我們可以看到他創建了一個DLL,其中他稱其為從Python DLL中的導出函數來執行簡單的Python代碼。目前,他僅添加了為3.7至3.9版本的Python DLL找到的支持,但是您可以通過修改源並重新編譯來輕鬆添加更多版本。他做到了這一點,因此它執行了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版本中
因此,現在我們已經弄清楚瞭如何獲取當前的運行代碼對象,我們可以簡單地向上走呼叫堆棧,直到找到將被解密的主模塊。
現在,我們幾乎已經弄清楚瞭如何解開Pyarmor的主要思想。現在,我將展示三種解開包裝的方法,我個人在不同情況下個人發現有用。
第一個要求您注入Python代碼,因此您必須運行Pyarmor腳本。當我們像我在主要問題上解釋的主代碼對像一樣,將仍然將某些函數加密,因此第一個方法調用Pyarmor運行時函數,因此加載了代碼對象所需的所有功能,例如__armor_enter__ and ____________enter__ and __armor_exit__ 。
這似乎是一件很簡單的事情,但是Pyarmor確實考慮了這一點,他們實現了限制模式。您可以在編譯Pyarmor腳本時指定此內容,默認情況下,限制模式為1。
我尚未測試每個限制模式,但它適用於默認模式。
當我們嘗試在REPL中運行此代碼時,您將收到以下錯誤:
> >> from pytransform import pyarmor_runtime
> >> pyarmor_runtime ()
Check bootstrap restrict mode failed這樣可以防止我們能夠使用__armor_enter__和__armor_exit__ 。
因此,我採取的下一步是與Tuts4You上的extremecoders聯繫。他通過提到我可以在_pytransform.dll上進行修補。我還要感謝他為我提供了僅在Python做這件事的解決方案。
如果我們在本機調試器中打開_pytransform.dll ,我選擇了x64dbg,我們將在當前模塊中尋找所有字符串。
如果我們現在通過搜索“ bootstrap”來過濾它,我們將獲得以下內容。
當我們在第一個搜索結果上觀看拆卸時,您會看到_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製作了一個腳本,可以將Memory中的特定地址指向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,以便我們獲得原始代碼對象。
由於Pyarmor在混淆時有多種選擇,因此我決定增加對所有常見的支持。
當它檢測到腳本中的__armor_enter__時,它將對其進行修改,以便在調用__armor_enter__之後立即返回代碼對象。
在函數調用之後,有一個POP_TOP OPCODE,它可以使用該函數的返回值從堆棧中刪除,我們只需將其替換為RETURN_VALUE opcode,以便我們可以在不實際運行原始bytecode的內存中獲得__armor_enter__功能的返回值,以便我們在內存中具有解密的代碼對象。請參閱下面的示例
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修改刪除代碼對象,即包裝標頭和頁腳。
在此之後,我們必須從co_names中刪除__armor_enter__和__armor_exit__ 。
我們為所有代碼對象遞歸地重複此操作。
輸出將是原始代碼對象。就像Pyarmor從未使用過。
因此,我們可以使用我們所有喜歡的工具,例如Depompyle3獲取原始源代碼。
第三種方法用方法#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具有限制模式,我已經顯示瞭如何繞過Bootstrap限制模式,因為只有在運行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是標頭的大小(實際上是從標題中的最後一個opcode的索引,這就是為什麼我們添加2)並從JUMP_ABSOLUTE opcode的參數中減去它。
實施此操作的最困難的部分是照顧當參數超過1字節的最大大小(255)時,我們可能必須添加的EXTENDED_ARG opcodes。我們在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工作方式。
本文有助於理解此操作碼。
Python中的指令是最新版本(3.6+)中的2個字節。一個字節用於操作碼,一個字節用於參數。當我們需要超過一個字節時,我們會使用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 。
我將該邏輯應用到該函數上,以便它返回必要的Extended_arg opcodes和新參數值(將低於或等於255)的列表。
然後,我只需在正確的索引上插入Extended_Arg即可。