Lightweight Python bindings for JADX

github.com · Gloomy_King8147 · 14 days ago · security
0 net
# pyjadx Python bindings for [JADX](https://github.com/skylot/jadx) via JPype. Decompile Android APK and DEX files to Java source from Python. ```python from pyjadx import Jadx with Jadx("app.apk") as jadx: for cls in jadx.classes: print(cls.full_name) print(cls.code) ``` ## Why pyjadx? JADX is the leading open-source Android decompiler — it produces clean Java/Kotlin source from Dalvik bytecode, preserving class hierarchies, generics, exception handling, and annotations. pyjadx brings this into Python without subprocess overhead: JADX runs in-process via JPype. ## Install ```bash pip install pyjadx ``` **Requirements:** JDK 11+ (JADX JARs are downloaded automatically on first use). ## Quick start ```python from pyjadx import Jadx with Jadx("app.apk") as jadx: # All top-level classes (inner classes via cls.inner_classes) for cls in jadx.classes: print(cls.full_name, len(cls.methods), "methods") # Search by fully qualified name main = jadx.search_class("com.example.MainActivity") if main: print(main.code) # Method-level decompilation for method in main.methods: print(f"{method.name}: {method.code}") # Cross-references for method in main.methods: print(f"{method.name} called by: {[c.name for c in method.callers]}") # Package navigation for pkg in jadx.packages: print(pkg.name, len(pkg.classes), "classes") ``` ## Supported inputs | Format | Example | |--------|---------| | APK | `Jadx("app.apk")` | | DEX | `Jadx("classes.dex")` | | Multi-DEX APK | Handled transparently — all classes merged | ## API reference ### `Jadx(path, *, threads=None, mode="auto", code_cache=False)` Main entry point. Use as a context manager or call `load()`/`close()` manually. | Parameter | Description | |-----------|-------------| | `path` | Path to APK or DEX file | | `threads` | JADX thread pool size (`None` = CPU count) | | `mode` | `"auto"`, `"restructure"`, `"simple"`, or `"fallback"` | | `code_cache` | `False` = in-memory cache (default), `True` = disk-backed | **Properties:** | Property | Type | Description | |----------|------|-------------| | `classes` | `list[JavaClass]` | Top-level classes across all DEX files | | `packages` | `list[JavaPackage]` | Package-level navigation | | `class_count` | `int` | Number of top-level classes | | `java` | `JObject` | Escape hatch to underlying `jadx.api.JadxDecompiler` | **Methods:** | Method | Description | |--------|-------------| | `load()` | Load and resolve classes (called by `__enter__`) | | `close()` | Release resources (called by `__exit__`) | | `decompile_all()` | Eagerly decompile all classes | | `search_class(name)` | Find class by fully qualified name | ### `JavaClass` | Property | Type | Description | |----------|------|-------------| | `name` | `str` | Short name (`"MainActivity"`) | | `full_name` | `str` | Fully qualified (`"com.example.MainActivity"`) | | `package` | `str` | Package name | | `code` | `str` | Decompiled Java source (lazy, cached) | | `smali` | `str` | Smali disassembly (lazy, cached) | | `methods` | `list[JavaMethod]` | Methods in this class | | `fields` | `list[JavaField]` | Fields in this class | | `inner_classes` | `list[JavaClass]` | Inner/anonymous classes | | `declaring_class` | `JavaClass \| None` | Parent class (if inner) | | `top_class` | `JavaClass` | Outermost enclosing class | | `dependencies` | `list[JavaClass]` | Classes this class depends on | | `is_inner` | `bool` | Whether this is an inner class | | `java` | `JObject` | Escape hatch to `jadx.api.JavaClass` | ### `JavaMethod` | Property | Type | Description | |----------|------|-------------| | `name` | `str` | Method name | | `full_name` | `str` | Fully qualified with class | | `code` | `str` | Decompiled source (lazy, cached) | | `callers` | `list[JavaMethod]` | Methods that call this one (lazy, cached) | | `callees` | `list[JavaMethod]` | Methods this one calls (lazy, cached) | | `override_related` | `list[JavaMethod]` | Bidirectional override chain | | `declaring_class` | `JavaClass` | Class this method belongs to | | `java` | `JObject` | Escape hatch to `jadx.api.JavaMethod` | ### `JavaField` | Property | Type | Description | |----------|------|-------------| | `name` | `str` | Field name | | `full_name` | `str` | Fully qualified with class | | `type` | `str` | Field type as string | | `callers` | `list[JavaMethod]` | Methods referencing this field (lazy, cached) | | `declaring_class` | `JavaClass` | Class this field belongs to | | `java` | `JObject` | Escape hatch to `jadx.api.JavaField` | ### `JavaPackage` | Property | Type | Description | |----------|------|-------------| | `name` | `str` | Full package name | | `classes` | `list[JavaClass]` | Classes in this package | | `sub_packages` | `list[JavaPackage]` | Sub-packages | | `java` | `JObject` | Escape hatch to `jadx.api.JavaPackage` | ## Escape hatch Every wrapper exposes its underlying JADX Java object via `.java`. This lets you call any JADX API we haven't wrapped yet: ```python with Jadx("app.apk") as jadx: cls = jadx.search_class("com.example.Foo") # Call JADX Java API directly code_info = cls.java.getCodeInfo() ``` ## JVM coexistence pyjadx coexists with other JPype consumers (e.g., [PyGhidra](https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/PyGhidra)) in the same process. If a JVM is already running, `pyjadx.start()` adds JADX JARs to the existing classpath. If not, it starts one. ```python import pyghidra pyghidra.start() # starts JVM for Ghidra import pyjadx pyjadx.start() # adds JADX JARs to existing JVM — no conflict ``` ## Configuration **Custom JADX installation:** ```python # Via environment variable # export JADX_HOME=/opt/jadx # Or at runtime import pyjadx pyjadx.start(jadx_home="/opt/jadx") ``` **JAR caching:** JADX JARs are auto-downloaded from Maven Central on first use and cached at `~/.pyjadx/jars//`. ## License MIT