From f5accc84643c40da1148cd753a8118019a381d16 Mon Sep 17 00:00:00 2001 From: Iscle Date: Thu, 21 Dec 2023 19:46:40 +0100 Subject: [PATCH] feat: add support for xapk files (#1597)(PR #2064) * feat: annotate JadxPlugin with NotNull Allows for better Kotlin support * feat: add support for custom resources loader * feat: add support for xapk resources loading * fix: rename "decode" to "load" * refactor: annotate JadxCodeInput with NotNull * feat: add support for xapk code loading * feat: add xapk support to file filter * fix code formatting * revert NotNull annotation * several improvements * refactor: fix typo --------- Co-authored-by: Skylot --- README.md | 2 +- jadx-cli/build.gradle.kts | 1 + .../src/main/java/jadx/cli/JadxCLIArgs.java | 2 +- .../main/java/jadx/api/JadxDecompiler.java | 25 ++++++++++++ .../main/java/jadx/api/ResourcesLoader.java | 21 ++++++++-- .../api/plugins/CustomResourcesLoader.java | 19 +++++++++ .../gui/ui/filedialog/FileDialogWrapper.java | 2 +- .../jadx/plugins/input/dex/DexFileLoader.java | 1 - jadx-plugins/jadx-xapk-input/build.gradle.kts | 11 ++++++ .../plugins/input/xapk/XapkCustomCodeInput.kt | 39 +++++++++++++++++++ .../input/xapk/XapkCustomResourcesLoader.kt | 37 ++++++++++++++++++ .../plugins/input/xapk/XapkInputPlugin.kt | 23 +++++++++++ .../jadx/plugins/input/xapk/XapkManifest.kt | 17 ++++++++ .../java/jadx/plugins/input/xapk/XapkUtils.kt | 28 +++++++++++++ .../services/jadx.api.plugins.JadxPlugin | 1 + settings.gradle.kts | 1 + 16 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/api/plugins/CustomResourcesLoader.java create mode 100644 jadx-plugins/jadx-xapk-input/build.gradle.kts create mode 100644 jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt create mode 100644 jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt create mode 100644 jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt create mode 100644 jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt create mode 100644 jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt create mode 100644 jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin diff --git a/README.md b/README.md index f956bd7750a..f11a1df4580 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ and also packed to `build/jadx-.zip` ### Usage ``` -jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) +jadx[-gui] [command] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk) commands (use ' --help' for command options): plugins - manage jadx plugins diff --git a/jadx-cli/build.gradle.kts b/jadx-cli/build.gradle.kts index b62378a49c2..1f270cdc9a7 100644 --- a/jadx-cli/build.gradle.kts +++ b/jadx-cli/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { runtimeOnly(project(":jadx-plugins:jadx-rename-mappings")) runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata")) runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin")) + runtimeOnly(project(":jadx-plugins:jadx-xapk-input")) implementation("org.jcommander:jcommander:1.83") implementation("ch.qos.logback:logback-classic:1.4.14") diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 102c6e4a1b2..47e2784dec5 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -34,7 +34,7 @@ public class JadxCLIArgs { - @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)") + @Parameter(description = " (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab, .xapk)") protected List files = new ArrayList<>(1); @Parameter(names = { "-d", "--output-dir" }, description = "output directory") diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 3c57cb92b59..fbd7fe9f4a7 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -28,6 +28,7 @@ import jadx.api.metadata.annotations.NodeDeclareRef; import jadx.api.metadata.annotations.VarNode; import jadx.api.metadata.annotations.VarRef; +import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.events.IJadxEvents; import jadx.api.plugins.input.ICodeLoader; @@ -99,6 +100,7 @@ public final class JadxDecompiler implements Closeable { private final JadxEventsImpl events = new JadxEventsImpl(); private final List customCodeLoaders = new ArrayList<>(); + private final List customResourcesLoaders = new ArrayList<>(); private final Map> customPasses = new HashMap<>(); public JadxDecompiler() { @@ -170,6 +172,7 @@ private void reset() { public void close() { reset(); closeInputs(); + closeLoaders(); args.close(); } @@ -184,6 +187,17 @@ private void closeInputs() { loadedInputs.clear(); } + private void closeLoaders() { + for (CustomResourcesLoader resourcesLoader : customResourcesLoaders) { + try { + resourcesLoader.close(); + } catch (Exception e) { + LOG.error("Failed to close resource loader: " + resourcesLoader, e); + } + } + customResourcesLoaders.clear(); + } + private void loadPlugins() { pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input"); pluginManager.load(args.getPluginLoader()); @@ -678,6 +692,17 @@ public List getCustomCodeLoaders() { return customCodeLoaders; } + public void addCustomResourcesLoader(CustomResourcesLoader loader) { + if (customResourcesLoaders.contains(loader)) { + return; + } + customResourcesLoaders.add(loader); + } + + public List getCustomResourcesLoaders() { + return customResourcesLoaders; + } + public void addCustomPass(JadxPass pass) { customPasses.computeIfAbsent(pass.getPassType(), l -> new ArrayList<>()).add(pass); } diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 43e01c6c2a2..15c39687b43 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -16,6 +16,7 @@ import jadx.api.ResourceFile.ZipRef; import jadx.api.impl.SimpleCodeInfo; +import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; @@ -140,9 +141,23 @@ private void loadFile(List list, File file) { if (file == null || file.isDirectory()) { return; } + + // Try to load the resources with a custom loader first + for (CustomResourcesLoader loader : jadxRef.getCustomResourcesLoaders()) { + if (loader.load(this, list, file)) { + LOG.debug("Custom loader used for {}", file.getAbsolutePath()); + return; + } + } + + // If no custom decoder was able to decode the resources, use the default decoder + defaultLoadFile(list, file, ""); + } + + public void defaultLoadFile(List list, File file, String subDir) { if (FileUtils.isZipFile(file)) { ZipSecurity.visitZipEntries(file, (zipFile, entry) -> { - addEntry(list, file, entry); + addEntry(list, file, entry, subDir); return null; }); } else { @@ -151,13 +166,13 @@ private void loadFile(List list, File file) { } } - private void addEntry(List list, File zipFile, ZipEntry entry) { + public void addEntry(List list, File zipFile, ZipEntry entry, String subDir) { if (entry.isDirectory()) { return; } String name = entry.getName(); ResourceType type = ResourceType.getFileType(name); - ResourceFile rf = ResourceFile.createResourceFile(jadxRef, name, type); + ResourceFile rf = ResourceFile.createResourceFile(jadxRef, subDir + name, type); if (rf != null) { rf.setZipRef(new ZipRef(zipFile, name)); list.add(rf); diff --git a/jadx-core/src/main/java/jadx/api/plugins/CustomResourcesLoader.java b/jadx-core/src/main/java/jadx/api/plugins/CustomResourcesLoader.java new file mode 100644 index 00000000000..c4a306b17a1 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/CustomResourcesLoader.java @@ -0,0 +1,19 @@ +package jadx.api.plugins; + +import java.io.Closeable; +import java.io.File; +import java.util.List; + +import jadx.api.ResourceFile; +import jadx.api.ResourcesLoader; + +public interface CustomResourcesLoader extends Closeable { + /** + * Load resources from file to list of ResourceFile + * + * @param list list to add loaded resources + * @param file file to load + * @return true if file was loaded + */ + boolean load(ResourcesLoader loader, List list, File file); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java b/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java index afd55c6e708..ac50b8d03da 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/filedialog/FileDialogWrapper.java @@ -17,7 +17,7 @@ public class FileDialogWrapper { private static final List OPEN_FILES_EXTS = Arrays.asList( - "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts"); + "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "jadx.kts", "xapk"); private final MainWindow mainWindow; diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java index 40c767c24b0..24a69cfe589 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java @@ -32,7 +32,6 @@ public class DexFileLoader { public DexFileLoader(DexInputOptions options) { this.options = options; - resetDexUniqId(); } public List collectDexFiles(List pathsList) { diff --git a/jadx-plugins/jadx-xapk-input/build.gradle.kts b/jadx-plugins/jadx-xapk-input/build.gradle.kts new file mode 100644 index 00000000000..13746a2ab7a --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("jadx-library") + id("jadx-kotlin") +} + +dependencies { + api(project(":jadx-core")) + + implementation(project(":jadx-plugins:jadx-dex-input")) + implementation("com.google.code.gson:gson:2.10.1") +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt new file mode 100644 index 00000000000..1038d11df1d --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomCodeInput.kt @@ -0,0 +1,39 @@ +package jadx.plugins.input.xapk + +import jadx.api.plugins.input.ICodeLoader +import jadx.api.plugins.input.JadxCodeInput +import jadx.api.plugins.utils.CommonFileUtils +import jadx.api.plugins.utils.ZipSecurity +import java.io.File +import java.nio.file.Path +import java.util.zip.ZipFile + +class XapkCustomCodeInput( + private val plugin: XapkInputPlugin, +) : JadxCodeInput { + override fun loadFiles(input: List): ICodeLoader { + val apkFiles = mutableListOf() + for (file in input.map { it.toFile() }) { + val manifest = XapkUtils.getManifest(file) ?: continue + if (!XapkUtils.isSupported(manifest)) continue + + ZipFile(file).use { zip -> + for (splitApk in manifest.splitApks) { + val splitApkEntry = zip.getEntry(splitApk.file) + if (splitApkEntry != null) { + val tmpFile = ZipSecurity.getInputStreamForEntry(zip, splitApkEntry).use { + CommonFileUtils.saveToTempFile(it, ".apk").toFile() + } + apkFiles.add(tmpFile) + } + } + } + } + + val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) + + apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } + + return codeLoader + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt new file mode 100644 index 00000000000..f2f17279d98 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkCustomResourcesLoader.kt @@ -0,0 +1,37 @@ +package jadx.plugins.input.xapk + +import jadx.api.ResourceFile +import jadx.api.ResourcesLoader +import jadx.api.plugins.CustomResourcesLoader +import jadx.api.plugins.utils.CommonFileUtils +import jadx.api.plugins.utils.ZipSecurity +import java.io.File + +class XapkCustomResourcesLoader : CustomResourcesLoader { + private val tmpFiles = mutableListOf() + + override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { + val manifest = XapkUtils.getManifest(file) ?: return false + if (!XapkUtils.isSupported(manifest)) return false + + val apkEntries = manifest.splitApks.map { it.file }.toHashSet() + ZipSecurity.visitZipEntries(file) { zip, entry -> + if (apkEntries.contains(entry.name)) { + val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + CommonFileUtils.saveToTempFile(it, ".apk").toFile() + } + loader.defaultLoadFile(list, tmpFile, entry.name + "/") + tmpFiles += tmpFile + } else { + loader.addEntry(list, file, entry, "") + } + null + } + return true + } + + override fun close() { + tmpFiles.forEach(CommonFileUtils::safeDeleteFile) + tmpFiles.clear() + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt new file mode 100644 index 00000000000..559c7289591 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkInputPlugin.kt @@ -0,0 +1,23 @@ +package jadx.plugins.input.xapk + +import jadx.api.plugins.JadxPlugin +import jadx.api.plugins.JadxPluginContext +import jadx.api.plugins.JadxPluginInfo +import jadx.plugins.input.dex.DexInputPlugin + +class XapkInputPlugin : JadxPlugin { + private val codeInput = XapkCustomCodeInput(this) + private val resourcesLoader = XapkCustomResourcesLoader() + internal var dexInputPlugin = DexInputPlugin() + + override fun getPluginInfo() = JadxPluginInfo( + "xapk-input", + "XAPK Input", + "Load .xapk files", + ) + + override fun init(context: JadxPluginContext) { + context.addCodeInput(codeInput) + context.decompiler.addCustomResourcesLoader(resourcesLoader) + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt new file mode 100644 index 00000000000..9ceaf201ba5 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkManifest.kt @@ -0,0 +1,17 @@ +package jadx.plugins.input.xapk + +import com.google.gson.annotations.SerializedName + +data class XapkManifest( + @SerializedName("xapk_version") + val xapkVersion: Int, + @SerializedName("split_apks") + val splitApks: List, +) { + data class SplitApk( + @SerializedName("file") + val file: String, + @SerializedName("id") + val id: String, + ) +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt new file mode 100644 index 00000000000..098663f68e2 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XapkUtils.kt @@ -0,0 +1,28 @@ +package jadx.plugins.input.xapk + +import com.google.gson.Gson +import jadx.api.plugins.utils.ZipSecurity +import jadx.core.utils.files.FileUtils +import java.io.File +import java.io.InputStreamReader +import java.util.zip.ZipFile + +object XapkUtils { + fun getManifest(file: File): XapkManifest? { + if (!FileUtils.isZipFile(file)) return null + try { + ZipFile(file).use { zip -> + val manifestEntry = zip.getEntry("manifest.json") ?: return null + return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use { + Gson().fromJson(it, XapkManifest::class.java) + } + } + } catch (e: Exception) { + return null + } + } + + fun isSupported(manifest: XapkManifest): Boolean { + return manifest.xapkVersion == 2 && manifest.splitApks.isNotEmpty() + } +} diff --git a/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 00000000000..e50351aa7b6 --- /dev/null +++ b/jadx-plugins/jadx-xapk-input/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +jadx.plugins.input.xapk.XapkInputPlugin diff --git a/settings.gradle.kts b/settings.gradle.kts index 44fb808dab4..09c75126c1f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ include("jadx-plugins:jadx-smali-input") include("jadx-plugins:jadx-java-convert") include("jadx-plugins:jadx-rename-mappings") include("jadx-plugins:jadx-kotlin-metadata") +include("jadx-plugins:jadx-xapk-input") include("jadx-plugins:jadx-script:jadx-script-plugin") include("jadx-plugins:jadx-script:jadx-script-runtime")