pymodhook
是一個記錄任意對Python模塊的調用的庫,用于Python逆向分析。
pymodhook
庫類似于Android的xposed框架,但不僅能記錄函數的調用參數和返回值,還能記錄模塊的類的任意方法調用,以及任意派生對象的訪問,基于pyobject.objproxy庫實現。
備注:請勿用本工具注入未授權的商業軟件!
GitHub: qfcy/PyModuleHook
目錄
- DLL注入工具
- 1.復制模塊文件
- 2.修改 `__hook__.py`
- 3.注入DLL
- 4.獲取注入結果
- 附:pymodhook庫的用法
- 示例
- 詳細用法
- 工作原理
DLL注入工具
由于只依賴于加載的python3x.dll,工具支持記錄Nuitka/Cython打包的應用的模塊調用,而不僅僅是PyInstaller。
1.復制模塊文件
首先用pip install pymodhook
安裝pymodhook
及其依賴的pyobject
包,
再打開<Python安裝目錄>/Lib/site-packages
文件夾(Python安裝目錄視環境而異),將pyobject
包,pymodhook.py
,pymodhook-patches
目錄,和__hook__.py復制到目錄下:
另外如果是Python 3.8或以下的版本,還需要復制astor
模塊。
2.修改 __hook__.py
__hook__.py
是注入的DLL執行的第一段Python代碼,默認的__hook__.py
是:
# 放入打包程序目錄的__hook__.py的模板
import atexit, pprint, tracebackCODE_FILE = "hook_output.py"
OPTIMIZED_CODE_FILE = "optimized_hook_output.py"
VAR_DUMP_FILE = "var_dump.txt"
ERR_FILE = "hooktool_err.log"def export_code():try:with open(CODE_FILE, "w", encoding="utf-8") as f:f.write(get_code())with open(VAR_DUMP_FILE, "w", encoding="utf-8") as f:dump_scope(file=f)with open(OPTIMIZED_CODE_FILE, "w", encoding="utf-8") as f:f.write(get_optimized_code())except Exception:with open(ERR_FILE, "w", encoding="utf-8") as f:traceback.print_exc(file=f)try:from pymodhook import *from pyobject.objproxy import ReprFormatProxyinit_hook()hook_modules("wx","matplotlib.pyplot","requests",deep_hook=True) # 本行可修改atexit.register(export_code)
except Exception:with open(ERR_FILE, "w", encoding="utf-8") as f:traceback.print_exc(file=f)
一般只需要將調用hook_modules()
的這行,修改成自定義的其他模塊即可。deep_hook=True
選項一般用于Cython/Nuitka打包的應用,對于普通應用deep_hook
是可選的。
另外對于特定庫,可能還需要自行修改pymodhook-patches目錄。
3.注入DLL
在項目的Release頁面下載DLLInject_win_amd64.zip。
下載后解壓并運行hook_win32.exe,搜索目標進程并選中,再點擊"Inject DLL"按鈕:
如果注入成功,會看到這個提示:
4.獲取注入結果
注入成功后如果程序退出(非強制終止進程),模塊hook的結果hook_output.py
, optimized_hook_output.py
和var_dump.txt
會在被注入進程的工作目錄生成。
hook_output.py
是原始的詳細調用記錄,optimized_hook_output.py
是簡化后的模塊調用代碼,var_dump.txt
為所有變量的轉儲。
如果結果生成失敗,還會額外生成一個文件hooktool_err.log
,記錄錯誤消息。
optimized_hook_output.py
的示例:
import tkinter as tk
Canvas = tk.Canvas
import matplotlib.pyplot as plt
import requests
var0 = tk.Tk()
ex_var1 = int(tk.wantobjects)
var15 = var0.tk
var0.title('Tk')
var0.withdraw()
var0.iconbitmap('paint.ico')
var0.geometry('400x300')
var0.overrideredirect(ex_var1)
var43 = Frame(var0, bg='gray92')
var43._last_child_ids = {}
var28 = Canvas(var43, bg='#d0d0d0', fg='#000000')
var28.pack(expand=ex_var1, fill='x')
var28._last_child_ids = {}
# external var53: <function object at 0x000001F3F0A27180>
var0.bind('<Button-1>', var53)
var0.mainloop()
...
var_dump.txt
的示例:
{...,'ex_var855': True,'ex_var860': True,'ex_var875': True,...'var123': <function BaseWidget.__init__ at 0x04616B28>,'var124': <tkinter.ttk.Button object .!frame.!button3>,'var125': {'command': <bound method Painter.save of <painter.Painter object at 0x047298F0>>,'text': '保存','width': 4},'var126': None,'var127': <function BaseWidget._setup at 0x04616AE0>,'var128': {'command': <bound method Painter.save of <painter.Painter object at 0x047298F0>>,'text': '保存','width': 4},...'var146': '.!frame.!button3','var147': <built-in method call of _tkinter.tkapp object at 0x048C3890>,'var148': '','var152': <function BaseWidget.__init__ at 0x04616B28>,'var153': <tkinter.ttk.Button object .!frame.!button4>,'var154': {'command': <bound method Painter.clear of <painter.Painter object at 0x047298F0>>,'text': '清除','width': 4},...
}
附:pymodhook庫的用法
運行命令pip install pymodhook
,即可安裝pymodhook
庫。
示例
hook了numpy
和matplotlib
庫的示例:
from pymodhook import *
init_hook()
hook_modules("numpy", "matplotlib.pyplot", for_=["__main__"]) # 記錄numpy和matplotlib的調用
enable_hook()
import numpy as np
import matplotlib.pyplot as plt
arr = np.array(range(1,11))
arr_squared = arr ** 2
mean = np.mean(arr)
std_dev = np.std(arr)
print(mean, std_dev)plt.plot(arr, arr_squared)
plt.show()# 顯示記錄的代碼
print(f"原始調用記錄:\n{get_code()}\n")
print(f"優化后的代碼:\n{get_optimized_code()}")
運行后會出現類似IDA等工具生成的代碼:
原始調用記錄:
import numpy as np
matplotlib = __import__('matplotlib.pyplot')
var0 = matplotlib.pyplot
var1 = np.array
var2 = var1(range(1, 11))
var3 = var2 ** 2
var4 = np.mean
var5 = var4(var2)
var6 = var2.mean
var7 = var6(axis=None, dtype=None, out=None)
var8 = np.std
var9 = var8(var2)
var10 = var2.std
var11 = var10(axis=None, dtype=None, out=None, ddof=0)
ex_var12 = str(var5)
ex_var13 = str(var9)
var14 = var0.plot
var15 = var14(var2, var3)
var16 = var2.shape
var17 = var2.shape
var18 = var2[(slice(None, None, None), None)]
var19 = var18.ndim
var20 = var3.shape
var21 = var3.shape
var22 = var3[(slice(None, None, None), None)]
var23 = var22.ndim
var24 = var2.values
var25 = var2._data
var26 = var2.__array_struct__
var27 = var3.values
...
var51 = var41.__array_struct__
var52 = var0.show
var53 = var52()優化后的代碼:
import numpy as np
import matplotlib.pyplot as plt
var2 = np.array(range(1, 11))
plt.plot(var2, var2 ** 2)
plt.show()
詳細用法
-
init_hook(export_trivial_obj=True, hook_method_call=False, **kw)
初始化模塊hook,需要在調用hook_module()
和hook_modules()
之前調用。export_trivial_obj
:是否不繼續hook模塊函數返回的基本類型(如整數、列表、字典等)。hook_method_call
:是否hook模塊類實例的方法內部調用(即方法的self
傳入的是ProxiedObj
而不是原先的對象)- 其它參數通過
**kw
傳遞給ObjChain
。
-
hook_module(module_name, for_=None, hook_once=False, deep_hook=False, deep_hook_internal=False, hook_reload=True)
鉤住(hook)一個模塊,后續導入這個模塊時會返回hook后的模塊。module_name
:要hook的模塊名(如"numpy"
)。for_
:只有從特定模塊(如["__main__"]
)導入時才應用hook,能避免底層模塊之間相互依賴導致的報錯。如果不提供,默認對所有模塊全局應用hook。hook_once
:只在首次導入時返回hook后的庫,后續直接返回原模塊。deep_hook
:是否hook模塊中的每個函數和類,而不是模塊本身。deep_hook
為True
時模塊始終被hook,for_
,hook_once
和enable_hook
不起作用。deep_hook_internal
:deep_hook
為True
時,是否hook以下劃線開頭的對象(雙下劃線對象如__loader__
除外)。hook_reload
:是否繼續hook通過importlib.reload()
返回的同一新模塊。
-
hook_modules(*modules, **kw)
一次hook多個模塊,如hook_modules("numpy","matplotlib")
,關鍵字參數的其他用法同hook_module
函數。 -
unhook_module(module_name)
取消對指定模塊的hook,包括deep_hook
后的模塊。module_name
:要撤銷hook的模塊名。
-
enable_hook()
啟用模塊hook的全局開關(默認關閉)。啟用后,導入被hook的模塊時才會返回hook對象。deep_hook
為True
時不需要手動調用enable_hook
。 -
disable_hook()
禁用模塊hook的全局開關。禁用時不會導入hook后的模塊,除非使用了deep_hook=True
。 -
import_module(module_name)
導入并返回子模塊對象,而不是根模塊。module_name
:如"matplotlib.pyplot"
,會返回pyplot
子模塊對象。
-
get_code(*args, **kw)
生成模塊原始調用記錄的Python代碼,可用于重現當前對象依賴關系,以及庫調用歷史。 -
get_optimized_code(*args, **kw)
生成優化后的代碼,同get_code
用法。(代碼優化在內部使用了有向無環圖(DAG),詳細優化原理見pyobject庫) -
get_scope_dump()
返回當前hook鏈的變量命名空間(作用域)字典的淺拷貝,用于調試和分析。 -
dump_scope(file=None)
將整個變量命名空間字典用pprint
輸出到流file
,某個對象的__repr__()
方法出錯時輸出不會中斷。file
默認為sys.stdout
。 -
getchain()
返回用于hook模塊的全局pyobject.ObjChain
實例,用于手動操作ObjChain
。如果尚未調用init_hook()
,返回None
。
pymodhook-patches目錄
pymodhook-patches目錄內部包含了多個以模塊名命名的json文件,包含不能hook的自定義的屬性和函數名,用于兼容特定的Python庫。
如matplotlib.pyplot.json
的格式如下:
{// 每個鍵是可選的"export_attrs":["attr"], // 要導出的屬性名(即plt.attr返回原始對象,而不是pyobject.ProxiedObj)"export_funcs":["plot","show"], // 要導出的函數名(即函數的調用返回值是原始對象,而不是pyobject.ProxiedObj)"alias_name":"plt" // 模塊的常用別名,用于控制代碼生成格式(如import matplotlib.pyplot as plt)
}
工作原理
庫在底層使用了pyobject.objproxy
庫中的ObjChain
,用于動態代碼生成,而本pymodhook
庫是pyobject.objproxy
的高層封裝。詳細原理參見pyobject.objproxy的文檔。