pyc_revive
.pyc → .py reconstructor. Pure stdlib. No uncompyle6. No pycdc. No xdis.
Handles every CPython from 2.0 through 3.15, with a bundled disassembler fallback for versions that don't match the running interpreter, and a portable Node.js + depyo path for the newest bytecode (3.14+).
install
pip install pyc-revive
The Python package is stdlib-only. On first run, pyc-revive
checks for Node.js and depyo, and installs them if missing — no admin,
no system changes, everything under %LOCALAPPDATA%\pyc_revive (or the
equivalent user dir on non-Windows):
- Node.js ≥ 20 via winget (user scope) if available, otherwise a portable ZIP extracted into the user dir. No MSI, no registry writes.
- depyo via npm into a user-writable prefix
(
%LOCALAPPDATA%\pyc_revive\npm-global). - optional pip backends (pychd, byteripper, uncompyle6, decompyle3)
installed individually with
--userfallback when the system site isn't writable.
To run the installer explicitly:
pyc-revive --install-deps
To skip the first-run bootstrap entirely:
pyc-revive --no-bootstrap Backdoor.pyc
From a checkout (not pip-installed):
python -m pyc_revive my file.pyc
python -m pyc_revive ./dist/app.pyc --out ./revived.py
python -m pyc_revive --install-deps
Input handling: exact path, spaces with no quotes, basename search in cwd + all subfolders + parent folder.
versions
Every CPython magic from 2.0 through 3.15 resolves (pymagic.py: full
table + nearest fallback + header sizes).
Three backends, one pipeline:
- native —
.pycmagic matches the running interpreter. stdlibmarshalloads the code object, stdlibdisdisassembles it. Zero dependencies. - pycdas — anything else 2.x–3.13. The bundled
pycdas.exedisassembles the foreign version,pycdas_parser.pyparses the dump into code trees,shim.pyadapts them to the pipeline. Everything downstream (normalize, segment, translate, CFG, emit, verify×3) runs unchanged. - depyo — 3.14+.
depyo_parser.pyrunsdepyo --skip-source-gen --asmand parses the.pyasmdump;depyo_postprocess.pyfixes depyo's mechanical emitter bugs (dropped statements, mangled imports, wrong keywords) using the pycdas pipeline's output as a repair source when both ran. When the pipeline fails to compile,depyo-directproduces working source from depyo's own decompiler.
Routing is by magic number, decided before any bytecode is touched: a
foreign magic goes straight to the appropriate backend. marshal.loads
can succeed on foreign payloads since the format is stable, and then
stdlib dis mis-decodes the unknown opcodes and can take the whole
process down instead of raising — so "try native, fall back on error"
is not used for foreign files.
3.14 and 3.15 support
3.14 was the first foreign 3.11+ target exercised end-to-end, and it needed real work beyond a new opcode table.
Translator additions:
LOAD_SMALL_INT,LOAD_FAST_BORROW,LOAD_FAST_BORROW_LOAD_FAST_BORROW(double 4-bit index, high nibble first),LOAD_SPECIAL,FORMAT_SIMPLE,TO_BOOL(transparent),CALL_KW(kwargs split likeCALL_FUNCTION_KW, NULL-skipped).BINARY_OPoparg ≥ 26 (3.14 renumbers the table; 26 =NB_SUBSCR, 27 = inplace).KW_NAMES— pycdas 3.13 sometimes prints it asLOAD_CONST <i> // Value: ('a', 'b'), a tuple of key names.shim.pyrewrites the opname when the argval is a string tuple immediately followed by aCALL_KW, so the translator's kwarg split runs.
Parser additions (pycdas_parser.py):
[Locals+Names]sections read intoco_varnames.[Exception Table]entries (S to E -> T [depth] (lasti?)) parsed and re-encoded toco_exceptiontablebytes on the shim, so the shared try/with-region machinery runs unchanged.
Shim additions (shim.py):
- 3.14 conditional-branch targets from the bundled pycdas land 2 bytes
low (it suppresses a hidden CACHE but forgets it in target math —
proven three ways: dead else bodies, inverted with-cleanup,
stack-underflow
RETURN). Repaired when the fingerprint is exact: 3.14+ dump,NOT_TAKENatbranch+4,target+2is a real offset. - 3.14 dropped
BEFORE_WITHfor an inline prologue (COPY, LOAD_SPECIAL, SWAP, SWAP, LOAD_SPECIAL, CALL); the shim folds it back to one syntheticBEFORE_WITH. LOAD_FAST_BORROWindex resolution, including the pair form.
Region detection:
- with-bodies are scoped by their exception-table range (fallthrough
code after the body dedents instead of hiding under
with). - try interiors stop at the body end (post-try joins don't indent under
try:). - post-try joins stranded textually before out-of-line handlers are
deferred past the
exceptclause. - RERAISE-ended handlers that pair with nothing are skipped as implicit-cleanup residue.
- duplicated
finallybodies pair by twin match: an unpaired RERAISE-ended handler whose leading statements duplicate a post-try block's leading statements is the exceptional finally copy (tails decide the split —[.., return]vs[.., raise]means the return is post-statement). The normal copy splits intofinally:+ rest.
3.15 goes through the same depyo path. depyo's unresolved name masks
(##NAME_N##, ##ERROR##, __common_const_N__) are collapsed or
substituted in depyo_postprocess.py; stdlib imports dropped by depyo
are reinserted from a canonical table.
pipeline
- find file (
finder.py) - detect version via magic (
pymagic.py) - load: native marshal, else pycdas / depyo backend (
loader.py,pycdas_parser.py,depyo_parser.py,shim.py) - disassemble + abstract (
bytecode.py:Inst,EditableBytecode, opcode/oparg/value-stack/metadata tables) - normalize / mask consts + names (
normalizer.py) - opcode-aware B/I/E segmentation (
segmenter.py) - statement translation (
translator.py: stack machine, defaults recovery, lambdas inline, 2.x print/exec/class ops) - control-flow graph from jumps (
cfg.py: if/for/while/try/except-as (bareexcept:too)/with/finally, clauses + clusters, 3.11+ exception table, SETUP-region splits) - emit + repairs (
emitter.py: source-order splicing onto def/class stub lines, bases recovery,@decoratorrestore,globaldecls, cond chains, ternaries, nested if/else, loop ranges, break/continue recovery, indent + suite-guard passes) - equivalence verify, 3 sweeps (
verifier.py: recompile revived source, compare opcode structure, repair, repeat ×3) - self-validation (
autofix.py: compile the output, diagnose the first error, apply a targeted repair — orphan handler, empty suite, bad indent, orphantry:— until it compiles or the budget runs out; ships the closest parse) - backend fallback (
reconstructor.py: if the pipeline's output doesn't compile and depyo is available, depyo-direct runs with the pipeline's source as a repair companion, then post-processing fixes the mechanical bugs)
layout
pyc_revive/
finder.py file resolution
pymagic.py magic-number table + version detection
loader.py native marshal loading
pycdas_parser.py bundled-disassembler dump parser
depyo_parser.py depyo --asm dump parser
depyo_postprocess.py repair pass for depyo output
shim.py foreign-version code trees behave like code objects
bytecode.py Inst + EditableBytecode
normalizer.py const/name masking
segmenter.py B/I/E opcode segmentation
translator.py opcode stream -> statements
cfg.py basic blocks + structured control flow
emitter.py source emission + structural repairs
verifier.py recompile + compare x3
autofix.py compile-repair loop
reconstructor.py orchestrator
cli.py interface
bootstrap.py first-run dep check
install_deps.py Node + depyo + pip backend installer
deps.py optional-backend reporter
debug_log.py verbose logging (PYC_REVIVE_VERBOSE=1)
__main__.py
pycdas.exe bundled disassembler
known limits
- inline comprehensions/genexps render as
func(...)placeholders. trybodies that fall through into always-run handlers keep flat order instead of guarded flow, unless an exception table is available (native 3.11+ and foreign dumps with[Exception Table]sections scope bodies, pair handlers, and defer stranded post-try joins properly).- exotic decorators reduce to bare defs; exotic call shapes degrade to commented disassembly rather than failing.
- pre-3.8 default-argument recovery is best-effort (flag-style
MAKE_FUNCTIONfrom 3.8+ recovers exactly). - verifier scores dip across versions: opcode sets drift between releases, so cross-version similarity reads lower than same-version.
- 3.14
FOR_ITERtargets are untested (no loop in any 3.14 sample yet); if the bundled pycdas hides caches after it the same way it does for conditional branches, loop joins may land 2 bytes off.
license
MIT
Metadata
Release files for pyc-revive 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyc_revive-1.0.2.tar.gz | 154.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyc_revive-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 313.5 kB
Release files / pyc_revive-1.0.2.tar.gz
| Download URL | pyc_revive-1.0.2.tar.gz |
|---|---|
| Size | 154.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d061aa6a75a3af318095b8551e86b1377ee6ad206e4ea729930fbc04a526fc1a
|
|
BLAKE2b-256 checksum How to use checksums |
10dc3c520c887427d578808087ccec853a0f46456d2446aa1bb49e598ecff4ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|
Release files / pyc_revive-1.0.2-py3-none-any.whl
| Download URL | pyc_revive-1.0.2-py3-none-any.whl |
|---|---|
| Size | 158.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8904193128ba97a3a44d1081cf7ea47bd79d683842431ffcd2041627f54be231
|
|
BLAKE2b-256 checksum How to use checksums |
fcc7603b4846070a40d22d8637c174a1a236cd09c3933ab1829ce1e4309d4262
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|