forked from mirrors/pyinstxtractor
106 lines
No EOL
3.4 KiB
Python
106 lines
No EOL
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Recursively decompile every .pyc file inside an _extracted folder using pycdc,
|
|
saving the results into a 'decompyle' folder with the same directory structure.
|
|
|
|
Usage:
|
|
python decompyle_with_pycdc.py <extracted_dir> [--pycdc PATH] [--out OUT_DIR]
|
|
|
|
Examples:
|
|
python decompyle_with_pycdc.py yourprogram.exe_extracted
|
|
python decompyle_with_pycdc.py yourprogram.exe_extracted --pycdc Release\\pycdc.exe --out decompyle
|
|
"""
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def find_pycdc(user_path: str | None) -> str:
|
|
"""Determine the path to the pycdc executable."""
|
|
if user_path:
|
|
if Path(user_path).is_file():
|
|
return user_path
|
|
sys.exit(f"[!] Could not find the specified pycdc: {user_path}")
|
|
|
|
# Common candidate locations (Windows / Linux)
|
|
candidates = [
|
|
"pycdc", "pycdc.exe",
|
|
"Release/pycdc.exe", "Release\\pycdc.exe",
|
|
"./pycdc",
|
|
]
|
|
for c in candidates:
|
|
p = Path(c)
|
|
if p.is_file():
|
|
return str(p)
|
|
# It may be on PATH, so return as-is (validated at run time)
|
|
return "pycdc"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Recursively decompile .pyc files from an _extracted folder using pycdc"
|
|
)
|
|
parser.add_argument("extracted_dir", help="pyinstxtractor output folder (_extracted)")
|
|
parser.add_argument("--pycdc", default=None, help="Path to the pycdc executable")
|
|
parser.add_argument("--out", default="decompyle", help="Output folder (default: decompyle)")
|
|
args = parser.parse_args()
|
|
|
|
src_root = Path(args.extracted_dir).resolve()
|
|
if not src_root.is_dir():
|
|
sys.exit(f"[!] Not a directory: {src_root}")
|
|
|
|
out_root = Path(args.out).resolve()
|
|
out_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
pycdc = find_pycdc(args.pycdc)
|
|
|
|
pyc_files = sorted(src_root.rglob("*.pyc"))
|
|
if not pyc_files:
|
|
sys.exit(f"[!] No .pyc files found in: {src_root}")
|
|
|
|
print(f"[*] pycdc : {pycdc}")
|
|
print(f"[*] Input dir : {src_root}")
|
|
print(f"[*] Output dir : {out_root}")
|
|
print(f"[*] Target files: {len(pyc_files)}\n")
|
|
|
|
ok = 0
|
|
fail = 0
|
|
for pyc in pyc_files:
|
|
rel = pyc.relative_to(src_root)
|
|
out_path = (out_root / rel).with_suffix(".py")
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[pycdc, str(pyc)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
)
|
|
except FileNotFoundError:
|
|
sys.exit(f"[!] Failed to run pycdc. Check the path: {pycdc}")
|
|
|
|
# pycdc writes the decompiled output to stdout and warnings/errors to stderr.
|
|
out_path.write_text(result.stdout, encoding="utf-8")
|
|
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
ok += 1
|
|
print(f"[OK] {rel} -> {out_path.relative_to(out_root)}")
|
|
else:
|
|
fail += 1
|
|
print(f"[FAIL] {rel}")
|
|
err = (result.stderr or "").strip()
|
|
if err:
|
|
# Show only the first line for brevity
|
|
print(f" {err.splitlines()[0]}")
|
|
|
|
print(f"\n[*] Done: {ok} succeeded, {fail} failed, {len(pyc_files)} total")
|
|
if fail:
|
|
print("[i] Failed files may be version mismatches or header-less entry-point .pyc files.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |