{"id":29130556,"url":"https://github.com/qfcy/pymodulehook","last_synced_at":"2025-06-30T04:07:41.615Z","repository":{"id":292142459,"uuid":"978892571","full_name":"qfcy/PyModuleHook","owner":"qfcy","description":"A library for recording arbitrary calls to Python modules, primarily intended for Python reverse engineering and analysis. 记录任意对Python模块的调用的库，主要用于Python逆向分析。","archived":false,"fork":false,"pushed_at":"2025-06-23T18:15:30.000Z","size":122,"stargazers_count":50,"open_issues_count":1,"forks_count":6,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-23T18:45:20.682Z","etag":null,"topics":["cython","hooks","nuitka","pyinstaller","python","python-module-hook","reverse-engineering"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qfcy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-06T17:05:41.000Z","updated_at":"2025-06-23T18:14:28.000Z","dependencies_parsed_at":"2025-05-08T11:21:23.767Z","dependency_job_id":"887c2b4e-2f48-4274-bda4-adf12c3ba723","html_url":"https://github.com/qfcy/PyModuleHook","commit_stats":null,"previous_names":["qfcy/pymodulehook"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/qfcy/PyModuleHook","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qfcy%2FPyModuleHook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qfcy%2FPyModuleHook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qfcy%2FPyModuleHook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qfcy%2FPyModuleHook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qfcy","download_url":"https://codeload.github.com/qfcy/PyModuleHook/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qfcy%2FPyModuleHook/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262708005,"owners_count":23351532,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cython","hooks","nuitka","pyinstaller","python","python-module-hook","reverse-engineering"],"created_at":"2025-06-30T04:07:38.674Z","updated_at":"2025-06-30T04:07:41.600Z","avatar_url":"https://github.com/qfcy.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cspan class=\"badge-placeholder\"\u003e[![Stars](https://img.shields.io/github/stars/qfcy/PyModuleHook)](https://img.shields.io/github/stars/qfcy/PyModuleHook)\u003c/span\u003e\r\n\u003cspan class=\"badge-placeholder\"\u003e[![GitHub release](https://img.shields.io/github/v/release/qfcy/PyModuleHook)](https://github.com/qfcy/PyModuleHook/releases/latest)\u003c/span\u003e\r\n\u003cspan class=\"badge-placeholder\"\u003e[![License: MIT](https://img.shields.io/github/license/qfcy/PyModuleHook)](https://github.com/qfcy/PyModuleHook/blob/main/LICENSE)\u003c/span\u003e\r\n\r\n[English | [中文](README_zh.md)]\r\n\r\n`pymodhook` is a library for recording arbitrary calls to Python modules, intended for Python reverse engineering and analysis.  \r\nThe `pymodhook` library is similar to the Xposed framework for Android, but it not only records function call arguments and return values—it can also record arbitrary method calls of module classes, as well as access to any derived objects, based on the [pyobject.objproxy](https://github.com/qfcy/pyobject?tab=readme-ov-file#object-proxy-classes-objchain-and-proxiedobj) library.\r\n\r\n## Installation\r\n\r\nJust run the command `pip install pymodhook`.  \r\n\r\n## Example Usage\r\n\r\nAn example that hooks the `numpy` and `matplotlib` libraries:\r\n```python\r\nfrom pymodhook import *\r\ninit_hook()\r\nhook_modules(\"numpy\", \"matplotlib.pyplot\", for_=[\"__main__\"]) # Record calls to numpy and matplotlib\r\nenable_hook()\r\nimport numpy as np\r\nimport matplotlib.pyplot as plt\r\narr = np.array(range(1,11))\r\narr_squared = arr ** 2\r\nmean = np.mean(arr)\r\nstd_dev = np.std(arr)\r\nprint(mean, std_dev)\r\n\r\nplt.plot(arr, arr_squared)\r\nplt.show()\r\n\r\n# Display the recorded code\r\nprint(f\"Raw call trace:\\n{get_code()}\\n\")\r\nprint(f\"Optimized code:\\n{get_optimized_code()}\")\r\n```\r\nAfter running, the output will be similar to that generated by tools like IDA:\r\n```python\r\nRaw call trace:\r\nimport numpy as np\r\nmatplotlib = __import__('matplotlib.pyplot')\r\nvar0 = matplotlib.pyplot\r\nvar1 = np.array\r\nvar2 = var1(range(1, 11))\r\nvar3 = var2 ** 2\r\nvar4 = np.mean\r\nvar5 = var4(var2)\r\nvar6 = var2.mean\r\nvar7 = var6(axis=None, dtype=None, out=None)\r\nvar8 = np.std\r\nvar9 = var8(var2)\r\nvar10 = var2.std\r\nvar11 = var10(axis=None, dtype=None, out=None, ddof=0)\r\nex_var12 = str(var5)\r\nex_var13 = str(var9)\r\nvar14 = var0.plot\r\nvar15 = var14(var2, var3)\r\nvar16 = var2.shape\r\nvar17 = var2.shape\r\nvar18 = var2[(slice(None, None, None), None)]\r\nvar19 = var18.ndim\r\nvar20 = var3.shape\r\nvar21 = var3.shape\r\nvar22 = var3[(slice(None, None, None), None)]\r\nvar23 = var22.ndim\r\nvar24 = var2.values\r\nvar25 = var2._data\r\nvar26 = var2.__array_struct__\r\nvar27 = var3.values\r\n...\r\nvar51 = var41.__array_struct__\r\nvar52 = var0.show\r\nvar53 = var52()\r\n\r\nOptimized code:\r\nimport numpy as np\r\nimport matplotlib.pyplot as plt\r\nvar2 = np.array(range(1, 11))\r\nplt.plot(var2, var2 ** 2)\r\nplt.show()\r\n```\r\n\r\n## Detailed Usage\r\n\r\n- `init_hook(export_trivial_obj=True, hook_method_call=False, **kw)`  \r\n  Initializes module hooking. This must be called before using `hook_module()` or `hook_modules()`.  \r\n  - `export_trivial_obj`: Whether to *not* hook basic types (such as int, list, dict) returned by module functions.\r\n  - `hook_method_call`: Whether to hook internal method calls on module class instances (i.e., methods where `self` is a `ProxiedObj` instead of the original object).\r\n  - Other parameters are passed to `ObjChain` via `**kw`.\r\n\r\n- `hook_module(module_name, for_=None, hook_once=False, deep_hook=False, deep_hook_internal=False, hook_reload=True)`  \r\n  Hooks a module so that later imports will return the hooked version.  \r\n  - `module_name`: The name of the module to hook (e.g., `\"numpy\"`).\r\n  - `for_`: Only applies the hook when imported from specific modules (e.g., `[\"__main__\"]`), to avoid errors caused by dependencies between lower-level modules. If not specified, the hook is applied globally.\r\n  - `hook_once`: Only returns the hooked module the first time it is imported; subsequent imports return the original module.\r\n  - `deep_hook`: Whether to hook every function and class within the module instead of just the module itself. When `deep_hook` is `True`, the module is always hooked, and `for_`, `hook_once`, and `enable_hook` have no effect.\r\n  - `deep_hook_internal`: If `deep_hook` is `True`, determines whether to hook objects whose names start with an underscore (excluding double-underscore objects like `__loader__`).\r\n  - `hook_reload`: Whether hooking is still applied after `importlib.reload()` returns a new module.\r\n\r\n- `hook_modules(*modules, **kw)`  \r\n  Hook multiple modules at once, for example, `hook_modules(\"numpy\",\"matplotlib\")`. Other keyword parameters are the same as in `hook_module`.\r\n\r\n- `unhook_module(module_name)`  \r\n  Unhook a specified module, including those hooked with `deep_hook`.\r\n  - `module_name`: The name of the module to unhook.\r\n\r\n- `enable_hook()`  \r\n  Enables the global hook switch (off by default). Only when enabled will imports return the hooked module. Not required if `deep_hook=True`.\r\n\r\n- `disable_hook()`  \r\n  Disables the global hook switch. While disabled, imports will not return the hooked module unless `deep_hook=True` is used.\r\n\r\n- `import_module(module_name)`  \r\n  Imports and returns a submodule object rather than the root module.\r\n  - `module_name`: For example, `\"matplotlib.pyplot\"` will return the `pyplot` submodule.\r\n\r\n- `get_code(*args, **kw)`  \r\n  Generates Python code for the raw call trace, which can be used to reconstruct the current object dependency relationships and usage history.\r\n\r\n- `get_optimized_code(*args, **kw)`  \r\n  Generates optimized code, similar to `get_code`. (Code optimization internally uses a Directed Acyclic Graph, DAG, see details in [pyobject](https://github.com/qfcy/pyobject?tab=readme-ov-file#object-proxy-classes-objchain-and-proxiedobj) library.).\r\n\r\n- `get_scope_dump()`\r\n  Returns a shallow copy of the variable namespace (scope) dictionary of the hook chain, commonly used for debugging and analysis.\r\n\r\n- `dump_scope(file=None)`\r\n  Dumps the entire variable namespace dictionary to the stream `file` using `pprint`. If an object's `__repr__()` method encounters an error, the output will not be interrupted. The default for `file` is `sys.stdout`.\r\n\r\n- `getchain()`  \r\n  Returns the global `pyobject.ObjChain` instance used for module hooking, allowing manual manipulation. If `init_hook()` was not called, returns `None`.\r\n\r\n## How It Works\r\n\r\nInternally, the library uses the `ObjChain` class from the `pyobject.objproxy` library for dynamic code generation. `pymodhook` itself is a higher-level wrapper around `pyobject.objproxy`. For more details, see the [pyobject.objproxy documentation](https://github.com/qfcy/pyobject?tab=readme-ov-file#object-proxy-classes-objchain-and-proxiedobj).\r\n\r\n#### The pymodhook-patches Directory  \r\n\r\nThe `pymodhook-patches` directory contains multiple JSON files named after Python modules. These files define custom attributes and function names that should not be hooked, ensuring compatibility with specific Python libraries.  \r\n\r\nFor example, the structure of `matplotlib.pyplot.json` is as follows:  \r\n```json5\r\n{\r\n    // All keys are optional\r\n    \"export_attrs\": [\"attr\"],  // Attribute names to export (i.e., `plt.attr` returns the original object instead of a `pyobject.ProxiedObj`)\r\n    \"export_funcs\": [\"plot\", \"show\"],  // Function names to export (i.e., return values remain original objects instead of being wrapped)\r\n    \"alias_name\": \"plt\",  // Common module alias (e.g., used for code generation formatting, such as `import matplotlib.pyplot as plt`)\r\n    \"use_proxied_obj\":[\"Figure\"] // Functions/classes that require further tracking; if the output code lacks certain calls, this item can be modified (effective only when deep_hook=True).\r\n}\r\n```\r\n\r\n## Usage of DLL Injection Tool  \r\n\r\nThe repository directory [hook_win32](https://github.com/qfcy/PyModuleHook/tree/main/tools/hook_win32) contains a DLL injection tool. Since it only relies on loaded `python3x.dll`, it supports recording module calls of applications packaged with Nuitka/Cython, not just PyInstaller.  \r\n**Note: Do NOT use this tool to inject any unauthorized commercial softwares!**  \r\n\r\n#### 1. Copy Module Files  \r\nFirstly, install `pymodhook` and its dependency `pyobject` using `pip install pymodhook`.  \r\nThen navigate to `\u003cPython installation directory\u003e/Lib/site-packages` (the Python installation directory may vary depending on the environment) and copy the `pyobject` package, `pymodhook.py`, the `pymodhook-patches` directory, and [\\_\\_hook\\_\\_.py](tools/templates/__hook__.py) into the directory:  \r\n![](https://i-blog.csdnimg.cn/direct/c23cec23ff2b41b0a5086d5e12e25ccf.png)  \r\nAdditionally, if using Python 3.8 or earlier, the `astor` module must also be copied.  \r\n\r\n#### 2. Modify \\_\\_hook\\_\\_.py  \r\n`__hook__.py` is the first piece of Python code executed by the injected DLL. The default `__hook__.py` is as follows:  \r\n```python  \r\n# Template for __hook__.py to be placed in the packaged program directory\r\nimport atexit, pprint, traceback\r\n\r\nCODE_FILE = \"hook_output.py\"\r\nOPTIMIZED_CODE_FILE = \"optimized_hook_output.py\"\r\nVAR_DUMP_FILE = \"var_dump.txt\"\r\nERR_FILE = \"hooktool_err.log\"\r\n\r\ndef export_code():\r\n    try:\r\n        with open(CODE_FILE, \"w\", encoding=\"utf-8\") as f:\r\n            f.write(get_code())\r\n        with open(VAR_DUMP_FILE, \"w\", encoding=\"utf-8\") as f:\r\n            dump_scope(file=f)\r\n        with open(OPTIMIZED_CODE_FILE, \"w\", encoding=\"utf-8\") as f:\r\n            f.write(get_optimized_code())\r\n    except Exception:\r\n        with open(ERR_FILE, \"w\", encoding=\"utf-8\") as f:\r\n            traceback.print_exc(file=f)\r\n\r\ntry:\r\n    from pymodhook import *\r\n    from pyobject.objproxy import ReprFormatProxy\r\n\r\n    init_hook()\r\n    hook_modules(\"wx\",\"matplotlib.pyplot\",\"requests\",deep_hook=True) # This line can be modified by your own\r\n    atexit.register(export_code)\r\nexcept Exception:\r\n    with open(ERR_FILE, \"w\", encoding=\"utf-8\") as f:\r\n        traceback.print_exc(file=f)\r\n```  \r\nGenerally, you only need to modify the line calling `hook_modules()` to include other custom modules. The `deep_hook=True` option is typically used for applications packaged with Cython/Nuitka and is optional for regular applications.  \r\nAdditionally, for specific libraries, you may need to manually modify the [pymodhook-patches directory](pymodhook-patches directory).  \r\n\r\n#### 3. Inject the DLL  \r\nDownload `DLLInject_win_amd64.zip` from the project's [Release](https://github.com/qfcy/PyModuleHook/releases/latest) page.  \r\nAfter downloading, extract and run `hook_win32.exe`, search for the target process, select it, and click the \"Inject DLL\" button:  \r\n![](https://i-blog.csdnimg.cn/direct/bb07a38301994bbabe40413a623feeed.png)  \r\nIf the injection is successful, you will see this prompt:  \r\n![](https://i-blog.csdnimg.cn/direct/1849346064e14ca680daff02b573ffd0.png)  \r\n\r\n#### 4. Retrieve Injection Results  \r\nAfter successful injection, if the program exits normally (without forced termination), the module hook results—`hook_output.py`, `optimized_hook_output.py`, and `var_dump.txt`—will be generated in the working directory of the injected process.  \r\n- `hook_output.py` contains the raw, detailed call logs.  \r\n- `optimized_hook_output.py` contains the simplified module call code.  \r\n- `var_dump.txt` contains the dump of all variables.  \r\n\r\nIf the result generation fails, an additional file `hooktool_err.log` will be created to record the error messages.  \r\n\r\nExample of `optimized_hook_output.py`:  \r\n```python  \r\nimport tkinter as tk  \r\nCanvas = tk.Canvas  \r\nimport matplotlib.pyplot as plt  \r\nimport requests  \r\nvar0 = tk.Tk()  \r\nex_var1 = int(tk.wantobjects)  \r\nvar15 = var0.tk  \r\nvar0.title('Tk')  \r\nvar0.withdraw()  \r\nvar0.iconbitmap('paint.ico')  \r\nvar0.geometry('400x300')  \r\nvar0.overrideredirect(ex_var1)  \r\nvar43 = Frame(var0, bg='gray92')  \r\nvar43._last_child_ids = {}  \r\nvar28 = Canvas(var43, bg='#d0d0d0', fg='#000000')  \r\nvar28.pack(expand=ex_var1, fill='x')  \r\nvar28._last_child_ids = {}  \r\n# external var53: \u003cfunction object at 0x000001F3F0A27180\u003e  \r\nvar0.bind('\u003cButton-1\u003e', var53)  \r\nvar0.mainloop()  \r\n...  \r\n```  \r\n\r\nExample of `var_dump.txt`:  \r\n```python  \r\n{...,  \r\n 'ex_var855': True,  \r\n 'ex_var860': True,  \r\n 'ex_var875': True,  \r\n ...  \r\n 'var123': \u003cfunction BaseWidget.__init__ at 0x04616B28\u003e,  \r\n 'var124': \u003ctkinter.ttk.Button object .!frame.!button3\u003e,  \r\n 'var125': {'command': \u003cbound method Painter.save of \u003cpainter.Painter object at 0x047298F0\u003e\u003e,  \r\n            'text': 'Save',  \r\n            'width': 4},  \r\n 'var126': None,  \r\n 'var127': \u003cfunction BaseWidget._setup at 0x04616AE0\u003e,  \r\n 'var128': {'command': \u003cbound method Painter.save of \u003cpainter.Painter object at 0x047298F0\u003e\u003e,  \r\n            'text': 'Save',  \r\n            'width': 4},  \r\n ...  \r\n 'var146': '.!frame.!button3',  \r\n 'var147': \u003cbuilt-in method call of _tkinter.tkapp object at 0x048C3890\u003e,  \r\n 'var148': '',  \r\n 'var152': \u003cfunction BaseWidget.__init__ at 0x04616B28\u003e,  \r\n 'var153': \u003ctkinter.ttk.Button object .!frame.!button4\u003e,  \r\n 'var154': {'command': \u003cbound method Painter.clear of \u003cpainter.Painter object at 0x047298F0\u003e\u003e,  \r\n            'text': 'Clear',  \r\n            'width': 4},  \r\n  ...  \r\n}  \r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqfcy%2Fpymodulehook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqfcy%2Fpymodulehook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqfcy%2Fpymodulehook/lists"}