snakecharm 0.1.1__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.
- snakecharm-0.1.1/.gitignore +21 -0
- snakecharm-0.1.1/LICENSE +21 -0
- snakecharm-0.1.1/PKG-INFO +203 -0
- snakecharm-0.1.1/README.md +183 -0
- snakecharm-0.1.1/pyproject.toml +117 -0
- snakecharm-0.1.1/snakecharm/__init__.py +385 -0
- snakecharm-0.1.1/snakecharm/clean/__init__.py +26 -0
- snakecharm-0.1.1/snakecharm/clean/passes.py +352 -0
- snakecharm-0.1.1/snakecharm/clean/rename_survey.py +174 -0
- snakecharm-0.1.1/snakecharm/clean/session.py +239 -0
- snakecharm-0.1.1/snakecharm/cli.py +327 -0
- snakecharm-0.1.1/snakecharm/decompile/__init__.py +20 -0
- snakecharm-0.1.1/snakecharm/decompile/backends.py +146 -0
- snakecharm-0.1.1/snakecharm/decompile/router.py +154 -0
- snakecharm-0.1.1/snakecharm/errors.py +84 -0
- snakecharm-0.1.1/snakecharm/evalx/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/evalx/budgets.py +265 -0
- snakecharm-0.1.1/snakecharm/evalx/interp.py +1280 -0
- snakecharm-0.1.1/snakecharm/evalx/resolve.py +263 -0
- snakecharm-0.1.1/snakecharm/evalx/whitelist.py +748 -0
- snakecharm-0.1.1/snakecharm/ingest/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/ingest/normalize.py +184 -0
- snakecharm-0.1.1/snakecharm/ingest/pyc.py +90 -0
- snakecharm-0.1.1/snakecharm/ingest/registry.py +117 -0
- snakecharm-0.1.1/snakecharm/ingest/source.py +73 -0
- snakecharm-0.1.1/snakecharm/ir/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/ir/codeunit.py +186 -0
- snakecharm-0.1.1/snakecharm/ir/disasm.py +295 -0
- snakecharm-0.1.1/snakecharm/ir/gen_tables.py +287 -0
- snakecharm-0.1.1/snakecharm/ir/marshal_.py +450 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py310.json +765 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py311.json +944 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py312.json +1122 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py313.json +1145 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py314.json +1175 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py38.json +460 -0
- snakecharm-0.1.1/snakecharm/ir/tables/py39.json +453 -0
- snakecharm-0.1.1/snakecharm/peel/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/peel/driver.py +341 -0
- snakecharm-0.1.1/snakecharm/peel/layer.py +73 -0
- snakecharm-0.1.1/snakecharm/report/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/report/emit.py +83 -0
- snakecharm-0.1.1/snakecharm/triage/__init__.py +0 -0
- snakecharm-0.1.1/snakecharm/triage/db/families.yaml +288 -0
- snakecharm-0.1.1/snakecharm/triage/detect.py +321 -0
- snakecharm-0.1.1/snakecharm/triage/signatures.py +276 -0
- snakecharm-0.1.1/snakecharm/unresolved.py +116 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.coverage
|
|
7
|
+
.coverage.*
|
|
8
|
+
htmlcov/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
|
|
11
|
+
# The fixture corpus is a build artifact of tests/make_corpus.py, regenerated
|
|
12
|
+
# on demand by conftest. Half-committing it is how a fresh clone ended up with
|
|
13
|
+
# a corpus that looked complete and was missing real.pyc.
|
|
14
|
+
tests/corpus/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
|
|
18
|
+
# Research area: obfuscator tools, generated fixtures, and live malware samples.
|
|
19
|
+
# Nothing in here is ever committed. Samples are third-party code we have no
|
|
20
|
+
# right to redistribute, and `git add -A` would put them in history permanently.
|
|
21
|
+
research/
|
snakecharm-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 snakecharm
|
|
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,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snakecharm
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Deobfuscate Python without executing it. A webcrack for Python.
|
|
5
|
+
Project-URL: Homepage, https://veryserious.systems
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: deobfuscation,malware-analysis,obfuscation,python,reverse-engineering
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
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.11
|
|
16
|
+
Requires-Dist: pyyaml>=6.0
|
|
17
|
+
Requires-Dist: rich>=13.0
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# snakecharm
|
|
22
|
+
|
|
23
|
+
**Python De-Obfuscation**
|
|
24
|
+
|
|
25
|
+
Unwraps obfuscated Python — `exec`/`eval`/`compile` chains over base64, hex,
|
|
26
|
+
zlib/gzip/bz2/lzma, XOR, `marshal`, reversal and `chr`/`ord` reconstruction —
|
|
27
|
+
without executing the sample.
|
|
28
|
+
|
|
29
|
+
Requires Python 3.11+. Pure Python; depends on `typer`, `rich`, `pyyaml`.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
pip install snakecharm
|
|
35
|
+
uv tool install snakecharm # isolated, puts snakecharm on PATH
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
snakecharm crack FILE [-o DIR] [--json] [--clean]
|
|
42
|
+
snakecharm detect FILE [--json]
|
|
43
|
+
snakecharm inspect FILE [--target-python X.Y]
|
|
44
|
+
snakecharm version
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| | |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `crack` | Unwrap. Recovered source to stdout, diagnostics to stderr. |
|
|
50
|
+
| `detect` | Fingerprint the obfuscator family; do not unwrap. |
|
|
51
|
+
| `inspect` | Disassemble a `.pyc` or bare marshal stream. |
|
|
52
|
+
| `version` | Print the version. |
|
|
53
|
+
|
|
54
|
+
`FILE` may be `-` to read from stdin.
|
|
55
|
+
|
|
56
|
+
Cleanup is a flag on `crack` (`--clean`), not a separate command.
|
|
57
|
+
|
|
58
|
+
### `crack` options
|
|
59
|
+
|
|
60
|
+
| Flag | Default | Meaning |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `-o, --output DIR` | — | Write the full artifact tree instead of stdout. |
|
|
63
|
+
| `--json` | off | Emit `report.json` to stdout. |
|
|
64
|
+
| `--clean` | off | Also emit a cleaned view (see [Cleanup](#cleanup)). |
|
|
65
|
+
| `--target-python X.Y` | host | Version to assume for marshal payloads. |
|
|
66
|
+
| `--decompiler NAME` | auto | Force a backend instead of routing by version. |
|
|
67
|
+
| `--max-layers N` | 512 | Layer cap; `0` for no limit. |
|
|
68
|
+
| `--max-memory MiB` | 1024 | Total decoded output; `0` for no limit. |
|
|
69
|
+
| `--max-fold MiB` | 256 | Largest single decoded value; `0` for no limit. |
|
|
70
|
+
| `--timeout SEC` | 300 | Wall clock; `0` for no limit. |
|
|
71
|
+
| `--no-peel` | off | Skip unwrapping; report detection only. |
|
|
72
|
+
| `--no-detect` | off | Skip fingerprinting. |
|
|
73
|
+
| `--plugins` | off | Load third-party ingest adapters. |
|
|
74
|
+
| `-q, --quiet` | off | Suppress the stderr summary. |
|
|
75
|
+
|
|
76
|
+
Exit codes: `0` complete, `1` incomplete, `2` input could not be processed.
|
|
77
|
+
Identifying the obfuscator does not affect the exit code.
|
|
78
|
+
|
|
79
|
+
## Output
|
|
80
|
+
|
|
81
|
+
```console
|
|
82
|
+
$ snakecharm crack sample.py
|
|
83
|
+
import os
|
|
84
|
+
import sys
|
|
85
|
+
|
|
86
|
+
MESSAGE = "recovered exactly"
|
|
87
|
+
LIMIT = 42
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
31 layers, depth 30, 431,204 bytes folded in 0.006s
|
|
91
|
+
detected: freecodingtools (100%)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
With `-o`:
|
|
95
|
+
|
|
96
|
+
```console
|
|
97
|
+
$ snakecharm crack sample.py -o out/
|
|
98
|
+
wrote out/
|
|
99
|
+
2 layers, depth 1, 1,966 bytes folded in 0.001s
|
|
100
|
+
detected: hyperion (100%), freecodingtools (67%)
|
|
101
|
+
|
|
102
|
+
$ find out -type f
|
|
103
|
+
out/deobfuscated.py
|
|
104
|
+
out/layers/00_depth0.py
|
|
105
|
+
out/layers/01_depth1.py
|
|
106
|
+
out/report.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`--clean` adds `out/cleaned.py`. `layers/` stays byte-exact.
|
|
110
|
+
|
|
111
|
+
Each layer file carries its derivation:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# ---------------------------------------------------------------
|
|
115
|
+
# snakecharm layer 1 (depth 1, source)
|
|
116
|
+
# provenance : zlib <- b64decode
|
|
117
|
+
# from : exec() at line 13, col 0 of layer 0
|
|
118
|
+
# sha256 : 58dda7518dfd60b35d3b522a654ad6c8532b076846f3a37d98340b05f2131acd
|
|
119
|
+
# ---------------------------------------------------------------
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When a site cannot be resolved it is reported, not skipped:
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
$ snakecharm crack script.py
|
|
126
|
+
not_reached : could not fold the argument of exec(): not reached during
|
|
127
|
+
interpretation, and folding it in isolation also failed: unbound name 'handle'
|
|
128
|
+
incomplete: 1 site(s) could not be resolved statically
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Library
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import snakecharm
|
|
135
|
+
|
|
136
|
+
result = snakecharm.crack(open("sample.py", "rb").read())
|
|
137
|
+
|
|
138
|
+
result.code # deepest recovered source
|
|
139
|
+
result.complete # bool
|
|
140
|
+
result.unresolved # [Unresolved(reason, message, layer, line, col)]
|
|
141
|
+
result.layers # [Layer(...)], each with .provenance and .digest
|
|
142
|
+
result.detections # [Detection(family, confidence, evidence)]
|
|
143
|
+
result.stats # {'layers': 31, 'max_depth': 30, 'bytes_folded': 431204, ...}
|
|
144
|
+
result.cleaned # str | None, when Options(clean=True)
|
|
145
|
+
result.save("out/") # writes the tree above
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`snakecharm.Options` mirrors the CLI flags. `snakecharm.Budget` holds the
|
|
149
|
+
resource limits.
|
|
150
|
+
|
|
151
|
+
To run the cleanup passes on their own, without peeling:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from snakecharm.clean import clean
|
|
155
|
+
|
|
156
|
+
result = clean("X = 1 + 2\n")
|
|
157
|
+
result.code # 'X = 3'
|
|
158
|
+
result.changed, result.refused, result.rolled_back
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Handles
|
|
162
|
+
|
|
163
|
+
- `exec`/`eval`/`compile` chains nested to arbitrary depth (default cap 512).
|
|
164
|
+
- Wrappers: `try`/`except`, `with`, class bodies, `if __name__ == "__main__"`,
|
|
165
|
+
`async def`, decorators.
|
|
166
|
+
- `.pyc` files and bare marshal streams, Python 3.8–3.14, from any host version.
|
|
167
|
+
- PEP 263 source encodings, decoded before analysis and recorded in the
|
|
168
|
+
provenance chain.
|
|
169
|
+
- Family fingerprinting: freecodingtools/pyobfuscate.com, Hyperion, Berserker,
|
|
170
|
+
Kramer, BlankOBF, Jawbreaker, PlusOBF, Vare, Anubis, Mr.X, PyArmor, Intensio,
|
|
171
|
+
Nuitka, PyInstaller, python-minifier. Detection is additive — an unrecognised
|
|
172
|
+
sample is still unwrapped. Signatures not confirmed against real output are
|
|
173
|
+
marked `[unvalidated signature]`.
|
|
174
|
+
|
|
175
|
+
Nothing in the default path executes sample code. Analysing live malware in a
|
|
176
|
+
disposable VM is still advisable.
|
|
177
|
+
|
|
178
|
+
### Cleanup
|
|
179
|
+
|
|
180
|
+
`--clean` runs, in order: constant folding, `getattr(o, "lit")` → `o.lit`,
|
|
181
|
+
removal of branches whose condition folds to a constant, and removal of unused
|
|
182
|
+
function locals.
|
|
183
|
+
|
|
184
|
+
Module-level globals and uncalled functions are never removed. A round that
|
|
185
|
+
fails to parse or compile is rolled back whole.
|
|
186
|
+
|
|
187
|
+
## Does not handle
|
|
188
|
+
|
|
189
|
+
| | |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Accurate 3.11+ decompilation | No released decompiler handles it. Falls back to annotated disassembly, labelled as such. |
|
|
192
|
+
| Identifier renaming | Mangled names are returned unchanged. |
|
|
193
|
+
| Packed executables | PyInstaller / cx_Freeze / py2exe are not unpacked. `detect` still names the packer. |
|
|
194
|
+
| Nuitka | Native machine code; no `.pyc` exists in the binary. Named, not recovered. |
|
|
195
|
+
| PyArmor BCC / RFT | Native C, and one-way identifier destruction, respectively. |
|
|
196
|
+
| MBA obfuscation | Arithmetic seeded with live in-scope variables defeats static folding. |
|
|
197
|
+
| Runtime-fetched payloads | Not present in the file. |
|
|
198
|
+
|
|
199
|
+
Decompiler backends (`uncompyle6`, `decompyle3`, `pycdc`) are used if found on
|
|
200
|
+
`PATH`. They are GPL, so they are never bundled, never imported, and invoked
|
|
201
|
+
only as subprocesses; without them, `.pyc` input renders as disassembly.
|
|
202
|
+
|
|
203
|
+
MIT. Created and provided by veryserious.systems.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# snakecharm
|
|
2
|
+
|
|
3
|
+
**Python De-Obfuscation**
|
|
4
|
+
|
|
5
|
+
Unwraps obfuscated Python — `exec`/`eval`/`compile` chains over base64, hex,
|
|
6
|
+
zlib/gzip/bz2/lzma, XOR, `marshal`, reversal and `chr`/`ord` reconstruction —
|
|
7
|
+
without executing the sample.
|
|
8
|
+
|
|
9
|
+
Requires Python 3.11+. Pure Python; depends on `typer`, `rich`, `pyyaml`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
pip install snakecharm
|
|
15
|
+
uv tool install snakecharm # isolated, puts snakecharm on PATH
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
snakecharm crack FILE [-o DIR] [--json] [--clean]
|
|
22
|
+
snakecharm detect FILE [--json]
|
|
23
|
+
snakecharm inspect FILE [--target-python X.Y]
|
|
24
|
+
snakecharm version
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `crack` | Unwrap. Recovered source to stdout, diagnostics to stderr. |
|
|
30
|
+
| `detect` | Fingerprint the obfuscator family; do not unwrap. |
|
|
31
|
+
| `inspect` | Disassemble a `.pyc` or bare marshal stream. |
|
|
32
|
+
| `version` | Print the version. |
|
|
33
|
+
|
|
34
|
+
`FILE` may be `-` to read from stdin.
|
|
35
|
+
|
|
36
|
+
Cleanup is a flag on `crack` (`--clean`), not a separate command.
|
|
37
|
+
|
|
38
|
+
### `crack` options
|
|
39
|
+
|
|
40
|
+
| Flag | Default | Meaning |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `-o, --output DIR` | — | Write the full artifact tree instead of stdout. |
|
|
43
|
+
| `--json` | off | Emit `report.json` to stdout. |
|
|
44
|
+
| `--clean` | off | Also emit a cleaned view (see [Cleanup](#cleanup)). |
|
|
45
|
+
| `--target-python X.Y` | host | Version to assume for marshal payloads. |
|
|
46
|
+
| `--decompiler NAME` | auto | Force a backend instead of routing by version. |
|
|
47
|
+
| `--max-layers N` | 512 | Layer cap; `0` for no limit. |
|
|
48
|
+
| `--max-memory MiB` | 1024 | Total decoded output; `0` for no limit. |
|
|
49
|
+
| `--max-fold MiB` | 256 | Largest single decoded value; `0` for no limit. |
|
|
50
|
+
| `--timeout SEC` | 300 | Wall clock; `0` for no limit. |
|
|
51
|
+
| `--no-peel` | off | Skip unwrapping; report detection only. |
|
|
52
|
+
| `--no-detect` | off | Skip fingerprinting. |
|
|
53
|
+
| `--plugins` | off | Load third-party ingest adapters. |
|
|
54
|
+
| `-q, --quiet` | off | Suppress the stderr summary. |
|
|
55
|
+
|
|
56
|
+
Exit codes: `0` complete, `1` incomplete, `2` input could not be processed.
|
|
57
|
+
Identifying the obfuscator does not affect the exit code.
|
|
58
|
+
|
|
59
|
+
## Output
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
$ snakecharm crack sample.py
|
|
63
|
+
import os
|
|
64
|
+
import sys
|
|
65
|
+
|
|
66
|
+
MESSAGE = "recovered exactly"
|
|
67
|
+
LIMIT = 42
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
31 layers, depth 30, 431,204 bytes folded in 0.006s
|
|
71
|
+
detected: freecodingtools (100%)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
With `-o`:
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
$ snakecharm crack sample.py -o out/
|
|
78
|
+
wrote out/
|
|
79
|
+
2 layers, depth 1, 1,966 bytes folded in 0.001s
|
|
80
|
+
detected: hyperion (100%), freecodingtools (67%)
|
|
81
|
+
|
|
82
|
+
$ find out -type f
|
|
83
|
+
out/deobfuscated.py
|
|
84
|
+
out/layers/00_depth0.py
|
|
85
|
+
out/layers/01_depth1.py
|
|
86
|
+
out/report.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`--clean` adds `out/cleaned.py`. `layers/` stays byte-exact.
|
|
90
|
+
|
|
91
|
+
Each layer file carries its derivation:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# ---------------------------------------------------------------
|
|
95
|
+
# snakecharm layer 1 (depth 1, source)
|
|
96
|
+
# provenance : zlib <- b64decode
|
|
97
|
+
# from : exec() at line 13, col 0 of layer 0
|
|
98
|
+
# sha256 : 58dda7518dfd60b35d3b522a654ad6c8532b076846f3a37d98340b05f2131acd
|
|
99
|
+
# ---------------------------------------------------------------
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When a site cannot be resolved it is reported, not skipped:
|
|
103
|
+
|
|
104
|
+
```console
|
|
105
|
+
$ snakecharm crack script.py
|
|
106
|
+
not_reached : could not fold the argument of exec(): not reached during
|
|
107
|
+
interpretation, and folding it in isolation also failed: unbound name 'handle'
|
|
108
|
+
incomplete: 1 site(s) could not be resolved statically
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Library
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import snakecharm
|
|
115
|
+
|
|
116
|
+
result = snakecharm.crack(open("sample.py", "rb").read())
|
|
117
|
+
|
|
118
|
+
result.code # deepest recovered source
|
|
119
|
+
result.complete # bool
|
|
120
|
+
result.unresolved # [Unresolved(reason, message, layer, line, col)]
|
|
121
|
+
result.layers # [Layer(...)], each with .provenance and .digest
|
|
122
|
+
result.detections # [Detection(family, confidence, evidence)]
|
|
123
|
+
result.stats # {'layers': 31, 'max_depth': 30, 'bytes_folded': 431204, ...}
|
|
124
|
+
result.cleaned # str | None, when Options(clean=True)
|
|
125
|
+
result.save("out/") # writes the tree above
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`snakecharm.Options` mirrors the CLI flags. `snakecharm.Budget` holds the
|
|
129
|
+
resource limits.
|
|
130
|
+
|
|
131
|
+
To run the cleanup passes on their own, without peeling:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from snakecharm.clean import clean
|
|
135
|
+
|
|
136
|
+
result = clean("X = 1 + 2\n")
|
|
137
|
+
result.code # 'X = 3'
|
|
138
|
+
result.changed, result.refused, result.rolled_back
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Handles
|
|
142
|
+
|
|
143
|
+
- `exec`/`eval`/`compile` chains nested to arbitrary depth (default cap 512).
|
|
144
|
+
- Wrappers: `try`/`except`, `with`, class bodies, `if __name__ == "__main__"`,
|
|
145
|
+
`async def`, decorators.
|
|
146
|
+
- `.pyc` files and bare marshal streams, Python 3.8–3.14, from any host version.
|
|
147
|
+
- PEP 263 source encodings, decoded before analysis and recorded in the
|
|
148
|
+
provenance chain.
|
|
149
|
+
- Family fingerprinting: freecodingtools/pyobfuscate.com, Hyperion, Berserker,
|
|
150
|
+
Kramer, BlankOBF, Jawbreaker, PlusOBF, Vare, Anubis, Mr.X, PyArmor, Intensio,
|
|
151
|
+
Nuitka, PyInstaller, python-minifier. Detection is additive — an unrecognised
|
|
152
|
+
sample is still unwrapped. Signatures not confirmed against real output are
|
|
153
|
+
marked `[unvalidated signature]`.
|
|
154
|
+
|
|
155
|
+
Nothing in the default path executes sample code. Analysing live malware in a
|
|
156
|
+
disposable VM is still advisable.
|
|
157
|
+
|
|
158
|
+
### Cleanup
|
|
159
|
+
|
|
160
|
+
`--clean` runs, in order: constant folding, `getattr(o, "lit")` → `o.lit`,
|
|
161
|
+
removal of branches whose condition folds to a constant, and removal of unused
|
|
162
|
+
function locals.
|
|
163
|
+
|
|
164
|
+
Module-level globals and uncalled functions are never removed. A round that
|
|
165
|
+
fails to parse or compile is rolled back whole.
|
|
166
|
+
|
|
167
|
+
## Does not handle
|
|
168
|
+
|
|
169
|
+
| | |
|
|
170
|
+
|---|---|
|
|
171
|
+
| Accurate 3.11+ decompilation | No released decompiler handles it. Falls back to annotated disassembly, labelled as such. |
|
|
172
|
+
| Identifier renaming | Mangled names are returned unchanged. |
|
|
173
|
+
| Packed executables | PyInstaller / cx_Freeze / py2exe are not unpacked. `detect` still names the packer. |
|
|
174
|
+
| Nuitka | Native machine code; no `.pyc` exists in the binary. Named, not recovered. |
|
|
175
|
+
| PyArmor BCC / RFT | Native C, and one-way identifier destruction, respectively. |
|
|
176
|
+
| MBA obfuscation | Arithmetic seeded with live in-scope variables defeats static folding. |
|
|
177
|
+
| Runtime-fetched payloads | Not present in the file. |
|
|
178
|
+
|
|
179
|
+
Decompiler backends (`uncompyle6`, `decompyle3`, `pycdc`) are used if found on
|
|
180
|
+
`PATH`. They are GPL, so they are never bundled, never imported, and invoked
|
|
181
|
+
only as subprocesses; without them, `.pyc` input renders as disassembly.
|
|
182
|
+
|
|
183
|
+
MIT. Created and provided by veryserious.systems.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snakecharm"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Deobfuscate Python without executing it. A webcrack for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["deobfuscation", "malware-analysis", "reverse-engineering", "python", "obfuscation"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Information Technology",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Core stays deliberately small. libcst is not needed until the v0.2 CST tier.
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typer>=0.12",
|
|
25
|
+
"rich>=13.0",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# No optional extras. `ast-grep-py` and `libcst` were both declared here and
|
|
30
|
+
# never imported -- a dependency you do not use is a claim you cannot back.
|
|
31
|
+
# Structural detection uses stdlib `ast`; the v0.2 cleanup tier uses `ast` +
|
|
32
|
+
# `ast.unparse` by design.
|
|
33
|
+
|
|
34
|
+
# Must stay below every `[project]` key: a TOML sub-table claims everything
|
|
35
|
+
# after it, so putting this higher silently swallowed `dependencies`.
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://veryserious.systems"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = ["pytest>=8.0", "hypothesis>=6.100"]
|
|
41
|
+
# Real obfuscators, used to generate the Tier-2 ground-truth corpus. Both MIT.
|
|
42
|
+
# Not needed to run snakecharm -- only to regenerate tests/corpus_real/.
|
|
43
|
+
obfuscators = ["python-minifier>=2.11", "intensio-obfuscator>=1.0.10"]
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
snakecharm = "snakecharm.cli:main"
|
|
47
|
+
snakecharm-gen-tables = "snakecharm.ir.gen_tables:main"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version]
|
|
50
|
+
path = "snakecharm/__init__.py"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.wheel]
|
|
53
|
+
packages = ["snakecharm"]
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.sdist]
|
|
56
|
+
# Allowlist, not denylist. The previous exclude-list shipped a 53 KB .coverage
|
|
57
|
+
# database full of this machine's absolute paths, because it was untracked and
|
|
58
|
+
# therefore not something anyone had thought to exclude.
|
|
59
|
+
#
|
|
60
|
+
# `/tests` is deliberately absent. PyPI rejects the sdist with "PyArmor-
|
|
61
|
+
# encrypted content is not allowed" because tests/test_packers.py holds a real
|
|
62
|
+
# two-line PyArmor stub -- the exact shape a protected file has, which is the
|
|
63
|
+
# whole reason it is a useful fixture for testing that we detect PyArmor. The
|
|
64
|
+
# fixture is not reshaped to get past the scanner: that would be evading a
|
|
65
|
+
# security control, and a fixture that no longer looks like the thing it stands
|
|
66
|
+
# for tests nothing. Tests are not needed to install or use the package, so they
|
|
67
|
+
# stay in the repository and out of the distribution.
|
|
68
|
+
include = [
|
|
69
|
+
"/snakecharm",
|
|
70
|
+
"/README.md",
|
|
71
|
+
"/LICENSE",
|
|
72
|
+
"/pyproject.toml",
|
|
73
|
+
]
|
|
74
|
+
exclude = ["*.pyc", "__pycache__"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
line-length = 100
|
|
78
|
+
# The corpus is deliberately malformed, obfuscated, or not text at all.
|
|
79
|
+
# Linting it is meaningless and its failures drown out real ones.
|
|
80
|
+
# Both corpora are deliberately obfuscated or malformed. Linting them is
|
|
81
|
+
# meaningless and their errors drown out real ones.
|
|
82
|
+
extend-exclude = ["tests/corpus", "tests/corpus_real"]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = ["F", "E", "W", "I", "UP", "B", "SIM"]
|
|
86
|
+
ignore = [
|
|
87
|
+
"E501", # long lines: the signature tables and opcode maps read better wide
|
|
88
|
+
"B008", # typer's Option()-in-default is the documented idiom
|
|
89
|
+
"SIM105", # contextlib.suppress is not clearer in tight interpreter loops
|
|
90
|
+
"SIM905", # a wrapped "a b c".split() reads better than a 30-element list literal
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.pytest.ini_options]
|
|
94
|
+
testpaths = ["tests"]
|
|
95
|
+
pythonpath = ["tests"]
|
|
96
|
+
markers = [
|
|
97
|
+
"slow: bomb/fuzz cases that allocate real memory",
|
|
98
|
+
]
|
|
99
|
+
filterwarnings = ["error::DeprecationWarning"]
|
|
100
|
+
|
|
101
|
+
[tool.pyright]
|
|
102
|
+
typeCheckingMode = "strict"
|
|
103
|
+
pythonVersion = "3.11"
|
|
104
|
+
include = ["snakecharm", "tests"]
|
|
105
|
+
# Third-party samples and obfuscator tools, never ours to type-check.
|
|
106
|
+
exclude = ["research", "**/__pycache__", "tests/corpus", "tests/corpus_real"]
|
|
107
|
+
# Without this the CLI resolves an ephemeral interpreter instead of the project
|
|
108
|
+
# venv, so typer/rich/pytest go unresolved and every value derived from them
|
|
109
|
+
# degrades to Unknown -- 1,219 diagnostics, most of them that one cascade.
|
|
110
|
+
venvPath = "."
|
|
111
|
+
venv = ".venv"
|
|
112
|
+
# Every module-private function in this codebase that looks unused is reached
|
|
113
|
+
# through a registration decorator -- @primitive in the whitelist, @register in
|
|
114
|
+
# the cleanup passes -- and is never named again after its definition. All 28
|
|
115
|
+
# reports were that pattern, so the rule cannot tell dead code from the central
|
|
116
|
+
# idiom here.
|
|
117
|
+
reportUnusedFunction = false
|