d810-cobra 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- d810_cobra-0.1.0/LICENSE +21 -0
- d810_cobra-0.1.0/MANIFEST.in +12 -0
- d810_cobra-0.1.0/PKG-INFO +158 -0
- d810_cobra-0.1.0/README.md +136 -0
- d810_cobra-0.1.0/pyproject.toml +65 -0
- d810_cobra-0.1.0/setup.cfg +4 -0
- d810_cobra-0.1.0/setup.py +174 -0
- d810_cobra-0.1.0/src/cpp/cobra_shim.cpp +222 -0
- d810_cobra-0.1.0/src/d810_cobra/__init__.py +70 -0
- d810_cobra-0.1.0/src/d810_cobra/_cobra.pyx +274 -0
- d810_cobra-0.1.0/src/d810_cobra/convert.py +131 -0
- d810_cobra-0.1.0/src/d810_cobra/detect.py +324 -0
- d810_cobra-0.1.0/src/d810_cobra/escalate.py +171 -0
- d810_cobra-0.1.0/src/d810_cobra/executor.py +206 -0
- d810_cobra-0.1.0/src/d810_cobra/expr.py +170 -0
- d810_cobra-0.1.0/src/d810_cobra/probe.py +80 -0
- d810_cobra-0.1.0/src/d810_cobra/prove.py +130 -0
- d810_cobra-0.1.0/src/d810_cobra/rules/__init__.py +6 -0
- d810_cobra-0.1.0/src/d810_cobra/rules/cobra_solve.py +359 -0
- d810_cobra-0.1.0/src/d810_cobra/solve.py +174 -0
- d810_cobra-0.1.0/src/d810_cobra/store.py +204 -0
- d810_cobra-0.1.0/src/d810_cobra/table.py +319 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/PKG-INFO +158 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/SOURCES.txt +27 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/dependency_links.txt +1 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/entry_points.txt +2 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/requires.txt +4 -0
- d810_cobra-0.1.0/src/d810_cobra.egg-info/top_level.txt +3 -0
- d810_cobra-0.1.0/src/include/cobra_shim.h +90 -0
d810_cobra-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 w00tzenheimer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Build inputs that live OUTSIDE the importable package.
|
|
2
|
+
#
|
|
3
|
+
# setuptools builds the sdist from packages + package-data, and package-data
|
|
4
|
+
# only reaches files inside a package. src/include and src/cpp are neither, so
|
|
5
|
+
# without these lines the sdist would omit the header and the C++ shim -- and
|
|
6
|
+
# `pip install d810-cobra` from source would fail at compile with a missing
|
|
7
|
+
# cobra_shim.h. The sdist IS published (deploy.yml build_sdist), so this is not
|
|
8
|
+
# hypothetical.
|
|
9
|
+
include LICENSE
|
|
10
|
+
include README.md
|
|
11
|
+
recursive-include src/include *.h
|
|
12
|
+
recursive-include src/cpp *.cpp
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: d810-cobra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CoBRA MBA-solver backend for d810 (mba-solve)
|
|
5
|
+
Author: w00tzenheimer
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/w00tzenheimer/d810-CoBRA
|
|
8
|
+
Project-URL: Issues, https://github.com/w00tzenheimer/d810-CoBRA/issues
|
|
9
|
+
Keywords: deobfuscation,mba,ida-pro,hex-rays,cobra
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: d810-ng>=0.6.6
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# d810-cobra
|
|
24
|
+
|
|
25
|
+
[](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml)
|
|
26
|
+
[](pyproject.toml)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](https://github.com/w00tzenheimer/d810-ng)
|
|
29
|
+
|
|
30
|
+
CoBRA MBA-solver backend for [d810](https://github.com/w00tzenheimer/d810-ng) —
|
|
31
|
+
the `mba-solve` pass.
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
pip install d810-cobra
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That is the whole installation. d810 discovers this package automatically; no
|
|
38
|
+
configuration, no `COBRA_ROOT`, no CMake, no C++23 toolchain on the user's
|
|
39
|
+
machine.
|
|
40
|
+
|
|
41
|
+
## Why it is a separate package
|
|
42
|
+
|
|
43
|
+
d810's 203 `mba-simplify` transforms are pattern-matched identities. On
|
|
44
|
+
coefficient-based linear MBA they fire zero times — measured, not assumed.
|
|
45
|
+
[CoBRA](https://github.com/trailofbits/CoBRA) is a signature-driven solver that
|
|
46
|
+
closes exactly that gap.
|
|
47
|
+
|
|
48
|
+
Shipping it inside d810 meant every d810 wheel carried a C++23 build of abseil,
|
|
49
|
+
highway and cobra-core, and CoBRA's version was pinned to a d810 commit. Split
|
|
50
|
+
out, d810's wheel stays pure and the two version independently.
|
|
51
|
+
|
|
52
|
+
## How d810 finds it
|
|
53
|
+
|
|
54
|
+
One entry point, in the unversioned `d810.backends` group:
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[project.entry-points."d810.backends"]
|
|
58
|
+
cobra = "d810_cobra:MANIFEST"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
MANIFEST = {
|
|
63
|
+
"name": "cobra",
|
|
64
|
+
"api_version": 1,
|
|
65
|
+
"provides": "d810_cobra.solve",
|
|
66
|
+
"rules": ("d810_cobra.rules.cobra_solve",),
|
|
67
|
+
"implements": {"mba-solve": "CobraSolveRule"},
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`MANIFEST` is a plain dict — deliberately not d810's `BackendManifest`. The
|
|
72
|
+
package depends on d810 at runtime, but the *manifest* must not: importing
|
|
73
|
+
`BackendManifest` would turn "this d810 predates the plugin protocol" from a
|
|
74
|
+
clean "backend not discovered" into an ImportError during d810 startup.
|
|
75
|
+
|
|
76
|
+
Its `provides` is a *string*, resolved lazily, so a version-incompatible d810
|
|
77
|
+
rejects this backend after reading three fields — without importing `solve.py`
|
|
78
|
+
and therefore without loading the compiled extension.
|
|
79
|
+
|
|
80
|
+
`rules` and `implements` are what make the pass actually *run*, and each closes
|
|
81
|
+
a failure that is silent without it:
|
|
82
|
+
|
|
83
|
+
- **`rules`** — d810 registers its own optimizer rules by scanning
|
|
84
|
+
`d810.optimizers.__path__`. That scan is path-scoped and cannot reach a rule
|
|
85
|
+
living inside this package. Without declaring it, the backend reports
|
|
86
|
+
`available` while `CobraSolveRule` never registers — indistinguishable from a
|
|
87
|
+
pass that ran and matched nothing. d810 imports these only after the backend
|
|
88
|
+
probes usable, so a missing binding yields no rule rather than a rule that
|
|
89
|
+
raises on every call.
|
|
90
|
+
- **`implements`** — d810 derives a pass's `allowed_rule_names` from it at
|
|
91
|
+
registration time, long before rules are imported; a rule outside that
|
|
92
|
+
allowlist is skipped at dispatch. Declaring it here is what let d810 stop
|
|
93
|
+
hardcoding `"CobraSolveRule"` in its own source. The key is d810's pass id
|
|
94
|
+
(`d810.core.pass_ids.PassId.MBA_SOLVE`), written as a plain string so that
|
|
95
|
+
declaring a manifest still requires no d810 import — `PassId` is a
|
|
96
|
+
`StrEnum`, so the two compare and hash identically.
|
|
97
|
+
|
|
98
|
+
d810 itself is a hard dependency (`solve.py` uses `d810.core.getLogger`,
|
|
99
|
+
`table.py` uses `d810.core.cache`, `convert.py`/`detect.py` use
|
|
100
|
+
`d810.hexrays.*`). Some of those are d810 internals rather than a published
|
|
101
|
+
API, so a d810 refactor can break this package without either side bumping a
|
|
102
|
+
major version.
|
|
103
|
+
|
|
104
|
+
d810 ships no MBA solver of its own — `cobra` is not one of its builtin
|
|
105
|
+
backends — so this package supplies the `mba-solve` implementation rather than
|
|
106
|
+
overriding one:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
cobra available d810-cobra 0.1.0
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Check it with `d810cli backends`. Without this package installed, d810's
|
|
113
|
+
`mba-solve` pass resolves no implementation and contributes no stages.
|
|
114
|
+
|
|
115
|
+
## Building from source
|
|
116
|
+
|
|
117
|
+
```console
|
|
118
|
+
git clone --recursive https://github.com/w00tzenheimer/d810-CoBRA
|
|
119
|
+
cd d810-CoBRA
|
|
120
|
+
python tools/build_cobra.py # abseil + highway + cobra-core (CMake + Ninja)
|
|
121
|
+
pip install -e .
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`tools/build_cobra.py` needs CMake and Ninja, and a C++23 compiler. On Windows
|
|
125
|
+
it pins MSVC (`cl`) explicitly: `-G Ninja` with no compiler pinned picks
|
|
126
|
+
whatever is first in `PATH`, and a MinGW build produces `libabsl_*.a` archives
|
|
127
|
+
that an MSVC-built `.pyd` can neither `/WHOLEARCHIVE:` nor safely link against.
|
|
128
|
+
|
|
129
|
+
The build **fails loudly** rather than producing a package without the binding.
|
|
130
|
+
A wheel that installs cleanly and simplifies nothing is the failure mode this
|
|
131
|
+
package exists to make impossible.
|
|
132
|
+
|
|
133
|
+
## Layout
|
|
134
|
+
|
|
135
|
+
| path | what |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `src/d810_cobra/expr.py` | parse/evaluate/accept — pure data, no IDA |
|
|
138
|
+
| `src/d810_cobra/probe.py` | locate `cobra-cli`, or report a structured skip |
|
|
139
|
+
| `src/d810_cobra/solve.py` | the backend entry point d810 resolves |
|
|
140
|
+
| `src/d810_cobra/_cobra.pyx` | Cython binding over `cobra_shim.cpp` |
|
|
141
|
+
| `src/d810_cobra/rules/` | the `mba-solve` peephole rule (needs d810 + Hex-Rays) |
|
|
142
|
+
| `src/include/cobra_shim.h` | C ABI the binding compiles against |
|
|
143
|
+
| `src/cpp/cobra_shim.cpp` | C++ shim over cobra-core |
|
|
144
|
+
| `third_party/cobra` | pinned CoBRA submodule |
|
|
145
|
+
|
|
146
|
+
Headers and the C++ shim sit outside the package, mirroring d810's own
|
|
147
|
+
`src/include`. They are build inputs, so the wheel ships only what is
|
|
148
|
+
importable; `MANIFEST.in` is what carries them into the sdist. Note that
|
|
149
|
+
setuptools auto-includes a declared Extension *source* but never an
|
|
150
|
+
`include_dirs` header — drop `MANIFEST.in` and the sdist still contains
|
|
151
|
+
`cobra_shim.cpp` while silently losing `cobra_shim.h`.
|
|
152
|
+
|
|
153
|
+
Everything that touches `ida_hexrays` lives under `rules/`, so the solver core
|
|
154
|
+
stays unit-testable without IDA.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT. CoBRA itself is vendored as a submodule under its own license.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# d810-cobra
|
|
2
|
+
|
|
3
|
+
[](https://github.com/w00tzenheimer/d810-CoBRA/actions/workflows/ci.yml)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/w00tzenheimer/d810-ng)
|
|
7
|
+
|
|
8
|
+
CoBRA MBA-solver backend for [d810](https://github.com/w00tzenheimer/d810-ng) —
|
|
9
|
+
the `mba-solve` pass.
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
pip install d810-cobra
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That is the whole installation. d810 discovers this package automatically; no
|
|
16
|
+
configuration, no `COBRA_ROOT`, no CMake, no C++23 toolchain on the user's
|
|
17
|
+
machine.
|
|
18
|
+
|
|
19
|
+
## Why it is a separate package
|
|
20
|
+
|
|
21
|
+
d810's 203 `mba-simplify` transforms are pattern-matched identities. On
|
|
22
|
+
coefficient-based linear MBA they fire zero times — measured, not assumed.
|
|
23
|
+
[CoBRA](https://github.com/trailofbits/CoBRA) is a signature-driven solver that
|
|
24
|
+
closes exactly that gap.
|
|
25
|
+
|
|
26
|
+
Shipping it inside d810 meant every d810 wheel carried a C++23 build of abseil,
|
|
27
|
+
highway and cobra-core, and CoBRA's version was pinned to a d810 commit. Split
|
|
28
|
+
out, d810's wheel stays pure and the two version independently.
|
|
29
|
+
|
|
30
|
+
## How d810 finds it
|
|
31
|
+
|
|
32
|
+
One entry point, in the unversioned `d810.backends` group:
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[project.entry-points."d810.backends"]
|
|
36
|
+
cobra = "d810_cobra:MANIFEST"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
MANIFEST = {
|
|
41
|
+
"name": "cobra",
|
|
42
|
+
"api_version": 1,
|
|
43
|
+
"provides": "d810_cobra.solve",
|
|
44
|
+
"rules": ("d810_cobra.rules.cobra_solve",),
|
|
45
|
+
"implements": {"mba-solve": "CobraSolveRule"},
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`MANIFEST` is a plain dict — deliberately not d810's `BackendManifest`. The
|
|
50
|
+
package depends on d810 at runtime, but the *manifest* must not: importing
|
|
51
|
+
`BackendManifest` would turn "this d810 predates the plugin protocol" from a
|
|
52
|
+
clean "backend not discovered" into an ImportError during d810 startup.
|
|
53
|
+
|
|
54
|
+
Its `provides` is a *string*, resolved lazily, so a version-incompatible d810
|
|
55
|
+
rejects this backend after reading three fields — without importing `solve.py`
|
|
56
|
+
and therefore without loading the compiled extension.
|
|
57
|
+
|
|
58
|
+
`rules` and `implements` are what make the pass actually *run*, and each closes
|
|
59
|
+
a failure that is silent without it:
|
|
60
|
+
|
|
61
|
+
- **`rules`** — d810 registers its own optimizer rules by scanning
|
|
62
|
+
`d810.optimizers.__path__`. That scan is path-scoped and cannot reach a rule
|
|
63
|
+
living inside this package. Without declaring it, the backend reports
|
|
64
|
+
`available` while `CobraSolveRule` never registers — indistinguishable from a
|
|
65
|
+
pass that ran and matched nothing. d810 imports these only after the backend
|
|
66
|
+
probes usable, so a missing binding yields no rule rather than a rule that
|
|
67
|
+
raises on every call.
|
|
68
|
+
- **`implements`** — d810 derives a pass's `allowed_rule_names` from it at
|
|
69
|
+
registration time, long before rules are imported; a rule outside that
|
|
70
|
+
allowlist is skipped at dispatch. Declaring it here is what let d810 stop
|
|
71
|
+
hardcoding `"CobraSolveRule"` in its own source. The key is d810's pass id
|
|
72
|
+
(`d810.core.pass_ids.PassId.MBA_SOLVE`), written as a plain string so that
|
|
73
|
+
declaring a manifest still requires no d810 import — `PassId` is a
|
|
74
|
+
`StrEnum`, so the two compare and hash identically.
|
|
75
|
+
|
|
76
|
+
d810 itself is a hard dependency (`solve.py` uses `d810.core.getLogger`,
|
|
77
|
+
`table.py` uses `d810.core.cache`, `convert.py`/`detect.py` use
|
|
78
|
+
`d810.hexrays.*`). Some of those are d810 internals rather than a published
|
|
79
|
+
API, so a d810 refactor can break this package without either side bumping a
|
|
80
|
+
major version.
|
|
81
|
+
|
|
82
|
+
d810 ships no MBA solver of its own — `cobra` is not one of its builtin
|
|
83
|
+
backends — so this package supplies the `mba-solve` implementation rather than
|
|
84
|
+
overriding one:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
cobra available d810-cobra 0.1.0
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Check it with `d810cli backends`. Without this package installed, d810's
|
|
91
|
+
`mba-solve` pass resolves no implementation and contributes no stages.
|
|
92
|
+
|
|
93
|
+
## Building from source
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
git clone --recursive https://github.com/w00tzenheimer/d810-CoBRA
|
|
97
|
+
cd d810-CoBRA
|
|
98
|
+
python tools/build_cobra.py # abseil + highway + cobra-core (CMake + Ninja)
|
|
99
|
+
pip install -e .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`tools/build_cobra.py` needs CMake and Ninja, and a C++23 compiler. On Windows
|
|
103
|
+
it pins MSVC (`cl`) explicitly: `-G Ninja` with no compiler pinned picks
|
|
104
|
+
whatever is first in `PATH`, and a MinGW build produces `libabsl_*.a` archives
|
|
105
|
+
that an MSVC-built `.pyd` can neither `/WHOLEARCHIVE:` nor safely link against.
|
|
106
|
+
|
|
107
|
+
The build **fails loudly** rather than producing a package without the binding.
|
|
108
|
+
A wheel that installs cleanly and simplifies nothing is the failure mode this
|
|
109
|
+
package exists to make impossible.
|
|
110
|
+
|
|
111
|
+
## Layout
|
|
112
|
+
|
|
113
|
+
| path | what |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `src/d810_cobra/expr.py` | parse/evaluate/accept — pure data, no IDA |
|
|
116
|
+
| `src/d810_cobra/probe.py` | locate `cobra-cli`, or report a structured skip |
|
|
117
|
+
| `src/d810_cobra/solve.py` | the backend entry point d810 resolves |
|
|
118
|
+
| `src/d810_cobra/_cobra.pyx` | Cython binding over `cobra_shim.cpp` |
|
|
119
|
+
| `src/d810_cobra/rules/` | the `mba-solve` peephole rule (needs d810 + Hex-Rays) |
|
|
120
|
+
| `src/include/cobra_shim.h` | C ABI the binding compiles against |
|
|
121
|
+
| `src/cpp/cobra_shim.cpp` | C++ shim over cobra-core |
|
|
122
|
+
| `third_party/cobra` | pinned CoBRA submodule |
|
|
123
|
+
|
|
124
|
+
Headers and the C++ shim sit outside the package, mirroring d810's own
|
|
125
|
+
`src/include`. They are build inputs, so the wheel ships only what is
|
|
126
|
+
importable; `MANIFEST.in` is what carries them into the sdist. Note that
|
|
127
|
+
setuptools auto-includes a declared Extension *source* but never an
|
|
128
|
+
`include_dirs` header — drop `MANIFEST.in` and the sdist still contains
|
|
129
|
+
`cobra_shim.cpp` while silently losing `cobra_shim.h`.
|
|
130
|
+
|
|
131
|
+
Everything that touches `ida_hexrays` lives under `rules/`, so the solver core
|
|
132
|
+
stays unit-testable without IDA.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. CoBRA itself is vendored as a submodule under its own license.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.1", "wheel", "Cython>=3.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Must match the PyPI Trusted Publisher registration exactly.
|
|
7
|
+
name = "d810-cobra"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "CoBRA MBA-solver backend for d810 (mba-solve)"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">= 3.10"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
authors = [{ name = "w00tzenheimer" }]
|
|
14
|
+
keywords = ["deobfuscation", "mba", "ida-pro", "hex-rays", "cobra"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# d810 IS a hard runtime dependency, including for the entry-point target:
|
|
24
|
+
# solve.py imports d810.core.getLogger, table.py needs d810.core.cache.CacheImpl
|
|
25
|
+
# and d810.core.typing, convert.py/detect.py need d810.hexrays.*. Only `expr`
|
|
26
|
+
# and `probe` are genuinely standalone.
|
|
27
|
+
#
|
|
28
|
+
# Not circular: d810 never imports this package, it only discovers it through
|
|
29
|
+
# the entry point.
|
|
30
|
+
#
|
|
31
|
+
# Caveat worth knowing: several of these are d810 INTERNALS rather than a
|
|
32
|
+
# published API (d810.core.cache, d810.core.typing), so a d810 refactor can
|
|
33
|
+
# break this package without a major version bump on either side.
|
|
34
|
+
dependencies = ["d810-ng>=0.6.6"]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
test = ["pytest>=8.0"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/w00tzenheimer/d810-CoBRA"
|
|
41
|
+
Issues = "https://github.com/w00tzenheimer/d810-CoBRA/issues"
|
|
42
|
+
|
|
43
|
+
# This is the whole integration surface. d810's BackendRegistry scans this
|
|
44
|
+
# group before its builtins, so an installed copy of this package supersedes
|
|
45
|
+
# anything d810 ships under the same name. The group is deliberately
|
|
46
|
+
# unversioned -- the compatibility contract lives in the manifest's
|
|
47
|
+
# api_version, not in the entry-point name.
|
|
48
|
+
[project.entry-points."d810.backends"]
|
|
49
|
+
cobra = "d810_cobra:MANIFEST"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
package-dir = { "" = "src" }
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["src"]
|
|
56
|
+
|
|
57
|
+
[tool.setuptools.package-data]
|
|
58
|
+
# cobra_shim.h is no longer here: it moved to src/include, matching d810's own
|
|
59
|
+
# layout. It reaches the sdist through MANIFEST.in instead -- package-data only
|
|
60
|
+
# covers files inside an importable package, and a header is a build input, not
|
|
61
|
+
# something a wheel consumer imports.
|
|
62
|
+
d810_cobra = ["*.pyx", "*.pxd"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests/unit"]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Build the CoBRA solver binding.
|
|
2
|
+
|
|
3
|
+
Unlike d810, where this extension was optional and the wheel had to remain
|
|
4
|
+
installable without it, the binding is the entire point of this package. So a
|
|
5
|
+
build that cannot produce it FAILS rather than quietly yielding an inert
|
|
6
|
+
install -- the failure mode we spent a long time chasing in d810 was exactly a
|
|
7
|
+
wheel that installed cleanly and simplified nothing.
|
|
8
|
+
|
|
9
|
+
The C++ side is built by ``tools/build_cobra.py`` (CMake + Ninja), which
|
|
10
|
+
produces the layout consumed here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import pathlib
|
|
17
|
+
import platform
|
|
18
|
+
|
|
19
|
+
from setuptools import setup
|
|
20
|
+
|
|
21
|
+
OSTYPE = platform.system()
|
|
22
|
+
HERE = pathlib.Path(__file__).parent.resolve()
|
|
23
|
+
|
|
24
|
+
#: Layout mirrors d810 itself, which keeps shared C headers in ``src/include``
|
|
25
|
+
#: rather than inside the importable package. Both ``_cobra.pyx`` and
|
|
26
|
+
#: ``cobra_shim.cpp`` include the header by bare name, so this must be on the
|
|
27
|
+
#: include path for either to compile.
|
|
28
|
+
#:
|
|
29
|
+
#: Keeping them out of ``src/d810_cobra`` means the wheel ships only what is
|
|
30
|
+
#: importable: the header and the C++ shim are build inputs, and a consumer who
|
|
31
|
+
#: pip-installs a built wheel has no use for either.
|
|
32
|
+
SRC = HERE / "src"
|
|
33
|
+
INCLUDE_DIR = SRC / "include"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _first_existing(root: pathlib.Path, candidates: tuple[str, ...]):
|
|
37
|
+
for rel in candidates:
|
|
38
|
+
candidate = root / rel
|
|
39
|
+
if candidate.exists():
|
|
40
|
+
return candidate
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_cobra_ext_modules():
|
|
45
|
+
try:
|
|
46
|
+
from Cython.Build import cythonize
|
|
47
|
+
from setuptools import Extension
|
|
48
|
+
except ImportError: # pragma: no cover - build-time only
|
|
49
|
+
raise ImportError("Cython is required to build the CoBRA binding")
|
|
50
|
+
|
|
51
|
+
# Default to the vendored submodule so `pip install -e .` works in a
|
|
52
|
+
# checkout with no environment set up. CI overrides this for the flat
|
|
53
|
+
# artifact layout.
|
|
54
|
+
cobra_root = os.environ.get("COBRA_ROOT") or str(HERE / "third_party" / "cobra")
|
|
55
|
+
root = pathlib.Path(cobra_root)
|
|
56
|
+
if not root.is_dir():
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"COBRA_ROOT={root} does not exist. Either init the submodule\n"
|
|
59
|
+
" git submodule update --init --recursive\n"
|
|
60
|
+
"and run python tools/build_cobra.py , or point COBRA_ROOT at a\n"
|
|
61
|
+
"prebuilt cobra-core bundle."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# TWO accepted layouts. The flat one exists so a released/CI static-lib
|
|
65
|
+
# bundle can be consumed directly -- requiring callers to reshape a
|
|
66
|
+
# download into a fake build tree is pure friction, and reshaping inside CI
|
|
67
|
+
# just relocates it.
|
|
68
|
+
#
|
|
69
|
+
# FLAT (release / CI artifact): <root>/lib/*.{a,lib} <root>/include/
|
|
70
|
+
# BUILD TREE (local checkout): <root>/build/lib/core/
|
|
71
|
+
# <root>/build-deps/install/{lib*,include}/
|
|
72
|
+
flat_lib = root / "lib"
|
|
73
|
+
flat_inc = root / "include"
|
|
74
|
+
is_flat = flat_lib.is_dir() and any(flat_lib.glob("*cobra-core*"))
|
|
75
|
+
|
|
76
|
+
if is_flat:
|
|
77
|
+
include_dirs = [str(INCLUDE_DIR), str(flat_inc)]
|
|
78
|
+
library_dirs = [str(flat_lib)]
|
|
79
|
+
else:
|
|
80
|
+
# Pick ONE dependency prefix and one core build; globbing several and
|
|
81
|
+
# merging them silently mixes incompatible trees.
|
|
82
|
+
deps_prefix = _first_existing(
|
|
83
|
+
root, ("build-deps/install", "build-deps-nollvm/install")
|
|
84
|
+
)
|
|
85
|
+
core_dir = _first_existing(root, ("build/lib/core", "build-nollvm/lib/core"))
|
|
86
|
+
if deps_prefix is None or core_dir is None:
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
f"COBRA_ROOT={root} does not look built. Accepted layouts:\n"
|
|
89
|
+
f" flat: {root}/lib/*cobra-core* + {root}/include/\n"
|
|
90
|
+
f" build tree: {root}/build/lib/core + {root}/build-deps/install\n"
|
|
91
|
+
"Run: python tools/build_cobra.py"
|
|
92
|
+
)
|
|
93
|
+
include_dirs = [
|
|
94
|
+
str(INCLUDE_DIR),
|
|
95
|
+
str(root / "include"),
|
|
96
|
+
str(deps_prefix / "include"),
|
|
97
|
+
]
|
|
98
|
+
# lib vs lib64: manylinux is RHEL-based, where CMAKE_INSTALL_LIBDIR
|
|
99
|
+
# defaults to lib64, so never hardcode "lib".
|
|
100
|
+
library_dirs = [str(core_dir)] + [
|
|
101
|
+
str(p) for p in sorted(deps_prefix.glob("lib*")) if p.is_dir()
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# CoBRA requires C++23 (Result.h uses std::expected).
|
|
105
|
+
std_args = ["/std:c++latest"] if OSTYPE == "Windows" else ["-std=c++23"]
|
|
106
|
+
|
|
107
|
+
# abseil scatters constants and singletons (container_internal::kSooControl,
|
|
108
|
+
# MixingHashState::kSeed, Mutex, Now, ...) across archive members that
|
|
109
|
+
# nothing else references, so a normal -l link never pulls them in. Python
|
|
110
|
+
# extensions link with -undefined dynamic_lookup, so this does NOT fail the
|
|
111
|
+
# link -- it fails later at dlopen with "symbol not found in flat
|
|
112
|
+
# namespace", which is much harder to diagnose. Force every abseil archive
|
|
113
|
+
# in and let the linker dead-strip the remainder.
|
|
114
|
+
#
|
|
115
|
+
# MSVC emits absl_base.lib; Unix toolchains emit libabsl_base.a. Globbing
|
|
116
|
+
# only the Unix shape silently yields NOTHING on Windows and the force-load
|
|
117
|
+
# never happens -- again a load-time failure, not a link-time one. An empty
|
|
118
|
+
# match is therefore fatal.
|
|
119
|
+
_absl_pattern = "absl_*.lib" if OSTYPE == "Windows" else "libabsl_*.a"
|
|
120
|
+
absl_archives = [
|
|
121
|
+
str(p) for d in library_dirs for p in sorted(pathlib.Path(d).glob(_absl_pattern))
|
|
122
|
+
]
|
|
123
|
+
if not absl_archives:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
f"no abseil archives matched {_absl_pattern!r} under {library_dirs}. "
|
|
126
|
+
"Without force-loading them the extension links but fails at import "
|
|
127
|
+
"with missing absl symbols; refusing to build a broken binding."
|
|
128
|
+
)
|
|
129
|
+
if OSTYPE == "Darwin":
|
|
130
|
+
link_args = [f"-Wl,-force_load,{a}" for a in absl_archives]
|
|
131
|
+
link_args.append("-Wl,-dead_strip")
|
|
132
|
+
elif OSTYPE == "Linux":
|
|
133
|
+
link_args = [
|
|
134
|
+
"-Wl,--whole-archive",
|
|
135
|
+
*absl_archives,
|
|
136
|
+
"-Wl,--no-whole-archive",
|
|
137
|
+
"-Wl,--gc-sections",
|
|
138
|
+
]
|
|
139
|
+
else:
|
|
140
|
+
link_args = [f"/WHOLEARCHIVE:{a}" for a in absl_archives]
|
|
141
|
+
|
|
142
|
+
return cythonize(
|
|
143
|
+
Extension(
|
|
144
|
+
"d810_cobra._cobra",
|
|
145
|
+
[
|
|
146
|
+
"src/d810_cobra/_cobra.pyx",
|
|
147
|
+
"src/cpp/cobra_shim.cpp",
|
|
148
|
+
],
|
|
149
|
+
language="c++",
|
|
150
|
+
include_dirs=include_dirs,
|
|
151
|
+
library_dirs=library_dirs,
|
|
152
|
+
libraries=["cobra-core"],
|
|
153
|
+
extra_compile_args=std_args,
|
|
154
|
+
extra_link_args=link_args,
|
|
155
|
+
),
|
|
156
|
+
compiler_directives={"language_level": "3", "binding": True},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
#: Building an sdist compiles nothing -- it packs sources -- but setuptools
|
|
161
|
+
#: evaluates ``ext_modules`` on EVERY PEP 517 hook, including
|
|
162
|
+
#: ``get_requires_for_build_sdist``, which runs before anything is packed. So
|
|
163
|
+
#: ``python -m build --sdist`` demanded a fully built cobra-core just to produce
|
|
164
|
+
#: a tarball, and deploy.yml's build_sdist job (which has no CoBRA build step by
|
|
165
|
+
#: design) died in 8s with the "does not look built" error.
|
|
166
|
+
#:
|
|
167
|
+
#: Deliberately opt-IN, rather than "return [] whenever CoBRA is missing": the
|
|
168
|
+
#: latter would let a *wheel* build silently produce a package with no binding.
|
|
169
|
+
#: That is precisely the failure this package refuses -- a wheel that installs
|
|
170
|
+
#: cleanly and simplifies nothing. Skipping the extension must require someone
|
|
171
|
+
#: to say so explicitly, and only for the one artifact that cannot contain it.
|
|
172
|
+
SDIST_ONLY = os.environ.get("D810_COBRA_SDIST_ONLY") == "1"
|
|
173
|
+
|
|
174
|
+
setup(ext_modules=[] if SDIST_ONLY else get_cobra_ext_modules())
|