├── .github └── workflows │ ├── release.old │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── _msbuild.py ├── _msbuild_test.py ├── pyproject.toml ├── src └── dlltracer │ ├── __init__.py │ ├── _dlltracertest.pyx │ ├── _native.pyx │ ├── audit_stub.c │ └── audit_stub.h └── tests └── test_basic.py /.github/workflows/release.old: -------------------------------------------------------------------------------- 1 | # We no longer release from GitHub Actions. 2 | # This file is kept for historical and/or copy-paste reasons 3 | 4 | name: PyPI Release 5 | 6 | on: 7 | push: 8 | tags: 9 | - '*.*.*' 10 | 11 | jobs: 12 | release_sdist: 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install --pre pymsbuild Cython 25 | pip install twine 26 | 27 | - name: Build 28 | run: python -m pymsbuild -d dist sdist 29 | env: 30 | GITHUB_REF: ${{ github.ref }} 31 | 32 | - name: Test 33 | run: python -m pip wheel (gi dist\*.tar.gz) 34 | 35 | - name: Push 36 | run: python -m twine upload dist\*.tar.gz 37 | env: 38 | TWINE_USERNAME: '__token__' 39 | TWINE_PASSWORD: ${{ secrets.pypi }} 40 | 41 | release_wheel: 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | matrix: 45 | os: [windows-latest] 46 | python-version: [3.7, 3.8, 3.9, '3.10', '3.11'] 47 | 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v2 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | 56 | - name: Install dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install --pre pymsbuild Cython 60 | pip install twine 61 | 62 | - name: Build 63 | run: python -m pymsbuild -d dist wheel 64 | env: 65 | GITHUB_REF: ${{ github.ref }} 66 | 67 | - name: Push 68 | run: python -m twine upload dist\*.whl 69 | env: 70 | TWINE_USERNAME: '__token__' 71 | TWINE_PASSWORD: ${{ secrets.pypi }} 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [windows-latest] 15 | python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12-dev'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | pip install --pre pymsbuild Cython 28 | pip install pytest 29 | 30 | - name: Build in place 31 | run: python -m pymsbuild -v 32 | 33 | - name: Test with pytest 34 | run: | 35 | python -m pymsbuild -v -c _msbuild_test.py 36 | pytest 37 | env: 38 | PYTHONPATH: src 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /env/ 4 | __pycache__/ 5 | 6 | *.lib 7 | *.pyd 8 | *.pdb 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dlltracer 2 | 3 | The `dlltracer` tool is an assistive tool for diagnosing import errors in 4 | CPython when they are caused by DLL resolution failures on Windows. 5 | 6 | In general, any DLL load error is reported as an `ImportError` of the top-level 7 | extension module. No more specific information is available for CPython to 8 | display, which can make it difficult to diagnose. 9 | 10 | This tool uses not-quite-documented performance events to report on the 11 | intermediate steps of importing an extension module. These events are 12 | undocumented and unsupported, so the format has been inferred by example and may 13 | change, but until it does it will report on the loads that _actually occur_. 14 | However, because it can't report on loads that _never_ occur, you'll still need 15 | to do some work to diagnose the root cause of the failure. 16 | 17 | The most useful static analysis tool is 18 | [dumpbin](https://docs.microsoft.com/cpp/build/reference/dumpbin-reference), 19 | which is included with Visual Studio. When passed a DLL or PYD file and the 20 | `/imports` option, it will list all dependencies that _should be_ loaded. It 21 | shows them by name, that is, before path resolution occurs. 22 | 23 | `dlltracer` performs dynamic analysis, which shows the DLLs that are loaded at 24 | runtime with their full paths. Combined with understanding the dependency 25 | graph of your module, it is easier to diagnose why the overall import fails. 26 | 27 | 28 | # Install 29 | 30 | ``` 31 | pip install dlltracer 32 | ``` 33 | 34 | Where the `pip` command may be replaced by a more appropriate command for your 35 | environment, such as `python -m pip` or `pip3.9`. 36 | 37 | 38 | # Use 39 | 40 | *Note:* Regardless of how output is collected, this tool *must be run as 41 | Administrator*. Otherwise, starting a trace will fail with a `PermissionError`. 42 | Only one thread may be tracing across your entire machine. Because the state 43 | of traces is not well managed by Windows, this tool will attempt to stop any 44 | other running traces. 45 | 46 | A basic trace that prints messages to standard output is: 47 | 48 | ```python 49 | import dlltracer 50 | import sys 51 | 52 | with dlltracer.Trace(out=sys.stdout): 53 | import module_to_trace 54 | ``` 55 | 56 | The output may look like this (for `import ssl`): 57 | 58 | ``` 59 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\kernel.appcore.dll 60 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\_ssl.pyd 61 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\crypt32.dll 62 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\libcrypto-1_1.dll 63 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\libssl-1_1.dll 64 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\user32.dll 65 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\win32u.dll 66 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\gdi32.dll 67 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\gdi32full.dll 68 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\msvcp_win.dll 69 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\imm32.dll 70 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\_socket.pyd 71 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\select.pyd 72 | ``` 73 | 74 | A failed import may look like this (for `import ssl` but with `libcrypto-1_1.dll` 75 | missing): 76 | 77 | ``` 78 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\kernel.appcore.dll 79 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\_ssl.pyd 80 | LoadLibrary \Device\HarddiskVolume3\Windows\System32\crypt32.dll 81 | LoadLibrary \Device\HarddiskVolume3\Program Files\Python39\DLLs\libssl-1_1.dll 82 | Failed \Device\HarddiskVolume3\Windows\System32\crypt32.dll 83 | Failed \Device\HarddiskVolume3\Program Files\Python39\DLLs\libssl-1_1.dll 84 | Failed \Device\HarddiskVolume3\Program Files\Python39\DLLs\_ssl.pyd 85 | Traceback (most recent call last): 86 | File "C:\Projects\test-script.py", line 28, in 87 | import ssl 88 | File "C:\Program Files\Python39\lib\ssl.py", line 98, in 89 | import _ssl # if we can't import it, let the error propagate 90 | ImportError: DLL load failed while importing _ssl: The specified module could not be found. 91 | ``` 92 | 93 | Notice that the missing DLL is never mentioned, and so human analysis is 94 | necessary to diagnose the root cause. 95 | 96 | ## Write to file 97 | 98 | To write output to a file-like object (anything that can be passed to the 99 | `file=` argument of `print`), pass it as the `out=` argument of `Trace`. 100 | 101 | ```python 102 | import dlltracer 103 | 104 | with open("log.txt", "w") as log: 105 | with dlltracer.Trace(out=log): 106 | import module_to_trace 107 | ``` 108 | 109 | 110 | ## Collect to list 111 | 112 | To collect events to an iterable object, pass `collect=True` to `Trace` and 113 | bind the context manager. The result will be a list containing event objects, 114 | typically `dlltracer.LoadEvent` and `dlltracer.LoadFailedEvent`. 115 | 116 | ```python 117 | import dlltracer 118 | 119 | with dlltracer.Trace(collect=True) as events: 120 | try: 121 | import module_to_trace 122 | except ImportError: 123 | # If we don't handle the error, program will exit before 124 | # we get to inspect the events. 125 | pass 126 | 127 | # Inspect the events after ending the trace 128 | all_loaded = {e.path for e in events if isinstance(e, dlltracer.LoadEvent)} 129 | all_failed = {e.path for e in events if isinstance(e, dlltracer.LoadFailedEvent)} 130 | ``` 131 | 132 | ## Raise audit events 133 | 134 | To raise audit events for DLL loads, pass `audit=True` to `Trace`. The events 135 | raised are `dlltracer.load` and `dlltracer.failed`, and both only include the 136 | path as an argument. 137 | 138 | ```python 139 | import dlltracer 140 | import sys 141 | 142 | def hook(event, args): 143 | if event == "dlltracer.load": 144 | # args = (path,) 145 | print("Loaded", args[0]) 146 | elif event == "dlltracer.failed": 147 | # args = (path,) 148 | print("Failed", args[0]) 149 | 150 | sys.add_audit_hook(hook) 151 | 152 | with dlltracer.Trace(audit=True): 153 | import module_to_trace 154 | ``` 155 | 156 | 157 | ## Additional events 158 | 159 | *Note:* This is mainly intended for development of `dlltracer`. 160 | 161 | Because event formats may change, and additional events may be of interest but 162 | are not yet handled, passing the `debug=True` option to `Trace` enables all 163 | events to be collected, written, or audited. Regular events are suppressed. 164 | 165 | ```python 166 | import dlltracer 167 | import sys 168 | 169 | def hook(event, args): 170 | if event != "dlltracer.debug": 171 | return 172 | 173 | # args schema: 174 | # provider is a UUID representing the event source 175 | # opcode is an int representing the operation 176 | # header is bytes taken directly from the event header 177 | # data is bytes taken directly from the event data 178 | provider, opcode, header, data = args 179 | 180 | sys.add_audit_hook(hook) 181 | 182 | with dlltracer.Trace(debug=True, audit=True, collect=True, out=sys.stderr) as events: 183 | try: 184 | import module_to_trace 185 | except ImportError: 186 | pass 187 | 188 | for e in events: 189 | assert isinstance(e, dlltracer.DebugEvent) 190 | # DebugEvent contains provider, opcode, header and data as for the audit event 191 | ``` 192 | 193 | 194 | # Contribute 195 | 196 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 197 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 198 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 199 | 200 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 201 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 202 | provided by the bot. You will only need to do this once across all repos using our CLA. 203 | 204 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 205 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 206 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 207 | 208 | 209 | # Trademarks 210 | 211 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 212 | trademarks or logos is subject to and must follow 213 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 214 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 215 | Any use of third-party trademarks or logos are subject to those third-party's policies. 216 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /_msbuild.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from pymsbuild import * 4 | from pymsbuild.cython import * 5 | 6 | VERSION = os.getenv("BUILD_BUILDNUMBER", "0.0.1") 7 | 8 | GHREF = os.getenv("GITHUB_REF") 9 | if GHREF: 10 | VERSION = GHREF.rpartition("/")[2] 11 | 12 | METADATA = { 13 | "Metadata-Version": "2.1", 14 | "Name": "dlltracer", 15 | "Version": VERSION, 16 | "Author": "Microsoft Corporation", 17 | "Author-email": "python@microsoft.com", 18 | "Home-page": "https://github.com/microsoft/dlltracer-python", 19 | "Project-url": [ 20 | "Bug Tracker, https://github.com/microsoft/dlltracer-python/issues", 21 | ], 22 | "Summary": "Python module for tracing Windows DLL loads", 23 | "Description": File("README.md"), 24 | "Description-Content-Type": "text/markdown", 25 | "Keywords": "Windows,Win32,DLL", 26 | "Classifier": [ 27 | "Development Status :: 5 - Production/Stable", 28 | "Environment :: Win32 (MS Windows)", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: Microsoft :: Windows", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | ], 39 | "Requires-Python": ">=3.7", 40 | } 41 | 42 | AUDIT_STUB = CSourceFile("dlltracer/audit_stub.c") 43 | 44 | PYD = CythonPydFile( 45 | "_native", 46 | ItemDefinition("ClCompile", 47 | AdditionalIncludeDirectories=ConditionalValue(Path("src;").absolute(), prepend=True) 48 | ), 49 | ItemDefinition("Link", 50 | GenerateDebugInformation=ConditionalValue("false", condition="$(Configuration) == 'Release'")), 51 | PyxFile("dlltracer/_native.pyx", TargetExt=".cpp"), 52 | IncludeFile("dlltracer/audit_stub.h"), 53 | AUDIT_STUB, 54 | ) 55 | 56 | PACKAGE = Package( 57 | "dlltracer", 58 | PyFile("dlltracer/__init__.py"), 59 | PYD, 60 | source="src", 61 | ) 62 | 63 | def init_PACKAGE(wheel_tag): 64 | if wheel_tag and not wheel_tag.startswith("cp37"): 65 | PYD.members.remove(AUDIT_STUB) 66 | -------------------------------------------------------------------------------- /_msbuild_test.py: -------------------------------------------------------------------------------- 1 | from pymsbuild import * 2 | from pymsbuild.cython import * 3 | 4 | METADATA = { 5 | "Name": "dlltracer", 6 | "Version": "0.0", 7 | "ExtSuffix": ".pyd", 8 | } 9 | 10 | PACKAGE = Package( 11 | "dlltracer", 12 | CythonPydFile( 13 | "_dlltracertest", 14 | PyxFile("dlltracer/_dlltracertest.pyx") 15 | ), 16 | source="src", 17 | ) 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["pymsbuild>=1.0.0b6", "Cython"] 3 | build-backend = "pymsbuild" 4 | -------------------------------------------------------------------------------- /src/dlltracer/__init__.py: -------------------------------------------------------------------------------- 1 | from ._native import Trace, LoadEvent, LoadFailedEvent, DebugEvent 2 | 3 | __author__ = "Microsoft Corporation " 4 | -------------------------------------------------------------------------------- /src/dlltracer/_dlltracertest.pyx: -------------------------------------------------------------------------------- 1 | 2 | # cython: language_level=3 3 | 4 | -------------------------------------------------------------------------------- /src/dlltracer/_native.pyx: -------------------------------------------------------------------------------- 1 | 2 | # cython: language_level=3 3 | 4 | from libc.string cimport memcpy, memset 5 | from cpython cimport version as sys_version 6 | from cpython.ref cimport PyObject, Py_INCREF 7 | 8 | from os.path import realpath as _realpath 9 | from threading import Thread as _Thread 10 | from uuid import UUID as _UUID 11 | 12 | _SystemTraceControlGuid = _UUID("9e814aad-3204-11d2-9a82-006008a86939") 13 | _LoadLibraryProvider = _UUID('2cb15d1d-5fc1-11d2-abe1-00a0c911f518') 14 | 15 | cdef extern from "dlltracer/audit_stub.h": 16 | cdef int PySys_Audit(const char* event, const char* fmt, ...) except -1 17 | 18 | 19 | cdef extern from "windows.h" nogil: 20 | ctypedef void* HANDLE 21 | ctypedef void* HMODULE 22 | ctypedef const char* LPCSTR 23 | ctypedef char* LPSTR 24 | ctypedef const Py_UNICODE* LPCWSTR 25 | ctypedef Py_UNICODE* LPWSTR 26 | ctypedef unsigned char UCHAR 27 | ctypedef unsigned int USHORT 28 | ctypedef unsigned int ULONG 29 | ctypedef unsigned long ULONGLONG 30 | ctypedef size_t ULONG_PTR 31 | ctypedef unsigned int NTSTATUS 32 | 33 | ctypedef struct GUID: 34 | pass 35 | 36 | ULONG GetLastError() 37 | 38 | ULONG WNODE_FLAG_TRACED_GUID 39 | ULONG EVENT_TRACE_REAL_TIME_MODE 40 | ULONG EVENT_TRACE_FLAG_IMAGE_LOAD 41 | LPCWSTR KERNEL_LOGGER_NAMEW 42 | 43 | ULONG ERROR_WMI_INSTANCE_NOT_FOUND 44 | 45 | cdef ULONG GetCurrentProcessId() 46 | 47 | 48 | cdef extern from "wmistr.h" nogil: 49 | ctypedef struct WNODE_HEADER: 50 | ULONG BufferSize 51 | GUID Guid 52 | ULONG ClientContext 53 | ULONG Flags 54 | 55 | 56 | cdef extern from "evntrace.h" nogil: 57 | ctypedef void* TRACEHANDLE 58 | ctypedef TRACEHANDLE* PTRACEHANDLE 59 | 60 | TRACEHANDLE INVALID_PROCESSTRACE_HANDLE 61 | 62 | ctypedef struct EVENT_TRACE_PROPERTIES: 63 | WNODE_HEADER Wnode 64 | ULONG BufferSize 65 | ULONG LogFileMode 66 | ULONG FlushTimer 67 | ULONG EnableFlags 68 | ULONG LogFileNameOffset 69 | ULONG LoggerNameOffset 70 | 71 | ctypedef struct EVENT_DESCRIPTOR: 72 | USHORT Id 73 | UCHAR Channel 74 | UCHAR Level 75 | UCHAR Opcode 76 | USHORT Task 77 | ULONGLONG Keyword 78 | 79 | ctypedef struct EVENT_HEADER: 80 | USHORT Size 81 | USHORT HeaderType 82 | ULONG ProcessId 83 | GUID ProviderId 84 | EVENT_DESCRIPTOR EventDescriptor 85 | 86 | ctypedef struct EVENT_RECORD: 87 | EVENT_HEADER EventHeader 88 | ULONG UserDataLength 89 | void *UserData 90 | 91 | ctypedef void (__stdcall *PEVENT_RECORD_CALLBACK)(EVENT_RECORD* EventRecord) except * nogil 92 | ctypedef struct EVENT_TRACE_LOGFILEW: 93 | LPWSTR LoggerName 94 | ULONG ProcessTraceMode 95 | PEVENT_RECORD_CALLBACK EventRecordCallback 96 | 97 | cdef NTSTATUS ControlTraceW(TRACEHANDLE pHandle, LPCWSTR name, EVENT_TRACE_PROPERTIES* pProps, ULONG ControlCode) 98 | ULONG EVENT_TRACE_CONTROL_STOP 99 | 100 | cdef NTSTATUS StartTraceW(PTRACEHANDLE pHandle, LPCWSTR name, EVENT_TRACE_PROPERTIES* pProps) 101 | cdef NTSTATUS StopTraceW(TRACEHANDLE pHandle, LPCWSTR name, EVENT_TRACE_PROPERTIES* pProps) 102 | 103 | cdef TRACEHANDLE OpenTraceW(EVENT_TRACE_LOGFILEW* file) 104 | cdef ULONG ProcessTrace(TRACEHANDLE* harray, ULONG handleCount, void* t1, void* t2) 105 | cdef void CloseTrace(TRACEHANDLE h) 106 | 107 | 108 | cdef extern from "Evntcons.h" nogil: 109 | ULONG PROCESS_TRACE_MODE_REAL_TIME 110 | ULONG PROCESS_TRACE_MODE_EVENT_RECORD 111 | ULONG PROCESS_TRACE_MODE_RAW_TIMESTAMP 112 | 113 | 114 | cdef ULONG _pid = GetCurrentProcessId() 115 | cdef object _collect = None 116 | cdef object _out = None 117 | cdef int _debug = 0 118 | cdef int _audit = 0 119 | 120 | 121 | class LoadEvent: 122 | def __init__(self, path): self.path = path 123 | def __repr__(self): return f"" 124 | def __str__(self): 125 | path = self.path 126 | if path.lower().startswith("\\device\\"): 127 | try: 128 | path = _realpath("\\\\." + path[7:]) 129 | except OSError: 130 | pass 131 | return f"LoadLibrary {path}" 132 | 133 | 134 | class LoadFailedEvent: 135 | def __init__(self, path): self.path = path 136 | def __repr__(self): return f"" 137 | def __str__(self): 138 | path = self.path 139 | if path.lower().startswith("\\device\\"): 140 | try: 141 | path = _realpath("\\\\." + path[7:]) 142 | except OSError: 143 | pass 144 | return f"Failed {self.path}" 145 | 146 | 147 | class DebugEvent: 148 | # Use this later on for separating up bytes in the str view 149 | _sep = [" ", " ", " ", " "] * 7 + [" ", " ", " ", "\n "] 150 | # Some likely-looking path indices so we can render as string 151 | _opcode_path_index = { 152 | 2: 0x38, 153 | 3: 0x38, 154 | 4: 0x38, 155 | 10: 0x14, 156 | 11: 0x28, 157 | 12: 0x48, 158 | 13: 0x14, 159 | 15: 0x0C, 160 | 21: 0x14, 161 | 22: 0x20, 162 | 25: 0, 163 | 34: 0, 164 | 35: 0x0C, 165 | } 166 | 167 | def __init__(self, provider, opcode, header, data): 168 | self.provider = provider 169 | self.opcode = opcode 170 | self.header = bytes(header) 171 | self.data = bytes(data) 172 | 173 | def __repr__(self): 174 | return f"" 175 | 176 | def __str__(self): 177 | try: 178 | i = self._opcode_path_index[self.opcode] 179 | path = (self.data[i:]).decode("utf-16-le").rstrip(" \0") 180 | data = self.data[:i] 181 | except KeyError: 182 | path = "" 183 | data = self.data 184 | except Exception as ex: 185 | path = repr(ex) 186 | data = self.data 187 | return ( 188 | f"{self.provider}: {self.opcode}" + 189 | "\nHeader = " + 190 | "".join(f"{c:02X}{self._sep[i%len(self._sep)]}" for i, c in enumerate(self.header)).rstrip() + 191 | "\nData = " + 192 | "".join(f"{c:02X}{self._sep[i%len(self._sep)]}" for i, c in enumerate(data)).rstrip() + 193 | "\nPath = " + path 194 | ) 195 | 196 | 197 | cdef int _check(ULONG r, str msg) except -1 nogil: 198 | if r == 0: 199 | return 0 200 | with gil: 201 | raise OSError(None, f"{msg} (0x{r:08X})", None, r & 0xFFFFFFFF) 202 | 203 | 204 | cdef void __stdcall _event_record_callback(EVENT_RECORD* EventRecord) noexcept nogil: 205 | cdef EVENT_HEADER* hdr = &EventRecord.EventHeader 206 | cdef unsigned char* b = EventRecord.UserData 207 | cdef Py_ssize_t cb = EventRecord.UserDataLength 208 | cdef int is_loadlibrary = 0 209 | if hdr.ProcessId != _pid: 210 | return 211 | with gil: 212 | u = _UUID(bytes_le=(&hdr.ProviderId)[0:sizeof(GUID)]) 213 | if u == _LoadLibraryProvider: 214 | is_loadlibrary = 1 215 | if _debug: 216 | with gil: 217 | b1 = bytes((&hdr.EventDescriptor)[0:sizeof(EVENT_DESCRIPTOR)]) 218 | b2 = bytes(b[0:cb]) 219 | if _collect is not None: 220 | _collect.append(DebugEvent(u, hdr.EventDescriptor.Opcode, b1, b2)) 221 | if _out: 222 | print(DebugEvent(u, hdr.EventDescriptor.Opcode, b1, b2), file=_out) 223 | if _audit: 224 | PySys_Audit("dlltracer.debug", "OiOO", 225 | u, hdr.EventDescriptor.Opcode, 226 | b1, b2) 227 | return 228 | 229 | if is_loadlibrary and hdr.EventDescriptor.Opcode == 10: 230 | if cb > 56: 231 | with gil: 232 | s = (b[56:cb]).decode("utf-16-le").rstrip("\0 ") 233 | if _collect is not None: 234 | _collect.append(LoadEvent(s)) 235 | if _out: 236 | print(LoadEvent(s), file=_out, flush=True) 237 | if _audit: 238 | PySys_Audit("dlltracer.load", "O", s) 239 | elif is_loadlibrary and hdr.EventDescriptor.Opcode == 2: 240 | if cb > 56: 241 | with gil: 242 | s = (b[56:cb]).decode("utf-16-le").rstrip("\0 ") 243 | if _collect is not None: 244 | _collect.append(LoadEvent(s)) 245 | if _out: 246 | print(LoadFailedEvent(s), file=_out, flush=True) 247 | if _audit: 248 | PySys_Audit("dlltracer.failed", "O", s) 249 | 250 | 251 | cdef class Trace: 252 | cdef bytearray _props, _lf 253 | cdef TRACEHANDLE _h_write 254 | cdef TRACEHANDLE _h_read 255 | cdef object _thread 256 | cdef object _out 257 | cdef object _collect 258 | cdef bint _audit, _debug 259 | 260 | def __cinit__(self): 261 | self._h_write = NULL 262 | self._h_read = NULL 263 | 264 | def __init__(self, bint collect=False, out=None, bint audit=False, bint debug=False): 265 | cdef EVENT_TRACE_PROPERTIES *props 266 | cdef EVENT_TRACE_LOGFILEW* lf 267 | 268 | self._thread = None 269 | 270 | self._props = b = bytearray(sizeof(EVENT_TRACE_PROPERTIES) + 512) 271 | props = self._props 272 | props.Wnode.BufferSize = len(b) 273 | gb = _SystemTraceControlGuid.bytes_le 274 | memcpy(&props.Wnode.Guid, gb, sizeof(GUID)) 275 | props.Wnode.Flags = WNODE_FLAG_TRACED_GUID 276 | props.BufferSize = 1024 277 | props.LogFileMode = EVENT_TRACE_REAL_TIME_MODE 278 | props.FlushTimer = 1 279 | props.EnableFlags = EVENT_TRACE_FLAG_IMAGE_LOAD 280 | props.LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES) 281 | 282 | self._lf = bytearray(sizeof(EVENT_TRACE_LOGFILEW)) 283 | lf = self._lf 284 | lf.LoggerName = KERNEL_LOGGER_NAMEW 285 | lf.ProcessTraceMode = ( 286 | PROCESS_TRACE_MODE_REAL_TIME 287 | | PROCESS_TRACE_MODE_EVENT_RECORD 288 | | PROCESS_TRACE_MODE_RAW_TIMESTAMP 289 | ) 290 | lf.EventRecordCallback = _event_record_callback 291 | 292 | self._collect = [] if collect else None 293 | self._out = out 294 | self._audit = audit 295 | if sys_version.PY_VERSION_HEX < 0x03080000 and audit: 296 | raise NotImplementedError("audit hooks are not available on this version of Python") 297 | self._debug = debug 298 | 299 | def __enter__(self): 300 | self.start() 301 | return self._collect 302 | 303 | def __dealloc__(self): 304 | self.close() 305 | 306 | def __exit__(self, *ex_info): 307 | self.close() 308 | 309 | def start(self): 310 | cdef EVENT_TRACE_PROPERTIES *props 311 | cdef EVENT_TRACE_LOGFILEW* lf 312 | cdef ULONG err = 0 313 | if self._thread: 314 | return 315 | props = self._props 316 | lf = self._lf 317 | with nogil: 318 | err = ControlTraceW(NULL, KERNEL_LOGGER_NAMEW, props, EVENT_TRACE_CONTROL_STOP) 319 | if err and err != ERROR_WMI_INSTANCE_NOT_FOUND: 320 | _check(err, "failed to stop existing trace") 321 | _check( 322 | StartTraceW(&self._h_write, KERNEL_LOGGER_NAMEW, props), 323 | "failed to start trace" 324 | ) 325 | self._h_read = OpenTraceW(lf) 326 | if self._h_read == INVALID_PROCESSTRACE_HANDLE: 327 | err = GetLastError() 328 | self._h_read = NULL 329 | else: 330 | err = 0 331 | try: 332 | _check(err, "failed to start reading trace") 333 | except: 334 | self.close() 335 | raise 336 | 337 | self._thread = _Thread(target=_process_thread, args=(self,)) 338 | self._thread.start() 339 | 340 | def close(self): 341 | if not self._thread or not self._h_write or not self._h_read: 342 | return 343 | cdef EVENT_TRACE_PROPERTIES *props = self._props 344 | with nogil: 345 | _check( 346 | StopTraceW(self._h_write, KERNEL_LOGGER_NAMEW, props), 347 | "failed to stop collecting trace" 348 | ) 349 | self._h_write = NULL 350 | CloseTrace(self._h_read) 351 | self._h_read = NULL 352 | self._thread.join() 353 | self._thread = None 354 | 355 | 356 | cdef object _process_thread(Trace owner): 357 | global _collect, _out, _audit, _debug 358 | 359 | cdef ULONG err = 0 360 | cdef TRACEHANDLE h = owner._h_read 361 | 362 | _collect = owner._collect 363 | _out = owner._out 364 | _audit = owner._audit 365 | _debug = owner._debug 366 | 367 | with nogil: 368 | err = ProcessTrace(&h, 1, NULL, NULL) 369 | -------------------------------------------------------------------------------- /src/dlltracer/audit_stub.c: -------------------------------------------------------------------------------- 1 | 2 | int __stdcall PySys_Audit(const char* event, const char* fmt, ...) { 3 | return 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/dlltracer/audit_stub.h: -------------------------------------------------------------------------------- 1 | 2 | #ifdef __cplusplus 3 | extern "C" { 4 | #endif 5 | 6 | #if PY_VERSION_HEX < 0x03080000 7 | int __stdcall PySys_Audit(const char* event, const char* fmt, ...); 8 | #endif 9 | 10 | #ifdef __cplusplus 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import dlltracer 2 | import io 3 | import pathlib 4 | import pytest 5 | import sys 6 | 7 | 8 | def _runner(fn, conn): 9 | try: 10 | conn.send(fn()) 11 | finally: 12 | conn.close() 13 | 14 | 15 | def run(fn): 16 | from multiprocessing import Pipe, Process 17 | 18 | parent_conn, child_conn = Pipe() 19 | p = Process(target=_runner, args=(fn, child_conn,)) 20 | p.start() 21 | try: 22 | return parent_conn.recv() 23 | finally: 24 | p.join() 25 | p.close() 26 | 27 | 28 | def do_test_out(): 29 | assert "dlltracer._dlltracertest" not in sys.modules 30 | buffer = io.StringIO() 31 | with dlltracer.Trace(out=buffer): 32 | from dlltracer import _dlltracertest 33 | 34 | return buffer.getvalue() 35 | 36 | 37 | def test_out(): 38 | assert "_dlltracertest.pyd" in run(do_test_out) 39 | 40 | 41 | def do_test_collect(): 42 | assert "dlltracer._dlltracertest" not in sys.modules 43 | with dlltracer.Trace(collect=True) as events: 44 | from dlltracer import _dlltracertest 45 | 46 | return events 47 | 48 | 49 | def test_collect(): 50 | events = run(do_test_collect) 51 | assert events 52 | names = set() 53 | for e in events: 54 | assert isinstance(e, dlltracer.LoadEvent) 55 | assert e.path 56 | assert repr(e) 57 | assert str(e) 58 | names.add(pathlib.PurePath(e.path).stem.casefold()) 59 | assert "_dlltracertest" in names 60 | 61 | 62 | def do_test_audit(): 63 | # TODO: Collect and verify the hooked events 64 | # For now, we simply ensure that we do not crash 65 | with dlltracer.Trace(audit=True): 66 | from dlltracer import _dlltracertest 67 | 68 | 69 | @pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Requires Python 3.8 or later") 70 | def test_audit(): 71 | run(do_test_audit) 72 | 73 | 74 | def do_test_audit_failure(): 75 | def hook(event, args): 76 | if event.startswith("dlltracer"): 77 | raise RuntimeError("forced abort") 78 | sys.addaudithook(hook) 79 | 80 | with dlltracer.Trace(audit=True): 81 | from dlltracer import _dlltracertest 82 | 83 | 84 | @pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Requires Python 3.8 or later") 85 | def test_audit_failure(): 86 | run(do_test_audit_failure) 87 | 88 | 89 | def do_test_debug(): 90 | with dlltracer.Trace(debug=True, collect=True) as events: 91 | from dlltracer import _dlltracertest 92 | 93 | return events 94 | 95 | 96 | def test_debug(): 97 | events = run(do_test_debug) 98 | assert events 99 | for e in events: 100 | assert isinstance(e, dlltracer.DebugEvent) 101 | assert e.provider 102 | assert e.opcode 103 | assert e.header is not None 104 | assert e.data is not None 105 | assert repr(e) 106 | assert str(e) 107 | --------------------------------------------------------------------------------