├── requirements.txt ├── proxy_pe_source_generator ├── templates │ ├── ProxyLibrary.h │ ├── ProxyLibraryGlobals.h │ └── ProxyLibrary.c └── __init__.py ├── LICENSE.md ├── generate_proxy_pe_source.py ├── README-CN.md ├── README.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | pefile 2 | -------------------------------------------------------------------------------- /proxy_pe_source_generator/templates/ProxyLibrary.h: -------------------------------------------------------------------------------- 1 | // Generated by ProxyPESourceGenerator 2 | 3 | #pragma once 4 | #include 5 | 6 | EXTERN_C void PASCAL ProxyLibrary_Load(); 7 | EXTERN_C void PASCAL ProxyLibrary_Unload(); 8 | -------------------------------------------------------------------------------- /proxy_pe_source_generator/templates/ProxyLibraryGlobals.h: -------------------------------------------------------------------------------- 1 | // Generated by ProxyPESourceGenerator 2 | 3 | #pragma once 4 | #include 5 | 6 | extern LPCSTR libraryFilePath; 7 | extern SIZE_T symbolCount; 8 | extern FARPROC symbolAddressArray[]; 9 | extern LPCSTR symbolNameArray[]; 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | **Copyright © 2025 YukiIsait** 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /generate_proxy_pe_source.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from proxy_pe_source_generator import ProxyPESourceGenerator 3 | 4 | if __name__ == '__main__': 5 | arg_parser = argparse.ArgumentParser(description='Generate source code for a proxy PE file') 6 | arg_parser.add_argument('-f', '--original-pe-file', 7 | required=True, 8 | help='Path to the original PE file') 9 | arg_parser.add_argument('-s', '--pe-file-path-in-source-file', 10 | help='Path to the PE file in the source file (default: same as original PE file path)') 11 | arg_parser.add_argument('-o', '--output-directory', 12 | default='.', 13 | help='Path to the output source directory (default: current directory)') 14 | arg_parser.add_argument('-e', '--encoding', 15 | default='utf-8', 16 | help='Encoding of the output files (default: utf-8)') 17 | args = arg_parser.parse_args() 18 | generator = ProxyPESourceGenerator(args.original_pe_file, args.pe_file_path_in_source_file) 19 | generator.generate_source(args.output_directory, args.encoding) 20 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # 代理库源代码生成器 2 | 3 | 🌍 **[English](README.md) | [简体中文](README-CN.md)** 4 | 5 | 一个用于生成动态链接库(DLL)的代理库源代码的脚本,主要用于实现对原动态链接库的劫持或对目标程序的注入。 6 | 7 | ## 使用方法 8 | 9 | ```text 10 | python3 generate_proxy_pe_source.py 11 | ``` 12 | 13 | ### 选项列表 14 | 15 | - `-f, --original-pe-file` 16 | - 原始 PE 文件路径(必填) 17 | 18 | - `-s, --pe-file-path-in-source-file` 19 | - 生成的源文件中 PE 文件的路径,用于在目标系统中找到相应的 PE 文件 20 | - 默认值:与 `-f, --original-pe-file` 相同 21 | 22 | - `-o, --output-directory` 23 | - 源代码输出目录 24 | - 默认值:当前目录 25 | 26 | - `-e, --encoding` 27 | - 生成的源代码编码 28 | - 默认值:UTF-8 29 | 30 | - `-h, --help` 31 | - 显示帮助信息 32 | 33 | ## 使用示例 34 | 35 | > 更详细的使用示例可参考 [WPSProfileVerificationPatch](https://github.com/YukiIsait/WPSProfileVerificationPatch) 项目。 36 | 37 | 1. 安装 Python 3.x 环境,并安装依赖库: 38 | 39 | ```powershell 40 | pip3 install -r requirements.txt 41 | ``` 42 | 43 | 2. 生成一个 64 位 `ncrypt.dll` 的代理库源代码: 44 | 45 | ```powershell 46 | python3 generate_proxy_pe_source.py -f $env:SystemRoot\System32\ncrypt.dll -s %SystemRoot%\System32\ncrypt.dll -o ncrypt64 47 | ``` 48 | 49 | 3. 生成一个 32 位 `ncrypt.dll` 的代理库源代码: 50 | 51 | ```powershell 52 | python3 generate_proxy_pe_source.py -f $env:SystemRoot\SysWOW64\ncrypt.dll -s %SystemRoot%\System32\ncrypt.dll -o ncrypt32 53 | ``` 54 | 55 | 4. 将生成的源代码文件添加到基于 MSBuild 的 DLL 项目中,由于不同架构的 `ncrypt.dll` 导出定义相同,可以将 `ProxyLibraryExports.asm` 文件重命名为 64 位和 32 位版本并设置条件编译。 56 | 57 | 5. 添加 **MASM** 目标后添加生成的 ASM 文件并将其类型设置为 **Microsoft Macro Assembler**。 58 | 59 | 6. 添加生成的 DEF 文件并将其设置为链接器的 **Module Definition File**。 60 | 61 | 7. 引用 `ProxyLibrary.h` 头文件并在 `DllMain` 的 `DLL_PROCESS_ATTACH` 中调用 `ProxyLibrary_Load`,在 `DLL_PROCESS_DETACH` 中调用 `ProxyLibrary_Unload`。 62 | 63 | 8. 编译项目即可生成用于 Hook 的代理 DLL 文件。 64 | 65 | ## 许可证 66 | 67 | 此项目根据 MIT 许可证授权,详见 [LICENSE](LICENSE.md) 文件。 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxy Library Source Code Generator 2 | 3 | 🌍 **[简体中文](README-CN.md) | [English](README.md)** 4 | 5 | A script for generating proxy library source code for dynamic link libraries (DLL), mainly used for hijacking the original dynamic link library or injecting into the target program. 6 | 7 | ## Usage 8 | 9 | ```text 10 | python3 generate_proxy_pe_source.py 11 | ``` 12 | 13 | ### Options List 14 | 15 | - `-f, --original-pe-file` 16 | - Path to the original PE file (required) 17 | 18 | - `-s, --pe-file-path-in-source-file` 19 | - Path to the PE file in the generated source file, used to locate the corresponding PE file on the target system 20 | - Default: same as `-f, --original-pe-file` 21 | 22 | - `-o, --output-directory` 23 | - Output directory for the source code 24 | - Default: current directory 25 | 26 | - `-e, --encoding` 27 | - Encoding for the generated source code 28 | - Default: UTF-8 29 | 30 | - `-h, --help` 31 | - Show help message 32 | 33 | ## Usage Examples 34 | 35 | > For more detailed usage examples, refer to the [WPSProfileVerificationPatch](https://github.com/YukiIsait/WPSProfileVerificationPatch) project. 36 | 37 | 1. Install Python 3.x environment and install dependencies: 38 | 39 | ```powershell 40 | pip3 install -r requirements.txt 41 | ``` 42 | 43 | 2. Generate proxy library source code for a 64-bit `ncrypt.dll`: 44 | 45 | ```powershell 46 | python3 generate_proxy_pe_source.py -f $env:SystemRoot\System32\ncrypt.dll -s %SystemRoot%\System32\ncrypt.dll -o ncrypt64 47 | ``` 48 | 49 | 3. Generate proxy library source code for a 32-bit `ncrypt.dll`: 50 | 51 | ```powershell 52 | python3 generate_proxy_pe_source.py -f $env:SystemRoot\SysWOW64\ncrypt.dll -s %SystemRoot%\System32\ncrypt.dll -o ncrypt32 53 | ``` 54 | 55 | 4. Add the generated source code files to an MSBuild-based DLL project. Since the export definitions of `ncrypt.dll` are the same for different architectures, you can rename the `ProxyLibraryExports.asm` file for 64-bit and 32-bit versions and set conditional compilation. 56 | 57 | 5. Add the **MASM** target, then add the generated ASM file and set its type to **Microsoft Macro Assembler**. 58 | 59 | 6. Add the generated DEF file and set it as the **Module Definition File** for the linker. 60 | 61 | 7. Include the `ProxyLibrary.h` header file and call `ProxyLibrary_Load` in `DllMain` during `DLL_PROCESS_ATTACH`, and call `ProxyLibrary_Unload` during `DLL_PROCESS_DETACH`. 62 | 63 | 8. Compile the project to generate the proxy DLL file for hooking. 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details. 68 | -------------------------------------------------------------------------------- /proxy_pe_source_generator/templates/ProxyLibrary.c: -------------------------------------------------------------------------------- 1 | // Generated by ProxyPESourceGenerator 2 | 3 | #include "ProxyLibrary.h" 4 | #include "ProxyLibraryGlobals.h" 5 | #include 6 | 7 | static HMODULE module = NULL; 8 | 9 | static void PASCAL UnimplementedFunction() { 10 | MessageBoxA(NULL, "Called an unimplemented function!", "Proxy Error", MB_ICONSTOP); 11 | ExitProcess(-3); 12 | } 13 | 14 | static void PASCAL LoadOriginalLibrary(LPCSTR libraryFilePath) { 15 | if (module != NULL) { 16 | return; 17 | } 18 | HANDLE processHeap = GetProcessHeap(); 19 | do { 20 | DWORD expandedLibraryFilePathSize = ExpandEnvironmentStringsA(libraryFilePath, NULL, 0); 21 | if (expandedLibraryFilePathSize == 0) { 22 | break; 23 | } 24 | LPSTR expandedLibraryFilePath = (LPSTR) HeapAlloc(processHeap, 0, expandedLibraryFilePathSize); 25 | if (expandedLibraryFilePath == NULL) { 26 | break; 27 | } 28 | expandedLibraryFilePathSize = ExpandEnvironmentStringsA(libraryFilePath, expandedLibraryFilePath, expandedLibraryFilePathSize); 29 | if (expandedLibraryFilePathSize == 0) { 30 | break; 31 | } 32 | module = LoadLibraryA(expandedLibraryFilePath); 33 | HeapFree(processHeap, 0, expandedLibraryFilePath); 34 | if (module != NULL) { 35 | return; 36 | } 37 | } while (0); 38 | do { 39 | SIZE_T libraryNameLength = 0; 40 | if (FAILED(StringCchLengthA(libraryFilePath, STRSAFE_MAX_CCH, &libraryNameLength))) { 41 | break; 42 | } 43 | LPSTR errorMessage = (LPSTR) HeapAlloc(processHeap, 0, libraryNameLength + 16); 44 | if (errorMessage == NULL) { 45 | break; 46 | } 47 | if (FAILED(StringCchCopyA(errorMessage, 16, "Failed to load "))) { 48 | break; 49 | } 50 | if (FAILED(StringCchCopyA(errorMessage + 15, libraryNameLength + 1, libraryFilePath))) { 51 | break; 52 | } 53 | MessageBoxA(NULL, errorMessage, "Proxy Error", MB_ICONSTOP); 54 | HeapFree(processHeap, 0, errorMessage); 55 | } while (0); 56 | ExitProcess(-2); 57 | } 58 | 59 | static FARPROC PASCAL GetOriginalProcedureAddress(LPCSTR procedureName) { 60 | if (module != NULL) { 61 | FARPROC address = GetProcAddress(module, procedureName); 62 | if (address != NULL) { 63 | return address; 64 | } 65 | } 66 | return (FARPROC) UnimplementedFunction; 67 | } 68 | 69 | void PASCAL ProxyLibrary_Load() { 70 | LoadOriginalLibrary(libraryFilePath); 71 | for (SIZE_T i = 0; i < symbolCount; i++) { 72 | symbolAddressArray[i] = GetOriginalProcedureAddress(symbolNameArray[i]); 73 | } 74 | } 75 | 76 | void PASCAL ProxyLibrary_Unload() { 77 | if (module != NULL) { 78 | FreeLibrary(module); 79 | module = NULL; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Ruff stuff: 171 | .ruff_cache/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | -------------------------------------------------------------------------------- /proxy_pe_source_generator/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import os 3 | import pefile 4 | 5 | 6 | class ProxyPESourceGenerator: 7 | def __init__(self, pe_file_path: str, pe_file_path_in_source_file: Optional[str] = None): 8 | if pe_file_path_in_source_file: 9 | self.__file_path = pe_file_path_in_source_file 10 | else: 11 | self.__file_path = pe_file_path 12 | pe = pefile.PE(pe_file_path) 13 | if pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_AMD64']: 14 | self.__is_amd64 = True 15 | elif pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_I386']: 16 | self.__is_amd64 = False 17 | else: 18 | raise ValueError('Unsupported architecture') 19 | if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'): 20 | raise ValueError('PE file does not have export directory') 21 | if not hasattr(pe.DIRECTORY_ENTRY_EXPORT, 'symbols'): 22 | raise ValueError('PE file does not have any export symbols') 23 | self.__export_symbols = [(symbol.name.decode(), symbol.ordinal) for symbol in pe.DIRECTORY_ENTRY_EXPORT.symbols] 24 | self.__templates_path = os.path.join(os.path.split(os.path.realpath(__file__))[0], 'templates') 25 | 26 | def __copy_template_file(self, file_name: str, target_path: str, encoding: str): 27 | with open(os.path.join(self.__templates_path, file_name), 'r', encoding='utf-8') as template_file: 28 | with open(os.path.join(target_path, file_name), 'w', encoding=encoding) as target_file: 29 | target_file.write(template_file.read()) 30 | 31 | def generate_source(self, source_path: str = '.', encoding: str = 'utf-8') -> str: 32 | if not os.path.exists(source_path): 33 | os.makedirs(source_path) 34 | 35 | # Copy ProxyLibrary.c, ProxyLibrary.h, and ProxyLibraryGlobals.h 36 | self.__copy_template_file('ProxyLibrary.c', source_path, encoding) 37 | self.__copy_template_file('ProxyLibrary.h', source_path, encoding) 38 | self.__copy_template_file('ProxyLibraryGlobals.h', source_path, encoding) 39 | 40 | # Generate ProxyLibraryExports.def 41 | with open(os.path.join(source_path, 'ProxyLibraryExports.def'), 'w', newline='', encoding=encoding) as def_file: 42 | def_file.write('; Generated by ProxyPESourceGenerator\r\n\r\n') 43 | def_file.write(f'LIBRARY "{os.path.splitext(os.path.basename(self.__file_path))[0]}"\r\n') 44 | def_file.write('EXPORTS\r\n') 45 | for symbol in self.__export_symbols: 46 | def_file.write(f'{symbol[0]} @{symbol[1]}\r\n') 47 | 48 | # Generate ProxyLibraryGlobals.c 49 | with open(os.path.join(source_path, 'ProxyLibraryGlobals.c'), 'w', newline='', encoding=encoding) as c_file: 50 | symbol_count = len(self.__export_symbols) 51 | c_file.write('// Generated by ProxyPESourceGenerator\r\n\r\n') 52 | c_file.write('#include "ProxyLibraryGlobals.h"\r\n\r\n') 53 | c_file.write(f'LPCSTR libraryFilePath = "{self.__file_path.replace('\\', '\\\\')}";\r\n') 54 | c_file.write(f'SIZE_T symbolCount = {symbol_count};\r\n') 55 | c_file.write(f'FARPROC symbolAddressArray[{symbol_count}];\r\n') 56 | c_file.write(f'LPCSTR symbolNameArray[{symbol_count}] = {{\r\n') 57 | for symbol in self.__export_symbols: 58 | c_file.write(f' "{symbol[0]}",\r\n') 59 | c_file.write('};\r\n') 60 | 61 | # Generate ProxyLibraryExports.asm 62 | with open(os.path.join(source_path, 'ProxyLibraryExports.asm'), 'w', newline='', encoding=encoding) as asm_file: 63 | asm_file.write('; Generated by ProxyPESourceGenerator\r\n\r\n') 64 | if not self.__is_amd64: 65 | asm_file.write('.model flat, stdcall\r\n\r\n') 66 | asm_file.write('.data\r\n') 67 | asm_file.write(f'extern symbolAddressArray:{"qword" if self.__is_amd64 else "dword"}\r\n\r\n') 68 | asm_file.write('.code\r\n') 69 | for i, symbol in enumerate(self.__export_symbols): 70 | asm_file.write(f'{symbol[0]} proc\r\n') 71 | asm_file.write(f' jmp symbolAddressArray+{i * (8 if self.__is_amd64 else 4)}\r\n') 72 | asm_file.write(f'{symbol[0]} endp\r\n') 73 | asm_file.write('end\r\n') 74 | --------------------------------------------------------------------------------