├── .gitattributes ├── .gitignore ├── .python-version ├── LICENSE ├── README.md ├── pyproject.toml ├── src └── rustbininfo │ ├── __init__.py │ ├── cache.py │ ├── exception.py │ ├── info │ ├── compiler.py │ ├── dependencies.py │ ├── http_client.py │ ├── models │ │ ├── crate.py │ │ ├── github_api.py │ │ ├── info.py │ │ └── registry.py │ ├── nightly │ │ └── nightly.py │ ├── rust_repo_handler.py │ └── specifics │ │ └── mingw.py │ ├── logger.py │ ├── main.py │ └── utils.py └── tests ├── archiver.exe ├── conftest.py ├── crackme.exe ├── gcc_10.3.0_mingw_8_hello.exe ├── test_download.py ├── test_info.py └── test_timestamp_guess.py /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/crackme.exe filter=lfs diff=lfs merge=lfs -text 2 | tests/archiver.exe filter=lfs diff=lfs merge=lfs -text 3 | tests/gcc_10.3.0_mingw_8_hello.exe filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | dist/ 3 | .pytest_cache 4 | .mypy_cache -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi package](https://badge.fury.io/py/rustbininfo.svg)](https://pypi.org/project/rustbininfo) 2 | [![GitHub](https://img.shields.io/github/license/N0fix/rustbininfo.svg)](https://github.com/N0fix/rustbininfo/blob/master/LICENSE) 3 | 4 | Get information about a stripped rust executable. 5 | 6 | This tool is mentioned in [this](https://nofix.re/posts/2024-11-02-rust-symbs/) and [this](https://nofix.re/posts/2024-08-03-arti-rust/) blogposts. 7 | 8 | ## Example 9 | 10 | ``` 11 | > rbi ~/Downloads/crackme.exe 12 | TargetRustInfo( 13 | rustc_version='1.65.0', 14 | rustc_commit_hash='9c20b2a8cc7588decb6de25ac6a7912dcef24d65', 15 | dependencies=[ 16 | Crate(name='crc-any', version='2.4.3', features=[], repository=None), 17 | Crate(name='rand', version='0.8.5', features=[], repository=None), 18 | Crate(name='rand_chacha', version='0.3.1', features=[], repository=None), 19 | Crate(name='short-crypt', version='1.0.26', features=[], repository=None) 20 | ], 21 | rust_dependencies_imphash='cd7358d2cd75458edda70d567f1555fa', 22 | guessed_toolchain='windows-msvc' 23 | ) 24 | ``` 25 | 26 | ## Usage 27 | ``` 28 | rbi 29 | usage: rbi [-h] [-f] [-d] target 30 | 31 | Get information about stripped rust executable, and download its dependencies. 32 | 33 | positional arguments: 34 | target 35 | 36 | options: 37 | -h, --help show this help message and exit 38 | -f, --full 39 | -d, --project-date Tries to guess date latest depdnency got added to the project, based on dependencies version 40 | 41 | Usage examples: 42 | 43 | rbi 'challenge.exe' 44 | rbi -f 'challenge.exe' 45 | rbi -d 'challenge.exe' 46 | ``` 47 | 48 | ## Tests 49 | 50 | Tests requieres git-lfs to retrive the test executable. 51 | 52 | Then, execute the following command: 53 | 54 | ``` 55 | git lfs fetch 56 | git lfs pull 57 | pytest -s 58 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rustbininfo" 3 | version = "0.7.2" 4 | description = "Get information about stripped rust executables" 5 | authors = [{name="Nofix", email="16479266+N0fix@users.noreply.github.com"}] 6 | readme = "README.md" 7 | requires-python = ">=3.10" 8 | dependencies = [ 9 | "beaker>=1.13.0", 10 | "httpx>=0.28.1", 11 | "pydantic>=2.10.6", 12 | "pytz>=2025.1", 13 | "requests>=2.32.3", 14 | "rich>=13.9.4", 15 | "semver>=3.0.4", 16 | "truststore>=0.10.1", 17 | "xdg-base-dirs>=6.0.2", 18 | "yara-python>=4.5.1", 19 | ] 20 | 21 | [project.scripts] 22 | rustbininfo = "rustbininfo.main:main_cli" 23 | rbi = "rustbininfo.main:main_cli" 24 | 25 | [project.optional-dependencies] 26 | gitpython = [ 27 | "gitpython>=3.1.44", 28 | ] 29 | 30 | [tool.ruff] 31 | line-length = 120 32 | 33 | [tool.ruff.lint.per-file-ignores] 34 | "__init__.py" = ["E402"] 35 | 36 | [tool.ruff.lint] 37 | select = ["ALL"] 38 | ignore=[ 39 | "INP001", 40 | "D100", 41 | "A004", 42 | "TID252", 43 | "S324", 44 | ] 45 | 46 | [build-system] 47 | requires = ["hatchling"] 48 | build-backend = "hatchling.build" 49 | 50 | [dependency-groups] 51 | dev = [ 52 | "pytest>=8.3.5", 53 | "ruff>=0.9.8", 54 | ] 55 | -------------------------------------------------------------------------------- /src/rustbininfo/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .info.compiler import BasicProvider 4 | 5 | try: 6 | import git 7 | 8 | from .info.rust_repo_handler import GitRepoProvider 9 | except: 10 | pass 11 | from .info.models.crate import Crate 12 | from .info.models.info import TargetRustInfo 13 | from .utils import get_min_max_update_time 14 | -------------------------------------------------------------------------------- /src/rustbininfo/cache.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from beaker.cache import CacheManager 4 | from beaker.util import parse_cache_config_options 5 | from xdg_base_dirs import xdg_cache_home 6 | 7 | package_name = __package__ if __package__ is not None else "RBI" 8 | 9 | cache_dir = pathlib.Path(xdg_cache_home()) / package_name 10 | cache_lock_dir = cache_dir / "lock" 11 | cache_dir.mkdir(exist_ok=True, parents=True) 12 | cache_lock_dir.mkdir(exist_ok=True, parents=True) 13 | 14 | cache_opts = {"cache.type": "file", "cache.data_dir": cache_dir, "cache.lock_dir": cache_lock_dir} 15 | cache: CacheManager = CacheManager(**parse_cache_config_options(cache_opts)) 16 | tmpl_cache = cache.get_cache("mytemplate.html", type="dbm") 17 | -------------------------------------------------------------------------------- /src/rustbininfo/exception.py: -------------------------------------------------------------------------------- 1 | class InvalidVersionError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /src/rustbininfo/info/compiler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import pathlib 5 | import re 6 | 7 | import httpx 8 | from pydantic import ValidationError 9 | 10 | from ..logger import log 11 | from .models.github_api import GitHubResponse, GithubSpecificTagInfo, GithubTagResponse 12 | 13 | 14 | class CompilerInfo: 15 | def __init__(self, repo_path: pathlib.Path | None = None): 16 | if repo_path: 17 | from .rust_repo_handler import GitRepoProvider 18 | 19 | self.provider = GitRepoProvider(repo_path) 20 | 21 | else: 22 | self.provider = BasicProvider() 23 | 24 | def get_rustc_commit(self, target: pathlib.Path) -> str | None: 25 | commit = self.provider.get_rustc_commit(target) 26 | 27 | if commit is None: 28 | commit = self.provider.guess_commit_from_dates(target) 29 | 30 | return commit 31 | 32 | 33 | class BasicProvider: 34 | def get_rustc_commit(self, target: pathlib.Path) -> str | None: 35 | """Find and returns rustc commit of a given rust executable file. 36 | 37 | Args: 38 | target (pathlib.Path): path to the target. 39 | 40 | Returns: 41 | str | None: None if no rustc commit could be found. 42 | 43 | """ 44 | with pathlib.Path(target).open("rb") as f: 45 | data = f.read() 46 | res = re.search(b"rustc/([a-z0-9]{40})", data) 47 | 48 | if res is None: 49 | return None 50 | 51 | return res.group(1).decode() 52 | 53 | def get_version_from_comment(self, target: pathlib.Path) -> str | None: 54 | with pathlib.Path(target).open("rb") as f: 55 | data = f.read() 56 | # .comment section: 57 | # rustc version 1.85.0-nightly 58 | # rustc version 1.83.0 59 | res = re.search(b"rustc version ([a-zA-Z0-9._-]+)", data) 60 | 61 | if res is None: 62 | return None 63 | 64 | return res.group(1).decode() 65 | 66 | def rustc_version_from_commit(self, commit: str) -> str: 67 | url = f"https://api.github.com/search/issues?q={commit}+repo:rust-lang/rust" 68 | try: 69 | res = GitHubResponse.model_validate(httpx.get(url, timeout=20).json()) 70 | if res.items and res.items[0].milestone and res.items[0].milestone.title: 71 | milestone_title = res.items[0].milestone.title 72 | return str(milestone_title) 73 | 74 | except ValidationError: 75 | log.exception("Validation error while processing GitHub response") 76 | raise 77 | 78 | return self.get_commit_date(commit) 79 | 80 | def get_latest_rustc_version(self) -> str | None: 81 | url = "https://github.com/rust-lang/rust/tags" 82 | res = httpx.get(url, timeout=20).text 83 | regex = re.compile(r"/rust-lang/rust/releases/tag/([0-9\.]+)") 84 | return regex.findall(res)[0] 85 | 86 | def get_rustc_version_date(self, rustc_version: str) -> str | None: 87 | URI = "https://api.github.com/repos/rust-lang/rust/tags?per_page=100" 88 | tags = GithubTagResponse.model_validate(httpx.get(URI, timeout=20).json()).root 89 | for tag in tags: 90 | if tag.name == rustc_version: 91 | response = GithubSpecificTagInfo.model_validate(httpx.get(tag.commit.url, timeout=20).json()) 92 | return datetime.datetime.fromisoformat(response.commit.committer.date).strftime("%Y-%m-%d") 93 | 94 | return None 95 | 96 | def get_commit_date(self, commit: str) -> str: 97 | URI = "https://api.github.com/repos/rust-lang/rust/commits" 98 | response = GithubSpecificTagInfo.model_validate( 99 | httpx.get(URI, params={"sha": commit, "per_page": "1"}).json()[0] 100 | ) 101 | return datetime.datetime.fromisoformat(response.commit.committer.date).strftime("%Y-%m-%d") 102 | 103 | def guess_commit_from_dates(self, target: pathlib.Path): 104 | URI = "https://api.github.com/repos/rust-lang/rust/commits" 105 | from rustbininfo.info.dependencies import get_dependencies 106 | from rustbininfo.utils import get_min_max_update_time 107 | 108 | t = get_dependencies(target, fast_load=True) 109 | date_min, date_max = get_min_max_update_time(t) 110 | if date_min.timestamp() <= 10_000: 111 | return None 112 | 113 | tags = httpx.get(URI, params={"since": date_min.isoformat(), "until": date_max.isoformat()}, timeout=20).json() 114 | return tags[0]["sha"] 115 | 116 | def get_rustc_version(self, target: pathlib.Path) -> tuple[str | None, str | None]: 117 | """Get rustc version used in target executable. 118 | 119 | Args: 120 | target (pathlib.Path): file path. 121 | 122 | Returns: 123 | Tuple[str, str]: Returns Tuple(commit, version). If search failed, returns Tuple(None, None) instead. 124 | 125 | """ 126 | commit = self.get_rustc_commit(target) 127 | 128 | version = self.get_version_from_comment(target) 129 | 130 | if commit is None: 131 | commit = self.guess_commit_from_dates(target) 132 | 133 | if commit is None and version is None: 134 | return (None, None) 135 | 136 | # version is not None, no need to continue and look it up via GitHub 137 | if version: 138 | log.debug(f"Found version {version} as a hardcoded string") 139 | return (commit, version) 140 | 141 | if commit is None: 142 | return (None, None) 143 | 144 | log.debug("Found commit %s", commit) 145 | version = self.rustc_version_from_commit(commit) 146 | if version is None: 147 | return (None, None) 148 | 149 | log.debug("Found tag/milestone %s", version) 150 | return (commit, version) 151 | -------------------------------------------------------------------------------- /src/rustbininfo/info/dependencies.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import re 3 | from typing import Set 4 | 5 | from ..logger import log 6 | from .models.crate import Crate 7 | 8 | 9 | def _guess_dependencies(content: bytes) -> Set: 10 | regexes = [ 11 | # "/index.crates.io-6f17d22bba15001f/rayon-core-1.12.1/src/job.rs 12 | rb"index.crates.io.[^\\\/]+.([a-zA-Z0-9_-]+-[a-zA-Z0-9._-]+)", 13 | # \registry\src\github.com-1ecc6299db9ec823\aho-corasick-0.7.15\src\ahocorasick.rs 14 | rb"registry.src.[^\\\/]+.([a-zA-Z0-9_-]+-[a-zA-Z0-9._-]+)", 15 | # /rust/deps\indexmap-2.2.6\src\map\core.rs 16 | rb"rust.deps.([a-zA-Z0-9_-]+-[a-zA-Z0-9._-]+)", 17 | # crate-1.0.0\src\lib.rs 18 | rb"\x00([a-z0-9_-]+-[0-9.]+-[a-zA-Z0-9._-]+)[\\/][a-z]", 19 | ] 20 | result = set() 21 | for reg in regexes: 22 | res = re.findall(reg, content) 23 | result.update(res) 24 | 25 | return result 26 | 27 | 28 | def get_dependencies(target: pathlib.Path, fast_load=False) -> Set[Crate]: 29 | reserved_names = ["rustc-demangle"] 30 | result = [] 31 | data = open(target, "rb").read() 32 | res = _guess_dependencies(data) 33 | for dep in set(res): 34 | # Cleaning dependency name 35 | try: 36 | dep = dep[: dep.index(b"\x00")] 37 | 38 | except: # noqa E722 39 | pass 40 | 41 | if dep == b"": 42 | continue 43 | # End of cleaning 44 | 45 | log.debug(f"Found dependency : {dep}") 46 | c = Crate.from_depstring(dep.decode(), fast_load) 47 | if c.name not in reserved_names: 48 | result.append(c) 49 | return set(result) 50 | -------------------------------------------------------------------------------- /src/rustbininfo/info/http_client.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | 3 | import httpx 4 | import truststore 5 | 6 | ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 7 | client = httpx.Client(verify=ctx) 8 | -------------------------------------------------------------------------------- /src/rustbininfo/info/models/crate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import pathlib 5 | import tarfile 6 | from pathlib import Path 7 | 8 | import semver 9 | from pydantic import BaseModel, Field 10 | 11 | from ...exception import InvalidVersionError 12 | from ...logger import log 13 | from ...utils import get_writable_dir_in_tmp 14 | from ..http_client import client 15 | from .registry import get_registry_cfg 16 | 17 | 18 | def _urljoin(base: str, *parts: str) -> str: 19 | for part in filter(None, parts): 20 | base = "{}/{}".format(base.rstrip("/"), part.lstrip("/")) 21 | return base 22 | 23 | 24 | class ReducedRepresentation: 25 | def __repr_args__(self: BaseModel) -> ReprArgs: 26 | # Disable repr of field if "repr" attribute is not False and value isn't evaluated to False 27 | return [ 28 | (key, value) 29 | for key, value in self.__dict__.items() 30 | if self.model_fields.get(key) and not self.model_fields[key].repr == False and value 31 | ] 32 | 33 | 34 | class Crate(ReducedRepresentation, BaseModel): 35 | name: str 36 | version: str 37 | features: list[str] = Field(default=[]) 38 | repository: str | None = Field(default=None) 39 | fast_load: bool | None = Field(init=True, repr=False) 40 | _available_versions: list[str] = [] 41 | _version_info: dict = None 42 | 43 | @classmethod 44 | def from_depstring(cls, dep_str: str, fast_load=True) -> Crate: 45 | try: 46 | name, version = dep_str.rsplit("-", 1) 47 | 48 | obj = cls( 49 | name=name, 50 | version=str(semver.Version.parse(version)), 51 | fast_load=fast_load, 52 | ) 53 | 54 | except: # noqa E722 55 | # e.g: crate-name-0.1.0-pre.2 56 | name, version, patch = dep_str.rsplit("-", 2) 57 | version += f"-{patch}" 58 | obj = cls( 59 | name=name, 60 | version=str(semver.Version.parse(version)), 61 | fast_load=fast_load, 62 | ) 63 | 64 | obj._fast_load = fast_load 65 | return obj 66 | 67 | def model_post_init(self, __context) -> None: 68 | if not self.fast_load: 69 | _ = self.metadata # triggers getter 70 | 71 | @property 72 | def metadata(self): 73 | if getattr(self, "_metadata", None) is None: 74 | self._metadata = self._get_metadata() 75 | 76 | return self._metadata 77 | 78 | @metadata.setter 79 | def metadata(self, value): 80 | self._metadata = value 81 | 82 | def _get_metadata(self) -> dict: 83 | log.debug("Downloading metadata for %s", self.name) 84 | uri = _urljoin("https://crates.io", *["api", "v1", "crates", self.name]) 85 | headers = {"User-Agent": "rustbinsign (https://github.com/N0fix/rustbinsign)"} 86 | res = client.get(uri, timeout=20, headers=headers) 87 | result = json.loads(res.text) 88 | # self._metadata = result 89 | for version in result["versions"]: 90 | self._available_versions.append(version["num"]) 91 | if version["num"] == self.version: 92 | self._version_info = version 93 | for feature in version["features"]: 94 | self.features.append(feature) 95 | 96 | if self.version not in self._available_versions: 97 | raise InvalidVersionError 98 | 99 | self.repository = result["crate"]["repository"] 100 | 101 | assert self._version_info is not None 102 | 103 | return result 104 | 105 | def _get_default_destination_dir(self) -> Path: 106 | return get_writable_dir_in_tmp() 107 | 108 | def download(self, destination_directory: Path | None = None) -> Path: 109 | log.info("Downloading crate %s", self.name) 110 | 111 | if destination_directory is None: 112 | destination_directory = self._get_default_destination_dir() 113 | 114 | uri = _urljoin(get_registry_cfg().dl, self.name, self.version, "download") 115 | headers = {"User-Agent": "rustbinsign (https://github.com/N0fix/rustbinsign)"} 116 | res = client.get(uri, timeout=20, headers=headers) 117 | assert res.status_code == 200 118 | 119 | result_file = destination_directory.joinpath(f"{self}.tar.gz") 120 | with pathlib.Path(result_file).open("wb+") as f: 121 | # Should ensure that the file is fully written to disk, unlink otherwise. 122 | try: 123 | f.write(res.content) 124 | 125 | except Exception as exc: 126 | pathlib.Path(result_file).unlink() 127 | raise exc 128 | 129 | return result_file 130 | 131 | def download_untar(self, destination_directory: Path | None = None, remove_tar: bool = False) -> Path: 132 | if not destination_directory: 133 | destination_directory = self._get_default_destination_dir() 134 | 135 | path = self.download(destination_directory) 136 | 137 | with tarfile.open(path) as tar: 138 | tar.extractall(destination_directory, filter="data") 139 | tar.close() 140 | 141 | if remove_tar: 142 | tar.unlink() 143 | 144 | return Path(f"{str(path).strip('.tar.gz')}") 145 | 146 | def __str__(self): 147 | return f"{self.name}-{self.version}" 148 | 149 | def __hash__(self): 150 | return hash((self.name, self.version)) 151 | -------------------------------------------------------------------------------- /src/rustbininfo/info/models/github_api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pydantic import BaseModel, RootModel 4 | 5 | 6 | class Milestone(BaseModel): 7 | title: str | None 8 | 9 | 10 | class GitHubIssue(BaseModel): 11 | milestone: Milestone | None 12 | 13 | 14 | class GitHubResponse(BaseModel): 15 | items: list[GitHubIssue] 16 | 17 | 18 | class GithubTagsCommitter(BaseModel): 19 | date: str 20 | 21 | 22 | class GithubSpecificTagCommit(BaseModel): 23 | committer: GithubTagsCommitter 24 | 25 | 26 | class GithubSpecificTagInfo(BaseModel): 27 | commit: GithubSpecificTagCommit 28 | 29 | 30 | class GithubTagsCommit(BaseModel): 31 | url: str 32 | 33 | 34 | class GithubTags(BaseModel): 35 | name: str 36 | commit: GithubTagsCommit 37 | 38 | 39 | class GithubTagResponse(RootModel[list[GithubTags]]): 40 | pass 41 | -------------------------------------------------------------------------------- /src/rustbininfo/info/models/info.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import hashlib 4 | from typing import TYPE_CHECKING 5 | 6 | import yara 7 | from pydantic import BaseModel 8 | 9 | from ..compiler import BasicProvider 10 | from ..dependencies import get_dependencies 11 | from ..specifics.mingw import ( 12 | RULE_MINGW_6_GCC_8_3_0, 13 | RULE_MINGW_7_GCC_9_3_0, 14 | RULE_MINGW_8_GCC_10_3_0, 15 | RULE_MINGW_10_GCC_12_2_0, 16 | RULE_MINGW_11_GCC_13_1_0, 17 | ) 18 | from .crate import Crate 19 | 20 | if TYPE_CHECKING: 21 | import pathlib 22 | 23 | """ 24 | def guess_is_debug(target: pathlib.Path) -> bool: 25 | needle = b"run with `RUST_BACKTRACE=1` environment variable to display a backtrace" 26 | data = open(target, "rb").read() 27 | return needle in data 28 | return False 29 | """ 30 | 31 | 32 | def guess_toolchain(target_content: bytes) -> str | None: 33 | """Guess toolchain of target executable using YARA rules. 34 | 35 | Args: 36 | target_content (bytes): target executable bytes. 37 | 38 | Returns: 39 | str | None: Recognized toolchain, None if no toolchain got recognized. 40 | 41 | """ 42 | known_heuristics = { 43 | lambda c: (yara.compile(source=RULE_MINGW_6_GCC_8_3_0).match(data=c)): "Mingw-w64 (Mingw6-GCC_8.3.0)", 44 | lambda c: (yara.compile(source=RULE_MINGW_7_GCC_9_3_0).match(data=c)): "Mingw-w64 (Mingw7-GCC_9.3.0)", 45 | lambda c: (yara.compile(source=RULE_MINGW_8_GCC_10_3_0).match(data=c)): "Mingw-w64 (Mingw8-GCC_10.3.0)", 46 | lambda c: (yara.compile(source=RULE_MINGW_10_GCC_12_2_0).match(data=c)): "Mingw-w64 (Mingw10-GCC_12.2.0)", 47 | lambda c: (yara.compile(source=RULE_MINGW_11_GCC_13_1_0).match(data=c)): "Mingw-w64 (Mingw11-GCC_13.1.0)", 48 | lambda c: (b"Mingw-w64 runtime failure" in c): "Mingw-w64 (Could not find mingw/gcc version)", 49 | lambda c: (b"_CxxThrowException" in c): "windows-msvc", 50 | lambda c: (b".CRT$" in c): "windows-msvc", 51 | lambda c: (b"/checkout/src/llvm-project/libunwind/src/DwarfInstructions.hpp" in c): "linux-musl", 52 | } 53 | 54 | for item, value in known_heuristics.items(): 55 | if item(target_content): 56 | return value 57 | 58 | return None 59 | 60 | 61 | def imphash(dependencies: list[Crate]): 62 | md5 = hashlib.md5() 63 | sorted_list = sorted([str(d) for d in dependencies]) 64 | for dep in sorted_list: 65 | md5.update(str(dep).encode()) 66 | 67 | return md5.hexdigest() 68 | 69 | 70 | class TargetRustInfo(BaseModel): 71 | rustc_version: str | None 72 | rustc_commit_hash: str | None 73 | dependencies: list[Crate] 74 | rust_dependencies_imphash: str 75 | guessed_toolchain: str | None = None 76 | # guess_is_debug_build: bool 77 | 78 | @classmethod 79 | def from_target(cls, path: pathlib.Path, fast_load: bool = True, provider: BasicProvider = None): 80 | if provider is None: 81 | provider = BasicProvider() 82 | content = open(path, "rb").read() 83 | commit, version = provider.get_rustc_version(path) 84 | if version is None: 85 | version = "nightly-unknown-date" 86 | dependencies: set[Crate] = get_dependencies(path, fast_load) 87 | dependencies = sorted(dependencies, key=lambda x: x.name) 88 | 89 | return TargetRustInfo( 90 | rustc_commit_hash=commit, 91 | rustc_version=version, 92 | dependencies=dependencies, 93 | rust_dependencies_imphash=imphash(dependencies), 94 | guessed_toolchain=guess_toolchain(content), 95 | # guess_is_debug_build=guess_is_debug(path), 96 | ) 97 | -------------------------------------------------------------------------------- /src/rustbininfo/info/models/registry.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | import tomllib 5 | from pydantic import BaseModel 6 | 7 | from ..http_client import client 8 | 9 | 10 | class RegistryCFG(BaseModel): 11 | registry_path: str 12 | dl: str 13 | api: str 14 | 15 | 16 | def _get_registry(src: dict) -> RegistryCFG: 17 | if url := src.get("registry"): 18 | url = url.split("+")[-1] 19 | result = client.get(url + "/config.json").json() 20 | dl, api = result["dl"], result["api"] 21 | return RegistryCFG(registry_path=f"{url}", dl=dl, api=api) 22 | 23 | if file := src.get("local-registry"): 24 | raise NotImplementedError 25 | 26 | if directory := src.get("directory"): 27 | raise NotImplementedError 28 | 29 | 30 | def _get_registry_cfg() -> RegistryCFG: 31 | env_to_fetch = "USERPROFILE" if os.name == "nt" else "HOME" 32 | 33 | cfg_path = pathlib.Path(os.environ.get("CARGO_HOME", pathlib.Path(os.environ.get(env_to_fetch)) / ".cargo")) 34 | potential_cfgs = [cfg_path / "config", cfg_path / "config.toml"] 35 | 36 | for cfg in potential_cfgs: 37 | if cfg.exists(): 38 | config = tomllib.loads(cfg.read_text()) 39 | if source := config.get("source"): 40 | current_src = source.get("crates-io") 41 | while True: 42 | if s := current_src.get("replace-with"): 43 | current_src = source.get(s) 44 | 45 | else: 46 | break 47 | 48 | return _get_registry(current_src) 49 | 50 | break 51 | 52 | return RegistryCFG( 53 | registry_path="https://index.crates.io/", dl="https://static.crates.io/crates", api="https://crates.io" 54 | ) 55 | 56 | 57 | def get_registry_cfg(): 58 | if hasattr(get_registry_cfg, "cfg"): 59 | return get_registry_cfg.cfg 60 | 61 | get_registry_cfg.cfg = _get_registry_cfg() 62 | return get_registry_cfg.cfg 63 | -------------------------------------------------------------------------------- /src/rustbininfo/info/nightly/nightly.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import httpx 4 | 5 | from rustbininfo.info.models.github_api import GithubSpecificTagInfo, GithubTagResponse 6 | 7 | 8 | class NightlyGetter: 9 | def get_nightly_toolchain_for_rustc_version(self, rustc_version) -> str | None: 10 | URI = "https://api.github.com/repos/rust-lang/rust/tags?per_page=100" 11 | tags = GithubTagResponse.model_validate(httpx.get(URI, timeout=20).json()).root 12 | for tag in tags: 13 | if tag.name == rustc_version: 14 | response = GithubSpecificTagInfo.model_validate(httpx.get(tag.commit.url, timeout=20).json()) 15 | return datetime.datetime.fromisoformat(response.commit.committer.date).strftime("%Y-%m-%d") 16 | 17 | return None 18 | -------------------------------------------------------------------------------- /src/rustbininfo/info/rust_repo_handler.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pathlib 3 | import sys 4 | 5 | from ..cache import cache 6 | from ..logger import log 7 | from .compiler import BasicProvider 8 | 9 | try: 10 | import git 11 | 12 | except ImportError as exc: 13 | error = "`git` was not found, have you installed the 'gitpython' extra ? (pip install rustbininfo[gitpython])" 14 | raise ImportError(error) from exc 15 | 16 | 17 | class GitRepoProvider(BasicProvider): 18 | def __init__(self, repo_path: pathlib.Path, update: bool = False): 19 | self.repo = git.Repo(repo_path) 20 | if update: 21 | self._ensure_repo_is_up_to_date() 22 | 23 | def _ensure_repo_is_up_to_date(self): 24 | try: 25 | o = self.repo.remotes.origin 26 | o.pull() 27 | 28 | except: 29 | log.warning("Could not pull latest change, results might be innacurate.") 30 | 31 | def rustc_version_from_commit(self, commit: str) -> str | None: 32 | if tag := self.repo.git.tag("--contains", commit): 33 | multiple_tags = "\n" in tag 34 | if multiple_tags: 35 | return tag.splitlines()[0] 36 | 37 | return tag 38 | 39 | return f"nightly-{self.get_commit_date(commit)}" 40 | 41 | def guess_commit_from_dates(self, target: pathlib.Path) -> str | None: 42 | from rustbininfo.info.dependencies import get_dependencies 43 | from rustbininfo.utils import get_min_max_update_time 44 | 45 | t = get_dependencies(target, fast_load=True) 46 | date_min, date_max = get_min_max_update_time(t) 47 | if date_min.timestamp() <= 10_000: 48 | return None 49 | 50 | return self.repo.git.log( 51 | f'--since="{date_min.isoformat()}', 52 | f"--until={date_max.isoformat()}", 53 | "-1", 54 | "--oneline", 55 | "--format=%H", 56 | ) 57 | 58 | def get_latest_rustc_version(self) -> str: 59 | return sorted(self.repo.tags, key=lambda t: t.commit.committed_datetime)[-1].name 60 | 61 | def get_rustc_version_date(self, rustc_version: str) -> str | None: 62 | for tag in self.repo.tags: 63 | if tag.name == rustc_version: 64 | return datetime.datetime.fromisoformat(tag.commit.committed_datetime).strftime("%Y-%m-%d") 65 | 66 | return None 67 | 68 | def get_commit_date(self, commit: str): 69 | return self.repo.git.log("-1", "--no-patch", "--format=%cs", commit) 70 | -------------------------------------------------------------------------------- /src/rustbininfo/info/specifics/mingw.py: -------------------------------------------------------------------------------- 1 | RULE_MINGW_6_GCC_8_3_0 = """rule rust_mingw6_gcc_8_3_0 { 2 | 3 | strings: 4 | 5 | $_ = { 6 | 55 // push rbp 7 | 41 57 // push r15 8 | 41 56 // push r14 9 | 41 55 // push r13 10 | 41 54 // push r12 11 | 57 // push rdi 12 | 56 // push rsi 13 | 53 // push rbx 14 | 48 83 EC 38 // sub rsp, 38h 15 | 48 8D AC 24 80 00 00 00 // lea rbp, [rsp+80h] 16 | 8B [5-6] // mov ebx, cs:was_init_95200 17 | 85 DB // test ebx, ebx 18 | 74 11 // jz short loc_401CC3 19 | 48 8D 65 B8 // lea rsp, [rbp-48h] 20 | 5B // pop rbx 21 | 5E // pop rsi 22 | 5F // pop rdi 23 | 41 5C // pop r12 24 | 41 5D // pop r13 25 | 41 5E // pop r14 26 | 41 5F // pop r15 27 | 5D // pop rbp 28 | C3 29 | } // pei386_runtime_relocator 30 | 31 | 32 | condition: 33 | all of them 34 | }""" 35 | 36 | RULE_MINGW_7_GCC_9_3_0 = """rule rust_mingw7_gcc_9_3_0 { 37 | 38 | strings: 39 | $a = { 40 | 55 // push rbp 41 | 41 57 // push r15 42 | 41 56 // push r14 43 | 41 55 // push r13 44 | 41 54 // push r12 45 | 57 // push rdi 46 | 56 // push rsi 47 | 53 // push rbx 48 | 48 83 EC 38 // sub rsp, 38h 49 | 48 8D AC 24 80 00 00 00 // lea rbp, [rsp+80h] 50 | 8B [5-6] // mov esi, cs:was_init_93800 51 | 85 F6 // test esi, esi 52 | 74 16 // jz short loc_4954A8 53 | 48 8D 65 B8 // lea rsp, [rbp-48h] 54 | 5B // pop rbx 55 | 5E // pop rsi 56 | 5F // pop rdi 57 | 41 5C // pop r12 58 | 41 5D // pop r13 59 | 41 5E // pop r14 60 | 41 5F // pop r15 61 | 5D // pop rbp 62 | C3 // retn 63 | } // pei386_runtime_relocator 64 | 65 | $b = { 66 | 48 83 EC 28 // sub rsp, 28h 67 | 48 [6] // mov rax, cs:_refptr_mingw_app_type 68 | C7 00 01 00 00 00 // mov dword ptr [rax], 1 69 | E8 [4] // call __security_init_cookie 70 | E8 [4] // call __tmainCRTStartup 71 | 90 // nop 72 | 90 // nop 73 | 48 83 C4 28 // add rsp, 28h 74 | C3 // retn 75 | } 76 | 77 | condition: 78 | all of them 79 | }""" 80 | 81 | RULE_MINGW_8_GCC_10_3_0 = """rule rust_mingw8_gcc_10_3_0 { 82 | 83 | strings: 84 | $a = { 85 | 55 // push rbp 86 | 41 57 // push r15 87 | 41 56 // push r14 88 | 41 55 // push r13 89 | 41 54 // push r12 90 | 57 // push rdi 91 | 56 // push rsi 92 | 53 // push rbx 93 | 48 83 EC 38 // sub rsp, 38h 94 | 48 8D AC 24 80 00 00 00 // lea rbp, [rsp+80h] 95 | 8B [5-6] // mov esi, cs:was_init_93800 96 | 85 F6 // test esi, esi 97 | 74 16 // jz short loc_4954A8 98 | 48 8D 65 B8 // lea rsp, [rbp-48h] 99 | 5B // pop rbx 100 | 5E // pop rsi 101 | 5F // pop rdi 102 | 41 5C // pop r12 103 | 41 5D // pop r13 104 | 41 5E // pop r14 105 | 41 5F // pop r15 106 | 5D // pop rbp 107 | C3 // retn 108 | } // pei386_runtime_relocator 109 | 110 | $b = { 111 | 48 83 EC 28 // sub rsp, 28h 112 | 48 [5-6] // mov rax, cs:_refptr_mingw_app_type 113 | C7 00 00 00 00 00 // mov dword ptr [rax], 0 114 | E8 [4] // call __tmainCRTStartup 115 | 90 // nop 116 | 90 // nop 117 | 48 83 C4 28 // add rsp, 28h 118 | C3 // retn 119 | } 120 | 121 | 122 | 123 | condition: 124 | $a and $b 125 | } 126 | """ 127 | 128 | RULE_MINGW_10_GCC_12_2_0 = """rule rust_mingw10_gcc_12_2_0 { 129 | 130 | strings: 131 | $a = { 132 | 55 // push rbp 133 | 41 57 // push r15 134 | 41 56 // push r14 135 | 41 55 // push r13 136 | 41 54 // push r12 137 | 57 // push rdi 138 | 56 // push rsi 139 | 53 // push rbx 140 | 48 83 EC 48 // sub rsp, 48h 141 | 48 8D 6C 24 40 // lea rbp, [rsp+40h] 142 | 44 [5-6] // mov r12d, cs:was_init_0 143 | 45 85 E4 // test r12d, r12d 144 | 74 17 // jz short loc_1400950A8 145 | 48 8D 65 08 // lea rsp, [rbp+8] 146 | 5B // pop rbx 147 | 5E // pop rsi 148 | 5F // pop rdi 149 | 41 5C // pop r12 150 | 41 5D // pop r13 151 | 41 5E // pop r14 152 | 41 5F // pop r15 153 | 5D // pop rbp 154 | C3 // retn 155 | } // pei386_runtime_relocator 156 | 157 | $b = { 158 | 83 FA 10 // cmp edx, 10h 159 | 0F 85 [4] // jnz loc_1400954A2 160 | 0F B7 37 // movzx esi, word ptr [rdi] 161 | 81 E1 C0 00 00 00 // and ecx, 0C0h 162 | 66 85 F6 // test si, si 163 | 0F 89 [4] // jns loc_140095440 164 | 48 81 CE 00 00 FF FF // or rsi, 0FFFFFFFFFFFF0000h 165 | 48 29 C6 // sub rsi, rax 166 | 4C 01 CE // add rsi, r9 167 | 85 C9 // test ecx, ecx 168 | } // pei386_runtime_relocator 169 | 170 | $c = { 171 | F3 0F 7E 81 98 00 00 00 // movq xmm0, qword ptr [rcx+98h] 172 | 41 0F 16 01 // movhps xmm0, qword ptr [r9] 173 | 0F 11 44 24 30 // movups xmmword ptr [rsp+98h+TargetIp], xmm0 174 | [7-8] // movdqu xmm0, cs:xmmword_1400BBFF0 175 | 0F 11 44 24 40 // movups xmmword ptr [rsp+98h+ReturnValue], xmm0 176 | } // _GCC_specific_handler rust only... Also, depends on personality 177 | 178 | condition: 179 | all of them 180 | 181 | }""" 182 | 183 | RULE_MINGW_11_GCC_13_1_0 = """rule rust_mingw11_gcc_13_1_0 { 184 | 185 | strings: 186 | 187 | $a = { 188 | 55 // push rbp 189 | 41 57 // push r15 190 | 41 56 // push r14 191 | 41 55 // push r13 192 | 41 54 // push r12 193 | 57 // push rdi 194 | 56 // push rsi 195 | 53 // push rbx 196 | 48 83 EC 48 // sub rsp, 48h 197 | 48 8D 6C 24 40 // lea rbp, [rsp+40h] 198 | 44 [5-6] // mov r12d, cs:dword_1400CC290 199 | 45 85 E4 // test r12d, r12d 200 | 74 17 // jz short loc_140097278 201 | 48 8D 65 08 // lea rsp, [rbp+8] 202 | 5B // pop rbx 203 | 5E // pop rsi 204 | 5F // pop rdi 205 | 41 5C // pop r12 206 | 41 5D // pop r13 207 | 41 5E // pop r14 208 | 41 5F // pop r15 209 | 5D // pop rbp 210 | C3 211 | } // pei386_runtime_relocator 212 | $b = { 213 | 83 FA 10 // cmp edx, 10h 214 | 0F 85 [4] // jnz loc_1400954A2 215 | 0F B7 37 // movzx esi, word ptr [rdi] 216 | 81 E1 C0 00 00 00 // and ecx, 0C0h 217 | 66 85 F6 // test si, si 218 | 0F 89 [4] // jns loc_140095440 219 | 48 81 CE 00 00 FF FF // or rsi, 0FFFFFFFFFFFF0000h 220 | 48 29 C6 // sub rsi, rax 221 | 4C 01 CE // add rsi, r9 222 | 85 C9 // test ecx, ecx 223 | } // pei386_runtime_relocator 224 | 225 | $c = { 226 | F3 0F 7E 81 98 00 00 00 // movq xmm0, qword ptr [rcx+98h] 227 | 41 0F 16 01 // movhps xmm0, qword ptr [r9] 228 | 0F 29 44 24 30 // movaps xmmword ptr [rsp+98h+var_68.cfa], xmm0 229 | [7-8] // movdqa xmm0, cs:xmmword_1400BC040 230 | 0F 29 44 24 40 // movaps xmmword ptr [rsp+98h+var_68.reg], xmm0 231 | } 232 | condition: 233 | all of them 234 | }""" 235 | -------------------------------------------------------------------------------- /src/rustbininfo/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | log = logging.getLogger("rustbinsign") 4 | log.addHandler(logging.NullHandler()) 5 | -------------------------------------------------------------------------------- /src/rustbininfo/main.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | from argparse import ArgumentParser, RawDescriptionHelpFormatter 4 | 5 | from rich import print 6 | 7 | from rustbininfo import TargetRustInfo, get_min_max_update_time 8 | from rustbininfo.info.compiler import BasicProvider 9 | from rustbininfo.info.nightly.nightly import NightlyGetter 10 | 11 | DESCRIPTION = """Get information about stripped rust executable, and download its dependencies.""" 12 | 13 | example_text = r"""Usage examples: 14 | 15 | rbi 'challenge.exe' 16 | rbi -d 'challenge.exe' 17 | """ 18 | 19 | 20 | def parse_args() -> ArgumentParser: # noqa: D103 21 | # Main parser 22 | parser = ArgumentParser( 23 | description=DESCRIPTION, 24 | formatter_class=RawDescriptionHelpFormatter, 25 | epilog=example_text, 26 | ) 27 | 28 | parser.add_argument("-f", "--full", action="store_true", default=False, required=False) 29 | 30 | parser.add_argument( 31 | "-d", 32 | "--project-date", 33 | help="Tries to guess date latest dependency got added to the project, based on dependencies version", 34 | required=False, 35 | action="store_true", 36 | ) 37 | 38 | parser.add_argument( 39 | "--version", 40 | help="Get compiler version date", 41 | required=False, 42 | action="store_true", 43 | ) 44 | 45 | parser.add_argument( 46 | "--nightly", 47 | "-n", 48 | help="Get compiler nightly version", 49 | required=False, 50 | action="store_true", 51 | ) 52 | 53 | parser.add_argument( 54 | "--repo", 55 | "-r", 56 | help="Define rust local repository, in case you get rate limited by GitHub. This option requires rustbininfo[gitpython] to be installed.", 57 | required=False, 58 | dest="repo", 59 | ) 60 | 61 | parser.add_argument( 62 | type=str, 63 | dest="target", 64 | ) 65 | 66 | return parser 67 | 68 | 69 | def get_provider(get_repo_provider: str): 70 | provider = BasicProvider() 71 | if get_repo_provider: 72 | from .info.rust_repo_handler import GitRepoProvider 73 | 74 | provider = GitRepoProvider(pathlib.Path(get_repo_provider)) 75 | 76 | return provider 77 | 78 | 79 | def main_cli() -> None: # noqa: D103 80 | parser = parse_args() 81 | args = parser.parse_args() 82 | 83 | if len(sys.argv) == 1: 84 | parser.print_help(sys.stderr) 85 | sys.exit(1) 86 | 87 | provider = get_provider(args.repo) 88 | 89 | if args.project_date or args.version or args.nightly: 90 | t = TargetRustInfo.from_target(args.target, provider=provider) 91 | 92 | if args.nightly: 93 | nightly_getter = NightlyGetter() 94 | date = nightly_getter.get_nightly_toolchain_for_rustc_version(t.rustc_version) 95 | print(f"Nightly version: nightly-{date}- (e.g nightly-{date}-x86_64-unknown-linux-gnu)") 96 | sys.exit(0) 97 | 98 | if args.project_date: 99 | min_date, max_date = get_min_max_update_time(t.dependencies) 100 | print(f"Latest dependency was added between {min_date} and {max_date}") 101 | sys.exit(0) 102 | 103 | if args.version: 104 | date = provider.get_rustc_version_date(t.rustc_version) 105 | print(date) 106 | sys.exit(0) 107 | 108 | print(TargetRustInfo.from_target(args.target, not args.full, provider=provider)) 109 | 110 | 111 | if __name__ == "__main__": 112 | main_cli() 113 | -------------------------------------------------------------------------------- /src/rustbininfo/utils.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import tempfile 3 | from datetime import datetime 4 | 5 | import pytz 6 | 7 | 8 | def get_writable_dir_in_tmp(name: str = __package__) -> pathlib.Path: 9 | """Get a writable directory in temp directory. 10 | 11 | Args: 12 | name (str, optional): Name of the writable directory. Defaults to __package__. 13 | 14 | Returns: 15 | pathlib.Path: Path of the target directory. 16 | 17 | """ 18 | destination_directory = pathlib.Path(tempfile.gettempdir()) / name 19 | destination_directory.mkdir(exist_ok=True) 20 | return destination_directory 21 | 22 | 23 | def get_min_max_update_time(crates: list) -> tuple[datetime, datetime]: 24 | """Get a range of dates from which latest dependency got added to the project. 25 | 26 | Args: 27 | crates (List[Crate]): dependencies of the project 28 | 29 | Returns: 30 | Tuple[int, int]: min_date, max_data 31 | 32 | """ 33 | utc = pytz.UTC 34 | min_date, max_date = ( 35 | utc.localize(datetime.fromtimestamp(0)), # noqa: DTZ006 36 | utc.localize( 37 | datetime.now(), # noqa: DTZ005 38 | ), 39 | ) 40 | 41 | for dep in crates: 42 | for i, version in enumerate(dep.metadata["versions"]): 43 | if version["num"] == dep.version: 44 | d = datetime.strptime(version["created_at"], "%Y-%m-%dT%H:%M:%S.%f%z") 45 | min_date = max(d, min_date) 46 | break 47 | 48 | for dep in crates: 49 | for i, version in enumerate(dep.metadata["versions"]): 50 | if version["num"] == dep.version and i != 0: 51 | d = datetime.strptime( 52 | dep.metadata["versions"][i - 1]["created_at"], 53 | "%Y-%m-%dT%H:%M:%S.%f%z", 54 | ) 55 | if d > min_date: 56 | max_date = d 57 | break 58 | 59 | return min_date, max_date 60 | -------------------------------------------------------------------------------- /tests/archiver.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2801053cbfe1852b2e5f1fcfe48efdbc6843d7673913b899c98cb1fac1a5751c 3 | size 1295360 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/N0fix/rustbininfo/edaf8e59f5f53f252d04ac036ac6518573c4fdfa/tests/conftest.py -------------------------------------------------------------------------------- /tests/crackme.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:917d44a7dd8c946dd5f3f43eb3184b8ce9dd57ebf665c1973579f88bf9db131f 3 | size 198144 4 | -------------------------------------------------------------------------------- /tests/gcc_10.3.0_mingw_8_hello.exe: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4fef04c8c28675c68764f714d33fb9c47d0f731d3ab5bbf700efa280121fd912 3 | size 5195591 4 | -------------------------------------------------------------------------------- /tests/test_download.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from rustbininfo import Crate 5 | 6 | 7 | def test_dates(): 8 | c = Crate.from_depstring("rpecli-0.1.1") 9 | result = c.download() 10 | assert Path(result).exists() 11 | os.unlink(result) 12 | -------------------------------------------------------------------------------- /tests/test_info.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from rustbininfo import TargetRustInfo 4 | 5 | 6 | def test_info(): 7 | target: Path = Path(__file__).parent.joinpath("crackme.exe") 8 | assert target.exists() 9 | t: TargetRustInfo = TargetRustInfo.from_target(target) 10 | assert t.rustc_version == "1.65.0" 11 | assert t.rustc_commit_hash == "9c20b2a8cc7588decb6de25ac6a7912dcef24d65" 12 | assert len(t.dependencies) == 4 13 | assert t.rust_dependencies_imphash == "cd7358d2cd75458edda70d567f1555fa" 14 | assert t.guessed_toolchain == "windows-msvc" 15 | 16 | 17 | def test_info_archiver(): 18 | target: Path = Path(__file__).parent.joinpath("archiver.exe") 19 | assert target.exists() 20 | t: TargetRustInfo = TargetRustInfo.from_target(target) 21 | assert t.rustc_commit_hash == "3c85e56249b0b1942339a6a989a971bf6f1c9e0f" 22 | assert len(t.dependencies) == 27 23 | assert t.rust_dependencies_imphash == "1fc25c0f86878f87c056dea2da6c6458" 24 | assert t.guessed_toolchain == "Mingw-w64 (Mingw8-GCC_10.3.0)" 25 | 26 | 27 | def test_info_mingw_hello(): 28 | target: Path = Path(__file__).parent.joinpath("gcc_10.3.0_mingw_8_hello.exe") 29 | assert target.exists() 30 | t: TargetRustInfo = TargetRustInfo.from_target(target) 31 | assert t.rustc_commit_hash == "129f3b9964af4d4a709d1383930ade12dfe7c081" 32 | assert len(t.dependencies) == 5 33 | assert t.rust_dependencies_imphash == "5aba5db7b50577c1f41434ccf9a8d3a5" 34 | assert t.guessed_toolchain == "Mingw-w64 (Mingw8-GCC_10.3.0)" 35 | -------------------------------------------------------------------------------- /tests/test_timestamp_guess.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from rustbininfo import TargetRustInfo, get_min_max_update_time 4 | 5 | 6 | def test_dates(): 7 | target: Path = Path(__file__).parent.joinpath("crackme.exe") 8 | assert target.exists() 9 | 10 | t: TargetRustInfo = TargetRustInfo.from_target(target) 11 | min_date, max_date = get_min_max_update_time(t.dependencies) 12 | assert str(min_date) == "2022-05-20 02:05:25.815861+00:00" 13 | assert str(max_date) == "2022-11-04 01:47:56.740655+00:00" 14 | --------------------------------------------------------------------------------