├── .github └── workflows │ ├── check.yml │ └── pyinstaller.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── unlicense.ico ├── poetry.lock ├── pyproject.toml ├── unlicense.spec └── unlicense ├── __init__.py ├── __main__.py ├── application.py ├── dump_utils.py ├── emulation.py ├── frida_exec.py ├── function_hashing.py ├── imports.py ├── lief_utils.py ├── logger.py ├── process_control.py ├── resources ├── __init__.py └── frida.js ├── version_detection.py ├── winlicense2.py └── winlicense3.py /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Python Check 2 | 3 | on: [push] 4 | 5 | jobs: 6 | check: 7 | name: Check Python ${{ matrix.python_version }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python_version: ["3.9", "3.10", "3.11"] 12 | runs-on: windows-latest 13 | 14 | defaults: 15 | run: 16 | shell: bash 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 1 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python_version }} 27 | architecture: x64 28 | 29 | - name: Install Poetry 30 | uses: snok/install-poetry@v1 31 | with: 32 | virtualenvs-create: true 33 | virtualenvs-in-project: true 34 | 35 | - name: Install Dependencies 36 | run: poetry install 37 | 38 | - name: Run yapf 39 | run: poetry run yapf -r -d unlicense 40 | 41 | - name: Run mypy 42 | run: poetry run mypy --strict unlicense 43 | -------------------------------------------------------------------------------- /.github/workflows/pyinstaller.yml: -------------------------------------------------------------------------------- 1 | name: PyInstaller Check 2 | 3 | on: [push] 4 | 5 | jobs: 6 | check: 7 | name: Check PyInstaller for Python ${{ matrix.python_version }} (${{ matrix.architecture }}) 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python_version: ["3.11"] 12 | architecture: [x64, x86] 13 | runs-on: windows-latest 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 1 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python_version }} 28 | architecture: ${{ matrix.architecture }} 29 | 30 | - name: Install Poetry 31 | uses: snok/install-poetry@v1 32 | with: 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | 36 | - name: Install Dependencies 37 | run: poetry install 38 | 39 | - name: Build PyInstaller package 40 | run: poetry run pyinstaller unlicense.spec 41 | 42 | - name: "Upload PyInstaller Artifact" 43 | uses: actions/upload-artifact@v3 44 | with: 45 | name: unlicense-py${{ matrix.python_version }}-${{ matrix.architecture }} 46 | path: dist/*.exe 47 | retention-days: 3 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Packages 4 | *.egg 5 | !/tests/**/*.egg 6 | /*.egg-info 7 | /dist/* 8 | build 9 | _build 10 | .cache 11 | *.so 12 | 13 | # Installer logs 14 | pip-log.txt 15 | 16 | # Unit test / coverage reports 17 | .coverage 18 | .tox 19 | .pytest_cache 20 | 21 | .DS_Store 22 | .idea/* 23 | .python-version 24 | .vscode/* 25 | 26 | /test.py 27 | /test_*.* 28 | 29 | /setup.cfg 30 | MANIFEST.in 31 | /setup.py 32 | /docs/site/* 33 | /tests/fixtures/simple_project/setup.py 34 | /tests/fixtures/project_with_extras/setup.py 35 | .mypy_cache 36 | 37 | .venv 38 | /releases/* 39 | pip-wheel-metadata 40 | 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.4.0] - 2023-08-14 6 | ### Added 7 | - Add a `--no_imports` option that allows dumping PEs at the original entry point without fixing imports 8 | 9 | ### Fixed 10 | - Fix a potential deadlock when dumping DLLs 11 | - Improve version detection for Themida/Winlicense 2.x 12 | - Improve version detection for Themida/Winlicense 3.x 13 | - Improve .text section detection for Themida/Winlicense 3.x 14 | - Fix `lief.not_found` exception happening when dumping certain MinGW EXEs 15 | - Fix TLS callback detection for some 32-bit EXEs 16 | - Handle wrapped imports from Themida/Winlicense 3.1.4.0 17 | - Improve IAT search algorithm for Themida/Winlicense 3.x 18 | - Allow unpacking EXEs that require admin privilege at medium integrity level 19 | - Properly skip DllMain invocations on thread creation/deletion when dumping DLLs 20 | 21 | ### Changed 22 | - Silence some misleading "error" logs that were emitted 23 | 24 | ## [0.3.0] - 2022-07-22 25 | ### Fixed 26 | - Fix a couple of bugs with the IAT search and resolution for Themida/Winlicense 3.x 27 | - Fix potentially invalid IAT truncations for Themida/WinLicense 3.x 28 | - OEP detection now works in a runtime-agnostic manner (and handles virtualized entry points and Delphi executables) 29 | - TLS callbacks are now properly detected and skipped 30 | 31 | ## [0.2.0] - 2022-05-31 32 | ### Added 33 | - Handle unpacking of 32-bit and 64-bit DLLs 34 | - Handle unpacking of 32-bit and 64-bit .NET assembly PEs (EXE only) 35 | - OEP detection times out after 10 seconds by default. The duration can be 36 | changed through the CLI. 37 | 38 | ### Fixed 39 | - Improve .text section detection for Themida/Winlicense 2.x 40 | 41 | ## [0.1.1] - 2022-04-06 42 | ### Fixed 43 | - Fix IAT patching in some cases for Themida/Winlicense 3.x 44 | - Fix inability to read remote chunks of memory bigger than 128 MiB 45 | - Improve version detection to handle packed Delphi executables 46 | - Improve IAT search algorithm for Themida/Winlicense 3.x 47 | - Gracefully handle bitness mismatch between interpreter and target PEs 48 | - Fix IAT truncation issue for IATs bigger than 4 KiB 49 | 50 | ## [0.1.0] - 2021-11-13 51 | 52 | Initial release with support for Themida/Winlicense 2.x and 3.x. 53 | This release has been tested on Themida 2.4 and 3.0. 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | Unlicense 635 | Copyright (C) 2021 Erwan Grelet 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Unlicense Copyright (C) 2021 Erwan Grelet 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unlicense 2 | 3 | [![GitHub release](https://img.shields.io/github/release/ergrelet/unlicense.svg)](https://github.com/ergrelet/unlicense/releases) [![Minimum Python version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) ![CI status](https://github.com/ergrelet/unlicense/actions/workflows/check.yml/badge.svg?branch=main) 4 | 5 | A Python 3 tool to dynamically unpack executables protected with 6 | Themida/WinLicense 2.x and 3.x. 7 | 8 | Warning: This tool will execute the target executable. Make sure to use this 9 | tool in a VM if you're unsure about what the target executable does. 10 | 11 | Note: You need to use a 32-bit Python interpreter to dump 32-bit executables. 12 | 13 | ## Features 14 | 15 | * Handles Themida/Winlicense 2.x and 3.x 16 | * Handles 32-bit and 64-bit PEs (EXEs and DLLs) 17 | * Handles 32-bit and 64-bit .NET assemblies (EXEs only) 18 | * Recovers the original entry point (OEP) automatically 19 | * Recovers the (obfuscated) import table automatically 20 | 21 | ## Known Limitations 22 | 23 | * Doesn't handle .NET assembly DLLs 24 | * Doesn't produce runnable dumps in most cases 25 | * Resolving imports for 32-bit executables packed with Themida 2.x is pretty slow 26 | * Requires a valid license file to unpack WinLicense-protected executables that 27 | require license files to start 28 | 29 | ## How To 30 | 31 | ### Download 32 | 33 | You can either download the PyInstaller-generated executables from the "Releases" 34 | section or fetch the project with `git` and install it with `pip`: 35 | ``` 36 | pip install git+https://github.com/ergrelet/unlicense.git 37 | ``` 38 | 39 | ### Use 40 | 41 | If you don't want to deal the command-line interface (CLI) you can simply 42 | drag-and-drop the target binary on the appropriate (32-bit or 64-bit) `unlicense` 43 | executable (which is available in the "Releases" section). 44 | 45 | Otherwise here's what the CLI looks like: 46 | ``` 47 | unlicense --help 48 | NAME 49 | unlicense.exe - Unpack executables protected with Themida/WinLicense 2.x and 3.x 50 | 51 | SYNOPSIS 52 | unlicense.exe PE_TO_DUMP 53 | 54 | DESCRIPTION 55 | Unpack executables protected with Themida/WinLicense 2.x and 3.x 56 | 57 | POSITIONAL ARGUMENTS 58 | PE_TO_DUMP 59 | Type: str 60 | 61 | FLAGS 62 | --verbose=VERBOSE 63 | Type: bool 64 | Default: False 65 | --pause_on_oep=PAUSE_ON_OEP 66 | Type: bool 67 | Default: False 68 | --no_imports=NO_IMPORTS 69 | Type: bool 70 | Default: False 71 | --force_oep=FORCE_OEP 72 | Type: Optional[Optional] 73 | Default: None 74 | --target_version=TARGET_VERSION 75 | Type: Optional[Optional] 76 | Default: None 77 | --timeout=TIMEOUT 78 | Type: int 79 | Default: 10 80 | 81 | NOTES 82 | You can also use flags syntax for POSITIONAL ARGUMENTS 83 | ``` 84 | -------------------------------------------------------------------------------- /assets/unlicense.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ergrelet/unlicense/95c8dc6a4324ff70c04bceda8bd166be6c5f0c73/assets/unlicense.ico -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "altgraph" 5 | version = "0.17.3" 6 | description = "Python graph (network) package" 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"}, 11 | {file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"}, 12 | ] 13 | 14 | [[package]] 15 | name = "astroid" 16 | version = "2.15.6" 17 | description = "An abstract syntax tree for Python with inference support." 18 | optional = false 19 | python-versions = ">=3.7.2" 20 | files = [ 21 | {file = "astroid-2.15.6-py3-none-any.whl", hash = "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c"}, 22 | {file = "astroid-2.15.6.tar.gz", hash = "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"}, 23 | ] 24 | 25 | [package.dependencies] 26 | lazy-object-proxy = ">=1.4.0" 27 | typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} 28 | wrapt = [ 29 | {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, 30 | {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, 31 | ] 32 | 33 | [[package]] 34 | name = "capstone" 35 | version = "4.0.2" 36 | description = "Capstone disassembly engine" 37 | optional = false 38 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 39 | files = [ 40 | {file = "capstone-4.0.2-py2.py3-none-manylinux1_i686.whl", hash = "sha256:da442f979414cf27e4621e70e835880878c858ea438c4f0e957e132593579e37"}, 41 | {file = "capstone-4.0.2-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:9d1a9096c5f875b11290317722ed44bb6e7c52e50cc79d791f142bce968c49aa"}, 42 | {file = "capstone-4.0.2-py2.py3-none-win32.whl", hash = "sha256:c3d9b443d1adb40ee2d9a4e7341169b76476ddcf3a54c03793b16cdc7cd35c5a"}, 43 | {file = "capstone-4.0.2-py2.py3-none-win_amd64.whl", hash = "sha256:0d65ffe8620920976ceadedc769f22318f6f150a592368d8a735612367ac8a1a"}, 44 | {file = "capstone-4.0.2.tar.gz", hash = "sha256:2842913092c9b69fd903744bc1b87488e1451625460baac173056e1808ec1c66"}, 45 | ] 46 | 47 | [[package]] 48 | name = "colorama" 49 | version = "0.4.6" 50 | description = "Cross-platform colored terminal text." 51 | optional = false 52 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 53 | files = [ 54 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 55 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 56 | ] 57 | 58 | [[package]] 59 | name = "dill" 60 | version = "0.3.6" 61 | description = "serialize all of python" 62 | optional = false 63 | python-versions = ">=3.7" 64 | files = [ 65 | {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, 66 | {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, 67 | ] 68 | 69 | [package.extras] 70 | graph = ["objgraph (>=1.7.2)"] 71 | 72 | [[package]] 73 | name = "fire" 74 | version = "0.4.0" 75 | description = "A library for automatically generating command line interfaces." 76 | optional = false 77 | python-versions = "*" 78 | files = [ 79 | {file = "fire-0.4.0.tar.gz", hash = "sha256:c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"}, 80 | ] 81 | 82 | [package.dependencies] 83 | six = "*" 84 | termcolor = "*" 85 | 86 | [[package]] 87 | name = "frida" 88 | version = "16.1.3" 89 | description = "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers" 90 | optional = false 91 | python-versions = ">=3.7" 92 | files = [ 93 | {file = "frida-16.1.3-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ea1412e8f22d035fd709abfe8cbd98f6ca3b57c15505e88d290752288ea59814"}, 94 | {file = "frida-16.1.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a25d1593b5bda6688c63159636a1c124ebc2cc5ae28bfe2ebe7f12bb2f7ee71"}, 95 | {file = "frida-16.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d0b9f14f79c0d8e3235edf232670177a5256ad197c8b9f563ac50dfb4bdb8aa"}, 96 | {file = "frida-16.1.3-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aeea0c0d43ce8d5f64afeec6f364463cebb16cfbcd6a4a8c6465997e74d0aacd"}, 97 | {file = "frida-16.1.3-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7de8ecf12db41e201a5ed99c9946a600c5607dd56ca95fb37c4ec029f476c9a2"}, 98 | {file = "frida-16.1.3-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:52e40b5af28f10119903c94bc9953153b4367e5dca965c7cacfcdd128e5d3e7c"}, 99 | {file = "frida-16.1.3-cp37-abi3-win32.whl", hash = "sha256:ddf9aa85b471f850edf2e2c6b9f53c50942dee2025c934687b72bde0a7983422"}, 100 | {file = "frida-16.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:2ec0a3b3c6011872ab243f84c29ba4b7d28da5fc0a9d563b6ee4958392b6a853"}, 101 | {file = "frida-16.1.3.tar.gz", hash = "sha256:859a099f3e671259b86172268ccb74d84fc9b0758956fbe8a1f22d77cfd598a8"}, 102 | ] 103 | 104 | [package.dependencies] 105 | typing-extensions = {version = "*", markers = "python_version < \"3.11\""} 106 | 107 | [[package]] 108 | name = "isort" 109 | version = "5.12.0" 110 | description = "A Python utility / library to sort Python imports." 111 | optional = false 112 | python-versions = ">=3.8.0" 113 | files = [ 114 | {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, 115 | {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, 116 | ] 117 | 118 | [package.extras] 119 | colors = ["colorama (>=0.4.3)"] 120 | pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] 121 | plugins = ["setuptools"] 122 | requirements-deprecated-finder = ["pip-api", "pipreqs"] 123 | 124 | [[package]] 125 | name = "lazy-object-proxy" 126 | version = "1.9.0" 127 | description = "A fast and thorough lazy object proxy." 128 | optional = false 129 | python-versions = ">=3.7" 130 | files = [ 131 | {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, 132 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, 133 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, 134 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, 135 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, 136 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, 137 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, 138 | {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, 139 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, 140 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, 141 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, 142 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, 143 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, 144 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, 145 | {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, 146 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, 147 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, 148 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, 149 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, 150 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, 151 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, 152 | {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, 153 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, 154 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, 155 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, 156 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, 157 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, 158 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, 159 | {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, 160 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, 161 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, 162 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, 163 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, 164 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, 165 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, 166 | {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, 167 | ] 168 | 169 | [[package]] 170 | name = "lief" 171 | version = "0.13.2" 172 | description = "Library to instrument executable formats" 173 | optional = false 174 | python-versions = ">=3.8" 175 | files = [ 176 | {file = "lief-0.13.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:0390cfaaf0e9aed46bebf26f00f34852768f76bc7f90abf7ceb384566200e5f5"}, 177 | {file = "lief-0.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5581bf0072c1e7a9ea2fb2e2252b8582016e8b298804b5461e552b402c9cd4e9"}, 178 | {file = "lief-0.13.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:dbbf2fb3d7807e815f345c77e287da162e081100f059ec03005995befc295d7f"}, 179 | {file = "lief-0.13.2-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:d344d37334c2b488dc02f04cb13c22cd61aa065eeb9bca7424588e0c8c23bdfb"}, 180 | {file = "lief-0.13.2-cp310-cp310-win32.whl", hash = "sha256:bc041b28b94139843a33c014e355822a9276b35f3c5ae10d82da56bf572f8222"}, 181 | {file = "lief-0.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:01d4075bbc3541e9dd3ef008045fa1eb128294a0c5b0c1f69ce60d8948d248c7"}, 182 | {file = "lief-0.13.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6570dacebe107ad60c2ba0968d1a865d316009d43cc85af3719d3eeb0911abf3"}, 183 | {file = "lief-0.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ce2e3f7c791efba327c2bb3499dbef81e682027109045a9bae696c62e2aeeb0"}, 184 | {file = "lief-0.13.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:11ab900e0644b6735ecdef2bbd04439b4866a527650fc054470c195d6cfe2917"}, 185 | {file = "lief-0.13.2-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:042ad2105a136b11a7494b9af8178468e8cb32b8fa2a0a55cb659a5605aeb069"}, 186 | {file = "lief-0.13.2-cp311-cp311-win32.whl", hash = "sha256:1ce289b6ab3cf4be654270007e8a2c0d2e42116180418c29d3ce83762955de63"}, 187 | {file = "lief-0.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:eccb248ffb598e410fd2ef7c1f171a3cde57a40c9bb8c4fa15d8e7b90eb4eb2d"}, 188 | {file = "lief-0.13.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:95731cadedd6ffc5fb48c147fcefe004624e436b75e8ee9fb2dbf2ae5f084342"}, 189 | {file = "lief-0.13.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8da75df0ea472557fcc37a27ba583bad5a8f3a256c186600d00a6dd0a57f718a"}, 190 | {file = "lief-0.13.2-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b99092f02c13f580c2d00b504af224b7e60e7c98a791e72ae8519f530b7687bb"}, 191 | {file = "lief-0.13.2-cp38-cp38-win32.whl", hash = "sha256:03db0138e4dbbdfa8bba74de312b0cebb30f504e44f38a9c8918b84022da340b"}, 192 | {file = "lief-0.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:36c5bea3f8460dee3ebb75d35949f445638ec85d2871f31e293c47fb4a0a5af7"}, 193 | {file = "lief-0.13.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:eca8ecbcae1ad851ed7cf1e22ec8accd74f2267fa7375194559fb917523d8a92"}, 194 | {file = "lief-0.13.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8703cb5308b4828563badc6885ff07a3926ec3403d1caa3aa75f24fe9cbcf84"}, 195 | {file = "lief-0.13.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c60f2f79e7d0d1f18dec7dcdb4d4f35e6b126ac29e2f2f056d28ec50599d868a"}, 196 | {file = "lief-0.13.2-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:e0f84a7443b7f1b02666fd16a9aa57f5d9027e60ba2885e0d76db8426d689707"}, 197 | {file = "lief-0.13.2-cp39-cp39-win32.whl", hash = "sha256:3f8f251de874929d9c9e94a35891621ab8c059149f8a1c24e543fd9cf0c2a31c"}, 198 | {file = "lief-0.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:2bbe294385e629aa7206b2f39f0ca34e3948605a8db50b22091603053889a759"}, 199 | ] 200 | 201 | [[package]] 202 | name = "macholib" 203 | version = "1.16.2" 204 | description = "Mach-O header analysis and editing" 205 | optional = false 206 | python-versions = "*" 207 | files = [ 208 | {file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"}, 209 | {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, 210 | ] 211 | 212 | [package.dependencies] 213 | altgraph = ">=0.17" 214 | 215 | [[package]] 216 | name = "mccabe" 217 | version = "0.7.0" 218 | description = "McCabe checker, plugin for flake8" 219 | optional = false 220 | python-versions = ">=3.6" 221 | files = [ 222 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 223 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 224 | ] 225 | 226 | [[package]] 227 | name = "mypy" 228 | version = "0.910" 229 | description = "Optional static typing for Python" 230 | optional = false 231 | python-versions = ">=3.5" 232 | files = [ 233 | {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, 234 | {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, 235 | {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, 236 | {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, 237 | {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, 238 | {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, 239 | {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, 240 | {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, 241 | {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, 242 | {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, 243 | {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, 244 | {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, 245 | {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, 246 | {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, 247 | {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, 248 | {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, 249 | {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, 250 | {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, 251 | {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, 252 | {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, 253 | {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, 254 | {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, 255 | {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, 256 | ] 257 | 258 | [package.dependencies] 259 | mypy-extensions = ">=0.4.3,<0.5.0" 260 | toml = "*" 261 | typing-extensions = ">=3.7.4" 262 | 263 | [package.extras] 264 | dmypy = ["psutil (>=4.0)"] 265 | python2 = ["typed-ast (>=1.4.0,<1.5.0)"] 266 | 267 | [[package]] 268 | name = "mypy-extensions" 269 | version = "0.4.4" 270 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 271 | optional = false 272 | python-versions = ">=2.7" 273 | files = [ 274 | {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, 275 | ] 276 | 277 | [[package]] 278 | name = "pefile" 279 | version = "2023.2.7" 280 | description = "Python PE parsing module" 281 | optional = false 282 | python-versions = ">=3.6.0" 283 | files = [ 284 | {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, 285 | {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, 286 | ] 287 | 288 | [[package]] 289 | name = "platformdirs" 290 | version = "3.9.1" 291 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 292 | optional = false 293 | python-versions = ">=3.7" 294 | files = [ 295 | {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, 296 | {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, 297 | ] 298 | 299 | [package.extras] 300 | docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 301 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] 302 | 303 | [[package]] 304 | name = "pyinstaller" 305 | version = "5.13.0" 306 | description = "PyInstaller bundles a Python application and all its dependencies into a single package." 307 | optional = false 308 | python-versions = "<3.13,>=3.7" 309 | files = [ 310 | {file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"}, 311 | {file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"}, 312 | {file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"}, 313 | {file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"}, 314 | {file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"}, 315 | {file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"}, 316 | {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"}, 317 | {file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"}, 318 | {file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"}, 319 | {file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"}, 320 | {file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"}, 321 | {file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"}, 322 | ] 323 | 324 | [package.dependencies] 325 | altgraph = "*" 326 | macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} 327 | pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} 328 | pyinstaller-hooks-contrib = ">=2021.4" 329 | pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} 330 | setuptools = ">=42.0.0" 331 | 332 | [package.extras] 333 | encryption = ["tinyaes (>=1.0.0)"] 334 | hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] 335 | 336 | [[package]] 337 | name = "pyinstaller-hooks-contrib" 338 | version = "2023.6" 339 | description = "Community maintained hooks for PyInstaller" 340 | optional = false 341 | python-versions = ">=3.7" 342 | files = [ 343 | {file = "pyinstaller-hooks-contrib-2023.6.tar.gz", hash = "sha256:596a72009d8692b043e0acbf5e1b476d93149900142ba01845dded91a0770cb5"}, 344 | {file = "pyinstaller_hooks_contrib-2023.6-py2.py3-none-any.whl", hash = "sha256:aa6d7d038814df6aa7bec7bdbebc7cb4c693d3398df858f6062957f0797d397b"}, 345 | ] 346 | 347 | [[package]] 348 | name = "pylint" 349 | version = "2.17.4" 350 | description = "python code static checker" 351 | optional = false 352 | python-versions = ">=3.7.2" 353 | files = [ 354 | {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, 355 | {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, 356 | ] 357 | 358 | [package.dependencies] 359 | astroid = ">=2.15.4,<=2.17.0-dev0" 360 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 361 | dill = [ 362 | {version = ">=0.2", markers = "python_version < \"3.11\""}, 363 | {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, 364 | ] 365 | isort = ">=4.2.5,<6" 366 | mccabe = ">=0.6,<0.8" 367 | platformdirs = ">=2.2.0" 368 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 369 | tomlkit = ">=0.10.1" 370 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 371 | 372 | [package.extras] 373 | spelling = ["pyenchant (>=3.2,<4.0)"] 374 | testutils = ["gitpython (>3)"] 375 | 376 | [[package]] 377 | name = "pyscylla" 378 | version = "0.11.1" 379 | description = "Python bindings for Scylla" 380 | optional = false 381 | python-versions = "*" 382 | files = [ 383 | {file = "pyscylla-0.11.1-cp310-cp310-win32.whl", hash = "sha256:2c7e393ab364ce8c3199f94b36974f393b189e12b39fb0779b928414b247cfa6"}, 384 | {file = "pyscylla-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:93e94d778bae927a74f82a07bec76a5dc47ab17e9a34f521e4aa92d40c62519c"}, 385 | {file = "pyscylla-0.11.1-cp311-cp311-win32.whl", hash = "sha256:6c229ee2f58ed2016859967cd48b780c5b4265c5c364a309c53c31e6cff9e077"}, 386 | {file = "pyscylla-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:80a19b4aa9d4524de73a327b8c1b4803042e32e456e2b73e8130a56a057026b5"}, 387 | {file = "pyscylla-0.11.1-cp38-cp38-win32.whl", hash = "sha256:d8ddec37218d1421173b2bd9d6f86fa626b875abdf098ed252f3f67cdf57b162"}, 388 | {file = "pyscylla-0.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:9ca3be51ebe7678084f2c6433e4e0a69bf80bbbabe533254d95beb01294fd0cc"}, 389 | {file = "pyscylla-0.11.1-cp39-cp39-win32.whl", hash = "sha256:3ce769f3cc5bd7514be7b4bcf0eb93e5769e9bd299756742ba2cbf2f455fa4ec"}, 390 | {file = "pyscylla-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:4c299f744245404197eb12d4d4112c2235d5dd0310d54043ab19729aa2ba049f"}, 391 | ] 392 | 393 | [[package]] 394 | name = "pywin32-ctypes" 395 | version = "0.2.2" 396 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 397 | optional = false 398 | python-versions = ">=3.6" 399 | files = [ 400 | {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, 401 | {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, 402 | ] 403 | 404 | [[package]] 405 | name = "setuptools" 406 | version = "68.0.0" 407 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 408 | optional = false 409 | python-versions = ">=3.7" 410 | files = [ 411 | {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, 412 | {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, 413 | ] 414 | 415 | [package.extras] 416 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 417 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 418 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 419 | 420 | [[package]] 421 | name = "six" 422 | version = "1.16.0" 423 | description = "Python 2 and 3 compatibility utilities" 424 | optional = false 425 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 426 | files = [ 427 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 428 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 429 | ] 430 | 431 | [[package]] 432 | name = "termcolor" 433 | version = "2.3.0" 434 | description = "ANSI color formatting for output in terminal" 435 | optional = false 436 | python-versions = ">=3.7" 437 | files = [ 438 | {file = "termcolor-2.3.0-py3-none-any.whl", hash = "sha256:3afb05607b89aed0ffe25202399ee0867ad4d3cb4180d98aaf8eefa6a5f7d475"}, 439 | {file = "termcolor-2.3.0.tar.gz", hash = "sha256:b5b08f68937f138fe92f6c089b99f1e2da0ae56c52b78bf7075fd95420fd9a5a"}, 440 | ] 441 | 442 | [package.extras] 443 | tests = ["pytest", "pytest-cov"] 444 | 445 | [[package]] 446 | name = "toml" 447 | version = "0.10.2" 448 | description = "Python Library for Tom's Obvious, Minimal Language" 449 | optional = false 450 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 451 | files = [ 452 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 453 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 454 | ] 455 | 456 | [[package]] 457 | name = "tomli" 458 | version = "2.0.1" 459 | description = "A lil' TOML parser" 460 | optional = false 461 | python-versions = ">=3.7" 462 | files = [ 463 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 464 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 465 | ] 466 | 467 | [[package]] 468 | name = "tomlkit" 469 | version = "0.11.8" 470 | description = "Style preserving TOML library" 471 | optional = false 472 | python-versions = ">=3.7" 473 | files = [ 474 | {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, 475 | {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, 476 | ] 477 | 478 | [[package]] 479 | name = "typing-extensions" 480 | version = "4.7.1" 481 | description = "Backported and Experimental Type Hints for Python 3.7+" 482 | optional = false 483 | python-versions = ">=3.7" 484 | files = [ 485 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 486 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 487 | ] 488 | 489 | [[package]] 490 | name = "unicorn" 491 | version = "1.0.3" 492 | description = "Unicorn CPU emulator engine" 493 | optional = false 494 | python-versions = "*" 495 | files = [ 496 | {file = "unicorn-1.0.3-py2.py3-none-macosx_10_14_x86_64.whl", hash = "sha256:23f842ce919fcba1a1a1d54471951cf41a4fbf34fd64a09545ed0f1250f9919c"}, 497 | {file = "unicorn-1.0.3-py2.py3-none-manylinux1_i686.whl", hash = "sha256:bc98daeff337a8edfd2bc45d7712d105c35083c55c9a85970f0a237dd9ba226b"}, 498 | {file = "unicorn-1.0.3-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:9121ba837e7bb25ece2a93d115f87a8e53e243dc06c60d2e91c4302073068ad7"}, 499 | {file = "unicorn-1.0.3-py2.py3-none-win32.whl", hash = "sha256:7e63a87fe1d235a8a731baf06bc4dfd3615247af440f8ea54b34834835a55e77"}, 500 | {file = "unicorn-1.0.3-py2.py3-none-win_amd64.whl", hash = "sha256:1f719f86fe0d41a0c6b65de4fce17b6f06791cf5eff5b228b6c05de5c49162e8"}, 501 | {file = "unicorn-1.0.3.tar.gz", hash = "sha256:08fd3616de4ad97ffcbddd3c40d3afb0bd789956b5a1e8905be90042b9706541"}, 502 | ] 503 | 504 | [[package]] 505 | name = "wrapt" 506 | version = "1.15.0" 507 | description = "Module for decorators, wrappers and monkey patching." 508 | optional = false 509 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 510 | files = [ 511 | {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, 512 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, 513 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, 514 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, 515 | {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, 516 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, 517 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, 518 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, 519 | {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, 520 | {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, 521 | {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, 522 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, 523 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, 524 | {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, 525 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, 526 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, 527 | {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, 528 | {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, 529 | {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, 530 | {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, 531 | {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, 532 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, 533 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, 534 | {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, 535 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, 536 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, 537 | {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, 538 | {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, 539 | {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, 540 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, 541 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, 542 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, 543 | {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, 544 | {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, 545 | {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, 546 | {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, 547 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, 548 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, 549 | {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, 550 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, 551 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, 552 | {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, 553 | {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, 554 | {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, 555 | {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, 556 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, 557 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, 558 | {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, 559 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, 560 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, 561 | {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, 562 | {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, 563 | {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, 564 | {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, 565 | {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, 566 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, 567 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, 568 | {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, 569 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, 570 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, 571 | {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, 572 | {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, 573 | {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, 574 | {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, 575 | {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, 576 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, 577 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, 578 | {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, 579 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, 580 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, 581 | {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, 582 | {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, 583 | {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, 584 | {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, 585 | {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, 586 | ] 587 | 588 | [[package]] 589 | name = "xxhash" 590 | version = "2.0.2" 591 | description = "Python binding for xxHash" 592 | optional = false 593 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 594 | files = [ 595 | {file = "xxhash-2.0.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dac3b94881b943bbe418f5829128b9c48f69a66f816ef8b72ee0129d676dbd7c"}, 596 | {file = "xxhash-2.0.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:43fd97f332bd581639bb99fe8f09f7e9113d49cad4d21bef0620867f92c802c6"}, 597 | {file = "xxhash-2.0.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:6e5058c3fa5b42ded9a303f1a5a42d3ff732cb54c108424c63e993fc3379513c"}, 598 | {file = "xxhash-2.0.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:dfacce97a3ccb46089e358ceaeca9300298511673bf87596da66882af386f6c7"}, 599 | {file = "xxhash-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1dfa115c8e07b3e1d94ebd60a6d6ee16ea692efb890e245addb0d33b47ee1dee"}, 600 | {file = "xxhash-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:fb28b0313c7582225373f343635674231518452331a9bdea8261d0e27b48594f"}, 601 | {file = "xxhash-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:427851234a87bfe6636c90b89bd65b7ca913befff3c7bcd92a3568e635fccc92"}, 602 | {file = "xxhash-2.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0b92a01dc8dcada8827de140a5df83c9e8e5c190ef8bf972c98ebbe0924ee044"}, 603 | {file = "xxhash-2.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:676d6964b8a9bdaf737ae6836b886ab53b2863c6aa00d43952b130a6130d1bdc"}, 604 | {file = "xxhash-2.0.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8362693a1ce5c1373f48f047470e7797ed17dfe5babc37ba7bef50d6e6f83a72"}, 605 | {file = "xxhash-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:515747159fccd23fc9d1b7afeaa8bd7fc36884188b47491713d22032c5f9e502"}, 606 | {file = "xxhash-2.0.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1787b9cea43f256f8d06c8429999d386a9da9cb000c265a4dde48dd08242528"}, 607 | {file = "xxhash-2.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d47ab1245ee4c7e6fc424ad990e4d7cfe0f206d617efe990fea34000a9242102"}, 608 | {file = "xxhash-2.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:81ec049f4936a49311e1fc58036d7d682b5c83d6d16ba1c852a981588c90e027"}, 609 | {file = "xxhash-2.0.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:df71aeedee74eaf670d1243b6722c8c77626f3b6e6cf2cd79f2e336b151749cd"}, 610 | {file = "xxhash-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a922315c8e20dae0d35e54b49fd7ee348fe0a5e2fd8ec02f6a74140e063fcdb3"}, 611 | {file = "xxhash-2.0.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:22ddd484cd92d138feeec556387894b8ec529bab7f2feb3a177eb84baadee8c1"}, 612 | {file = "xxhash-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:b4964e7ddca1ef9d7addef40a9f5eaa97aeda367c1d895e392533c0d2f9c3b8e"}, 613 | {file = "xxhash-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:6077fdb44f68920c4ac8e2f34b2a107c9a218f00a698253c824a0c6c1b9622a3"}, 614 | {file = "xxhash-2.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:04ae5706ddfe0fd2b46cd0b6487d3edae7e724e27d732b055ffd0f9539c4afc5"}, 615 | {file = "xxhash-2.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c4a892bc47b6ea92bbb82499a81882548ce990d62c1862b3834f1f70e8cf4423"}, 616 | {file = "xxhash-2.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:57d43ce9594676b503c0a0a383481cb4e5cf736f88970bd41849fe15a68a5d48"}, 617 | {file = "xxhash-2.0.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c2e44d162c3361392dbde736ee8ba3d1a414f63e32be6c71186f2b0654559d26"}, 618 | {file = "xxhash-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:0beb79835ca47af257f8126fccd9d5e0ba56ba7d39dab6f6b5a7acea4d8ac4b5"}, 619 | {file = "xxhash-2.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f2bef10c417c4667310cc240d49e521e6b5fc90c4ff77a1ec78649869685e8d3"}, 620 | {file = "xxhash-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:9b6bb1bd34a6365c790c328a604ec5a628059fef6e4486380caa89bc12787a6e"}, 621 | {file = "xxhash-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4243dbeb1ce09d359289844f0c54676343857fdc6a092184aea159fecdf6d9f3"}, 622 | {file = "xxhash-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71b38300e1803ab32ee787f89cdbc032b46ac5834eca9109d8fb576ae1a31741"}, 623 | {file = "xxhash-2.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a8a68d117178f15c96cb9ae2613f53db94e0fdb34ffc69c7ab600c899c7a966c"}, 624 | {file = "xxhash-2.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd9c72520f790ce6eaa535cdad1a53ded22deab43766cfa7cef42834a9a65561"}, 625 | {file = "xxhash-2.0.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:f95adf6091fa13ce19fab21fadb8d07210822320568d24a6405d6b557afc0411"}, 626 | {file = "xxhash-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:00aaf882036d2a0fa7652cf9aeaaf2ad077b784c09ef8d60f5d97ebf0d47ffa1"}, 627 | {file = "xxhash-2.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bb8c0efad20da40da1aa56f36b929b965d1adede8a1d5b37b702d378a683e0dd"}, 628 | {file = "xxhash-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:6fc0b8c21a181b771e1f0c25eb8a0a241af0126f1fc19f4c3cde7233de91326f"}, 629 | {file = "xxhash-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b232b47a3aa825e0df14b1bd3e051dd327c8539e382728ddb81997d26de5256a"}, 630 | {file = "xxhash-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc328d3d635ec851d6befdf6ced2134d587d3be973dbbbc489da24c0c88ecb01"}, 631 | {file = "xxhash-2.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9e6e5e095417060bed45119c510d5bc846b62e2a8218cb3e5a19b3ccf12e4c18"}, 632 | {file = "xxhash-2.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b4b7d4d19c125738c5fc48356505dfbd63b3cdf826dd868a1b80a73de48729b7"}, 633 | {file = "xxhash-2.0.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:686fcf2aff041df65470eccc7dcea5e7e77cfad99efcaba0c6f58bbd81846e10"}, 634 | {file = "xxhash-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cb3a196fd1d55ce86b1123cbf3ef6603f80f4d0b46541412bb5056b0563ef384"}, 635 | {file = "xxhash-2.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:68d067427f2c6f7b3014e28bf4794b0876ab5f6366b53e1d6f59d275b4f19a8d"}, 636 | {file = "xxhash-2.0.2-cp38-cp38-win32.whl", hash = "sha256:73649555656dd17e809b9b3c54855f4f72144024b0e6395cd37b5395fa0f48c3"}, 637 | {file = "xxhash-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:dafd1066c99d448a7a1226f10766b61ff752aaad8a4392e4cae30aafefa6fff5"}, 638 | {file = "xxhash-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb1e9e347c9810a272154814cf5ce33a6c3ac7d0d7cbcb066e92dd5f9fa4db8f"}, 639 | {file = "xxhash-2.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ebff22f1783f641c6c2b313bfc44d6cc620c17409ec512e67c7c6de809155880"}, 640 | {file = "xxhash-2.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b7640e043ac6e0f503eadb108e6971d69b0c95c23fbcac3e5632578f9f906050"}, 641 | {file = "xxhash-2.0.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:db2352d375e6594620c462c029d3c1a1b18ff7168e470657e354f1b8b332d9dd"}, 642 | {file = "xxhash-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f49dbd3b8e4cc13f2df92fb3db39204e3258105a212e23784cbb340e415ae8ed"}, 643 | {file = "xxhash-2.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e70059c5cc8f0cecd16d8cb0263de8f317239cabee3fa4af35c0a1ddaed2110e"}, 644 | {file = "xxhash-2.0.2-cp39-cp39-win32.whl", hash = "sha256:a0199a07a264be96ed658ba3b4e9ee58a3c678e51a18e134e2518cf1a8171e18"}, 645 | {file = "xxhash-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:173d3f662dc88de734bd622e46a3bbac6fd00e957b3e098fa8b75b141aa4354e"}, 646 | {file = "xxhash-2.0.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:e94fdff9b102ca7c0969230d209f7ce17020db17a89d026ac45d8ffb9e4929ec"}, 647 | {file = "xxhash-2.0.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:d7175cd7f490aae742d18eb9b519e74180958f88fa8ff47091727b3efb57bfbf"}, 648 | {file = "xxhash-2.0.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:d707d2a053a5d55ccd2e59d7a228636cafeebb44c9ac3ca1c088f4d384c8c3a9"}, 649 | {file = "xxhash-2.0.2-pp27-pypy_73-win32.whl", hash = "sha256:dad190caa293abbb39d96b4a09f121fc971d81eb19c96e4e0db89a99a7d59b93"}, 650 | {file = "xxhash-2.0.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5dc3da5fa855dd8e35f24d20fabfcd29c0b3ac85a14dc2c329c029971ae4eeb7"}, 651 | {file = "xxhash-2.0.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:17a3b0a2ff20879ed5c9d9c178349e9c6257db11b193e4103282d7a78ef9cb08"}, 652 | {file = "xxhash-2.0.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c75f8375c80c3815f49a744ef1a8303577757eb9a2dc53bed33d9318b760fec6"}, 653 | {file = "xxhash-2.0.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:eb2670ed6c435189aeb479bfff990e00b849ae0ff49945632db74b2a2a08d192"}, 654 | {file = "xxhash-2.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ff518ec1bd7cc33218f8f3325848c56e9c73c5df30138a64a89dd65ab1e1ffb5"}, 655 | {file = "xxhash-2.0.2-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:c4a0806ffb33c9d892b5565fa010c252c7e0f4d01ded901a637dfede624e4d0c"}, 656 | {file = "xxhash-2.0.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:fdfac2014301da79cebcd8f9535c875f63242fe404d741cec5f70f400cc6a561"}, 657 | {file = "xxhash-2.0.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:357f6a52bd18a80635cf4c83f648c42fa0609713b4183929ed019f7627af4b68"}, 658 | {file = "xxhash-2.0.2.tar.gz", hash = "sha256:b7bead8cf6210eadf9cecf356e17af794f57c0939a3d420a00d87ea652f87b49"}, 659 | ] 660 | 661 | [[package]] 662 | name = "yapf" 663 | version = "0.32.0" 664 | description = "A formatter for Python code." 665 | optional = false 666 | python-versions = "*" 667 | files = [ 668 | {file = "yapf-0.32.0-py2.py3-none-any.whl", hash = "sha256:8fea849025584e486fd06d6ba2bed717f396080fd3cc236ba10cb97c4c51cf32"}, 669 | {file = "yapf-0.32.0.tar.gz", hash = "sha256:a3f5085d37ef7e3e004c4ba9f9b3e40c54ff1901cd111f05145ae313a7c67d1b"}, 670 | ] 671 | 672 | [metadata] 673 | lock-version = "2.0" 674 | python-versions = ">=3.9, <3.12" 675 | content-hash = "8e67216ad88ed384a913dcb7d6a8546ed3399d0365df9e9d43ce0115c95d699d" 676 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "unlicense" 3 | version = "0.4.0" 4 | description = "Unpack executables protected with Themida/WinLicense 2.x and 3.x" 5 | authors = ["Erwan Grelet"] 6 | license = "GPL-3.0-or-later" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.9, <3.12" 10 | frida = "^16.1" 11 | unicorn = "^1.0" 12 | lief = "^0.13" 13 | fire = "^0.4" 14 | capstone = "^4.0" 15 | xxhash = "^2.0" 16 | pyscylla = "^0.11" 17 | 18 | [tool.poetry.dev-dependencies] 19 | mypy = "^0.910" 20 | pylint = "^2.11" 21 | yapf = "^0.32.0" 22 | toml = "^0.10.2" 23 | pyinstaller = "^5.13" 24 | 25 | [tool.poetry.scripts] 26 | unlicense = 'unlicense.application:main' 27 | 28 | [build-system] 29 | requires = ["poetry-core>=1.0.0"] 30 | build-backend = "poetry.core.masonry.api" 31 | 32 | [tool.pylint.'MESSAGES CONTROL'] 33 | max-line-length = 120 34 | disable = "C0114, C0115, C0116, I1101" 35 | -------------------------------------------------------------------------------- /unlicense.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | from PyInstaller.utils.hooks import collect_dynamic_libs 4 | 5 | dlls = collect_dynamic_libs("capstone") + collect_dynamic_libs("unicorn") 6 | resource_files = [('unlicense/', 'unlicense')] 7 | block_cipher = None 8 | 9 | a = Analysis(['unlicense\\__main__.py'], 10 | pathex=[], 11 | binaries=dlls, 12 | datas=resource_files, 13 | hiddenimports=[], 14 | hookspath=[], 15 | hooksconfig={}, 16 | runtime_hooks=[], 17 | excludes=[], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher, 21 | noarchive=False) 22 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 23 | 24 | exe = EXE(pyz, 25 | a.scripts, 26 | a.binaries, 27 | a.zipfiles, 28 | a.datas, [], 29 | name='unlicense', 30 | debug=False, 31 | bootloader_ignore_signals=False, 32 | strip=False, 33 | upx=False, 34 | upx_exclude=[], 35 | runtime_tmpdir=None, 36 | console=True, 37 | icon='assets/unlicense.ico', 38 | disable_windowed_traceback=False, 39 | target_arch=None, 40 | codesign_identity=None, 41 | entitlements_file=None) 42 | -------------------------------------------------------------------------------- /unlicense/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ergrelet/unlicense/95c8dc6a4324ff70c04bceda8bd166be6c5f0c73/unlicense/__init__.py -------------------------------------------------------------------------------- /unlicense/__main__.py: -------------------------------------------------------------------------------- 1 | from unlicense.application import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /unlicense/application.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import threading 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | import fire # type: ignore 9 | 10 | from . import frida_exec, winlicense2, winlicense3 11 | from .dump_utils import dump_dotnet_assembly, dump_pe, get_section_ranges, interpreter_can_dump_pe, probe_text_sections 12 | from .logger import setup_logger 13 | from .version_detection import detect_winlicense_version 14 | 15 | # Supported Themida/WinLicense major versions 16 | SUPPORTED_VERSIONS = [2, 3] 17 | LOG = logging.getLogger("unlicense") 18 | 19 | 20 | def main() -> None: 21 | fire.Fire(run_unlicense) 22 | 23 | 24 | def run_unlicense( 25 | pe_to_dump: str, 26 | verbose: bool = False, 27 | pause_on_oep: bool = False, 28 | no_imports: bool = False, 29 | force_oep: Optional[int] = None, 30 | target_version: Optional[int] = None, 31 | timeout: int = 10, 32 | ) -> None: 33 | """ 34 | Unpack executables protected with Themida/WinLicense 2.x and 3.x 35 | """ 36 | setup_logger(LOG, verbose) 37 | 38 | # Make sure child processes won't try to run as administrator 39 | _force_run_as_invoker() 40 | 41 | pe_path = Path(pe_to_dump) 42 | if not pe_path.is_file(): 43 | LOG.error("'%s' isn't a file or doesn't exist", pe_path) 44 | sys.exit(1) 45 | 46 | # Detect Themida/Winlicense version if needed 47 | if target_version is None: 48 | target_version = detect_winlicense_version(pe_to_dump) 49 | if target_version is None: 50 | LOG.error("Failed to automatically detect packer version") 51 | sys.exit(2) 52 | elif target_version not in SUPPORTED_VERSIONS: 53 | LOG.error("Target version '%d' is not supported", target_version) 54 | sys.exit(2) 55 | LOG.info("Detected packer version: %d.x", target_version) 56 | 57 | # Check PE architecture and bitness 58 | if not interpreter_can_dump_pe(pe_to_dump): 59 | LOG.error("Target PE cannot be dumped with this interpreter. " 60 | "This is most likely a 32 vs 64 bit mismatch.") 61 | sys.exit(3) 62 | 63 | section_ranges = get_section_ranges(pe_to_dump) 64 | text_section_ranges = probe_text_sections(pe_to_dump) 65 | if text_section_ranges is None: 66 | LOG.error("Failed to automatically detect .text section") 67 | sys.exit(4) 68 | 69 | dumped_image_base = 0 70 | dumped_oep = 0 71 | is_dotnet = False 72 | oep_reached = threading.Event() 73 | 74 | def notify_oep_reached(image_base: int, oep: int, dotnet: bool) -> None: 75 | nonlocal dumped_image_base 76 | nonlocal dumped_oep 77 | nonlocal is_dotnet 78 | dumped_image_base = image_base 79 | dumped_oep = oep 80 | is_dotnet = dotnet 81 | oep_reached.set() 82 | 83 | # Spawn the packed executable and instrument it to find its OEP 84 | process_controller = frida_exec.spawn_and_instrument( 85 | pe_path, text_section_ranges, notify_oep_reached) 86 | try: 87 | # Block until OEP is reached 88 | if not oep_reached.wait(float(timeout)): 89 | LOG.error("Original entry point wasn't reached before timeout") 90 | sys.exit(4) 91 | 92 | LOG.info("OEP reached: OEP=%s BASE=%s DOTNET=%r", hex(dumped_oep), 93 | hex(dumped_image_base), is_dotnet) 94 | if pause_on_oep: 95 | input("Thread blocked, press ENTER to proceed with the dumping.") 96 | 97 | if force_oep is not None: 98 | dumped_oep = dumped_image_base + force_oep 99 | LOG.info("Using given OEP RVA value instead (%s)", hex(force_oep)) 100 | 101 | # Pick the range that contains the OEP 102 | text_section_range = text_section_ranges[0] 103 | for range in text_section_ranges: 104 | if range.contains(dumped_oep - dumped_image_base): 105 | text_section_range = range 106 | 107 | # .NET assembly dumping works the same way regardless of the version 108 | if is_dotnet: 109 | LOG.info("Dumping .NET assembly ...") 110 | if not dump_dotnet_assembly(process_controller, dumped_image_base): 111 | LOG.error(".NET assembly dump failed") 112 | # Do not bother recovering imports and start dumping if requested 113 | elif no_imports: 114 | dump_pe(process_controller, pe_to_dump, dumped_image_base, 115 | dumped_oep, 0, 0, True) 116 | # Fix imports and dump the executable 117 | elif target_version == 2: 118 | winlicense2.fix_and_dump_pe(process_controller, pe_to_dump, 119 | dumped_image_base, dumped_oep, 120 | text_section_range) 121 | elif target_version == 3: 122 | winlicense3.fix_and_dump_pe(process_controller, pe_to_dump, 123 | dumped_image_base, dumped_oep, 124 | section_ranges, text_section_range) 125 | finally: 126 | # Try to kill the process on exit 127 | process_controller.terminate_process() 128 | 129 | 130 | def _force_run_as_invoker() -> None: 131 | os.environ["__COMPAT_LAYER"] = "RUNASINVOKER" 132 | -------------------------------------------------------------------------------- /unlicense/dump_utils.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import logging 3 | import os 4 | import platform 5 | import shutil 6 | import struct 7 | from tempfile import TemporaryDirectory 8 | from typing import List, Optional 9 | 10 | import lief 11 | import pyscylla # type: ignore 12 | 13 | from unlicense.lief_utils import lief_pe_data_directories, lief_pe_sections 14 | 15 | from .process_control import MemoryRange, ProcessController 16 | 17 | LOG = logging.getLogger(__name__) 18 | 19 | 20 | def get_section_ranges(pe_file_path: str) -> List[MemoryRange]: 21 | section_ranges: List[MemoryRange] = [] 22 | binary = lief.PE.parse(pe_file_path) 23 | if binary is None: 24 | LOG.error("Failed to parse PE '%s'", pe_file_path) 25 | return section_ranges 26 | 27 | for section in lief_pe_sections(binary): 28 | section_ranges += [ 29 | MemoryRange(section.virtual_address, section.virtual_size, "r--") 30 | ] 31 | 32 | return section_ranges 33 | 34 | 35 | def probe_text_sections(pe_file_path: str) -> Optional[List[MemoryRange]]: 36 | text_sections = [] 37 | binary = lief.PE.parse(pe_file_path) 38 | if binary is None: 39 | LOG.error("Failed to parse PE '%s'", pe_file_path) 40 | return None 41 | 42 | # Find the potential original text sections (i.e., executable sections with 43 | # "empty" names or named '.text*'). 44 | # Note(ergrelet): we thus do not want to include Themida/WinLicense's 45 | # sections in that list. 46 | for section in lief_pe_sections(binary): 47 | section_name = section.fullname 48 | stripped_section_name = section_name.replace(' ', 49 | '').replace('\00', '') 50 | if len(stripped_section_name) > 0 and \ 51 | stripped_section_name not in [".text", ".textbss", ".textidx"]: 52 | break 53 | 54 | if section.has_characteristic( 55 | lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE): 56 | LOG.debug("Probed .text section at (0x%x, 0x%x)", 57 | section.virtual_address, section.virtual_size) 58 | text_sections += [ 59 | MemoryRange(section.virtual_address, section.virtual_size, 60 | "r-x") 61 | ] 62 | 63 | return None if len(text_sections) == 0 else text_sections 64 | 65 | 66 | def dump_pe( 67 | process_controller: ProcessController, 68 | pe_file_path: str, 69 | image_base: int, 70 | oep: int, 71 | iat_addr: int, 72 | iat_size: int, 73 | add_new_iat: bool, 74 | ) -> bool: 75 | # Reclaim as much memory as possible. This is kind of a hack for 32-bit 76 | # interpreters not to run out of memory when dumping. 77 | # Idea: `pefile` might be less memory hungry than `lief` for our use case? 78 | process_controller.clear_cached_data() 79 | gc.collect() 80 | 81 | with TemporaryDirectory() as tmp_dir: 82 | TMP_FILE_PATH1 = os.path.join(tmp_dir, "unlicense.tmp2") 83 | TMP_FILE_PATH2 = os.path.join(tmp_dir, "unlicense.tmp2") 84 | try: 85 | pyscylla.dump_pe(process_controller.pid, image_base, oep, 86 | TMP_FILE_PATH1, pe_file_path) 87 | except pyscylla.ScyllaException as scylla_exception: 88 | LOG.error("Failed to dump PE: %s", str(scylla_exception)) 89 | return False 90 | 91 | LOG.info("Fixing dump ...") 92 | try: 93 | pyscylla.fix_iat(process_controller.pid, image_base, iat_addr, 94 | iat_size, add_new_iat, TMP_FILE_PATH1, 95 | TMP_FILE_PATH2) 96 | except pyscylla.ScyllaException as scylla_exception: 97 | LOG.error("Failed to fix IAT: %s", str(scylla_exception)) 98 | return False 99 | 100 | try: 101 | pyscylla.rebuild_pe(TMP_FILE_PATH2, False, True, False) 102 | except pyscylla.ScyllaException as scylla_exception: 103 | LOG.error("Failed to rebuild PE: %s", str(scylla_exception)) 104 | return False 105 | 106 | LOG.info("Rebuilding PE ...") 107 | output_file_name = f"unpacked_{process_controller.main_module_name}" 108 | _fix_pe(TMP_FILE_PATH2, output_file_name) 109 | 110 | LOG.info("Output file has been saved at '%s'", output_file_name) 111 | 112 | return True 113 | 114 | 115 | def dump_dotnet_assembly( 116 | process_controller: ProcessController, 117 | image_base: int, 118 | ) -> bool: 119 | output_file_name = f"unpacked_{process_controller.main_module_name}" 120 | try: 121 | pyscylla.dump_pe(process_controller.pid, image_base, image_base, 122 | output_file_name, None) 123 | except pyscylla.ScyllaException as scylla_exception: 124 | LOG.error("Failed to dump PE: %s", str(scylla_exception)) 125 | return False 126 | 127 | LOG.info("Output file has been saved at '%s'", output_file_name) 128 | 129 | return True 130 | 131 | 132 | def _fix_pe(pe_file_path: str, output_file_path: str) -> None: 133 | with TemporaryDirectory() as tmp_dir: 134 | TMP_FILE_PATH = os.path.join(tmp_dir, "unlicense.tmp") 135 | _rebuild_pe(pe_file_path, TMP_FILE_PATH) 136 | _resize_pe(TMP_FILE_PATH, output_file_path) 137 | 138 | 139 | def _rebuild_pe(pe_file_path: str, output_file_path: str) -> None: 140 | binary = lief.PE.parse(pe_file_path) 141 | if binary is None: 142 | LOG.error("Failed to parse PE '%s'", pe_file_path) 143 | return 144 | 145 | # Rename sections 146 | _resolve_section_names(binary) 147 | 148 | # Disable ASLR 149 | binary.header.add_characteristic( 150 | lief.PE.HEADER_CHARACTERISTICS.RELOCS_STRIPPED) 151 | binary.optional_header.remove(lief.PE.DLL_CHARACTERISTICS.DYNAMIC_BASE) 152 | # Rebuild PE 153 | builder = lief.PE.Builder(binary) 154 | builder.build_dos_stub(True) 155 | builder.build_overlay(True) 156 | builder.build() 157 | builder.write(output_file_path) 158 | 159 | 160 | def _resolve_section_names(binary: lief.PE.Binary) -> None: 161 | for data_dir in lief_pe_data_directories(binary): 162 | if data_dir.type == lief.PE.DATA_DIRECTORY.RESOURCE_TABLE and \ 163 | data_dir.section is not None: 164 | LOG.debug(".rsrc section found (RVA=%s)", 165 | hex(data_dir.section.virtual_address)) 166 | data_dir.section.name = ".rsrc" 167 | 168 | ep_address = binary.optional_header.addressof_entrypoint 169 | for section in lief_pe_sections(binary): 170 | if section.virtual_address + section.virtual_size > ep_address >= section.virtual_address: 171 | LOG.debug(".text section found (RVA=%s)", 172 | hex(section.virtual_address)) 173 | section.name = ".text" 174 | 175 | 176 | def _resize_pe(pe_file_path: str, output_file_path: str) -> None: 177 | pe_size = _get_pe_size(pe_file_path) 178 | if pe_size is None: 179 | return None 180 | 181 | # Copy file 182 | shutil.copy(pe_file_path, output_file_path) 183 | # Truncate file 184 | with open(output_file_path, "ab") as pe_file: 185 | pe_file.truncate(pe_size) 186 | 187 | 188 | def _get_pe_size(pe_file_path: str) -> Optional[int]: 189 | binary = lief.PE.parse(pe_file_path) 190 | if binary is None: 191 | LOG.error("Failed to parse PE '%s'", pe_file_path) 192 | return None 193 | 194 | number_of_sections = len(binary.sections) 195 | if number_of_sections == 0: 196 | # Shouldn't happen but hey 197 | return None 198 | 199 | # Determine the actual PE raw size 200 | highest_section = binary.sections[0] 201 | for section in lief_pe_sections(binary): 202 | # Select section with the highest offset 203 | if section.offset > highest_section.offset: 204 | highest_section = section 205 | # If sections have the same offset, select the one with the biggest size 206 | elif section.offset == highest_section.offset and section.size > highest_section.size: 207 | highest_section = section 208 | pe_size = highest_section.offset + highest_section.size 209 | 210 | return pe_size 211 | 212 | 213 | def pointer_size_to_fmt(pointer_size: int) -> str: 214 | if pointer_size == 4: 215 | return " bool: 222 | current_platform = platform.machine() 223 | binary = lief.parse(pe_file_path) 224 | pe_architecture = binary.header.machine 225 | 226 | # 64-bit OS on x86 227 | if current_platform == "AMD64": 228 | bitness = struct.calcsize("P") * 8 229 | if bitness == 64: 230 | # Only 64-bit PEs are supported 231 | return bool(pe_architecture == lief.PE.MACHINE_TYPES.AMD64) 232 | if bitness == 32: 233 | # Only 32-bit PEs are supported 234 | return bool(pe_architecture == lief.PE.MACHINE_TYPES.I386) 235 | return False 236 | 237 | # 32-bit OS on x86 238 | if current_platform == "x86": 239 | # Only 32-bit PEs are supported 240 | return bool(pe_architecture == lief.PE.MACHINE_TYPES.I386) 241 | 242 | return False 243 | -------------------------------------------------------------------------------- /unlicense/emulation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from typing import Callable, Dict, Tuple, Any, Optional 4 | 5 | from unicorn import ( # type: ignore 6 | Uc, UcError, UC_ARCH_X86, UC_MODE_32, UC_MODE_64, UC_PROT_READ, 7 | UC_PROT_WRITE, UC_PROT_ALL, UC_HOOK_MEM_UNMAPPED, UC_HOOK_BLOCK) 8 | from unicorn.x86_const import ( # type: ignore 9 | UC_X86_REG_ESP, UC_X86_REG_EBP, UC_X86_REG_EIP, UC_X86_REG_RSP, 10 | UC_X86_REG_RBP, UC_X86_REG_RIP, UC_X86_REG_MSR, UC_X86_REG_EAX, 11 | UC_X86_REG_RAX) 12 | 13 | from .dump_utils import pointer_size_to_fmt 14 | from .process_control import ProcessController, Architecture, ReadProcessMemoryError 15 | 16 | STACK_MAGIC_RET_ADDR = 0xdeadbeef 17 | LOG = logging.getLogger(__name__) 18 | 19 | 20 | def resolve_wrapped_api( 21 | wrapper_start_addr: int, 22 | process_controller: ProcessController, 23 | expected_ret_addr: Optional[int] = None) -> Optional[int]: 24 | arch = process_controller.architecture 25 | if arch == Architecture.X86_32: 26 | uc_arch = UC_ARCH_X86 27 | uc_mode = UC_MODE_32 28 | pc_register = UC_X86_REG_EIP 29 | sp_register = UC_X86_REG_ESP 30 | bp_register = UC_X86_REG_EBP 31 | result_register = UC_X86_REG_EAX 32 | stack_addr = 0xff000000 33 | setup_teb = _setup_teb_x86 34 | elif arch == Architecture.X86_64: 35 | uc_arch = UC_ARCH_X86 36 | uc_mode = UC_MODE_64 37 | pc_register = UC_X86_REG_RIP 38 | sp_register = UC_X86_REG_RSP 39 | bp_register = UC_X86_REG_RBP 40 | result_register = UC_X86_REG_RAX 41 | stack_addr = 0xff00000000000000 42 | setup_teb = _setup_teb_x64 43 | else: 44 | raise NotImplementedError(f"Architecture '{arch}' isn't supported") 45 | 46 | uc = Uc(uc_arch, uc_mode) 47 | try: 48 | # Map fake return address's page in case wrappers try to access it 49 | aligned_addr = STACK_MAGIC_RET_ADDR - (STACK_MAGIC_RET_ADDR % 50 | process_controller.page_size) 51 | uc.mem_map(aligned_addr, process_controller.page_size, UC_PROT_ALL) 52 | 53 | # Setup a stack 54 | stack_size = 3 * process_controller.page_size 55 | stack_start = stack_addr + stack_size - process_controller.page_size 56 | uc.mem_map(stack_addr, stack_size, UC_PROT_READ | UC_PROT_WRITE) 57 | uc.mem_write( 58 | stack_start, 59 | struct.pack(pointer_size_to_fmt(process_controller.pointer_size), 60 | STACK_MAGIC_RET_ADDR)) 61 | uc.reg_write(sp_register, stack_start) 62 | uc.reg_write(bp_register, stack_start) 63 | 64 | # Setup FS/GSBASE 65 | setup_teb(uc, process_controller) 66 | 67 | # Setup hooks 68 | if expected_ret_addr is None: 69 | stop_on_ret_addr = STACK_MAGIC_RET_ADDR 70 | else: 71 | stop_on_ret_addr = expected_ret_addr 72 | uc.hook_add(UC_HOOK_MEM_UNMAPPED, 73 | _unicorn_hook_unmapped, 74 | user_data=process_controller) 75 | uc.hook_add(UC_HOOK_BLOCK, 76 | _unicorn_hook_block, 77 | user_data=(process_controller, stop_on_ret_addr)) 78 | 79 | uc.emu_start(wrapper_start_addr, wrapper_start_addr + 1024) 80 | 81 | # Read and return PC 82 | pc = uc.reg_read(result_register) 83 | assert isinstance(pc, int) 84 | 85 | return pc 86 | except UcError as e: 87 | LOG.debug("ERROR: %s", str(e)) 88 | pc = uc.reg_read(pc_register) 89 | assert isinstance(pc, int) 90 | sp = uc.reg_read(sp_register) 91 | assert isinstance(sp, int) 92 | bp = uc.reg_read(bp_register) 93 | assert isinstance(bp, int) 94 | LOG.debug("PC=%s", hex(pc)) 95 | LOG.debug("SP=%s", hex(sp)) 96 | LOG.debug("BP=%s", hex(bp)) 97 | return None 98 | 99 | 100 | def _setup_teb_x86(uc: Uc, process_info: ProcessController) -> None: 101 | MSG_IA32_FS_BASE = 0xC0000100 102 | teb_addr = 0xff100000 103 | peb_addr = 0xff200000 104 | # Map tables 105 | uc.mem_map(teb_addr, process_info.page_size, UC_PROT_READ | UC_PROT_WRITE) 106 | uc.mem_map(peb_addr, process_info.page_size, UC_PROT_READ | UC_PROT_WRITE) 107 | uc.mem_write(teb_addr + 0x18, struct.pack(pointer_size_to_fmt(4), 108 | teb_addr)) 109 | uc.mem_write(teb_addr + 0x30, struct.pack(pointer_size_to_fmt(4), 110 | peb_addr)) 111 | uc.reg_write(UC_X86_REG_MSR, (MSG_IA32_FS_BASE, teb_addr)) 112 | 113 | 114 | def _setup_teb_x64(uc: Uc, process_info: ProcessController) -> None: 115 | MSG_IA32_GS_BASE = 0xC0000101 116 | teb_addr = 0xff10000000000000 117 | peb_addr = 0xff20000000000000 118 | # Map tables 119 | uc.mem_map(teb_addr, process_info.page_size, UC_PROT_READ | UC_PROT_WRITE) 120 | uc.mem_map(peb_addr, process_info.page_size, UC_PROT_READ | UC_PROT_WRITE) 121 | uc.mem_write(teb_addr + 0x30, struct.pack(pointer_size_to_fmt(8), 122 | teb_addr)) 123 | uc.mem_write(teb_addr + 0x60, struct.pack(pointer_size_to_fmt(8), 124 | peb_addr)) 125 | uc.reg_write(UC_X86_REG_MSR, (MSG_IA32_GS_BASE, teb_addr)) 126 | 127 | 128 | def _unicorn_hook_unmapped(uc: Uc, _access: Any, address: int, _size: int, 129 | _value: int, 130 | process_controller: ProcessController) -> bool: 131 | LOG.debug("Unmapped memory at %s", hex(address)) 132 | if address == 0: 133 | return False 134 | 135 | page_size = process_controller.page_size 136 | aligned_addr = address - (address & (page_size - 1)) 137 | try: 138 | in_process_data = process_controller.read_process_memory( 139 | aligned_addr, page_size) 140 | uc.mem_map(aligned_addr, len(in_process_data), UC_PROT_ALL) 141 | uc.mem_write(aligned_addr, in_process_data) 142 | LOG.debug("Mapped %d bytes at %s", len(in_process_data), 143 | hex(aligned_addr)) 144 | return True 145 | except UcError as e: 146 | LOG.error("ERROR: %s", str(e)) 147 | return False 148 | except ReadProcessMemoryError as e: 149 | # Log this error as debug as it's expected to happen in cases where we 150 | # reach the end of the IAT. 151 | LOG.debug("ERROR: %s", str(e)) 152 | return False 153 | except Exception as e: 154 | LOG.error("ERROR: %s", str(e)) 155 | return False 156 | 157 | 158 | def _unicorn_hook_block(uc: Uc, address: int, _size: int, 159 | user_data: Tuple[ProcessController, int]) -> None: 160 | process_controller, stop_on_ret_addr = user_data 161 | ptr_size = process_controller.pointer_size 162 | arch = process_controller.architecture 163 | if arch == Architecture.X86_32: 164 | pc_register = UC_X86_REG_EIP 165 | sp_register = UC_X86_REG_ESP 166 | result_register = UC_X86_REG_EAX 167 | elif arch == Architecture.X86_64: 168 | pc_register = UC_X86_REG_RIP 169 | sp_register = UC_X86_REG_RSP 170 | result_register = UC_X86_REG_RAX 171 | else: 172 | raise NotImplementedError(f"Unsupported architecture: {arch}") 173 | 174 | exports_dict = process_controller.enumerate_exported_functions() 175 | if address in exports_dict: 176 | # Reached an export or returned to the call site 177 | sp = uc.reg_read(sp_register) 178 | assert isinstance(sp, int) 179 | ret_addr_data = uc.mem_read(sp, ptr_size) 180 | ret_addr = struct.unpack(pointer_size_to_fmt(ptr_size), 181 | ret_addr_data)[0] 182 | api_name = exports_dict[address]['name'] 183 | LOG.debug("Reached API '%s'", api_name) 184 | if ret_addr == stop_on_ret_addr or \ 185 | ret_addr == stop_on_ret_addr + 1 \ 186 | or ret_addr == STACK_MAGIC_RET_ADDR: 187 | # Most wrappers should end up here directly 188 | uc.reg_write(result_register, address) 189 | uc.emu_stop() 190 | return 191 | if _is_no_return_api(api_name): 192 | # Note: Dirty fix for ExitProcess-like wrappers on WinLicense 3.x 193 | LOG.debug("Reached noreturn API, stopping emulation") 194 | uc.reg_write(result_register, address) 195 | uc.emu_stop() 196 | return 197 | if _is_bogus_api(api_name): 198 | # Note: Starting with Themida 3.1.4.0, wrappers call some useless 199 | # APIs to fool emulation-based unwrappers 200 | LOG.debug("Reached bogus API call, skipping") 201 | # "Simulate" bogus call 202 | result, arg_count = _simulate_bogus_api(api_name) 203 | # Set result 204 | uc.reg_write(result_register, result) 205 | 206 | # Fix the stack 207 | if arch == Architecture.X86_32: 208 | # Pop return address and arguments from the stack 209 | uc.reg_write(sp_register, sp + ptr_size * (1 + arg_count)) 210 | elif arch == Architecture.X86_64: 211 | # Pop return address and arguments from the stack 212 | stack_arg_count = max(0, arg_count - 4) 213 | uc.reg_write(sp_register, 214 | sp + ptr_size * (1 + stack_arg_count)) 215 | 216 | # Set next address 217 | uc.reg_write(pc_register, ret_addr) 218 | return 219 | 220 | 221 | def _is_no_return_api(api_name: str) -> bool: 222 | NO_RETURN_APIS = ["ExitProcess", "FatalExit", "ExitThread"] 223 | return api_name in NO_RETURN_APIS 224 | 225 | 226 | def _is_bogus_api(api_name: str) -> bool: 227 | BOGUS_APIS = ["Sleep"] 228 | return api_name in BOGUS_APIS 229 | 230 | 231 | def _simulate_bogus_api(api_name: str) -> Tuple[int, int]: 232 | BOGUS_API_MAP: Dict[str, Tuple[int, int]] = { 233 | "Sleep": (0, 1), 234 | } 235 | return BOGUS_API_MAP[api_name] 236 | -------------------------------------------------------------------------------- /unlicense/frida_exec.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | from importlib import resources 4 | from pathlib import Path 5 | from typing import (List, Callable, Dict, Any, Optional) 6 | 7 | import frida 8 | import frida.core 9 | 10 | from .process_control import (ProcessController, Architecture, MemoryRange, 11 | QueryProcessMemoryError, ReadProcessMemoryError, 12 | WriteProcessMemoryError) 13 | 14 | LOG = logging.getLogger(__name__) 15 | # See issue #7: messages cannot exceed 128MiB 16 | MAX_DATA_CHUNK_SIZE = 64 * 1024 * 1024 17 | 18 | OepReachedCallback = Callable[[int, int, bool], None] 19 | 20 | 21 | class FridaProcessController(ProcessController): 22 | 23 | def __init__(self, pid: int, main_module_name: str, 24 | frida_session: frida.core.Session, 25 | frida_script: frida.core.Script): 26 | frida_rpc = frida_script.exports 27 | 28 | # Initialize ProcessController 29 | super().__init__(pid, main_module_name, 30 | _str_to_architecture(frida_rpc.get_architecture()), 31 | frida_rpc.get_pointer_size(), 32 | frida_rpc.get_page_size()) 33 | 34 | # Initialize FridaProcessController specifics 35 | self._frida_rpc = frida_rpc 36 | self._frida_session = frida_session 37 | self._exported_functions_cache: Optional[Dict[int, Dict[str, 38 | Any]]] = None 39 | 40 | def find_module_by_address(self, address: int) -> Optional[Dict[str, Any]]: 41 | value: Optional[Dict[str, 42 | Any]] = self._frida_rpc.find_module_by_address( 43 | hex(address)) 44 | return value 45 | 46 | def find_range_by_address( 47 | self, 48 | address: int, 49 | include_data: bool = False) -> Optional[MemoryRange]: 50 | value: Optional[Dict[str, 51 | Any]] = self._frida_rpc.find_range_by_address( 52 | hex(address)) 53 | if value is None: 54 | return None 55 | return self._frida_range_to_mem_range(value, include_data) 56 | 57 | def find_export_by_name(self, module_name: str, 58 | export_name: str) -> Optional[int]: 59 | export_address: Optional[str] = self._frida_rpc.find_export_by_name( 60 | module_name, export_name) 61 | if export_address is None: 62 | return None 63 | return int(export_address, 16) 64 | 65 | def enumerate_modules(self) -> List[str]: 66 | value: List[str] = self._frida_rpc.enumerate_modules() 67 | return value 68 | 69 | def enumerate_module_ranges( 70 | self, 71 | module_name: str, 72 | include_data: bool = False) -> List[MemoryRange]: 73 | 74 | def convert_range(dict_range: Dict[str, Any]) -> MemoryRange: 75 | return self._frida_range_to_mem_range(dict_range, include_data) 76 | 77 | value: List[Dict[str, Any]] = self._frida_rpc.enumerate_module_ranges( 78 | module_name) 79 | return list(map(convert_range, value)) 80 | 81 | def enumerate_exported_functions(self, 82 | update_cache: bool = False 83 | ) -> Dict[int, Dict[str, Any]]: 84 | if self._exported_functions_cache is None or update_cache: 85 | value: List[Dict[ 86 | str, Any]] = self._frida_rpc.enumerate_exported_functions( 87 | self.main_module_name) 88 | exports_dict = {int(e["address"], 16): e for e in value} 89 | self._exported_functions_cache = exports_dict 90 | return exports_dict 91 | return self._exported_functions_cache 92 | 93 | def allocate_process_memory(self, size: int, near: int) -> int: 94 | buffer_addr = self._frida_rpc.allocate_process_memory(size, near) 95 | return int(buffer_addr, 16) 96 | 97 | def query_memory_protection(self, address: int) -> str: 98 | try: 99 | protection: str = self._frida_rpc.query_memory_protection( 100 | hex(address)) 101 | return protection 102 | except frida.core.RPCException as rpc_exception: 103 | raise QueryProcessMemoryError from rpc_exception 104 | 105 | def set_memory_protection(self, address: int, size: int, 106 | protection: str) -> bool: 107 | result: bool = self._frida_rpc.set_memory_protection( 108 | hex(address), size, protection) 109 | return result 110 | 111 | def read_process_memory(self, address: int, size: int) -> bytes: 112 | read_data = bytearray(size) 113 | try: 114 | for offset in range(0, size, MAX_DATA_CHUNK_SIZE): 115 | chunk_size = min(MAX_DATA_CHUNK_SIZE, size - offset) 116 | data = self._frida_rpc.read_process_memory( 117 | hex(address + offset), chunk_size) 118 | if data is None: 119 | raise ReadProcessMemoryError( 120 | "read_process_memory failed (invalid parameters?)") 121 | read_data[offset:offset + chunk_size] = data 122 | return bytes(read_data) 123 | except frida.core.RPCException as rpc_exception: 124 | raise ReadProcessMemoryError from rpc_exception 125 | 126 | def write_process_memory(self, address: int, data: List[int]) -> None: 127 | try: 128 | self._frida_rpc.write_process_memory(hex(address), data) 129 | except frida.core.RPCException as rpc_exception: 130 | raise WriteProcessMemoryError from rpc_exception 131 | 132 | def terminate_process(self) -> None: 133 | self._frida_rpc.notify_dumping_finished() 134 | frida.kill(self.pid) 135 | self._frida_session.detach() 136 | 137 | def _frida_range_to_mem_range(self, dict_range: Dict[str, Any], 138 | with_data: bool) -> MemoryRange: 139 | base = int(dict_range["base"], 16) 140 | size = dict_range["size"] 141 | data = None 142 | if with_data: 143 | data = self.read_process_memory(base, size) 144 | return MemoryRange(base=base, 145 | size=size, 146 | protection=dict_range["protection"], 147 | data=data) 148 | 149 | 150 | def _str_to_architecture(frida_arch: str) -> Architecture: 151 | if frida_arch == "ia32": 152 | return Architecture.X86_32 153 | if frida_arch == "x64": 154 | return Architecture.X86_64 155 | raise ValueError 156 | 157 | 158 | def spawn_and_instrument( 159 | pe_path: Path, text_section_ranges: List[MemoryRange], 160 | notify_oep_reached: OepReachedCallback) -> ProcessController: 161 | pid: int 162 | if pe_path.suffix == ".dll": 163 | # Use `rundll32` to load the DLL 164 | rundll32_path = "C:\\Windows\\System32\\rundll32.exe" 165 | pid = frida.spawn( 166 | rundll32_path, 167 | [rundll32_path, str(pe_path.absolute()), "#0"]) 168 | else: 169 | pid = frida.spawn(str(pe_path)) 170 | 171 | main_module_name = pe_path.name 172 | session = frida.attach(pid) 173 | frida_js = resources.open_text("unlicense.resources", "frida.js").read() 174 | script = session.create_script(frida_js) 175 | on_message_callback = functools.partial(_frida_callback, 176 | notify_oep_reached) 177 | script.on('message', on_message_callback) 178 | script.load() 179 | 180 | frida_rpc = script.exports 181 | process_controller = FridaProcessController(pid, main_module_name, session, 182 | script) 183 | frida_rpc.setup_oep_tracing(pe_path.name, [[r.base, r.size] 184 | for r in text_section_ranges]) 185 | frida.resume(pid) 186 | 187 | return process_controller 188 | 189 | 190 | def _frida_callback(notify_oep_reached: OepReachedCallback, 191 | message: Dict[str, Any], _data: Any) -> None: 192 | msg_type = message['type'] 193 | if msg_type == 'error': 194 | LOG.error(message) 195 | LOG.error(message['stack']) 196 | return 197 | 198 | if msg_type == 'send': 199 | payload = message['payload'] 200 | event = payload.get('event', '') 201 | if event == 'oep_reached': 202 | # Note: We cannot use RPCs in `on_message` callbacks, so we have to 203 | # delay the actual dumping. 204 | notify_oep_reached(int(payload['BASE'], 205 | 16), int(payload['OEP'], 16), 206 | bool(payload['DOTNET'])) 207 | return 208 | 209 | raise NotImplementedError('Unknown message received') 210 | -------------------------------------------------------------------------------- /unlicense/function_hashing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Callable 3 | 4 | import xxhash # type: ignore 5 | from capstone import (Cs, CsInsn) # type: ignore 6 | from capstone.x86 import X86_OP_MEM, X86_OP_IMM # type: ignore 7 | from unicorn.x86_const import UC_X86_REG_FS, UC_X86_REG_GS, UC_X86_REG_ESP # type: ignore 8 | 9 | from .process_control import ProcessController 10 | 11 | LOG = logging.getLogger(__name__) 12 | EMPTY_FUNCTION_HASH = int(xxhash.xxh32().digest().hex(), 16) 13 | 14 | 15 | def compute_function_hash(md: Cs, function_start_addr: int, 16 | get_data: Callable[[int, int], bytes], 17 | process_controller: ProcessController) -> int: 18 | """ 19 | Compute a function's hash with `xxhash` by iterating over all the 20 | instructions and hashing them, without following JCC and `call` instructions 21 | and until a `ret` instruction or a `jmp REG/MEM` is reached. 22 | This function is used to generate function hashes that aren't modified by 23 | Themida's mutations on "inlined" imports. 24 | """ 25 | BB_MAX_SIZE = 0x600 26 | x = xxhash.xxh32() 27 | 28 | ret_reached = False 29 | basic_block_addr = function_start_addr 30 | prev_basic_block_addr = 0 31 | visited_addresses = set() 32 | while not ret_reached: 33 | if prev_basic_block_addr == basic_block_addr: 34 | LOG.debug("Not a new basic block, aborting") 35 | break 36 | prev_basic_block_addr = basic_block_addr 37 | instructions = md.disasm(get_data(basic_block_addr, BB_MAX_SIZE), 38 | basic_block_addr) 39 | 40 | for instruction in instructions: 41 | visited_addresses.add(instruction.address) 42 | if instruction.mnemonic == "ret": 43 | ret_reached = True 44 | _hash_instruction(x, instruction, process_controller) 45 | break 46 | elif instruction.mnemonic == "call": 47 | op = instruction.operands[0] 48 | if op.type == X86_OP_IMM and not _is_in_file_mapping( 49 | op.value.imm, process_controller): 50 | basic_block_addr = op.value.imm 51 | break 52 | elif instruction.mnemonic[0] == 'j': 53 | op = instruction.operands[0] 54 | if op.type == X86_OP_IMM: 55 | if instruction.mnemonic == "jmp": 56 | if op.value.imm in visited_addresses: 57 | LOG.debug("Loop detected, aborting") 58 | ret_reached = True 59 | _hash_instruction(x, instruction, 60 | process_controller) 61 | else: 62 | basic_block_addr = op.value.imm 63 | break 64 | else: 65 | ret_reached = True 66 | _hash_instruction(x, instruction, process_controller) 67 | break 68 | 69 | _hash_instruction(x, instruction, process_controller) 70 | 71 | return int(x.digest().hex(), 16) 72 | 73 | 74 | def _hash_instruction(x: xxhash.xxh32, instruction: CsInsn, 75 | process_controller: ProcessController) -> None: 76 | """ 77 | Hashing function for x86 instructions. It is empirically built to only hash 78 | instruction information that is not altered by Themida's code mutation. This 79 | function only hashes a few very common instructions. 80 | """ 81 | if instruction.mnemonic == "call": 82 | op = instruction.operands[0] 83 | if op.type == X86_OP_IMM and _is_in_file_mapping( 84 | op.value.imm, process_controller): 85 | val = f"{instruction.mnemonic},{op.value.imm:x}" 86 | x.update(val) 87 | elif op.type == X86_OP_MEM and _is_in_file_mapping( 88 | op.value.mem.disp, process_controller): 89 | val = f"{instruction.mnemonic}," \ 90 | f"{op.value.mem.segment:x}," \ 91 | f"{op.value.mem.base:x}," \ 92 | f"{op.value.mem.index:x}," \ 93 | f"{op.value.mem.disp:x}" 94 | x.update(val) 95 | elif instruction.mnemonic == "push": 96 | op = instruction.operands[0] 97 | if instruction.size == 2 and op.type == X86_OP_IMM: 98 | val = f"{instruction.mnemonic},{op.value.imm:x}" 99 | x.update(val) 100 | elif instruction.mnemonic == "mov": 101 | for i, op in enumerate(instruction.operands): 102 | if op.type == X86_OP_MEM: 103 | if op.value.mem.segment in [ 104 | UC_X86_REG_FS, UC_X86_REG_GS 105 | ] or (op.value.mem.base != UC_X86_REG_ESP 106 | and op.value.mem.disp != 0): 107 | val = f"{instruction.mnemonic},{i}," \ 108 | f"{op.value.mem.segment:x}," \ 109 | f"{op.value.mem.base:x}," \ 110 | f"{op.value.mem.index:x}," \ 111 | f"{op.value.mem.disp:x}" 112 | x.update(val) 113 | elif instruction.mnemonic == 'jmp': 114 | op = instruction.operands[0] 115 | if op.type == X86_OP_MEM and _is_in_file_mapping( 116 | op.value.mem.disp, process_controller): 117 | val = f"{instruction.mnemonic},{op.value.mem.disp:x}" 118 | x.update(val) 119 | elif instruction.mnemonic in ["and", "cmp", "xor"]: 120 | for i, op in enumerate(instruction.operands): 121 | if op.type == X86_OP_MEM: 122 | if op.value.mem.base != UC_X86_REG_ESP: 123 | val = f"{instruction.mnemonic},{i},{op.value.mem.base:x},{op.value.mem.disp:x}" 124 | x.update(val) 125 | elif instruction.mnemonic in ["shl", "shr"]: 126 | rop = instruction.operands[1] 127 | if rop.type == X86_OP_IMM: 128 | val = f"{instruction.mnemonic},{rop.value.imm:x}" 129 | x.update(val) 130 | elif instruction.mnemonic == "ret": 131 | if len(instruction.operands) == 0: 132 | val = f"{instruction.mnemonic}" 133 | else: 134 | op = instruction.operands[0] 135 | val = f"{instruction.mnemonic},{op.value.imm:x}" 136 | x.update(val) 137 | elif instruction.mnemonic in [ 138 | "fld", "fldz", "fstp", "fcompp", "div", "mul" 139 | ]: 140 | val = f"{instruction.mnemonic},{instruction.op_str}" 141 | x.update(val) 142 | 143 | 144 | def _is_in_file_mapping(address: int, 145 | process_controller: ProcessController) -> bool: 146 | """ 147 | Check if an address is located in a mapped file. 148 | """ 149 | # Filter out obviously invalid addresses without invoking an RPC 150 | if address < 4096: 151 | return False 152 | 153 | module = process_controller.find_module_by_address(address) 154 | return module is not None 155 | -------------------------------------------------------------------------------- /unlicense/imports.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from collections import defaultdict 4 | from typing import Any, Dict, List, Optional, Set, Tuple 5 | 6 | from capstone import Cs # type: ignore 7 | from capstone.x86 import X86_OP_MEM, X86_OP_IMM # type: ignore 8 | 9 | from .dump_utils import pointer_size_to_fmt 10 | from .process_control import Architecture, MemoryRange, ProcessController, ProcessControllerException 11 | 12 | LOG = logging.getLogger(__name__) 13 | 14 | # Describes a map of API addresses to every call site that should point to it 15 | # (instr_addr, call_size, instr_was_jmp) 16 | ImportCallSiteInfo = Tuple[int, int, bool] 17 | ImportToCallSiteDict = Dict[int, List[ImportCallSiteInfo]] 18 | # Describes a set of all found call sites 19 | # (instr_addr, call_size, instr_was_jmp, call_dest, ptr_addr) 20 | ImportWrapperInfo = Tuple[int, int, bool, int, Optional[int]] 21 | WrapperSet = Set[ImportWrapperInfo] 22 | 23 | 24 | def find_wrapped_imports( 25 | text_section_range: MemoryRange, 26 | exports_dict: Dict[int, Dict[str, Any]], # 27 | md: Cs, 28 | process_controller: ProcessController 29 | ) -> Tuple[ImportToCallSiteDict, WrapperSet]: 30 | """ 31 | Go through a code section and try to find wrapped (or not) import calls 32 | and jmps by disassembling instructions and using a few basic heuristics. 33 | """ 34 | arch = process_controller.architecture 35 | ptr_size = process_controller.pointer_size 36 | ptr_format = pointer_size_to_fmt(ptr_size) 37 | 38 | # Not supposed to be None 39 | assert text_section_range.data is not None 40 | text_section_data = text_section_range.data 41 | 42 | wrapper_set: WrapperSet = set() 43 | api_to_calls: ImportToCallSiteDict = defaultdict(list) 44 | i = 0 45 | while i < text_section_range.size: 46 | # Quick pre-filter 47 | if not _is_wrapped_thunk_jmp(text_section_data, i) and \ 48 | not _is_wrapped_call(text_section_data, i) and \ 49 | not _is_wrapped_tail_call(text_section_data, i) and \ 50 | not _is_indirect_call(text_section_data, i): 51 | i += 1 52 | continue 53 | 54 | # Check if the instruction is a jmp or should be replaced with a jmp. 55 | # This include checking for tail calls ("jmp X; int 3"). 56 | if text_section_data[i] == 0xE9 or \ 57 | text_section_data[i:i + 2] == bytes([0x90, 0xE9]) or \ 58 | text_section_data[i:i + 2] == bytes([0xFF, 0x25]) or \ 59 | _is_wrapped_tail_call(text_section_data, i): 60 | instr_was_jmp = True 61 | else: 62 | instr_was_jmp = False 63 | 64 | instr_addr = text_section_range.base + i 65 | instrs = md.disasm(text_section_data[i:i + 6], instr_addr) 66 | 67 | # Ensure the instructions are "call/jmp" or "nop; call/jmp" 68 | instruction = next(instrs) 69 | if instruction.mnemonic in ["call", "jmp"]: 70 | call_size = instruction.size 71 | op = instruction.operands[0] 72 | elif instruction.mnemonic == "nop": 73 | instruction = next(instrs) 74 | if instruction.mnemonic in ["call", "jmp"]: 75 | call_size = instruction.size 76 | op = instruction.operands[0] 77 | else: 78 | i += 1 79 | continue 80 | else: 81 | i += 1 82 | continue 83 | 84 | # Parse destination address or ignore in case of error 85 | if op.type == X86_OP_IMM: 86 | call_dest = op.value.imm 87 | ptr_addr = None 88 | elif op.type == X86_OP_MEM: 89 | try: 90 | if arch == Architecture.X86_32: 91 | ptr_addr = op.value.mem.disp 92 | data = process_controller.read_process_memory( 93 | ptr_addr, ptr_size) 94 | call_dest = struct.unpack(ptr_format, data)[0] 95 | elif arch == Architecture.X86_64: 96 | ptr_addr = instruction.address + instruction.size + op.value.mem.disp 97 | data = process_controller.read_process_memory( 98 | ptr_addr, ptr_size) 99 | call_dest = struct.unpack(ptr_format, data)[0] 100 | else: 101 | raise NotImplementedError( 102 | f"Unsupported architecture: {arch}") 103 | except ProcessControllerException: 104 | i += 1 105 | continue 106 | else: 107 | i += 1 108 | continue 109 | 110 | # Verify that the destination is outside of the .text section 111 | if not text_section_range.contains(call_dest): 112 | # Not wrapped, add it to list of "resolved wrappers" 113 | if call_dest in exports_dict: 114 | api_to_calls[call_dest].append( 115 | (instr_addr, call_size, instr_was_jmp)) 116 | i += call_size + 1 117 | continue 118 | # Wrapped, add it to set of wrappers to resolve 119 | if _is_in_executable_range(call_dest, process_controller): 120 | wrapper_set.add((instr_addr, call_size, instr_was_jmp, 121 | call_dest, ptr_addr)) 122 | i += call_size + 1 123 | continue 124 | i += 1 125 | 126 | return api_to_calls, wrapper_set 127 | 128 | 129 | def _is_indirect_call(code_section_data: bytes, offset: int) -> bool: 130 | """ 131 | Check if the instruction at `offset` is an `FF15` call. 132 | """ 133 | return code_section_data[offset:offset + 2] == bytes([0xFF, 0x15]) 134 | 135 | 136 | def _is_wrapped_thunk_jmp(code_section_data: bytes, offset: int) -> bool: 137 | """ 138 | Check if the instruction at `offset` is a wrapped jmp from a thunk table. 139 | """ 140 | if offset > len(code_section_data) - 6: 141 | return False 142 | 143 | is_e9_jmp = code_section_data[offset] == 0xE9 144 | # Dirty trick to catch last elements of thunk tables 145 | if offset > 6: 146 | jmp_behind = code_section_data[offset - 5] == 0xE9 or \ 147 | code_section_data[offset - 6] == 0xE9 148 | else: 149 | jmp_behind = False 150 | 151 | return (is_e9_jmp and code_section_data[offset + 6] in [0xE9, 0x90]) or \ 152 | (is_e9_jmp and code_section_data[offset + 5] in [0xCC, 0x90, 0xE9]) or \ 153 | (code_section_data[offset:offset + 2] == bytes([0x90, 0xE9])) or \ 154 | (is_e9_jmp and jmp_behind) or \ 155 | (code_section_data[offset:offset + 2] == bytes([0xFF, 0x25]) and code_section_data[offset + 6] in [0x8B, 0xC0]) # Turbo delphi-style tuhnk 156 | 157 | 158 | def _is_wrapped_call(code_section_data: bytes, offset: int) -> bool: 159 | """ 160 | Check if the instruction at `offset` is a wrapped import call. Themida 2.x 161 | replaces `FF15` calls with `E8` calls followed or preceded by a `nop`. 162 | """ 163 | return (code_section_data[offset] == 0xE8 and code_section_data[offset + 5] == 0x90) or \ 164 | (code_section_data[offset:offset + 2] == bytes([0x90, 0xE8])) 165 | 166 | 167 | def _is_wrapped_tail_call(code_section_data: bytes, offset: int) -> bool: 168 | """ 169 | Check if the instruction at `offset` is a tail call (and thus should be 170 | transformed into a `jmp`). 171 | """ 172 | is_call = code_section_data[offset] == 0xE8 173 | return (is_call and code_section_data[offset + 5] == 0xCC) or \ 174 | (is_call and code_section_data[offset + 6] == 0xCC) or \ 175 | (code_section_data[offset:offset + 2] == bytes([0x90, 0xE8]) 176 | and code_section_data[offset + 6] == 0xCC) or ( 177 | code_section_data[offset:offset + 2] == bytes([0xFF, 0x25]) 178 | and code_section_data[offset + 6] == 0xCC) 179 | 180 | 181 | def _is_in_executable_range(address: int, 182 | process_controller: ProcessController) -> bool: 183 | """ 184 | Check if an address is located in an executable memory range. 185 | """ 186 | mem_range = process_controller.find_range_by_address(address) 187 | if mem_range is None: 188 | return False 189 | 190 | protection: str = mem_range.protection[2] 191 | return protection == 'x' 192 | -------------------------------------------------------------------------------- /unlicense/lief_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Iterator 2 | import lief 3 | 4 | 5 | # Note(ergrelet): this wrapper is a workaround for some bug in LIEF. We're not 6 | # iterating directly on `binary.sections` because it seems LIEF crashes for some 7 | # unkown reasons on certain executables 8 | def lief_pe_sections(binary: lief.PE.Binary) -> Iterator[lief.PE.Section]: 9 | num_of_sections = len(binary.sections) 10 | for i in range(num_of_sections): 11 | yield binary.sections[i] 12 | 13 | 14 | # Note(ergrelet): same as above 15 | def lief_pe_data_directories( 16 | binary: lief.PE.Binary) -> Iterator[lief.PE.DataDirectory]: 17 | num_of_data_dirs = len(binary.data_directories) 18 | for i in range(num_of_data_dirs): 19 | yield binary.data_directories[i] -------------------------------------------------------------------------------- /unlicense/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import lief 4 | 5 | 6 | def setup_logger(logger: logging.Logger, verbose: bool) -> None: 7 | lief.logging.disable() 8 | if verbose: 9 | log_level = logging.DEBUG 10 | else: 11 | log_level = logging.INFO 12 | 13 | logger.setLevel(log_level) 14 | 15 | # Create a console handler with a higher log level 16 | stream_handler = logging.StreamHandler() 17 | stream_handler.setLevel(log_level) 18 | 19 | stream_handler.setFormatter(CustomFormatter()) 20 | 21 | logger.addHandler(stream_handler) 22 | 23 | 24 | class CustomFormatter(logging.Formatter): 25 | 26 | grey = "\x1b[38;20m" 27 | green = "\x1b[1;32m" 28 | yellow = "\x1b[33;20m" 29 | red = "\x1b[31;20m" 30 | bold_red = "\x1b[31;1m" 31 | reset = "\x1b[0m" 32 | format_problem_str = "%(levelname)s - %(message)s" 33 | 34 | FORMATS = { 35 | logging.DEBUG: grey + "%(levelname)s - %(message)s" + reset, 36 | logging.INFO: green + "%(levelname)s" + reset + " - %(message)s", 37 | logging.WARNING: yellow + format_problem_str + reset, 38 | logging.ERROR: red + format_problem_str + reset, 39 | logging.CRITICAL: bold_red + format_problem_str + reset 40 | } 41 | 42 | def format(self, record: logging.LogRecord) -> str: 43 | log_fmt = self.FORMATS.get(record.levelno) 44 | formatter = logging.Formatter(log_fmt) 45 | return formatter.format(record) 46 | -------------------------------------------------------------------------------- /unlicense/process_control.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import enum 3 | from typing import Dict, List, Any, Optional 4 | 5 | 6 | class Architecture(enum.Enum): 7 | X86_32 = 0 8 | X86_64 = 1 9 | 10 | 11 | class MemoryRange: 12 | 13 | def __init__(self, 14 | base: int, 15 | size: int, 16 | protection: str, 17 | data: Optional[bytes] = None): 18 | self.base = base 19 | self.size = size 20 | self.protection = protection 21 | self.data = data 22 | 23 | def __str__(self) -> str: 24 | return f"(base=0x{self.base:x}, size=0x{self.size:x}, prot={self.protection})" 25 | 26 | def contains(self, addr: int) -> bool: 27 | return self.base <= addr < self.base + self.size 28 | 29 | 30 | class ProcessController(abc.ABC): 31 | 32 | def __init__(self, pid: int, main_module_name: str, 33 | architecture: Architecture, pointer_size: int, 34 | page_size: int): 35 | self.pid = pid 36 | self.main_module_name = main_module_name 37 | self.architecture = architecture 38 | self.pointer_size = pointer_size 39 | self.page_size = page_size 40 | self._main_module_ranges: Optional[List[MemoryRange]] = None 41 | 42 | @abc.abstractmethod 43 | def find_module_by_address(self, address: int) -> Optional[Dict[str, Any]]: 44 | raise NotImplementedError 45 | 46 | @abc.abstractmethod 47 | def find_range_by_address( 48 | self, 49 | address: int, 50 | include_data: bool = False) -> Optional[MemoryRange]: 51 | raise NotImplementedError 52 | 53 | @abc.abstractmethod 54 | def find_export_by_name(self, module_name: str, 55 | export_name: str) -> Optional[int]: 56 | raise NotImplementedError 57 | 58 | @abc.abstractmethod 59 | def enumerate_modules(self) -> List[str]: 60 | raise NotImplementedError 61 | 62 | @abc.abstractmethod 63 | def enumerate_module_ranges( 64 | self, 65 | module_name: str, 66 | include_data: bool = False) -> List[MemoryRange]: 67 | raise NotImplementedError 68 | 69 | @abc.abstractmethod 70 | def enumerate_exported_functions(self, 71 | update_cache: bool = False 72 | ) -> Dict[int, Dict[str, Any]]: 73 | raise NotImplementedError 74 | 75 | @abc.abstractmethod 76 | def allocate_process_memory(self, size: int, near: int) -> int: 77 | raise NotImplementedError 78 | 79 | @abc.abstractmethod 80 | def query_memory_protection(self, address: int) -> str: 81 | raise NotImplementedError 82 | 83 | @abc.abstractmethod 84 | def set_memory_protection(self, address: int, size: int, 85 | protection: str) -> bool: 86 | raise NotImplementedError 87 | 88 | @abc.abstractmethod 89 | def read_process_memory(self, address: int, size: int) -> bytes: 90 | raise NotImplementedError 91 | 92 | @abc.abstractmethod 93 | def write_process_memory(self, address: int, data: List[int]) -> None: 94 | raise NotImplementedError 95 | 96 | @abc.abstractmethod 97 | def terminate_process(self) -> None: 98 | raise NotImplementedError 99 | 100 | @property 101 | def main_module_ranges(self) -> List[MemoryRange]: 102 | # Lazily load ranges on first request (after OEP has been reached) 103 | if self._main_module_ranges is None: 104 | self._main_module_ranges = self.enumerate_module_ranges( 105 | self.main_module_name, True) 106 | return self._main_module_ranges 107 | 108 | def clear_cached_data(self) -> None: 109 | """ 110 | Can be used to better control memory consumption. This is useful for 111 | 32-bit interpreters. 112 | """ 113 | self._main_module_ranges = None 114 | 115 | 116 | class ProcessControllerException(Exception): 117 | pass 118 | 119 | 120 | class QueryProcessMemoryError(ProcessControllerException): 121 | pass 122 | 123 | 124 | class ReadProcessMemoryError(ProcessControllerException): 125 | pass 126 | 127 | 128 | class WriteProcessMemoryError(ProcessControllerException): 129 | pass 130 | -------------------------------------------------------------------------------- /unlicense/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ergrelet/unlicense/95c8dc6a4324ff70c04bceda8bd166be6c5f0c73/unlicense/resources/__init__.py -------------------------------------------------------------------------------- /unlicense/resources/frida.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const green = "\x1b[1;36m" 4 | const reset = "\x1b[0m" 5 | 6 | let allocatedBuffers = []; 7 | let originalPageProtections = new Map(); 8 | let oepTracingListeners = []; 9 | let oepReached = false; 10 | 11 | // DLLs-related 12 | let skipDllOepInstr32 = null; 13 | let skipDllOepInstr64 = null; 14 | let dllOepCandidate = null; 15 | 16 | // TLS-related 17 | let skipTlsInstr32 = null; 18 | let skipTlsInstr64 = null; 19 | let tlsCallbackCount = 0; 20 | 21 | function log(message) { 22 | console.log(`${green}frida-agent${reset}: ${message}`); 23 | } 24 | 25 | function initializeTrampolines() { 26 | const instructionsBytes = new Uint8Array([ 27 | 0xC3, // ret 28 | 0xC2, 0x0C, 0x00, // ret 0x0C 29 | 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3, // mov eax, 1; ret 30 | 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC2, 0x0C, 0x00 // mov eax, 1; ret 0x0C 31 | ]); 32 | 33 | let bufferPointer = Memory.alloc(instructionsBytes.length); 34 | Memory.protect(bufferPointer, instructionsBytes.length, 'rwx'); 35 | bufferPointer.writeByteArray(instructionsBytes.buffer); 36 | 37 | skipTlsInstr64 = bufferPointer; 38 | skipTlsInstr32 = bufferPointer.add(0x1); 39 | skipDllOepInstr64 = bufferPointer.add(0x4); 40 | skipDllOepInstr32 = bufferPointer.add(0xA); 41 | } 42 | 43 | function rangeContainsAddress(range, address) { 44 | const rangeStart = range.base; 45 | const rangeEnd = range.base.add(range.size); 46 | return rangeStart.compare(address) <= 0 && rangeEnd.compare(address) > 0; 47 | } 48 | 49 | function notifyOepFound(dumpedModule, oepCandidate) { 50 | oepReached = true; 51 | 52 | // Make OEP ranges readable and writeable during the dumping phase 53 | setOepRangesProtection('rw-'); 54 | // Remove hooks used to find the OEP 55 | removeOepTracingHooks(); 56 | 57 | let isDotNetInitialized = isDotNetProcess(); 58 | send({ 'event': 'oep_reached', 'OEP': oepCandidate, 'BASE': dumpedModule.base, 'DOTNET': isDotNetInitialized }) 59 | let sync_op = recv('block_on_oep', function (_value) { }); 60 | // Note: never returns 61 | sync_op.wait(); 62 | } 63 | 64 | function isDotNetProcess() { 65 | return Process.findModuleByName("clr.dll") != null; 66 | } 67 | 68 | function makeOepRangesInaccessible(dumpedModule, expectedOepRanges) { 69 | // Ensure potential OEP ranges are not accessible 70 | expectedOepRanges.forEach((oepRange) => { 71 | const sectionStart = dumpedModule.base.add(oepRange[0]); 72 | const expectedSectionSize = oepRange[1]; 73 | Memory.protect(sectionStart, expectedSectionSize, '---'); 74 | originalPageProtections.set(sectionStart.toString(), expectedSectionSize); 75 | }); 76 | } 77 | 78 | function setOepRangesProtection(protection) { 79 | // Set pages' protection 80 | originalPageProtections.forEach((size, address_str, _map) => { 81 | Memory.protect(ptr(address_str), size, protection); 82 | }); 83 | } 84 | 85 | function removeOepTracingHooks() { 86 | oepTracingListeners.forEach(listener => { 87 | listener.detach(); 88 | }) 89 | oepTracingListeners = []; 90 | } 91 | 92 | function registerExceptionHandler(dumpedModule, expectedOepRanges, moduleIsDll) { 93 | // Register an exception handler that'll detect the OEP 94 | Process.setExceptionHandler(exp => { 95 | let oepCandidate = exp.context.pc; 96 | let threadId = Process.getCurrentThreadId(); 97 | 98 | if (exp.memory != null) { 99 | // Weird case where executing code actually only triggers a "read" 100 | // access violation on inaccessible pages. This can happen on some 101 | // 32-bit executables. 102 | if (exp.memory.operation == "read" && exp.memory.address.equals(exp.context.pc)) { 103 | // If we're in a TLS callback, the first argument is the 104 | // module's base address 105 | if (!moduleIsDll && isTlsCallback(exp.context, dumpedModule)) { 106 | log(`TLS callback #${tlsCallbackCount} detected (at ${exp.context.pc}), skipping ...`); 107 | tlsCallbackCount++; 108 | 109 | // Modify PC to skip the callback's execution and return 110 | skipTlsCallback(exp.context); 111 | return true; 112 | } 113 | 114 | log(`OEP found (thread #${threadId}): ${oepCandidate}`); 115 | // Report the potential OEP 116 | notifyOepFound(dumpedModule, oepCandidate); 117 | } 118 | 119 | // If the access violation is not an execution, "allow" the operation. 120 | // Note: Pages will be reprotected on the next call to 121 | // `NtProtectVirtualMemory`. 122 | if (exp.memory.operation != "execute") { 123 | Memory.protect(exp.memory.address, Process.pageSize, "rw-"); 124 | return true; 125 | } 126 | } 127 | 128 | let expectionHandled = false; 129 | expectedOepRanges.forEach((oepRange) => { 130 | const sectionStart = dumpedModule.base.add(oepRange[0]); 131 | const sectionSize = oepRange[1]; 132 | const sectionRange = { base: sectionStart, size: sectionSize }; 133 | 134 | if (rangeContainsAddress(sectionRange, oepCandidate)) { 135 | // If we're in a TLS callback, the first argument is the 136 | // module's base address 137 | if (!moduleIsDll && isTlsCallback(exp.context, dumpedModule)) { 138 | log(`TLS callback #${tlsCallbackCount} detected (at ${exp.context.pc}), skipping ...`); 139 | tlsCallbackCount++; 140 | 141 | // Modify PC to skip the callback's execution and return 142 | skipTlsCallback(exp.context); 143 | expectionHandled = true; 144 | return; 145 | } 146 | 147 | if (moduleIsDll) { 148 | // Save the potential OEP and and skip `DllMain` (`DLL_PROCESS_ATTACH`). 149 | // Note: When dumping DLLs we have to release the loader 150 | // lock before starting to dump. 151 | // Other threads might call `DllMain` with the `DLL_THREAD_ATTACH` 152 | // or `DLL_THREAD_DETACH` reasons later so we also skip the `DllMain` 153 | // even after the OEP has been reached. 154 | if (!oepReached) { 155 | log(`OEP found (thread #${threadId}): ${oepCandidate}`); 156 | dllOepCandidate = oepCandidate; 157 | } 158 | 159 | skipDllEntryPoint(exp.context); 160 | expectionHandled = true; 161 | return; 162 | } 163 | 164 | // Report the potential OEP 165 | log(`OEP found (thread #${threadId}): ${oepCandidate}`); 166 | notifyOepFound(dumpedModule, oepCandidate); 167 | } 168 | }); 169 | 170 | return expectionHandled; 171 | }); 172 | log("Exception handler registered"); 173 | } 174 | 175 | function isTlsCallback(exceptionCtx, dumpedModule) { 176 | if (Process.arch == "x64") { 177 | // If we're in a TLS callback, the first argument is the 178 | // module's base address 179 | let moduleBase = exceptionCtx.rcx; 180 | if (!moduleBase.equals(dumpedModule.base)) { 181 | return false; 182 | } 183 | // If we're in a TLS callback, the second argument is the 184 | // reason (from 0 to 3). 185 | let reason = exceptionCtx.rdx; 186 | if (reason.compare(ptr(4)) > 0) { 187 | return false; 188 | } 189 | } 190 | else if (Process.arch == "ia32") { 191 | let sp = exceptionCtx.sp; 192 | 193 | let moduleBase = sp.add(0x4).readPointer(); 194 | if (!moduleBase.equals(dumpedModule.base)) { 195 | return false; 196 | } 197 | let reason = sp.add(0x8).readPointer(); 198 | if (reason.compare(ptr(4)) > 0) { 199 | return false; 200 | } 201 | } else { 202 | return false; 203 | } 204 | 205 | return true; 206 | } 207 | 208 | function skipTlsCallback(exceptionCtx) { 209 | if (Process.arch == "x64") { 210 | // Redirect to a `ret` instruction 211 | exceptionCtx.rip = skipTlsInstr64; 212 | } 213 | else if (Process.arch == "ia32") { 214 | // Redirect to a `ret 0xC` instruction 215 | exceptionCtx.eip = skipTlsInstr32; 216 | } 217 | } 218 | 219 | function skipDllEntryPoint(exceptionCtx) { 220 | if (Process.arch == "x64") { 221 | // Redirect to a `mov eax, 1; ret` instructions 222 | exceptionCtx.rip = skipDllOepInstr64; 223 | } 224 | else if (Process.arch == "ia32") { 225 | // Redirect to a `mov eax, 1; ret 0xC` instructions 226 | exceptionCtx.eip = skipDllOepInstr32; 227 | } 228 | } 229 | 230 | // Define available RPCs 231 | rpc.exports = { 232 | setupOepTracing: function (moduleName, expectedOepRanges) { 233 | log(`Setting up OEP tracing for "${moduleName}"`); 234 | 235 | let targetIsDll = moduleName.endsWith(".dll"); 236 | let dumpedModule = null; 237 | 238 | initializeTrampolines(); 239 | 240 | // If the target isn't a DLL, it should be loaded already 241 | if (!targetIsDll) { 242 | dumpedModule = Process.findModuleByName(moduleName); 243 | } 244 | 245 | // Hook `ntdll.LdrLoadDll` on exit to get called at a point where the 246 | // loader lock is released. Needed to unpack (32-bit) DLLs. 247 | const loadDll = Module.findExportByName('ntdll', 'LdrLoadDll'); 248 | const loadDllListener = Interceptor.attach(loadDll, { 249 | onLeave: function (_args) { 250 | // If `dllOepCandidate` is set, proceed with the dumping 251 | // but only once (for our target). Then let other executions go 252 | // through as it's not DLLs we're intersted in. 253 | if (dllOepCandidate != null && !oepReached) { 254 | notifyOepFound(dumpedModule, dllOepCandidate); 255 | } 256 | } 257 | }); 258 | oepTracingListeners.push(loadDllListener); 259 | 260 | let exceptionHandlerRegistered = false; 261 | const ntProtectVirtualMemory = Module.findExportByName('ntdll', 'NtProtectVirtualMemory'); 262 | if (ntProtectVirtualMemory != null) { 263 | const ntProtectVirtualMemoryListener = Interceptor.attach(ntProtectVirtualMemory, { 264 | onEnter: function (args) { 265 | let addr = args[1].readPointer(); 266 | if (dumpedModule != null && addr.equals(dumpedModule.base)) { 267 | // Reset potential OEP ranges to not accessible to 268 | // (hopefully) catch the entry point next time. 269 | makeOepRangesInaccessible(dumpedModule, expectedOepRanges); 270 | if (!exceptionHandlerRegistered) { 271 | registerExceptionHandler(dumpedModule, expectedOepRanges, targetIsDll); 272 | exceptionHandlerRegistered = true; 273 | } 274 | } 275 | } 276 | }); 277 | oepTracingListeners.push(ntProtectVirtualMemoryListener); 278 | } 279 | 280 | // Hook `ntdll.RtlActivateActivationContextUnsafeFast` on exit as a mean 281 | // to get called after new PE images are loaded and before their entry 282 | // point is called. Needed to unpack DLLs. 283 | let initializeFusionHooked = false; 284 | const activateActivationContext = Module.findExportByName('ntdll', 'RtlActivateActivationContextUnsafeFast'); 285 | const activateActivationContextListener = Interceptor.attach(activateActivationContext, { 286 | onLeave: function (_args) { 287 | if (dumpedModule == null) { 288 | dumpedModule = Process.findModuleByName(moduleName); 289 | if (dumpedModule == null) { 290 | // Module isn't loaded yet 291 | return; 292 | } 293 | log(`Target module has been loaded (thread #${this.threadId}) ...`); 294 | } 295 | // After this, the target module is loaded. 296 | 297 | if (targetIsDll) { 298 | if (!exceptionHandlerRegistered) { 299 | makeOepRangesInaccessible(dumpedModule, expectedOepRanges); 300 | registerExceptionHandler(dumpedModule, expectedOepRanges, targetIsDll); 301 | exceptionHandlerRegistered = true; 302 | } 303 | } 304 | 305 | // Hook `clr.InitializeFusion` if present. 306 | // This is used to detect a good point during the CLR's 307 | // initialization, to dump .NET EXE assemblies 308 | const initializeFusion = Module.findExportByName('clr', 'InitializeFusion'); 309 | if (initializeFusion != null && !initializeFusionHooked) { 310 | const initializeFusionListener = Interceptor.attach(initializeFusion, { 311 | onEnter: function (_args) { 312 | log(`.NET assembly loaded (thread #${this.threadId})`); 313 | notifyOepFound(dumpedModule, '0'); 314 | } 315 | }); 316 | oepTracingListeners.push(initializeFusionListener); 317 | initializeFusionHooked = true; 318 | } 319 | } 320 | }); 321 | oepTracingListeners.push(activateActivationContextListener); 322 | }, 323 | notifyDumpingFinished: function () { 324 | // Make OEP executable again once dumping is finished 325 | setOepRangesProtection('rwx'); 326 | }, 327 | getArchitecture: function () { return Process.arch; }, 328 | getPointerSize: function () { return Process.pointerSize; }, 329 | getPageSize: function () { return Process.pageSize; }, 330 | findModuleByAddress: function (address) { 331 | return Process.findModuleByAddress(ptr(address)); 332 | }, 333 | findRangeByAddress: function (address) { 334 | return Process.findRangeByAddress(ptr(address)); 335 | }, 336 | findExportByName: function (moduleName, exportName) { 337 | const mod = Process.findModuleByName(moduleName); 338 | if (mod == null) { 339 | return null; 340 | } 341 | 342 | return mod.findExportByName(exportName); 343 | }, 344 | enumerateModules: function () { 345 | const modules = Process.enumerateModules(); 346 | const moduleNames = modules.map(module => { 347 | return module.name; 348 | }); 349 | return moduleNames; 350 | }, 351 | enumerateModuleRanges: function (moduleName) { 352 | let ranges = Process.enumerateRangesSync("r--"); 353 | return ranges.filter(range => { 354 | const module = Process.findModuleByAddress(range.base); 355 | return module != null && module.name.toUpperCase() == moduleName.toUpperCase(); 356 | }); 357 | }, 358 | enumerateExportedFunctions: function (excludedModuleName) { 359 | const modules = Process.enumerateModules(); 360 | const exports = modules.reduce((acc, m) => { 361 | if (m.name != excludedModuleName) { 362 | m.enumerateExports().forEach(e => { 363 | if (e.type == "function" && e.hasOwnProperty('address')) { 364 | acc.push(e); 365 | } 366 | }); 367 | } 368 | 369 | return acc; 370 | }, []); 371 | return exports; 372 | }, 373 | allocateProcessMemory: function (size, near) { 374 | const sizeRounded = size + (Process.pageSize - size % Process.pageSize); 375 | const addr = Memory.alloc(sizeRounded, { near: ptr(near), maxDistance: 0xff000000 }); 376 | allocatedBuffers.push(addr) 377 | return addr; 378 | }, 379 | queryMemoryProtection: function (address) { 380 | return Process.getRangeByAddress(ptr(address))['protection']; 381 | }, 382 | setMemoryProtection: function (address, size, protection) { 383 | return Memory.protect(ptr(address), size, protection); 384 | }, 385 | readProcessMemory: function (address, size) { 386 | return Memory.readByteArray(ptr(address), size); 387 | }, 388 | writeProcessMemory: function (address, bytes) { 389 | return Memory.writeByteArray(ptr(address), bytes); 390 | } 391 | }; 392 | -------------------------------------------------------------------------------- /unlicense/version_detection.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import logging 3 | 4 | import lief 5 | 6 | from unlicense.lief_utils import lief_pe_sections 7 | 8 | THEMIDA2_IMPORTED_MODS = ["kernel32.dll", "comctl32.dll"] 9 | THEMIDA2_IMPORTED_FUNCS = ["lstrcpy", "InitCommonControls"] 10 | LOG = logging.getLogger(__name__) 11 | 12 | 13 | def detect_winlicense_version(pe_file_path: str) -> Optional[int]: 14 | binary = lief.PE.parse(pe_file_path) 15 | if binary is None: 16 | LOG.error("Failed to parse PE '%s'", pe_file_path) 17 | return None 18 | 19 | # Version 3.x 20 | # Note: The '.boot' section might not always be present, so we do not check 21 | # for it. 22 | try: 23 | if binary.get_section(".themida") is not None or \ 24 | binary.get_section(".winlice") is not None: 25 | return 3 26 | except lief.not_found: # type: ignore 27 | # Not Themida 3.x 28 | pass 29 | 30 | # Version 2.x 31 | if len(binary.imports) == 2 and len(binary.imported_functions) == 2: 32 | if binary.imports[0].name in THEMIDA2_IMPORTED_MODS and \ 33 | binary.imports[1].name in THEMIDA2_IMPORTED_MODS and \ 34 | binary.imported_functions[0].name in THEMIDA2_IMPORTED_FUNCS and \ 35 | binary.imported_functions[1].name in THEMIDA2_IMPORTED_FUNCS: 36 | return 2 37 | 38 | # These x86 instructions are always present at the beginning of a section 39 | # in Themida/WinLicense 2.x 40 | instr_patterns = [[ 41 | 0x56, 0x50, 0x53, 0xE8, 0x01, 0x00, 0x00, 0x00, 0xCC, 0x58 42 | ], [ 43 | 0x83, 0xEC, 0x04, 0x50, 0x53, 0xE8, 0x01, 0x00, 0x00, 0x00, 0xCC, 0x58 44 | ]] 45 | 46 | for section in lief_pe_sections(binary): 47 | for pattern in instr_patterns: 48 | if pattern == list(section.content[:len(pattern)]): 49 | return 2 50 | 51 | # Failed to automatically detect version 52 | return None 53 | -------------------------------------------------------------------------------- /unlicense/winlicense2.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from typing import Dict, Tuple, Any, Optional 4 | 5 | from capstone import ( # type: ignore 6 | Cs, CS_ARCH_X86, CS_MODE_32, CS_MODE_64) 7 | 8 | from .imports import ImportToCallSiteDict, WrapperSet, find_wrapped_imports 9 | from .dump_utils import dump_pe, pointer_size_to_fmt 10 | from .emulation import resolve_wrapped_api 11 | from .function_hashing import compute_function_hash, EMPTY_FUNCTION_HASH 12 | from .process_control import (ProcessController, Architecture, MemoryRange, 13 | ReadProcessMemoryError) 14 | 15 | LOG = logging.getLogger(__name__) 16 | 17 | 18 | def fix_and_dump_pe(process_controller: ProcessController, pe_file_path: str, 19 | image_base: int, oep: int, 20 | text_section_range: MemoryRange) -> None: 21 | """ 22 | Main dumping routine for Themida/WinLicense 2.x. 23 | """ 24 | # Convert RVA range to VA range 25 | section_virtual_addr = image_base + text_section_range.base 26 | text_section_range = MemoryRange( 27 | section_virtual_addr, text_section_range.size, "r-x", 28 | process_controller.read_process_memory(section_virtual_addr, 29 | text_section_range.size)) 30 | assert text_section_range.data is not None 31 | LOG.debug(".text section: %s", str(text_section_range)) 32 | 33 | arch = process_controller.architecture 34 | exports_dict = process_controller.enumerate_exported_functions() 35 | 36 | # Instanciate the disassembler 37 | if arch == Architecture.X86_32: 38 | cs_mode = CS_MODE_32 39 | elif arch == Architecture.X86_64: 40 | cs_mode = CS_MODE_64 41 | else: 42 | raise NotImplementedError(f"Unsupported architecture: {arch}") 43 | md = Cs(CS_ARCH_X86, cs_mode) 44 | md.detail = True 45 | 46 | LOG.info("Looking for wrapped imports ...") 47 | api_to_calls, wrapper_set = find_wrapped_imports(text_section_range, 48 | exports_dict, md, 49 | process_controller) 50 | 51 | LOG.info("Potential import wrappers found: %d", len(wrapper_set)) 52 | export_hashes = None 53 | # Hash-matching strategy is only needed for 32-bit PEs 54 | if arch == Architecture.X86_32: 55 | LOG.info("Generating exports' hashes, this might take some time ...") 56 | export_hashes = _generate_export_hashes(md, exports_dict, 57 | process_controller) 58 | 59 | LOG.info("Resolving imports ...") 60 | _resolve_imports(api_to_calls, wrapper_set, export_hashes, md, 61 | process_controller) 62 | LOG.info("Imports resolved: %d", len(api_to_calls)) 63 | 64 | iat_addr, iat_size = _generate_new_iat_in_process(api_to_calls, 65 | text_section_range.base, 66 | process_controller) 67 | LOG.info("Generated the fake IAT at %s, size=%s", hex(iat_addr), 68 | hex(iat_size)) 69 | 70 | # Ensure the range is writable 71 | process_controller.set_memory_protection(text_section_range.base, 72 | text_section_range.size, "rwx") 73 | # Replace detected references to wrappers or imports 74 | LOG.info("Patching call and jmp sites ...") 75 | _fix_import_references_in_process(api_to_calls, iat_addr, 76 | process_controller) 77 | # Restore memory protection to RX 78 | process_controller.set_memory_protection(text_section_range.base, 79 | text_section_range.size, "r-x") 80 | 81 | LOG.info("Dumping PE with OEP=%s ...", hex(oep)) 82 | dump_pe(process_controller, pe_file_path, image_base, oep, iat_addr, 83 | iat_size, True) 84 | 85 | 86 | def _generate_export_hashes( 87 | md: Cs, exports_dict: Dict[int, Dict[str, Any]], 88 | process_controller: ProcessController) -> Dict[int, int]: 89 | """ 90 | Go through the given export dictionary and produce a hash for each function 91 | listed in it. 92 | """ 93 | result = {} 94 | modules = process_controller.enumerate_modules() 95 | LOG.debug("Hashing exports for %s", str(modules)) 96 | ranges = [] 97 | for module_name in modules: 98 | if module_name != process_controller.main_module_name: 99 | ranges += process_controller.enumerate_module_ranges( 100 | module_name, include_data=True) 101 | ranges = list( 102 | filter(lambda mem_range: mem_range.protection[2] == 'x', ranges)) 103 | 104 | def get_data(addr: int, size: int) -> bytes: 105 | for mem_range in ranges: 106 | if mem_range.data is None: 107 | continue 108 | if mem_range.contains(addr): 109 | offset = addr - mem_range.base 110 | return mem_range.data[offset:offset + size] 111 | return bytes() 112 | 113 | exports_count = len(exports_dict) 114 | for i, (export_addr, _) in enumerate(exports_dict.items()): 115 | export_hash = compute_function_hash(md, export_addr, get_data, 116 | process_controller) 117 | if export_hash != EMPTY_FUNCTION_HASH: 118 | result[export_hash] = export_addr 119 | else: 120 | LOG.debug("Empty hash for %s", hex(export_addr)) 121 | LOG.debug("Exports hashed: %d/%d", i, exports_count) 122 | 123 | return result 124 | 125 | 126 | def _resolve_imports(api_to_calls: ImportToCallSiteDict, 127 | wrapper_set: WrapperSet, 128 | export_hashes: Optional[Dict[int, int]], md: Cs, 129 | process_controller: ProcessController) -> None: 130 | """ 131 | Resolve potential import wrappers by hash-matching or emulation. 132 | """ 133 | arch = process_controller.architecture 134 | page_size = process_controller.page_size 135 | 136 | def get_data(addr: int, size: int) -> bytes: 137 | try: 138 | return process_controller.read_process_memory(addr, size) 139 | except ReadProcessMemoryError: 140 | # In case we crossed a page boundary and tried to read an invalid 141 | # page, reduce size to stop at page boundary, and try again. 142 | size = page_size - (addr % page_size) 143 | return process_controller.read_process_memory(addr, size) 144 | 145 | # Iterate over the set of potential import wrappers and try to resolve them 146 | resolved_wrappers: Dict[int, int] = {} 147 | problematic_wrappers = set() 148 | for call_addr, call_size, instr_was_jmp, wrapper_addr, _ in wrapper_set: 149 | resolved_addr = resolved_wrappers.get(wrapper_addr) 150 | if resolved_addr is not None: 151 | LOG.debug("Already resolved wrapper: %s -> %s", hex(wrapper_addr), 152 | hex(resolved_addr)) 153 | api_to_calls[resolved_addr].append( 154 | (call_addr, call_size, instr_was_jmp)) 155 | continue 156 | 157 | if wrapper_addr in problematic_wrappers: 158 | # Already failed to resolve this one, ignore 159 | LOG.debug("Skipping unresolved wrapper") 160 | continue 161 | 162 | # If 32-bit executable, try hash-matching 163 | if export_hashes is not None and arch == Architecture.X86_32: 164 | try: 165 | import_hash = compute_function_hash(md, wrapper_addr, get_data, 166 | process_controller) 167 | except Exception as ex: 168 | LOG.debug("Failure for wrapper at %s: %s", hex(wrapper_addr), 169 | str(ex)) 170 | problematic_wrappers.add(wrapper_addr) 171 | continue 172 | if import_hash != EMPTY_FUNCTION_HASH: 173 | LOG.debug("Hash: %s", hex(import_hash)) 174 | resolved_addr = export_hashes.get(import_hash) 175 | if resolved_addr is not None: 176 | LOG.debug("Hash matched") 177 | LOG.debug("Resolved API: %s -> %s", hex(wrapper_addr), 178 | hex(resolved_addr)) 179 | resolved_wrappers[wrapper_addr] = resolved_addr 180 | api_to_calls[resolved_addr].append( 181 | (call_addr, call_size, instr_was_jmp)) 182 | continue 183 | 184 | # Try to resolve the destination address by emulating the wrapper 185 | resolved_addr = resolve_wrapped_api(call_addr, process_controller, 186 | call_addr + call_size) 187 | if resolved_addr is not None: 188 | LOG.debug("Resolved API: %s -> %s", hex(wrapper_addr), 189 | hex(resolved_addr)) 190 | resolved_wrappers[wrapper_addr] = resolved_addr 191 | api_to_calls[resolved_addr].append( 192 | (call_addr, call_size, instr_was_jmp)) 193 | else: 194 | problematic_wrappers.add(wrapper_addr) 195 | 196 | 197 | def _generate_new_iat_in_process( 198 | imports_dict: ImportToCallSiteDict, near_to_ptr: int, 199 | process_controller: ProcessController) -> Tuple[int, int]: 200 | """ 201 | Generate a new IAT from a list of imported function addresses and write 202 | it into a new buffer into the target process. `near_to_ptr` is used to 203 | allocate the new IAT near the unpacked module (which is needed for 64-bit 204 | processes). 205 | """ 206 | ptr_size = process_controller.pointer_size 207 | ptr_format = pointer_size_to_fmt(ptr_size) 208 | iat_size = len(imports_dict) * ptr_size 209 | # Allocate a new buffer in the target process 210 | iat_addr = process_controller.allocate_process_memory( 211 | iat_size, near_to_ptr) 212 | 213 | # Generate the new IAT and write it into the buffer 214 | new_iat_data = bytearray() 215 | for import_addr in imports_dict: 216 | new_iat_data += struct.pack(ptr_format, import_addr) 217 | process_controller.write_process_memory(iat_addr, list(new_iat_data)) 218 | 219 | return iat_addr, iat_size 220 | 221 | 222 | def _fix_import_references_in_process( 223 | api_to_calls: ImportToCallSiteDict, iat_addr: int, 224 | process_controller: ProcessController) -> None: 225 | """ 226 | Replace resolved wrapper call sites with call/jmp to the new IAT (that 227 | contains resolved imports). 228 | """ 229 | arch = process_controller.architecture 230 | ptr_size = process_controller.pointer_size 231 | 232 | for i, call_addrs in enumerate(api_to_calls.values()): 233 | for call_addr, _, instr_was_jmp in call_addrs: 234 | if arch == Architecture.X86_32: 235 | # Absolute 236 | operand = iat_addr + i * ptr_size 237 | fmt = " None: 20 | """ 21 | Main dumping routine for Themida/WinLicense 3.x. 22 | """ 23 | LOG.info("Looking for the IAT...") 24 | iat_range = _find_iat(process_controller, image_base, section_ranges, 25 | text_section_range) 26 | if iat_range is None: 27 | LOG.error("IAT not found") 28 | return 29 | iat_addr = iat_range.base 30 | LOG.info("IAT found: %s-%s", hex(iat_addr), hex(iat_addr + iat_range.size)) 31 | 32 | LOG.info("Resolving imports ...") 33 | unwrap_res = _unwrap_iat(iat_range, process_controller) 34 | if unwrap_res is None: 35 | LOG.error("IAT unwrapping failed") 36 | return 37 | 38 | iat_size, resolved_import_count = unwrap_res 39 | LOG.info("Imports resolved: %d", resolved_import_count) 40 | LOG.info("Fixed IAT at %s, size=%s", hex(iat_addr), hex(iat_size)) 41 | 42 | LOG.info("Dumping PE with OEP=%s ...", hex(oep)) 43 | dump_pe(process_controller, pe_file_path, image_base, oep, iat_addr, 44 | iat_size, False) 45 | 46 | 47 | def _find_iat(process_controller: ProcessController, image_base: int, 48 | section_ranges: List[MemoryRange], 49 | text_section_range: MemoryRange) -> Optional[MemoryRange]: 50 | """ 51 | Try to find the "obfuscated" IAT. It seems the start of the IAT is always 52 | at the "start" of a memory range of the main module. 53 | """ 54 | exports_dict = process_controller.enumerate_exported_functions() 55 | LOG.debug("Exports count: %d", len(exports_dict)) 56 | 57 | # First way: look for "good-looking" memory pages in the main module 58 | LOG.info("Performing linear scan in data sections...") 59 | linear_scan_result = _find_iat_from_data_sections(process_controller, 60 | image_base, 61 | section_ranges, 62 | exports_dict) 63 | if linear_scan_result is not None: 64 | # Linear scan found something, return that 65 | return linear_scan_result 66 | 67 | # Second way: look for wrapped imports in the text section 68 | LOG.info("Looking for wrapped imports in code sections...") 69 | return _find_iat_from_code_sections(process_controller, image_base, 70 | text_section_range, exports_dict) 71 | 72 | 73 | def _find_iat_from_data_sections( 74 | process_controller: ProcessController, image_base: int, 75 | section_ranges: List[MemoryRange], 76 | exports_dict: Dict[int, Dict[str, Any]]) -> Optional[MemoryRange]: 77 | """ 78 | Look for "good-looking" memory pages in the main module. 79 | """ 80 | page_size = process_controller.page_size 81 | # Look at the beginning of PE sections 82 | for section_range in section_ranges: 83 | page_addr = image_base + section_range.base 84 | data = process_controller.read_process_memory(page_addr, page_size) 85 | LOG.debug("Looking for the IAT at (%s, %s)", hex(page_addr), 86 | hex(page_size)) 87 | iat_start_offset = _find_iat_start(data, exports_dict, 88 | process_controller) 89 | if iat_start_offset is not None: 90 | return MemoryRange(page_addr + iat_start_offset, 91 | section_range.size - iat_start_offset, 92 | section_range.protection) 93 | 94 | # Look at memory ranges 95 | for m_range in process_controller.main_module_ranges: 96 | page_count = m_range.size // page_size 97 | # Empirical choice: look at the first 4 pages of each memory range 98 | for page_index in range(0, min(4, page_count)): 99 | page_addr = m_range.base + page_index * page_size 100 | data = process_controller.read_process_memory(page_addr, page_size) 101 | LOG.debug("Looking for the IAT at (%s, %s)", hex(page_addr), 102 | hex(page_size)) 103 | iat_start_offset = _find_iat_start(data, exports_dict, 104 | process_controller) 105 | if iat_start_offset is not None: 106 | return MemoryRange( 107 | page_addr + iat_start_offset, 108 | m_range.size - page_index * page_size - iat_start_offset, 109 | m_range.protection) 110 | 111 | return None 112 | 113 | 114 | def _find_iat_from_code_sections( 115 | process_controller: ProcessController, image_base: int, 116 | text_section_range: MemoryRange, 117 | exports_dict: Dict[int, Dict[str, Any]]) -> Optional[MemoryRange]: 118 | """ 119 | Look for wrapper imports in the text section, similarly to what's done for 120 | Themida 2.x. 121 | """ 122 | # Convert RVA range to VA range and fetch data 123 | section_virtual_addr = image_base + text_section_range.base 124 | text_section_range = MemoryRange( 125 | section_virtual_addr, text_section_range.size, "r-x", 126 | process_controller.read_process_memory(section_virtual_addr, 127 | text_section_range.size)) 128 | assert text_section_range.data is not None 129 | 130 | # Instanciate the disassembler 131 | arch = process_controller.architecture 132 | if arch == Architecture.X86_32: 133 | cs_mode = CS_MODE_32 134 | elif arch == Architecture.X86_64: 135 | cs_mode = CS_MODE_64 136 | else: 137 | raise NotImplementedError(f"Unsupported architecture: {arch}") 138 | md = Cs(CS_ARCH_X86, cs_mode) 139 | md.detail = True 140 | 141 | _, wrapper_set = find_wrapped_imports(text_section_range, exports_dict, md, 142 | process_controller) 143 | if len(wrapper_set) == 0: 144 | return None 145 | 146 | # Find biggest contiguous chunk 147 | ptr_it = map(lambda t: t[4], wrapper_set) 148 | valid_ptr_it = filter(lambda v: v is not None, ptr_it) 149 | ordered_ptr_list: List[int] = sorted(set(valid_ptr_it)) # type: ignore 150 | if len(ordered_ptr_list) == 0: 151 | return None 152 | 153 | LOG.info("Potential import wrappers found: %d", len(ordered_ptr_list)) 154 | pointer_size = process_controller.pointer_size 155 | biggest_chunk_index = 0 156 | biggest_chunk_size = 0 157 | current_chunk_index = 0 158 | current_chunk_size = 0 159 | for i in range(1, len(ordered_ptr_list)): 160 | assert current_chunk_index + current_chunk_size == i - 1 161 | prev_ptr = ordered_ptr_list[i - 1] 162 | cur_ptr = ordered_ptr_list[i] 163 | 164 | if cur_ptr == prev_ptr + pointer_size: 165 | # Same chunk -> expand 166 | current_chunk_size += 1 167 | else: 168 | # New chunk -> reset 169 | current_chunk_index = i 170 | current_chunk_size = 0 171 | 172 | if current_chunk_size > biggest_chunk_size: 173 | # Update biggest chunk 174 | biggest_chunk_index = current_chunk_index 175 | biggest_chunk_size = current_chunk_size 176 | 177 | iat_candidate_addr = ordered_ptr_list[biggest_chunk_index] 178 | iat_candidate_size = biggest_chunk_size 179 | return MemoryRange(iat_candidate_addr, iat_candidate_size, "r--") 180 | 181 | 182 | def _find_iat_start(data: bytes, exports: Dict[int, Dict[str, Any]], 183 | process_controller: ProcessController) -> Optional[int]: 184 | """ 185 | Check whether `data` looks like an "obfuscated" IAT. Themida 3.x wraps 186 | most of the imports but not all of them (the threshold of 2% of valid 187 | imports and 75% of pointers to R*X memory has been chosen empirically). 188 | Returns `None` if this doesn't look like there's an obfuscated IAT in `data`. 189 | """ 190 | ptr_format = pointer_size_to_fmt(process_controller.pointer_size) 191 | elem_count = min(100, len(data) // process_controller.pointer_size) 192 | LOG.debug("Scanning %d elements, pointer size is %d", elem_count, 193 | process_controller.pointer_size) 194 | data_size = elem_count * process_controller.pointer_size 195 | # Look for beginning of IAT 196 | start_offset = 0 197 | for i in range(0, 198 | len(data) // process_controller.pointer_size, 199 | process_controller.pointer_size): 200 | ptr = struct.unpack(ptr_format, 201 | data[i:i + process_controller.pointer_size])[0] 202 | if ptr in exports: 203 | start_offset = i 204 | break 205 | try: 206 | if process_controller.query_memory_protection(ptr) == "rwx": 207 | start_offset = i 208 | break 209 | except QueryProcessMemoryError: 210 | # Ignore invalid pointers 211 | pass 212 | 213 | LOG.debug("Potential start offset %s for the IAT", hex(start_offset)) 214 | non_null_count = 0 215 | valid_ptr_count = 0 216 | rx_dest_count = 0 217 | for i in range(start_offset, data_size, process_controller.pointer_size): 218 | ptr = struct.unpack(ptr_format, 219 | data[i:i + process_controller.pointer_size])[0] 220 | if ptr != 0: 221 | non_null_count += 1 222 | if ptr in exports: 223 | valid_ptr_count += 1 224 | try: 225 | prot = process_controller.query_memory_protection(ptr) 226 | if prot[0] == 'r' and prot[2] == 'x': 227 | rx_dest_count += 1 228 | except QueryProcessMemoryError: 229 | pass 230 | 231 | LOG.debug("Non-null pointer count: %d", non_null_count) 232 | LOG.debug("Valid APIs count: %d", valid_ptr_count) 233 | LOG.debug("R*X destination count: %d", rx_dest_count) 234 | required_valid_elements = int(1 + (non_null_count * 0.02)) 235 | required_rx_elements = int(1 + (non_null_count * 0.75)) 236 | if valid_ptr_count >= required_valid_elements and rx_dest_count >= required_rx_elements: 237 | return start_offset 238 | return None 239 | 240 | 241 | def _unwrap_iat( 242 | iat_range: MemoryRange, 243 | process_controller: ProcessController) -> Optional[Tuple[int, int]]: 244 | """ 245 | Resolve wrapped imports from the IAT and fix it in the target process. 246 | """ 247 | ptr_format = pointer_size_to_fmt(process_controller.pointer_size) 248 | ranges = process_controller.enumerate_module_ranges( 249 | process_controller.main_module_name) 250 | 251 | def in_main_module(address: int) -> bool: 252 | for m_range in ranges: 253 | if m_range.contains(address): 254 | return True 255 | return False 256 | 257 | exports_dict = process_controller.enumerate_exported_functions() 258 | exit_process_addr = process_controller.find_export_by_name( 259 | "kernel32.dll", "ExitProcess") 260 | new_iat_data = bytearray() 261 | resolved_import_count = 0 262 | successive_failures = 0 263 | last_resolution_offset = 0 264 | for current_addr in range(iat_range.base, iat_range.base + iat_range.size, 265 | process_controller.page_size): 266 | data_size = process_controller.page_size - ( 267 | current_addr % process_controller.page_size) 268 | page_data = process_controller.read_process_memory( 269 | current_addr, data_size) 270 | for i in range(0, len(page_data), process_controller.pointer_size): 271 | wrapper_start = struct.unpack( 272 | ptr_format, 273 | page_data[i:i + process_controller.pointer_size])[0] 274 | # Wrappers are located in one of the module's section 275 | if in_main_module(wrapper_start): 276 | resolved_api = resolve_wrapped_api(wrapper_start, 277 | process_controller) 278 | if resolved_api not in exports_dict: 279 | successive_failures += 1 280 | # Note: When TLS callbacks are used, `kernel32.ExitProcess` 281 | # is hooked via the IAT and thus might not resolved properly. 282 | new_iat_data += struct.pack(ptr_format, exit_process_addr) 283 | else: 284 | LOG.debug("Resolved API: %s -> %s", hex(wrapper_start), 285 | hex(resolved_api)) 286 | new_iat_data += struct.pack(ptr_format, resolved_api) 287 | resolved_import_count += 1 288 | last_resolution_offset = len(new_iat_data) 289 | if successive_failures > 0: 290 | LOG.warning( 291 | "A resolved API wasn't an export, " 292 | "it's been replaced with 'kernel32.ExitProcess'.") 293 | successive_failures = 0 294 | 295 | # Dumb check to detect the "end" of the IAT 296 | if resolved_api is None and successive_failures >= IAT_MAX_SUCCESSIVE_FAILURES: 297 | # Remove the last elements 298 | new_iat_data = new_iat_data[:last_resolution_offset + 1] 299 | # Ensure the range is writable 300 | process_controller.set_memory_protection( 301 | iat_range.base, len(new_iat_data), "rw-") 302 | # Update IAT 303 | process_controller.write_process_memory( 304 | iat_range.base, list(new_iat_data)) 305 | return len(new_iat_data), resolved_import_count 306 | elif wrapper_start in exports_dict: 307 | # Not wrapped, add as is 308 | new_iat_data += struct.pack(ptr_format, wrapper_start) 309 | resolved_import_count += 1 310 | last_resolution_offset = len(new_iat_data) 311 | if successive_failures > 0: 312 | LOG.warning( 313 | "A resolved API wasn't an export, " 314 | "it's been replaced with 'kernel32.ExitProcess'.") 315 | successive_failures = 0 316 | else: 317 | # Junk pointer (most likely null). Keep for alignment 318 | new_iat_data += struct.pack(ptr_format, wrapper_start) 319 | 320 | # Update IAT with the our newly computed IAT 321 | if len(new_iat_data) > 0: 322 | # Ensure the range is writable 323 | process_controller.set_memory_protection(iat_range.base, 324 | len(new_iat_data), "rw-") 325 | # Update IAT 326 | process_controller.write_process_memory(iat_range.base, 327 | list(new_iat_data)) 328 | return len(new_iat_data), resolved_import_count 329 | 330 | return None 331 | --------------------------------------------------------------------------------