From d84f0389ec5356942ce07219799152ecae842b27 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:51:28 +0000 Subject: [PATCH] feat: custom zip reader implementation to fight tampering fix(zip): use size info from CD if LFH entry is incorrect refactor: move custom zip implementation into new module feat: move ZipSecurity into jadx-zip module --- .../src/main/java/jadx/cli/JadxAppCommon.java | 48 +++ jadx-cli/src/main/java/jadx/cli/JadxCLI.java | 23 +- .../java/jadx/cli/tools/ConvertArscFile.java | 12 +- jadx-commons/jadx-zip/README.md | 3 + jadx-commons/jadx-zip/build.gradle.kts | 3 + .../src/main/java/jadx/zip/IZipEntry.java | 36 ++ .../src/main/java/jadx/zip/IZipParser.java | 9 + .../src/main/java/jadx/zip/ZipContent.java | 35 ++ .../src/main/java/jadx/zip/ZipReader.java | 111 +++++ .../main/java/jadx/zip/ZipReaderFlags.java | 32 ++ .../main/java/jadx/zip/ZipReaderOptions.java | 29 ++ .../jadx/zip/fallback/FallbackZipEntry.java | 61 +++ .../jadx/zip/fallback/FallbackZipParser.java | 109 +++++ .../java/jadx/zip/parser/JadxZipEntry.java | 93 ++++ .../java/jadx/zip/parser/JadxZipParser.java | 403 ++++++++++++++++++ .../main/java/jadx/zip/parser/ZipDeflate.java | 24 ++ .../zip/security/DisabledZipSecurity.java | 35 ++ .../jadx/zip/security/IJadxZipSecurity.java | 35 ++ .../jadx/zip/security/JadxZipSecurity.java | 126 ++++++ .../zip/security}/LimitedInputStream.java | 25 +- jadx-core/build.gradle.kts | 1 + .../main/java/jadx/api/JadxDecompiler.java | 16 +- .../src/main/java/jadx/api/ResourceFile.java | 7 +- .../main/java/jadx/api/ResourcesLoader.java | 27 +- .../jadx/api/plugins/JadxPluginContext.java | 6 + .../jadx/api/plugins/utils/ZipSecurity.java | 153 ++----- .../java/jadx/api/security/IJadxSecurity.java | 4 +- .../jadx/api/security/JadxSecurityFlag.java | 3 +- .../jadx/api/security/impl/JadxSecurity.java | 39 ++ .../java/jadx/core/dex/visitors/SaveCode.java | 15 +- .../java/jadx/core/plugins/PluginContext.java | 6 + .../java/jadx/core/utils/files/FileUtils.java | 25 +- .../java/jadx/core/utils/files/ZipFile.java | 32 -- .../java/jadx/core/utils/files/ZipPatch.java | 199 --------- .../core/xmlgen/ResTableBinaryParser.java | 3 +- .../java/jadx/core/xmlgen/ResourcesSaver.java | 9 +- .../src/main/java/jadx/gui/JadxWrapper.java | 2 + .../java/jadx/gui/ui/codearea/HexArea.java | 2 +- .../jadx/plugins/tools/JadxPluginsList.java | 7 +- .../plugins/input/apkm/ApkmCustomCodeInput.kt | 17 +- .../input/apkm/ApkmCustomResourcesLoader.kt | 14 +- .../plugins/input/apkm/ApkmInputPlugin.kt | 9 +- .../java/jadx/plugins/input/apkm/ApkmUtils.kt | 11 +- .../jadx/plugins/input/dex/DexFileLoader.java | 36 +- .../plugins/input/dex/DexInputPlugin.java | 1 + .../input/javaconvert/JavaConvertLoader.java | 32 +- .../input/javaconvert/JavaConvertPlugin.java | 5 +- .../plugins/input/java/JavaInputLoader.java | 65 ++- .../plugins/input/java/JavaInputPlugin.java | 9 +- .../plugins/input/xapk/XapkCustomCodeInput.kt | 20 +- .../input/xapk/XapkCustomResourcesLoader.kt | 12 +- .../plugins/input/xapk/XapkInputPlugin.kt | 20 +- .../java/jadx/plugins/input/xapk/XapkUtils.kt | 11 +- settings.gradle.kts | 1 + 54 files changed, 1557 insertions(+), 514 deletions(-) create mode 100644 jadx-cli/src/main/java/jadx/cli/JadxAppCommon.java create mode 100644 jadx-commons/jadx-zip/README.md create mode 100644 jadx-commons/jadx-zip/build.gradle.kts create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipEntry.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipParser.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipContent.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReader.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderFlags.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderOptions.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipEntry.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipParser.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipEntry.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/ZipDeflate.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/security/DisabledZipSecurity.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/security/IJadxZipSecurity.java create mode 100644 jadx-commons/jadx-zip/src/main/java/jadx/zip/security/JadxZipSecurity.java rename {jadx-core/src/main/java/jadx/api/plugins/utils => jadx-commons/jadx-zip/src/main/java/jadx/zip/security}/LimitedInputStream.java (67%) delete mode 100644 jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java delete mode 100644 jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java diff --git a/jadx-cli/src/main/java/jadx/cli/JadxAppCommon.java b/jadx-cli/src/main/java/jadx/cli/JadxAppCommon.java new file mode 100644 index 000000000..f2087ae20 --- /dev/null +++ b/jadx-cli/src/main/java/jadx/cli/JadxAppCommon.java @@ -0,0 +1,48 @@ +package jadx.cli; + +import java.util.Set; + +import jadx.api.JadxArgs; +import jadx.api.security.JadxSecurityFlag; +import jadx.api.security.impl.JadxSecurity; +import jadx.commons.app.JadxCommonEnv; +import jadx.zip.security.DisabledZipSecurity; +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.JadxZipSecurity; + +public class JadxAppCommon { + + public static void applyEnvVars(JadxArgs jadxArgs) { + Set flags = JadxSecurityFlag.all(); + IJadxZipSecurity zipSecurity; + + boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false); + if (disableXmlSecurity) { + flags.remove(JadxSecurityFlag.SECURE_XML_PARSER); + // TODO: not related to 'xml security', but kept for compatibility + flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE); + } + + boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false); + if (disableZipSecurity) { + flags.remove(JadxSecurityFlag.SECURE_ZIP_READER); + zipSecurity = DisabledZipSecurity.INSTANCE; + } else { + JadxZipSecurity jadxZipSecurity = new JadxZipSecurity(); + int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2); + if (maxZipEntriesCount != -2) { + jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount); + } + int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2); + if (zipBombMinUncompressedSize != -2) { + jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize); + } + int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2); + if (setZipBombDetectionFactor != -2) { + jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor); + } + zipSecurity = jadxZipSecurity; + } + jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity)); + } +} diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index ef5465fcb..4ce788384 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -1,7 +1,5 @@ package jadx.cli; -import java.util.Set; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,11 +8,8 @@ import jadx.api.JadxDecompiler; import jadx.api.impl.AnnotatedCodeWriter; import jadx.api.impl.NoOpCodeCache; import jadx.api.impl.SimpleCodeWriter; -import jadx.api.security.JadxSecurityFlag; -import jadx.api.security.impl.JadxSecurity; import jadx.cli.LogHelper.LogLevelEnum; import jadx.cli.plugins.JadxFilesGetter; -import jadx.commons.app.JadxCommonEnv; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.plugins.tools.JadxExternalPluginsLoader; @@ -54,7 +49,7 @@ public class JadxCLI { jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE); initCodeWriterProvider(jadxArgs); - applyEnvVars(jadxArgs); + JadxAppCommon.applyEnvVars(jadxArgs); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); if (checkForErrors(jadx)) { @@ -87,22 +82,6 @@ public class JadxCLI { } } - private static void applyEnvVars(JadxArgs jadxArgs) { - Set flags = JadxSecurityFlag.all(); - boolean modified = false; - boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false); - if (disableXmlSecurity) { - flags.remove(JadxSecurityFlag.SECURE_XML_PARSER); - // TODO: not related to 'xml security', but kept for compatibility - flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE); - modified = true; - } - // TODO: migrate 'ZipSecurity' - if (modified) { - jadxArgs.setSecurity(new JadxSecurity(flags)); - } - } - private static boolean checkForErrors(JadxDecompiler jadx) { if (jadx.getRoot().getClasses().isEmpty()) { if (jadx.getArgs().isSkipResources()) { diff --git a/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java b/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java index a4088efb7..ff1c91cd7 100644 --- a/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java +++ b/jadx-cli/src/main/java/jadx/cli/tools/ConvertArscFile.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,8 +17,10 @@ import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.android.TextResMapFile; -import jadx.core.utils.files.ZipFile; import jadx.core.xmlgen.ResTableBinaryParser; +import jadx.zip.IZipEntry; +import jadx.zip.ZipContent; +import jadx.zip.ZipReader; import static jadx.core.utils.files.FileUtils.expandDirs; @@ -53,18 +54,19 @@ public class ConvertArscFile { LOG.info("Input entries count: {}", resMap.size()); RootNode root = new RootNode(new JadxArgs()); // not really needed + ZipReader zipReader = new ZipReader(); rewritesCount = 0; for (Path resFile : inputResFiles) { ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true); if (resFile.getFileName().toString().endsWith(".jar")) { // Load resources.arsc from android.jar - try (ZipFile zip = new ZipFile(resFile.toFile())) { - ZipEntry entry = zip.getEntry("resources.arsc"); + try (ZipContent zip = zipReader.open(resFile.toFile())) { + IZipEntry entry = zip.searchEntry("resources.arsc"); if (entry == null) { LOG.error("Failed to load \"resources.arsc\" from {}", resFile); continue; } - try (InputStream inputStream = zip.getInputStream(entry)) { + try (InputStream inputStream = entry.getInputStream()) { resTableParser.decode(inputStream); } } diff --git a/jadx-commons/jadx-zip/README.md b/jadx-commons/jadx-zip/README.md new file mode 100644 index 000000000..8f8b0727a --- /dev/null +++ b/jadx-commons/jadx-zip/README.md @@ -0,0 +1,3 @@ +## jadx zip + +Custom zip reader implementation to fight tampering and provide additional security checks diff --git a/jadx-commons/jadx-zip/build.gradle.kts b/jadx-commons/jadx-zip/build.gradle.kts new file mode 100644 index 000000000..d974fd945 --- /dev/null +++ b/jadx-commons/jadx-zip/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("jadx-library") +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipEntry.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipEntry.java new file mode 100644 index 000000000..8bb96d776 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipEntry.java @@ -0,0 +1,36 @@ +package jadx.zip; + +import java.io.File; +import java.io.InputStream; + +public interface IZipEntry { + + /** + * Zip entry name + */ + String getName(); + + /** + * Uncompressed bytes + */ + byte[] getBytes(); + + /** + * Stream of uncompressed bytes. + */ + InputStream getInputStream(); + + long getCompressedSize(); + + long getUncompressedSize(); + + boolean isDirectory(); + + File getZipFile(); + + /** + * Return true if {@link #getBytes()} method is more optimal to use other than + * {@link #getInputStream()} + */ + boolean preferBytes(); +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipParser.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipParser.java new file mode 100644 index 000000000..377e9d29a --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/IZipParser.java @@ -0,0 +1,9 @@ +package jadx.zip; + +import java.io.Closeable; +import java.io.IOException; + +public interface IZipParser extends Closeable { + + ZipContent open() throws IOException; +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipContent.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipContent.java new file mode 100644 index 000000000..986fe13c8 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipContent.java @@ -0,0 +1,35 @@ +package jadx.zip; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +public class ZipContent implements Closeable { + private final IZipParser zipParser; + private final List entries; + private final Map entriesMap; + + public ZipContent(IZipParser zipParser, List entries) { + this.zipParser = zipParser; + this.entries = entries; + this.entriesMap = entries.stream().collect(Collectors.toMap(IZipEntry::getName, Function.identity())); + } + + public List getEntries() { + return entries; + } + + public @Nullable IZipEntry searchEntry(String fileName) { + return entriesMap.get(fileName); + } + + @Override + public void close() throws IOException { + zipParser.close(); + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReader.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReader.java new file mode 100644 index 000000000..a9cf6fbd6 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReader.java @@ -0,0 +1,111 @@ +package jadx.zip; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jetbrains.annotations.Nullable; + +import jadx.zip.fallback.FallbackZipParser; +import jadx.zip.parser.JadxZipParser; +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.JadxZipSecurity; + +/** + * Jadx wrapper to provide custom zip parser ({@link JadxZipParser}) + * with fallback to default Java implementation. + */ +public class ZipReader { + private final ZipReaderOptions options; + + public ZipReader() { + this(ZipReaderOptions.getDefault()); + } + + public ZipReader(Set flags) { + this(new ZipReaderOptions(new JadxZipSecurity(), flags)); + } + + public ZipReader(IJadxZipSecurity security) { + this(new ZipReaderOptions(security, ZipReaderFlags.none())); + } + + public ZipReader(ZipReaderOptions options) { + this.options = options; + } + + @SuppressWarnings("resource") + public ZipContent open(File zipFile) throws IOException { + try { + JadxZipParser jadxParser = new JadxZipParser(zipFile, options); + IZipParser detectedParser = detectParser(zipFile, jadxParser); + if (detectedParser != jadxParser) { + jadxParser.close(); + } + return detectedParser.open(); + } catch (Exception e) { + if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { + throw new IOException("Failed to open zip: " + zipFile, e); + } + // switch to fallback parser + return buildFallbackParser(zipFile).open(); + } + } + + /** + * Visit valid entries in a zip file. + * Return not null value from visitor to stop iteration. + */ + public @Nullable R visitEntries(File file, Function visitor) { + try (ZipContent content = open(file)) { + for (IZipEntry entry : content.getEntries()) { + R result = visitor.apply(entry); + if (result != null) { + return result; + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e); + } + return null; + } + + public void readEntries(File file, BiConsumer visitor) { + visitEntries(file, entry -> { + if (!entry.isDirectory()) { + try (InputStream in = entry.getInputStream()) { + visitor.accept(entry, in); + } catch (Exception e) { + throw new RuntimeException("Failed to process zip entry: " + entry, e); + } + } + return null; + }); + } + + public ZipReaderOptions getOptions() { + return options; + } + + private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) { + if (zipFile.getName().endsWith(".apk") + || options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) { + return jadxParser; + } + if (!jadxParser.canOpen()) { + return buildFallbackParser(zipFile); + } + // default + if (options.getFlags().contains(ZipReaderFlags.FALLBACK_AS_DEFAULT)) { + return buildFallbackParser(zipFile); + } + return jadxParser; + } + + private FallbackZipParser buildFallbackParser(File zipFile) { + return new FallbackZipParser(zipFile, options); + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderFlags.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderFlags.java new file mode 100644 index 000000000..45252a2b3 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderFlags.java @@ -0,0 +1,32 @@ +package jadx.zip; + +import java.util.EnumSet; +import java.util.Set; + +public enum ZipReaderFlags { + /** + * Search all local file headers by signature without reading + * 'central directory' and 'end of central directory' entries + */ + IGNORE_CENTRAL_DIR_ENTRIES, + + /** + * Enable additional checks to verify zip data and report possible tampering + */ + REPORT_TAMPERING, + + /** + * Use fallback (java built-in implementation) parser as default. + * Custom implementation will be used for '*.apk' files only. + */ + FALLBACK_AS_DEFAULT, + + /** + * Use only jadx custom parser and do not switch to fallback on errors. + */ + DONT_USE_FALLBACK; + + public static Set none() { + return EnumSet.noneOf(ZipReaderFlags.class); + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderOptions.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderOptions.java new file mode 100644 index 000000000..e7153de5f --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/ZipReaderOptions.java @@ -0,0 +1,29 @@ +package jadx.zip; + +import java.util.Set; + +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.JadxZipSecurity; + +public class ZipReaderOptions { + + public static ZipReaderOptions getDefault() { + return new ZipReaderOptions(new JadxZipSecurity(), ZipReaderFlags.none()); + } + + private final IJadxZipSecurity zipSecurity; + private final Set flags; + + public ZipReaderOptions(IJadxZipSecurity zipSecurity, Set flags) { + this.zipSecurity = zipSecurity; + this.flags = flags; + } + + public IJadxZipSecurity getZipSecurity() { + return zipSecurity; + } + + public Set getFlags() { + return flags; + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipEntry.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipEntry.java new file mode 100644 index 000000000..26ea33641 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipEntry.java @@ -0,0 +1,61 @@ +package jadx.zip.fallback; + +import java.io.File; +import java.io.InputStream; +import java.util.zip.ZipEntry; + +import jadx.zip.IZipEntry; + +public class FallbackZipEntry implements IZipEntry { + private final FallbackZipParser parser; + private final ZipEntry zipEntry; + + public FallbackZipEntry(FallbackZipParser parser, ZipEntry zipEntry) { + this.parser = parser; + this.zipEntry = zipEntry; + } + + public ZipEntry getZipEntry() { + return zipEntry; + } + + @Override + public String getName() { + return zipEntry.getName(); + } + + @Override + public boolean preferBytes() { + return false; + } + + @Override + public byte[] getBytes() { + return parser.getBytes(this); + } + + @Override + public InputStream getInputStream() { + return parser.getInputStream(this); + } + + @Override + public long getCompressedSize() { + return zipEntry.getCompressedSize(); + } + + @Override + public long getUncompressedSize() { + return zipEntry.getSize(); + } + + @Override + public boolean isDirectory() { + return zipEntry.isDirectory(); + } + + @Override + public File getZipFile() { + return parser.getZipFile(); + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipParser.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipParser.java new file mode 100644 index 000000000..11ecc0b2e --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/fallback/FallbackZipParser.java @@ -0,0 +1,109 @@ +package jadx.zip.fallback; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.zip.IZipEntry; +import jadx.zip.IZipParser; +import jadx.zip.ZipContent; +import jadx.zip.ZipReaderOptions; +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.LimitedInputStream; + +public class FallbackZipParser implements IZipParser { + private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class); + private final File file; + private final IJadxZipSecurity zipSecurity; + private final boolean useLimitedDataStream; + + private ZipFile zipFile; + + public FallbackZipParser(File file, ZipReaderOptions options) { + this.file = file; + this.zipSecurity = options.getZipSecurity(); + this.useLimitedDataStream = zipSecurity.useLimitedDataStream(); + } + + @Override + public ZipContent open() throws IOException { + zipFile = new ZipFile(file); + + int maxEntriesCount = zipSecurity.getMaxEntriesCount(); + if (maxEntriesCount == -1) { + maxEntriesCount = Integer.MAX_VALUE; + } + + List list = new ArrayList<>(); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement()); + if (isValidEntry(zipEntry)) { + list.add(zipEntry); + if (list.size() > maxEntriesCount) { + throw new IllegalStateException("Max entries count limit exceeded: " + list.size()); + } + } + } + return new ZipContent(this, list); + } + + private boolean isValidEntry(IZipEntry zipEntry) { + boolean validEntry = zipSecurity.isValidEntry(zipEntry); + if (!validEntry) { + LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry); + } + return validEntry; + } + + public byte[] getBytes(FallbackZipEntry entry) { + try (InputStream is = getEntryStream(entry)) { + return is.readAllBytes(); + } catch (Exception e) { + throw new RuntimeException("Failed to read bytes for entry: " + entry.getName(), e); + } + } + + public InputStream getInputStream(FallbackZipEntry entry) { + try { + return getEntryStream(entry); + } catch (Exception e) { + throw new RuntimeException("Failed to open input stream for entry: " + entry.getName(), e); + } + } + + private InputStream getEntryStream(FallbackZipEntry entry) throws IOException { + InputStream entryStream = zipFile.getInputStream(entry.getZipEntry()); + InputStream stream; + if (useLimitedDataStream) { + stream = new LimitedInputStream(entryStream, entry.getUncompressedSize()); + } else { + stream = entryStream; + } + return new BufferedInputStream(stream); + } + + public File getZipFile() { + return file; + } + + @Override + public void close() throws IOException { + try { + if (zipFile != null) { + zipFile.close(); + } + } finally { + zipFile = null; + } + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipEntry.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipEntry.java new file mode 100644 index 000000000..d019b4411 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipEntry.java @@ -0,0 +1,93 @@ +package jadx.zip.parser; + +import java.io.File; +import java.io.InputStream; + +import jadx.zip.IZipEntry; + +public final class JadxZipEntry implements IZipEntry { + private final JadxZipParser parser; + private final String fileName; + private final int compressMethod; + private final int entryStart; + private final int dataStart; + private final long compressedSize; + private final long uncompressedSize; + + JadxZipEntry(JadxZipParser parser, String fileName, int entryStart, int dataStart, + int compressMethod, long compressedSize, long uncompressedSize) { + this.parser = parser; + this.fileName = fileName; + this.entryStart = entryStart; + this.dataStart = dataStart; + this.compressMethod = compressMethod; + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + } + + public boolean isSizesValid() { + if (compressedSize <= 0) { + return false; + } + if (uncompressedSize <= 0) { + return false; + } + return compressedSize <= uncompressedSize; + } + + public String getName() { + return fileName; + } + + @Override + public long getCompressedSize() { + return compressedSize; + } + + @Override + public long getUncompressedSize() { + return uncompressedSize; + } + + @Override + public boolean isDirectory() { + return fileName.endsWith("/"); + } + + @Override + public boolean preferBytes() { + return true; + } + + @Override + public byte[] getBytes() { + return parser.getBytes(this); + } + + @Override + public InputStream getInputStream() { + return parser.getInputStream(this); + } + + public int getEntryStart() { + return entryStart; + } + + public int getDataStart() { + return dataStart; + } + + public int getCompressMethod() { + return compressMethod; + } + + @Override + public File getZipFile() { + return parser.getZipFile(); + } + + @Override + public String toString() { + return parser.getZipFile().getName() + ':' + fileName; + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java new file mode 100644 index 000000000..8df43a44e --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/JadxZipParser.java @@ -0,0 +1,403 @@ +package jadx.zip.parser; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.zip.IZipEntry; +import jadx.zip.IZipParser; +import jadx.zip.ZipContent; +import jadx.zip.ZipReaderFlags; +import jadx.zip.ZipReaderOptions; +import jadx.zip.fallback.FallbackZipParser; +import jadx.zip.security.IJadxZipSecurity; + +/** + * Custom and simple zip parser to fight tampering. + * Many zip features aren't supported: + * - Compression methods other than STORE or DEFLATE + * - Zip64 + * - Checksum verification + * - Multi file archives + */ +public final class JadxZipParser implements IZipParser { + private static final Logger LOG = LoggerFactory.getLogger(JadxZipParser.class); + + private static final byte LOCAL_FILE_HEADER_START = 0x50; + private static final int LOCAL_FILE_HEADER_SIGN = 0x04034b50; + private static final int CD_SIGN = 0x02014b50; + private static final int END_OF_CD_SIGN = 0x06054b50; + + private final File zipFile; + private final ZipReaderOptions options; + private final IJadxZipSecurity zipSecurity; + private final Set flags; + private final boolean verify; + + private RandomAccessFile file; + private FileChannel fileChannel; + private ByteBuffer byteBuffer; + + private int endOfCDStart = -2; + + private @Nullable ZipContent fallbackZipContent; + + public JadxZipParser(File zipFile, ZipReaderOptions options) { + this.zipFile = zipFile; + this.options = options; + this.zipSecurity = options.getZipSecurity(); + this.flags = options.getFlags(); + this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING); + } + + @Override + public ZipContent open() throws IOException { + load(); + try { + int maxEntriesCount = zipSecurity.getMaxEntriesCount(); + if (maxEntriesCount == -1) { + maxEntriesCount = Integer.MAX_VALUE; + } + List entries; + if (flags.contains(ZipReaderFlags.IGNORE_CENTRAL_DIR_ENTRIES)) { + entries = searchLocalFileHeaders(maxEntriesCount); + } else { + entries = loadFromCentralDirs(maxEntriesCount); + } + return new ZipContent(this, entries); + } catch (Exception e) { + if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) { + throw new IOException("Failed to open zip: " + zipFile + ", error: " + e.getMessage(), e); + } + LOG.warn("Zip open failed, switching to fallback parser, zip: {}", zipFile, e); + return initFallbackParser(); + } + } + + @SuppressWarnings("RedundantIfStatement") + public boolean canOpen() { + try { + load(); + int eocdStart = searchEndOfCDStart(); + ByteBuffer buf = byteBuffer; + buf.position(eocdStart + 4); + int diskNum = readU2(buf); + if (diskNum == 0xFFFF) { + // Zip64 + return false; + } + return true; + } catch (Exception e) { + LOG.warn("Jadx parser can't open zip file: {}", zipFile, e); + return false; + } + } + + private boolean isValidEntry(JadxZipEntry zipEntry) { + boolean validEntry = zipSecurity.isValidEntry(zipEntry); + if (!validEntry) { + LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry); + } + return validEntry; + } + + private void load() throws IOException { + if (byteBuffer != null) { + // already loaded + return; + } + file = new RandomAccessFile(zipFile, "r"); + long size = file.length(); + if (size >= Integer.MAX_VALUE) { + throw new IOException("Zip file is too big"); + } + int fileLen = (int) size; + if (fileLen < 100 * 1024 * 1024) { + // load files smaller than 100MB directly into memory + byte[] bytes = new byte[fileLen]; + file.readFully(bytes); + byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer(); + file.close(); + file = null; + } else { + // for big files - use a memory mapped file + fileChannel = file.getChannel(); + byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); + } + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + } + + private List searchLocalFileHeaders(int maxEntriesCount) { + List entries = new ArrayList<>(); + while (true) { + int start = searchEntryStart(); + if (start == -1) { + return entries; + } + JadxZipEntry zipEntry = loadFileEntry(start); + if (isValidEntry(zipEntry)) { + entries.add(zipEntry); + if (entries.size() > maxEntriesCount) { + throw new IllegalStateException("Max entries count limit exceeded: " + entries.size()); + } + } + } + } + + private List loadFromCentralDirs(int maxEntriesCount) throws IOException { + int eocdStart = searchEndOfCDStart(); + if (eocdStart < 0) { + throw new RuntimeException("End of central directory not found"); + } + ByteBuffer buf = byteBuffer; + buf.position(eocdStart + 10); + int entriesCount = readU2(buf); + buf.position(eocdStart + 16); + int cdOffset = buf.getInt(); + + if (entriesCount > maxEntriesCount) { + throw new IllegalStateException("Max entries count limit exceeded: " + entriesCount); + } + List entries = new ArrayList<>(entriesCount); + buf.position(cdOffset); + for (int i = 0; i < entriesCount; i++) { + JadxZipEntry zipEntry = loadCDEntry(); + if (isValidEntry(zipEntry)) { + entries.add(zipEntry); + } + } + return entries; + } + + private JadxZipEntry loadCDEntry() { + ByteBuffer buf = byteBuffer; + int start = buf.position(); + buf.position(start + 28); + int fileNameLen = readU2(buf); + int extraFieldLen = readU2(buf); + int commentLen = readU2(buf); + buf.position(start + 42); + int fileEntryStart = buf.getInt(); + int entryEnd = start + 46 + fileNameLen + extraFieldLen + commentLen; + JadxZipEntry entry = loadFileEntry(fileEntryStart); + if (verify) { + compareCDAndLFH(buf, start, entry); + } + if (!entry.isSizesValid()) { + entry = fixEntryFromCD(entry, start); + } + buf.position(entryEnd); + return entry; + } + + private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) { + ByteBuffer buf = byteBuffer; + buf.position(start + 10); + int comprMethod = readU2(buf); + buf.position(start + 20); + int comprSize = buf.getInt(); + int unComprSize = buf.getInt(); + return new JadxZipEntry(this, entry.getName(), start, entry.getDataStart(), comprMethod, comprSize, unComprSize); + } + + private static void compareCDAndLFH(ByteBuffer buf, int start, JadxZipEntry entry) { + buf.position(start + 10); + int comprMethod = readU2(buf); + if (comprMethod != entry.getCompressMethod()) { + LOG.warn("Compression method differ in CD {} and LFH {} for {}", + comprMethod, entry.getCompressMethod(), entry); + } + buf.position(start + 20); + int comprSize = buf.getInt(); + int unComprSize = buf.getInt(); + if (comprSize != entry.getCompressedSize()) { + LOG.warn("Compressed size differ in CD {} and LFH {} for {}", + comprSize, entry.getCompressedSize(), entry); + } + if (unComprSize != entry.getUncompressedSize()) { + LOG.warn("Uncompressed size differ in CD {} and LFH {} for {}", + unComprSize, entry.getUncompressedSize(), entry); + } + } + + private JadxZipEntry loadFileEntry(int start) { + ByteBuffer buf = byteBuffer; + buf.position(start + 8); + int comprMethod = readU2(buf); + buf.position(start + 18); + int comprSize = buf.getInt(); + int unComprSize = buf.getInt(); + int fileNameLen = readU2(buf); + int extraFieldLen = readU2(buf); + String fileName = readString(buf, fileNameLen); + int dataStart = start + 30 + fileNameLen + extraFieldLen; + buf.position(dataStart + comprSize); + return new JadxZipEntry(this, fileName, start, dataStart, comprMethod, comprSize, unComprSize); + } + + private int searchEndOfCDStart() throws IOException { + if (endOfCDStart != -2) { + return endOfCDStart; + } + ByteBuffer buf = byteBuffer; + int pos = buf.limit() - 22; + int minPos = Math.max(0, pos - 0xffff); + while (true) { + buf.position(pos); + int sign = buf.getInt(); + if (sign == END_OF_CD_SIGN) { + endOfCDStart = pos; + return pos; + } + pos--; + if (pos < minPos) { + throw new IOException("End of central directory record not found"); + } + } + } + + private int searchEntryStart() { + ByteBuffer buf = byteBuffer; + while (true) { + int start = buf.position(); + if (start + 4 > buf.limit()) { + return -1; + } + byte b = buf.get(); + if (b == LOCAL_FILE_HEADER_START) { + buf.position(start); + int sign = buf.getInt(); + if (sign == LOCAL_FILE_HEADER_SIGN) { + return start; + } + } + } + } + + InputStream getInputStream(JadxZipEntry entry) { + return new ByteArrayInputStream(getBytes(entry)); + } + + synchronized byte[] getBytes(JadxZipEntry entry) { + int compressMethod = entry.getCompressMethod(); + if (verify) { + if (compressMethod == 0) { + if (entry.getCompressedSize() != entry.getUncompressedSize()) { + LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}", + entry.getCompressedSize(), entry.getUncompressedSize(), entry); + } + } else if (compressMethod != 8) { + LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry); + } + } + if (compressMethod == 8) { + try { + return ZipDeflate.decompressEntryToBytes(byteBuffer, entry); + } catch (Exception e) { + if (isEncrypted(entry)) { + throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e); + } + if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) { + throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e); + } + LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e); + return useFallbackParser(entry); + } + } + // treat any other compression methods values as UNCOMPRESSED + return bufferToBytes(entry.getDataStart(), (int) entry.getUncompressedSize()); + } + + @SuppressWarnings("resource") + private byte[] useFallbackParser(JadxZipEntry entry) { + IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName()); + if (zipEntry == null) { + throw new RuntimeException("Fallback parser can't find entry: " + entry); + } + return zipEntry.getBytes(); + } + + @SuppressWarnings("resource") + private ZipContent initFallbackParser() { + if (fallbackZipContent == null) { + try { + fallbackZipContent = new FallbackZipParser(zipFile, options).open(); + } catch (Exception e) { + throw new RuntimeException("Fallback parser failed to open file: " + zipFile, e); + } + } + return fallbackZipContent; + } + + private boolean isEncrypted(JadxZipEntry entry) { + int flags = readFlags(entry); + return (flags & 1) != 0; + } + + private int readFlags(JadxZipEntry entry) { + ByteBuffer buf = byteBuffer; + buf.position(entry.getEntryStart() + 6); + return readU2(buf); + } + + byte[] bufferToBytes(int start, int size) { + ByteBuffer buf = byteBuffer; + byte[] data = new byte[size]; + buf.position(start); + buf.get(data); + return data; + } + + private static int readU2(ByteBuffer buf) { + return buf.getShort() & 0xFFFF; + } + + private static String readString(ByteBuffer buf, int fileNameLen) { + byte[] bytes = new byte[fileNameLen]; + buf.get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public void close() throws IOException { + try { + if (fileChannel != null) { + fileChannel.close(); + } + if (file != null) { + file.close(); + } + if (fallbackZipContent != null) { + fallbackZipContent.close(); + } + } finally { + fileChannel = null; + file = null; + byteBuffer = null; + endOfCDStart = -2; + fallbackZipContent = null; + } + } + + public File getZipFile() { + return zipFile; + } + + @Override + public String toString() { + return "JadxZipParser{" + zipFile + '}'; + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/ZipDeflate.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/ZipDeflate.java new file mode 100644 index 000000000..357314671 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/parser/ZipDeflate.java @@ -0,0 +1,24 @@ +package jadx.zip.parser; + +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +final class ZipDeflate { + + static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException { + buf.position(entry.getDataStart()); + ByteBuffer entryBuf = buf.slice(); + entryBuf.limit((int) entry.getCompressedSize()); + byte[] out = new byte[(int) entry.getUncompressedSize()]; + Inflater inflater = new Inflater(true); + inflater.setInput(entryBuf); + int written = inflater.inflate(out); + if (written != out.length) { + throw new DataFormatException("Unexpected size of decompressed entry: " + entry + + ", got: " + written + ", expected: " + out.length); + } + inflater.end(); + return out; + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/DisabledZipSecurity.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/DisabledZipSecurity.java new file mode 100644 index 000000000..d13a9b56b --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/DisabledZipSecurity.java @@ -0,0 +1,35 @@ +package jadx.zip.security; + +import java.io.File; + +import jadx.zip.IZipEntry; + +public class DisabledZipSecurity implements IJadxZipSecurity { + + public static final DisabledZipSecurity INSTANCE = new DisabledZipSecurity(); + + @Override + public boolean isValidEntry(IZipEntry entry) { + return true; + } + + @Override + public boolean isValidEntryName(String entryName) { + return true; + } + + @Override + public boolean isInSubDirectory(File baseDir, File file) { + return true; + } + + @Override + public boolean useLimitedDataStream() { + return false; + } + + @Override + public int getMaxEntriesCount() { + return -1; + } +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/IJadxZipSecurity.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/IJadxZipSecurity.java new file mode 100644 index 000000000..44f490a18 --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/IJadxZipSecurity.java @@ -0,0 +1,35 @@ +package jadx.zip.security; + +import java.io.File; + +import jadx.zip.IZipEntry; + +public interface IJadxZipSecurity { + + /** + * Check if zip entry is valid and safe to process + */ + boolean isValidEntry(IZipEntry entry); + + /** + * Check if the zip entry name is valid. + * This check should be part of {@link #isValidEntry(IZipEntry)} method. + */ + boolean isValidEntryName(String entryName); + + /** + * Use limited InputStream for entry uncompressed data + */ + boolean useLimitedDataStream(); + + /** + * Max entries count expected in a zip file, fail zip open if the limit exceeds. + * Return -1 to disable entries count check. + */ + int getMaxEntriesCount(); + + /** + * Check if a file will be inside baseDir after a system resolves its path + */ + boolean isInSubDirectory(File baseDir, File file); +} diff --git a/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/JadxZipSecurity.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/JadxZipSecurity.java new file mode 100644 index 000000000..7b1404cca --- /dev/null +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/JadxZipSecurity.java @@ -0,0 +1,126 @@ +package jadx.zip.security; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.zip.IZipEntry; + +public class JadxZipSecurity implements IJadxZipSecurity { + private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class); + + private static final File CWD = getCWD(); + + /** + * The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor + * times + */ + private int zipBombDetectionFactor = 100; + + /** + * Zip entries that have an uncompressed size of less than zipBombMinUncompressedSize are considered + * safe + */ + private int zipBombMinUncompressedSize = 25 * 1024 * 1024; + + private int maxEntriesCount = 100_000; + + @Override + public boolean isValidEntry(IZipEntry entry) { + return isValidEntryName(entry.getName()) && !isZipBomb(entry); + } + + @Override + public boolean useLimitedDataStream() { + return false; + } + + @Override + public int getMaxEntriesCount() { + return maxEntriesCount; + } + + /** + * Checks that entry name contains no any traversals and prevents cases like "../classes.dex", + * to limit output only to the specified directory + */ + @Override + public boolean isValidEntryName(String entryName) { + if (entryName.contains("..")) { // quick pre-check + if (entryName.contains("../") || entryName.contains("..\\")) { + LOG.error("Path traversal attack detected in entry: '{}'", entryName); + return false; + } + } + try { + File currentPath = CWD; + File canonical = new File(currentPath, entryName).getCanonicalFile(); + if (isInSubDirectoryInternal(currentPath, canonical)) { + return true; + } + } catch (Exception e) { + // check failed + } + LOG.error("Invalid file name or path traversal attack detected: {}", entryName); + return false; + } + + @Override + public boolean isInSubDirectory(File baseDir, File file) { + try { + return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile()); + } catch (IOException e) { + return false; + } + } + + public boolean isZipBomb(IZipEntry entry) { + long compressedSize = entry.getCompressedSize(); + long uncompressedSize = entry.getUncompressedSize(); + boolean invalidSize = compressedSize < 0 || uncompressedSize < 0; + boolean possibleZipBomb = uncompressedSize >= zipBombMinUncompressedSize + && compressedSize * zipBombDetectionFactor < uncompressedSize; + if (invalidSize || possibleZipBomb) { + LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", + compressedSize, uncompressedSize, entry.getName()); + return true; + } + return false; + } + + private static boolean isInSubDirectoryInternal(File baseDir, File file) { + File current = file; + while (true) { + if (current == null) { + return false; + } + if (current.equals(baseDir)) { + return true; + } + current = current.getParentFile(); + } + } + + public void setMaxEntriesCount(int maxEntriesCount) { + this.maxEntriesCount = maxEntriesCount; + } + + public void setZipBombDetectionFactor(int zipBombDetectionFactor) { + this.zipBombDetectionFactor = zipBombDetectionFactor; + } + + public void setZipBombMinUncompressedSize(int zipBombMinUncompressedSize) { + this.zipBombMinUncompressedSize = zipBombMinUncompressedSize; + } + + private static File getCWD() { + try { + return new File(".").getCanonicalFile(); + } catch (IOException e) { + throw new RuntimeException("Failed to init current working dir constant", e); + } + } + +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/LimitedInputStream.java similarity index 67% rename from jadx-core/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java rename to jadx-commons/jadx-zip/src/main/java/jadx/zip/security/LimitedInputStream.java index dfa4ba5ee..66a9e04f5 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/utils/LimitedInputStream.java +++ b/jadx-commons/jadx-zip/src/main/java/jadx/zip/security/LimitedInputStream.java @@ -1,21 +1,21 @@ -package jadx.api.plugins.utils; +package jadx.zip.security; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; public class LimitedInputStream extends FilterInputStream { - private final long maxSize; private long currentPos; - protected LimitedInputStream(InputStream in, long maxSize) { + public LimitedInputStream(InputStream in, long maxSize) { super(in); this.maxSize = maxSize; } - private void checkPos() { + private void addAndCheckPos(long count) { + currentPos += count; if (currentPos > maxSize) { throw new IllegalStateException("Read limit exceeded"); } @@ -25,18 +25,17 @@ public class LimitedInputStream extends FilterInputStream { public int read() throws IOException { int data = super.read(); if (data != -1) { - currentPos++; - checkPos(); + addAndCheckPos(1); } return data; } + @SuppressWarnings("NullableProblems") @Override public int read(byte[] b, int off, int len) throws IOException { int count = super.read(b, off, len); if (count > 0) { - currentPos += count; - checkPos(); + addAndCheckPos(count); } return count; } @@ -44,10 +43,14 @@ public class LimitedInputStream extends FilterInputStream { @Override public long skip(long n) throws IOException { long skipped = super.skip(n); - if (skipped != 0) { - currentPos += skipped; - checkPos(); + if (skipped > 0) { + addAndCheckPos(skipped); } return skipped; } + + @Override + public boolean markSupported() { + return false; + } } diff --git a/jadx-core/build.gradle.kts b/jadx-core/build.gradle.kts index 7fb87c570..c49449c1b 100644 --- a/jadx-core/build.gradle.kts +++ b/jadx-core/build.gradle.kts @@ -4,6 +4,7 @@ plugins { dependencies { api(project(":jadx-plugins:jadx-input-api")) + api(project(":jadx-commons:jadx-zip")) implementation("com.google.code.gson:gson:2.11.0") diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 09226c838..09d7daadb 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -50,9 +50,9 @@ import jadx.core.utils.DecompilerScheduler; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; -import jadx.core.utils.files.ZipPatch; import jadx.core.utils.tasks.TaskExecutor; import jadx.core.xmlgen.ResourcesSaver; +import jadx.zip.ZipReader; /** * Jadx API usage example: @@ -87,6 +87,7 @@ public final class JadxDecompiler implements Closeable { private final JadxArgs args; private final JadxPluginManager pluginManager; private final List loadedInputs = new ArrayList<>(); + private final ZipReader zipReader; private RootNode root; private List classes; @@ -109,6 +110,7 @@ public final class JadxDecompiler implements Closeable { this.args = Objects.requireNonNull(args); this.pluginManager = new JadxPluginManager(this); this.resourcesLoader = new ResourcesLoader(this); + this.zipReader = new ZipReader(args.getSecurity()); } public void load() { @@ -145,9 +147,7 @@ public final class JadxDecompiler implements Closeable { private void loadInputFiles() { loadedInputs.clear(); - List inputs = ZipPatch.patchZipFiles(args.getInputFiles()); - args.setInputFiles(inputs); - List inputPaths = Utils.collectionMap(inputs, File::toPath); + List inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath); List inputFiles = FileUtils.expandDirs(inputPaths); long start = System.currentTimeMillis(); for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) { @@ -333,7 +333,7 @@ public final class JadxDecompiler implements Closeable { // process AndroidManifest.xml first to load complete resource ids table for (ResourceFile resourceFile : getResources()) { if (resourceFile.getType() == ResourceType.MANIFEST) { - new ResourcesSaver(outDir, resourceFile).run(); + new ResourcesSaver(this, outDir, resourceFile).run(); break; } } @@ -352,7 +352,7 @@ public final class JadxDecompiler implements Closeable { // ignore resource made from input file continue; } - tasks.add(new ResourcesSaver(outDir, resourceFile)); + tasks.add(new ResourcesSaver(this, outDir, resourceFile)); } executor.addParallelTasks(tasks); } @@ -702,6 +702,10 @@ public final class JadxDecompiler implements Closeable { return resourcesLoader; } + public ZipReader getZipReader() { + return zipReader; + } + @Override public String toString() { return "jadx decompiler " + getVersion(); diff --git a/jadx-core/src/main/java/jadx/api/ResourceFile.java b/jadx-core/src/main/java/jadx/api/ResourceFile.java index bd92e73aa..1ea87eae7 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceFile.java +++ b/jadx-core/src/main/java/jadx/api/ResourceFile.java @@ -2,7 +2,6 @@ package jadx.api; import java.io.File; -import jadx.api.plugins.utils.ZipSecurity; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.entry.ResourceEntry; @@ -42,7 +41,7 @@ public class ResourceFile { } public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) { - if (!ZipSecurity.isValidZipEntryName(name)) { + if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) { return null; } return new ResourceFile(decompiler, name, type); @@ -98,6 +97,10 @@ public class ResourceFile { return zipRef; } + public JadxDecompiler getDecompiler() { + return decompiler; + } + @Override public String toString() { return "ResourceFile{name='" + name + '\'' + ", type=" + type + '}'; diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index 96e3cfdd5..e84ccc886 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -6,9 +6,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.zip.ZipEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,18 +19,19 @@ import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.resources.IResContainerFactory; import jadx.api.plugins.resources.IResTableParserProvider; import jadx.api.plugins.resources.IResourcesLoader; -import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.Utils; import jadx.core.utils.android.Res9patchStreamDecoder; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; -import jadx.core.utils.files.ZipFile; import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.IResTableParser; import jadx.core.xmlgen.ResContainer; import jadx.core.xmlgen.ResTableBinaryParserProvider; +import jadx.zip.IZipEntry; +import jadx.zip.ZipContent; +import jadx.zip.ZipReader; import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE; import static jadx.core.utils.files.FileUtils.copyStream; @@ -101,21 +102,19 @@ public final class ResourcesLoader implements IResourcesLoader { return decoder.decode(file.length(), inputStream); } } else { - try (ZipFile zipFile = new ZipFile(zipRef.getZipFile())) { - ZipEntry entry = zipFile.getEntry(zipRef.getEntryName()); + ZipReader zipReader = rf.getDecompiler().getZipReader(); + try (ZipContent content = zipReader.open(zipRef.getZipFile())) { + IZipEntry entry = content.searchEntry(zipRef.getEntryName()); if (entry == null) { throw new IOException("Zip entry not found: " + zipRef); } - if (!ZipSecurity.isValidZipEntry(entry)) { - return null; - } - try (InputStream inputStream = ZipSecurity.getInputStreamForEntry(zipFile, entry)) { - return decoder.decode(entry.getSize(), inputStream); + try (InputStream inputStream = entry.getInputStream()) { + return decoder.decode(entry.getUncompressedSize(), inputStream); } } } } catch (Exception e) { - throw new JadxException("Error decode: " + rf.getDeobfName(), e); + throw new JadxException("Error decode: " + rf.getOriginalName(), e); } } @@ -208,7 +207,7 @@ public final class ResourcesLoader implements IResourcesLoader { public void defaultLoadFile(List list, File file, String subDir) { if (FileUtils.isZipFile(file)) { - ZipSecurity.visitZipEntries(file, (zipFile, entry) -> { + jadxRef.getZipReader().visitEntries(file, entry -> { addEntry(list, file, entry, subDir); return null; }); @@ -218,7 +217,7 @@ public final class ResourcesLoader implements IResourcesLoader { } } - public void addEntry(List list, File zipFile, ZipEntry entry, String subDir) { + public void addEntry(List list, File zipFile, IZipEntry entry, String subDir) { if (entry.isDirectory()) { return; } @@ -234,7 +233,7 @@ public final class ResourcesLoader implements IResourcesLoader { public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE); copyStream(is, baos); - return new SimpleCodeInfo(baos.toString("UTF-8")); + return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8)); } private synchronized BinaryXMLParser loadBinaryXmlParser() { diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java index d70cb7165..edfb3c252 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginContext.java @@ -14,6 +14,7 @@ import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.options.JadxPluginOptions; import jadx.api.plugins.pass.JadxPass; import jadx.api.plugins.resources.IResourcesLoader; +import jadx.zip.ZipReader; public interface JadxPluginContext { @@ -59,4 +60,9 @@ public interface JadxPluginContext { * Access to plugin specific files and directories */ IJadxFiles files(); + + /** + * Custom jadx zip reader to fight tampering and provide additional security checks + */ + ZipReader getZipReader(); } diff --git a/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java b/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java index 1c5eff98f..2d3cf9b74 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java +++ b/jadx-core/src/main/java/jadx/api/plugins/utils/ZipSecurity.java @@ -4,63 +4,52 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.Enumeration; import java.util.function.BiConsumer; -import java.util.function.BiFunction; +import java.util.function.Function; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import jadx.api.JadxDecompiler; +import jadx.api.plugins.JadxPluginContext; import jadx.core.utils.Utils; -import jadx.core.utils.exceptions.JadxRuntimeException; -import jadx.core.utils.files.ZipFile; +import jadx.zip.IZipEntry; +import jadx.zip.ZipReader; +import jadx.zip.security.DisabledZipSecurity; +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.JadxZipSecurity; +import jadx.zip.security.LimitedInputStream; +/** + * Deprecated, migrate to {@link ZipReader}.
+ * Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or + * {@link JadxPluginContext#getZipReader()}. + */ +@Deprecated public class ZipSecurity { - private static final Logger LOG = LoggerFactory.getLogger(ZipSecurity.class); - private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_ZIP_SECURITY", false); - /** - * size of uncompressed zip entry shouldn't be bigger of compressed in - * {@link #ZIP_BOMB_DETECTION_FACTOR} times - */ - private static final int ZIP_BOMB_DETECTION_FACTOR = 100; - - /** - * Zip entries that have an uncompressed size of less than {@link #ZIP_BOMB_MIN_UNCOMPRESSED_SIZE} - * are considered safe - */ - private static final int ZIP_BOMB_MIN_UNCOMPRESSED_SIZE = 25 * 1024 * 1024; - private static final int MAX_ENTRIES_COUNT = Utils.getEnvVarInt("JADX_ZIP_MAX_ENTRIES_COUNT", 100_000); + private static final IJadxZipSecurity ZIP_SECURITY = buildZipSecurity(); + + private static final ZipReader ZIP_READER = new ZipReader(ZIP_SECURITY); + + private static IJadxZipSecurity buildZipSecurity() { + if (DISABLE_CHECKS) { + return DisabledZipSecurity.INSTANCE; + } + JadxZipSecurity jadxZipSecurity = new JadxZipSecurity(); + jadxZipSecurity.setMaxEntriesCount(MAX_ENTRIES_COUNT); + return jadxZipSecurity; + } + private ZipSecurity() { } - private static boolean isInSubDirectoryInternal(File baseDir, File file) { - File current = file; - while (true) { - if (current == null) { - return false; - } - if (current.equals(baseDir)) { - return true; - } - current = current.getParentFile(); - } - } - public static boolean isInSubDirectory(File baseDir, File file) { - if (DISABLE_CHECKS) { - return true; - } - try { - return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile()); - } catch (IOException e) { - return false; - } + return ZIP_SECURITY.isInSubDirectory(baseDir, file); } /** @@ -68,48 +57,15 @@ public class ZipSecurity { * to limit output only to the specified directory */ public static boolean isValidZipEntryName(String entryName) { - if (DISABLE_CHECKS) { - return true; - } - if (entryName.contains("..")) { // quick pre-check - if (entryName.contains("../") || entryName.contains("..\\")) { - LOG.error("Path traversal attack detected in entry: '{}'", entryName); - return false; - } - } - try { - File currentPath = CommonFileUtils.CWD; - File canonical = new File(currentPath, entryName).getCanonicalFile(); - if (isInSubDirectoryInternal(currentPath, canonical)) { - return true; - } - } catch (Exception e) { - // check failed - } - LOG.error("Invalid file name or path traversal attack detected: {}", entryName); - return false; + return ZIP_SECURITY.isValidEntryName(entryName); } - public static boolean isZipBomb(ZipEntry entry) { - if (DISABLE_CHECKS) { - return false; - } - long compressedSize = entry.getCompressedSize(); - long uncompressedSize = entry.getSize(); - boolean invalidSize = (compressedSize < 0) || (uncompressedSize < 0); - boolean possibleZipBomb = (uncompressedSize >= ZIP_BOMB_MIN_UNCOMPRESSED_SIZE) - && (compressedSize * ZIP_BOMB_DETECTION_FACTOR < uncompressedSize); - if (invalidSize || possibleZipBomb) { - LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}", - compressedSize, uncompressedSize, entry.getName()); - return true; - } - return false; + public static boolean isZipBomb(IZipEntry entry) { + return !ZIP_SECURITY.isValidEntry(entry); } - public static boolean isValidZipEntry(ZipEntry entry) { - return isValidZipEntryName(entry.getName()) - && !isZipBomb(entry); + public static boolean isValidZipEntry(IZipEntry entry) { + return ZIP_SECURITY.isValidEntry(entry); } public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException { @@ -122,44 +78,15 @@ public class ZipSecurity { } /** - * Visit valid entries in zip file. + * Visit valid entries in a zip file. * Return not null value from visitor to stop iteration. */ @Nullable - public static R visitZipEntries(File file, BiFunction visitor) { - try (ZipFile zip = new ZipFile(file)) { - Enumeration entries = zip.entries(); - int entriesProcessed = 0; - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (isValidZipEntry(entry)) { - R result = visitor.apply(zip, entry); - if (result != null) { - return result; - } - entriesProcessed++; - if (!DISABLE_CHECKS && entriesProcessed > MAX_ENTRIES_COUNT) { - throw new JadxRuntimeException("Zip entries count limit exceeded: " + MAX_ENTRIES_COUNT - + ", last entry: " + entry.getName()); - } - } - } - } catch (Exception e) { - throw new JadxRuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e); - } - return null; + public static R visitZipEntries(File file, Function visitor) { + return ZIP_READER.visitEntries(file, visitor); } - public static void readZipEntries(File file, BiConsumer visitor) { - visitZipEntries(file, (zip, entry) -> { - if (!entry.isDirectory()) { - try (InputStream in = getInputStreamForEntry(zip, entry)) { - visitor.accept(entry, in); - } catch (Exception e) { - throw new JadxRuntimeException("Failed to process zip entry: " + entry.getName()); - } - } - return null; - }); + public static void readZipEntries(File file, BiConsumer visitor) { + ZIP_READER.readEntries(file, visitor); } } diff --git a/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java b/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java index a3bd5f97f..956a669a2 100644 --- a/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java +++ b/jadx-core/src/main/java/jadx/api/security/IJadxSecurity.java @@ -4,7 +4,9 @@ import java.io.InputStream; import org.w3c.dom.Document; -public interface IJadxSecurity { +import jadx.zip.security.IJadxZipSecurity; + +public interface IJadxSecurity extends IJadxZipSecurity { /** * Check if application package is safe diff --git a/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java b/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java index 1073453d9..471ea0df6 100644 --- a/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java +++ b/jadx-core/src/main/java/jadx/api/security/JadxSecurityFlag.java @@ -6,7 +6,8 @@ import java.util.Set; public enum JadxSecurityFlag { VERIFY_APP_PACKAGE, - SECURE_XML_PARSER; + SECURE_XML_PARSER, + SECURE_ZIP_READER; public static Set all() { return EnumSet.allOf(JadxSecurityFlag.class); diff --git a/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java b/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java index ed412319b..960330bf5 100644 --- a/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java +++ b/jadx-core/src/main/java/jadx/api/security/impl/JadxSecurity.java @@ -1,5 +1,6 @@ package jadx.api.security.impl; +import java.io.File; import java.io.InputStream; import java.util.Set; @@ -12,14 +13,52 @@ import org.w3c.dom.Document; import jadx.api.security.IJadxSecurity; import jadx.api.security.JadxSecurityFlag; import jadx.core.deobf.NameMapper; +import jadx.zip.IZipEntry; +import jadx.zip.security.DisabledZipSecurity; +import jadx.zip.security.IJadxZipSecurity; +import jadx.zip.security.JadxZipSecurity; + +import static jadx.api.security.JadxSecurityFlag.SECURE_ZIP_READER; public class JadxSecurity implements IJadxSecurity { private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class); private final Set flags; + private final IJadxZipSecurity zipSecurity; public JadxSecurity(Set flags) { this.flags = flags; + this.zipSecurity = flags.contains(SECURE_ZIP_READER) ? new JadxZipSecurity() : DisabledZipSecurity.INSTANCE; + } + + public JadxSecurity(Set flags, IJadxZipSecurity zipSecurity) { + this.flags = flags; + this.zipSecurity = zipSecurity; + } + + @Override + public boolean isValidEntry(IZipEntry entry) { + return zipSecurity.isValidEntry(entry); + } + + @Override + public boolean isValidEntryName(String entryName) { + return zipSecurity.isValidEntryName(entryName); + } + + @Override + public boolean isInSubDirectory(File baseDir, File file) { + return zipSecurity.isInSubDirectory(baseDir, file); + } + + @Override + public boolean useLimitedDataStream() { + return zipSecurity.useLimitedDataStream(); + } + + @Override + public int getMaxEntriesCount() { + return zipSecurity.getMaxEntriesCount(); } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java index c9eb6e64a..ea32b47a5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java @@ -2,13 +2,13 @@ package jadx.core.dex.visitors; import java.io.File; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.JadxArgs; -import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; @@ -35,18 +35,15 @@ public class SaveCode { if (codeStr.isEmpty()) { return; } - if (cls.root().getArgs().isSkipFilesSave()) { + JadxArgs args = cls.root().getArgs(); + if (args.isSkipFilesSave()) { return; } String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls.root()); - save(codeStr, dir, fileName); - } - - public static void save(String code, File dir, String fileName) { - if (!ZipSecurity.isValidZipEntryName(fileName)) { + if (!args.getSecurity().isValidEntryName(fileName)) { return; } - save(code, new File(dir, fileName)); + save(codeStr, new File(dir, fileName)); } public static void save(ICodeInfo codeInfo, File file) { @@ -55,7 +52,7 @@ public class SaveCode { public static void save(String code, File file) { File outFile = FileUtils.prepareFile(file); - try (PrintWriter out = new PrintWriter(outFile, "UTF-8")) { + try (PrintWriter out = new PrintWriter(outFile, StandardCharsets.UTF_8)) { out.println(code); } catch (Exception e) { LOG.error("Save file error", e); diff --git a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java index 4739dd0d0..043424e70 100644 --- a/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java +++ b/jadx-core/src/main/java/jadx/core/plugins/PluginContext.java @@ -32,6 +32,7 @@ import jadx.core.plugins.files.JadxFilesData; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; +import jadx.zip.ZipReader; public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData, Comparable { private final JadxDecompiler decompiler; @@ -190,6 +191,11 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData, closeable); } + @Override + public ZipReader getZipReader() { + return decompiler.getZipReader(); + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index 835485c80..85d52d4ac 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -24,9 +24,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.security.MessageDigest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.stream.Collectors; @@ -276,6 +276,11 @@ public class FileUtils { StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } + public static void writeFile(Path file, byte[] data) throws IOException { + FileUtils.makeDirsForFile(file); + Files.write(file, data, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + public static void writeFile(Path file, InputStream is) throws IOException { FileUtils.makeDirsForFile(file); Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING); @@ -358,20 +363,18 @@ public class FileUtils { return new String(hexChars, StandardCharsets.US_ASCII); } + private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 }; + public static boolean isZipFile(File file) { try (InputStream is = new FileInputStream(file)) { - byte[] headers = new byte[4]; - int read = is.read(headers, 0, 4); - if (read == headers.length) { - String headerString = bytesToHex(headers); - if (Objects.equals(headerString, "504b0304")) { - return true; - } - } + int len = ZIP_FILE_MAGIC.length; + byte[] headers = new byte[len]; + int read = is.read(headers); + return read == len && Arrays.equals(headers, ZIP_FILE_MAGIC); } catch (Exception e) { - LOG.error("Failed read zip file: {}", file.getAbsolutePath(), e); + LOG.error("Failed to read zip file: {}", file.getAbsolutePath(), e); + return false; } - return false; } public static String getPathBaseName(Path file) { diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java deleted file mode 100644 index c9a366d34..000000000 --- a/jadx-core/src/main/java/jadx/core/utils/files/ZipFile.java +++ /dev/null @@ -1,32 +0,0 @@ -package jadx.core.utils.files; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -/** - * Deprecated zip file wrapper - */ -public class ZipFile extends java.util.zip.ZipFile { - - public ZipFile(File file) throws IOException { - this(file, OPEN_READ); - } - - public ZipFile(File file, int mode) throws IOException { - this(file, mode, StandardCharsets.UTF_8); - } - - public ZipFile(String name, Charset charset) throws IOException { - this(new File(name), OPEN_READ, charset); - } - - public ZipFile(String name) throws IOException { - this(name, StandardCharsets.UTF_8); - } - - public ZipFile(File file, int mode, Charset charset) throws IOException { - super(file, mode, charset); - } -} diff --git a/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java b/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java deleted file mode 100644 index a08acc491..000000000 --- a/jadx-core/src/main/java/jadx/core/utils/files/ZipPatch.java +++ /dev/null @@ -1,199 +0,0 @@ -package jadx.core.utils.files; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.lang.reflect.UndeclaredThrowableException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ZipPatch { - private static final Logger LOG = LoggerFactory.getLogger(ZipPatch.class); - - public static List patchZipFiles(List inputs) { - List result = new ArrayList<>(inputs.size()); - for (File input : inputs) { - try { - result.add(patchZipFile(input)); - } catch (Throwable e) { - LOG.warn("Failed to patch zip file: {}", input.getAbsolutePath(), e); - result.add(input); - } - } - return result; - } - - private static File patchZipFile(File file) throws IOException { - String fileName = file.getPath().toLowerCase(); - if (!fileName.endsWith(".apk") && !fileName.endsWith(".zip")) { - return file; - } - - var cDirEntriesToFix = new ArrayList(); - var localHeaders = new ArrayList(); - List localHeaderToFix; - - try (var raFile = new RandomAccessFile(file, "r")) { - var endOfCDirOffset = findEndOfCentralDir(raFile); - - raFile.seek(endOfCDirOffset + 0x10); - var cDirOffset = Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt())); - raFile.seek(endOfCDirOffset + 0x0a); - var cDirNumEntries = Short.toUnsignedLong(Short.reverseBytes(raFile.readShort())); - - for (long i = 0, off = cDirOffset; i < cDirNumEntries; i++) { - var info = readHeader(raFile, off); - - if (!info.validCompression()) { - cDirEntriesToFix.add(off); - } - - raFile.seek(off + 0x2a); - localHeaders.add(Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt()))); - - off += info.dataOffset; - } - - localHeaderToFix = localHeaders - .stream() - .filter(off -> !readHeaderVexxed(raFile, off).validCompression()) - .collect(Collectors.toList()); - - if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) { - return file; - } - } - - var newFile = copyFile(file); - - try (var newRaFile = new RandomAccessFile(newFile, "rwd")) { - - for (var off : cDirEntriesToFix) { - var info = readHeader(newRaFile, off); - - newRaFile.seek(off + 0x0a); - newRaFile.writeShort(0); - - newRaFile.seek(off + 0x14); - newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); - - } - - for (var off : localHeaderToFix) { - var info = readHeader(newRaFile, off); - - newRaFile.seek(off + 0x08); - newRaFile.writeShort(0); - - newRaFile.seek(off + 0x12); - newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize)); - - newRaFile.seek(off + 0x1c); - newRaFile.writeShort(0); - - moveBlockBack(newRaFile, off + info.dataOffset, info.uncompressedSize, info.extraLen); - } - } - LOG.info("Input zip file patched: {}", file.getAbsolutePath()); - return newFile; - } - - private static void moveBlockBack(RandomAccessFile file, long offset, long size, long delta) throws IOException { - var buffer = new byte[1024 * 1024]; - - while (size > 0) { - var len = (int) Math.min(buffer.length, size); - - file.seek(offset); - file.read(buffer, 0, len); - file.seek(offset - delta); - file.write(buffer, 0, len); - - size -= len; - offset += len; - } - } - - private static File copyFile(File file) throws IOException { - var newFile = FileUtils.createTempFile(file.getName()).toFile(); - try (var in = new FileInputStream(file)) { - try (var out = new FileOutputStream(newFile)) { - in.transferTo(out); - } - } - return newFile; - } - - private static long findEndOfCentralDir(RandomAccessFile file) throws IOException { - var offset = file.length() - 0x15L + 1; - - do { - if (offset <= 0) { - throw new IllegalArgumentException("File is not a valid ZIP: End of central directory record not found"); - } - file.seek(--offset); - } while (Integer.reverseBytes(file.readInt()) != 0x06054b50); - - return offset; - } - - private static class HeaderInfo { - short compression; - long uncompressedSize; - long dataOffset; - long extraLen; - - boolean validCompression() { - return compression == 0x0 || compression == 0x8; - } - } - - private static HeaderInfo readHeaderVexxed(RandomAccessFile file, long offset) { - try { - return readHeader(file, offset); - } catch (IOException e) { - throw new UndeclaredThrowableException(e); - } - } - - private static HeaderInfo readHeader(RandomAccessFile file, long offset) throws IOException { - var info = new HeaderInfo(); - - file.seek(offset); - var signature = Integer.reverseBytes(file.readInt()); - - if (signature != 0x02014b50 && signature != 0x04034b50) { - throw new IllegalArgumentException( - String.format("Invalid ZIP header signature %x at offset %x", - signature, offset)); - } - - var isCentralHeader = signature == 0x02014b50; - var delta = isCentralHeader ? 0 : -2; - - file.seek(offset + 0x0a + delta); - info.compression = Short.reverseBytes(file.readShort()); - - file.seek(offset + 0x18 + delta); - info.uncompressedSize = Integer.toUnsignedLong(Integer.reverseBytes(file.readInt())); - - file.seek(offset + 0x1c + delta); - var nameLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); - info.extraLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); - var commentLen = 0L; - - if (isCentralHeader) { - commentLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort())); - } - - info.dataOffset = (isCentralHeader ? 0x2e : 0x1e) + nameLen + info.extraLen + commentLen; - - return info; - } -} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java index be0240812..7a5d20b54 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java @@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory; import jadx.api.ICodeInfo; import jadx.api.args.ResourceNameSource; -import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.IFieldInfoRef; @@ -392,7 +391,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl private static final ResourceEntry STUB_ENTRY = new ResourceEntry(-1, "stub", "stub", "stub", ""); private ResourceEntry buildResourceEntry(PackageChunk pkg, String config, int resRef, String typeName, String origKeyName) { - if (!ZipSecurity.isValidZipEntryName(origKeyName)) { + if (!root.getArgs().getSecurity().isValidEntryName(origKeyName)) { // malicious entry, ignore it // can't return null here, return stub without adding it to storage return STUB_ENTRY; diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java index 5e0d3b2e1..387121af1 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -8,9 +8,10 @@ import java.nio.file.StandardCopyOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.JadxDecompiler; import jadx.api.ResourceFile; import jadx.api.ResourcesLoader; -import jadx.api.plugins.utils.ZipSecurity; +import jadx.api.security.IJadxSecurity; import jadx.core.dex.visitors.SaveCode; import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -21,10 +22,12 @@ public class ResourcesSaver implements Runnable { private final ResourceFile resourceFile; private final File outDir; + private final IJadxSecurity security; - public ResourcesSaver(File outDir, ResourceFile resourceFile) { + public ResourcesSaver(JadxDecompiler decompiler, File outDir, ResourceFile resourceFile) { this.resourceFile = resourceFile; this.outDir = outDir; + this.security = decompiler.getArgs().getSecurity(); } @Override @@ -52,7 +55,7 @@ public class ResourcesSaver implements Runnable { private void save(ResContainer rc, File outDir) { File outFile = new File(outDir, rc.getFileName()); - if (!ZipSecurity.isInSubDirectory(outDir, outFile)) { + if (!security.isInSubDirectory(outDir, outFile)) { LOG.error("Invalid resource name or path traversal attack detected: {}", outFile.getPath()); return; } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 65b795937..23f453176 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -23,6 +23,7 @@ import jadx.api.impl.InMemoryCodeCache; import jadx.api.metadata.ICodeNodeRef; import jadx.api.usage.impl.EmptyUsageInfoCache; import jadx.api.usage.impl.InMemoryUsageInfoCache; +import jadx.cli.JadxAppCommon; import jadx.cli.plugins.JadxFilesGetter; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.ProcessState; @@ -66,6 +67,7 @@ public class JadxWrapper { JadxArgs jadxArgs = getSettings().toJadxArgs(); jadxArgs.setPluginLoader(new JadxExternalPluginsLoader()); project.fillJadxArgs(jadxArgs); + JadxAppCommon.applyEnvVars(jadxArgs); decompiler = new JadxDecompiler(jadxArgs); guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java index 479afe20c..487d378e7 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/HexArea.java @@ -53,7 +53,7 @@ public class HexArea extends AbstractCodeArea { public void load() { byte[] bytes = null; if (binaryNode instanceof JResource) { - JResource jResource = ((JResource) binaryNode); + JResource jResource = (JResource) binaryNode; try { bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes()); } catch (JadxException e) { diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java index 24ba6f7a9..890c0b842 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java @@ -15,7 +15,6 @@ import org.jetbrains.annotations.Nullable; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import jadx.api.plugins.utils.ZipSecurity; import jadx.core.utils.files.FileUtils; import jadx.plugins.tools.data.JadxPluginListCache; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -24,6 +23,7 @@ import jadx.plugins.tools.resolvers.github.LocationInfo; import jadx.plugins.tools.resolvers.github.data.Asset; import jadx.plugins.tools.resolvers.github.data.Release; import jadx.plugins.tools.utils.PluginUtils; +import jadx.zip.ZipReader; import static jadx.core.utils.GsonUtils.buildGson; import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE; @@ -125,14 +125,15 @@ public class JadxPluginsList { private static List loadListBundle(Path tmpListFile) { Gson gson = buildGson(); List entries = new ArrayList<>(); - ZipSecurity.readZipEntries(tmpListFile.toFile(), (entry, in) -> { + new ZipReader().visitEntries(tmpListFile.toFile(), entry -> { if (entry.getName().endsWith(".json")) { - try (Reader reader = new InputStreamReader(in)) { + try (Reader reader = new InputStreamReader(entry.getInputStream())) { entries.addAll(gson.fromJson(reader, LIST_TYPE)); } catch (Exception e) { throw new RuntimeException("Failed to read plugins list entry: " + entry.getName()); } } + return null; }); return entries; } diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt index b41299769..ef21ba7e7 100644 --- a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomCodeInput.kt @@ -3,24 +3,29 @@ package jadx.plugins.input.apkm 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 jadx.plugins.input.dex.DexInputPlugin +import jadx.zip.ZipReader import java.io.File import java.nio.file.Path class ApkmCustomCodeInput( - private val plugin: ApkmInputPlugin, + private val dexInputPlugin: DexInputPlugin, + private val zipReader: ZipReader, ) : JadxCodeInput { + override fun loadFiles(input: List): ICodeLoader { val apkFiles = mutableListOf() for (file in input.map { it.toFile() }) { + if (!file.name.endsWith(".apkm")) continue + // Check if this is a valid APKM file - val manifest = ApkmUtils.getManifest(file) ?: continue + val manifest = ApkmUtils.getManifest(file, zipReader) ?: continue if (!ApkmUtils.isSupported(manifest)) continue // Load all files ending with .apk - ZipSecurity.visitZipEntries(file) { zip, entry -> + zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { - val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } apkFiles.add(tmpFile) @@ -29,7 +34,7 @@ class ApkmCustomCodeInput( } } - val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) + val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt index 7142ef256..3738dc234 100644 --- a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmCustomResourcesLoader.kt @@ -4,21 +4,25 @@ 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 jadx.zip.ZipReader import java.io.File -class ApkmCustomResourcesLoader : CustomResourcesLoader { +class ApkmCustomResourcesLoader( + private val zipReader: ZipReader, +) : CustomResourcesLoader { private val tmpFiles = mutableListOf() override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { + if (!file.name.endsWith(".apkm")) return false + // Check if this is a valid APKM file - val manifest = ApkmUtils.getManifest(file) ?: return false + val manifest = ApkmUtils.getManifest(file, zipReader) ?: return false if (!ApkmUtils.isSupported(manifest)) return false // Load all files ending with .apk - ZipSecurity.visitZipEntries(file) { zip, entry -> + zipReader.visitEntries(file) { entry -> if (entry.name.endsWith(".apk")) { - val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } loader.defaultLoadFile(list, tmpFile, entry.name + "/") diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt index 90a88410c..bb2d144eb 100644 --- a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmInputPlugin.kt @@ -6,9 +6,6 @@ import jadx.api.plugins.JadxPluginInfo import jadx.plugins.input.dex.DexInputPlugin class ApkmInputPlugin : JadxPlugin { - private val codeInput = ApkmCustomCodeInput(this) - private val resourcesLoader = ApkmCustomResourcesLoader() - internal lateinit var dexInputPlugin: DexInputPlugin override fun getPluginInfo() = JadxPluginInfo( "apkm-input", @@ -17,8 +14,8 @@ class ApkmInputPlugin : JadxPlugin { ) override fun init(context: JadxPluginContext) { - dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) - context.addCodeInput(codeInput) - context.decompiler.addCustomResourcesLoader(resourcesLoader) + val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) + context.addCodeInput(ApkmCustomCodeInput(dexInputPlugin, context.zipReader)) + context.decompiler.addCustomResourcesLoader(ApkmCustomResourcesLoader(context.zipReader)) } } diff --git a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt index 5e30c23e7..026d2ac71 100644 --- a/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt +++ b/jadx-plugins/jadx-apkm-input/src/main/java/jadx/plugins/input/apkm/ApkmUtils.kt @@ -1,19 +1,18 @@ package jadx.plugins.input.apkm -import jadx.api.plugins.utils.ZipSecurity import jadx.core.utils.GsonUtils.buildGson import jadx.core.utils.files.FileUtils -import jadx.core.utils.files.ZipFile +import jadx.zip.ZipReader import java.io.File import java.io.InputStreamReader object ApkmUtils { - fun getManifest(file: File): ApkmManifest? { + fun getManifest(file: File, zipReader: ZipReader): ApkmManifest? { if (!FileUtils.isZipFile(file)) return null try { - ZipFile(file).use { zip -> - val manifestEntry = zip.getEntry("info.json") ?: return null - return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use { + zipReader.open(file).use { zip -> + val manifestEntry = zip.searchEntry("info.json") ?: return null + return InputStreamReader(manifestEntry.inputStream).use { buildGson().fromJson(it, ApkmManifest::class.java) } } 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 a9c13065e..c8ee3b458 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 @@ -18,10 +18,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; -import jadx.api.plugins.utils.ZipSecurity; import jadx.plugins.input.dex.sections.DexConsts; import jadx.plugins.input.dex.sections.DexHeaderV41; import jadx.plugins.input.dex.utils.DexCheckSum; +import jadx.zip.IZipEntry; +import jadx.zip.ZipContent; +import jadx.zip.ZipReader; public class DexFileLoader { private static final Logger LOG = LoggerFactory.getLogger(DexFileLoader.class); @@ -31,10 +33,16 @@ public class DexFileLoader { private final DexInputOptions options; + private ZipReader zipReader = new ZipReader(); + public DexFileLoader(DexInputOptions options) { this.options = options; } + public void setZipReader(ZipReader zipReader) { + this.zipReader = zipReader; + } + public List collectDexFiles(List pathsList) { return pathsList.stream() .map(Path::toFile) @@ -76,6 +84,13 @@ public class DexFileLoader { } } + private List loadFromZipEntry(byte[] content, String fileName) { + if (isStartWithBytes(content, DexConsts.DEX_FILE_MAGIC) || fileName.endsWith(".dex")) { + return loadDexReaders(fileName, content); + } + return Collections.emptyList(); + } + public List loadDexReaders(String fileName, byte[] content) { DexHeaderV41 dexHeaderV41 = DexHeaderV41.readIfPresent(content); if (dexHeaderV41 != null) { @@ -106,14 +121,25 @@ public class DexFileLoader { private List collectDexFromZip(File file) { List result = new ArrayList<>(); - try { - ZipSecurity.readZipEntries(file, (entry, in) -> { + try (ZipContent zip = zipReader.open(file)) { + for (IZipEntry entry : zip.getEntries()) { + if (entry.isDirectory()) { + continue; + } try { - result.addAll(load(null, in, entry.getName())); + List readers; + if (entry.preferBytes()) { + readers = loadFromZipEntry(entry.getBytes(), entry.getName()); + } else { + readers = load(null, entry.getInputStream(), entry.getName()); + } + if (!readers.isEmpty()) { + result.addAll(readers); + } } catch (Exception e) { LOG.error("Failed to read zip entry: {}", entry, e); } - }); + } } catch (Exception e) { LOG.error("Failed to process zip file: {}", file.getAbsolutePath(), e); } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java index e2d40407d..6d8ff79fe 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java @@ -31,6 +31,7 @@ public class DexInputPlugin implements JadxPlugin { public void init(JadxPluginContext context) { context.registerOptions(options); context.addCodeInput(this::loadFiles); + loader.setZipReader(context.getZipReader()); } public ICodeLoader loadFiles(List input) { diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java index e37350e98..d26212edd 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertLoader.java @@ -17,16 +17,22 @@ import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.utils.CommonFileUtils; -import jadx.api.plugins.utils.ZipSecurity; +import jadx.api.security.IJadxSecurity; +import jadx.zip.ZipReader; public class JavaConvertLoader { private static final Logger LOG = LoggerFactory.getLogger(JavaConvertLoader.class); private final JavaConvertOptions options; + private final ZipReader zipReader; + private final IJadxSecurity security; - public JavaConvertLoader(JavaConvertOptions options) { + public JavaConvertLoader(JavaConvertOptions options, JadxPluginContext context) { this.options = options; + this.zipReader = context.getZipReader(); + this.security = context.getArgs().getSecurity(); } public ConvertResult process(List input) { @@ -64,9 +70,13 @@ public class JavaConvertLoader { try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { for (Path file : clsFiles) { String clsName = AsmUtils.getNameFromClassFile(file); - if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) { + if (clsName == null) { throw new IOException("Can't read class name from file: " + file); } + if (!security.isValidEntryName(clsName)) { + LOG.warn("Skip class with invalid name: {}", clsName); + continue; + } addFileToJar(jo, file, clsName + ".class"); } } @@ -82,7 +92,7 @@ public class JavaConvertLoader { PathMatcher aarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.aar"); input.stream() .filter(aarMatcher::matches) - .forEach(path -> ZipSecurity.readZipEntries(path.toFile(), (entry, in) -> { + .forEach(path -> zipReader.readEntries(path.toFile(), (entry, in) -> { try { String entryName = entry.getName(); if (entryName.endsWith(".jar")) { @@ -105,8 +115,8 @@ public class JavaConvertLoader { } private boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception { - // check if jar need a full repackage - Boolean repackNeeded = ZipSecurity.visitZipEntries(path.toFile(), (zipFile, zipEntry) -> { + // check if jar needs a full repackaging + Boolean repackNeeded = zipReader.visitEntries(path.toFile(), zipEntry -> { String entryName = zipEntry.getName(); if (zipEntry.isDirectory()) { if (entryName.equals("BOOT-INF/")) { @@ -131,7 +141,7 @@ public class JavaConvertLoader { Path jarFile = Files.createTempFile("jadx-classes-", ".jar"); result.addTempPath(jarFile); try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) { - ZipSecurity.readZipEntries(path.toFile(), (entry, in) -> { + zipReader.readEntries(path.toFile(), (entry, in) -> { try { String entryName = entry.getName(); if (entryName.endsWith(".class")) { @@ -142,10 +152,14 @@ public class JavaConvertLoader { } byte[] clsFileContent = CommonFileUtils.loadBytes(in); String clsName = AsmUtils.getNameFromClassFile(clsFileContent); - if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) { + if (clsName == null) { throw new IOException("Can't read class name from file: " + entryName); } - addJarEntry(jo, clsName + ".class", clsFileContent, entry.getLastModifiedTime()); + if (!security.isValidEntryName(clsName)) { + LOG.warn("Ignore class with invalid name: {} from {}", clsName, entry); + } else { + addJarEntry(jo, clsName + ".class", clsFileContent, null); + } } else if (entryName.endsWith(".jar")) { Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar"); result.addTempPath(tempJar); diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 56596e1df..7dde155a6 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -17,9 +17,9 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput { public static final String PLUGIN_ID = "java-convert"; private final JavaConvertOptions options = new JavaConvertOptions(); - private final JavaConvertLoader loader = new JavaConvertLoader(options); private JadxPluginRuntimeData dexInput; + private JavaConvertLoader loader; @Override public JadxPluginInfo getPluginInfo() { @@ -32,8 +32,9 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput { @Override public void init(JadxPluginContext context) { - dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID); context.registerOptions(options); + dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID); + loader = new JavaConvertLoader(options, context); context.addCodeInput(this); } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java index 1d0980ae0..e7e4ec269 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputLoader.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -17,7 +18,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.utils.CommonFileUtils; -import jadx.api.plugins.utils.ZipSecurity; +import jadx.core.plugins.files.TempFilesGetter; +import jadx.core.utils.files.FileUtils; +import jadx.zip.IZipEntry; +import jadx.zip.ZipContent; +import jadx.zip.ZipReader; public class JavaInputLoader { private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class); @@ -26,8 +31,24 @@ public class JavaInputLoader { private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE }; private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 }; + private final ZipReader zipReader; + private final Path tempPath; + private int classUniqId = 1; + public JavaInputLoader(ZipReader zipReader, Path tempPath) { + this.zipReader = zipReader; + this.tempPath = tempPath; + } + + /** + * This will use zip reader with default options and ignore provided in jadx args + */ + @Deprecated + public JavaInputLoader() { + this(new ZipReader(), TempFilesGetter.INSTANCE.getTempDir()); + } + public List collectFiles(List inputFiles) { return inputFiles.stream() .map(Path::toFile) @@ -78,6 +99,23 @@ public class JavaInputLoader { return Collections.emptyList(); } + private List loadReaderFromZipEntry(byte[] content, String name, String parentFileName) throws IOException { + if (isStartWithBytes(content, JAVA_CLASS_FILE_MAGIC) || name.endsWith(".class")) { + String source = concatSource(parentFileName, name); + JavaClassReader reader = new JavaClassReader(getNextUniqId(), source, content); + return Collections.singletonList(reader); + } + if (isStartWithBytes(content, ZIP_FILE_MAGIC) || CommonFileUtils.isZipFileExt(name)) { + Path tempZip = Files.createTempFile(tempPath, "temp", ".zip"); + FileUtils.writeFile(tempZip, content); + File zipFile = tempZip.toFile(); + List readers = collectFromZip(zipFile, concatSource(parentFileName, name)); + CommonFileUtils.safeDeleteFile(zipFile); + return readers; + } + return Collections.emptyList(); + } + private static String concatSource(@Nullable String parentFileName, String name) { if (parentFileName == null) { return name; @@ -87,19 +125,28 @@ public class JavaInputLoader { private List collectFromZip(File file, String name) { List result = new ArrayList<>(); - try { - ZipSecurity.readZipEntries(file, (entry, in) -> { + try (ZipContent zip = zipReader.open(file)) { + for (IZipEntry entry : zip.getEntries()) { + if (entry.isDirectory()) { + continue; + } + String entryName = entry.getName(); + if (entryName.startsWith("META-INF/versions/")) { + // skip classes for different java versions + continue; + } try { - String entryName = entry.getName(); - if (entryName.startsWith("META-INF/versions/")) { - // skip classes for different java versions - return; + List readers; + if (entry.preferBytes()) { + readers = loadReaderFromZipEntry(entry.getBytes(), entryName, name); + } else { + readers = loadReader(entry.getInputStream(), entryName, null, name); } - result.addAll(loadReader(in, entryName, null, name)); + result.addAll(readers); } catch (Exception e) { LOG.error("Failed to read zip entry: {}", entry, e); } - }); + } } catch (Exception e) { LOG.error("Failed to process zip file: {}", name, e); } diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java index 1b2999aa1..f3cd609ca 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java @@ -25,7 +25,14 @@ public class JavaInputPlugin implements JadxPlugin { @Override public void init(JadxPluginContext context) { - context.addCodeInput(JavaInputPlugin::loadClassFiles); + context.addCodeInput(inputFiles -> { + JavaInputLoader loader = new JavaInputLoader(context.getZipReader(), context.files().getPluginTempDir()); + List readers = loader.collectFiles(inputFiles); + if (readers.isEmpty()) { + return EmptyCodeLoader.INSTANCE; + } + return new JavaLoadResult(readers, null); + }); } public static ICodeLoader loadClassFiles(List inputFiles) { 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 index 89db1df15..72a6126ba 100644 --- 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 @@ -3,25 +3,29 @@ 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 jadx.core.utils.files.ZipFile +import jadx.plugins.input.dex.DexInputPlugin +import jadx.zip.ZipReader import java.io.File import java.nio.file.Path class XapkCustomCodeInput( - private val plugin: XapkInputPlugin, + private val dexInputPlugin: DexInputPlugin, + private val zipReader: ZipReader, ) : JadxCodeInput { + override fun loadFiles(input: List): ICodeLoader { val apkFiles = mutableListOf() for (file in input.map { it.toFile() }) { - val manifest = XapkUtils.getManifest(file) ?: continue + if (!file.name.endsWith(".xapk")) continue + + val manifest = XapkUtils.getManifest(file, zipReader) ?: continue if (!XapkUtils.isSupported(manifest)) continue - ZipFile(file).use { zip -> + zipReader.open(file).use { zip -> for (splitApk in manifest.splitApks) { - val splitApkEntry = zip.getEntry(splitApk.file) + val splitApkEntry = zip.searchEntry(splitApk.file) if (splitApkEntry != null) { - val tmpFile = ZipSecurity.getInputStreamForEntry(zip, splitApkEntry).use { + val tmpFile = splitApkEntry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } apkFiles.add(tmpFile) @@ -30,7 +34,7 @@ class XapkCustomCodeInput( } } - val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) + val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() }) apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) } 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 index 7cdbfa570..671a882db 100644 --- 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 @@ -4,20 +4,22 @@ 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 jadx.zip.ZipReader import java.io.File -class XapkCustomResourcesLoader : CustomResourcesLoader { +class XapkCustomResourcesLoader(private val zipReader: ZipReader) : CustomResourcesLoader { private val tmpFiles = mutableListOf() override fun load(loader: ResourcesLoader, list: MutableList, file: File): Boolean { - val manifest = XapkUtils.getManifest(file) ?: return false + if (!file.name.endsWith(".xapk")) return false + + val manifest = XapkUtils.getManifest(file, zipReader) ?: return false if (!XapkUtils.isSupported(manifest)) return false val apkEntries = manifest.splitApks.map { it.file }.toHashSet() - ZipSecurity.visitZipEntries(file) { zip, entry -> + zipReader.visitEntries(file) { entry -> if (apkEntries.contains(entry.name)) { - val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use { + val tmpFile = entry.inputStream.use { CommonFileUtils.saveToTempFile(it, ".apk").toFile() } loader.defaultLoadFile(list, tmpFile, entry.name + "/") 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 index f4cf213b1..40465a2b1 100644 --- 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 @@ -3,22 +3,20 @@ package jadx.plugins.input.xapk import jadx.api.plugins.JadxPlugin import jadx.api.plugins.JadxPluginContext import jadx.api.plugins.JadxPluginInfo +import jadx.api.plugins.JadxPluginInfoBuilder import jadx.plugins.input.dex.DexInputPlugin class XapkInputPlugin : JadxPlugin { - private val codeInput = XapkCustomCodeInput(this) - private val resourcesLoader = XapkCustomResourcesLoader() - internal lateinit var dexInputPlugin: DexInputPlugin - override fun getPluginInfo() = JadxPluginInfo( - "xapk-input", - "XAPK Input", - "Load .xapk files", - ) + override fun getPluginInfo(): JadxPluginInfo = + JadxPluginInfoBuilder.pluginId("xapk-input") + .name("XAPK Input") + .description("Load .xapk files") + .build() override fun init(context: JadxPluginContext) { - dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) - context.addCodeInput(codeInput) - context.decompiler.addCustomResourcesLoader(resourcesLoader) + val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java) + context.addCodeInput(XapkCustomCodeInput(dexInputPlugin, context.zipReader)) + context.decompiler.addCustomResourcesLoader(XapkCustomResourcesLoader(context.zipReader)) } } 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 index 12c877d14..e33303dbc 100644 --- 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 @@ -1,19 +1,18 @@ package jadx.plugins.input.xapk -import jadx.api.plugins.utils.ZipSecurity import jadx.core.utils.GsonUtils.buildGson import jadx.core.utils.files.FileUtils -import jadx.core.utils.files.ZipFile +import jadx.zip.ZipReader import java.io.File import java.io.InputStreamReader object XapkUtils { - fun getManifest(file: File): XapkManifest? { + fun getManifest(file: File, zipReader: ZipReader): 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 { + zipReader.open(file).use { zip -> + val manifestEntry = zip.searchEntry("manifest.json") ?: return null + return InputStreamReader(manifestEntry.inputStream).use { buildGson().fromJson(it, XapkManifest::class.java) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index a797dd99f..476c004c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ include("jadx-gui") include("jadx-plugins-tools") include("jadx-commons:jadx-app-commons") +include("jadx-commons:jadx-zip") include("jadx-plugins:jadx-input-api") include("jadx-plugins:jadx-dex-input")